fix(admin): 诊断解析放错了函数 + 补 kb_search 形状测试
上一个提交里,诊断解析被塞进了用户面的 KbSearch,而真正传 diag 的
AdminKbSearch 还在用老代码 —— 于是试验台永远拿不到 routes。写法上是
字符串替换只改了文件里第一处匹配,而这两个 handler 的收尾代码一模一样。
用户面已还原(它不传 diag,拿到的本就是裸数组,多一层解析是错的)。
补 3 组 mcp 层测试钉死 kb_search 的两种返回形状:
- 不带 diag(生产调用:dispatcher / Agent 工具链)→ 裸 JSON 数组,契约不变;
- 带 diag → {hits, routes} 对象;
- not ready 时不被总闸短路,仍逐路诊断。
这层是"改可观测性顺手改坏生产"的高风险位置:diag 分支若无条件生效,所有
调用方的解析都会静默失败。
真环境验证(本地 gateway + mcp-go + 真 PG/Milvus/bleve):
vector empty 该知识库在 Milvus 中没有向量(未入库或集合被重建过)
fulltext ok 15 命中
graph disabled Neo4j 未连接或未配置
以前这三行都只是"0 条"。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -18,9 +18,9 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/sundynix/sundynix-shared/blob"
|
||||
"github.com/sundynix/sundynix-gateway/internal/nats"
|
||||
"github.com/sundynix/sundynix-gateway/internal/store"
|
||||
"github.com/sundynix/sundynix-shared/blob"
|
||||
"github.com/sundynix/sundynix-shared/contract"
|
||||
)
|
||||
|
||||
@@ -548,18 +548,9 @@ func (h *Handler) KbSearch(c *gin.Context) {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": res.Error})
|
||||
return
|
||||
}
|
||||
var out struct {
|
||||
Hits []map[string]any `json:"hits"`
|
||||
Routes []map[string]any `json:"routes"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(res.Content), &out); err != nil {
|
||||
// 兼容老 mcp-go(未部署新版时仍回裸数组),别因为这个把试验台整个打挂。
|
||||
var hits []map[string]any
|
||||
_ = json.Unmarshal([]byte(res.Content), &hits)
|
||||
c.JSON(http.StatusOK, gin.H{"hits": hits})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"hits": out.Hits, "routes": out.Routes})
|
||||
var hits []map[string]any
|
||||
_ = json.Unmarshal([]byte(res.Content), &hits)
|
||||
c.JSON(http.StatusOK, gin.H{"hits": hits})
|
||||
}
|
||||
|
||||
// AdminKbSearch: POST /api/v1/admin/kb/search —— 管理端「检索试验台」:按**完整作用域键**
|
||||
@@ -602,9 +593,19 @@ func (h *Handler) AdminKbSearch(c *gin.Context) {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": res.Error})
|
||||
return
|
||||
}
|
||||
var hits []map[string]any
|
||||
_ = json.Unmarshal([]byte(res.Content), &hits)
|
||||
c.JSON(http.StatusOK, gin.H{"hits": hits})
|
||||
// 带 diag 时 mcp-go 回的是 {hits, routes} 对象;老版本回裸数组,做兼容降级
|
||||
// (少了诊断而已,不该让整个试验台打不开)。
|
||||
var out struct {
|
||||
Hits []map[string]any `json:"hits"`
|
||||
Routes []map[string]any `json:"routes"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(res.Content), &out); err != nil {
|
||||
var hits []map[string]any
|
||||
_ = json.Unmarshal([]byte(res.Content), &hits)
|
||||
c.JSON(http.StatusOK, gin.H{"hits": hits})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"hits": out.Hits, "routes": out.Routes})
|
||||
}
|
||||
|
||||
// KbGraph: GET /api/v1/kb/graph?kb= —— 某知识库的图谱三元组(→ mcp-go kb_graph,Neo4j)。
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/sundynix/sundynix-shared/contract"
|
||||
|
||||
"github.com/sundynix/sundynix-mcp-go/internal/rag"
|
||||
)
|
||||
|
||||
// kb_search 的两种返回形状必须泾渭分明:
|
||||
// - 不带 diag(生产调用:dispatcher、Agent 工具链)→ 裸 JSON 数组,契约不能变;
|
||||
// - 带 diag(只有检索试验台会传)→ {hits, routes} 对象,带每一路的诊断。
|
||||
//
|
||||
// 把它钉住是因为这层是"改可观测性顺手改坏生产"的高风险位置:diag 分支若写成无条件
|
||||
// 生效,所有调用方拿到的就从数组变成了对象,工具链会静默解析失败。
|
||||
|
||||
// newFulltextOnlyGateway 造一个只有全文路可用的 Gateway:无 embedding、无 Milvus、无 Neo4j。
|
||||
// 这正是 mcp-go 先于 gateway 启动(控制面配置尚未下发)时的真实状态。
|
||||
func newFulltextOnlyGateway(t *testing.T) *Gateway {
|
||||
t.Helper()
|
||||
t.Setenv("BLEVE_PATH", t.TempDir()+"/bleve")
|
||||
e := rag.Open(context.Background(), rag.Config{}) // 不给 Milvus/Neo4j/embedding
|
||||
return &Gateway{rag: e}
|
||||
}
|
||||
|
||||
func TestKbSearch_ProductionShapeIsBareArray(t *testing.T) {
|
||||
g := newFulltextOnlyGateway(t)
|
||||
res := g.kbSearch(context.Background(), &contract.ToolCall{
|
||||
Tool: "kb_search",
|
||||
Args: map[string]any{"kb": "k1", "q": "星间链路"},
|
||||
})
|
||||
if !res.OK {
|
||||
t.Fatalf("不应失败: %s", res.Error)
|
||||
}
|
||||
var arr []map[string]any
|
||||
if err := json.Unmarshal([]byte(res.Content), &arr); err != nil {
|
||||
t.Fatalf("生产调用必须返回裸数组(工具链据此解析),得 %q: %v", res.Content, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKbSearch_DiagShapeCarriesRoutes(t *testing.T) {
|
||||
g := newFulltextOnlyGateway(t)
|
||||
res := g.kbSearch(context.Background(), &contract.ToolCall{
|
||||
Tool: "kb_search",
|
||||
Args: map[string]any{"kb": "k1", "q": "星间链路", "diag": true, "mode": "hybrid"},
|
||||
})
|
||||
if !res.OK {
|
||||
t.Fatalf("不应失败: %s", res.Error)
|
||||
}
|
||||
var out struct {
|
||||
Hits []map[string]any `json:"hits"`
|
||||
Routes []rag.RouteDiag `json:"routes"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(res.Content), &out); err != nil {
|
||||
t.Fatalf("diag 调用应返回 {hits,routes} 对象,得 %q: %v", res.Content, err)
|
||||
}
|
||||
if len(out.Routes) != 3 {
|
||||
t.Fatalf("应有 vector/fulltext/graph 三路诊断,得 %d 条: %+v", len(out.Routes), out.Routes)
|
||||
}
|
||||
for _, r := range out.Routes {
|
||||
if r.Status == "" {
|
||||
t.Fatalf("每一路都必须有 status,否则界面仍无法解释这个空: %+v", r)
|
||||
}
|
||||
// 没配置的路必须给出原因——这正是当初排查半天的痛点。
|
||||
if r.Status == "disabled" && r.Note == "" {
|
||||
t.Fatalf("%s 路 disabled 却没写原因", r.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 未配置 embedding/Milvus 时不能整体短路返回空:以前用 rag.Ready() 当总闸,
|
||||
// 连不依赖它们的全文/图谱路一起毙掉,表现为"知识库什么都搜不到"且不报错。
|
||||
func TestKbSearch_NotShortCircuitedByReadiness(t *testing.T) {
|
||||
g := newFulltextOnlyGateway(t)
|
||||
if g.rag.Ready() {
|
||||
t.Fatal("前提不成立:该引擎本应 not ready")
|
||||
}
|
||||
res := g.kbSearch(context.Background(), &contract.ToolCall{
|
||||
Tool: "kb_search",
|
||||
Args: map[string]any{"kb": "k1", "q": "星间链路", "diag": true},
|
||||
})
|
||||
var out struct {
|
||||
Routes []rag.RouteDiag `json:"routes"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(res.Content), &out); err != nil {
|
||||
t.Fatalf("not ready 时也应正常走到各路诊断,得 %q", res.Content)
|
||||
}
|
||||
if len(out.Routes) == 0 {
|
||||
t.Fatal("not ready 时被总闸短路了(返回空数组而非逐路诊断)")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user