Merge remote-tracking branch 'origin/feat/mcp-graph-neighbors-tool'
# Conflicts: # system-dev/wiki/status.md
This commit is contained in:
@@ -0,0 +1,158 @@
|
|||||||
|
/**
|
||||||
|
* KBDB graph 查詢 MCP 薄殼(issue #68,D17 KBDB MCP 面)
|
||||||
|
*
|
||||||
|
* rule 07 §5(薄殼鐵律):能力長在 workflow(graph_neighbors,registry/examples/graph-neighbors),
|
||||||
|
* MCP 只做介面轉換 + 暴露,無業務邏輯——不碰 D1、不動 workflow 引擎本體。
|
||||||
|
*
|
||||||
|
* D17「KBDB MCP=RAG 套餐」三模式:關鍵字(kbdb_search keyword)/語義(kbdb_search semantic)
|
||||||
|
* /圖(本檔)。本檔補上 MCP 面缺的第三模式:關係遍歷(1-hop/N-hop 鄰居)。
|
||||||
|
*
|
||||||
|
* 走 #28 地基的同步查詢端點(cypher-executor webhooks-named.ts):
|
||||||
|
* GET /q/:ns/:name — 同步執行 workflow,直接回「最終節點輸出」本身(200 直出,非 202、非信封)。
|
||||||
|
* 本工具打 GET /q/{orgNamespace}/graph_neighbors(經既有 CYPHER_EXECUTOR service binding,
|
||||||
|
* 不新增 binding),namespace 用 MCP token 解析出的 orgNamespace(與 whoami 同源)。
|
||||||
|
*
|
||||||
|
* workflow 未部署(用戶沒裝 graph_neighbors)→ 404 → 誠實回錯誤+怎麼裝,不 crash(鐵律)。
|
||||||
|
*
|
||||||
|
* graph_neighbors workflow 的 input 形狀(registry/examples/graph-neighbors/workflow.yaml):
|
||||||
|
* node(起點)、depth(預設 1)、template(triplet template 名)、namespace(owner_id)、
|
||||||
|
* kbdb_base(呼叫者自己的 KBDB 對外 URL——workflow 刻意不寫死任何一家的庫)、directed。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
import { z } from "zod";
|
||||||
|
import type { Env } from "../types.js";
|
||||||
|
import { cypherFetch, errorResponse, successResponse } from "../lib/cypher-client.js";
|
||||||
|
|
||||||
|
/** graph 查詢 workflow 名(與 registry/examples/graph-neighbors/workflow.yaml 的 name 一致)。 */
|
||||||
|
export const GRAPH_NEIGHBORS_WORKFLOW = "graph_neighbors";
|
||||||
|
|
||||||
|
/** 未裝 workflow 時的安裝指引(404 與內層錯誤共用,訊息一致)。 */
|
||||||
|
const INSTALL_HINTS = [
|
||||||
|
`graph 查詢由「${GRAPH_NEIGHBORS_WORKFLOW}」workflow 承載(薄殼設計,MCP 本身不算圖)`,
|
||||||
|
"安裝:拿 registry/examples/graph-neighbors/workflow.yaml,call arcrun_push_workflow 部署(name 必須是 graph_neighbors)",
|
||||||
|
"或用 CLI:acr push registry/examples/graph-neighbors/workflow.yaml",
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 註冊全部 KBDB graph 查詢工具(issue #68)。 */
|
||||||
|
export function registerAllKbdbGraphTools(server: McpServer, env: Env, orgNamespace: string) {
|
||||||
|
registerGraphNeighbors(server, env, orgNamespace);
|
||||||
|
// graph_traverse:repo 內目前只有 graph-neighbors 有 workflow 定義(registry/examples/),
|
||||||
|
// traverse 尚無可對齊的 input 形狀 → 不猜、不過度工程;等 workflow 進 registry 再加薄殼。
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* kbdb_graph_neighbors — knowledge graph 1-hop/N-hop 鄰居查詢。
|
||||||
|
* 薄殼調 GET /q/{ns}/graph_neighbors,結果(最終節點輸出)原樣回給 MCP client。
|
||||||
|
*/
|
||||||
|
export function registerGraphNeighbors(server: McpServer, env: Env, orgNamespace: string) {
|
||||||
|
server.tool(
|
||||||
|
"kbdb_graph_neighbors",
|
||||||
|
"knowledge graph 鄰居查詢(1-hop/N-hop 關係遍歷):給一個節點名,沿 KBDB triplet" +
|
||||||
|
"(subject-predicate-object)記錄做 BFS,回傳 depth 跳內的鄰居清單" +
|
||||||
|
"([{node, predicate, from, depth}])。與 kbdb_search(關鍵字/語義)互補:" +
|
||||||
|
"找「跟 X 有關係的東西」用本工具,找「內容含關鍵字的東西」用 kbdb_search。" +
|
||||||
|
"需要 namespace 裡已部署 graph_neighbors workflow(沒裝會回安裝指引,不會 crash)。",
|
||||||
|
{
|
||||||
|
subject: z.string().min(1).describe(
|
||||||
|
"起點節點名(graph triplet 的 subject/object 值),如 'Arcrun'",
|
||||||
|
),
|
||||||
|
depth: z.number().int().min(1).max(10).optional().describe(
|
||||||
|
"最大跳數(N-hop),預設 1(只看直接鄰居)",
|
||||||
|
),
|
||||||
|
kbdb_base: z.string().min(1).describe(
|
||||||
|
"你自己部署的 KBDB 對外 base URL(如 https://arcrun-kbdb.<你的subdomain>.workers.dev " +
|
||||||
|
"或 KBDB custom domain)。workflow 刻意不寫死任何一家的庫——" +
|
||||||
|
"帶錯(或照抄別人的值)=查詢打進別人的庫",
|
||||||
|
),
|
||||||
|
template: z.string().optional().describe(
|
||||||
|
"triplet 記錄的 template 名,預設 'graph_triplet'(以實際部署的 kbdb-graph-plugin " +
|
||||||
|
"triplet template 為準,不確定可 kbdb_list_templates 查)",
|
||||||
|
),
|
||||||
|
directed: z.boolean().optional().describe(
|
||||||
|
"true=只走 subject→object 有向邊;預設 false(把 triplet 當雙向邊,無向鄰居)",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
async ({ subject, depth, kbdb_base, template, directed }) => {
|
||||||
|
if (!orgNamespace) {
|
||||||
|
return errorResponse(
|
||||||
|
"no_namespace",
|
||||||
|
"此 MCP 連線沒有解析出 namespace,無法定位 graph workflow",
|
||||||
|
["call arcrun_whoami 確認身份", "確認 MCP token / OAuth 設定正確"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// input 形狀對齊 graph_neighbors workflow(node/depth/template/namespace/kbdb_base/directed)。
|
||||||
|
const query: Record<string, string | number> = {
|
||||||
|
node: subject,
|
||||||
|
depth: depth ?? 1,
|
||||||
|
template: template ?? "graph_triplet",
|
||||||
|
namespace: orgNamespace,
|
||||||
|
kbdb_base,
|
||||||
|
};
|
||||||
|
if (directed) query.directed = "true";
|
||||||
|
|
||||||
|
// /q/:ns/:name:namespace 走 path(cypher opaque-key 模型,orgNamespace 即分區 key),
|
||||||
|
// 走既有 CYPHER_EXECUTOR service binding(cypherFetch),不新增 binding。
|
||||||
|
const res = await cypherFetch(
|
||||||
|
env,
|
||||||
|
`/q/${encodeURIComponent(orgNamespace)}/${GRAPH_NEIGHBORS_WORKFLOW}`,
|
||||||
|
{ apiKey: orgNamespace, query },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.status === 404) {
|
||||||
|
// workflow 沒裝 → 誠實 + 給安裝路徑(鐵律:不 crash、不假綠)。
|
||||||
|
return errorResponse(
|
||||||
|
"workflow_not_installed",
|
||||||
|
`namespace「${orgNamespace}」尚未部署 ${GRAPH_NEIGHBORS_WORKFLOW} workflow,graph 查詢無法使用`,
|
||||||
|
INSTALL_HINTS,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bodyText = await res.text();
|
||||||
|
let data: unknown = null;
|
||||||
|
try {
|
||||||
|
data = bodyText ? JSON.parse(bodyText) : null;
|
||||||
|
} catch {
|
||||||
|
data = bodyText;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
// 409=paused(無法同步查詢)、413=輸出過大、500=執行失敗,端點都回 {error,...}。
|
||||||
|
const err = (data ?? {}) as { error?: string };
|
||||||
|
return errorResponse(
|
||||||
|
"graph_query_failed",
|
||||||
|
`graph 查詢失敗 HTTP ${res.status}: ${err.error ?? "unknown"}`,
|
||||||
|
[
|
||||||
|
"確認 kbdb_base 是你自己 KBDB 的對外 URL 且可被 cypher-executor fetch(1042 陷阱:優先 custom domain)",
|
||||||
|
"確認 template 名對得上實際 triplet template(kbdb_list_templates)",
|
||||||
|
"call arcrun_list_recent_executions('graph_neighbors') 看 trace",
|
||||||
|
],
|
||||||
|
typeof data === "string" ? data : JSON.stringify(data),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 200 = workflow 跑完,body 即最終節點輸出本身。但 workflow 內部仍可能回
|
||||||
|
// { success:false, error }(如缺參數)——不把它假裝成成功結果。
|
||||||
|
const out = data as { success?: boolean; error?: string; count?: number } | null;
|
||||||
|
if (out && typeof out === "object" && out.success === false) {
|
||||||
|
return errorResponse(
|
||||||
|
"graph_query_failed",
|
||||||
|
out.error ?? "graph_neighbors workflow 回報失敗",
|
||||||
|
["確認 subject 非空", "確認參數形狀(depth 為正整數)"],
|
||||||
|
JSON.stringify(out),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 原樣回給 MCP client(薄殼:不加工、不重排)。
|
||||||
|
return successResponse(out, [
|
||||||
|
`${out?.count ?? 0} 個鄰居(depth 上限 ${depth ?? 1})`,
|
||||||
|
"count=0 且不確定資料有沒有進圖:kbdb_query(template='graph_triplet') 看 triplet 記錄",
|
||||||
|
"找關鍵字內容改用 kbdb_search;取單筆全文用 kbdb_get_record",
|
||||||
|
]);
|
||||||
|
} catch (e) {
|
||||||
|
return errorResponse("internal_error", e instanceof Error ? e.message : String(e), ["稍後重試"]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ import { registerAllWorkflowCrudTools } from "./arcrun_workflow_crud.js";
|
|||||||
import { registerAllSkillExampleTools } from "./arcrun_skills_examples.js";
|
import { registerAllSkillExampleTools } from "./arcrun_skills_examples.js";
|
||||||
import { registerAllRecipeTools } from "./arcrun_recipe.js";
|
import { registerAllRecipeTools } from "./arcrun_recipe.js";
|
||||||
import { registerAllKbdbDataTools } from "./kbdb_data.js";
|
import { registerAllKbdbDataTools } from "./kbdb_data.js";
|
||||||
|
import { registerAllKbdbGraphTools } from "./kbdb_graph.js";
|
||||||
import { registerWhoami } from "./arcrun_whoami.js";
|
import { registerWhoami } from "./arcrun_whoami.js";
|
||||||
|
|
||||||
export function registerAllTools(server: McpServer, env: Env, orgNamespace: string, partnerToken: string) {
|
export function registerAllTools(server: McpServer, env: Env, orgNamespace: string, partnerToken: string) {
|
||||||
@@ -49,6 +50,9 @@ export function registerAllTools(server: McpServer, env: Env, orgNamespace: stri
|
|||||||
// kbdb-base Phase 9.1: KBDB 資料層薄殼(template/record/query/search,HANDOFF §2)
|
// kbdb-base Phase 9.1: KBDB 資料層薄殼(template/record/query/search,HANDOFF §2)
|
||||||
// 鐵律:不提供建表/SQL tool,AI 只有 template+slot 可用(類 Supabase 萬用表)
|
// 鐵律:不提供建表/SQL tool,AI 只有 template+slot 可用(類 Supabase 萬用表)
|
||||||
registerAllKbdbDataTools(server, env);
|
registerAllKbdbDataTools(server, env);
|
||||||
|
// issue #68: KBDB graph 查詢薄殼(kbdb_graph_neighbors,調 /q/:ns/graph_neighbors 同步查詢端點)
|
||||||
|
// 補齊 D17「KBDB MCP=RAG 套餐」第三模式:關鍵字/語義之外的圖(關係遍歷)
|
||||||
|
registerAllKbdbGraphTools(server, env, orgNamespace);
|
||||||
// §7.8 P1 D2: whoami(與 CLI acr whoami 對齊,AI 不繞 CLI 自己 curl 猜帳號)
|
// §7.8 P1 D2: whoami(與 CLI acr whoami 對齊,AI 不繞 CLI 自己 curl 猜帳號)
|
||||||
registerWhoami(server, env, orgNamespace);
|
registerWhoami(server, env, orgNamespace);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
import type { Env } from "../../../src/types.js";
|
||||||
|
import {
|
||||||
|
registerGraphNeighbors,
|
||||||
|
GRAPH_NEIGHBORS_WORKFLOW,
|
||||||
|
} from "../../../src/tools/kbdb_graph.js";
|
||||||
|
|
||||||
|
// ── 假 McpServer:只攔 tool 註冊,抓出 handler 直接呼叫 ─────────────────────────
|
||||||
|
type ToolHandler = (args: Record<string, unknown>) => Promise<{
|
||||||
|
content: { type: string; text: string }[];
|
||||||
|
isError?: boolean;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
function makeServer() {
|
||||||
|
const tools = new Map<string, { description: string; handler: ToolHandler }>();
|
||||||
|
const server = {
|
||||||
|
tool(name: string, description: string, _schema: unknown, handler: ToolHandler) {
|
||||||
|
tools.set(name, { description, handler });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return { server: server as unknown as McpServer, tools };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 假 CYPHER_EXECUTOR binding:記錄請求、回預設 response ───────────────────────
|
||||||
|
function makeEnv(respond: (url: URL, init?: RequestInit) => Response) {
|
||||||
|
const calls: { url: URL; init?: RequestInit }[] = [];
|
||||||
|
const env = {
|
||||||
|
CYPHER_EXECUTOR: {
|
||||||
|
fetch: async (input: string, init?: RequestInit) => {
|
||||||
|
const url = new URL(input);
|
||||||
|
calls.push({ url, init });
|
||||||
|
return respond(url, init);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as Env;
|
||||||
|
return { env, calls };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseResult(r: { content: { text: string }[] }) {
|
||||||
|
return JSON.parse(r.content[0].text) as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("kbdb_graph_neighbors: registration", () => {
|
||||||
|
it("registers under kbdb_* prefix (D17 KBDB MCP boundary)", () => {
|
||||||
|
const { server, tools } = makeServer();
|
||||||
|
const { env } = makeEnv(() => new Response("{}"));
|
||||||
|
registerGraphNeighbors(server, env, "leo");
|
||||||
|
expect(tools.has("kbdb_graph_neighbors")).toBe(true);
|
||||||
|
expect(tools.get("kbdb_graph_neighbors")!.description).toContain("graph");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("kbdb_graph_neighbors: request shape", () => {
|
||||||
|
it("hits GET /q/:ns/graph_neighbors with workflow input shape", async () => {
|
||||||
|
const { server, tools } = makeServer();
|
||||||
|
const { env, calls } = makeEnv(
|
||||||
|
() =>
|
||||||
|
new Response(
|
||||||
|
JSON.stringify({ success: true, start: "Arcrun", depth: 2, neighbors: [], count: 0 }),
|
||||||
|
{ status: 200 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
registerGraphNeighbors(server, env, "leo");
|
||||||
|
const res = await tools.get("kbdb_graph_neighbors")!.handler({
|
||||||
|
subject: "Arcrun",
|
||||||
|
depth: 2,
|
||||||
|
kbdb_base: "https://kbdb.example.com",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(calls).toHaveLength(1);
|
||||||
|
const url = calls[0].url;
|
||||||
|
expect(url.pathname).toBe(`/q/leo/${GRAPH_NEIGHBORS_WORKFLOW}`);
|
||||||
|
// workflow input 形狀(registry/examples/graph-neighbors/workflow.yaml)
|
||||||
|
expect(url.searchParams.get("node")).toBe("Arcrun");
|
||||||
|
expect(url.searchParams.get("depth")).toBe("2");
|
||||||
|
expect(url.searchParams.get("template")).toBe("graph_triplet"); // 預設
|
||||||
|
expect(url.searchParams.get("namespace")).toBe("leo"); // orgNamespace 注入
|
||||||
|
expect(url.searchParams.get("kbdb_base")).toBe("https://kbdb.example.com");
|
||||||
|
expect(url.searchParams.get("directed")).toBeNull(); // 預設無向 → 不帶
|
||||||
|
|
||||||
|
const body = parseResult(res);
|
||||||
|
expect(body.ok).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("depth defaults to 1, directed=true is forwarded, template overridable", async () => {
|
||||||
|
const { server, tools } = makeServer();
|
||||||
|
const { env, calls } = makeEnv(
|
||||||
|
() => new Response(JSON.stringify({ success: true, neighbors: [], count: 0 })),
|
||||||
|
);
|
||||||
|
registerGraphNeighbors(server, env, "leo");
|
||||||
|
await tools.get("kbdb_graph_neighbors")!.handler({
|
||||||
|
subject: "A",
|
||||||
|
kbdb_base: "https://kbdb.example.com",
|
||||||
|
template: "my_triplet",
|
||||||
|
directed: true,
|
||||||
|
});
|
||||||
|
const url = calls[0].url;
|
||||||
|
expect(url.searchParams.get("depth")).toBe("1");
|
||||||
|
expect(url.searchParams.get("directed")).toBe("true");
|
||||||
|
expect(url.searchParams.get("template")).toBe("my_triplet");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("kbdb_graph_neighbors: honest errors (鐵律:workflow 沒裝不 crash)", () => {
|
||||||
|
it("404 → workflow_not_installed with install hints, not a crash", async () => {
|
||||||
|
const { server, tools } = makeServer();
|
||||||
|
const { env } = makeEnv(
|
||||||
|
() => new Response(JSON.stringify({ error: '找不到 workflow "graph_neighbors"' }), { status: 404 }),
|
||||||
|
);
|
||||||
|
registerGraphNeighbors(server, env, "leo");
|
||||||
|
const res = await tools.get("kbdb_graph_neighbors")!.handler({
|
||||||
|
subject: "A",
|
||||||
|
kbdb_base: "https://kbdb.example.com",
|
||||||
|
});
|
||||||
|
expect(res.isError).toBe(true);
|
||||||
|
const body = parseResult(res);
|
||||||
|
expect(body.error_code).toBe("workflow_not_installed");
|
||||||
|
expect(JSON.stringify(body.next_actions)).toContain("graph-neighbors/workflow.yaml");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("500 execution failure → graph_query_failed, error passed through", async () => {
|
||||||
|
const { server, tools } = makeServer();
|
||||||
|
const { env } = makeEnv(
|
||||||
|
() => new Response(JSON.stringify({ success: false, error: "boom", trace: [] }), { status: 500 }),
|
||||||
|
);
|
||||||
|
registerGraphNeighbors(server, env, "leo");
|
||||||
|
const res = await tools.get("kbdb_graph_neighbors")!.handler({
|
||||||
|
subject: "A",
|
||||||
|
kbdb_base: "https://kbdb.example.com",
|
||||||
|
});
|
||||||
|
expect(res.isError).toBe(true);
|
||||||
|
const body = parseResult(res);
|
||||||
|
expect(body.error_code).toBe("graph_query_failed");
|
||||||
|
expect(String(body.human_message)).toContain("boom");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("HTTP 200 but workflow-level success:false → not faked as success", async () => {
|
||||||
|
const { server, tools } = makeServer();
|
||||||
|
const { env } = makeEnv(
|
||||||
|
() =>
|
||||||
|
new Response(JSON.stringify({ success: false, error: "graph_neighbors 缺 start(node)參數" }), {
|
||||||
|
status: 200,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
registerGraphNeighbors(server, env, "leo");
|
||||||
|
const res = await tools.get("kbdb_graph_neighbors")!.handler({
|
||||||
|
subject: "A",
|
||||||
|
kbdb_base: "https://kbdb.example.com",
|
||||||
|
});
|
||||||
|
expect(res.isError).toBe(true);
|
||||||
|
const body = parseResult(res);
|
||||||
|
expect(body.error_code).toBe("graph_query_failed");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("empty orgNamespace → no_namespace error, no fetch made", async () => {
|
||||||
|
const { server, tools } = makeServer();
|
||||||
|
const { env, calls } = makeEnv(() => new Response("{}"));
|
||||||
|
registerGraphNeighbors(server, env, "");
|
||||||
|
const res = await tools.get("kbdb_graph_neighbors")!.handler({
|
||||||
|
subject: "A",
|
||||||
|
kbdb_base: "https://kbdb.example.com",
|
||||||
|
});
|
||||||
|
expect(res.isError).toBe(true);
|
||||||
|
expect(parseResult(res).error_code).toBe("no_namespace");
|
||||||
|
expect(calls).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -3,7 +3,7 @@ name: status
|
|||||||
description: 當前進度、進行中 Phase、已知問題、下一步(動態文件,每 session 更新)
|
description: 當前進度、進行中 Phase、已知問題、下一步(動態文件,每 session 更新)
|
||||||
metadata:
|
metadata:
|
||||||
type: project
|
type: project
|
||||||
last_updated: 2026-07-14
|
last_updated: 2026-07-19
|
||||||
---
|
---
|
||||||
|
|
||||||
# 當前進度(動態)
|
# 當前進度(動態)
|
||||||
@@ -15,6 +15,11 @@ metadata:
|
|||||||
|
|
||||||
## 📍 當前位置
|
## 📍 當前位置
|
||||||
|
|
||||||
|
> **2026-07-19(#68 MCP graph tool)**:`kbdb_graph_neighbors` PR 已開(分支
|
||||||
|
> `feat/mcp-graph-neighbors-tool`),等審+gated 部署(merge 後需 leo 閘 redeploy arcrun-mcp)。
|
||||||
|
> 薄殼:MCP 新 tool 調 `/q/:ns/graph_neighbors` 同步查詢端點(#28 地基),補齊 D17 KBDB MCP
|
||||||
|
> 三模式(關鍵字/語義/圖)。graph_traverse 未加(repo 內無該 workflow 定義可對齊,不猜)。
|
||||||
|
>
|
||||||
> **2026-07-19 本 session(bugfix:kbdb search 兩缺口 #66/#67,分支
|
> **2026-07-19 本 session(bugfix:kbdb search 兩缺口 #66/#67,分支
|
||||||
> `fix/search-source-filter-and-semantic-threshold`,雲端總管交辦)**:
|
> `fix/search-source-filter-and-semantic-threshold`,雲端總管交辦)**:
|
||||||
> - **#66/#67 修復 PR 已開(雲端總管交辦),等審+gated 部署**。
|
> - **#66/#67 修復 PR 已開(雲端總管交辦),等審+gated 部署**。
|
||||||
|
|||||||
Reference in New Issue
Block a user