c2897ba68b
leo 2026-08-13:「它應該是**你的知識來源**⋯⋯**沒有它你是瞎的**,
從你對 Arcrun 的認知就知道你的內建 Memory 是沒用的。」
## 病灶一:指路的話被綁在一個會無聲消失的段落上
instructions 裡唯一提到「這裡有知識可查」的,是 buildLibraryMapInstructions
拼上去的【藏書地圖】——而那段是**選配的**(1500ms 逾時、catch 回 null、失敗無聲)。
必定出現的那段靜態文字(startHere)從頭到尾只講工作流、零件、recipe、邊的語法,
`kbdb_search`/`kbdb_get_map` 一個字都沒有。
實害:同一天,一條 portal 連線的 session 為了回答「Arcrun 是什麼」去讀了 682 行
原始碼——而 `kbdb_search(q="Arcrun 是什麼")` 當下回得出 33 筆真答案。
修法:
- 新增 `KNOWLEDGE_FIRST` 靜態段(與 startHere 同級,KBDB 掛了照樣出現),
排在最前面:這條連線後面有主人的知識庫、第一個動作是 kbdb_search、
工具怎麼呼叫、以及「沒查到/沒查/地圖沒取到」三件事不可以講成同一句。
- 地圖失敗**不再靜默**:拿不到就印【藏書地圖:這次沒取到】,
stale token 另印身分版(要使用者重新連線)——「沒取到」與「這裡沒有知識」
不可以長得一樣(Arcrun#109 同族)。鐵律不變:失敗仍然不擋連線。
- 抽出 `buildServerInstructions(env, identity)` 供測試印出完整字串。
## 病灶二:一個 401 被逐字翻譯成「不存在」
`arcrun_get_skill('write_intent_workflow')` 回「skill ... 不存在」,
但 `kbdb_search` 撈得到 `page_name: "skill-write_intent_workflow"`
(entry_type: agent-skill、source: installer-seed)——**與本工具查的鍵逐字相符**。
真兇是 `kbdbGetByPageName` 的 `if (!resp.ok) return null;`。
而 401 的來源是該實例的 arcrun-mcp 沒有 KBDB_INTERNAL_TOKEN
(kbdb-client 沒 token 就匿名送出)=部署面的事,但這支 code 把它講成了假話。
修法:
- 非 2xx 一律拋 KbdbAccessError(帶真 status)→ kbdb_unauthorized/kbdb_unreachable,
訊息明說「這是讀不到,不是不存在」,並交出還走得通的那條路(kbdb_search)。
- `not_found` 只留給「KBDB 正常回應但真的沒這張卡」,並說明是「這台實例沒 seed」。
- 五支工具吃 identity:stale → identity_missing;portal 撞 401 時額外說明
「是這批工具還走服務憑據,不是你的帳號讀不到」。
- instructions 步驟 4 不再指名 `arcrun_get_skill('INDEX')`(leo21c 實測根本沒 seed
這支)——改成先 `arcrun_list_skills()` 看這台實例真的有哪幾支。
## 驗
- `mcp` 測試 130 passed(新增 mcp-instructions 7 項+skills-examples-honesty 10 項),tsc exit 0。
- 三種情境(地圖抓得到/抓不到/舊 token)各印一份完整 instructions 實測,
三份的開場都指向 kbdb_search。
⚠️ 未部署(部署是 leo 的手動閘)。上線後才會到用戶手上。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
414 lines
18 KiB
TypeScript
414 lines
18 KiB
TypeScript
/**
|
||
* Skills + Examples lookup MCP tools — LI SDD M3.2 / M3.4
|
||
*
|
||
* 對應 docs/3-specs/llm-interface/ Milestone 3.2 + 3.4。
|
||
*
|
||
* - arcrun_list_skills — 列 KBDB entry_type=agent-skill 全部
|
||
* - arcrun_get_skill — 用 slug 拿 skill markdown 全文
|
||
* - arcrun_list_examples — 列 KBDB entry_type=workflow-example 全部
|
||
* - arcrun_get_example — 用 slug 拿 example yaml + description + tags
|
||
* - 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=Y(base 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";
|
||
import { toolName } from "../brand.js";
|
||
import { z } from "zod";
|
||
import type { Env } from "../types.js";
|
||
import { kbdbFetch } from "../lib/kbdb-client.js";
|
||
import { errorResponse, successResponse } from "../lib/cypher-client.js";
|
||
import { staleIdentityError, type KnowledgeIdentity } from "../lib/portal-client.js";
|
||
|
||
/**
|
||
* 🔴 2026-08-13:**「讀不到」不准長得像「不存在」**(Arcrun#100/#109 同一族)。
|
||
*
|
||
* 實撞(總管在 leo21c/portal-login 連線上親跑,且已對照證實):
|
||
* `arcrun_get_skill('write_intent_workflow')` → 「skill "write_intent_workflow" **不存在**」
|
||
* 但 `kbdb_search` 撈得到 `page_name: "skill-write_intent_workflow"`、
|
||
* `entry_type: "agent-skill"`、`source: "installer-seed"`——**與本工具查的鍵逐字相符**。
|
||
* 真兇:下面的 `kbdbGetByPageName` 舊版寫 `if (!resp.ok) return null;`
|
||
* ⇒ **一個 HTTP 401 被逐字翻譯成「不存在」**。
|
||
*
|
||
* 401 從哪來(不是這支 code 的錯,但這支 code 把它講成了假話):`kbdb-client.ts` 只在
|
||
* `env.KBDB_INTERNAL_TOKEN` 存在時才掛 Authorization ⇒ 沒設就匿名送出 ⇒ KBDB 回 401。
|
||
* 而該實例的 `arcrun-mcp` 很可能根本沒拿到那把 token(安裝器的 secret 迴圈漏了它)。
|
||
* ⇒ **修 token 是部署面的事(leo 的手);這支 code 該做的是把話講對。**
|
||
*
|
||
* 為什麼講對很重要:instructions 叫 AI「第一步先讀 skill」。它照做、拿到「不存在」,
|
||
* 於是結論是「這裡沒有 skill」⇒ 去猜、去 grep repo、去上網——**那正是「AI 是瞎的」的機械過程**。
|
||
*
|
||
* 現在:非 2xx 一律拋 `KbdbAccessError`(帶真的 status),由 `kbdbFailure()` 講成它自己
|
||
* (401/403=讀不到;5xx/連不上=連不上),並把**還走得通的那條路**(`kbdb_search`)交給 AI。
|
||
* `not_found` 只保留給「KBDB 正常回應、但真的沒有這張卡」。
|
||
*/
|
||
class KbdbAccessError extends Error {
|
||
constructor(
|
||
readonly status: number,
|
||
readonly what: string,
|
||
readonly detail?: string,
|
||
) {
|
||
super(`KBDB ${what} HTTP ${status}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* KBDB 讀取失敗 → 誠實的錯誤回覆。
|
||
*
|
||
* `searchHint` 是「同一份內容還能從哪裡拿」——這批 registry 卡片(skill/example)本身就住在
|
||
* KBDB entries 裡,而 `kbdb_search` 走的是**另一條**路(登入身分走 portal 資料面),
|
||
* 這條 401 時那條常常還活著(2026-08-13 實測即如此)。所以這不是安慰話,是真的可行的下一步。
|
||
*/
|
||
function kbdbFailure(e: unknown, searchHint: string, identity: KnowledgeIdentity) {
|
||
const portalNote =
|
||
identity.kind === "portal"
|
||
? "你這條是帳密登入的連線,但 skill/example 這批工具目前仍走**服務內部憑據**(還沒接上登入身分)——" +
|
||
"所以它讀不到,不代表你的帳號讀不到。"
|
||
: "";
|
||
if (e instanceof KbdbAccessError) {
|
||
const unauthorized = e.status === 401 || e.status === 403;
|
||
return errorResponse(
|
||
unauthorized ? "kbdb_unauthorized" : "kbdb_unreachable",
|
||
(unauthorized
|
||
? `讀不到 KBDB(HTTP ${e.status}:這條連線的憑據被拒或根本沒帶)。`
|
||
: `讀不到 KBDB(HTTP ${e.status})。`) +
|
||
"🔴 **這是「讀不到」,不是「不存在」**——內容還在庫裡,只是這條路被擋住了。" +
|
||
(portalNote ? ` ${portalNote}` : ""),
|
||
[
|
||
searchHint,
|
||
"kbdb_get_map() 看這台實例有哪些庫(那條走得通就更確定是這批工具的路壞了,不是庫空了)",
|
||
"🔴 不准把這個錯誤回報成「找不到/沒有這個 skill」——請照實說「KBDB 這條路讀不到」",
|
||
"持續失敗:告訴 leo 這台實例的 arcrun-mcp 少了 secret KBDB_INTERNAL_TOKEN",
|
||
],
|
||
e.detail,
|
||
);
|
||
}
|
||
return errorResponse(
|
||
"kbdb_unreachable",
|
||
`讀不到 KBDB:${e instanceof Error ? e.message : String(e)}。` +
|
||
"🔴 **這是「讀不到」,不是「不存在」**。" +
|
||
(portalNote ? ` ${portalNote}` : ""),
|
||
[searchHint, "稍後重試", "🔴 不准把它回報成「沒有這個 skill/example」"],
|
||
);
|
||
}
|
||
|
||
// 基本盤 entries row(與舊 v3 block 欄位 1:1,差別只在 type→entry_type)
|
||
interface KbdbBlock {
|
||
id: string;
|
||
page_name?: string | null;
|
||
content?: string | null;
|
||
entry_type?: string;
|
||
tags_json?: string;
|
||
metadata_json?: string | null;
|
||
source?: string | null;
|
||
updated_at?: number;
|
||
}
|
||
|
||
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 KbdbAccessError(resp.status, `list entry_type=${entryType}`, await resp.text().catch(() => ""));
|
||
}
|
||
const data = await resp.json<{ entries?: KbdbBlock[] }>();
|
||
return data.entries ?? [];
|
||
}
|
||
|
||
/**
|
||
* 依 page_name 取一張卡。
|
||
*
|
||
* 🔴 回 `null` **只代表「KBDB 好好回答了,而它說沒有這張卡」**。
|
||
* 讀不到(401/5xx/連不上)一律拋 `KbdbAccessError`——**絕不 return null**,
|
||
* 否則呼叫端會把它講成「不存在」(2026-08-13 的真實事故,見檔頭)。
|
||
*/
|
||
async function kbdbGetByPageName(env: Env, pageName: string): Promise<KbdbBlock | null> {
|
||
const resp = await kbdbFetch(env, `/entries?page_name=${encodeURIComponent(pageName)}&limit=1`);
|
||
if (!resp.ok) {
|
||
throw new KbdbAccessError(resp.status, `get page_name=${pageName}`, await resp.text().catch(() => ""));
|
||
}
|
||
const data = await resp.json<{ entries?: KbdbBlock[] }>();
|
||
return data.entries?.[0] ?? null;
|
||
}
|
||
|
||
function parseTags(tagsJson?: string): string[] {
|
||
if (!tagsJson) return [];
|
||
try {
|
||
const arr = JSON.parse(tagsJson);
|
||
return Array.isArray(arr) ? arr : [];
|
||
} catch {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
export function registerListSkills(server: McpServer, env: Env, identity: KnowledgeIdentity) {
|
||
server.tool(
|
||
toolName("list_skills"),
|
||
"列所有 agent-skill blocks(從 arcrun/registry/skills/ 同步進 KBDB)。每個 skill 是個 markdown playbook,描述 AI 面對 X 問題該怎麼想 + 該用哪個 example。回 [{slug, title, tags}]。call get_skill(slug) 拿完整內文。",
|
||
{
|
||
tag: z.string().optional().describe("optional 標籤過濾。如 'rag' / 'watcher' / 'debug'"),
|
||
},
|
||
async ({ tag }) => {
|
||
if (identity.kind === "stale") return staleIdentityError();
|
||
try {
|
||
const blocks = await kbdbList(env, "agent-skill", 100);
|
||
const skills = blocks
|
||
.map((b) => {
|
||
const tags = parseTags(b.tags_json);
|
||
let title = b.page_name?.replace(/^skill-/, "") ?? "(no title)";
|
||
try {
|
||
const meta = b.metadata_json ? JSON.parse(b.metadata_json) : null;
|
||
if (meta?.title) title = meta.title;
|
||
} catch {}
|
||
return {
|
||
slug: b.page_name?.replace(/^skill-/, "") ?? "",
|
||
page_name: b.page_name,
|
||
title,
|
||
tags,
|
||
chars: (b.content ?? "").length,
|
||
};
|
||
})
|
||
.filter((s) => !tag || s.tags.includes(`skill:${tag}`) || s.tags.includes(tag) || s.slug.includes(tag));
|
||
|
||
return successResponse(
|
||
{ count: skills.length, skills },
|
||
[
|
||
skills.length === 0
|
||
? "沒有 skill 命中。試 list_skills() 不帶 tag 看全部"
|
||
: "call arcrun_get_skill(slug) 拿單個 skill 完整 markdown",
|
||
// 誠實:這裡回的是**這台實例被 seed 進去的那幾支**,不是「全世界的 skill 目錄」。
|
||
// 上面清單沒有的名字(例如 'INDEX')就是這台沒有——別照舊教材去猜一個 slug。
|
||
"🔴 只用上面清單裡真的有的 slug;清單沒有=這台實例沒 seed 進去,不要硬猜名字",
|
||
],
|
||
);
|
||
} catch (e) {
|
||
return kbdbFailure(e, "改用 kbdb_search({ q: 'skill' }) 直接在知識庫裡找 skill 卡片(那條路走的是另一組憑據)", identity);
|
||
}
|
||
},
|
||
);
|
||
}
|
||
|
||
export function registerGetSkill(server: McpServer, env: Env, identity: KnowledgeIdentity) {
|
||
server.tool(
|
||
toolName("get_skill"),
|
||
"拿單一 agent-skill 完整 markdown playbook。slug 從 list_skills 取得。",
|
||
{
|
||
slug: z.string().describe("skill slug,例如 'build_watcher_workflow' / 'rag_with_arcrun'"),
|
||
},
|
||
async ({ slug }) => {
|
||
if (identity.kind === "stale") return staleIdentityError();
|
||
try {
|
||
const pageName = slug.startsWith("skill-") ? slug : `skill-${slug}`;
|
||
const block = await kbdbGetByPageName(env, pageName);
|
||
if (!block) {
|
||
// 走到這裡=KBDB **有正常回答**,而它說沒有這張卡(讀不到的情況上面已經拋出去了)。
|
||
return errorResponse(
|
||
"not_found",
|
||
`KBDB 正常回應,但沒有 page_name="${pageName}" 這張卡——這台實例沒有 seed 這支 skill。` +
|
||
"(不同實例 seed 的 skill 不一樣,別照舊教材假設某個名字一定在。)",
|
||
[
|
||
"call arcrun_list_skills() 看**這台實例真的有**哪幾支",
|
||
`kbdb_search({ q: '${slug}' }) 看內容是不是被存成別的名字`,
|
||
"確認拼字正確(不需要 'skill-' prefix)",
|
||
],
|
||
);
|
||
}
|
||
return successResponse({
|
||
slug,
|
||
page_name: block.page_name,
|
||
content: block.content,
|
||
tags: parseTags(block.tags_json),
|
||
});
|
||
} catch (e) {
|
||
return kbdbFailure(
|
||
e,
|
||
`改用 kbdb_search({ q: 'skill-${slug}' }) 撈同一張卡片(skill 就住在知識庫的 entries 裡,那條路走另一組憑據)`,
|
||
identity,
|
||
);
|
||
}
|
||
},
|
||
);
|
||
}
|
||
|
||
export function registerListExamples(server: McpServer, env: Env, identity: KnowledgeIdentity) {
|
||
server.tool(
|
||
toolName("list_examples"),
|
||
"列所有 workflow-example blocks(從 arcrun/registry/examples/ 同步進 KBDB)。每個 example 是可直接 push 的 workflow YAML 範本 + description。回 [{slug, tags}]。call get_example / search_examples 拿細節。",
|
||
{
|
||
tag: z.string().optional().describe("optional 標籤過濾。如 'rag' / 'cron' / 'llm' / 'webhook'"),
|
||
},
|
||
async ({ tag }) => {
|
||
if (identity.kind === "stale") return staleIdentityError();
|
||
try {
|
||
const blocks = await kbdbList(env, "workflow-example", 200);
|
||
const examples = blocks
|
||
.map((b) => {
|
||
const tags = parseTags(b.tags_json);
|
||
return {
|
||
slug: b.page_name?.replace(/^example-/, "") ?? "",
|
||
page_name: b.page_name,
|
||
tags,
|
||
chars: (b.content ?? "").length,
|
||
};
|
||
})
|
||
.filter((e) => !tag || e.tags.includes(tag) || e.tags.includes(`example:${tag}`) || e.slug.includes(tag));
|
||
|
||
return successResponse(
|
||
{ count: examples.length, examples },
|
||
[
|
||
examples.length === 0
|
||
? "沒有 example 命中。試 list_examples() 不帶 tag 看全部"
|
||
: "call arcrun_get_example(slug) 拿單個 YAML + description",
|
||
],
|
||
);
|
||
} catch (e) {
|
||
return kbdbFailure(e, "改用 kbdb_search({ q: 'example' }) 直接在知識庫裡找 example 卡片(那條路走另一組憑據)", identity);
|
||
}
|
||
},
|
||
);
|
||
}
|
||
|
||
export function registerGetExample(server: McpServer, env: Env, identity: KnowledgeIdentity) {
|
||
server.tool(
|
||
toolName("get_example"),
|
||
"拿單一 workflow-example 完整 YAML + description。slug 從 list_examples / search_examples 取得。可直接拿 YAML 改成你自己的 → push。",
|
||
{
|
||
slug: z.string().describe("example slug,例如 'rag-search-answer' / 'cron-watcher'"),
|
||
},
|
||
async ({ slug }) => {
|
||
if (identity.kind === "stale") return staleIdentityError();
|
||
try {
|
||
const pageName = slug.startsWith("example-") ? slug : `example-${slug}`;
|
||
const block = await kbdbGetByPageName(env, pageName);
|
||
if (!block) {
|
||
// KBDB 好好回答了,而它說沒有這張卡(讀不到的情況已在 kbdbGetByPageName 拋出)。
|
||
return errorResponse(
|
||
"not_found",
|
||
`KBDB 正常回應,但沒有 page_name="${pageName}" 這張卡——這台實例沒 seed 這個 example。`,
|
||
[
|
||
"call arcrun_list_examples() 看**這台實例真的有**哪些 slug",
|
||
"或 arcrun_search_examples(use_case) 用關鍵字找",
|
||
`kbdb_search({ q: '${slug}' }) 看內容是不是被存成別的名字`,
|
||
],
|
||
);
|
||
}
|
||
|
||
let description_md = "";
|
||
try {
|
||
const meta = block.metadata_json ? JSON.parse(block.metadata_json) : null;
|
||
description_md = meta?.description_md ?? "";
|
||
} catch {}
|
||
|
||
return successResponse({
|
||
slug,
|
||
page_name: block.page_name,
|
||
workflow_yaml: block.content,
|
||
description_md,
|
||
tags: parseTags(block.tags_json),
|
||
}, [
|
||
"拿 workflow_yaml 改成你自己的 → call arcrun_push_workflow",
|
||
"看 description_md 了解設計意圖 / 改造方向",
|
||
]);
|
||
} catch (e) {
|
||
return kbdbFailure(
|
||
e,
|
||
`改用 kbdb_search({ q: 'example-${slug}' }) 撈同一張卡片(example 就住在知識庫的 entries 裡)`,
|
||
identity,
|
||
);
|
||
}
|
||
},
|
||
);
|
||
}
|
||
|
||
export function registerSearchExamples(server: McpServer, env: Env, identity: KnowledgeIdentity) {
|
||
server.tool(
|
||
toolName("search_examples"),
|
||
"用 use case 關鍵字搜 workflow examples,回最相關 N 個。" +
|
||
"注意:基本盤目前是 D1 LIKE 關鍵字搜尋(非語義 embedding;語義是 kbdb-base Phase 1 的 embed 模組,尚未上)。" +
|
||
"→ 用具體詞('email'、'cron'、'rag')比整句自然語言命中率高。也會比對 slug/tag。",
|
||
{
|
||
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 }) => {
|
||
if (identity.kind === "stale") return staleIdentityError();
|
||
try {
|
||
const k = top_k ?? 5;
|
||
const q = query.trim();
|
||
|
||
// 基本盤無語義 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 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 命中 +1;slug/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: q },
|
||
[
|
||
"關鍵字沒命中(基本盤是 LIKE 非語義,換更具體/不同的詞再試)",
|
||
"改用 arcrun_list_examples(tag='...') 走 tag 過濾",
|
||
"或 arcrun_list_examples() 看全部清單自己挑",
|
||
],
|
||
);
|
||
}
|
||
|
||
return successResponse(
|
||
{ count: examples.length, examples, query: q, search_mode: "keyword" },
|
||
[
|
||
"call arcrun_get_example(slug) 拿完整 YAML",
|
||
"score 高 = 關鍵字命中越多(slug/tag 命中加權)",
|
||
"search_mode:keyword — 基本盤無語義,命中靠字面;換具體詞可改善",
|
||
],
|
||
);
|
||
} catch (e) {
|
||
return kbdbFailure(e, `改用 kbdb_search({ q: '${query.trim()}' }) 直接查知識庫(那條路走另一組憑據)`, identity);
|
||
}
|
||
},
|
||
);
|
||
}
|
||
|
||
export function registerAllSkillExampleTools(server: McpServer, env: Env, identity: KnowledgeIdentity) {
|
||
registerListSkills(server, env, identity);
|
||
registerGetSkill(server, env, identity);
|
||
registerListExamples(server, env, identity);
|
||
registerGetExample(server, env, identity);
|
||
registerSearchExamples(server, env, identity);
|
||
}
|