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>
160 lines
11 KiB
TypeScript
160 lines
11 KiB
TypeScript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
||
import { registerAllTools } from "./tools/registry.js";
|
||
import { buildLibraryMapInstructions } from "./lib/library-map.js";
|
||
import type { KnowledgeIdentity } from "./lib/portal-client.js";
|
||
import { Env } from "./types.js";
|
||
|
||
/**
|
||
* 【這條連線上有主人的知識庫】——**靜態、必定出現**的指路段(2026-08-13,leo)。
|
||
*
|
||
* leo 原話:「它應該是**你的知識來源**⋯⋯**沒有它你是瞎的**,從你對 Arcrun 的認知就知道
|
||
* 你的內建 Memory 是沒用的。」
|
||
*
|
||
* 為什麼要獨立成一段、而且**不准依賴任何查詢**:
|
||
* 這條連線本來就查得到答案——`kbdb_search(q="Arcrun 是什麼")` 當天實測回 33 筆真答案。
|
||
* 但同一天,一個連著這條 MCP 的 session 為了回答同一題,去讀了 682 行原始碼——
|
||
* **因為它不知道這裡有知識庫可查**。instructions 裡唯一提到知識的,是下面那段【藏書地圖】,
|
||
* 而那段是**選配的**(1500ms 逾時、失敗回 null);那條 portal 連線就沒收到它。
|
||
* ⇒ **「這裡有知識庫、怎麼查」這句話本身被綁在一個會無聲消失的段落上,才是 AI 開場全瞎的成因。**
|
||
* 所以它跟下面的 Arcrun 指路段同級:靜態常數,KBDB 掛了、地圖抓不到,它照樣出現。
|
||
*/
|
||
const KNOWLEDGE_FIRST = [
|
||
"【這條連線上有主人的知識庫——先查它,再查別的】",
|
||
"",
|
||
"這條 MCP 連線後面接著一個 **KBDB 知識庫**:這台實例的主人長期累積的筆記、決策、",
|
||
"踩過的坑、專案現況、skill 與工作流紀錄,都在裡面。**你不是從零開始的**——",
|
||
"你對這些專案的內建印象多半是錯的或過時的,庫裡那份才是主人認的版本。",
|
||
"",
|
||
"🔴 **有人問你「X 是什麼/為什麼這樣做/之前怎麼決定的/現在做到哪」——",
|
||
"你的第一個動作是 `kbdb_search`,不是 grep 原始碼、不是上網搜、不是回答「我不知道」。**",
|
||
"",
|
||
'- `kbdb_search({ q: "Arcrun 是什麼" })` — 關鍵字查(預設 `mode:\'keyword\'`,基本盤永遠可用)。',
|
||
" 換幾組講法再放棄;想要語義相似度用 `mode:'semantic'`。**這一支是你的第一站。**",
|
||
"- `kbdb_get_map()` — 不知道該進哪個庫時先看藏書地圖(下面若有【藏書地圖】就是它的快照)。",
|
||
'- `kbdb_graph_neighbors({ subject: "Arcrun" })` — 查某個東西跟誰有關係(三元組遍歷)。',
|
||
"- `kbdb_list_templates` / `kbdb_query` — 按 template 取整批結構化資料。",
|
||
"",
|
||
"🔴 **這三件事不可以講成同一句**(講成同一句就是在騙人):",
|
||
"① 「知識庫裡沒有」 ② 「我沒查」 ③ 「地圖沒取到/某庫顯示 0」。",
|
||
"查過真的沒有 → 明說「知識庫裡查不到,以下是我從原始碼/網路推的」,再去讀 code 或上網。",
|
||
"**沒查就回答=拿你的猜測冒充主人的知識,那是這條連線上最嚴重的錯。**",
|
||
"",
|
||
"🔴 **地圖是索引,不是庫存清單**:某庫顯示 `0 triplets`、或下面整段【藏書地圖】沒出現,",
|
||
"都**不代表**沒有知識(可能只是還沒重算、或這次沒抓到)。要知道有沒有,只有一個方法:`kbdb_search` 查過。",
|
||
"同理,任何工具回 401/連不上/沒權限,那是**讀不到**,不是**不存在**——照它給的 next_actions 修,",
|
||
"別把它改口講成「這裡沒有」(`arcrun_get_skill` 曾把 KBDB 的 401 講成「skill 不存在」,就是這個病)。",
|
||
].join("\n");
|
||
|
||
/** 地圖沒拿到時的**明講**(不可靜默):「沒取到」和「這裡沒有知識」不可以長得一樣。 */
|
||
const MAP_UNAVAILABLE_NOTE = [
|
||
"【藏書地圖:這次沒取到】",
|
||
"地圖抓取逾時/回錯/或它回報的清單是空的(也可能只是還沒重算過)。",
|
||
"🔴 **這是「地圖沒拿到」,不是「這裡沒有知識」。** 上面那條規則照舊:",
|
||
"要知道庫裡有什麼,直接 `kbdb_search`;想再抓一次地圖呼叫 `kbdb_get_map()`(它會回報真正的原因)。",
|
||
].join("\n");
|
||
|
||
/** 舊 token(stale):拿不到地圖是**身分問題**,同樣要明講,並給可執行的修法。 */
|
||
const MAP_STALE_NOTE = [
|
||
"【藏書地圖:拿不到,因為這條連線是舊版簽發的 token】",
|
||
"這條連線的 token 沒帶登入者身分,`kbdb_*` 會回 `identity_missing`。",
|
||
"🔴 **這不代表知識庫是空的**——是這條連線還沒認得你。",
|
||
"仍然先呼叫一次 `kbdb_search` 確認錯誤碼;若真的是 `identity_missing`,",
|
||
"請使用者到 claude.ai → Settings → Connectors 把這個 connector 重新連線一次(重新輸入 Portal 帳密),",
|
||
"**不要改口說「查不到資料」或自己去猜答案。**",
|
||
].join("\n");
|
||
|
||
/**
|
||
* 組這條連線的 server instructions(`initialize` 時送出,**AI 沒辦法自己再要一次** ⇒ 必須一次到位)。
|
||
*
|
||
* 匯出給測試:三種情境(地圖抓得到/抓不到/舊 token)各印一份完整字串,逐份確認
|
||
* 「一個什麼都不知道的 AI 讀完,下一個動作會不會是去查知識庫」。
|
||
*/
|
||
export async function buildServerInstructions(
|
||
env: Env,
|
||
identity: KnowledgeIdentity,
|
||
): Promise<string> {
|
||
// library-map SDD M4(design §4/§6):連線時把全館藏書地圖嵌進 server instructions,
|
||
// session 一開就知道館裡有哪些庫(push 零查詢)。builder 內建 timeout+isolate TTL 快取
|
||
//(選型理由見 lib/library-map.ts 檔頭);任何失敗回 null → 絕不擋 MCP 連線(鐵律)。
|
||
//
|
||
// 🔴 2026-08-12:以帳密連線時**改用登入者的身分**組地圖——否則 instructions 會把
|
||
// 整個知識庫的庫名一次推給一個可能只有部分權限的帳號(地圖本身就是情報)。
|
||
// 快取也因此改成 per-session key(見 lib/library-map.ts)。
|
||
//
|
||
// 🔴 2026-08-13:失敗**不再靜默略過**。原本 null → 整段消失,於是「地圖沒取到」與
|
||
// 「這裡沒有知識」在 AI 眼裡長得一模一樣(Arcrun#109 同一族:保險拒絕了 vs 程式碼不存在,
|
||
// 畫面上都是沉默)。現在改成印一句明話。鐵律沒變——**失敗仍然不擋連線**,只是不再無聲。
|
||
const mapInstructions = await buildLibraryMapInstructions(env, identity);
|
||
|
||
// 2026-07-30(leo 問「人類說『幫我用 arcrun 寫 xxx』,Haiku 會知道要用這些資源嗎?
|
||
// 如果不會,要寫什麼在外面讓它一聽到就知道?」):
|
||
// 實測答案是**不會**——AI 只看到 41 個工具名,會自己猜(很可能直接跳到 push_workflow
|
||
// 瞎編,或去讀 registry/examples 那 8/13 引用不存在零件的壞範例)。
|
||
// instructions 是唯一「AI 一連上就必看」的欄位 ⇒ 開場就指路,不依賴任何查詢成功。
|
||
// ⚠️ 這段是靜態常數:即使 KBDB 掛了、藏書地圖抓不到,它也必須出現(鐵律:不擋連線)。
|
||
const startHere = [
|
||
"# Arcrun — 你已經配備了這套工具,別上網找",
|
||
"",
|
||
"**Arcrun 是什麼**:跑在 Cloudflare 上的工作流引擎(類 n8n)。你用 `>>` 寫「意圖」,",
|
||
"系統告訴你有哪些現成零件/recipe 可用,你只要填 payload——**不必自己寫程式**。",
|
||
"**你現在就有完整能力**:查零件、查 recipe、看實跑過的 workflow、部署、觸發、看執行紀錄。",
|
||
"⚠️ **不要上網搜 Arcrun 文件**(網路上沒有/會過時)。答案都在下面的工具裡。",
|
||
"",
|
||
"【先讀這裡】要在 Arcrun 上做任何事(用戶說「幫我用 Arcrun 做 X」),**照這個順序**:",
|
||
"",
|
||
"1. `arcrun_get_skill('write_intent_workflow')` — **必讀第一支**。",
|
||
" 教你用 `>>` 寫「意圖工作流」。你**不需要先知道有哪些零件**,先寫意圖。",
|
||
" ⚠️ 這支若回錯(401/連不上/`kbdb_unreachable`),那是**這條連線讀不到 KBDB**,",
|
||
" **不是 skill 不存在**——同一份內容用 `kbdb_search({ q: 'skill-write_intent_workflow' })` 撈得到。",
|
||
"2. `arcrun_whoami()` — 確認連到哪個帳號(勿自行 curl 猜帳號 URL)。",
|
||
"3. 把意圖丟 `POST /cypher/search` 或 `arcrun_validate_yaml` — 系統告訴你哪些零件存在。",
|
||
"4. 卡住/不知道該查什麼 → 先 `arcrun_list_skills()` **看這台實例真的有哪幾支**,再挑一支讀。",
|
||
" (2026-08-13 實測:不同實例 seed 的 skill 不一樣,有的實例只有兩支、連 `INDEX` 都沒有。",
|
||
" **不要照教材直接指名一個 slug** ——先列清單,或 `kbdb_search({ q: 'skill' })` 直接在庫裡找。)",
|
||
"5. 缺零件時:缺 API → 寫 recipe(`arcrun_recipe_push`);缺能力 → 投稿零件 PR。",
|
||
" 🔴 **不要因為查不到零件就改寫成 `code` 節點**——那叫「腹語術」(表面用 Arcrun、",
|
||
" 實際全寫 JS)。`code` 只用於局部整形(例:剝掉 LLM 回應的雜訊)。",
|
||
"",
|
||
"邊:`ON_SUCCESS`(成功往下)、`對每個 <變數>`(FOREACH)、",
|
||
"以及**條件分支**(2026-08-01 起引擎支援):",
|
||
"`ON_TRUE`/`ON_FALSE`(配 `if_control`)、`ON_BRANCH`+`branch:` 標籤",
|
||
"(配 `switch` 的每個 case/`try_catch` 的 try·catch)。",
|
||
"🔴 **需要判斷時用分支邊,不要寫 code 判斷**——查零件的回應會附 `branch_hint`",
|
||
"(哪些邊型+可照抄範例),照著接即可。",
|
||
"🔴 **分支跑完怎麼判斷成功**:看 `verdict`(`arcrun_get_execution_trace` 或",
|
||
"`GET /workflows/<name>/executions`)。**走 true 路時 false 路的節點不出現=正確行為**,",
|
||
"不是失敗——別因為「只有一條路有輸出」就以為壞掉而改寫成 code(2026-08-01 實撞)。",
|
||
"第一個節點固定是 `input`。",
|
||
].join("\n");
|
||
|
||
// 地圖那段永遠有東西可印:拿到 → 印地圖;沒拿到 → 印「沒拿到」,不是消失。
|
||
// stale 與「抓失敗」分開講,因為修法不同(前者要使用者重新連線,後者只是這次沒抓到)。
|
||
const mapSection =
|
||
mapInstructions ?? (identity.kind === "stale" ? MAP_STALE_NOTE : MAP_UNAVAILABLE_NOTE);
|
||
|
||
// 知識段排在 Arcrun 指路段之前:AI 最常被問的是「X 是什麼」,那題的正解是查庫,不是查零件。
|
||
return [KNOWLEDGE_FIRST, startHere, mapSection].join("\n\n---\n\n");
|
||
}
|
||
|
||
export async function handleMcpRequest(
|
||
request: Request,
|
||
env: Env,
|
||
orgNamespace: string,
|
||
partnerToken: string,
|
||
identity: KnowledgeIdentity,
|
||
): Promise<Response> {
|
||
const instructions = await buildServerInstructions(env, identity);
|
||
|
||
const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
||
const server = new McpServer(
|
||
{ name: "arcrun-mcp-server", version: "1.0.0" },
|
||
{ instructions },
|
||
);
|
||
|
||
registerAllTools(server, env, orgNamespace, partnerToken, identity);
|
||
await server.connect(transport);
|
||
|
||
return transport.handleRequest(request);
|
||
}
|