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 }