# FWH MCP Server 测试 SKIP / FAIL 详细分析 > 配套 `report.md`(自动生成的用例明细)与 `pii_report.md`(脱敏专项)。 > 本文对 1 个 FAIL 和 73 个 SKIP 逐类给出根因分析、代码定位与修复建议。 > 分析基于源码阅读,非推测;关键行号已标注。 --- ## 一、总览 | 类别 | 数量 | 是否缺陷 | 结论 | |------|------|----------|------| | FAIL | 1 | **是(真实缺陷)** | `service_feedback_history` nil 指针 panic | | SKIP-A 需真实样本参数 | 51 | 否 | 工具有必填参数,本轮无样本,未构造调用 | | SKIP-B 依赖未初始化 | 约 8 | **是(配置缺陷)** | MCP `main.go` 少调了 FWH / Plane / Queue 的初始化 | | SKIP-C 数据为空 | 3 | 否 | 生产库当日无对应统计文档 | | SKIP-D Plane 参数缺失被远端拒 | 3 | 次要(健壮性) | handler 忽略必填校验错误,导致 404 而非清晰报错 | **核心结论**:真正需要修的是 **MCP Server 的初始化不完整**(`cmd/mcp/main.go`)。它只初始化了 DB/Cache/MinIO/Plane 底层 client,漏掉了 3 处初始化,直接导致 1 个 FAIL + 约 8 个本应 PASS 的 SKIP。 --- ## 二、FAIL 详细分析 ### FAIL-1 `service_feedback_history` — nil 指针 panic **现象** ``` internal error: panic recovered in service_feedback_history tool handler: runtime error: invalid memory address or nil pointer dereference ``` (panic 被 `server.WithRecovery()` 捕获,进程未崩溃,但该工具完全不可用。) **根因(已定位)** 调用链:`handleFeedbackHistory` → `services.GetFeedbackHistory("")` → `repositories.Plane.ListIntakeIssues(...)`。 `repositories.Plane` 是一个包级全局变量,只有 `repositories.InitPlane()` 才会给它赋值: `repositories/repositories.go:22,26-29` ```go var Plane *plane_v2.FeedbackPlaneService // InitPlane 初始化新架构 Plane(启动时调用) func InitPlane(cfg *config.PlaneWebApiConfig) { plane_v2.InitPlaneClient(cfg.Host, cfg.Email, cfg.Password) Plane = plane_v2.NewFeedbackPlaneService(cfg.WorkspaceSlug, cfg.ProjectId, ...) } // 通用初始化接口(给MCP Server等外部模块使用) var InitPlaneClient = plane_v2.InitPlaneClient // 只初始化底层 client,不赋值 Plane ``` 而 MCP 的 `cmd/mcp/main.go:48` 只调用了 **底层** 的 `InitPlaneClient`: ```go repositories.InitPlaneClient(planeCfg.Host, planeCfg.Email, planeCfg.Password) ``` **没有调用 `repositories.InitPlane(...)`**,所以 `repositories.Plane` 始终为 `nil` → 空指针解引用 panic。 **修复建议(二选一)** 方案 A(推荐,最小改动)——在 `cmd/mcp/main.go` 用 `InitPlane` 替换 `InitPlaneClient`: ```go // 原:repositories.InitPlaneClient(planeCfg.Host, planeCfg.Email, planeCfg.Password) repositories.InitPlane(&cfg.PlaneWebApi) ``` `InitPlane` 内部已包含 `InitPlaneClient`,同时会给 `repositories.Plane` 赋值,`data_plane_v2_*` 与 `service_feedback_*` 都能正常工作。 方案 B(防御性,二者叠加)——保留底层 init 的同时补充: ```go repositories.InitPlaneClient(planeCfg.Host, planeCfg.Email, planeCfg.Password) repositories.InitPlane(&cfg.PlaneWebApi) // 追加,给 Plane 全局赋值 ``` **附加健壮性建议**:`services.GetFeedbackHistory` 入口应做 nil 校验,避免未初始化时 panic: ```go func GetFeedbackHistory(userName string) ([]FeedbackHistoryItem, error) { if repositories.Plane == nil { return nil, fmt.Errorf("Plane 未初始化") } ... } ``` --- ## 三、SKIP 详细分析(按类) ### SKIP-B 依赖未初始化(**建议修复**) 这些工具连接失败/URL 非法,根因都是 MCP `main.go` 缺少初始化,而非依赖服务本身不可用。 #### B-1 FWH 远程 API:`http: no Host in request URL` **受影响用例**:`data_fwh_query_site_base_info_list`、`data_fwh_query_site_list`、 `data_fwh_query_alarm_statistics`、`data_fwh_query_fault_record_call_record_list`、 `data_fwh_query_tou_template_list`、`service_tou_template_list` 等(凡走 FWH Pro API)。 **现象** ``` Post "https:///hes-gateway/manage/selectIotTouTemplateList": http: no Host in request URL ``` 注意 `https://` 后面 **host 为空**。 **根因(已定位)** FWH 客户端的 host 来自包级变量 `fwhEnergy`,只有 `FwhHttpInit` 会赋值: `repositories/api/fwh_pro/fwhHttpClient.go:16-31` ```go var fwhEnergy string // 未初始化时为 "" func FwhHttpInit(cfg *config.FwhApiConfig) { fwhEnergy = cfg.FwhEnergy // ← 关键赋值 ... } func getFullEnergyUrl(relativeURL string) string { return "https://" + fwhEnergy + relativeURL // fwhEnergy="" → "https:///..." } ``` HTTP Server 的 `main.go:61` 有调用 `repositories.FwhHttpInit(&cfg.FwhApi)`, 但 **MCP 的 `cmd/mcp/main.go` 没有调用**,因此 `fwhEnergy` 为空 → URL 无 host。 > 说明:`config.env` 里 `FWH_ENERGY="energy.franklinwh.com"` 是配置好的, > 问题不在配置文件,而在 MCP 启动时没把它注入 FWH 客户端。 **修复建议**:在 `cmd/mcp/main.go` 初始化段补一行: ```go repositories.FwhHttpInit(&cfg.FwhApi) ``` (若还需测试环境 API,可按需追加 `FwhTestApi`。) #### B-2 后台任务:`inspector not initialized` **受影响用例**:`service_background_task_list` **根因(已定位)** `task/queue/fwhQueue/fwhTaskQueue.go:98` ```go return nil, fmt.Errorf("inspector not initialized") ``` Asynq 的 inspector 需在启动时初始化。MCP `main.go` 未初始化任务队列 inspector。 **修复建议**(按需): - 若希望 MCP 支持查询后台任务,在 `main.go` 补充任务队列 inspector 的初始化(参考 HTTP Server 启动流程中的队列初始化)。 - 若 MCP 场景不需要该能力,建议 handler 在 inspector 为空时返回**明确的业务提示**(如"后台任务查询在当前部署未启用"),而非底层错误串,避免误导。 #### 小结(SKIP-B 一次性修复) 在 `cmd/mcp/main.go` 的初始化段(现为 `database.Init` → `cache.Init` → `minioUtil.InitClient` → `InitPlaneClient`)补齐: ```go repositories.FwhHttpInit(&cfg.FwhApi) // 修复 B-1:FWH API host repositories.InitPlane(&cfg.PlaneWebApi) // 修复 FAIL-1 + Plane 相关 // (可选)初始化任务队列 inspector,修复 B-2 ``` 预计可将 FWH 相关 4~6 个 SKIP、feedback panic、以及后台任务查询一并转为可用。 --- ### SKIP-D Plane 参数被远端拒绝(**次要健壮性问题**) **受影响用例**:`plane_list_labels`、`plane_list_states`(测试以 `{}` 调用),返回 `404 Not Found`。 **根因(已定位)** 这两个工具在 schema 中把 `workspace_slug`/`project_id` 声明为 `Required`, 但 handler **忽略了必填校验错误**: `cmd/mcp/tools/data/plane_v2_api_handlers.go:164-177` ```go func handlePlaneV2ListLabels(...) { slug, _ := req.RequireString("workspace_slug") // ← 错误被丢弃 projectId, _ := req.RequireString("project_id") // ← 错误被丢弃 data, err := planeV2Svc.ListLabels(slug, projectId) // 用空串请求 → 远端 404 ... } ``` 对比正确写法(如 `handlePlaneV2GetWorkspace` / `handlePlaneV2ListProjects`)是检查错误并 `return helper.ParamError(...)`。 **影响**:功能上这些工具**需要**真实 slug/project_id 才能用(属 SKIP-A 类), 但由于未做前置校验,缺参时返回的是含糊的远端 404,而不是清晰的"参数 workspace_slug 必填"。 **修复建议**:统一补齐必填校验: ```go slug, err := req.RequireString("workspace_slug") if err != nil { return helper.ParamError("workspace_slug"), nil } projectId, err := req.RequireString("project_id") if err != nil { return helper.ParamError("project_id"), nil } ``` 建议全量排查 `plane_v2_api_handlers.go` 中所有 `:= req.RequireString(...)` 后紧跟 `, _` 丢弃错误的写法。 > 备注:`plane_list_projects` 用例本轮同样因缺 `workspace_slug` 被判 SKIP, > 但它 handler 有正确校验,返回了清晰的"参数 workspace_slug 必填",属正常行为。 --- ### SKIP-A 需真实样本参数(**非缺陷,测试限制**) **受影响用例**(约 51 个):所有含必填业务参数的只读工具,例如 `service_site_query_info`(sn)、`service_sim_query`(giccid)、 `data_pgsql_query_snapshot_data`(sn,date)、`data_sim_*`(iccid)、 `plane_get_*`(workspace_slug,project_id,...)、`storage_minio_read_object`(bucket,key) 等。 **原因**:这些工具需要真实存在的 SN / GICCID / 任务 ID / 文件名 / Plane UUID 才能构造有意义的调用;本轮测试无这些样本,驱动主动 SKIP(避免用假值产生无意义的错误)。 **这不是缺陷**。补测方法:提供样本后运行驱动的样本模式(见"如何补测")。这些工具的**参数校验分支**已在 L1 层抽样验证正确(缺参返回 `参数 xx 必填`)。 --- ### SKIP-C 数据为空(**非缺陷**) **受影响用例**:`service_monitor_alarm_stats`、`service_dashboard_alarm_detail`(`mongo: no documents in result`)。 **原因**:生产库 `StatisticsAlarm` 集合当日/最新无文档。工具行为正确(查不到返回错误/空),只是恰好无数据。 `service_monitor_shutdown_stats`、`service_dashboard_shutdown_detail` 同类工具有数据、已 PASS,可佐证逻辑正常。 **建议**(可选):对"无文档"场景,`services` 层可返回**空结果 + 提示**(`helper.EmptyResult`)而非 error,语义更友好;当前作为 error 也可接受。 --- ## 四、修复优先级建议 | 优先级 | 问题 | 修复位置 | 预期收益 | |--------|------|----------|----------| | **P0** | FAIL-1 feedback panic | `cmd/mcp/main.go` 改用 `InitPlane` | 消除 panic,Plane/feedback 可用 | | **P0** | B-1 FWH host 为空 | `cmd/mcp/main.go` 补 `FwhHttpInit` | 恢复所有 `data_fwh_*` / TOU 远程查询 | | P1 | SKIP-D Plane handler 忽略必填校验 | `plane_v2_api_handlers.go` 补错误检查 | 缺参时清晰报错而非 404 | | P2 | B-2 后台任务 inspector | `cmd/mcp/main.go` 补队列初始化 或 友好提示 | 后台任务查询可用/语义清晰 | | P2 | GetFeedbackHistory nil 防御 | `services/feedbackServices.go` | 纵深防御 | | P3 | SKIP-C 空数据语义 | `services` 统计相关 | 语义友好(可选) | **一次性关键修复**(`cmd/mcp/main.go`,覆盖 P0): ```go // 现有: database.Init(&cfg.App) cache.Init(cfg.App.RedisAddr) minioUtil.InitClient(&cfg.App) repositories.InitPlaneClient(planeCfg.Host, planeCfg.Email, planeCfg.Password) // 建议调整为: database.Init(&cfg.App) cache.Init(cfg.App.RedisAddr) minioUtil.InitClient(&cfg.App) repositories.FwhHttpInit(&cfg.FwhApi) // 新增:FWH API host repositories.InitPlane(&cfg.PlaneWebApi) // 替换:同时初始化 Plane 全局实例 ``` --- ## 五、如何补测(回归验证) 1. 应用上述 `main.go` 修复并重新构建:`go build -o fwh-mcp-server ./cmd/mcp/` 2. 重新运行驱动: `go run ./cmd/mcp/test/runner -bin <新二进制> -cwd /var/www/fwh-toolbox/server -out ./cmd/mcp/test` 3. 预期:FAIL-1 转 PASS;FWH/TOU 相关 SKIP-B 转 PASS 或 SKIP-A(若仍缺样本)。 4. 提供真实 SN / GICCID / Plane workspace_slug+project_id / 已知 MinIO 对象后, 可为驱动增加样本入参(`runL2` 的 preset 段),将 SKIP-A 用例转为真实冒烟。 ## 六、回归验证结果(已应用修复后) 已应用 P0 + P1 修复并重跑驱动,结果对比: | 指标 | 修复前 | 修复后 | |------|--------|--------| | PASS | 50 | **57** | | FAIL | 1 | **0** | | SKIP | 73 | 67 | 已应用的修复: 1. `cmd/mcp/main.go`:新增 `repositories.FwhHttpInit(&cfg.FwhApi)`;将 `InitPlaneClient` 替换为 `repositories.InitPlane(&cfg.PlaneWebApi)`。 2. `services/feedbackServices.go`:`GetFeedbackHistory` / `SubmitFeedback` 增加 `repositories.Plane == nil` 防御。 3. `cmd/mcp/tools/data/plane_v2_api_handlers.go`:新增 `requireSlugProject` 辅助函数,修复 15 个 handler 忽略 `workspace_slug`/`project_id` 必填校验的问题。 验证要点: - **FAIL-1 已消除**:`service_feedback_history` → PASS(返回 506B 合法结果)。 - **B-1 已修复**:`data_fwh_query_site_list`、`data_fwh_query_tou_template_list`、`service_tou_template_list` 等 FWH 远程查询 → PASS。 - **D 已修复**:`plane_list_labels`/`states`/`projects` 缺参时返回清晰的"参数 workspace_slug 必填",不再是 404。 - **L4 脱敏仍 4/4 PASS**,修复未影响脱敏。 - 剩余 SKIP:61 项需真实样本参数(非缺陷)、2 项统计集合空数据(非缺陷)、1 项 `service_background_task_list`(B-2,inspector 未初始化,属 P2 待定项,本次未改)。 --- ## 七、第二轮:真实样本补测 + MinIO/文件写读闭环 使用用户提供的样本(SN `10130095A00X25280030`、GICCID `8931084524121848556`、 Plane workspace `fwh` / project `55efdd5f-...`)补测原 SKIP-A 用例,并做 MinIO 写 + 文件读闭环。 结果:**149 用例 PASS 82 / FAIL 0 / SKIP 67**(较上一轮 +25 用例、+25 PASS)。 - **L2S 真实样本(21/21 PASS)**:站点详情/功率流/模式/告警、SIM 详情/月用量、 Plane 项目/工作项/标签/状态/周期/模块/收件箱/成员 全部真实调用成功。 - **L3RW 写读闭环(4/4 PASS)**:`storage_file_write`(写入 MinIO)→ `storage_file_list` → `storage_file_read`(读回内容一致)→ `storage_minio_list_objects`(确认对象落盘)。 - **PII 端到端(7/7 PASS)**:新增 3 个真实 SN/GICCID 详情(site_query_info / sim_query / smarthub_card_info)脱敏检测,均无明文泄露。 ### 本轮新发现缺陷(已修复) **BUG-2 `data_fwh_query_system_update_log` 反序列化失败** - 现象:`json: cannot unmarshal number into Go struct field SystemUpdateRecordInfo.result.upgradeType of type string` - 根因:`domain/fwhProDomain.go` 中 `SystemUpdateRecordInfo.UpgradeType` 声明为 `string`, 但 FWH 上游 API 对该字段实际返回 **number**,导致整个响应反序列化失败、工具不可用。 - 修复:将 `UpgradeType string` 改为 `UpgradeType int`(上游该字段返回数值,用数值类型更明确、类型安全;该字段仅透传输出、无消费方,改动安全)。 - 验证:修复后 `data_fwh_query_system_update_log` → PASS。 > 说明:该缺陷仅在特定 SN(`upgradeType` 为数字)时触发,第一轮因无样本被 SKIP 掩盖, > 说明真实样本补测对发现"数据形态相关"的反序列化 bug 很有价值。建议排查同族结构 > (如 `AHubUpdateRecordInfo`、`SystemUpdateDetailInfo` 等)是否也存在 number/string 混用字段。 --- ## 八、第三轮:级联补测(L2C + L2X),覆盖率最大化 在样本轮基础上新增两阶段自动补测,把"有必填参数但可从已 PASS 接口派生"的工具全部覆盖,**无需额外外部样本**。 - **L2C 级联补测(12 PASS / 5 SKIP)**:先调列表接口,用 `jsonExtract`(支持 `data.list[0].iccid` 路径)提取第一个 id/key/name,再喂给详情接口。覆盖: auto_update get_package/get_config、tou detail(service + data_fwh)、site_analysis get_config/data、 batch_download path、site_monitor config_get、acceleronix card_info/data_pool_sim_info、 minio read_object(正确的大小校验判 PASS)、plane get_intake_issue。 - **L2X 额外补测(7 PASS)**:递归级联 / 自造输入。覆盖: site_power_flow(样本 SN+date)、plane get_workspace/list_pages/list_stickies、 batch_download file_tree(递归取 path 树第一个文件叶子)、 storage_file_read_to_local(写后读到本地)、storage_file_upload(自造本地临时文件上传)。 累计结果:**173 用例 PASS 101 / FAIL 0 / SKIP 72**(可补测工具已全部覆盖)。 ### 剩余 SKIP 的性质(已无"可补测"项) 排除各阶段已 PASS 后,剩 23 个工具,全部**无法靠测试手段自行解决**: - **B 组(13)需"确有数据"的样本**:快照 file_name、有快照的 sn+date、真实 alarm_code、 有效 cmd_type、有故障录波/EMS/告警的 SN、有升级详情的 order、有用量的 month。 - **C 组(1)需代码改动**:`service_background_task_list` 需在 MCP main.go 初始化 asynq inspector。 - **D 组(9)环境数据为空**:告警统计集合当日无文档;Plane 该 project 无 issue/cycle/module/page。 工具逻辑正常,需先有业务数据。 > 每个剩余 SKIP 的"缺什么/如何补"已在 `report.md` 的「SKIP 逐条分析」章节动态列出。 --- ## 九、已确认无问题的项(供对照) - 协议与 119 个工具注册、schema、命名规范:全部正常。 - 参数校验(L1 14 项):全部正确拦截。 - **PII 脱敏(L4)**:生产库真实数据经 `service_site_search`/`service_sim_search`/`data_mongo_find` 返回均正确脱敏(account/手机/地址/经纬度/IMEI/IMSI/deviceIp 均 `******`),保留字段(sn/giccid/_id)正常;helper 单测 21 项全 PASS。脱敏是本次测试重点,结论为**安全达标**。