fix(mcp,kbdb): LI M3 skills/examples 改打基本盤 /entries(修死 route 假綠)

skills/examples 整條從舊 v3 /blocks /search 改打 KBDB 基本盤 /entries
(entry_type 對應)。5 個已上線 MCP 工具原本對死 route 回 404(假綠),
現修正;sync-registry-to-kbdb.py 改打 /entries idempotent upsert。
誠實降級:基本盤無語義 search → LIKE 關鍵字(embed 模組上線再換回語義)。
順手 gitignore scripts/__pycache__/。

對應 kbdb-base tasks 9.4 / llm-interface M3.2/M3.4。mcp + kbdb tsc exit 0。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
uncle6me-web
2026-06-14 22:12:11 +08:00
parent ef1f789525
commit b9bf3ec3d5
3 changed files with 159 additions and 143 deletions
+64 -52
View File
@@ -1,18 +1,27 @@
/**
* Skills + Examples lookup MCP tools — LI SDD M3.2
* Skills + Examples lookup MCP tools — LI SDD M3.2 / M3.4
*
* 對應 .agents/specs/llm-interface/ Milestone 3.2 + 3.4。
* 對應 docs/3-specs/llm-interface/ Milestone 3.2 + 3.4。
*
* - arcrun_list_skills — 列 KBDB type=agent-skill 全部
* - arcrun_list_skills — 列 KBDB entry_type=agent-skill 全部
* - arcrun_get_skill — 用 slug 拿 skill markdown 全文
* - arcrun_list_examples — 列 KBDB type=workflow-example 全部
* - arcrun_list_examples — 列 KBDB entry_type=workflow-example 全部
* - arcrun_get_example — 用 slug 拿 example yaml + description + tags
* - arcrun_search_examples — 自然語言 use case → 命中相關 example
* - arcrun_search_examples — use case 關鍵字 → 命中相關 example
*
* Skills / examples 由 arcrun/scripts/sync-registry-to-kbdb.py 從
* arcrun/registry/{skills,examples} 同步進 KBDB。
*
* 直接走 KBDB service binding(既有 pattern),不經 cypher-executor。
*
* 2026-06-14 重寫:KBDB 降基本盤後(三表 entries/templates/records,無 v3 blocks 表、
* 無語義 search),原打 /blocks /search 的舊路徑全失效。改打基本盤 /entries:
* - entry_type 取代 blocks 的 type 欄(entries 表原生有 entry_type/page_name/tags_json/metadata_json
* - GET /blocks?type=X → GET /entries?entry_type=X
* - GET /blocks?page_name=Y → GET /entries?page_name=Ybase listEntries 加了 page_name 過濾)
* - POST /search(語義) → GET /entries/search?q=D1 LIKE 關鍵字,基本盤無語義;
* 誠實降級:search_examples 現在是「關鍵字」非「語義」。embed 模組(kbdb-base Phase 1
* 上線後只換內部、工具簽名不變。
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -21,29 +30,30 @@ import type { Env } from "../types.js";
import { kbdbFetch } from "../lib/kbdb-client.js";
import { errorResponse, successResponse } from "../lib/cypher-client.js";
// 基本盤 entries row(與舊 v3 block 欄位 1:1,差別只在 type→entry_type
interface KbdbBlock {
id: string;
page_name?: string | null;
content?: string | null;
type?: string;
entry_type?: string;
tags_json?: string;
metadata_json?: string | null;
source?: string | null;
updated_at?: number;
}
async function kbdbList(env: Env, type: string, limit = 100): Promise<KbdbBlock[]> {
const resp = await kbdbFetch(env, `/blocks?type=${type}&limit=${limit}`);
if (!resp.ok) throw new Error(`KBDB list type=${type} HTTP ${resp.status}`);
const data = await resp.json<{ blocks?: KbdbBlock[] }>();
return data.blocks ?? [];
async function kbdbList(env: Env, entryType: string, limit = 100): Promise<KbdbBlock[]> {
const resp = await kbdbFetch(env, `/entries?entry_type=${encodeURIComponent(entryType)}&limit=${limit}`);
if (!resp.ok) throw new Error(`KBDB list entry_type=${entryType} HTTP ${resp.status}`);
const data = await resp.json<{ entries?: KbdbBlock[] }>();
return data.entries ?? [];
}
async function kbdbGetByPageName(env: Env, pageName: string): Promise<KbdbBlock | null> {
const resp = await kbdbFetch(env, `/blocks?page_name=${encodeURIComponent(pageName)}&limit=1`);
const resp = await kbdbFetch(env, `/entries?page_name=${encodeURIComponent(pageName)}&limit=1`);
if (!resp.ok) return null;
const data = await resp.json<{ blocks?: KbdbBlock[] }>();
return data.blocks?.[0] ?? null;
const data = await resp.json<{ entries?: KbdbBlock[] }>();
return data.entries?.[0] ?? null;
}
function parseTags(tagsJson?: string): string[] {
@@ -234,54 +244,55 @@ export function registerGetExample(server: McpServer, env: Env) {
export function registerSearchExamples(server: McpServer, env: Env) {
server.tool(
"arcrun_search_examples",
"用自然語言 use case 搜 workflow examples回最相關 N 個。內部走 KBDB semantic searchembedding 比對)+ tag 過濾。",
"用 use case 關鍵字搜 workflow examples回最相關 N 個。" +
"注意:基本盤目前是 D1 LIKE 關鍵字搜尋(非語義 embedding;語義是 kbdb-base Phase 1 的 embed 模組,尚未上)。" +
"→ 用具體詞('email'、'cron'、'rag')比整句自然語言命中率高。也會比對 slug/tag。",
{
query: z.string().min(3).describe("use case 描述,例如 '每天早上發 email 摘要' / 'RAG 從文件回答問題'"),
query: z.string().min(2).describe("use case 關鍵字,例如 'email 摘要' / 'cron 排程' / 'rag'。基本盤是關鍵字非語義,用詞要具體"),
top_k: z.number().int().min(1).max(20).optional().describe("回幾個結果(預設 5"),
},
async ({ query, top_k }) => {
try {
const k = top_k ?? 5;
// KBDB /search 是 unified semantic search(既有),filter type=workflow-example
const resp = await kbdbFetch(env, `/search`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query,
topK: k * 3, // overfetch 後 filter type
}),
});
const q = query.trim();
if (!resp.ok) {
return errorResponse(
"fetch_failed",
`KBDB search HTTP ${resp.status}`,
["稍後重試", "改用 arcrun_list_examples(tag=...) 過濾"],
await resp.text().catch(() => ""),
);
}
// 基本盤無語義 search:撈全部 workflow-example,用 query 對 content/slug/tag 做關鍵字比對排序。
// examples 只有 ~10 筆,client 端過濾零負擔;embed 模組上線後可改打語義 search)
const blocks = await kbdbList(env, "workflow-example", 200);
const ql = q.toLowerCase();
const terms = ql.split(/\s+/).filter(Boolean);
const data = await resp.json<{ results?: Array<{ block?: KbdbBlock; score?: number }> }>();
const all = data.results ?? [];
const examples = all
.filter((r) => r.block?.type === "workflow-example")
.slice(0, k)
.map((r) => {
const b = r.block!;
return {
slug: b.page_name?.replace(/^example-/, "") ?? "",
page_name: b.page_name,
score: r.score,
tags: parseTags(b.tags_json),
preview: (b.content ?? "").slice(0, 200),
};
});
const scored = blocks
.map((b) => {
const slug = b.page_name?.replace(/^example-/, "") ?? "";
const tags = parseTags(b.tags_json);
const hay = `${slug} ${tags.join(" ")} ${(b.content ?? "")}`.toLowerCase();
// 每個 term 命中 +1slug/tag 命中額外加權
let score = 0;
for (const t of terms) {
if (hay.includes(t)) score += 1;
if (slug.toLowerCase().includes(t)) score += 2;
if (tags.some((tag) => tag.toLowerCase().includes(t))) score += 2;
}
return { b, slug, tags, score };
})
.filter((r) => r.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, k);
const examples = scored.map((r) => ({
slug: r.slug,
page_name: r.b.page_name,
score: r.score,
tags: r.tags,
preview: (r.b.content ?? "").slice(0, 200),
}));
if (examples.length === 0) {
return successResponse(
{ count: 0, examples: [], query },
{ count: 0, examples: [], query: q },
[
"沒命中。可能 KBDB /search 還在等 embedding 建好(剛 sync 完要 1-5 分鐘",
"關鍵字沒命中(基本盤是 LIKE 非語義,換更具體/不同的詞再試",
"改用 arcrun_list_examples(tag='...') 走 tag 過濾",
"或 arcrun_list_examples() 看全部清單自己挑",
],
@@ -289,10 +300,11 @@ export function registerSearchExamples(server: McpServer, env: Env) {
}
return successResponse(
{ count: examples.length, examples, query },
{ count: examples.length, examples, query: q, search_mode: "keyword" },
[
"call arcrun_get_example(slug) 拿完整 YAML",
"score 高 = 跟你 query 更相關",
"score 高 = 關鍵字命中越多(slug/tag 命中加權)",
"search_mode:keyword — 基本盤無語義,命中靠字面;換具體詞可改善",
],
);
} catch (e) {