| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123 |
- package helper
- import (
- "encoding/json"
- "fmt"
- "os"
- "path/filepath"
- "strings"
- "github.com/mark3labs/mcp-go/mcp"
- )
- // StringListSource 表示一批字符串(如 SN / GICCID 列表)的来源解析结果,
- // 用于统一"数组入参"与"文件入参"两种传参方式。
- type StringListSource struct {
- Items []string // 解析出的字符串列表
- FromFile bool // 是否来自文件入参
- FilePath string // 当 FromFile 为 true 时的文件绝对路径
- }
- // ResolveStringListOrFile 统一解析"数组或文件"两种批量入参,优先级与安全规则:
- //
- // - 若提供了 fileKey(文件入参):优先使用文件,忽略数组入参。
- // 文件路径必须通过 ValidateSourcePath 校验(仅允许 AgentBaseDir 下),
- // 否则返回错误结果。文件内容按扩展名解析:
- // - .json → JSON 字符串数组,如 ["A","B"]
- // - 其它(.ndjson/.txt/无扩展名)→ 每行一个值(忽略空行、去除首尾空白)
- // - 否则若提供了 arrayKey(结构化数组 schema,WithArray+WithStringItems):使用数组。
- // - 二者都未提供:返回 (nil-source, errResult),由调用方作为参数错误返回。
- //
- // 返回 (source, errResult):errResult 非 nil 时表示解析失败,调用方应直接 return 该结果。
- func ResolveStringListOrFile(req mcp.CallToolRequest, arrayKey, fileKey string) (*StringListSource, *mcp.CallToolResult) {
- // 1) 文件入参优先
- if fileKey != "" {
- if raw := strings.TrimSpace(req.GetString(fileKey, "")); raw != "" {
- abs, err := ValidateSourcePath(raw)
- if err != nil {
- return nil, ParamInvalidError(fileKey, err.Error())
- }
- items, perr := parseStringListFile(abs)
- if perr != nil {
- return nil, ParamInvalidError(fileKey, perr.Error())
- }
- return &StringListSource{Items: items, FromFile: true, FilePath: abs}, nil
- }
- }
- // 2) 数组入参:兼容两种历史风格
- // a) WithArray 结构化数组(WithStringItems)→ GetStringSlice
- // b) JSON 字符串数组(WithString + 手动 unmarshal)→ req.GetString 后解析
- if arrayKey != "" {
- if items, ok := GetStringSlice(req, arrayKey); ok {
- return &StringListSource{Items: items, FromFile: false}, nil
- }
- if s := strings.TrimSpace(req.GetString(arrayKey, "")); s != "" {
- if items, ok := tryParseJSONStringArray([]byte(s)); ok {
- return &StringListSource{Items: items, FromFile: false}, nil
- }
- return nil, ParamInvalidError(arrayKey, "需要 JSON 字符串数组,如 [\"A\",\"B\"]")
- }
- }
- // 3) 都没提供
- hint := arrayKey
- if fileKey != "" {
- hint = fmt.Sprintf("%s(数组)或 %s(文件路径)", arrayKey, fileKey)
- }
- return nil, ParamInvalidError(arrayKey, fmt.Sprintf("必须提供 %s 之一", hint))
- }
- // parseStringListFile 按扩展名解析本地文件为字符串列表。
- // - .json:解析为 JSON 字符串数组;也兼容 {"items":[...]} 或 NDJSON 回退。
- // - 其它:按行解析,每行一个值,忽略空行、去除首尾空白。
- //
- // 传入的 path 应已通过 ValidateSourcePath 校验。
- func parseStringListFile(path string) ([]string, error) {
- b, err := os.ReadFile(path)
- if err != nil {
- return nil, fmt.Errorf("读取文件失败: %v", err)
- }
- if len(strings.TrimSpace(string(b))) == 0 {
- return nil, fmt.Errorf("文件为空")
- }
- ext := strings.ToLower(filepath.Ext(path))
- if ext == ".json" {
- if items, ok := tryParseJSONStringArray(b); ok {
- return items, nil
- }
- // .json 但不是纯字符串数组:回退按行(可能是 NDJSON 的 .json 命名)
- }
- // 按行解析
- items := parseLines(b)
- if len(items) == 0 {
- return nil, fmt.Errorf("未从文件解析出任何有效值")
- }
- return items, nil
- }
- // tryParseJSONStringArray 尝试把字节解析为 JSON 字符串数组 ["a","b"]。
- // 成功返回 (items, true);不是该形态则返回 (nil, false)。
- func tryParseJSONStringArray(b []byte) ([]string, bool) {
- var arr []string
- if err := json.Unmarshal(b, &arr); err == nil {
- return arr, true
- }
- return nil, false
- }
- // parseLines 按行解析,每行一个值,忽略空行、去除首尾空白。
- func parseLines(b []byte) []string {
- lines := strings.Split(string(b), "\n")
- out := make([]string, 0, len(lines))
- for _, ln := range lines {
- s := strings.TrimSpace(ln)
- if s == "" {
- continue
- }
- out = append(out, s)
- }
- return out
- }
|