mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/yunionio/cloudpods.git
synced 2026-09-20 08:03:53 +08:00
625 lines
16 KiB
Go
625 lines
16 KiB
Go
// Copyright 2019 Yunion
|
||
//
|
||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||
// you may not use this file except in compliance with the License.
|
||
// You may obtain a copy of the License at
|
||
//
|
||
// http://www.apache.org/licenses/LICENSE-2.0
|
||
//
|
||
// Unless required by applicable law or agreed to in writing, software
|
||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
// See the License for the specific language governing permissions and
|
||
// limitations under the License.
|
||
|
||
package models
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"strings"
|
||
)
|
||
|
||
// progressLabelCache 缓存 list 结果中的 id→可读名称,供后续进度文案使用。
|
||
type progressLabelCache struct {
|
||
regions map[string]string
|
||
}
|
||
|
||
func newProgressLabelCache() *progressLabelCache {
|
||
return &progressLabelCache{regions: make(map[string]string)}
|
||
}
|
||
|
||
func (c *progressLabelCache) rememberFromTool(toolName, resultText string) {
|
||
if c == nil || isToolResultError(resultText) {
|
||
return
|
||
}
|
||
name := strings.TrimPrefix(toolName, "climc_")
|
||
if strings.Contains(name, "cloud_region_list") {
|
||
c.rememberRegions(resultText)
|
||
}
|
||
}
|
||
|
||
func (c *progressLabelCache) rememberRegions(resultText string) {
|
||
if c.regions == nil {
|
||
c.regions = make(map[string]string)
|
||
}
|
||
items, _ := extractListItems(resultText)
|
||
for _, item := range items {
|
||
id := jsonString(item, "id")
|
||
name := jsonString(item, "name")
|
||
if id == "" || name == "" {
|
||
continue
|
||
}
|
||
ext := jsonString(item, "external_id")
|
||
label := name
|
||
if ext != "" && !strings.EqualFold(ext, name) {
|
||
label = fmt.Sprintf("%s(%s)", name, ext)
|
||
}
|
||
c.regions[id] = label
|
||
c.regions[name] = label
|
||
if ext != "" {
|
||
c.regions[ext] = label
|
||
}
|
||
}
|
||
}
|
||
|
||
func (c *progressLabelCache) regionLabel(idOrName string) string {
|
||
if idOrName == "" {
|
||
return ""
|
||
}
|
||
if c != nil && c.regions != nil {
|
||
if v := c.regions[idOrName]; v != "" {
|
||
return v
|
||
}
|
||
}
|
||
return idOrName
|
||
}
|
||
|
||
// summarizeToolProgress 将工具调用结果整理成面向用户的进度文案(逐项展示资源,而非工具名)。
|
||
func summarizeToolProgress(toolName string, args map[string]interface{}, resultText string, labels *progressLabelCache) string {
|
||
name := strings.TrimPrefix(toolName, "climc_")
|
||
if isToolResultError(resultText) {
|
||
return fmt.Sprintf("✗ %s失败:%s\n", progressLabel(name), truncateRunes(stripMCPHint(resultText), 180))
|
||
}
|
||
|
||
switch {
|
||
case name == "docs_search" || strings.HasSuffix(name, "docs_search"):
|
||
return formatResourceListProgress("文档", resultText, []string{"name", "title", "path"})
|
||
|
||
case name == "docs_get" || strings.HasSuffix(name, "docs_get"):
|
||
path := firstArg(args, "path", "PATH")
|
||
if path != "" {
|
||
return fmt.Sprintf("✓ 已阅读文档:%s\n", path)
|
||
}
|
||
return "✓ 已阅读文档\n"
|
||
|
||
case strings.Contains(name, "cloud_region_capability"):
|
||
id := firstArg(args, "id", "ID", "name")
|
||
region := labels.regionLabel(id)
|
||
types := extractStorageTypeHints(resultText)
|
||
if region != "" && types != "" {
|
||
return fmt.Sprintf("✓ 区域 %s 可用磁盘类型:%s\n", region, types)
|
||
}
|
||
if types != "" {
|
||
return fmt.Sprintf("✓ 可用磁盘类型:%s\n", types)
|
||
}
|
||
return fmt.Sprintf("✓ 已查询区域能力%s\n", paren(region))
|
||
|
||
case strings.Contains(name, "cloud_region_list"):
|
||
return formatResourceListProgress("区域", resultText, []string{"name", "external_id", "id"})
|
||
|
||
case strings.Contains(name, "cached_image_list"), strings.Contains(name, "image_list"):
|
||
return formatResourceListProgress("镜像", resultText, []string{"name", "os_type", "os_distribution", "id"})
|
||
|
||
case strings.Contains(name, "server_sku_list"):
|
||
return formatResourceListProgress("套餐", resultText, []string{"name", "instance_type_category", "cpu_core_count", "memory_size_mb", "id"})
|
||
|
||
case strings.Contains(name, "network_list"):
|
||
return formatResourceListProgress("网络", resultText, []string{"name", "guest_ip_prefix", "vpc", "id"})
|
||
|
||
case strings.Contains(name, "vpc_list"):
|
||
return formatResourceListProgress("VPC", resultText, []string{"name", "cidr_block", "id"})
|
||
|
||
case strings.Contains(name, "storage_list"):
|
||
return formatResourceListProgress("存储", resultText, []string{"name", "storage_type", "capacity", "id"})
|
||
|
||
case strings.Contains(name, "server_list"):
|
||
return formatResourceListProgress("虚拟机", resultText, []string{"name", "status", "id"})
|
||
|
||
case strings.Contains(name, "server_create"):
|
||
return formatServerCreateProgress(args, resultText, labels)
|
||
|
||
case strings.Contains(name, "server_show"):
|
||
return formatSingleResourceProgress("虚拟机详情", resultText, []string{"name", "status", "id"})
|
||
|
||
case strings.HasPrefix(name, "server_"):
|
||
id := firstArg(args, "id", "ID", "name")
|
||
action := strings.TrimPrefix(name, "server_")
|
||
return fmt.Sprintf("✓ 虚拟机%s%s\n", actionLabel(action), paren(id))
|
||
|
||
default:
|
||
return formatGenericProgress(name, args, resultText)
|
||
}
|
||
}
|
||
|
||
func isToolResultError(resultText string) bool {
|
||
s := strings.TrimSpace(resultText)
|
||
return strings.Contains(s, "调用失败") ||
|
||
strings.Contains(s, "返回错误") ||
|
||
strings.HasPrefix(s, "工具 ") && strings.Contains(s, "失败")
|
||
}
|
||
|
||
func progressLabel(toolName string) string {
|
||
switch {
|
||
case strings.Contains(toolName, "cloud_region_list"):
|
||
return "查询区域"
|
||
case strings.Contains(toolName, "cloud_region_capability"):
|
||
return "查询区域能力"
|
||
case strings.Contains(toolName, "cached_image"):
|
||
return "查询镜像"
|
||
case strings.Contains(toolName, "image_list"):
|
||
return "查询镜像"
|
||
case strings.Contains(toolName, "server_sku"):
|
||
return "查询套餐"
|
||
case strings.Contains(toolName, "network_list"):
|
||
return "查询网络"
|
||
case strings.Contains(toolName, "vpc_list"):
|
||
return "查询 VPC"
|
||
case strings.Contains(toolName, "server_create"):
|
||
return "创建虚拟机"
|
||
default:
|
||
return toolName
|
||
}
|
||
}
|
||
|
||
func actionLabel(action string) string {
|
||
switch action {
|
||
case "start":
|
||
return "已启动"
|
||
case "stop":
|
||
return "已停止"
|
||
case "restart":
|
||
return "已重启"
|
||
case "delete":
|
||
return "已删除"
|
||
case "set_password", "set-password":
|
||
return "已重置密码"
|
||
default:
|
||
return "操作完成"
|
||
}
|
||
}
|
||
|
||
func formatServerCreateProgress(args map[string]interface{}, resultText string, labels *progressLabelCache) string {
|
||
var b strings.Builder
|
||
b.WriteString("✓ 选用资源创建虚拟机:")
|
||
parts := make([]string, 0, 6)
|
||
if v := firstArg(args, "name", "NAME"); v != "" {
|
||
if isTruthy(args["generate-name"]) || isTruthy(args["generate_name"]) || isTruthy(args["GenerateName"]) {
|
||
parts = append(parts, "名称模板="+v+"(自动去重)")
|
||
} else {
|
||
parts = append(parts, "名称="+v)
|
||
}
|
||
}
|
||
if v := firstArg(args, "hypervisor"); v != "" {
|
||
parts = append(parts, "平台="+v)
|
||
}
|
||
if v := firstArg(args, "prefer-region", "prefer_region", "region"); v != "" {
|
||
parts = append(parts, "区域="+labels.regionLabel(v))
|
||
}
|
||
if v := firstArg(args, "instance-type", "instance_type", "sku"); v != "" {
|
||
parts = append(parts, "规格="+v)
|
||
}
|
||
if v := firstArg(args, "ncpu"); v != "" {
|
||
parts = append(parts, "CPU="+v)
|
||
}
|
||
if v := firstArg(args, "mem-spec", "mem_spec"); v != "" {
|
||
parts = append(parts, "内存="+v)
|
||
}
|
||
if disks := argStringSlice(args, "disk"); len(disks) > 0 {
|
||
parts = append(parts, "磁盘="+truncateRunes(disks[0], 80))
|
||
}
|
||
if nets := argStringSlice(args, "net"); len(nets) > 0 {
|
||
parts = append(parts, "网络="+strings.Join(nets, ","))
|
||
} else {
|
||
parts = append(parts, "网络=自动调度")
|
||
}
|
||
b.WriteString(strings.Join(parts, ","))
|
||
b.WriteByte('\n')
|
||
|
||
body := stripMCPHint(resultText)
|
||
if obj := parseJSONObject(body); obj != nil {
|
||
status := jsonString(obj, "final_status")
|
||
sid := jsonString(obj, "server_id")
|
||
sname := ""
|
||
if sid == "" {
|
||
if srv, ok := obj["server"].(map[string]interface{}); ok {
|
||
sid = jsonString(srv, "id")
|
||
sname = jsonString(srv, "name")
|
||
if status == "" {
|
||
status = jsonString(srv, "status")
|
||
}
|
||
}
|
||
} else if srv, ok := obj["server"].(map[string]interface{}); ok {
|
||
sname = jsonString(srv, "name")
|
||
}
|
||
if waitErr := jsonString(obj, "wait_error"); waitErr != "" {
|
||
b.WriteString(fmt.Sprintf(" 创建未完成:%s\n", truncateRunes(waitErr, 160)))
|
||
if hint := jsonString(obj, "hint"); hint != "" {
|
||
b.WriteString(fmt.Sprintf(" 提示:%s\n", truncateRunes(hint, 160)))
|
||
}
|
||
return b.String()
|
||
}
|
||
if sid != "" || status != "" || sname != "" {
|
||
b.WriteString(" 结果:")
|
||
bits := make([]string, 0, 3)
|
||
if sname != "" {
|
||
bits = append(bits, "名称="+sname)
|
||
}
|
||
if sid != "" {
|
||
bits = append(bits, "id="+sid)
|
||
}
|
||
if status != "" {
|
||
bits = append(bits, "状态="+status)
|
||
}
|
||
b.WriteString(strings.Join(bits, ","))
|
||
b.WriteByte('\n')
|
||
}
|
||
}
|
||
return b.String()
|
||
}
|
||
|
||
func isTruthy(v interface{}) bool {
|
||
switch x := v.(type) {
|
||
case bool:
|
||
return x
|
||
case string:
|
||
s := strings.ToLower(strings.TrimSpace(x))
|
||
return s == "true" || s == "1" || s == "yes" || s == "on"
|
||
case float64:
|
||
return x != 0
|
||
case int:
|
||
return x != 0
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
func formatResourceListProgress(kind, resultText string, fields []string) string {
|
||
items, total := extractListItems(resultText)
|
||
if len(items) == 0 {
|
||
if total == 0 {
|
||
return fmt.Sprintf("✓ 未找到可用%s\n", kind)
|
||
}
|
||
return fmt.Sprintf("✓ 已查询%s(共 %d 条)\n", kind, total)
|
||
}
|
||
if total <= 0 {
|
||
total = len(items)
|
||
}
|
||
labels := make([]string, 0, 5)
|
||
for i, item := range items {
|
||
if i >= 5 {
|
||
break
|
||
}
|
||
labels = append(labels, formatItemLabel(item, fields))
|
||
}
|
||
more := ""
|
||
if total > len(labels) {
|
||
more = fmt.Sprintf("等共 %d 个", total)
|
||
} else {
|
||
more = fmt.Sprintf("共 %d 个", total)
|
||
}
|
||
return fmt.Sprintf("✓ 已找到%s:%s(%s)\n", kind, strings.Join(labels, "、"), more)
|
||
}
|
||
|
||
func formatSingleResourceProgress(kind, resultText string, fields []string) string {
|
||
obj := parseJSONObject(stripMCPHint(resultText))
|
||
if obj == nil {
|
||
return fmt.Sprintf("✓ 已获取%s\n", kind)
|
||
}
|
||
return fmt.Sprintf("✓ %s:%s\n", kind, formatItemLabel(obj, fields))
|
||
}
|
||
|
||
func formatGenericProgress(toolName string, args map[string]interface{}, resultText string) string {
|
||
id := firstArg(args, "id", "ID", "name", "NAME")
|
||
items, total := extractListItems(resultText)
|
||
if len(items) > 0 {
|
||
return formatResourceListProgress(progressLabel(toolName), resultText, []string{"name", "id"})
|
||
}
|
||
if id != "" {
|
||
return fmt.Sprintf("✓ %s完成%s\n", progressLabel(toolName), paren(id))
|
||
}
|
||
_ = resultText
|
||
if total > 0 {
|
||
return fmt.Sprintf("✓ %s完成(%d 条)\n", progressLabel(toolName), total)
|
||
}
|
||
return fmt.Sprintf("✓ %s完成\n", progressLabel(toolName))
|
||
}
|
||
|
||
func formatItemLabel(item map[string]interface{}, fields []string) string {
|
||
parts := make([]string, 0, 3)
|
||
seen := map[string]bool{}
|
||
for _, f := range fields {
|
||
v := jsonString(item, f)
|
||
if v == "" || seen[v] {
|
||
continue
|
||
}
|
||
seen[v] = true
|
||
parts = append(parts, v)
|
||
if len(parts) >= 2 {
|
||
break
|
||
}
|
||
}
|
||
if len(parts) == 0 {
|
||
return "(未命名)"
|
||
}
|
||
if len(parts) == 1 {
|
||
return parts[0]
|
||
}
|
||
return fmt.Sprintf("%s(%s)", parts[0], parts[1])
|
||
}
|
||
|
||
func extractListItems(resultText string) ([]map[string]interface{}, int) {
|
||
body := stripMCPHint(resultText)
|
||
obj := parseJSONObject(body)
|
||
if obj != nil {
|
||
total := jsonInt(obj, "total")
|
||
if total <= 0 {
|
||
total = jsonInt(obj, "count")
|
||
}
|
||
for _, key := range []string{"data", "hits"} {
|
||
data, ok := obj[key].([]interface{})
|
||
if !ok {
|
||
continue
|
||
}
|
||
items := make([]map[string]interface{}, 0, len(data))
|
||
for _, d := range data {
|
||
if m, ok := d.(map[string]interface{}); ok {
|
||
items = append(items, m)
|
||
}
|
||
}
|
||
if total <= 0 {
|
||
total = len(items)
|
||
}
|
||
return items, total
|
||
}
|
||
// 单对象结果
|
||
if jsonString(obj, "id") != "" || jsonString(obj, "name") != "" {
|
||
return []map[string]interface{}{obj}, 1
|
||
}
|
||
}
|
||
if arr := parseJSONArray(body); len(arr) > 0 {
|
||
items := make([]map[string]interface{}, 0, len(arr))
|
||
for _, d := range arr {
|
||
if m, ok := d.(map[string]interface{}); ok {
|
||
items = append(items, m)
|
||
}
|
||
}
|
||
return items, len(items)
|
||
}
|
||
return nil, 0
|
||
}
|
||
|
||
func extractStorageTypeHints(resultText string) string {
|
||
obj := parseJSONObject(stripMCPHint(resultText))
|
||
if obj == nil {
|
||
return ""
|
||
}
|
||
for _, key := range []string{"storage_types2", "StorageTypes2"} {
|
||
raw, ok := obj[key]
|
||
if !ok {
|
||
continue
|
||
}
|
||
m, ok := raw.(map[string]interface{})
|
||
if !ok {
|
||
continue
|
||
}
|
||
set := make([]string, 0, 8)
|
||
seen := map[string]bool{}
|
||
for _, v := range m {
|
||
arr, ok := v.([]interface{})
|
||
if !ok {
|
||
continue
|
||
}
|
||
for _, x := range arr {
|
||
s, ok := x.(string)
|
||
if !ok || s == "" || seen[s] {
|
||
continue
|
||
}
|
||
seen[s] = true
|
||
if i := strings.Index(s, "/"); i > 0 {
|
||
s = s[:i]
|
||
}
|
||
set = append(set, s)
|
||
if len(set) >= 6 {
|
||
return strings.Join(set, "、")
|
||
}
|
||
}
|
||
}
|
||
if len(set) > 0 {
|
||
return strings.Join(set, "、")
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func stripMCPHint(s string) string {
|
||
s = strings.TrimSpace(s)
|
||
if i := strings.Index(s, "[MCP下一步]"); i >= 0 {
|
||
s = strings.TrimSpace(s[:i])
|
||
}
|
||
return s
|
||
}
|
||
|
||
func parseJSONObject(s string) map[string]interface{} {
|
||
s = strings.TrimSpace(s)
|
||
if s == "" || s[0] != '{' {
|
||
// 可能前后有非 JSON 文本,尝试截取第一个对象
|
||
start := strings.Index(s, "{")
|
||
end := strings.LastIndex(s, "}")
|
||
if start < 0 || end <= start {
|
||
return nil
|
||
}
|
||
s = s[start : end+1]
|
||
}
|
||
var obj map[string]interface{}
|
||
if err := json.Unmarshal([]byte(s), &obj); err != nil {
|
||
return nil
|
||
}
|
||
return obj
|
||
}
|
||
|
||
func parseJSONArray(s string) []interface{} {
|
||
s = strings.TrimSpace(s)
|
||
if s == "" {
|
||
return nil
|
||
}
|
||
if s[0] != '[' {
|
||
start := strings.Index(s, "[")
|
||
end := strings.LastIndex(s, "]")
|
||
if start < 0 || end <= start {
|
||
return nil
|
||
}
|
||
s = s[start : end+1]
|
||
}
|
||
var arr []interface{}
|
||
if err := json.Unmarshal([]byte(s), &arr); err != nil {
|
||
return nil
|
||
}
|
||
return arr
|
||
}
|
||
|
||
func firstArg(args map[string]interface{}, keys ...string) string {
|
||
if args == nil {
|
||
return ""
|
||
}
|
||
normalize := func(k string) string {
|
||
return strings.ReplaceAll(strings.ToLower(k), "_", "-")
|
||
}
|
||
for _, key := range keys {
|
||
if v, ok := args[key]; ok {
|
||
if s := stringifyArg(v); s != "" {
|
||
return s
|
||
}
|
||
}
|
||
want := normalize(key)
|
||
for k, v := range args {
|
||
if normalize(k) == want {
|
||
if s := stringifyArg(v); s != "" {
|
||
return s
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func argStringSlice(args map[string]interface{}, key string) []string {
|
||
if args == nil {
|
||
return nil
|
||
}
|
||
v, ok := args[key]
|
||
if !ok {
|
||
alt := strings.ReplaceAll(key, "-", "_")
|
||
v, ok = args[alt]
|
||
if !ok {
|
||
return nil
|
||
}
|
||
}
|
||
switch x := v.(type) {
|
||
case []string:
|
||
return x
|
||
case []interface{}:
|
||
out := make([]string, 0, len(x))
|
||
for _, item := range x {
|
||
if s := stringifyArg(item); s != "" {
|
||
out = append(out, s)
|
||
}
|
||
}
|
||
return out
|
||
case string:
|
||
if x != "" {
|
||
return []string{x}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func stringifyArg(v interface{}) string {
|
||
switch x := v.(type) {
|
||
case string:
|
||
return strings.TrimSpace(x)
|
||
case float64:
|
||
if x == float64(int64(x)) {
|
||
return fmt.Sprintf("%d", int64(x))
|
||
}
|
||
return fmt.Sprintf("%v", x)
|
||
case int:
|
||
return fmt.Sprintf("%d", x)
|
||
case int64:
|
||
return fmt.Sprintf("%d", x)
|
||
case bool:
|
||
return fmt.Sprintf("%v", x)
|
||
case []interface{}:
|
||
if len(x) == 0 {
|
||
return ""
|
||
}
|
||
return stringifyArg(x[0])
|
||
case []string:
|
||
if len(x) == 0 {
|
||
return ""
|
||
}
|
||
return x[0]
|
||
default:
|
||
return strings.TrimSpace(fmt.Sprint(x))
|
||
}
|
||
}
|
||
|
||
func jsonString(m map[string]interface{}, key string) string {
|
||
if m == nil {
|
||
return ""
|
||
}
|
||
v, ok := m[key]
|
||
if !ok {
|
||
return ""
|
||
}
|
||
return stringifyArg(v)
|
||
}
|
||
|
||
func jsonInt(m map[string]interface{}, key string) int {
|
||
if m == nil {
|
||
return 0
|
||
}
|
||
v, ok := m[key]
|
||
if !ok {
|
||
return 0
|
||
}
|
||
switch x := v.(type) {
|
||
case float64:
|
||
return int(x)
|
||
case int:
|
||
return x
|
||
case int64:
|
||
return int(x)
|
||
case json.Number:
|
||
n, _ := x.Int64()
|
||
return int(n)
|
||
default:
|
||
return 0
|
||
}
|
||
}
|
||
|
||
func paren(s string) string {
|
||
if s == "" {
|
||
return ""
|
||
}
|
||
return "(" + s + ")"
|
||
}
|
||
|
||
func truncateRunes(s string, max int) string {
|
||
rs := []rune(strings.TrimSpace(s))
|
||
if max <= 0 || len(rs) <= max {
|
||
return string(rs)
|
||
}
|
||
return string(rs[:max]) + "…"
|
||
}
|