list_source.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. package helper
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "os"
  6. "path/filepath"
  7. "strings"
  8. "github.com/mark3labs/mcp-go/mcp"
  9. )
  10. // StringListSource 表示一批字符串(如 SN / GICCID 列表)的来源解析结果,
  11. // 用于统一"数组入参"与"文件入参"两种传参方式。
  12. type StringListSource struct {
  13. Items []string // 解析出的字符串列表
  14. FromFile bool // 是否来自文件入参
  15. FilePath string // 当 FromFile 为 true 时的文件绝对路径
  16. }
  17. // ResolveStringListOrFile 统一解析"数组或文件"两种批量入参,优先级与安全规则:
  18. //
  19. // - 若提供了 fileKey(文件入参):优先使用文件,忽略数组入参。
  20. // 文件路径必须通过 ValidateSourcePath 校验(仅允许 AgentBaseDir 下),
  21. // 否则返回错误结果。文件内容按扩展名解析:
  22. // - .json → JSON 字符串数组,如 ["A","B"]
  23. // - 其它(.ndjson/.txt/无扩展名)→ 每行一个值(忽略空行、去除首尾空白)
  24. // - 否则若提供了 arrayKey(结构化数组 schema,WithArray+WithStringItems):使用数组。
  25. // - 二者都未提供:返回 (nil-source, errResult),由调用方作为参数错误返回。
  26. //
  27. // 返回 (source, errResult):errResult 非 nil 时表示解析失败,调用方应直接 return 该结果。
  28. func ResolveStringListOrFile(req mcp.CallToolRequest, arrayKey, fileKey string) (*StringListSource, *mcp.CallToolResult) {
  29. // 1) 文件入参优先
  30. if fileKey != "" {
  31. if raw := strings.TrimSpace(req.GetString(fileKey, "")); raw != "" {
  32. abs, err := ValidateSourcePath(raw)
  33. if err != nil {
  34. return nil, ParamInvalidError(fileKey, err.Error())
  35. }
  36. items, perr := parseStringListFile(abs)
  37. if perr != nil {
  38. return nil, ParamInvalidError(fileKey, perr.Error())
  39. }
  40. return &StringListSource{Items: items, FromFile: true, FilePath: abs}, nil
  41. }
  42. }
  43. // 2) 数组入参:兼容两种历史风格
  44. // a) WithArray 结构化数组(WithStringItems)→ GetStringSlice
  45. // b) JSON 字符串数组(WithString + 手动 unmarshal)→ req.GetString 后解析
  46. if arrayKey != "" {
  47. if items, ok := GetStringSlice(req, arrayKey); ok {
  48. return &StringListSource{Items: items, FromFile: false}, nil
  49. }
  50. if s := strings.TrimSpace(req.GetString(arrayKey, "")); s != "" {
  51. if items, ok := tryParseJSONStringArray([]byte(s)); ok {
  52. return &StringListSource{Items: items, FromFile: false}, nil
  53. }
  54. return nil, ParamInvalidError(arrayKey, "需要 JSON 字符串数组,如 [\"A\",\"B\"]")
  55. }
  56. }
  57. // 3) 都没提供
  58. hint := arrayKey
  59. if fileKey != "" {
  60. hint = fmt.Sprintf("%s(数组)或 %s(文件路径)", arrayKey, fileKey)
  61. }
  62. return nil, ParamInvalidError(arrayKey, fmt.Sprintf("必须提供 %s 之一", hint))
  63. }
  64. // parseStringListFile 按扩展名解析本地文件为字符串列表。
  65. // - .json:解析为 JSON 字符串数组;也兼容 {"items":[...]} 或 NDJSON 回退。
  66. // - 其它:按行解析,每行一个值,忽略空行、去除首尾空白。
  67. //
  68. // 传入的 path 应已通过 ValidateSourcePath 校验。
  69. func parseStringListFile(path string) ([]string, error) {
  70. b, err := os.ReadFile(path)
  71. if err != nil {
  72. return nil, fmt.Errorf("读取文件失败: %v", err)
  73. }
  74. if len(strings.TrimSpace(string(b))) == 0 {
  75. return nil, fmt.Errorf("文件为空")
  76. }
  77. ext := strings.ToLower(filepath.Ext(path))
  78. if ext == ".json" {
  79. if items, ok := tryParseJSONStringArray(b); ok {
  80. return items, nil
  81. }
  82. // .json 但不是纯字符串数组:回退按行(可能是 NDJSON 的 .json 命名)
  83. }
  84. // 按行解析
  85. items := parseLines(b)
  86. if len(items) == 0 {
  87. return nil, fmt.Errorf("未从文件解析出任何有效值")
  88. }
  89. return items, nil
  90. }
  91. // tryParseJSONStringArray 尝试把字节解析为 JSON 字符串数组 ["a","b"]。
  92. // 成功返回 (items, true);不是该形态则返回 (nil, false)。
  93. func tryParseJSONStringArray(b []byte) ([]string, bool) {
  94. var arr []string
  95. if err := json.Unmarshal(b, &arr); err == nil {
  96. return arr, true
  97. }
  98. return nil, false
  99. }
  100. // parseLines 按行解析,每行一个值,忽略空行、去除首尾空白。
  101. func parseLines(b []byte) []string {
  102. lines := strings.Split(string(b), "\n")
  103. out := make([]string, 0, len(lines))
  104. for _, ln := range lines {
  105. s := strings.TrimSpace(ln)
  106. if s == "" {
  107. continue
  108. }
  109. out = append(out, s)
  110. }
  111. return out
  112. }