Compare commits
56 Commits
21293568d5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d7a98f53e7 | |||
| cacaa33f7d | |||
| fda9d169d5 | |||
| 7b478378aa | |||
| ceb7638d74 | |||
| 9fa88065ab | |||
| 969acff325 | |||
| 87f6de7c9f | |||
| 576afa2364 | |||
| 730c2af245 | |||
| 9ba6ba75fc | |||
| aa49b4eeb2 | |||
| 5b22d569c9 | |||
| 30c9312c57 | |||
| 1616517ace | |||
| 7dc3e615e9 | |||
| ccd531863a | |||
| 537b520143 | |||
| 45169ab25e | |||
| 5dd1d1e7a9 | |||
| b8a5201e8a | |||
| 340b461f58 | |||
| 5318398849 | |||
| 9609924818 | |||
| 043e9acc31 | |||
| cfb759a774 | |||
| 73bdb90780 | |||
| 9ad95ee3a6 | |||
| 68f042cfd0 | |||
| 5d441aa7d3 | |||
| 43323b1a78 | |||
| 2b807da324 | |||
| 3f2e45f5dc | |||
| c543abaa8a | |||
| 49248b18c5 | |||
| 05e879faa7 | |||
| 0b72b818a0 | |||
| 7498911856 | |||
| 94d5452425 | |||
| 4b6cc159b8 | |||
| 8286c8afec | |||
| ca1ed2aaf6 | |||
| 88f308642e | |||
| c3856470ad | |||
| 1ccee0055e | |||
| 302e024804 | |||
| c2897ba68b | |||
| 614fe44812 | |||
| 9ff933ba98 | |||
| 2fcae722e7 | |||
| cac874601f | |||
| b223a69884 | |||
| fad5da0e17 | |||
| 13155a1d7b | |||
| bb548b6fdf | |||
| e05518a2b4 |
@@ -142,6 +142,37 @@ SDD 屬於架構決策,必須人確認。CC 不可以自行在 `docs/3-specs/`
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 第六類:租戶字串來源(Arcrun#108/#105 同族)
|
||||
|
||||
### 6.1 靜態租戶字串不得用於資料面過濾
|
||||
**知識資料面的 `owner_id`(三元組/entries/records/藏書地圖/工作流 KV)必須與寫入端同源。**
|
||||
寫入端只有一個真相源=使用者 `~/.arcrun/config.yaml` 的 `api_key`(=實例 namespace,
|
||||
CLI push/小幫手上傳/MCP 都用它)。讀取端拿另一份手抄的環境變數預設值 → 全被過濾掉。
|
||||
|
||||
實害:`portalTenant(env) = env.CONSOLE_TENANT || "leo"` 讓 leo 的 **1854 條三元組被過濾成 0 個庫**
|
||||
(#108);前一天 `ownerNamespace(env) = env.MCP_OWNER_NAMESPACE || "leo"` 是同一句話(#105)。
|
||||
|
||||
**規則**:
|
||||
1. `cypher-executor/src/lib/tenant.ts` 是租戶字串的**唯一產地**。
|
||||
`CONSOLE_TENANT` / `ARCRUN_NAMESPACE` 只能在該檔被讀取。
|
||||
2. 知識資料面用 `knowledgeOwner(env)`(回 `TenantId`),過濾一律經
|
||||
`ownerQuery()` / `ownerField()`——它們只吃 `TenantId`,`tsc` 就擋掉「隨手一個 string」。
|
||||
3. 帳號層用 `accountTenant(env)`(回 `string`,**刻意不是 TenantId**):帳號子 namespace
|
||||
`{tenant}::portal` 與 cypher 自己寫的設定用它,型別上不可能流進知識資料面。
|
||||
4. 身分解析路徑上**不准有字面預設值**。解析不到 → 丟 `TenantUnresolvedError`,
|
||||
誠實回「讀不到」(不是「你沒有」,#100 同一條)。
|
||||
|
||||
**機械強制**(規則存在但沒機制驗證=它會再犯第三次):
|
||||
- 出貨閘:`scripts/build-worker-artifacts.mjs` 編 tier2 成品前先掃,違規 → **編不出成品**。
|
||||
- 本機自查:`cd cypher-executor && npm run check:tenant`(`npm test` 也會先跑它)。
|
||||
- 規則本體:`cypher-executor/scripts/tenant-source-rules.mjs`(純函式);
|
||||
閘自己的測試:`cypher-executor/tests/tenant-gate.test.ts`(壞例子會擋+合法寫法零誤攔)。
|
||||
|
||||
> 尚未接上 PreToolUse hook(`.claude/hooks/` 為受保護檔案,需人類加入)。
|
||||
> 要加的話:檢查器已備妥 `--stdin <相對路徑>` 模式,可在寫入前擋。
|
||||
|
||||
## Hook Block 訊息格式
|
||||
|
||||
當 hook 擋住一個操作時,訊息格式統一為:
|
||||
|
||||
@@ -99,6 +99,38 @@ CLI / MCP / Python lib / JS lib 全是薄殼:只做「介面轉換 + 暴露」
|
||||
|
||||
---
|
||||
|
||||
## 3.6 自舉例外:能力該「只實作一次」,但不一定要是 HTTP API(2026-08-12 立)
|
||||
|
||||
> 立這條的原因:`Arcrun#97`(更新把使用者的工作流與登入弄不見)的修法一開始寫在
|
||||
> `cli/src/lib/resource-resolver.ts` ——**能力住在介面層,違反 §0**。
|
||||
> 後果不是理論:**安裝器(arcrun-rag)拿不到它,於是同一個 bug 只修了一半**,
|
||||
> 走 `acr` 的人有保護、走 `install.arcrun.dev` 的人沒有——**而所有真實用戶走後者**。
|
||||
> leo 2026-08-12:「**根本就不應該在 CLI,我要的是一個大家都可以用到的規則。**」
|
||||
|
||||
修法(PR #111)把它搬到 **`shared/resource-rule/`:一份零依賴 ESM**,
|
||||
`acr` 與安裝器共用。**它刻意不是 cypher 的 API 端點**,三個理由:
|
||||
|
||||
| 為什麼不放 API | 說明 |
|
||||
|---|---|
|
||||
| **自舉** | 這條規則要在「決定怎麼裝」的當下用得到,而安裝器的工作正是把 cypher 生出來。放進 cypher = 要先有雞才能有蛋。 |
|
||||
| **輸入是使用者自己的帳號狀態** | 判斷依據是使用者 CF 帳號上的綁定。送去平台託管的 worker 換答案 ⇒ ①「能不能安裝」綁在平台是否活著 ②使用者的帳號拓撲交給第三方。 |
|
||||
| **它根本不需要是服務** | 這是**純函式**,唯一的 IO 由呼叫端注入。**§0 要求「能力只實作一次」,不是「能力一定要是 HTTP」。** |
|
||||
|
||||
🔴 **所以本檔 §0 的正確讀法是**:能力**只准有一份**,且**不准住在任何單一介面裡**。
|
||||
「放 API」是達成它的**常見手段**,不是唯一手段。
|
||||
**判準仍然是那句口訣**:「這段邏輯換一個介面要不要重寫?」要 → 它是能力。
|
||||
|
||||
📌 **給下一個人**:看到 `shared/` 底下的純函式**不要「修正」成 API 端點**——
|
||||
先讀 `shared/resource-rule/README.md §2`,那裡記著評估過並否決的其他形態
|
||||
(共用 npm 套件=自舉問題換位置;做成零件=要用 TinyGo 重寫一次,那才是第二份實作)。
|
||||
|
||||
📌 **打包例外**:`acr` 是獨立 npm 套件,`npm pack` 打不進套件目錄外的檔案 ⇒
|
||||
`cli/` 下必須有一份**逐位元組副本**。那不是第二份實作——
|
||||
`scripts/sync-resource-rule.mjs --check` 一有漂移就 exit 1,且 `build`/`test` 都會先跑它
|
||||
(同 `cli/harness/` 的既有慣例)。**手改副本 = build 紅 = publish 擋下。**
|
||||
|
||||
---
|
||||
|
||||
## 4. 統一帳號來源(薄殼共用同一身份)
|
||||
|
||||
所有薄殼讀**同一份**身份設定:
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -3243,15 +3243,20 @@ var init_kbdb_proxy = __esm({
|
||||
const owner = tenant(c);
|
||||
if (!owner) return c.json(NEED_KEY, 401);
|
||||
const body = await c.req.json().catch(() => null);
|
||||
if (!body || !body.template || !body.values) {
|
||||
return c.json({ error: "template \u8207 values \u5FC5\u586B" }, 400);
|
||||
if (!body || !body.template || !body.values && !body.entry_ids) {
|
||||
return c.json({ error: "template \u5FC5\u586B\uFF0Cvalues \u8207 entry_ids \u81F3\u5C11\u8981\u6709\u4E00\u500B" }, 400);
|
||||
}
|
||||
const { base, headers } = kbdbBase(c.env);
|
||||
const res = await fetch(`${base}/records`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
// 強制以租戶身份隔離:忽略 caller 自帶 owner_id,一律用 header 身份(防跨租戶寫入)
|
||||
body: JSON.stringify({ template: body.template, values: body.values, owner_id: owner })
|
||||
body: JSON.stringify({
|
||||
template: body.template,
|
||||
...body.values ? { values: body.values } : {},
|
||||
...body.entry_ids ? { entry_ids: body.entry_ids } : {},
|
||||
owner_id: owner
|
||||
})
|
||||
});
|
||||
return new Response(res.body, { status: res.status, headers: { "Content-Type": "application/json" } });
|
||||
});
|
||||
@@ -3400,8 +3405,9 @@ function validateName(name) {
|
||||
function validSensitivity(s) {
|
||||
return s === "standard" || s === "high";
|
||||
}
|
||||
async function putWorkerSecret(env, secretRef, value) {
|
||||
if (!env.CF_SECRETS_API_TOKEN || !env.CF_ACCOUNT_ID) {
|
||||
async function putWorkerSecret(env, secretRef, value, tokenOverride) {
|
||||
const token = tokenOverride || env.CF_SECRETS_API_TOKEN;
|
||||
if (!token || !env.CF_ACCOUNT_ID) {
|
||||
throw new Error(
|
||||
"\u6B64 worker \u7F3A CF_SECRETS_API_TOKEN / CF_ACCOUNT_ID \u8A2D\u5B9A\uFF0C\u5BEB\u5165\u8DEF\u5F91\u672A\u5C31\u7DD2\uFF08\u898B credential-store-migration.md T3\uFF1Aacr init/update \u61C9\u78BA\u4FDD\u9019\u5169\u9805\u5C31\u7DD2\uFF09"
|
||||
);
|
||||
@@ -3410,7 +3416,7 @@ async function putWorkerSecret(env, secretRef, value) {
|
||||
const res = await fetch(url, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
Authorization: `Bearer ${env.CF_SECRETS_API_TOKEN}`,
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ name: secretRef, text: value, type: "secret_text" })
|
||||
@@ -3421,14 +3427,15 @@ async function putWorkerSecret(env, secretRef, value) {
|
||||
throw new Error(`CF Workers Secrets \u5BEB\u5165\u5931\u6557\uFF1A${detail}`);
|
||||
}
|
||||
}
|
||||
async function deleteWorkerSecret(env, secretRef) {
|
||||
if (!env.CF_SECRETS_API_TOKEN || !env.CF_ACCOUNT_ID) {
|
||||
async function deleteWorkerSecret(env, secretRef, tokenOverride) {
|
||||
const token = tokenOverride || env.CF_SECRETS_API_TOKEN;
|
||||
if (!token || !env.CF_ACCOUNT_ID) {
|
||||
throw new Error("\u6B64 worker \u7F3A CF_SECRETS_API_TOKEN / CF_ACCOUNT_ID \u8A2D\u5B9A\uFF0C\u522A\u9664\u8DEF\u5F91\u672A\u5C31\u7DD2");
|
||||
}
|
||||
const url = `https://api.cloudflare.com/client/v4/accounts/${env.CF_ACCOUNT_ID}/workers/scripts/${CYPHER_SCRIPT_NAME}/secrets/${secretRef}`;
|
||||
const res = await fetch(url, {
|
||||
method: "DELETE",
|
||||
headers: { Authorization: `Bearer ${env.CF_SECRETS_API_TOKEN}` }
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
if (res.status === 404) return;
|
||||
const body = await res.json().catch(() => null);
|
||||
@@ -3789,6 +3796,9 @@ async function resolveCredentialRefs(data, env, apiKey) {
|
||||
if (names.size === 0) return data;
|
||||
const nameList = [...names];
|
||||
const resolvedSecrets = await resolveSecretsFromNewHome(env, apiKey, nameList);
|
||||
if (nameList.every((n) => Object.prototype.hasOwnProperty.call(resolvedSecrets, n))) {
|
||||
return replaceCredentialRefs(data, resolvedSecrets);
|
||||
}
|
||||
const url = wasmWorkerUrl("auth_static_key", env.WORKER_SUBDOMAIN);
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
@@ -9198,11 +9208,8 @@ function shardIndex(name) {
|
||||
function shardNameOf(index) {
|
||||
return index === 0 ? AUTH_STORE_PREFIX : `${AUTH_STORE_PREFIX}_${index}`;
|
||||
}
|
||||
function authStorePresent(env) {
|
||||
return shardNames(env).length > 0 || overlay !== null && Date.now() - overlayAt < AUTH_OVERLAY_TTL_MS;
|
||||
}
|
||||
function authStoreWritable(env) {
|
||||
return Boolean(env.CF_SECRETS_API_TOKEN && env.CF_ACCOUNT_ID);
|
||||
function authStoreWritable(env, tokenOverride) {
|
||||
return Boolean((tokenOverride || env.CF_SECRETS_API_TOKEN) && env.CF_ACCOUNT_ID);
|
||||
}
|
||||
function readAuthStore(env) {
|
||||
if (overlay && Date.now() - overlayAt < AUTH_OVERLAY_TTL_MS) return overlay;
|
||||
@@ -9238,15 +9245,10 @@ function findAuthUserById(env, id) {
|
||||
function isAuthStoreId(recordId) {
|
||||
return recordId.startsWith(AUTH_ID_PREFIX);
|
||||
}
|
||||
function newAuthUserId() {
|
||||
const arr = new Uint8Array(12);
|
||||
crypto.getRandomValues(arr);
|
||||
return AUTH_ID_PREFIX + Array.from(arr).map((b) => b.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
async function writeAuthStore(env, data) {
|
||||
if (!authStoreWritable(env)) {
|
||||
async function writeAuthStore(env, data, tokenOverride) {
|
||||
if (!authStoreWritable(env, tokenOverride)) {
|
||||
throw new AuthStoreWriteError(
|
||||
"\u9019\u53F0\u5BE6\u4F8B\u9084\u4E0D\u80FD\u5BEB\u5165\u8A8D\u8B49\u5132\u5B58\uFF08\u7F3A CF_SECRETS_API_TOKEN / CF_ACCOUNT_ID\uFF09\u3002\u8A8D\u8B49\u5206\u96E2\u9700\u8981\u9019\u5169\u9805\u624D\u5BEB\u5F97\u9032 Workers Secrets\u2014\u2014\u8ACB\u91CD\u65B0\u57F7\u884C\u5B89\u88DD\uFF0F\u66F4\u65B0\u8B93\u5B83\u5C31\u7DD2\u3002"
|
||||
"\u9019\u53F0\u5BE6\u4F8B\u76EE\u524D\u5BEB\u4E0D\u9032\u8A8D\u8B49\u5132\u5B58\uFF08\u7F3A\u53EF\u7528\u7684 Cloudflare \u5BEB\u5165\u6191\u8B49\uFF1ACF_SECRETS_API_TOKEN\uFF09\u3002\u9019\u662F\u5E73\u53F0\u7AEF\u7684\u5DF2\u77E5\u9650\u5236\uFF0C\u4E0D\u662F\u4F60\u64CD\u4F5C\u932F\u8AA4\u2014\u2014\u76EE\u524D\u6C92\u6709\u4F60\u81EA\u5DF1\u5728\u756B\u9762\u4E0A\u80FD\u505A\u7684\u4E0B\u4E00\u6B65\uFF0C\u8ACB\u628A\u9019\u5247\u8A0A\u606F\u5B8C\u6574\u622A\u5716\uFF0F\u8907\u88FD\u7D66\u652F\u63F4\uFF0C\u4E26\u8A3B\u660E\u4F60\u525B\u624D\u5728\u505A\u4EC0\u9EBC\uFF08\u4F8B\u5982\uFF1A\u5B89\u88DD\u7CBE\u9748\u88E1\u5EFA\u7ACB\u7B2C\u4E00\u500B\u5E33\u865F\u3001\u4E8B\u5F8C\u65B0\u589E\u4F7F\u7528\u8005\u3001\u6216\u4FEE\u6539\u5BC6\u78BC\uFF09\uFF0C\u6703\u9700\u8981\u4EBA\u5DE5\u5354\u52A9\u6392\u9664\u3002"
|
||||
);
|
||||
}
|
||||
const shards = [];
|
||||
@@ -9269,10 +9271,10 @@ async function writeAuthStore(env, data) {
|
||||
}
|
||||
const existing = shardNames(env);
|
||||
for (let i = 0; i < shards.length; i++) {
|
||||
await putWorkerSecret(env, shardNameOf(i), shards[i]);
|
||||
await putWorkerSecret(env, shardNameOf(i), shards[i], tokenOverride);
|
||||
}
|
||||
for (const name of existing) {
|
||||
if (shardIndex(name) >= shards.length) await deleteWorkerSecret(env, name);
|
||||
if (shardIndex(name) >= shards.length) await deleteWorkerSecret(env, name, tokenOverride);
|
||||
}
|
||||
overlay = { version: 1, console: data.console ?? null, users: [...data.users] };
|
||||
overlayAt = Date.now();
|
||||
@@ -9319,31 +9321,30 @@ function unionStores(a, b) {
|
||||
}
|
||||
return { version: 1, console: a.console ?? b.console ?? null, users: [...byId.values()] };
|
||||
}
|
||||
async function mutateAuthStore(env, fn) {
|
||||
async function mutateAuthStore(env, fn, tokenOverride) {
|
||||
await hydrateFromAccelerator(env);
|
||||
const next = unionStores(readAuthStore(env), readAuthStoreFromEnv(env));
|
||||
await fn(next);
|
||||
await writeAuthStore(env, next);
|
||||
await writeAuthStore(env, next, tokenOverride);
|
||||
return next;
|
||||
}
|
||||
function authStoreStatus(env) {
|
||||
const data = readAuthStore(env);
|
||||
return {
|
||||
present: authStorePresent(env),
|
||||
writable: authStoreWritable(env),
|
||||
users: data.users.length,
|
||||
console_configured: Boolean(data.console),
|
||||
shards: shardNames(env).length
|
||||
};
|
||||
}
|
||||
|
||||
// cypher-executor/src/routes/health.ts
|
||||
var healthRouter = new Hono2();
|
||||
function authStoreStatus(env) {
|
||||
const legacy = readAuthStore(env);
|
||||
return {
|
||||
console: { home: "sessions-kv", writable: true, legacy_secrets_present: legacy.console !== null },
|
||||
portal_users: { home: "kbdb", writable: true, legacy_secrets_present: legacy.users.length > 0 }
|
||||
};
|
||||
}
|
||||
healthRouter.get("/health", (c) => {
|
||||
const bundleVersion = c.env.ARCRUN_BUNDLE_VERSION;
|
||||
const bundleCommit = c.env.ARCRUN_BUNDLE_COMMIT;
|
||||
return c.json({
|
||||
ok: true,
|
||||
...bundleVersion ? { bundle_version: bundleVersion } : {},
|
||||
...bundleCommit ? { bundle_commit: bundleCommit } : {},
|
||||
auth_store: authStoreStatus(c.env),
|
||||
// arcrun-rag#38/#69/#25(2026-08-11):安裝器判斷「要不要重推」只比 bundle_version——
|
||||
// 但這次要修的洞是「installer 從沒注入過 PORTAL_MAIL_RELAY_BASE」,跟 bundle 內容
|
||||
@@ -12555,6 +12556,45 @@ init_kbdb_proxy();
|
||||
|
||||
// cypher-executor/src/routes/console-auth.ts
|
||||
init_dist();
|
||||
|
||||
// cypher-executor/src/lib/tenant.ts
|
||||
var TenantUnresolvedError = class extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = "TenantUnresolvedError";
|
||||
}
|
||||
};
|
||||
function knowledgeOwner(env) {
|
||||
const injected = (env.ARCRUN_NAMESPACE ?? "").trim();
|
||||
if (injected) return injected;
|
||||
const legacy = (env.CONSOLE_TENANT ?? "").trim();
|
||||
if (legacy) return legacy;
|
||||
throw new TenantUnresolvedError(
|
||||
"\u9019\u500B\u90E8\u7F72\u6C92\u6709\u77E5\u8B58\u547D\u540D\u7A7A\u9593\uFF08ARCRUN_NAMESPACE / CONSOLE_TENANT \u90FD\u6C92\u8A2D\uFF09\u2014\u2014\u4E0D\u77E5\u9053\u8981\u53BB\u54EA\u4E00\u683C\u627E\u8CC7\u6599\u3002\u8ACB\u8DD1 `acr update` \u8B93\u5B83\u5F9E\u4F60\u7684 ~/.arcrun/config.yaml \u6CE8\u5165\u3002"
|
||||
);
|
||||
}
|
||||
function tenantFromApiKey(apiKey) {
|
||||
const key = (apiKey ?? "").trim();
|
||||
if (!key) throw new TenantUnresolvedError("\u7F3A\u5C11 X-Arcrun-API-Key\uFF0C\u7121\u6CD5\u6C7A\u5B9A\u67E5\u8A62\u7BC4\u570D");
|
||||
return key;
|
||||
}
|
||||
function accountTenant(env) {
|
||||
return env.CONSOLE_TENANT || "leo";
|
||||
}
|
||||
function ownerQuery(tenant2) {
|
||||
return `owner_id=${encodeURIComponent(tenant2)}`;
|
||||
}
|
||||
function ownerField(tenant2) {
|
||||
return tenant2;
|
||||
}
|
||||
function censusQueryAllTenants() {
|
||||
return "owner_id=";
|
||||
}
|
||||
function isOwnedBy(value, tenant2) {
|
||||
return typeof value === "string" && value === tenant2;
|
||||
}
|
||||
|
||||
// cypher-executor/src/routes/console-auth.ts
|
||||
var consoleAuthRouter = new Hono2();
|
||||
var CREDS_KEY = "console:credentials";
|
||||
var SESSION_PREFIX = "console_sess:";
|
||||
@@ -12581,38 +12621,38 @@ async function hashPassword(password, salt) {
|
||||
return h;
|
||||
}
|
||||
function tenantOf(c) {
|
||||
return c.env.CONSOLE_TENANT || "leo";
|
||||
return knowledgeOwner(c.env);
|
||||
}
|
||||
async function loadCredentials(env) {
|
||||
let fromStore = readAuthStore(env).console;
|
||||
if (!fromStore && await hydrateFromAccelerator(env)) {
|
||||
fromStore = readAuthStore(env).console;
|
||||
}
|
||||
if (fromStore) return { creds: fromStore, source: "secrets" };
|
||||
const raw2 = await env.SESSIONS_KV.get(CREDS_KEY);
|
||||
if (!raw2) return { creds: null, source: "none" };
|
||||
let legacy = null;
|
||||
try {
|
||||
legacy = JSON.parse(raw2);
|
||||
} catch {
|
||||
return { creds: null, source: "none" };
|
||||
if (raw2) {
|
||||
try {
|
||||
return { creds: JSON.parse(raw2), source: "kv" };
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
const legacy = readAuthStore(env).console;
|
||||
if (!legacy) return { creds: null, source: "none" };
|
||||
try {
|
||||
await mutateAuthStore(env, (data) => {
|
||||
if (!data.console) data.console = legacy;
|
||||
});
|
||||
await env.SESSIONS_KV.put(CREDS_KEY, JSON.stringify(legacy));
|
||||
} catch {
|
||||
}
|
||||
return { creds: legacy, source: "legacy-kv" };
|
||||
return { creds: legacy, source: "legacy-secrets" };
|
||||
}
|
||||
async function saveCredentials(env, record) {
|
||||
await mutateAuthStore(env, (data) => {
|
||||
data.console = record;
|
||||
});
|
||||
await env.SESSIONS_KV.put(CREDS_KEY, JSON.stringify(record));
|
||||
}
|
||||
function consoleAuthStoreStatus(env) {
|
||||
return {
|
||||
home: "sessions-kv",
|
||||
writable: true,
|
||||
// binding-based,只要 wrangler.toml 有這個 binding 就一定寫得進去
|
||||
legacy_secrets_present: readAuthStore(env).console !== null
|
||||
};
|
||||
}
|
||||
consoleAuthRouter.get("/console/auth-status", async (c) => {
|
||||
const { creds, source } = await loadCredentials(c.env);
|
||||
return c.json({ configured: !!creds, credentials_source: source, auth_store: authStoreStatus(c.env) });
|
||||
return c.json({ configured: !!creds, credentials_source: source, auth_store: consoleAuthStoreStatus(c.env) });
|
||||
});
|
||||
consoleAuthRouter.post("/console/setup", async (c) => {
|
||||
const { creds: existing } = await loadCredentials(c.env);
|
||||
@@ -12638,8 +12678,7 @@ consoleAuthRouter.post("/console/setup", async (c) => {
|
||||
try {
|
||||
await saveCredentials(c.env, record);
|
||||
} catch (e) {
|
||||
const msg = e instanceof AuthStoreWriteError ? e.message : String(e);
|
||||
return c.json({ error: `\u5E33\u5BC6\u6C92\u6709\u5B58\u8D77\u4F86\uFF1A${msg}`, code: "auth_store_not_writable" }, 502);
|
||||
return c.json({ error: `\u5E33\u5BC6\u6C92\u6709\u5B58\u8D77\u4F86\uFF1A${e instanceof Error ? e.message : String(e)}`, code: "auth_store_not_writable" }, 502);
|
||||
}
|
||||
const token = randomHex(32);
|
||||
await c.env.SESSIONS_KV.put(`${SESSION_PREFIX}${token}`, JSON.stringify({ created_at: Date.now() }), {
|
||||
@@ -12664,8 +12703,7 @@ consoleAuthRouter.post("/console/setup/reset", async (c) => {
|
||||
try {
|
||||
await saveCredentials(c.env, record);
|
||||
} catch (e) {
|
||||
const msg = e instanceof AuthStoreWriteError ? e.message : String(e);
|
||||
return c.json({ error: `\u65B0\u5E33\u5BC6\u6C92\u6709\u5B58\u8D77\u4F86\uFF1A${msg}`, code: "auth_store_not_writable" }, 502);
|
||||
return c.json({ error: `\u65B0\u5E33\u5BC6\u6C92\u6709\u5B58\u8D77\u4F86\uFF1A${e instanceof Error ? e.message : String(e)}`, code: "auth_store_not_writable" }, 502);
|
||||
}
|
||||
return c.json({ success: true });
|
||||
});
|
||||
@@ -12676,7 +12714,7 @@ consoleAuthRouter.post("/console/login", async (c) => {
|
||||
{
|
||||
error: "\u9019\u53F0\u5BE6\u4F8B\u9084\u6C92\u6709\u7BA1\u7406\u54E1\u5E33\u5BC6\uFF08\u6216\u8B80\u4E0D\u5230\uFF09\u2014\u2014\u4E0D\u662F\u5BC6\u78BC\u932F\u3002\u8ACB\u5148\u5B8C\u6210\u9996\u6B21\u8A2D\u5B9A\u3002",
|
||||
code: "auth_store_empty",
|
||||
auth_store: authStoreStatus(c.env)
|
||||
auth_store: consoleAuthStoreStatus(c.env)
|
||||
},
|
||||
400
|
||||
);
|
||||
@@ -12685,18 +12723,8 @@ consoleAuthRouter.post("/console/login", async (c) => {
|
||||
const email = (body?.email ?? "").trim().toLowerCase();
|
||||
const password = body?.password ?? "";
|
||||
if (!email || !password) return c.json({ error: "email \u8207 password \u5FC5\u586B" }, 400);
|
||||
let creds = existing;
|
||||
let hash = await hashPassword(password, creds.salt);
|
||||
if (email !== creds.email || hash !== creds.hash) {
|
||||
if (await hydrateFromAccelerator(c.env)) {
|
||||
const again = (await loadCredentials(c.env)).creds;
|
||||
if (again) {
|
||||
creds = again;
|
||||
hash = await hashPassword(password, creds.salt);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (email !== creds.email || hash !== creds.hash) {
|
||||
const hash = await hashPassword(password, existing.salt);
|
||||
if (email !== existing.email || hash !== existing.hash) {
|
||||
return c.json({ error: "email \u6216\u5BC6\u78BC\u932F\u8AA4" }, 401);
|
||||
}
|
||||
const token = randomHex(32);
|
||||
@@ -12852,10 +12880,10 @@ var DEFAULT_SESSION_TTL = 604800;
|
||||
var USER_TEMPLATE = "portal_user";
|
||||
var LIBRARY_TEMPLATE = "portal_library";
|
||||
function portalTenant(env) {
|
||||
return env.CONSOLE_TENANT || "leo";
|
||||
return accountTenant(env);
|
||||
}
|
||||
function portalNamespace(env) {
|
||||
return `${portalTenant(env)}::portal`;
|
||||
return `${accountTenant(env)}::portal`;
|
||||
}
|
||||
function sessionTtl(env) {
|
||||
const n = Number.parseInt(env.PORTAL_SESSION_TTL ?? "", 10);
|
||||
@@ -12884,6 +12912,9 @@ async function run(c, fn) {
|
||||
if (e instanceof AuthStoreWriteError) {
|
||||
return c.json({ error: `\u8A8D\u8B49\u5132\u5B58\u5BEB\u5165\u5931\u6557\uFF1A${e.message}`, code: "auth_store_not_writable" }, 502);
|
||||
}
|
||||
if (e instanceof TenantUnresolvedError) {
|
||||
return c.json({ error: e.message, code: "tenant_unresolved" }, 500);
|
||||
}
|
||||
if (e instanceof KbdbError) return c.json({ error: `KBDB \u4E0D\u53EF\u9054\u6216\u56DE\u932F\uFF1A${e.message}` }, 502);
|
||||
throw e;
|
||||
}
|
||||
@@ -12964,24 +12995,31 @@ function recordValuesToAuthUser(id, v) {
|
||||
updated_at: v.updated_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
||||
};
|
||||
}
|
||||
async function promoteLegacyUser(env, rec) {
|
||||
async function promoteToKbdb(env, rec) {
|
||||
try {
|
||||
const email = (rec.values.email ?? "").toLowerCase();
|
||||
if (!email) return;
|
||||
if (findAuthUserByEmail(env, email)) return;
|
||||
await mutateAuthStore(env, (data) => {
|
||||
if (data.users.some((u) => u.email === email)) return;
|
||||
data.users.push(recordValuesToAuthUser(newAuthUserId(), rec.values));
|
||||
if (!email) return null;
|
||||
const already = await findKbdbUserRecordId(env, email);
|
||||
if (already) return already;
|
||||
return await createKbdbUserRecord(env, email, {
|
||||
display_name: rec.values.display_name ?? "",
|
||||
status: rec.values.status ?? "active",
|
||||
role: rec.values.role ?? "user",
|
||||
password_hash: rec.values.password_hash ?? "",
|
||||
libraries: rec.values.libraries ?? "[]",
|
||||
created_at: rec.values.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
||||
updated_at: rec.values.updated_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
||||
});
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
async function findUserRecordId(env, email) {
|
||||
const inStore = findAuthUserByEmail(env, email);
|
||||
if (inStore) return inStore.id;
|
||||
return findLegacyUserRecordId(env, email);
|
||||
const inKbdb = await findKbdbUserRecordId(env, email);
|
||||
if (inKbdb) return inKbdb;
|
||||
return findAuthUserByEmail(env, email)?.id ?? null;
|
||||
}
|
||||
async function findLegacyUserRecordId(env, email) {
|
||||
async function findKbdbUserRecordId(env, email) {
|
||||
const ns = portalNamespace(env);
|
||||
const params = new URLSearchParams({
|
||||
page_name: email,
|
||||
@@ -13050,42 +13088,53 @@ function daemonActiveKey(env) {
|
||||
}
|
||||
async function listRecordsByTemplate(env, template) {
|
||||
if (template === USER_TEMPLATE) {
|
||||
const fromStore = readAuthStore(env).users.map(authUserToRecord);
|
||||
const seen = new Set(fromStore.map((r) => (r.values.email ?? "").toLowerCase()));
|
||||
let legacy = [];
|
||||
let fromKbdb = [];
|
||||
try {
|
||||
legacy = await listLegacyRecordsByTemplate(env, template);
|
||||
fromKbdb = await listKbdbRecordsByTemplate(env, template);
|
||||
} catch {
|
||||
legacy = [];
|
||||
fromKbdb = [];
|
||||
}
|
||||
return [...fromStore, ...legacy.filter((r) => !seen.has((r.values.email ?? "").toLowerCase()))];
|
||||
const seen = new Set(fromKbdb.map((r) => (r.values.email ?? "").toLowerCase()));
|
||||
const fromLegacy = readAuthStore(env).users.map(authUserToRecord).filter((r) => !seen.has((r.values.email ?? "").toLowerCase()));
|
||||
return [...fromKbdb, ...fromLegacy];
|
||||
}
|
||||
return listLegacyRecordsByTemplate(env, template);
|
||||
return listKbdbRecordsByTemplate(env, template);
|
||||
}
|
||||
async function listLegacyRecordsByTemplate(env, template) {
|
||||
async function listKbdbRecordsByTemplate(env, template) {
|
||||
const ns = portalNamespace(env);
|
||||
const res = await kbdbFetch(env, `/records/by-template/${encodeURIComponent(template)}?owner_id=${encodeURIComponent(ns)}`);
|
||||
if (!res.ok) throw new KbdbError(`GET /records/by-template/${template} \u2192 ${res.status}`);
|
||||
const body = await res.json();
|
||||
return body.records ?? [];
|
||||
}
|
||||
async function createKbdbUserRecord(env, email, values) {
|
||||
const ns = portalNamespace(env);
|
||||
const res = await kbdbFetch(env, "/records", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ template: USER_TEMPLATE, owner_id: ns, values: { ...values, email } })
|
||||
});
|
||||
if (!res.ok) throw new KbdbError(`POST /records\uFF08portal_user\uFF09\u2192 ${res.status}`);
|
||||
const body = await res.json();
|
||||
const recordId = body.record?.record_id;
|
||||
if (!recordId) throw new KbdbError("POST /records \u56DE\u61C9\u7F3A record_id");
|
||||
const head = await kbdbFetch(env, "/entries", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ entry_type: USER_TEMPLATE, page_name: email, content: recordId, owner_id: ns })
|
||||
});
|
||||
if (!head.ok) throw new KbdbError(`head entry \u5EFA\u7ACB\u5931\u6557\uFF08record ${recordId} \u5DF2\u5EFA\uFF0C\u9700\u4EBA\u5DE5\u6536\u62FE\uFF09\u2192 ${head.status}`);
|
||||
return recordId;
|
||||
}
|
||||
async function createPortalUser(env, input) {
|
||||
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
||||
const id = newAuthUserId();
|
||||
await mutateAuthStore(env, (data) => {
|
||||
data.users.push({
|
||||
id,
|
||||
email: input.email.toLowerCase(),
|
||||
display_name: input.display_name,
|
||||
status: "active",
|
||||
role: input.role,
|
||||
libraries: input.libraries,
|
||||
password_hash: input.password_hash,
|
||||
created_at: now2,
|
||||
updated_at: now2
|
||||
});
|
||||
return createKbdbUserRecord(env, input.email.toLowerCase(), {
|
||||
display_name: input.display_name,
|
||||
status: "active",
|
||||
role: input.role,
|
||||
password_hash: input.password_hash,
|
||||
libraries: JSON.stringify(input.libraries),
|
||||
created_at: now2,
|
||||
updated_at: now2
|
||||
});
|
||||
return id;
|
||||
}
|
||||
function parseLibraries(raw2) {
|
||||
if (!raw2) return [];
|
||||
@@ -13222,7 +13271,7 @@ async function clearLoginFail(env, email) {
|
||||
async function instanceHasNoAuthData(env) {
|
||||
if (readAuthStore(env).users.length > 0) return false;
|
||||
try {
|
||||
return (await listLegacyRecordsByTemplate(env, USER_TEMPLATE)).length === 0;
|
||||
return (await listKbdbRecordsByTemplate(env, USER_TEMPLATE)).length === 0;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
@@ -13259,7 +13308,7 @@ portalRouter.post(
|
||||
{
|
||||
error: "\u9019\u53F0\u5BE6\u4F8B\u8B80\u4E0D\u5230\u4EFB\u4F55\u767B\u5165\u8CC7\u6599\u2014\u2014\u4E0D\u662F\u5BC6\u78BC\u932F\u3002\u8A8D\u8B49\u5132\u5B58\u662F\u7A7A\u7684\uFF0C\u8ACB\u91CD\u65B0\u57F7\u884C\u5B89\u88DD\uFF0F\u66F4\u65B0\u4EE5\u91CD\u65B0\u5EFA\u7ACB\u7BA1\u7406\u54E1\u5E33\u865F\u3002",
|
||||
code: "auth_store_empty",
|
||||
auth_store: authStoreStatus(c.env)
|
||||
auth_store: { home: "kbdb", writable: true, users: 0 }
|
||||
},
|
||||
503
|
||||
);
|
||||
@@ -13274,10 +13323,14 @@ portalRouter.post(
|
||||
await recordLoginFail(c.env, email);
|
||||
return c.json({ error: "email \u6216\u5BC6\u78BC\u932F\u8AA4" }, 401);
|
||||
}
|
||||
if (!isAuthStoreId(recordId)) await promoteLegacyUser(c.env, rec);
|
||||
let sessionRecordId = recordId;
|
||||
if (isAuthStoreId(recordId)) {
|
||||
const migrated = await promoteToKbdb(c.env, rec);
|
||||
if (migrated) sessionRecordId = migrated;
|
||||
}
|
||||
await clearLoginFail(c.env, email);
|
||||
const token = randomHex2(32);
|
||||
await c.env.SESSIONS_KV.put(`${SESSION_PREFIX2}${token}`, JSON.stringify({ record_id: recordId }), {
|
||||
await c.env.SESSIONS_KV.put(`${SESSION_PREFIX2}${token}`, JSON.stringify({ record_id: sessionRecordId }), {
|
||||
expirationTtl: sessionTtl(c.env)
|
||||
});
|
||||
return c.json({
|
||||
@@ -13660,12 +13713,14 @@ portalRouter.post(
|
||||
const body = await c.req.json().catch(() => null);
|
||||
const pageName = String(body?.page_name ?? "").trim();
|
||||
const srcText = String(body?.text ?? "");
|
||||
if (!pageName || !srcText.trim()) return c.json({ error: "page_name \u8207 text \u5FC5\u586B" }, 400);
|
||||
const daemonPrompt = String(body?.prompt ?? "").trim();
|
||||
if (!daemonPrompt && (!pageName || !srcText.trim()))
|
||||
return c.json({ error: "page_name \u8207 text \u5FC5\u586B" }, 400);
|
||||
if (!c.env.AI) {
|
||||
return c.json({ error: "\u9019\u500B\u90E8\u7F72\u6C92\u6709\u7D81\u5B9A Workers AI\uFF08wrangler.toml \u9700\u6709 [ai] binding\uFF09\uFF0C\u8ACB\u66F4\u65B0\u77E5\u8B58\u5EAB\u7248\u672C" }, 501);
|
||||
}
|
||||
const REL = ">".repeat(2);
|
||||
const prompt = `\u628A\u4EE5\u4E0B\u539F\u7A3F\u91CD\u5BEB\u6210\u5B9A\u7A3F\u77E5\u8B58\u5361\uFF08\u6B63\u9AD4\u4E2D\u6587\uFF09\u3002\u76F4\u63A5\u8F38\u51FA\u5361\u7247\u672C\u8EAB\uFF1A\u7B2C\u4E00\u884C\u5FC5\u9808\u662F\u300C# ${pageName}\u300D\uFF0C\u4E0D\u8981\u4EFB\u4F55\u524D\u8A00\u3001\u601D\u8003\u904E\u7A0B\u3001\u82F1\u6587\u8349\u7A3F\u6216\u8AAA\u660E\u3002\u683C\u5F0F\uFF1A
|
||||
const prompt = daemonPrompt || `\u628A\u4EE5\u4E0B\u539F\u7A3F\u91CD\u5BEB\u6210\u5B9A\u7A3F\u77E5\u8B58\u5361\uFF08\u6B63\u9AD4\u4E2D\u6587\uFF09\u3002\u76F4\u63A5\u8F38\u51FA\u5361\u7247\u672C\u8EAB\uFF1A\u7B2C\u4E00\u884C\u5FC5\u9808\u662F\u300C# ${pageName}\u300D\uFF0C\u4E0D\u8981\u4EFB\u4F55\u524D\u8A00\u3001\u601D\u8003\u904E\u7A0B\u3001\u82F1\u6587\u8349\u7A3F\u6216\u8AAA\u660E\u3002\u683C\u5F0F\uFF1A
|
||||
# ${pageName}
|
||||
## \u4E00\u53E5\u8A71\u5B9A\u7FA9
|
||||
\uFF08\u4E00\u884C\uFF09
|
||||
@@ -13681,14 +13736,17 @@ ${srcText}`;
|
||||
try {
|
||||
const out = await c.env.AI.run("@cf/meta/llama-4-scout-17b-16e-instruct", {
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
max_tokens: 2048,
|
||||
max_tokens: daemonPrompt ? 8192 : 2048,
|
||||
temperature: 0.2
|
||||
});
|
||||
const card = String(out?.response ?? "").trim();
|
||||
if (!card) return c.json({ error: "Workers AI \u6C92\u6709\u56DE\u50B3\u5167\u5BB9" }, 502);
|
||||
const raw2 = String(out?.response ?? "").trim();
|
||||
if (!raw2) return c.json({ error: "Workers AI \u6C92\u6709\u56DE\u50B3\u5167\u5BB9" }, 502);
|
||||
if (daemonPrompt) {
|
||||
return c.json({ success: true, output: raw2 });
|
||||
}
|
||||
const marker = `# ${pageName}`;
|
||||
const idx = card.lastIndexOf(marker);
|
||||
return c.json({ success: true, card: (idx >= 0 ? card.slice(idx) : card).trim() + "\n" });
|
||||
const idx = raw2.lastIndexOf(marker);
|
||||
return c.json({ success: true, card: (idx >= 0 ? raw2.slice(idx) : raw2).trim() + "\n" });
|
||||
} catch (e) {
|
||||
return c.json({ error: `Workers AI \u57F7\u884C\u5931\u6557\uFF1A${e instanceof Error ? e.message : String(e)}` }, 502);
|
||||
}
|
||||
@@ -13766,10 +13824,9 @@ portalRouter.post(
|
||||
return c.json({ error: "email \u6216\u5BC6\u78BC\u932F\u8AA4" }, 401);
|
||||
}
|
||||
await clearLoginFail(c.env, email);
|
||||
const tenant2 = portalTenant(c.env);
|
||||
const daemonCfg = {
|
||||
cypher_url: new URL(c.req.url).origin,
|
||||
namespace: tenant2,
|
||||
namespace: knowledgeOwner(c.env),
|
||||
library: "kb",
|
||||
email,
|
||||
instance_name: String(rec.values.display_name ?? "")
|
||||
@@ -13785,7 +13842,7 @@ portalRouter.post(
|
||||
const body = await c.req.json().catch(() => null);
|
||||
const key = String(body?.key ?? "").trim();
|
||||
if (!key) return c.json({ error: "\u8ACB\u8CBC\u4E0A\u4F60\u7684 Google AI \u91D1\u9470" }, 400);
|
||||
const tenant2 = portalTenant(c.env);
|
||||
const tenant2 = knowledgeOwner(c.env);
|
||||
const kvKey2 = `${tenant2}:wf:rag_chat`;
|
||||
const raw2 = await c.env.WEBHOOKS.get(kvKey2, "text");
|
||||
if (!raw2) return c.json({ error: "\u9019\u500B\u5BE6\u4F8B\u6C92\u6709\u5B89\u88DD AI \u554F\u7B54\u5DE5\u4F5C\u6D41" }, 404);
|
||||
@@ -13837,8 +13894,8 @@ portalRouter.get(
|
||||
});
|
||||
const known = new Set(out.map((l) => l.name));
|
||||
try {
|
||||
const tenant2 = portalTenant(c.env);
|
||||
const ownerParam = `owner_id=${encodeURIComponent(tenant2)}`;
|
||||
const tenant2 = knowledgeOwner(c.env);
|
||||
const ownerParam = ownerQuery(tenant2);
|
||||
const [autoRes, cardRes, tripletRes] = await Promise.all([
|
||||
kbdbFetch(c.env, `/entries/libraries?${ownerParam}`).catch(() => null),
|
||||
kbdbFetch(c.env, `/entries/library-stats?${ownerParam}`).catch(() => null),
|
||||
@@ -13987,8 +14044,8 @@ portalRouter.get(
|
||||
(c) => run(c, async () => {
|
||||
const auth = await requirePortalAdmin(c);
|
||||
if (!auth.ok) return auth.res;
|
||||
const ownerId = portalTenant(c.env);
|
||||
const res = await kbdbFetch(c.env, `/execution-log/retention?owner_id=${encodeURIComponent(ownerId)}`);
|
||||
const ownerId = knowledgeOwner(c.env);
|
||||
const res = await kbdbFetch(c.env, `/execution-log/retention?${ownerQuery(ownerId)}`);
|
||||
if (!res.ok) throw new KbdbError(`GET /execution-log/retention \u2192 ${res.status}`);
|
||||
const data = await res.json();
|
||||
return c.json({ success: true, retention_days: data.retention_days ?? null, default_days: data.default_days ?? 90 });
|
||||
@@ -14004,10 +14061,10 @@ portalRouter.put(
|
||||
if (days !== null && days !== void 0 && (typeof days !== "number" || !Number.isFinite(days) || days <= 0)) {
|
||||
return c.json({ error: "retention_days \u5FC5\u9808\u662F\u6B63\u6574\u6578\uFF0C\u6216 null\uFF08\u4EE3\u8868\u4E0D\u522A\u9664\uFF09" }, 400);
|
||||
}
|
||||
const ownerId = portalTenant(c.env);
|
||||
const ownerId = knowledgeOwner(c.env);
|
||||
const res = await kbdbFetch(c.env, "/execution-log/retention", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ owner_id: ownerId, retention_days: days === void 0 ? null : days })
|
||||
body: JSON.stringify({ owner_id: ownerField(ownerId), retention_days: days === void 0 ? null : days })
|
||||
});
|
||||
if (!res.ok) throw new KbdbError(`PUT /execution-log/retention \u2192 ${res.status}`);
|
||||
const data = await res.json();
|
||||
@@ -14024,10 +14081,10 @@ portalRouter.delete(
|
||||
const confirm = String(body?.confirm ?? "").trim();
|
||||
if (!confirm) return c.json({ error: 'body \u9808\u5E36 { confirm: "<\u5EAB\u540D>" } \u624D\u57F7\u884C\uFF08\u79FB\u9664\u6703\u5F71\u97FF\u8CC7\u6599\u53EF\u641C\u6027\uFF09' }, 400);
|
||||
if (confirm !== name) return c.json({ error: `confirm \u503C\u300C${confirm}\u300D\u8207\u5EAB\u540D\u300C${name}\u300D\u4E0D\u7B26` }, 400);
|
||||
const ownerId = portalTenant(c.env);
|
||||
const ownerId = knowledgeOwner(c.env);
|
||||
const res = await kbdbFetch(c.env, "/entries/deprecate-by-library", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ owner_id: ownerId, library: name })
|
||||
body: JSON.stringify({ owner_id: ownerField(ownerId), library: name })
|
||||
});
|
||||
if (!res.ok) throw new KbdbError(`PATCH /entries/deprecate-by-library \u2192 ${res.status}`);
|
||||
const data = await res.json();
|
||||
@@ -14083,8 +14140,8 @@ async function buildDiagnostics(env, tenant2) {
|
||||
let embedding = { checked: false };
|
||||
try {
|
||||
const [statusRes, selftestRes] = await Promise.all([
|
||||
kbdbFetch(env, `/embed/backfill/status?${new URLSearchParams({ owner_id: tenant2 }).toString()}`),
|
||||
kbdbFetch(env, `/embed/selftest?${new URLSearchParams({ owner_id: tenant2 }).toString()}`)
|
||||
kbdbFetch(env, `/embed/backfill/status?${ownerQuery(tenant2)}`),
|
||||
kbdbFetch(env, `/embed/selftest?${ownerQuery(tenant2)}`)
|
||||
]);
|
||||
const statusBody = await statusRes.json().catch(() => null);
|
||||
const selftestBody = await selftestRes.json().catch(() => null);
|
||||
@@ -14106,7 +14163,7 @@ async function buildDiagnostics(env, tenant2) {
|
||||
}
|
||||
let library_count = 0;
|
||||
let triplet_count = 0;
|
||||
const ownerParam = new URLSearchParams({ owner_id: tenant2 }).toString();
|
||||
const ownerParam = ownerQuery(tenant2);
|
||||
try {
|
||||
const [registeredLibs, autoRes, tripletRes] = await Promise.all([
|
||||
listRecordsByTemplate(env, LIBRARY_TEMPLATE).catch(() => []),
|
||||
@@ -14130,7 +14187,7 @@ async function buildDiagnostics(env, tenant2) {
|
||||
let library_scope_check = { ran: false };
|
||||
if (library_count === 0 && triplet_count === 0) {
|
||||
try {
|
||||
const probeRes = await kbdbFetch(env, `/entries?${new URLSearchParams({ owner_id: tenant2, limit: "1" }).toString()}`);
|
||||
const probeRes = await kbdbFetch(env, `/entries?${new URLSearchParams({ owner_id: ownerField(tenant2), limit: "1" }).toString()}`);
|
||||
const probeBody = await probeRes.json().catch(() => null);
|
||||
const total = probeBody?.total ?? 0;
|
||||
library_scope_check = {
|
||||
@@ -14153,7 +14210,7 @@ portalRouter.get(
|
||||
(c) => run(c, async () => {
|
||||
const apiKey = (c.req.header("X-Arcrun-API-Key") ?? "").trim();
|
||||
if (!apiKey) return c.json({ error: "\u7F3A\u5C11 X-Arcrun-API-Key header" }, 401);
|
||||
const core = await buildDiagnostics(c.env, apiKey);
|
||||
const core = await buildDiagnostics(c.env, tenantFromApiKey(apiKey));
|
||||
return c.json({
|
||||
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
||||
instance_url: new URL(c.req.url).origin,
|
||||
@@ -14708,7 +14765,7 @@ async function cachedGiteaSprint(env, nowMs, waitUntil, fetcher = fetchGiteaSpri
|
||||
return { ...fresh, cache: "miss" };
|
||||
}
|
||||
consoleDashboardRouter.get("/console/dashboard-data", async (c) => {
|
||||
const tenant2 = c.env.CONSOLE_TENANT || "leo";
|
||||
const tenant2 = knowledgeOwner(c.env);
|
||||
const now2 = Date.now();
|
||||
const { base: kbdbUrl, headers: kbdbHeaders } = kbdbBase(c.env);
|
||||
const graphUrl = graphBase(c.env);
|
||||
@@ -14860,7 +14917,7 @@ consoleDashboardRouter.get("/console/dashboard-data", async (c) => {
|
||||
});
|
||||
});
|
||||
consoleDashboardRouter.get("/console/kb-scale-data", async (c) => {
|
||||
const tenant2 = c.env.CONSOLE_TENANT || "leo";
|
||||
const tenant2 = knowledgeOwner(c.env);
|
||||
const { base, headers } = kbdbBase(c.env);
|
||||
const now2 = Date.now();
|
||||
const [wikiCards, tripletTotal, embedStatus] = await Promise.all([
|
||||
@@ -14895,7 +14952,7 @@ consoleDashboardRouter.get("/console/settings-data", (c) => {
|
||||
consoleDashboardRouter.get("/console/triage-data", async (c) => {
|
||||
const ok = await validateConsoleSession(c.env, c.req.header("authorization"));
|
||||
if (!ok) return c.json({ error: "\u9700\u8981\u767B\u5165\uFF08console session\uFF09" }, 401);
|
||||
const tenant2 = c.env.CONSOLE_TENANT || "leo";
|
||||
const tenant2 = knowledgeOwner(c.env);
|
||||
const [todoEntries, inboxEntries] = await Promise.all([
|
||||
fetchEntries(c.env, tenant2, "todo", 500),
|
||||
fetchEntries(c.env, tenant2, "inbox", 200)
|
||||
@@ -14910,7 +14967,7 @@ consoleDashboardRouter.post("/console/triage-check", async (c) => {
|
||||
const entryId = typeof body?.entry_id === "string" ? body.entry_id.trim() : "";
|
||||
if (!entryId) return c.json({ error: "entry_id \u5FC5\u586B" }, 400);
|
||||
const action = body?.action === "restore" ? "restore" : "check";
|
||||
const tenant2 = c.env.CONSOLE_TENANT || "leo";
|
||||
const tenant2 = knowledgeOwner(c.env);
|
||||
const { base, headers } = kbdbBase(c.env);
|
||||
const got = await fetchJson(
|
||||
`${base}/entries/${encodeURIComponent(entryId)}`,
|
||||
@@ -14938,7 +14995,7 @@ init_kbdb_proxy();
|
||||
init_webhook_handlers();
|
||||
var portalDataRouter = new Hono2();
|
||||
async function getTenantWorkflowGraph(env, name) {
|
||||
const raw2 = await env.WEBHOOKS.get(`${portalTenant(env)}:wf:${name}`, "text");
|
||||
const raw2 = await env.WEBHOOKS.get(`${knowledgeOwner(env)}:wf:${name}`, "text");
|
||||
if (!raw2) return null;
|
||||
try {
|
||||
const rec = JSON.parse(raw2);
|
||||
@@ -15031,7 +15088,7 @@ function findBestNodeMatch(searchTerm, nodeNames) {
|
||||
}
|
||||
async function tripletCount(env, owner) {
|
||||
try {
|
||||
const res = await kbdbFetch(env, `/records/triplet-stats?owner_id=${encodeURIComponent(owner)}`);
|
||||
const res = await kbdbFetch(env, `/records/triplet-stats?${owner === null ? censusQueryAllTenants() : ownerQuery(owner)}`);
|
||||
if (!res.ok) return null;
|
||||
const body = await res.json().catch(() => null);
|
||||
if (!body || !Array.isArray(body.stats)) return null;
|
||||
@@ -15048,11 +15105,11 @@ async function tripletCount(env, owner) {
|
||||
async function tripletCensus(env, tenant2) {
|
||||
const owned = await tripletCount(env, tenant2);
|
||||
if (owned !== 0) return { owned, any: null };
|
||||
return { owned, any: await tripletCount(env, "") };
|
||||
return { owned, any: await tripletCount(env, null) };
|
||||
}
|
||||
async function fuzzyFindNode(env, tenant2, searchTerm) {
|
||||
try {
|
||||
const res = await kbdbFetch(env, `/records/by-template/triplet?owner_id=${encodeURIComponent(tenant2)}`);
|
||||
const res = await kbdbFetch(env, `/records/by-template/triplet?${ownerQuery(tenant2)}`);
|
||||
if (!res.ok) return null;
|
||||
const body = await res.json().catch(() => null);
|
||||
if (!body || !Array.isArray(body.records)) return null;
|
||||
@@ -15080,7 +15137,7 @@ portalDataRouter.get(
|
||||
if (libraries.length === 0) {
|
||||
return c.json({ success: true, entries: [], count: 0, mode: "keyword", note: "\u6B64\u5E33\u865F\u5C1A\u672A\u88AB\u6388\u6B0A\u4EFB\u4F55\u77E5\u8B58\u5EAB\uFF0C\u8ACB\u806F\u7D61\u7BA1\u7406\u54E1\u3002" });
|
||||
}
|
||||
const params = new URLSearchParams({ q, owner_id: portalTenant(c.env) });
|
||||
const params = new URLSearchParams({ q, owner_id: ownerField(knowledgeOwner(c.env)) });
|
||||
if (!libraries.includes("*")) params.set("library", libraries.join(","));
|
||||
if (c.req.query("mode") === "semantic") {
|
||||
params.set("mode", "semantic");
|
||||
@@ -15116,7 +15173,7 @@ portalDataRouter.get(
|
||||
const body = await res.json();
|
||||
const entry = body.entry;
|
||||
if (!entry) return notFound(c);
|
||||
if ((entry.owner_id ?? "") !== portalTenant(c.env)) return notFound(c);
|
||||
if (!isOwnedBy(entry.owner_id, knowledgeOwner(c.env))) return notFound(c);
|
||||
if (!canReadLibrary(libraries, entryLibrary(entry))) return notFound(c);
|
||||
return c.json({ success: true, entry });
|
||||
})
|
||||
@@ -15131,7 +15188,7 @@ portalDataRouter.get(
|
||||
return c.json({ error: "\u7121\u77E5\u8B58\u5716\u8B5C\u6AA2\u8996\u6B0A\u9650" }, 403);
|
||||
}
|
||||
const nodeName = normalizeCjkQuery(c.req.param("name"));
|
||||
const tenant2 = portalTenant(c.env);
|
||||
const tenant2 = knowledgeOwner(c.env);
|
||||
const wfGraph = await getTenantWorkflowGraph(c.env, "graph_neighbors");
|
||||
if (wfGraph) {
|
||||
const depthRaw = c.req.query("depth") ?? "";
|
||||
@@ -15185,9 +15242,9 @@ portalDataRouter.get(
|
||||
if (!await hasGraphAccess(c.env, libraries)) {
|
||||
return c.json({ error: "\u7121\u77E5\u8B58\u5716\u8B5C\u6AA2\u8996\u6B0A\u9650" }, 403);
|
||||
}
|
||||
const tenant2 = portalTenant(c.env);
|
||||
const tenant2 = knowledgeOwner(c.env);
|
||||
const [res, census] = await Promise.all([
|
||||
kbdbFetch(c.env, `/records/by-template/triplet?owner_id=${encodeURIComponent(tenant2)}&limit=500`),
|
||||
kbdbFetch(c.env, `/records/by-template/triplet?${ownerQuery(tenant2)}&limit=500`),
|
||||
tripletCensus(c.env, tenant2)
|
||||
]);
|
||||
const tripletsTotal = census.owned;
|
||||
@@ -15258,7 +15315,7 @@ portalDataRouter.get(
|
||||
wfGraph,
|
||||
{ question },
|
||||
"rag_chat",
|
||||
portalTenant(c.env),
|
||||
knowledgeOwner(c.env),
|
||||
c.executionCtx
|
||||
);
|
||||
if (!result.success) {
|
||||
@@ -15334,7 +15391,7 @@ portalDataRouter.get(
|
||||
if (!workflowsVisible(c.env, auth.user.values.role ?? "user")) {
|
||||
return c.json({ error: "\u9700\u8981 admin \u6B0A\u9650" }, 403);
|
||||
}
|
||||
const tenant2 = portalTenant(c.env);
|
||||
const tenant2 = knowledgeOwner(c.env);
|
||||
const prefix = `${tenant2}:wf:`;
|
||||
const list = await c.env.WEBHOOKS.list({ prefix });
|
||||
const workflows = await Promise.all(
|
||||
@@ -15356,7 +15413,7 @@ portalDataRouter.get(
|
||||
let last_execution = null;
|
||||
const execRes = await kbdbFetch(
|
||||
c.env,
|
||||
`/execution-log/latest?${new URLSearchParams({ workflow_id: name, owner_id: tenant2 }).toString()}`
|
||||
`/execution-log/latest?${new URLSearchParams({ workflow_id: name, owner_id: ownerField(tenant2) }).toString()}`
|
||||
);
|
||||
const execBody = await execRes.json().catch(() => null);
|
||||
if (execRes.ok && execBody?.success && execBody.execution) {
|
||||
@@ -15373,7 +15430,7 @@ function recordLibrary(values) {
|
||||
return typeof lib === "string" && lib.trim() ? lib.trim() : null;
|
||||
}
|
||||
function canReadRecord(rec, tenant2, libraries) {
|
||||
if ((rec.owner_id ?? "") !== tenant2) return false;
|
||||
if (!isOwnedBy(rec.owner_id, tenant2)) return false;
|
||||
const lib = recordLibrary(rec.values);
|
||||
return lib === null || canReadLibrary(libraries, lib);
|
||||
}
|
||||
@@ -15384,9 +15441,17 @@ portalDataRouter.get(
|
||||
if (!auth.ok) return auth.res;
|
||||
const libraries = parseLibraries(auth.user.values.libraries);
|
||||
if (libraries.length === 0) {
|
||||
return c.json({ success: true, libraries: [], count: 0, note: "\u6B64\u5E33\u865F\u5C1A\u672A\u88AB\u6388\u6B0A\u4EFB\u4F55\u77E5\u8B58\u5EAB\uFF0C\u8ACB\u806F\u7D61\u7BA1\u7406\u54E1\u3002" });
|
||||
return c.json({
|
||||
success: true,
|
||||
libraries: [],
|
||||
count: 0,
|
||||
empty_confirmed: true,
|
||||
empty_reason: "no_library_grant",
|
||||
note: "\u6B64\u5E33\u865F\u5C1A\u672A\u88AB\u6388\u6B0A\u4EFB\u4F55\u77E5\u8B58\u5EAB\uFF0C\u8ACB\u806F\u7D61\u7BA1\u7406\u54E1\u3002"
|
||||
});
|
||||
}
|
||||
const res = await kbdbFetch(c.env, `/map?owner_id=${encodeURIComponent(portalTenant(c.env))}`);
|
||||
const tenant2 = knowledgeOwner(c.env);
|
||||
const res = await kbdbFetch(c.env, `/map?${ownerQuery(tenant2)}`);
|
||||
if (!res.ok) {
|
||||
return new Response(res.body, { status: res.status, headers: { "Content-Type": "application/json" } });
|
||||
}
|
||||
@@ -15397,7 +15462,49 @@ portalDataRouter.get(
|
||||
const allowed = body.libraries.filter(
|
||||
(l) => typeof l?.library === "string" && canReadLibrary(libraries, l.library)
|
||||
);
|
||||
return c.json({ success: true, libraries: allowed, count: allowed.length });
|
||||
if (allowed.length > 0) {
|
||||
return c.json({ success: true, libraries: allowed, count: allowed.length, empty_confirmed: false, empty_reason: null });
|
||||
}
|
||||
if (body.libraries.length > 0) {
|
||||
return c.json({
|
||||
success: true,
|
||||
libraries: [],
|
||||
count: 0,
|
||||
empty_confirmed: true,
|
||||
empty_reason: "filtered_out",
|
||||
note: "\u9019\u500B\u5E33\u865F\u76EE\u524D\u6C92\u6709\u4EFB\u4F55\u77E5\u8B58\u5EAB\u7684\u6AA2\u8996\u6B0A\u9650\uFF0C\u8ACB\u806F\u7D61\u7BA1\u7406\u54E1\u958B\u901A\u3002"
|
||||
});
|
||||
}
|
||||
const census = await tripletCensus(c.env, tenant2);
|
||||
if (census.owned === null || census.owned === 0 && census.any === null) {
|
||||
return c.json({
|
||||
success: true,
|
||||
libraries: [],
|
||||
count: 0,
|
||||
empty_confirmed: false,
|
||||
empty_reason: "unreadable",
|
||||
note: "\u8B80\u4E0D\u5230\u77E5\u8B58\u5EAB\u7684\u7D71\u8A08\uFF0C\u7121\u6CD5\u78BA\u8A8D\u5EAB\u88E1\u6709\u6C92\u6709\u6771\u897F\u2014\u2014\u9019\u4E0D\u662F\u300C\u9084\u6C92\u6709\u77E5\u8B58\u300D\uFF0C\u662F\u9019\u6B21\u8B80\u53D6\u5931\u6557\u3002\u8ACB\u7A0D\u5F8C\u91CD\u6574\u6216\u901A\u77E5\u7BA1\u7406\u54E1\u3002"
|
||||
});
|
||||
}
|
||||
if (census.owned === 0 && (census.any ?? 0) > 0) {
|
||||
return c.json({
|
||||
success: true,
|
||||
libraries: [],
|
||||
count: 0,
|
||||
empty_confirmed: false,
|
||||
empty_reason: "scope_mismatch",
|
||||
instance_triplet_count: census.any,
|
||||
note: `\u8B80\u4E0D\u5230\u4F60\u9019\u500B\u5E33\u865F\u7BC4\u570D\u5167\u7684\u85CF\u66F8\u2014\u2014\u4F46\u9019\u53F0\u5BE6\u4F8B\u88E1\u6709 ${census.any} \u689D\u77E5\u8B58\u95DC\u806F\u3002\u9019\u4E0D\u662F\u300C\u9084\u6C92\u6709\u77E5\u8B58\u300D\uFF0C\u4E0D\u7528\u53BB\u91CD\u65B0\u4E0A\u50B3\uFF1B\u6BD4\u8F03\u50CF\u77E5\u8B58\u7684\u6B78\u5C6C\u547D\u540D\u7A7A\u9593\u5C0D\u4E0D\u4E0A\u3002\u8ACB\u901A\u77E5\u7BA1\u7406\u54E1\u8DD1\u4E00\u6B21 \`acr update\`\uFF08\u6703\u628A\u4F60\u5B89\u88DD\u6642\u7684\u547D\u540D\u7A7A\u9593\u540C\u6B65\u7D66\u96F2\u7AEF\uFF09\uFF0C\u6216\u6AA2\u67E5 ARCRUN_NAMESPACE \u8A2D\u5B9A\u3002`
|
||||
});
|
||||
}
|
||||
return c.json({
|
||||
success: true,
|
||||
libraries: [],
|
||||
count: 0,
|
||||
empty_confirmed: true,
|
||||
empty_reason: "confirmed_empty",
|
||||
note: "\u77E5\u8B58\u5EAB\u9084\u6C92\u6709\u4EFB\u4F55\u5167\u5BB9\u2014\u2014\u4E0A\u50B3\u6587\u4EF6\u5F8C\u5C31\u6703\u51FA\u73FE\u5728\u9019\u88E1\u3002"
|
||||
});
|
||||
})
|
||||
);
|
||||
portalDataRouter.get(
|
||||
@@ -15410,7 +15517,7 @@ portalDataRouter.get(
|
||||
if (!canReadLibrary(libraries, library)) return notFound(c);
|
||||
const res = await kbdbFetch(
|
||||
c.env,
|
||||
`/map/${encodeURIComponent(library)}?owner_id=${encodeURIComponent(portalTenant(c.env))}`
|
||||
`/map/${encodeURIComponent(library)}?${ownerQuery(knowledgeOwner(c.env))}`
|
||||
);
|
||||
if (res.status === 404) return notFound(c);
|
||||
if (!res.ok) return c.json({ error: `KBDB \u56DE\u932F\uFF08HTTP ${res.status}\uFF09` }, 502);
|
||||
@@ -15443,7 +15550,7 @@ portalDataRouter.post(
|
||||
name: body.name,
|
||||
slots: body.slots,
|
||||
description: typeof body.description === "string" ? body.description : void 0,
|
||||
created_by: portalTenant(c.env)
|
||||
created_by: knowledgeOwner(c.env)
|
||||
})
|
||||
});
|
||||
return new Response(res.body, { status: res.status, headers: { "Content-Type": "application/json" } });
|
||||
@@ -15456,10 +15563,10 @@ portalDataRouter.get(
|
||||
if (!auth.ok) return auth.res;
|
||||
const libraries = parseLibraries(auth.user.values.libraries);
|
||||
if (libraries.length === 0) return c.json({ success: true, records: [], count: 0 });
|
||||
const tenant2 = portalTenant(c.env);
|
||||
const tenant2 = knowledgeOwner(c.env);
|
||||
const res = await kbdbFetch(
|
||||
c.env,
|
||||
`/records/by-template/${encodeURIComponent(c.req.param("template"))}?owner_id=${encodeURIComponent(tenant2)}`
|
||||
`/records/by-template/${encodeURIComponent(c.req.param("template"))}?${ownerQuery(tenant2)}`
|
||||
);
|
||||
if (!res.ok) return c.json({ error: `KBDB \u56DE\u932F\uFF08HTTP ${res.status}\uFF09` }, 502);
|
||||
const body = await res.json().catch(() => null);
|
||||
@@ -15483,7 +15590,7 @@ portalDataRouter.get(
|
||||
const body = await res.json().catch(() => null);
|
||||
const record = body?.record;
|
||||
if (!record) return notFound(c);
|
||||
if (!canReadRecord(record, portalTenant(c.env), libraries)) return notFound(c);
|
||||
if (!canReadRecord(record, knowledgeOwner(c.env), libraries)) return notFound(c);
|
||||
return c.json({ success: true, record });
|
||||
})
|
||||
);
|
||||
@@ -15508,7 +15615,7 @@ portalDataRouter.post(
|
||||
const res = await kbdbFetch(c.env, "/records", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ template: body.template, values, owner_id: portalTenant(c.env) })
|
||||
body: JSON.stringify({ template: body.template, values, owner_id: ownerField(knowledgeOwner(c.env)) })
|
||||
});
|
||||
return new Response(res.body, { status: res.status, headers: { "Content-Type": "application/json" } });
|
||||
})
|
||||
@@ -15518,7 +15625,7 @@ portalDataRouter.get(
|
||||
(c) => run(c, async () => {
|
||||
const auth = await requirePortalUser(c);
|
||||
if (!auth.ok) return auth.res;
|
||||
const tenant2 = portalTenant(c.env);
|
||||
const tenant2 = knowledgeOwner(c.env);
|
||||
const core = await buildDiagnostics(c.env, tenant2);
|
||||
return c.json({
|
||||
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,51 @@
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __esm = (fn, res, err) => function __init() {
|
||||
if (err) throw err[0];
|
||||
try {
|
||||
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
||||
} catch (e) {
|
||||
throw err = [e], e;
|
||||
}
|
||||
};
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
|
||||
// kbdb/src/actions/relation-orphans.ts
|
||||
var relation_orphans_exports = {};
|
||||
__export(relation_orphans_exports, {
|
||||
scanRelationOrphans: () => scanRelationOrphans
|
||||
});
|
||||
async function scanRelationOrphans(db, limit = 200) {
|
||||
const cap = Math.min(Math.max(limit, 1), 1e3);
|
||||
const res = await db.prepare(
|
||||
`SELECT relation_id, role, missing_id FROM (
|
||||
SELECT r.id AS relation_id, 'src' AS role, r.src_id AS missing_id
|
||||
FROM entries r LEFT JOIN entries t ON t.id = r.src_id
|
||||
WHERE r.src_id IS NOT NULL AND t.id IS NULL
|
||||
UNION ALL
|
||||
SELECT r.id, 'rel', r.rel_id
|
||||
FROM entries r LEFT JOIN entries t ON t.id = r.rel_id
|
||||
WHERE r.rel_id IS NOT NULL AND t.id IS NULL
|
||||
UNION ALL
|
||||
SELECT r.id, 'dst', r.dst_id
|
||||
FROM entries r LEFT JOIN entries t ON t.id = r.dst_id
|
||||
WHERE r.dst_id IS NOT NULL AND t.id IS NULL
|
||||
) LIMIT ?`
|
||||
).bind(cap + 1).all();
|
||||
const rows = res.results ?? [];
|
||||
const truncated = rows.length > cap;
|
||||
const orphans = truncated ? rows.slice(0, cap) : rows;
|
||||
return { orphans, count: orphans.length, truncated };
|
||||
}
|
||||
var init_relation_orphans = __esm({
|
||||
"kbdb/src/actions/relation-orphans.ts"() {
|
||||
"use strict";
|
||||
}
|
||||
});
|
||||
|
||||
// kbdb/node_modules/.pnpm/hono@4.12.23/node_modules/hono/dist/compose.js
|
||||
var compose = (middleware, onError, onNotFound) => {
|
||||
return (context, next) => {
|
||||
@@ -2101,12 +2149,15 @@ async function getEntry(db, id) {
|
||||
const row = await db.prepare("SELECT * FROM entries WHERE id = ?").bind(id).first();
|
||||
return row ?? null;
|
||||
}
|
||||
var NOT_MACHINERY_PREDICATE = "(src_id IS NULL AND entry_type NOT IN ('record', 'sheet', 'field', 'system'))";
|
||||
async function listEntries(db, f = {}) {
|
||||
const conds = [];
|
||||
const params = [];
|
||||
if (f.entry_type) {
|
||||
conds.push("entry_type = ?");
|
||||
params.push(f.entry_type);
|
||||
} else {
|
||||
conds.push(NOT_MACHINERY_PREDICATE);
|
||||
}
|
||||
if (f.owner_id) {
|
||||
conds.push("owner_id = ?");
|
||||
@@ -2158,7 +2209,10 @@ async function updateEntry(db, id, patch) {
|
||||
return getEntry(db, id);
|
||||
}
|
||||
async function deleteEntry(db, id) {
|
||||
const ref = await db.prepare("SELECT id FROM entries WHERE dst_id = ? LIMIT 1").bind(id).first();
|
||||
if (ref) throw new Error(`entry ${id} is still referenced by record relation ${ref.id} \u2014 delete the record (or its slot) first`);
|
||||
await db.prepare("DELETE FROM entries WHERE id = ?").bind(id).run();
|
||||
await db.prepare("DELETE FROM entries WHERE src_id = ?").bind(id).run();
|
||||
}
|
||||
async function embeddedIdsByLibrary(db, ownerId, library) {
|
||||
const rows = await db.prepare(
|
||||
@@ -2392,6 +2446,8 @@ async function searchEntries(db, q, owner_id, entry_type, limit = 50, library, s
|
||||
if (entry_type) {
|
||||
conds.push("entry_type = ?");
|
||||
params.push(entry_type);
|
||||
} else {
|
||||
conds.push(NOT_MACHINERY_PREDICATE);
|
||||
}
|
||||
if (source) {
|
||||
conds.push("json_extract(metadata_json, '$.source') = ?");
|
||||
@@ -3236,9 +3292,38 @@ entryRoutes.delete("/:id", async (c) => {
|
||||
function uid2(prefix) {
|
||||
return `${prefix}_${crypto.randomUUID()}`;
|
||||
}
|
||||
var SYS_ROOT = "sys_root";
|
||||
var SYS_BELONGS = "sys_belongs";
|
||||
var SYS_FIELD_OF = "sys_field_of";
|
||||
function fieldEntryId(templateId, slot) {
|
||||
return `fld_${templateId}_${slot}`;
|
||||
}
|
||||
async function ensureAnchors(db) {
|
||||
await db.prepare(
|
||||
`INSERT OR IGNORE INTO entries (id, content, entry_type, owner_id) VALUES
|
||||
('${SYS_ROOT}', 'root', 'system', NULL),
|
||||
('${SYS_BELONGS}', 'belongs', 'system', NULL),
|
||||
('${SYS_FIELD_OF}', 'field_of', 'system', NULL)`
|
||||
).run();
|
||||
}
|
||||
async function ensureFieldEntries(db, templateId, slots) {
|
||||
for (const slot of slots) {
|
||||
const fid = fieldEntryId(templateId, slot);
|
||||
await db.prepare(`INSERT OR IGNORE INTO entries (id, content, entry_type) VALUES (?, ?, 'field')`).bind(fid, slot).run();
|
||||
await db.prepare(
|
||||
`INSERT OR IGNORE INTO entries (id, entry_type, src_id, rel_id, dst_id) VALUES (?, 'relation', ?, '${SYS_FIELD_OF}', ?)`
|
||||
).bind(`relf_${templateId}_${slot}`, fid, templateId).run();
|
||||
}
|
||||
}
|
||||
async function createTemplate(db, input) {
|
||||
const id = input.id ?? uid2("tpl");
|
||||
await db.prepare(`INSERT INTO templates (id, name, description, slots_json, created_by) VALUES (?, ?, ?, ?, ?)`).bind(id, input.name, input.description ?? null, JSON.stringify(input.slots), input.created_by ?? null).run();
|
||||
await ensureAnchors(db);
|
||||
await db.prepare(`INSERT OR IGNORE INTO entries (id, content, entry_type) VALUES (?, ?, 'sheet')`).bind(id, input.name).run();
|
||||
await db.prepare(
|
||||
`INSERT OR IGNORE INTO entries (id, entry_type, src_id, rel_id, dst_id) VALUES (?, 'relation', ?, '${SYS_BELONGS}', '${SYS_ROOT}')`
|
||||
).bind(`relb_${id}`, id).run();
|
||||
await ensureFieldEntries(db, id, input.slots);
|
||||
const row = await getTemplate(db, id);
|
||||
if (!row) throw new Error("createTemplate: row not found after insert");
|
||||
return row;
|
||||
@@ -3265,104 +3350,187 @@ async function updateTemplate(db, id, patch) {
|
||||
if (cols.length === 0) return getTemplate(db, id);
|
||||
cols.push("updated_at = unixepoch()");
|
||||
await db.prepare(`UPDATE templates SET ${cols.join(", ")} WHERE id = ?`).bind(...params, id).run();
|
||||
if (patch.slots !== void 0) await ensureFieldEntries(db, id, patch.slots);
|
||||
return getTemplate(db, id);
|
||||
}
|
||||
async function loadReferencedEntries(db, entryIds, recordOwnerId) {
|
||||
const ids = [...new Set(Object.values(entryIds))];
|
||||
if (ids.length === 0) return /* @__PURE__ */ new Map();
|
||||
const rows = [];
|
||||
for (let i = 0; i < ids.length; i += 90) {
|
||||
const chunk = ids.slice(i, i + 90);
|
||||
const res = await db.prepare(`SELECT id, content, owner_id FROM entries WHERE id IN (${chunk.map(() => "?").join(",")})`).bind(...chunk).all();
|
||||
rows.push(...res.results ?? []);
|
||||
}
|
||||
const found = new Map(rows.map((r) => [r.id, r]));
|
||||
const missing = ids.filter((id) => !found.has(id));
|
||||
if (missing.length > 0) throw new Error(`entry not found: ${missing.join(", ")}`);
|
||||
if (recordOwnerId != null) {
|
||||
const foreign = rows.filter((r) => r.owner_id != null && r.owner_id !== recordOwnerId);
|
||||
if (foreign.length > 0) {
|
||||
throw new Error(
|
||||
`entry owner mismatch: ${foreign.map((r) => `${r.id}(${r.owner_id})`).join(", ")} != ${recordOwnerId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
return new Map(rows.map((r) => [r.id, r.content]));
|
||||
}
|
||||
async function recordBelongs(db, recordId) {
|
||||
const row = await db.prepare(`SELECT dst_id FROM entries WHERE src_id = ? AND rel_id = '${SYS_BELONGS}' AND dst_id != '${SYS_ROOT}' LIMIT 1`).bind(recordId).first();
|
||||
return row ?? null;
|
||||
}
|
||||
async function insertCellRelation(db, recordId, templateId, slot, dstEntryId, ownerId) {
|
||||
await db.prepare(
|
||||
`INSERT INTO entries (id, entry_type, owner_id, src_id, rel_id, dst_id) VALUES (?, 'relation', ?, ?, ?, ?)`
|
||||
).bind(uid2("relv"), ownerId, recordId, fieldEntryId(templateId, slot), dstEntryId).run();
|
||||
}
|
||||
async function createRecord(db, input) {
|
||||
const tpl = await getTemplate(db, input.template);
|
||||
if (!tpl) throw new Error(`template not found: ${input.template}`);
|
||||
const slots = JSON.parse(tpl.slots_json);
|
||||
const recordId = input.record_id ?? uid2("rec");
|
||||
for (const slot of slots) {
|
||||
if (!(slot in input.values)) continue;
|
||||
const values = input.values ?? {};
|
||||
const entryIds = input.entry_ids ?? {};
|
||||
const refSlots = Object.keys(entryIds);
|
||||
const ownerId = input.owner_id ?? null;
|
||||
const both = refSlots.filter((s) => s in values);
|
||||
if (both.length > 0) throw new Error(`slot given both value and entry_id: ${both.join(", ")}`);
|
||||
const unknown = refSlots.filter((s) => !slots.includes(s));
|
||||
if (unknown.length > 0) throw new Error(`slot not in template: ${unknown.join(", ")}`);
|
||||
const referenced = await loadReferencedEntries(db, entryIds, ownerId);
|
||||
await db.prepare(`INSERT OR IGNORE INTO entries (id, entry_type, owner_id) VALUES (?, 'record', ?)`).bind(recordId, ownerId).run();
|
||||
await db.prepare(
|
||||
`INSERT OR IGNORE INTO entries (id, entry_type, owner_id, src_id, rel_id, dst_id) VALUES (?, 'relation', ?, ?, '${SYS_BELONGS}', ?)`
|
||||
).bind(`relb_${recordId}_${tpl.id}`, ownerId, recordId, tpl.id).run();
|
||||
const writtenSlots = slots.filter((s) => s in entryIds || s in values);
|
||||
await ensureFieldEntries(db, tpl.id, writtenSlots);
|
||||
for (const slot of writtenSlots) {
|
||||
if (slot in entryIds) {
|
||||
await insertCellRelation(db, recordId, tpl.id, slot, entryIds[slot], ownerId);
|
||||
continue;
|
||||
}
|
||||
const entry = await createEntry(db, {
|
||||
content: input.values[slot],
|
||||
content: values[slot],
|
||||
entry_type: "value",
|
||||
owner_id: input.owner_id ?? null
|
||||
owner_id: ownerId
|
||||
});
|
||||
await db.prepare(`INSERT INTO entry_values (id, record_id, template_id, slot_name, entry_id) VALUES (?, ?, ?, ?, ?)`).bind(uid2("ev"), recordId, tpl.id, slot, entry.id).run();
|
||||
await insertCellRelation(db, recordId, tpl.id, slot, entry.id, ownerId);
|
||||
}
|
||||
return { record_id: recordId, template_id: tpl.id, values: input.values, owner_id: input.owner_id ?? null };
|
||||
const out = { ...values };
|
||||
for (const [slot, entryId] of Object.entries(entryIds)) out[slot] = referenced.get(entryId) ?? "";
|
||||
return { record_id: recordId, template_id: tpl.id, values: out, owner_id: ownerId };
|
||||
}
|
||||
async function updateRecord(db, recordId, values) {
|
||||
const evRes = await db.prepare(
|
||||
`SELECT ev.slot_name AS slot_name, ev.entry_id AS entry_id, ev.template_id AS template_id, e.owner_id AS owner_id
|
||||
FROM entry_values ev JOIN entries e ON ev.entry_id = e.id
|
||||
WHERE ev.record_id = ?`
|
||||
const belongs = await recordBelongs(db, recordId);
|
||||
if (!belongs) return null;
|
||||
const templateId = belongs.dst_id;
|
||||
const cellRes = await db.prepare(
|
||||
`SELECT f.content AS slot_name, r.dst_id AS entry_id
|
||||
FROM entries r JOIN entries f ON r.rel_id = f.id
|
||||
WHERE r.src_id = ? AND r.rel_id != '${SYS_BELONGS}'`
|
||||
).bind(recordId).all();
|
||||
const evRows = evRes.results ?? [];
|
||||
if (evRows.length === 0) return null;
|
||||
const templateId = evRows[0].template_id;
|
||||
const recordOwnerId = evRows.find((r) => r.owner_id != null)?.owner_id ?? null;
|
||||
const slotToEntry = new Map(evRows.map((r) => [r.slot_name, r.entry_id]));
|
||||
const cells = cellRes.results ?? [];
|
||||
const slotToEntries = /* @__PURE__ */ new Map();
|
||||
for (const c of cells) {
|
||||
const list = slotToEntries.get(c.slot_name) ?? [];
|
||||
list.push(c.entry_id);
|
||||
slotToEntries.set(c.slot_name, list);
|
||||
}
|
||||
const identity = await db.prepare("SELECT owner_id FROM entries WHERE id = ?").bind(recordId).first();
|
||||
const recordOwnerId = identity?.owner_id ?? null;
|
||||
const tpl = await getTemplate(db, templateId);
|
||||
const allowed = tpl ? JSON.parse(tpl.slots_json) : [...slotToEntry.keys()];
|
||||
const allowed = tpl ? JSON.parse(tpl.slots_json) : [...slotToEntries.keys()];
|
||||
for (const [slot, content] of Object.entries(values)) {
|
||||
if (!allowed.includes(slot)) {
|
||||
throw new Error(`slot not in template: ${slot}`);
|
||||
}
|
||||
const entryId = slotToEntry.get(slot);
|
||||
if (entryId) {
|
||||
await db.prepare(`UPDATE entries SET content = ?, updated_at = unixepoch() WHERE id = ?`).bind(content, entryId).run();
|
||||
const entryIds = slotToEntries.get(slot);
|
||||
if (entryIds && entryIds.length > 0) {
|
||||
for (const entryId of entryIds) {
|
||||
await db.prepare(`UPDATE entries SET content = ?, updated_at = unixepoch() WHERE id = ?`).bind(content, entryId).run();
|
||||
}
|
||||
} else {
|
||||
await ensureFieldEntries(db, templateId, [slot]);
|
||||
const entry = await createEntry(db, { content, entry_type: "value", owner_id: recordOwnerId });
|
||||
await db.prepare(`INSERT INTO entry_values (id, record_id, template_id, slot_name, entry_id) VALUES (?, ?, ?, ?, ?)`).bind(uid2("ev"), recordId, templateId, slot, entry.id).run();
|
||||
await insertCellRelation(db, recordId, templateId, slot, entry.id, recordOwnerId);
|
||||
}
|
||||
}
|
||||
return getRecord(db, recordId);
|
||||
}
|
||||
async function getRecord(db, recordId) {
|
||||
const belongs = await recordBelongs(db, recordId);
|
||||
if (!belongs) return null;
|
||||
const res = await db.prepare(
|
||||
`SELECT ev.slot_name as slot, e.content as content, ev.template_id as template_id, e.owner_id as owner_id
|
||||
FROM entry_values ev JOIN entries e ON ev.entry_id = e.id
|
||||
WHERE ev.record_id = ?`
|
||||
`SELECT f.content AS slot, v.content AS content
|
||||
FROM entries r
|
||||
JOIN entries f ON r.rel_id = f.id
|
||||
JOIN entries v ON r.dst_id = v.id
|
||||
WHERE r.src_id = ? AND r.rel_id != '${SYS_BELONGS}'`
|
||||
).bind(recordId).all();
|
||||
const rows = res.results ?? [];
|
||||
if (rows.length === 0) return null;
|
||||
const values = {};
|
||||
for (const r of rows) values[r.slot] = r.content;
|
||||
const owner_id = rows.find((r) => r.owner_id != null)?.owner_id ?? null;
|
||||
return { record_id: recordId, template_id: rows[0].template_id, values, owner_id };
|
||||
for (const r of res.results ?? []) values[r.slot] = r.content;
|
||||
const identity = await db.prepare("SELECT owner_id FROM entries WHERE id = ?").bind(recordId).first();
|
||||
return { record_id: recordId, template_id: belongs.dst_id, values, owner_id: identity?.owner_id ?? null };
|
||||
}
|
||||
async function searchByTemplate(db, template, owner_id, limit = 100) {
|
||||
const tpl = await getTemplate(db, template);
|
||||
if (!tpl) return [];
|
||||
const cap = Math.min(limit, 500);
|
||||
const res = owner_id ? await db.prepare(
|
||||
`SELECT DISTINCT ev.record_id as record_id FROM entry_values ev
|
||||
JOIN entries e ON ev.entry_id = e.id
|
||||
WHERE ev.template_id = ? AND e.owner_id = ?
|
||||
ORDER BY ev.created_at DESC LIMIT ?`
|
||||
).bind(tpl.id, owner_id, cap).all() : await db.prepare(`SELECT DISTINCT record_id FROM entry_values WHERE template_id = ? ORDER BY created_at DESC LIMIT ?`).bind(tpl.id, cap).all();
|
||||
`SELECT src_id AS record_id FROM entries
|
||||
WHERE rel_id = '${SYS_BELONGS}' AND dst_id = ? AND owner_id = ?
|
||||
ORDER BY created_at DESC, rowid DESC LIMIT ?`
|
||||
).bind(tpl.id, owner_id, cap).all() : await db.prepare(
|
||||
`SELECT src_id AS record_id FROM entries
|
||||
WHERE rel_id = '${SYS_BELONGS}' AND dst_id = ?
|
||||
ORDER BY created_at DESC, rowid DESC LIMIT ?`
|
||||
).bind(tpl.id, cap).all();
|
||||
const ids = (res.results ?? []).map((r) => r.record_id);
|
||||
if (ids.length === 0) return [];
|
||||
const byId = /* @__PURE__ */ new Map();
|
||||
for (const id of ids) byId.set(id, { record_id: id, template_id: tpl.id, values: {}, owner_id: null });
|
||||
for (let i = 0; i < ids.length; i += 90) {
|
||||
const chunk = ids.slice(i, i + 90);
|
||||
const placeholders = chunk.map(() => "?").join(",");
|
||||
const evRes = await db.prepare(
|
||||
`SELECT ev.record_id as record_id, ev.slot_name as slot, e.content as content, ev.template_id as template_id, e.owner_id as owner_id
|
||||
FROM entry_values ev JOIN entries e ON ev.entry_id = e.id
|
||||
WHERE ev.record_id IN (${placeholders})`
|
||||
).bind(...chunk).all();
|
||||
for (const r of evRes.results ?? []) {
|
||||
let rec = byId.get(r.record_id);
|
||||
if (!rec) {
|
||||
rec = { record_id: r.record_id, template_id: r.template_id, values: {}, owner_id: null };
|
||||
byId.set(r.record_id, rec);
|
||||
}
|
||||
rec.values[r.slot] = r.content;
|
||||
if (rec.owner_id == null && r.owner_id != null) rec.owner_id = r.owner_id;
|
||||
const [cellRes, identRes] = await Promise.all([
|
||||
db.prepare(
|
||||
`SELECT r.src_id AS record_id, f.content AS slot, v.content AS content
|
||||
FROM entries r
|
||||
JOIN entries f ON r.rel_id = f.id
|
||||
JOIN entries v ON r.dst_id = v.id
|
||||
WHERE r.src_id IN (${placeholders}) AND r.rel_id != '${SYS_BELONGS}'`
|
||||
).bind(...chunk).all(),
|
||||
db.prepare(`SELECT id, owner_id FROM entries WHERE id IN (${placeholders})`).bind(...chunk).all()
|
||||
]);
|
||||
for (const r of cellRes.results ?? []) {
|
||||
const rec = byId.get(r.record_id);
|
||||
if (rec) rec.values[r.slot] = r.content;
|
||||
}
|
||||
for (const r of identRes.results ?? []) {
|
||||
const rec = byId.get(r.id);
|
||||
if (rec) rec.owner_id = r.owner_id;
|
||||
}
|
||||
}
|
||||
return ids.map((id) => byId.get(id)).filter((r) => !!r);
|
||||
}
|
||||
async function deleteRecord(db, recordId) {
|
||||
const evRes = await db.prepare("SELECT entry_id FROM entry_values WHERE record_id = ?").bind(recordId).all();
|
||||
const rows = evRes.results ?? [];
|
||||
if (rows.length === 0) return false;
|
||||
await db.prepare("DELETE FROM entry_values WHERE record_id = ?").bind(recordId).run();
|
||||
for (const { entry_id } of rows) {
|
||||
await db.prepare("DELETE FROM entries WHERE id = ?").bind(entry_id).run();
|
||||
const belongs = await recordBelongs(db, recordId);
|
||||
if (!belongs) return false;
|
||||
const cellRes = await db.prepare(`SELECT dst_id FROM entries WHERE src_id = ? AND rel_id != '${SYS_BELONGS}'`).bind(recordId).all();
|
||||
const dsts = (cellRes.results ?? []).map((r) => r.dst_id);
|
||||
await db.prepare(`DELETE FROM entries WHERE src_id = ?`).bind(recordId).run();
|
||||
await db.prepare(
|
||||
`DELETE FROM entries WHERE id = ?1 AND entry_type = 'record'
|
||||
AND NOT EXISTS (SELECT 1 FROM entries WHERE dst_id = ?1)`
|
||||
).bind(recordId).run();
|
||||
for (const dst of dsts) {
|
||||
await db.prepare(
|
||||
`DELETE FROM entries WHERE id = ?1
|
||||
AND entry_type NOT IN ('sheet', 'field', 'system')
|
||||
AND NOT EXISTS (SELECT 1 FROM entries WHERE dst_id = ?1)
|
||||
AND NOT EXISTS (SELECT 1 FROM entries WHERE src_id = ?1)
|
||||
AND NOT EXISTS (SELECT 1 FROM entries WHERE rel_id = ?1)`
|
||||
).bind(dst).run();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -3395,10 +3563,17 @@ templateRoutes.patch("/:id", async (c) => {
|
||||
|
||||
// kbdb/src/routes/records.ts
|
||||
var recordRoutes = new Hono2();
|
||||
var isStringMap = (v) => !!v && typeof v === "object" && !Array.isArray(v) && Object.values(v).every((x) => typeof x === "string");
|
||||
recordRoutes.post("/", async (c) => {
|
||||
const body = await c.req.json().catch(() => null);
|
||||
if (!body || !body.template || !body.values) {
|
||||
return c.json({ success: false, error: "template and values required" }, 400);
|
||||
if (!body || !body.template || !body.values && !body.entry_ids) {
|
||||
return c.json({ success: false, error: "template and values (or entry_ids) required" }, 400);
|
||||
}
|
||||
if (body.values !== void 0 && !isStringMap(body.values)) {
|
||||
return c.json({ success: false, error: "values must be an object of {slot: string}" }, 400);
|
||||
}
|
||||
if (body.entry_ids !== void 0 && !isStringMap(body.entry_ids)) {
|
||||
return c.json({ success: false, error: "entry_ids must be an object of {slot: entry_id}" }, 400);
|
||||
}
|
||||
try {
|
||||
const rec = await createRecord(c.env.DB, body);
|
||||
@@ -3411,20 +3586,14 @@ recordRoutes.get("/triplet-stats", async (c) => {
|
||||
const owner = c.req.query("owner_id") || "";
|
||||
const rows = await c.env.DB.prepare(
|
||||
`SELECT
|
||||
COALESCE(NULLIF(lib_e.content, ''), 'general') AS library,
|
||||
COALESCE(NULLIF(lib_v.content, ''), 'general') AS library,
|
||||
COUNT(*) AS triplet_count
|
||||
FROM (
|
||||
SELECT DISTINCT ev.record_id
|
||||
FROM entry_values ev
|
||||
JOIN templates t ON ev.template_id = t.id
|
||||
JOIN entries e ON ev.entry_id = e.id
|
||||
WHERE t.name = 'triplet'
|
||||
AND (?1 = '' OR e.owner_id = ?1)
|
||||
) AS tr
|
||||
LEFT JOIN entry_values lev
|
||||
ON lev.record_id = tr.record_id AND lev.slot_name = 'library'
|
||||
LEFT JOIN entries lib_e ON lib_e.id = lev.entry_id
|
||||
GROUP BY COALESCE(NULLIF(lib_e.content, ''), 'general')
|
||||
FROM entries b
|
||||
JOIN templates t ON b.dst_id = t.id AND t.name = 'triplet'
|
||||
LEFT JOIN entries lr ON lr.src_id = b.src_id AND lr.rel_id = ('fld_' || b.dst_id || '_library')
|
||||
LEFT JOIN entries lib_v ON lib_v.id = lr.dst_id
|
||||
WHERE b.rel_id = 'sys_belongs' AND (?1 = '' OR b.owner_id = ?1)
|
||||
GROUP BY COALESCE(NULLIF(lib_v.content, ''), 'general')
|
||||
ORDER BY library`
|
||||
).bind(owner).all();
|
||||
const stats = (rows.results ?? []).map((r) => ({ library: r.library, triplet_count: r.triplet_count }));
|
||||
@@ -3604,31 +3773,35 @@ async function ensureTripletLibrarySlot(db, tripletTemplate) {
|
||||
return true;
|
||||
}
|
||||
function tripletPivotSql(ownerFiltered) {
|
||||
return `SELECT ev.record_id AS rid,
|
||||
MAX(CASE WHEN ev.slot_name = 'subject' THEN e.content END) AS subject,
|
||||
MAX(CASE WHEN ev.slot_name = 'object' THEN e.content END) AS object,
|
||||
MAX(CASE WHEN ev.slot_name = 'predicate' THEN e.content END) AS predicate,
|
||||
MAX(CASE WHEN ev.slot_name = 'status' THEN e.content END) AS status,
|
||||
MAX(CASE WHEN ev.slot_name = 'library' THEN e.content END) AS library,
|
||||
MAX(CASE WHEN ev.slot_name = 'source_uri' THEN e.content END) AS source_uri
|
||||
FROM entry_values ev JOIN entries e ON ev.entry_id = e.id
|
||||
WHERE ev.template_id = ?${ownerFiltered ? " AND e.owner_id = ?" : ""}
|
||||
GROUP BY ev.record_id`;
|
||||
return `SELECT b.src_id AS rid,
|
||||
MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_subject' THEN v.content END) AS subject,
|
||||
MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_object' THEN v.content END) AS object,
|
||||
MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_predicate' THEN v.content END) AS predicate,
|
||||
MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_status' THEN v.content END) AS status,
|
||||
MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_library' THEN v.content END) AS library,
|
||||
MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_source_uri' THEN v.content END) AS source_uri
|
||||
FROM entries b
|
||||
LEFT JOIN entries r ON r.src_id = b.src_id AND r.rel_id != 'sys_belongs'
|
||||
LEFT JOIN entries v ON v.id = r.dst_id
|
||||
WHERE b.rel_id = 'sys_belongs' AND b.dst_id = ?${ownerFiltered ? " AND b.owner_id = ?" : ""}
|
||||
GROUP BY b.src_id`;
|
||||
}
|
||||
function mapPivotSql(ownerFiltered) {
|
||||
return `SELECT ev.record_id AS rid,
|
||||
MAX(CASE WHEN ev.slot_name = 'library' THEN e.content END) AS library,
|
||||
MAX(CASE WHEN ev.slot_name = 'narrative' THEN e.content END) AS narrative,
|
||||
MAX(CASE WHEN ev.slot_name = 'top_entities' THEN e.content END) AS top_entities,
|
||||
MAX(CASE WHEN ev.slot_name = 'relation_profile' THEN e.content END) AS relation_profile,
|
||||
MAX(CASE WHEN ev.slot_name = 'bridges' THEN e.content END) AS bridges,
|
||||
MAX(CASE WHEN ev.slot_name = 'triplet_count' THEN e.content END) AS triplet_count,
|
||||
MAX(CASE WHEN ev.slot_name = 'commit_hash' THEN e.content END) AS commit_hash,
|
||||
MAX(CASE WHEN ev.slot_name = 'status' THEN e.content END) AS status,
|
||||
MAX(ev.created_at) AS ts
|
||||
FROM entry_values ev JOIN entries e ON ev.entry_id = e.id
|
||||
WHERE ev.template_id = ?${ownerFiltered ? " AND e.owner_id = ?" : ""}
|
||||
GROUP BY ev.record_id`;
|
||||
return `SELECT b.src_id AS rid,
|
||||
MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_library' THEN v.content END) AS library,
|
||||
MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_narrative' THEN v.content END) AS narrative,
|
||||
MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_top_entities' THEN v.content END) AS top_entities,
|
||||
MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_relation_profile' THEN v.content END) AS relation_profile,
|
||||
MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_bridges' THEN v.content END) AS bridges,
|
||||
MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_triplet_count' THEN v.content END) AS triplet_count,
|
||||
MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_commit_hash' THEN v.content END) AS commit_hash,
|
||||
MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_status' THEN v.content END) AS status,
|
||||
MAX(r.created_at) AS ts
|
||||
FROM entries b
|
||||
LEFT JOIN entries r ON r.src_id = b.src_id AND r.rel_id != 'sys_belongs'
|
||||
LEFT JOIN entries v ON v.id = r.dst_id
|
||||
WHERE b.rel_id = 'sys_belongs' AND b.dst_id = ?${ownerFiltered ? " AND b.owner_id = ?" : ""}
|
||||
GROUP BY b.src_id`;
|
||||
}
|
||||
function parseJsonArray(raw2) {
|
||||
if (!raw2) return [];
|
||||
@@ -3736,6 +3909,7 @@ async function recomputeLibraryMap(db, input) {
|
||||
await updateRecord(db, row.rid, { status: "superseded" });
|
||||
superseded.push(row.rid);
|
||||
}
|
||||
const entryCount = (await liveEntryCountsByLibrary(db, owner)).get(library) ?? 0;
|
||||
return {
|
||||
map: {
|
||||
record_id: blockEntry.id,
|
||||
@@ -3746,6 +3920,7 @@ async function recomputeLibraryMap(db, input) {
|
||||
relation_profile: relationProfile,
|
||||
bridges,
|
||||
triplet_count: tripletCount,
|
||||
entry_count: entryCount,
|
||||
commit_hash: input.commit_hash ?? null,
|
||||
status: "active",
|
||||
updated_at: blockEntry.created_at
|
||||
@@ -3759,18 +3934,36 @@ async function liveTripletCountsByLibrary(db, tripletTemplateId, owner_id) {
|
||||
const params = owner_id ? [tripletTemplateId, owner_id] : [tripletTemplateId];
|
||||
const res = await db.prepare(
|
||||
// kbdb-sql-ok:牆內本體(kbdb/src/actions/),checkout 開在巢狀 worktree matrix/arcrun/.worktree-fix-87/(避免打斷另一 session 佔用中的 matrix/arcrun 主 checkout),hook 逐字比對 matrix/arcrun/kbdb/src/ 吃不到中間多出的 worktree 目錄層,非繞牆
|
||||
`SELECT COALESCE(NULLIF(lib_e.content, ''), 'general') AS library, COUNT(*) AS n
|
||||
`SELECT COALESCE(NULLIF(tr.library, ''), 'general') AS library, COUNT(*) AS n
|
||||
FROM (
|
||||
SELECT ev.record_id AS rid,
|
||||
MAX(CASE WHEN ev.slot_name = 'status' THEN e.content END) AS status
|
||||
FROM entry_values ev JOIN entries e ON ev.entry_id = e.id
|
||||
WHERE ev.template_id = ?${owner_id ? " AND e.owner_id = ?" : ""}
|
||||
GROUP BY ev.record_id
|
||||
SELECT b.src_id AS rid,
|
||||
MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_status' THEN v.content END) AS status,
|
||||
MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_library' THEN v.content END) AS library
|
||||
FROM entries b
|
||||
LEFT JOIN entries r ON r.src_id = b.src_id AND r.rel_id != 'sys_belongs'
|
||||
LEFT JOIN entries v ON v.id = r.dst_id
|
||||
WHERE b.rel_id = 'sys_belongs' AND b.dst_id = ?${owner_id ? " AND b.owner_id = ?" : ""}
|
||||
GROUP BY b.src_id
|
||||
) AS tr
|
||||
LEFT JOIN entry_values lev ON lev.record_id = tr.rid AND lev.slot_name = 'library'
|
||||
LEFT JOIN entries lib_e ON lib_e.id = lev.entry_id
|
||||
WHERE COALESCE(tr.status, 'active') = 'active'
|
||||
GROUP BY COALESCE(NULLIF(lib_e.content, ''), 'general')`
|
||||
GROUP BY COALESCE(NULLIF(tr.library, ''), 'general')`
|
||||
).bind(...params).all();
|
||||
const m = /* @__PURE__ */ new Map();
|
||||
for (const r of res.results ?? []) m.set(r.library, r.n);
|
||||
return m;
|
||||
}
|
||||
async function liveEntryCountsByLibrary(db, owner_id) {
|
||||
const params = owner_id ? [owner_id] : [];
|
||||
const res = await db.prepare(
|
||||
// kbdb-sql-ok:牆內本體(kbdb/src/actions/),checkout 開在巢狀 worktree /private/tmp/wt-arcrun-library-map-honesty-87/(同 962d863/5919c6b 已記載的假警報成因:hook 逐字比對 matrix/arcrun/kbdb/src/ 吃不到中間多出的 worktree 目錄層,非繞牆)
|
||||
`SELECT COALESCE(NULLIF(json_extract(metadata_json, '$.library'), ''), 'general') AS library,
|
||||
COUNT(*) AS n
|
||||
FROM entries
|
||||
WHERE ${owner_id ? "owner_id = ? AND " : ""}entry_type != 'value'
|
||||
AND src_id IS NULL
|
||||
AND entry_type NOT IN ('record', 'sheet', 'field', 'system')
|
||||
AND NOT (entry_type = 'block' AND COALESCE(json_extract(metadata_json, '$.kind'), '') = 'library_map')
|
||||
GROUP BY COALESCE(NULLIF(json_extract(metadata_json, '$.library'), ''), 'general')`
|
||||
).bind(...params).all();
|
||||
const m = /* @__PURE__ */ new Map();
|
||||
for (const r of res.results ?? []) m.set(r.library, r.n);
|
||||
@@ -3788,10 +3981,12 @@ async function knownLibraryNames(db, owner_id) {
|
||||
if (libTpl) {
|
||||
const libParams = owner_id ? [libTpl.id, owner_id] : [libTpl.id];
|
||||
const libRows = await db.prepare(
|
||||
`SELECT MAX(CASE WHEN ev.slot_name = 'name' THEN e.content END) AS name
|
||||
FROM entry_values ev JOIN entries e ON ev.entry_id = e.id
|
||||
WHERE ev.template_id = ?${owner_id ? " AND e.owner_id = ?" : ""}
|
||||
GROUP BY ev.record_id`
|
||||
`SELECT MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_name' THEN v.content END) AS name
|
||||
FROM entries b
|
||||
LEFT JOIN entries r ON r.src_id = b.src_id AND r.rel_id != 'sys_belongs'
|
||||
LEFT JOIN entries v ON v.id = r.dst_id
|
||||
WHERE b.rel_id = 'sys_belongs' AND b.dst_id = ?${owner_id ? " AND b.owner_id = ?" : ""}
|
||||
GROUP BY b.src_id`
|
||||
).bind(...libParams).all();
|
||||
for (const r of libRows.results ?? []) if (r.name) names.add(r.name);
|
||||
}
|
||||
@@ -3825,11 +4020,15 @@ async function listLibraryMaps(db, owner_id) {
|
||||
const tpl = await getTemplate(db, LIBRARY_MAP_TEMPLATE_NAME);
|
||||
if (!tpl) return [];
|
||||
const params = owner_id ? [tpl.id, owner_id] : [tpl.id];
|
||||
const res = await db.prepare(
|
||||
`WITH m AS (${mapPivotSql(!!owner_id)})
|
||||
SELECT * FROM m WHERE COALESCE(m.status, 'active') = 'active' AND m.library IS NOT NULL
|
||||
ORDER BY m.ts DESC`
|
||||
).bind(...params).all();
|
||||
const [res, entryCounts] = await Promise.all([
|
||||
db.prepare(
|
||||
// kbdb-sql-ok:牆內本體(kbdb/src/actions/),既有查詢(listLibraryMaps 原本就有)此次改包進 Promise.all 才重新觸發掃描,非新增違規;worktree 路徑假警報同上方 liveEntryCountsByLibrary 註解
|
||||
`WITH m AS (${mapPivotSql(!!owner_id)})
|
||||
SELECT * FROM m WHERE COALESCE(m.status, 'active') = 'active' AND m.library IS NOT NULL
|
||||
ORDER BY m.ts DESC`
|
||||
).bind(...params).all(),
|
||||
liveEntryCountsByLibrary(db, owner_id)
|
||||
]);
|
||||
const byLib = /* @__PURE__ */ new Map();
|
||||
for (const r of res.results ?? []) {
|
||||
if (!r.library || byLib.has(r.library)) continue;
|
||||
@@ -3838,6 +4037,7 @@ async function listLibraryMaps(db, owner_id) {
|
||||
narrative: r.narrative || null,
|
||||
top_entities: parseJsonArray(r.top_entities).slice(0, 3).map((t) => t.name),
|
||||
triplet_count: Number(r.triplet_count ?? 0) || 0,
|
||||
entry_count: entryCounts.get(r.library) ?? 0,
|
||||
updated_at: r.ts
|
||||
});
|
||||
}
|
||||
@@ -3853,7 +4053,10 @@ async function getLibraryMapDetail(db, library, owner_id) {
|
||||
ORDER BY m.ts DESC LIMIT 1`
|
||||
).bind(...params).first();
|
||||
if (!row) return null;
|
||||
const blockEntry = await getEntry(db, row.rid);
|
||||
const [blockEntry, entryCounts] = await Promise.all([
|
||||
getEntry(db, row.rid),
|
||||
liveEntryCountsByLibrary(db, owner_id)
|
||||
]);
|
||||
return {
|
||||
record_id: row.rid,
|
||||
library,
|
||||
@@ -3863,6 +4066,7 @@ async function getLibraryMapDetail(db, library, owner_id) {
|
||||
relation_profile: parseJsonArray(row.relation_profile),
|
||||
bridges: parseJsonArray(row.bridges),
|
||||
triplet_count: Number(row.triplet_count ?? 0) || 0,
|
||||
entry_count: entryCounts.get(library) ?? 0,
|
||||
commit_hash: row.commit_hash || null,
|
||||
status: row.status ?? "active",
|
||||
updated_at: row.ts
|
||||
@@ -4150,6 +4354,12 @@ app.use("*", async (c, next) => {
|
||||
});
|
||||
app.get("/", (c) => c.json({ service: "arcrun-kbdb", tier: "base", status: "ok" }));
|
||||
app.get("/health", (c) => c.json({ ok: true }));
|
||||
app.get("/maintenance/relation-orphans", async (c) => {
|
||||
const { scanRelationOrphans: scanRelationOrphans2 } = await Promise.resolve().then(() => (init_relation_orphans(), relation_orphans_exports));
|
||||
const limit = Number(c.req.query("limit") ?? "200");
|
||||
const report = await scanRelationOrphans2(c.env.DB, Number.isFinite(limit) ? limit : 200);
|
||||
return c.json({ success: true, ...report });
|
||||
});
|
||||
app.route("/entries", entryRoutes);
|
||||
app.route("/templates", templateRoutes);
|
||||
app.route("/records", recordRoutes);
|
||||
|
||||
@@ -31262,16 +31262,109 @@ function registerAllWorkflowCrudTools(server, env) {
|
||||
registerRunWorkflow(server, env);
|
||||
}
|
||||
|
||||
// mcp/src/lib/portal-client.ts
|
||||
async function portalFetch(env, session, path, opts = {}) {
|
||||
if (!env.CYPHER_EXECUTOR) {
|
||||
throw new Error("CYPHER_EXECUTOR service binding not configured");
|
||||
}
|
||||
const url = new URL(`https://cypher${path}`);
|
||||
for (const [k, v] of Object.entries(opts.query ?? {})) {
|
||||
if (v !== void 0 && v !== "") url.searchParams.set(k, String(v));
|
||||
}
|
||||
return env.CYPHER_EXECUTOR.fetch(url.toString(), {
|
||||
method: opts.method ?? "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${session}`
|
||||
},
|
||||
body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
|
||||
});
|
||||
}
|
||||
function resolveKnowledgeIdentity(authPath, portal) {
|
||||
if (authPath !== "oauth") return { kind: "service" };
|
||||
return portal?.session ? { kind: "portal", portal } : { kind: "stale" };
|
||||
}
|
||||
function staleIdentityError() {
|
||||
return errorResponse(
|
||||
"identity_missing",
|
||||
"\u9019\u689D MCP \u9023\u7DDA\u662F\u820A\u7248\u7C3D\u767C\u7684 token\uFF0C\u88E1\u9762\u6C92\u6709\u767B\u5165\u8005\u8EAB\u5206\uFF0C\u56E0\u6B64\u67E5\u4E0D\u5230\u4EFB\u4F55\u77E5\u8B58\u5167\u5BB9\u3002\u91CD\u65B0\u9023\u7DDA\u4E00\u6B21\uFF08\u5728 claude.ai \u7684 connector \u8A2D\u5B9A\u88E1\u91CD\u65B0\u6388\u6B0A\u3001\u8F38\u5165\u4F60\u7684 Portal \u5E33\u5BC6\uFF09\u5373\u53EF\u2014\u2014\u4E0D\u9700\u8981\u53E6\u5916\u627E\u4EFB\u4F55 credential \u6216\u91D1\u9470\u3002",
|
||||
[
|
||||
"\u5230 claude.ai \u2192 Settings \u2192 Connectors\uFF0C\u628A\u9019\u500B connector \u91CD\u65B0\u9023\u7DDA\u4E00\u6B21\uFF08\u6703\u8DF3\u51FA\u8F38\u5165 Portal \u5E33\u5BC6\u7684\u9801\u9762\uFF09",
|
||||
"\u91CD\u9023\u5F8C kbdb_* \u5168\u90E8\u5DE5\u5177\u90FD\u6703\u7528\u4F60\u9019\u500B\u5E33\u865F\u7684\u6B0A\u9650\u67E5\u8A62"
|
||||
]
|
||||
);
|
||||
}
|
||||
async function portalError(res, what) {
|
||||
const detail = await res.text().catch(() => "");
|
||||
if (res.status === 401) {
|
||||
return errorResponse(
|
||||
"session_expired",
|
||||
`${what}\u5931\u6557\uFF1A\u767B\u5165\u968E\u6BB5\u5DF2\u904E\u671F\uFF08portal session \u5230\u671F\u6216\u5DF2\u767B\u51FA\uFF09\u3002`,
|
||||
[
|
||||
"\u5230 claude.ai \u2192 Settings \u2192 Connectors \u91CD\u65B0\u9023\u7DDA\u9019\u500B connector\uFF08\u91CD\u65B0\u8F38\u5165 Portal \u5E33\u5BC6\uFF09",
|
||||
"\u91CD\u9023\u5F8C\u6B0A\u9650\u8207\u4F60\u5728 portal \u7DB2\u9801\u4E0A\u770B\u5230\u7684\u4E00\u81F4"
|
||||
],
|
||||
detail
|
||||
);
|
||||
}
|
||||
if (res.status === 403) {
|
||||
return errorResponse(
|
||||
"forbidden",
|
||||
`${what}\u5931\u6557\uFF1A\u9019\u500B\u5E33\u865F\u6C92\u6709\u9019\u9805\u6B0A\u9650\uFF08\u5E33\u865F\u53EF\u80FD\u5DF2\u505C\u7528\uFF0C\u6216\u6C92\u6709\u88AB\u6388\u6B0A\u8A72\u77E5\u8B58\u5EAB\uFF09\u3002`,
|
||||
["\u8ACB\u77E5\u8B58\u5EAB\u7BA1\u7406\u54E1\u5728 portal \u7684\u5E33\u865F\u7BA1\u7406\u88E1\u78BA\u8A8D\u4F60\u7684\u72C0\u614B\u8207\u53EF\u7528\u77E5\u8B58\u5EAB"],
|
||||
detail
|
||||
);
|
||||
}
|
||||
return errorResponse(`portal_${res.status}`, `${what}\u5931\u6557\uFF08HTTP ${res.status}\uFF09`, ["\u7A0D\u5F8C\u91CD\u8A66"], detail);
|
||||
}
|
||||
|
||||
// mcp/src/tools/arcrun_skills_examples.ts
|
||||
var KbdbAccessError = class extends Error {
|
||||
constructor(status, what, detail) {
|
||||
super(`KBDB ${what} HTTP ${status}`);
|
||||
this.status = status;
|
||||
this.what = what;
|
||||
this.detail = detail;
|
||||
}
|
||||
status;
|
||||
what;
|
||||
detail;
|
||||
};
|
||||
function kbdbFailure(e, searchHint, identity) {
|
||||
const portalNote = identity.kind === "portal" ? "\u4F60\u9019\u689D\u662F\u5E33\u5BC6\u767B\u5165\u7684\u9023\u7DDA\uFF0C\u4F46 skill\uFF0Fexample \u9019\u6279\u5DE5\u5177\u76EE\u524D\u4ECD\u8D70**\u670D\u52D9\u5167\u90E8\u6191\u64DA**\uFF08\u9084\u6C92\u63A5\u4E0A\u767B\u5165\u8EAB\u5206\uFF09\u2014\u2014\u6240\u4EE5\u5B83\u8B80\u4E0D\u5230\uFF0C\u4E0D\u4EE3\u8868\u4F60\u7684\u5E33\u865F\u8B80\u4E0D\u5230\u3002" : "";
|
||||
if (e instanceof KbdbAccessError) {
|
||||
const unauthorized = e.status === 401 || e.status === 403;
|
||||
return errorResponse(
|
||||
unauthorized ? "kbdb_unauthorized" : "kbdb_unreachable",
|
||||
(unauthorized ? `\u8B80\u4E0D\u5230 KBDB\uFF08HTTP ${e.status}\uFF1A\u9019\u689D\u9023\u7DDA\u7684\u6191\u64DA\u88AB\u62D2\u6216\u6839\u672C\u6C92\u5E36\uFF09\u3002` : `\u8B80\u4E0D\u5230 KBDB\uFF08HTTP ${e.status}\uFF09\u3002`) + "\u{1F534} **\u9019\u662F\u300C\u8B80\u4E0D\u5230\u300D\uFF0C\u4E0D\u662F\u300C\u4E0D\u5B58\u5728\u300D**\u2014\u2014\u5167\u5BB9\u9084\u5728\u5EAB\u88E1\uFF0C\u53EA\u662F\u9019\u689D\u8DEF\u88AB\u64CB\u4F4F\u4E86\u3002" + (portalNote ? ` ${portalNote}` : ""),
|
||||
[
|
||||
searchHint,
|
||||
"kbdb_get_map() \u770B\u9019\u53F0\u5BE6\u4F8B\u6709\u54EA\u4E9B\u5EAB\uFF08\u90A3\u689D\u8D70\u5F97\u901A\u5C31\u66F4\u78BA\u5B9A\u662F\u9019\u6279\u5DE5\u5177\u7684\u8DEF\u58DE\u4E86\uFF0C\u4E0D\u662F\u5EAB\u7A7A\u4E86\uFF09",
|
||||
"\u{1F534} \u4E0D\u51C6\u628A\u9019\u500B\u932F\u8AA4\u56DE\u5831\u6210\u300C\u627E\u4E0D\u5230\uFF0F\u6C92\u6709\u9019\u500B skill\u300D\u2014\u2014\u8ACB\u7167\u5BE6\u8AAA\u300CKBDB \u9019\u689D\u8DEF\u8B80\u4E0D\u5230\u300D",
|
||||
"\u6301\u7E8C\u5931\u6557\uFF1A\u544A\u8A34 leo \u9019\u53F0\u5BE6\u4F8B\u7684 arcrun-mcp \u5C11\u4E86 secret KBDB_INTERNAL_TOKEN"
|
||||
],
|
||||
e.detail
|
||||
);
|
||||
}
|
||||
return errorResponse(
|
||||
"kbdb_unreachable",
|
||||
`\u8B80\u4E0D\u5230 KBDB\uFF1A${e instanceof Error ? e.message : String(e)}\u3002\u{1F534} **\u9019\u662F\u300C\u8B80\u4E0D\u5230\u300D\uFF0C\u4E0D\u662F\u300C\u4E0D\u5B58\u5728\u300D**\u3002` + (portalNote ? ` ${portalNote}` : ""),
|
||||
[searchHint, "\u7A0D\u5F8C\u91CD\u8A66", "\u{1F534} \u4E0D\u51C6\u628A\u5B83\u56DE\u5831\u6210\u300C\u6C92\u6709\u9019\u500B skill\uFF0Fexample\u300D"]
|
||||
);
|
||||
}
|
||||
async function kbdbList(env, entryType, limit = 100) {
|
||||
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}`);
|
||||
if (!resp.ok) {
|
||||
throw new KbdbAccessError(resp.status, `list entry_type=${entryType}`, await resp.text().catch(() => ""));
|
||||
}
|
||||
const data = await resp.json();
|
||||
return data.entries ?? [];
|
||||
}
|
||||
async function kbdbGetByPageName(env, pageName) {
|
||||
const resp = await kbdbFetch(env, `/entries?page_name=${encodeURIComponent(pageName)}&limit=1`);
|
||||
if (!resp.ok) return null;
|
||||
if (!resp.ok) {
|
||||
throw new KbdbAccessError(resp.status, `get page_name=${pageName}`, await resp.text().catch(() => ""));
|
||||
}
|
||||
const data = await resp.json();
|
||||
return data.entries?.[0] ?? null;
|
||||
}
|
||||
@@ -31284,7 +31377,7 @@ function parseTags(tagsJson) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
function registerListSkills(server, env) {
|
||||
function registerListSkills(server, env, identity) {
|
||||
server.tool(
|
||||
toolName("list_skills"),
|
||||
"\u5217\u6240\u6709 agent-skill blocks\uFF08\u5F9E arcrun/registry/skills/ \u540C\u6B65\u9032 KBDB\uFF09\u3002\u6BCF\u500B skill \u662F\u500B markdown playbook\uFF0C\u63CF\u8FF0 AI \u9762\u5C0D X \u554F\u984C\u8A72\u600E\u9EBC\u60F3 + \u8A72\u7528\u54EA\u500B example\u3002\u56DE [{slug, title, tags}]\u3002call get_skill(slug) \u62FF\u5B8C\u6574\u5167\u6587\u3002",
|
||||
@@ -31292,6 +31385,7 @@ function registerListSkills(server, env) {
|
||||
tag: external_exports.string().optional().describe("optional \u6A19\u7C64\u904E\u6FFE\u3002\u5982 '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) => {
|
||||
@@ -31313,20 +31407,19 @@ function registerListSkills(server, env) {
|
||||
return successResponse(
|
||||
{ count: skills.length, skills },
|
||||
[
|
||||
skills.length === 0 ? "\u6C92\u6709 skill \u547D\u4E2D\u3002\u8A66 list_skills() \u4E0D\u5E36 tag \u770B\u5168\u90E8" : "call arcrun_get_skill(slug) \u62FF\u55AE\u500B skill \u5B8C\u6574 markdown"
|
||||
skills.length === 0 ? "\u6C92\u6709 skill \u547D\u4E2D\u3002\u8A66 list_skills() \u4E0D\u5E36 tag \u770B\u5168\u90E8" : "call arcrun_get_skill(slug) \u62FF\u55AE\u500B skill \u5B8C\u6574 markdown",
|
||||
// 誠實:這裡回的是**這台實例被 seed 進去的那幾支**,不是「全世界的 skill 目錄」。
|
||||
// 上面清單沒有的名字(例如 'INDEX')就是這台沒有——別照舊教材去猜一個 slug。
|
||||
"\u{1F534} \u53EA\u7528\u4E0A\u9762\u6E05\u55AE\u88E1\u771F\u7684\u6709\u7684 slug\uFF1B\u6E05\u55AE\u6C92\u6709\uFF1D\u9019\u53F0\u5BE6\u4F8B\u6C92 seed \u9032\u53BB\uFF0C\u4E0D\u8981\u786C\u731C\u540D\u5B57"
|
||||
]
|
||||
);
|
||||
} catch (e) {
|
||||
return errorResponse(
|
||||
"fetch_failed",
|
||||
e instanceof Error ? e.message : String(e),
|
||||
["\u7A0D\u5F8C\u91CD\u8A66", "\u82E5\u6301\u7E8C\u5931\u6557\uFF0C\u544A\u8A34 leo"]
|
||||
);
|
||||
return kbdbFailure(e, "\u6539\u7528 kbdb_search({ q: 'skill' }) \u76F4\u63A5\u5728\u77E5\u8B58\u5EAB\u88E1\u627E skill \u5361\u7247\uFF08\u90A3\u689D\u8DEF\u8D70\u7684\u662F\u53E6\u4E00\u7D44\u6191\u64DA\uFF09", identity);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
function registerGetSkill(server, env) {
|
||||
function registerGetSkill(server, env, identity) {
|
||||
server.tool(
|
||||
toolName("get_skill"),
|
||||
"\u62FF\u55AE\u4E00 agent-skill \u5B8C\u6574 markdown playbook\u3002slug \u5F9E list_skills \u53D6\u5F97\u3002",
|
||||
@@ -31334,15 +31427,17 @@ function registerGetSkill(server, env) {
|
||||
slug: external_exports.string().describe("skill slug\uFF0C\u4F8B\u5982 '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) {
|
||||
return errorResponse(
|
||||
"not_found",
|
||||
`skill "${slug}" \u4E0D\u5B58\u5728`,
|
||||
`KBDB \u6B63\u5E38\u56DE\u61C9\uFF0C\u4F46\u6C92\u6709 page_name="${pageName}" \u9019\u5F35\u5361\u2014\u2014\u9019\u53F0\u5BE6\u4F8B\u6C92\u6709 seed \u9019\u652F skill\u3002\uFF08\u4E0D\u540C\u5BE6\u4F8B seed \u7684 skill \u4E0D\u4E00\u6A23\uFF0C\u5225\u7167\u820A\u6559\u6750\u5047\u8A2D\u67D0\u500B\u540D\u5B57\u4E00\u5B9A\u5728\u3002\uFF09`,
|
||||
[
|
||||
"call arcrun_list_skills() \u770B\u53EF\u7528 slug",
|
||||
"call arcrun_list_skills() \u770B**\u9019\u53F0\u5BE6\u4F8B\u771F\u7684\u6709**\u54EA\u5E7E\u652F",
|
||||
`kbdb_search({ q: '${slug}' }) \u770B\u5167\u5BB9\u662F\u4E0D\u662F\u88AB\u5B58\u6210\u5225\u7684\u540D\u5B57`,
|
||||
"\u78BA\u8A8D\u62FC\u5B57\u6B63\u78BA\uFF08\u4E0D\u9700\u8981 'skill-' prefix\uFF09"
|
||||
]
|
||||
);
|
||||
@@ -31354,16 +31449,16 @@ function registerGetSkill(server, env) {
|
||||
tags: parseTags(block.tags_json)
|
||||
});
|
||||
} catch (e) {
|
||||
return errorResponse(
|
||||
"fetch_failed",
|
||||
e instanceof Error ? e.message : String(e),
|
||||
["\u7A0D\u5F8C\u91CD\u8A66"]
|
||||
return kbdbFailure(
|
||||
e,
|
||||
`\u6539\u7528 kbdb_search({ q: 'skill-${slug}' }) \u6488\u540C\u4E00\u5F35\u5361\u7247\uFF08skill \u5C31\u4F4F\u5728\u77E5\u8B58\u5EAB\u7684 entries \u88E1\uFF0C\u90A3\u689D\u8DEF\u8D70\u53E6\u4E00\u7D44\u6191\u64DA\uFF09`,
|
||||
identity
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
function registerListExamples(server, env) {
|
||||
function registerListExamples(server, env, identity) {
|
||||
server.tool(
|
||||
toolName("list_examples"),
|
||||
"\u5217\u6240\u6709 workflow-example blocks\uFF08\u5F9E arcrun/registry/examples/ \u540C\u6B65\u9032 KBDB\uFF09\u3002\u6BCF\u500B example \u662F\u53EF\u76F4\u63A5 push \u7684 workflow YAML \u7BC4\u672C + description\u3002\u56DE [{slug, tags}]\u3002call get_example / search_examples \u62FF\u7D30\u7BC0\u3002",
|
||||
@@ -31371,6 +31466,7 @@ function registerListExamples(server, env) {
|
||||
tag: external_exports.string().optional().describe("optional \u6A19\u7C64\u904E\u6FFE\u3002\u5982 '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) => {
|
||||
@@ -31389,16 +31485,12 @@ function registerListExamples(server, env) {
|
||||
]
|
||||
);
|
||||
} catch (e) {
|
||||
return errorResponse(
|
||||
"fetch_failed",
|
||||
e instanceof Error ? e.message : String(e),
|
||||
["\u7A0D\u5F8C\u91CD\u8A66"]
|
||||
);
|
||||
return kbdbFailure(e, "\u6539\u7528 kbdb_search({ q: 'example' }) \u76F4\u63A5\u5728\u77E5\u8B58\u5EAB\u88E1\u627E example \u5361\u7247\uFF08\u90A3\u689D\u8DEF\u8D70\u53E6\u4E00\u7D44\u6191\u64DA\uFF09", identity);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
function registerGetExample(server, env) {
|
||||
function registerGetExample(server, env, identity) {
|
||||
server.tool(
|
||||
toolName("get_example"),
|
||||
"\u62FF\u55AE\u4E00 workflow-example \u5B8C\u6574 YAML + description\u3002slug \u5F9E list_examples / search_examples \u53D6\u5F97\u3002\u53EF\u76F4\u63A5\u62FF YAML \u6539\u6210\u4F60\u81EA\u5DF1\u7684 \u2192 push\u3002",
|
||||
@@ -31406,16 +31498,18 @@ function registerGetExample(server, env) {
|
||||
slug: external_exports.string().describe("example slug\uFF0C\u4F8B\u5982 '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) {
|
||||
return errorResponse(
|
||||
"not_found",
|
||||
`example "${slug}" \u4E0D\u5B58\u5728`,
|
||||
`KBDB \u6B63\u5E38\u56DE\u61C9\uFF0C\u4F46\u6C92\u6709 page_name="${pageName}" \u9019\u5F35\u5361\u2014\u2014\u9019\u53F0\u5BE6\u4F8B\u6C92 seed \u9019\u500B example\u3002`,
|
||||
[
|
||||
"call arcrun_list_examples() \u770B\u53EF\u7528 slug",
|
||||
"\u6216 arcrun_search_examples(use_case) \u7528\u81EA\u7136\u8A9E\u8A00\u627E"
|
||||
"call arcrun_list_examples() \u770B**\u9019\u53F0\u5BE6\u4F8B\u771F\u7684\u6709**\u54EA\u4E9B slug",
|
||||
"\u6216 arcrun_search_examples(use_case) \u7528\u95DC\u9375\u5B57\u627E",
|
||||
`kbdb_search({ q: '${slug}' }) \u770B\u5167\u5BB9\u662F\u4E0D\u662F\u88AB\u5B58\u6210\u5225\u7684\u540D\u5B57`
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -31436,16 +31530,16 @@ function registerGetExample(server, env) {
|
||||
"\u770B description_md \u4E86\u89E3\u8A2D\u8A08\u610F\u5716 / \u6539\u9020\u65B9\u5411"
|
||||
]);
|
||||
} catch (e) {
|
||||
return errorResponse(
|
||||
"fetch_failed",
|
||||
e instanceof Error ? e.message : String(e),
|
||||
["\u7A0D\u5F8C\u91CD\u8A66"]
|
||||
return kbdbFailure(
|
||||
e,
|
||||
`\u6539\u7528 kbdb_search({ q: 'example-${slug}' }) \u6488\u540C\u4E00\u5F35\u5361\u7247\uFF08example \u5C31\u4F4F\u5728\u77E5\u8B58\u5EAB\u7684 entries \u88E1\uFF09`,
|
||||
identity
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
function registerSearchExamples(server, env) {
|
||||
function registerSearchExamples(server, env, identity) {
|
||||
server.tool(
|
||||
toolName("search_examples"),
|
||||
"\u7528 use case \u95DC\u9375\u5B57\u641C workflow examples\uFF0C\u56DE\u6700\u76F8\u95DC N \u500B\u3002\u6CE8\u610F\uFF1A\u57FA\u672C\u76E4\u76EE\u524D\u662F D1 LIKE \u95DC\u9375\u5B57\u641C\u5C0B\uFF08\u975E\u8A9E\u7FA9 embedding\uFF1B\u8A9E\u7FA9\u662F kbdb-base Phase 1 \u7684 embed \u6A21\u7D44\uFF0C\u5C1A\u672A\u4E0A\uFF09\u3002\u2192 \u7528\u5177\u9AD4\u8A5E\uFF08'email'\u3001'cron'\u3001'rag'\uFF09\u6BD4\u6574\u53E5\u81EA\u7136\u8A9E\u8A00\u547D\u4E2D\u7387\u9AD8\u3002\u4E5F\u6703\u6BD4\u5C0D slug/tag\u3002",
|
||||
@@ -31454,6 +31548,7 @@ function registerSearchExamples(server, env) {
|
||||
top_k: external_exports.number().int().min(1).max(20).optional().describe("\u56DE\u5E7E\u500B\u7D50\u679C\uFF08\u9810\u8A2D 5\uFF09")
|
||||
},
|
||||
async ({ query, top_k }) => {
|
||||
if (identity.kind === "stale") return staleIdentityError();
|
||||
try {
|
||||
const k = top_k ?? 5;
|
||||
const q = query.trim();
|
||||
@@ -31498,21 +31593,17 @@ function registerSearchExamples(server, env) {
|
||||
]
|
||||
);
|
||||
} catch (e) {
|
||||
return errorResponse(
|
||||
"internal_error",
|
||||
e instanceof Error ? e.message : String(e),
|
||||
["\u91CD\u8A66\u4E00\u6B21"]
|
||||
);
|
||||
return kbdbFailure(e, `\u6539\u7528 kbdb_search({ q: '${query.trim()}' }) \u76F4\u63A5\u67E5\u77E5\u8B58\u5EAB\uFF08\u90A3\u689D\u8DEF\u8D70\u53E6\u4E00\u7D44\u6191\u64DA\uFF09`, identity);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
function registerAllSkillExampleTools(server, env) {
|
||||
registerListSkills(server, env);
|
||||
registerGetSkill(server, env);
|
||||
registerListExamples(server, env);
|
||||
registerGetExample(server, env);
|
||||
registerSearchExamples(server, env);
|
||||
function registerAllSkillExampleTools(server, env, identity) {
|
||||
registerListSkills(server, env, identity);
|
||||
registerGetSkill(server, env, identity);
|
||||
registerListExamples(server, env, identity);
|
||||
registerGetExample(server, env, identity);
|
||||
registerSearchExamples(server, env, identity);
|
||||
}
|
||||
|
||||
// mcp/src/tools/arcrun_recipe.ts
|
||||
@@ -31713,62 +31804,6 @@ function registerRecipeDelete(server, env) {
|
||||
);
|
||||
}
|
||||
|
||||
// mcp/src/lib/portal-client.ts
|
||||
async function portalFetch(env, session, path, opts = {}) {
|
||||
if (!env.CYPHER_EXECUTOR) {
|
||||
throw new Error("CYPHER_EXECUTOR service binding not configured");
|
||||
}
|
||||
const url = new URL(`https://cypher${path}`);
|
||||
for (const [k, v] of Object.entries(opts.query ?? {})) {
|
||||
if (v !== void 0 && v !== "") url.searchParams.set(k, String(v));
|
||||
}
|
||||
return env.CYPHER_EXECUTOR.fetch(url.toString(), {
|
||||
method: opts.method ?? "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${session}`
|
||||
},
|
||||
body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
|
||||
});
|
||||
}
|
||||
function resolveKnowledgeIdentity(authPath, portal) {
|
||||
if (authPath !== "oauth") return { kind: "service" };
|
||||
return portal?.session ? { kind: "portal", portal } : { kind: "stale" };
|
||||
}
|
||||
function staleIdentityError() {
|
||||
return errorResponse(
|
||||
"identity_missing",
|
||||
"\u9019\u689D MCP \u9023\u7DDA\u662F\u820A\u7248\u7C3D\u767C\u7684 token\uFF0C\u88E1\u9762\u6C92\u6709\u767B\u5165\u8005\u8EAB\u5206\uFF0C\u56E0\u6B64\u67E5\u4E0D\u5230\u4EFB\u4F55\u77E5\u8B58\u5167\u5BB9\u3002\u91CD\u65B0\u9023\u7DDA\u4E00\u6B21\uFF08\u5728 claude.ai \u7684 connector \u8A2D\u5B9A\u88E1\u91CD\u65B0\u6388\u6B0A\u3001\u8F38\u5165\u4F60\u7684 Portal \u5E33\u5BC6\uFF09\u5373\u53EF\u2014\u2014\u4E0D\u9700\u8981\u53E6\u5916\u627E\u4EFB\u4F55 credential \u6216\u91D1\u9470\u3002",
|
||||
[
|
||||
"\u5230 claude.ai \u2192 Settings \u2192 Connectors\uFF0C\u628A\u9019\u500B connector \u91CD\u65B0\u9023\u7DDA\u4E00\u6B21\uFF08\u6703\u8DF3\u51FA\u8F38\u5165 Portal \u5E33\u5BC6\u7684\u9801\u9762\uFF09",
|
||||
"\u91CD\u9023\u5F8C kbdb_* \u5168\u90E8\u5DE5\u5177\u90FD\u6703\u7528\u4F60\u9019\u500B\u5E33\u865F\u7684\u6B0A\u9650\u67E5\u8A62"
|
||||
]
|
||||
);
|
||||
}
|
||||
async function portalError(res, what) {
|
||||
const detail = await res.text().catch(() => "");
|
||||
if (res.status === 401) {
|
||||
return errorResponse(
|
||||
"session_expired",
|
||||
`${what}\u5931\u6557\uFF1A\u767B\u5165\u968E\u6BB5\u5DF2\u904E\u671F\uFF08portal session \u5230\u671F\u6216\u5DF2\u767B\u51FA\uFF09\u3002`,
|
||||
[
|
||||
"\u5230 claude.ai \u2192 Settings \u2192 Connectors \u91CD\u65B0\u9023\u7DDA\u9019\u500B connector\uFF08\u91CD\u65B0\u8F38\u5165 Portal \u5E33\u5BC6\uFF09",
|
||||
"\u91CD\u9023\u5F8C\u6B0A\u9650\u8207\u4F60\u5728 portal \u7DB2\u9801\u4E0A\u770B\u5230\u7684\u4E00\u81F4"
|
||||
],
|
||||
detail
|
||||
);
|
||||
}
|
||||
if (res.status === 403) {
|
||||
return errorResponse(
|
||||
"forbidden",
|
||||
`${what}\u5931\u6557\uFF1A\u9019\u500B\u5E33\u865F\u6C92\u6709\u9019\u9805\u6B0A\u9650\uFF08\u5E33\u865F\u53EF\u80FD\u5DF2\u505C\u7528\uFF0C\u6216\u6C92\u6709\u88AB\u6388\u6B0A\u8A72\u77E5\u8B58\u5EAB\uFF09\u3002`,
|
||||
["\u8ACB\u77E5\u8B58\u5EAB\u7BA1\u7406\u54E1\u5728 portal \u7684\u5E33\u865F\u7BA1\u7406\u88E1\u78BA\u8A8D\u4F60\u7684\u72C0\u614B\u8207\u53EF\u7528\u77E5\u8B58\u5EAB"],
|
||||
detail
|
||||
);
|
||||
}
|
||||
return errorResponse(`portal_${res.status}`, `${what}\u5931\u6557\uFF08HTTP ${res.status}\uFF09`, ["\u7A0D\u5F8C\u91CD\u8A66"], detail);
|
||||
}
|
||||
|
||||
// mcp/src/tools/kbdb_data.ts
|
||||
var OWNER_IGNORED_HINT = "owner_id \u5728\u767B\u5165\u8EAB\u5206\u4E0B\u4E0D\u751F\u6548\uFF1A\u67E5\u8A62\u7BC4\u570D\u7531\u4F60\u7684\u5E33\u865F\u6B0A\u9650\u6C7A\u5B9A\uFF08\u8207\u4F60\u5728 portal \u7DB2\u9801\u770B\u5230\u7684\u4E00\u81F4\uFF09";
|
||||
function registerAllKbdbDataTools(server, env, identity) {
|
||||
@@ -32142,8 +32177,10 @@ function renderLibraryMapLines(libraries) {
|
||||
const narrative = (l.narrative ?? "").trim() || "\uFF08narrative \u5F85\u88DC\uFF09";
|
||||
const clipped = narrative.length > MAX_NARRATIVE_CHARS ? `${narrative.slice(0, MAX_NARRATIVE_CHARS)}\u2026` : narrative;
|
||||
const core = entityNames(l.top_entities, 3);
|
||||
const count = Number(l.triplet_count ?? 0) || 0;
|
||||
return `- ${l.library}\uFF1A${clipped}\uFF5C\u6838\u5FC3\uFF1A${core.length ? core.join("\u3001") : "\uFF08\u5C1A\u7121\uFF09"}\uFF5C${count} triplets`;
|
||||
const tripletCount = Number(l.triplet_count ?? 0) || 0;
|
||||
const entryCount = Number(l.entry_count ?? 0) || 0;
|
||||
const countPart = tripletCount > 0 ? `${tripletCount} triplets` : entryCount > 0 ? `0 triplets\uFF0F${entryCount} \u7B46\u539F\u59CB\u5167\u5BB9\uFF08\u5C1A\u672A\u8403\u53D6\u95DC\u4FC2\uFF0Ckbdb_search \u67E5\u5F97\u5230\uFF09` : `0 triplets\uFF0F0 \u5167\u5BB9`;
|
||||
return `- ${l.library}\uFF1A${clipped}\uFF5C\u6838\u5FC3\uFF1A${core.length ? core.join("\u3001") : "\uFF08\u5C1A\u7121\uFF09"}\uFF5C${countPart}`;
|
||||
});
|
||||
const omitted = rows.length > MAX_LIBRARY_LINES ? `
|
||||
\uFF08\u5176\u9918 ${rows.length - MAX_LIBRARY_LINES} \u5EAB\u7565\uFF0Ckbdb_get_map \u53EF\u770B\u5168\u90E8\uFF09` : "";
|
||||
@@ -32195,6 +32232,13 @@ var RECOMPUTE_HINTS = [
|
||||
function registerAllKbdbMapTools(server, env, identity) {
|
||||
registerGetMap(server, env, identity);
|
||||
}
|
||||
function entryOnlyHint(tripletCount, entryCount, library) {
|
||||
if (tripletCount > 0 || entryCount <= 0) return [];
|
||||
const lib = library ? `\u300C${library}\u300D` : "\u9019\u500B\u5EAB";
|
||||
return [
|
||||
`${lib} triplet_count\uFF1D0\uFF0C\u4F46\u6709 ${entryCount} \u7B46\u539F\u59CB\u5167\u5BB9\uFF08entries\uFF09\u2014\u2014\u4E09\u5143\u7D44\u8403\u53D6\u9084\u6C92\u5C0D\u5B83\u8DD1\u904E\uFF0C\u4E0D\u4EE3\u8868\u6C92\u6709\u77E5\u8B58\u3002\u7528 kbdb_search\uFF08\u95DC\u9375\u5B57\u6216\u8A9E\u7FA9\uFF09\u76F4\u63A5\u67E5\u5F97\u5230\u5167\u5BB9\u3002`
|
||||
];
|
||||
}
|
||||
function registerGetMap(server, env, identity) {
|
||||
server.tool(
|
||||
"kbdb_get_map",
|
||||
@@ -32229,7 +32273,8 @@ function registerGetMap(server, env, identity) {
|
||||
...l,
|
||||
// 防禦:top_entities 若是 JSON 字串形就 parse 成名字清單(失敗當空,誠實不 crash)。
|
||||
top_entities: entityNames(l.top_entities, 3),
|
||||
triplet_count: Number(l.triplet_count ?? 0) || 0
|
||||
triplet_count: Number(l.triplet_count ?? 0) || 0,
|
||||
entry_count: Number(l.entry_count ?? 0) || 0
|
||||
}));
|
||||
if (libraries.length === 0) {
|
||||
return successResponse({ libraries: [], count: 0 }, [
|
||||
@@ -32238,9 +32283,15 @@ function registerGetMap(server, env, identity) {
|
||||
...RECOMPUTE_HINTS
|
||||
]);
|
||||
}
|
||||
const entryOnlyLibs = libraries.filter(
|
||||
(l) => (l.triplet_count ?? 0) === 0 && (l.entry_count ?? 0) > 0
|
||||
);
|
||||
return successResponse({ libraries, count: libraries.length }, [
|
||||
"\u8981\u770B\u67D0\u5EAB\u7D30\u7BC0\uFF1Akbdb_get_map(library='\u5EAB\u540D')",
|
||||
"\u9032\u5EAB\u67E5\u5167\u5BB9\uFF1Akbdb_search\uFF08\u95DC\u9375\u5B57/\u8A9E\u7FA9\uFF09\uFF1B\u67E5\u95DC\u4FC2\uFF1Akbdb_graph_neighbors"
|
||||
"\u9032\u5EAB\u67E5\u5167\u5BB9\uFF1Akbdb_search\uFF08\u95DC\u9375\u5B57/\u8A9E\u7FA9\uFF09\uFF1B\u67E5\u95DC\u4FC2\uFF1Akbdb_graph_neighbors",
|
||||
...entryOnlyLibs.length > 0 ? [
|
||||
`${entryOnlyLibs.map((l) => l.library).join("\u3001")} \u9019\u5E7E\u5EAB triplet_count\uFF1D0 \u4F46 entry_count\uFF1E0\uFF1A\u6709\u539F\u59CB\u5167\u5BB9\uFF0C\u53EA\u662F\u9084\u6C92\u8403\u53D6\u51FA\u4E09\u5143\u7D44\u95DC\u4FC2\u2014\u2014\u5225\u628A 0 triplets \u8B80\u6210\u300C\u6C92\u6709\u77E5\u8B58\u300D\uFF0C\u76F4\u63A5 kbdb_search \u9032\u53BB\u67E5\u3002`
|
||||
] : []
|
||||
]);
|
||||
}
|
||||
const res = await mapFetch(`/map/${encodeURIComponent(library)}${qs}`);
|
||||
@@ -32268,11 +32319,13 @@ function registerGetMap(server, env, identity) {
|
||||
top_entities: parseSlotArray(raw2.top_entities),
|
||||
relation_profile: parseSlotArray(raw2.relation_profile),
|
||||
bridges: parseSlotArray(raw2.bridges),
|
||||
triplet_count: Number(raw2.triplet_count ?? 0) || 0
|
||||
triplet_count: Number(raw2.triplet_count ?? 0) || 0,
|
||||
entry_count: Number(raw2.entry_count ?? 0) || 0
|
||||
};
|
||||
return successResponse({ map: map2 }, [
|
||||
"bridges\uFF1D\u6B64\u5EAB entity \u540C\u6642\u51FA\u73FE\u5728\u54EA\u4E9B\u5176\u4ED6\u5EAB\uFF08\u53EA\u6709\u5169\u5074\u4E09\u5143\u7D44\u90FD\u6A19\u4E86 library \u503C\u624D\u6293\u5F97\u5230\uFF0C\u820A\u8CC7\u6599\u82E5\u6C92\u6A19\u6703\u504F\u7A00\u758F\uFF0C\u662F\u8AA0\u5BE6\u73FE\u6CC1\u4E0D\u662F bug\uFF09",
|
||||
"\u6CBF\u6838\u5FC3 entity \u6316\u95DC\u4FC2\uFF1Akbdb_graph_neighbors(subject=entity \u540D)"
|
||||
"\u6CBF\u6838\u5FC3 entity \u6316\u95DC\u4FC2\uFF1Akbdb_graph_neighbors(subject=entity \u540D)",
|
||||
...entryOnlyHint(map2.triplet_count, map2.entry_count, library)
|
||||
]);
|
||||
} catch (e) {
|
||||
return errorResponse("internal_error", e instanceof Error ? e.message : String(e), ["\u7A0D\u5F8C\u91CD\u8A66"]);
|
||||
@@ -32340,7 +32393,7 @@ function registerAllTools(server, env, orgNamespace, partnerToken, identity) {
|
||||
registerReportFeedback(server, env, orgNamespace);
|
||||
registerAllIntrospectionTools(server, env);
|
||||
registerAllWorkflowCrudTools(server, env);
|
||||
registerAllSkillExampleTools(server, env);
|
||||
registerAllSkillExampleTools(server, env, identity);
|
||||
registerAllRecipeTools(server, env);
|
||||
registerAllKbdbDataTools(server, env, identity);
|
||||
registerAllKbdbGraphTools(server, env, orgNamespace, identity);
|
||||
@@ -32349,7 +32402,47 @@ function registerAllTools(server, env, orgNamespace, partnerToken, identity) {
|
||||
}
|
||||
|
||||
// mcp/src/mcp-handler.ts
|
||||
async function handleMcpRequest(request, env, orgNamespace, partnerToken, identity) {
|
||||
var KNOWLEDGE_FIRST = [
|
||||
"\u3010\u9019\u689D\u9023\u7DDA\u4E0A\u6709\u4E3B\u4EBA\u7684\u77E5\u8B58\u5EAB\u2014\u2014\u5148\u67E5\u5B83\uFF0C\u518D\u67E5\u5225\u7684\u3011",
|
||||
"",
|
||||
"\u9019\u689D MCP \u9023\u7DDA\u5F8C\u9762\u63A5\u8457\u4E00\u500B **KBDB \u77E5\u8B58\u5EAB**\uFF1A\u9019\u53F0\u5BE6\u4F8B\u7684\u4E3B\u4EBA\u9577\u671F\u7D2F\u7A4D\u7684\u7B46\u8A18\u3001\u6C7A\u7B56\u3001",
|
||||
"\u8E29\u904E\u7684\u5751\u3001\u5C08\u6848\u73FE\u6CC1\u3001skill \u8207\u5DE5\u4F5C\u6D41\u7D00\u9304\uFF0C\u90FD\u5728\u88E1\u9762\u3002**\u4F60\u4E0D\u662F\u5F9E\u96F6\u958B\u59CB\u7684**\u2014\u2014",
|
||||
"\u4F60\u5C0D\u9019\u4E9B\u5C08\u6848\u7684\u5167\u5EFA\u5370\u8C61\u591A\u534A\u662F\u932F\u7684\u6216\u904E\u6642\u7684\uFF0C\u5EAB\u88E1\u90A3\u4EFD\u624D\u662F\u4E3B\u4EBA\u8A8D\u7684\u7248\u672C\u3002",
|
||||
"",
|
||||
"\u{1F534} **\u6709\u4EBA\u554F\u4F60\u300CX \u662F\u4EC0\u9EBC\uFF0F\u70BA\u4EC0\u9EBC\u9019\u6A23\u505A\uFF0F\u4E4B\u524D\u600E\u9EBC\u6C7A\u5B9A\u7684\uFF0F\u73FE\u5728\u505A\u5230\u54EA\u300D\u2014\u2014",
|
||||
"\u4F60\u7684\u7B2C\u4E00\u500B\u52D5\u4F5C\u662F `kbdb_search`\uFF0C\u4E0D\u662F grep \u539F\u59CB\u78BC\u3001\u4E0D\u662F\u4E0A\u7DB2\u641C\u3001\u4E0D\u662F\u56DE\u7B54\u300C\u6211\u4E0D\u77E5\u9053\u300D\u3002**",
|
||||
"",
|
||||
"- `kbdb_search({ q: \"Arcrun \u662F\u4EC0\u9EBC\" })` \u2014 \u95DC\u9375\u5B57\u67E5\uFF08\u9810\u8A2D `mode:'keyword'`\uFF0C\u57FA\u672C\u76E4\u6C38\u9060\u53EF\u7528\uFF09\u3002",
|
||||
" \u63DB\u5E7E\u7D44\u8B1B\u6CD5\u518D\u653E\u68C4\uFF1B\u60F3\u8981\u8A9E\u7FA9\u76F8\u4F3C\u5EA6\u7528 `mode:'semantic'`\u3002**\u9019\u4E00\u652F\u662F\u4F60\u7684\u7B2C\u4E00\u7AD9\u3002**",
|
||||
"- `kbdb_get_map()` \u2014 \u4E0D\u77E5\u9053\u8A72\u9032\u54EA\u500B\u5EAB\u6642\u5148\u770B\u85CF\u66F8\u5730\u5716\uFF08\u4E0B\u9762\u82E5\u6709\u3010\u85CF\u66F8\u5730\u5716\u3011\u5C31\u662F\u5B83\u7684\u5FEB\u7167\uFF09\u3002",
|
||||
'- `kbdb_graph_neighbors({ subject: "Arcrun" })` \u2014 \u67E5\u67D0\u500B\u6771\u897F\u8DDF\u8AB0\u6709\u95DC\u4FC2\uFF08\u4E09\u5143\u7D44\u904D\u6B77\uFF09\u3002',
|
||||
"- `kbdb_list_templates` / `kbdb_query` \u2014 \u6309 template \u53D6\u6574\u6279\u7D50\u69CB\u5316\u8CC7\u6599\u3002",
|
||||
"",
|
||||
"\u{1F534} **\u9019\u4E09\u4EF6\u4E8B\u4E0D\u53EF\u4EE5\u8B1B\u6210\u540C\u4E00\u53E5**\uFF08\u8B1B\u6210\u540C\u4E00\u53E5\u5C31\u662F\u5728\u9A19\u4EBA\uFF09\uFF1A",
|
||||
"\u2460 \u300C\u77E5\u8B58\u5EAB\u88E1\u6C92\u6709\u300D\u3000\u2461 \u300C\u6211\u6C92\u67E5\u300D\u3000\u2462 \u300C\u5730\u5716\u6C92\u53D6\u5230\uFF0F\u67D0\u5EAB\u986F\u793A 0\u300D\u3002",
|
||||
"\u67E5\u904E\u771F\u7684\u6C92\u6709 \u2192 \u660E\u8AAA\u300C\u77E5\u8B58\u5EAB\u88E1\u67E5\u4E0D\u5230\uFF0C\u4EE5\u4E0B\u662F\u6211\u5F9E\u539F\u59CB\u78BC\uFF0F\u7DB2\u8DEF\u63A8\u7684\u300D\uFF0C\u518D\u53BB\u8B80 code \u6216\u4E0A\u7DB2\u3002",
|
||||
"**\u6C92\u67E5\u5C31\u56DE\u7B54\uFF1D\u62FF\u4F60\u7684\u731C\u6E2C\u5192\u5145\u4E3B\u4EBA\u7684\u77E5\u8B58\uFF0C\u90A3\u662F\u9019\u689D\u9023\u7DDA\u4E0A\u6700\u56B4\u91CD\u7684\u932F\u3002**",
|
||||
"",
|
||||
"\u{1F534} **\u5730\u5716\u662F\u7D22\u5F15\uFF0C\u4E0D\u662F\u5EAB\u5B58\u6E05\u55AE**\uFF1A\u67D0\u5EAB\u986F\u793A `0 triplets`\u3001\u6216\u4E0B\u9762\u6574\u6BB5\u3010\u85CF\u66F8\u5730\u5716\u3011\u6C92\u51FA\u73FE\uFF0C",
|
||||
"\u90FD**\u4E0D\u4EE3\u8868**\u6C92\u6709\u77E5\u8B58\uFF08\u53EF\u80FD\u53EA\u662F\u9084\u6C92\u91CD\u7B97\u3001\u6216\u9019\u6B21\u6C92\u6293\u5230\uFF09\u3002\u8981\u77E5\u9053\u6709\u6C92\u6709\uFF0C\u53EA\u6709\u4E00\u500B\u65B9\u6CD5\uFF1A`kbdb_search` \u67E5\u904E\u3002",
|
||||
"\u540C\u7406\uFF0C\u4EFB\u4F55\u5DE5\u5177\u56DE 401\uFF0F\u9023\u4E0D\u4E0A\uFF0F\u6C92\u6B0A\u9650\uFF0C\u90A3\u662F**\u8B80\u4E0D\u5230**\uFF0C\u4E0D\u662F**\u4E0D\u5B58\u5728**\u2014\u2014\u7167\u5B83\u7D66\u7684 next_actions \u4FEE\uFF0C",
|
||||
"\u5225\u628A\u5B83\u6539\u53E3\u8B1B\u6210\u300C\u9019\u88E1\u6C92\u6709\u300D\uFF08`arcrun_get_skill` \u66FE\u628A KBDB \u7684 401 \u8B1B\u6210\u300Cskill \u4E0D\u5B58\u5728\u300D\uFF0C\u5C31\u662F\u9019\u500B\u75C5\uFF09\u3002"
|
||||
].join("\n");
|
||||
var MAP_UNAVAILABLE_NOTE = [
|
||||
"\u3010\u85CF\u66F8\u5730\u5716\uFF1A\u9019\u6B21\u6C92\u53D6\u5230\u3011",
|
||||
"\u5730\u5716\u6293\u53D6\u903E\u6642\uFF0F\u56DE\u932F\uFF0F\u6216\u5B83\u56DE\u5831\u7684\u6E05\u55AE\u662F\u7A7A\u7684\uFF08\u4E5F\u53EF\u80FD\u53EA\u662F\u9084\u6C92\u91CD\u7B97\u904E\uFF09\u3002",
|
||||
"\u{1F534} **\u9019\u662F\u300C\u5730\u5716\u6C92\u62FF\u5230\u300D\uFF0C\u4E0D\u662F\u300C\u9019\u88E1\u6C92\u6709\u77E5\u8B58\u300D\u3002** \u4E0A\u9762\u90A3\u689D\u898F\u5247\u7167\u820A\uFF1A",
|
||||
"\u8981\u77E5\u9053\u5EAB\u88E1\u6709\u4EC0\u9EBC\uFF0C\u76F4\u63A5 `kbdb_search`\uFF1B\u60F3\u518D\u6293\u4E00\u6B21\u5730\u5716\u547C\u53EB `kbdb_get_map()`\uFF08\u5B83\u6703\u56DE\u5831\u771F\u6B63\u7684\u539F\u56E0\uFF09\u3002"
|
||||
].join("\n");
|
||||
var MAP_STALE_NOTE = [
|
||||
"\u3010\u85CF\u66F8\u5730\u5716\uFF1A\u62FF\u4E0D\u5230\uFF0C\u56E0\u70BA\u9019\u689D\u9023\u7DDA\u662F\u820A\u7248\u7C3D\u767C\u7684 token\u3011",
|
||||
"\u9019\u689D\u9023\u7DDA\u7684 token \u6C92\u5E36\u767B\u5165\u8005\u8EAB\u5206\uFF0C`kbdb_*` \u6703\u56DE `identity_missing`\u3002",
|
||||
"\u{1F534} **\u9019\u4E0D\u4EE3\u8868\u77E5\u8B58\u5EAB\u662F\u7A7A\u7684**\u2014\u2014\u662F\u9019\u689D\u9023\u7DDA\u9084\u6C92\u8A8D\u5F97\u4F60\u3002",
|
||||
"\u4ECD\u7136\u5148\u547C\u53EB\u4E00\u6B21 `kbdb_search` \u78BA\u8A8D\u932F\u8AA4\u78BC\uFF1B\u82E5\u771F\u7684\u662F `identity_missing`\uFF0C",
|
||||
"\u8ACB\u4F7F\u7528\u8005\u5230 claude.ai \u2192 Settings \u2192 Connectors \u628A\u9019\u500B connector \u91CD\u65B0\u9023\u7DDA\u4E00\u6B21\uFF08\u91CD\u65B0\u8F38\u5165 Portal \u5E33\u5BC6\uFF09\uFF0C",
|
||||
"**\u4E0D\u8981\u6539\u53E3\u8AAA\u300C\u67E5\u4E0D\u5230\u8CC7\u6599\u300D\u6216\u81EA\u5DF1\u53BB\u731C\u7B54\u6848\u3002**"
|
||||
].join("\n");
|
||||
async function buildServerInstructions(env, identity) {
|
||||
const mapInstructions = await buildLibraryMapInstructions(env, identity);
|
||||
const startHere = [
|
||||
"# Arcrun \u2014 \u4F60\u5DF2\u7D93\u914D\u5099\u4E86\u9019\u5957\u5DE5\u5177\uFF0C\u5225\u4E0A\u7DB2\u627E",
|
||||
@@ -32363,9 +32456,13 @@ async function handleMcpRequest(request, env, orgNamespace, partnerToken, identi
|
||||
"",
|
||||
"1. `arcrun_get_skill('write_intent_workflow')` \u2014 **\u5FC5\u8B80\u7B2C\u4E00\u652F**\u3002",
|
||||
" \u6559\u4F60\u7528 `>>` \u5BEB\u300C\u610F\u5716\u5DE5\u4F5C\u6D41\u300D\u3002\u4F60**\u4E0D\u9700\u8981\u5148\u77E5\u9053\u6709\u54EA\u4E9B\u96F6\u4EF6**\uFF0C\u5148\u5BEB\u610F\u5716\u3002",
|
||||
" \u26A0\uFE0F \u9019\u652F\u82E5\u56DE\u932F\uFF08401\uFF0F\u9023\u4E0D\u4E0A\uFF0F`kbdb_unreachable`\uFF09\uFF0C\u90A3\u662F**\u9019\u689D\u9023\u7DDA\u8B80\u4E0D\u5230 KBDB**\uFF0C",
|
||||
" **\u4E0D\u662F skill \u4E0D\u5B58\u5728**\u2014\u2014\u540C\u4E00\u4EFD\u5167\u5BB9\u7528 `kbdb_search({ q: 'skill-write_intent_workflow' })` \u6488\u5F97\u5230\u3002",
|
||||
"2. `arcrun_whoami()` \u2014 \u78BA\u8A8D\u9023\u5230\u54EA\u500B\u5E33\u865F\uFF08\u52FF\u81EA\u884C curl \u731C\u5E33\u865F URL\uFF09\u3002",
|
||||
"3. \u628A\u610F\u5716\u4E1F `POST /cypher/search` \u6216 `arcrun_validate_yaml` \u2014 \u7CFB\u7D71\u544A\u8A34\u4F60\u54EA\u4E9B\u96F6\u4EF6\u5B58\u5728\u3002",
|
||||
"4. \u5361\u4F4F\uFF0F\u4E0D\u77E5\u9053\u8A72\u67E5\u4EC0\u9EBC \u2192 `arcrun_get_skill('INDEX')`\uFF08\u5168\u9928\u5C0E\u822A\uFF1A\u4EC0\u9EBC\u554F\u984C\u67E5\u54EA\u88E1\uFF0B\u5DF2\u77E5\u7684\u5751\uFF09\u3002",
|
||||
"4. \u5361\u4F4F\uFF0F\u4E0D\u77E5\u9053\u8A72\u67E5\u4EC0\u9EBC \u2192 \u5148 `arcrun_list_skills()` **\u770B\u9019\u53F0\u5BE6\u4F8B\u771F\u7684\u6709\u54EA\u5E7E\u652F**\uFF0C\u518D\u6311\u4E00\u652F\u8B80\u3002",
|
||||
" \uFF082026-08-13 \u5BE6\u6E2C\uFF1A\u4E0D\u540C\u5BE6\u4F8B seed \u7684 skill \u4E0D\u4E00\u6A23\uFF0C\u6709\u7684\u5BE6\u4F8B\u53EA\u6709\u5169\u652F\u3001\u9023 `INDEX` \u90FD\u6C92\u6709\u3002",
|
||||
" **\u4E0D\u8981\u7167\u6559\u6750\u76F4\u63A5\u6307\u540D\u4E00\u500B slug** \u2014\u2014\u5148\u5217\u6E05\u55AE\uFF0C\u6216 `kbdb_search({ q: 'skill' })` \u76F4\u63A5\u5728\u5EAB\u88E1\u627E\u3002\uFF09",
|
||||
"5. \u7F3A\u96F6\u4EF6\u6642\uFF1A\u7F3A API \u2192 \u5BEB recipe\uFF08`arcrun_recipe_push`\uFF09\uFF1B\u7F3A\u80FD\u529B \u2192 \u6295\u7A3F\u96F6\u4EF6 PR\u3002",
|
||||
" \u{1F534} **\u4E0D\u8981\u56E0\u70BA\u67E5\u4E0D\u5230\u96F6\u4EF6\u5C31\u6539\u5BEB\u6210 `code` \u7BC0\u9EDE**\u2014\u2014\u90A3\u53EB\u300C\u8179\u8A9E\u8853\u300D\uFF08\u8868\u9762\u7528 Arcrun\u3001",
|
||||
" \u5BE6\u969B\u5168\u5BEB JS\uFF09\u3002`code` \u53EA\u7528\u65BC\u5C40\u90E8\u6574\u5F62\uFF08\u4F8B\uFF1A\u525D\u6389 LLM \u56DE\u61C9\u7684\u96DC\u8A0A\uFF09\u3002",
|
||||
@@ -32381,11 +32478,11 @@ async function handleMcpRequest(request, env, orgNamespace, partnerToken, identi
|
||||
"\u4E0D\u662F\u5931\u6557\u2014\u2014\u5225\u56E0\u70BA\u300C\u53EA\u6709\u4E00\u689D\u8DEF\u6709\u8F38\u51FA\u300D\u5C31\u4EE5\u70BA\u58DE\u6389\u800C\u6539\u5BEB\u6210 code\uFF082026-08-01 \u5BE6\u649E\uFF09\u3002",
|
||||
"\u7B2C\u4E00\u500B\u7BC0\u9EDE\u56FA\u5B9A\u662F `input`\u3002"
|
||||
].join("\n");
|
||||
const instructions = mapInstructions ? `${startHere}
|
||||
|
||||
---
|
||||
|
||||
${mapInstructions}` : startHere;
|
||||
const mapSection = mapInstructions ?? (identity.kind === "stale" ? MAP_STALE_NOTE : MAP_UNAVAILABLE_NOTE);
|
||||
return [KNOWLEDGE_FIRST, startHere, mapSection].join("\n\n---\n\n");
|
||||
}
|
||||
async function handleMcpRequest(request, env, orgNamespace, partnerToken, identity) {
|
||||
const instructions = await buildServerInstructions(env, identity);
|
||||
const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
|
||||
const server = new McpServer(
|
||||
{ name: "arcrun-mcp-server", version: "1.0.0" },
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
+570
-31
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"built_for": "arcrun-tier2-worker-artifacts",
|
||||
"generated_at": "2026-08-12T12:02:15.698Z",
|
||||
"repo_head": "89b80ff90e95f07b5d44978ef58090ac783f53bd",
|
||||
"generated_at": "2026-08-15T15:53:28.612Z",
|
||||
"repo_head": "cacaa33f7d4e51d0011096250a33bb5107c67074",
|
||||
"repo_dirty": false,
|
||||
"workers": [
|
||||
{
|
||||
"name": "arcrun-cypher-executor",
|
||||
"source_dir": "cypher-executor",
|
||||
"source_commit": "10d150ac2b4385af95a457f3c411430c4a146cf9",
|
||||
"source_commit": "9fa88065abdc5a795d3847711f04101480621af0",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-cypher-executor/worker.mjs",
|
||||
"js_bytes": 584610,
|
||||
"content_sha256": "6a424274ecf7beb28b747672296c4c33f949c6d7b134e8054e640e92198910ba",
|
||||
"js_bytes": 590499,
|
||||
"content_sha256": "94a21ad012ee0c48d78e9a74752048917ddb4616f1ea089976834acd887a9d14",
|
||||
"modules": [],
|
||||
"compat_date": "2025-02-19",
|
||||
"compat_flags": [
|
||||
@@ -58,11 +58,11 @@
|
||||
{
|
||||
"name": "arcrun-kbdb",
|
||||
"source_dir": "kbdb",
|
||||
"source_commit": "10d150ac2b4385af95a457f3c411430c4a146cf9",
|
||||
"source_commit": "ceb7638d746f0e8847552aef721f6f53ef87ff0b",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-kbdb/worker.mjs",
|
||||
"js_bytes": 149797,
|
||||
"content_sha256": "8b23853cbc88aee0ca15ef20ca46e92bd8e75064cd311af2847f4d51811960b1",
|
||||
"js_bytes": 160306,
|
||||
"content_sha256": "47b09d60f8d122f7ec234d146a99bc68b2661c56ad6a98eca146d99ec5de03ea",
|
||||
"modules": [],
|
||||
"compat_date": "2025-02-19",
|
||||
"compat_flags": [
|
||||
@@ -84,6 +84,308 @@
|
||||
},
|
||||
"warnings": []
|
||||
},
|
||||
{
|
||||
"name": "arcrun-code",
|
||||
"source_dir": "registry/components/code",
|
||||
"source_commit": "621cb8d948d61be6202063fd02effb3f538437fe",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-code/worker.mjs",
|
||||
"js_bytes": 153758,
|
||||
"content_sha256": "751634a3fc9a99cc2da662026818d754c48f031d10bff3b3be2d3a8ee2311bd6",
|
||||
"modules": [
|
||||
{
|
||||
"name": "quickjs.wasm",
|
||||
"type": "application/wasm",
|
||||
"file": "arcrun-code/quickjs.wasm",
|
||||
"sha256": "105c3bed22d457e43e3d1c3c1c6959fda62a8fe06f0fc8a985303c3a2be72232"
|
||||
}
|
||||
],
|
||||
"compat_date": "2025-02-19",
|
||||
"compat_flags": [],
|
||||
"requires": {
|
||||
"kv": [],
|
||||
"d1": [],
|
||||
"vectorize": 0,
|
||||
"ai": false,
|
||||
"vars": {
|
||||
"COMPONENT_ID": "code"
|
||||
}
|
||||
},
|
||||
"warnings": []
|
||||
},
|
||||
{
|
||||
"name": "arcrun-mcp",
|
||||
"source_dir": "mcp",
|
||||
"source_commit": "1ccee0055ebdb1f6d74094df3e807f56b849fa6f",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-mcp/worker.mjs",
|
||||
"js_bytes": 1192612,
|
||||
"content_sha256": "9bc8dffb34d0f36608a78b2cfeb8ee8b5c793f52b4497f5d4d5f69f70700972e",
|
||||
"modules": [],
|
||||
"compat_date": "2024-11-27",
|
||||
"compat_flags": [
|
||||
"nodejs_compat"
|
||||
],
|
||||
"requires": {
|
||||
"kv": [
|
||||
"OAUTH_KV"
|
||||
],
|
||||
"d1": [],
|
||||
"vectorize": 0,
|
||||
"ai": false,
|
||||
"vars": {}
|
||||
},
|
||||
"warnings": []
|
||||
},
|
||||
{
|
||||
"name": "arcrun-array-ops",
|
||||
"source_dir": ".component-builds/array_ops",
|
||||
"source_commit": "922a57fe3458dfda82bcc25d287cef93f8dab838",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-array-ops/worker.mjs",
|
||||
"js_bytes": 67802,
|
||||
"content_sha256": "35ad5be6332dde2299efd10be726300c6cace1eb085ab24f08173536ca7fb950",
|
||||
"modules": [
|
||||
{
|
||||
"name": "component.wasm",
|
||||
"type": "application/wasm",
|
||||
"file": "arcrun-array-ops/component.wasm",
|
||||
"sha256": "74dc9d99f8b9ef43628b55c85c05beb1af8f6f90d78d07cd132ffe4b865147ae"
|
||||
}
|
||||
],
|
||||
"compat_date": "2025-02-19",
|
||||
"compat_flags": [],
|
||||
"requires": {
|
||||
"kv": [],
|
||||
"d1": [],
|
||||
"vectorize": 0,
|
||||
"ai": false,
|
||||
"vars": {
|
||||
"COMPONENT_ID": "array_ops"
|
||||
}
|
||||
},
|
||||
"warnings": []
|
||||
},
|
||||
{
|
||||
"name": "arcrun-auth-oauth2",
|
||||
"source_dir": ".component-builds/auth_oauth2",
|
||||
"source_commit": "20c7610371c401f622ef4e6ea1b9ba812963be95",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-auth-oauth2/worker.mjs",
|
||||
"js_bytes": 80472,
|
||||
"content_sha256": "a9b0d45ee6a63e0eebd45554df16d9fa4e6e717e0c31dac13b630e132f1a96db",
|
||||
"modules": [
|
||||
{
|
||||
"name": "component.wasm",
|
||||
"type": "application/wasm",
|
||||
"file": "arcrun-auth-oauth2/component.wasm",
|
||||
"sha256": "3622c20644e6c8c39487109e7353e2590bad90b1207139e34dfb0cf1c2696664"
|
||||
}
|
||||
],
|
||||
"compat_date": "2025-02-19",
|
||||
"compat_flags": [
|
||||
"nodejs_compat"
|
||||
],
|
||||
"requires": {
|
||||
"kv": [
|
||||
"CREDENTIALS_KV",
|
||||
"RECIPES"
|
||||
],
|
||||
"d1": [],
|
||||
"vectorize": 0,
|
||||
"ai": false,
|
||||
"vars": {
|
||||
"COMPONENT_ID": "auth_oauth2"
|
||||
}
|
||||
},
|
||||
"warnings": []
|
||||
},
|
||||
{
|
||||
"name": "arcrun-auth-service-account",
|
||||
"source_dir": ".component-builds/auth_service_account",
|
||||
"source_commit": "20c7610371c401f622ef4e6ea1b9ba812963be95",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-auth-service-account/worker.mjs",
|
||||
"js_bytes": 81693,
|
||||
"content_sha256": "68675ec76f3f060309492e55301936a47f4ae0a5e9b00865eb861e0e631e40e5",
|
||||
"modules": [
|
||||
{
|
||||
"name": "component.wasm",
|
||||
"type": "application/wasm",
|
||||
"file": "arcrun-auth-service-account/component.wasm",
|
||||
"sha256": "7d0808b4b9a2df4fbb54977eafe34a5bdb99e0cc941d97462c62e26f3651951e"
|
||||
}
|
||||
],
|
||||
"compat_date": "2025-02-19",
|
||||
"compat_flags": [
|
||||
"nodejs_compat"
|
||||
],
|
||||
"requires": {
|
||||
"kv": [
|
||||
"CREDENTIALS_KV",
|
||||
"RECIPES"
|
||||
],
|
||||
"d1": [],
|
||||
"vectorize": 0,
|
||||
"ai": false,
|
||||
"vars": {
|
||||
"COMPONENT_ID": "auth_service_account"
|
||||
}
|
||||
},
|
||||
"warnings": []
|
||||
},
|
||||
{
|
||||
"name": "arcrun-auth-static-key",
|
||||
"source_dir": ".component-builds/auth_static_key",
|
||||
"source_commit": "20c7610371c401f622ef4e6ea1b9ba812963be95",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-auth-static-key/worker.mjs",
|
||||
"js_bytes": 81217,
|
||||
"content_sha256": "140c163d2e8c609eebf281de25d16e521d85004f8437cc842613c249e44aa7ff",
|
||||
"modules": [
|
||||
{
|
||||
"name": "component.wasm",
|
||||
"type": "application/wasm",
|
||||
"file": "arcrun-auth-static-key/component.wasm",
|
||||
"sha256": "cd0f69a80ced0979749e0ec30b1981795a49eaa56bdb2ba102cd4b64f4670369"
|
||||
}
|
||||
],
|
||||
"compat_date": "2025-02-19",
|
||||
"compat_flags": [
|
||||
"nodejs_compat"
|
||||
],
|
||||
"requires": {
|
||||
"kv": [
|
||||
"CREDENTIALS_KV",
|
||||
"RECIPES"
|
||||
],
|
||||
"d1": [],
|
||||
"vectorize": 0,
|
||||
"ai": false,
|
||||
"vars": {
|
||||
"COMPONENT_ID": "auth_static_key"
|
||||
}
|
||||
},
|
||||
"warnings": []
|
||||
},
|
||||
{
|
||||
"name": "arcrun-cron",
|
||||
"source_dir": ".component-builds/cron",
|
||||
"source_commit": "6e92ca03727bf22143499d6c0df06b40cb65c17b",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-cron/worker.mjs",
|
||||
"js_bytes": 78911,
|
||||
"content_sha256": "17068f03f686aee6c0aec9d7d748bbb1998edc8918260033f985697bc718aa76",
|
||||
"modules": [
|
||||
{
|
||||
"name": "component.wasm",
|
||||
"type": "application/wasm",
|
||||
"file": "arcrun-cron/component.wasm",
|
||||
"sha256": "afb3a14f8c8f00f4f5573f44153cb1dcf6fc715b2fb47c2afb0aaf95b74d3cde"
|
||||
}
|
||||
],
|
||||
"compat_date": "2025-02-19",
|
||||
"compat_flags": [
|
||||
"nodejs_compat"
|
||||
],
|
||||
"requires": {
|
||||
"kv": [],
|
||||
"d1": [],
|
||||
"vectorize": 0,
|
||||
"ai": false,
|
||||
"vars": {
|
||||
"COMPONENT_ID": "cron"
|
||||
}
|
||||
},
|
||||
"warnings": []
|
||||
},
|
||||
{
|
||||
"name": "arcrun-date-ops",
|
||||
"source_dir": ".component-builds/date_ops",
|
||||
"source_commit": "922a57fe3458dfda82bcc25d287cef93f8dab838",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-date-ops/worker.mjs",
|
||||
"js_bytes": 67781,
|
||||
"content_sha256": "60ebc1d17ab0b9d3bb66f1c71ec63eb5d35bc97ed9f8b4cd1c1c95f4eeb171b7",
|
||||
"modules": [
|
||||
{
|
||||
"name": "component.wasm",
|
||||
"type": "application/wasm",
|
||||
"file": "arcrun-date-ops/component.wasm",
|
||||
"sha256": "4e2c1329ce4014aa86a9a6449d8699e67c6c616f5c07088dc21216d4036ca41e"
|
||||
}
|
||||
],
|
||||
"compat_date": "2025-02-19",
|
||||
"compat_flags": [],
|
||||
"requires": {
|
||||
"kv": [],
|
||||
"d1": [],
|
||||
"vectorize": 0,
|
||||
"ai": false,
|
||||
"vars": {
|
||||
"COMPONENT_ID": "date_ops"
|
||||
}
|
||||
},
|
||||
"warnings": []
|
||||
},
|
||||
{
|
||||
"name": "arcrun-filter",
|
||||
"source_dir": ".component-builds/filter",
|
||||
"source_commit": "922a57fe3458dfda82bcc25d287cef93f8dab838",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-filter/worker.mjs",
|
||||
"js_bytes": 67739,
|
||||
"content_sha256": "c7fd5070ff7c0f0341e9dd364805e393f9d68056a846f48e6bc61ef6f4e67187",
|
||||
"modules": [
|
||||
{
|
||||
"name": "component.wasm",
|
||||
"type": "application/wasm",
|
||||
"file": "arcrun-filter/component.wasm",
|
||||
"sha256": "6a0692b386522918b3b5af79cf7877394d1f02c350018dbaef68ef380dad1b9e"
|
||||
}
|
||||
],
|
||||
"compat_date": "2025-02-19",
|
||||
"compat_flags": [],
|
||||
"requires": {
|
||||
"kv": [],
|
||||
"d1": [],
|
||||
"vectorize": 0,
|
||||
"ai": false,
|
||||
"vars": {
|
||||
"COMPONENT_ID": "filter"
|
||||
}
|
||||
},
|
||||
"warnings": []
|
||||
},
|
||||
{
|
||||
"name": "arcrun-foreach-control",
|
||||
"source_dir": ".component-builds/foreach_control",
|
||||
"source_commit": "922a57fe3458dfda82bcc25d287cef93f8dab838",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-foreach-control/worker.mjs",
|
||||
"js_bytes": 67928,
|
||||
"content_sha256": "e52410c9611a93ac7fe38c7b11291cc18711c7da3370492788c3c55bc0fb03d5",
|
||||
"modules": [
|
||||
{
|
||||
"name": "component.wasm",
|
||||
"type": "application/wasm",
|
||||
"file": "arcrun-foreach-control/component.wasm",
|
||||
"sha256": "8ae161ab37cc78ef6fae65364a47f7accb572378b395c4637daa2ffa57f14632"
|
||||
}
|
||||
],
|
||||
"compat_date": "2025-02-19",
|
||||
"compat_flags": [],
|
||||
"requires": {
|
||||
"kv": [],
|
||||
"d1": [],
|
||||
"vectorize": 0,
|
||||
"ai": false,
|
||||
"vars": {
|
||||
"COMPONENT_ID": "foreach_control"
|
||||
}
|
||||
},
|
||||
"warnings": []
|
||||
},
|
||||
{
|
||||
"name": "arcrun-http-request",
|
||||
"source_dir": ".component-builds/http_request",
|
||||
@@ -117,19 +419,19 @@
|
||||
"warnings": []
|
||||
},
|
||||
{
|
||||
"name": "arcrun-code",
|
||||
"source_dir": "registry/components/code",
|
||||
"source_commit": "621cb8d948d61be6202063fd02effb3f538437fe",
|
||||
"name": "arcrun-if-control",
|
||||
"source_dir": ".component-builds/if_control",
|
||||
"source_commit": "922a57fe3458dfda82bcc25d287cef93f8dab838",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-code/worker.mjs",
|
||||
"js_bytes": 153758,
|
||||
"content_sha256": "751634a3fc9a99cc2da662026818d754c48f031d10bff3b3be2d3a8ee2311bd6",
|
||||
"main_file": "arcrun-if-control/worker.mjs",
|
||||
"js_bytes": 67823,
|
||||
"content_sha256": "557ecf4ee955105af07b2227b08365eb9de8c39ad8ffb323b101397704f438bd",
|
||||
"modules": [
|
||||
{
|
||||
"name": "quickjs.wasm",
|
||||
"name": "component.wasm",
|
||||
"type": "application/wasm",
|
||||
"file": "arcrun-code/quickjs.wasm",
|
||||
"sha256": "105c3bed22d457e43e3d1c3c1c6959fda62a8fe06f0fc8a985303c3a2be72232"
|
||||
"file": "arcrun-if-control/component.wasm",
|
||||
"sha256": "87c81e9cf5e664f60c06a364508dd365d49953e1b7afba6a5753fce5a4c82636"
|
||||
}
|
||||
],
|
||||
"compat_date": "2025-02-19",
|
||||
@@ -140,35 +442,272 @@
|
||||
"vectorize": 0,
|
||||
"ai": false,
|
||||
"vars": {
|
||||
"COMPONENT_ID": "code"
|
||||
"COMPONENT_ID": "if_control"
|
||||
}
|
||||
},
|
||||
"warnings": []
|
||||
},
|
||||
{
|
||||
"name": "arcrun-mcp",
|
||||
"source_dir": "mcp",
|
||||
"source_commit": "10d150ac2b4385af95a457f3c411430c4a146cf9",
|
||||
"name": "arcrun-merge",
|
||||
"source_dir": ".component-builds/merge",
|
||||
"source_commit": "922a57fe3458dfda82bcc25d287cef93f8dab838",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-mcp/worker.mjs",
|
||||
"js_bytes": 1179487,
|
||||
"content_sha256": "1cd4c4d079d72bf7cba7c490ba6a88476f70b3ea51af7e5c93f9a184ae3c0ce6",
|
||||
"modules": [],
|
||||
"compat_date": "2024-11-27",
|
||||
"compat_flags": [
|
||||
"nodejs_compat"
|
||||
"main_file": "arcrun-merge/worker.mjs",
|
||||
"js_bytes": 67718,
|
||||
"content_sha256": "e4216249436000bbb4db0318cc9028f936ca6537d3d656228cfdd3cb31df25f8",
|
||||
"modules": [
|
||||
{
|
||||
"name": "component.wasm",
|
||||
"type": "application/wasm",
|
||||
"file": "arcrun-merge/component.wasm",
|
||||
"sha256": "55a4d143246df172f60b5e15e7b26e55279deda9db436e3d65bccc25c86d7cd9"
|
||||
}
|
||||
],
|
||||
"compat_date": "2025-02-19",
|
||||
"compat_flags": [],
|
||||
"requires": {
|
||||
"kv": [
|
||||
"OAUTH_KV"
|
||||
],
|
||||
"kv": [],
|
||||
"d1": [],
|
||||
"vectorize": 0,
|
||||
"ai": false,
|
||||
"vars": {
|
||||
"COMPONENT_ID": "merge"
|
||||
}
|
||||
},
|
||||
"warnings": []
|
||||
},
|
||||
{
|
||||
"name": "arcrun-number-ops",
|
||||
"source_dir": ".component-builds/number_ops",
|
||||
"source_commit": "922a57fe3458dfda82bcc25d287cef93f8dab838",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-number-ops/worker.mjs",
|
||||
"js_bytes": 67823,
|
||||
"content_sha256": "91821a291117e033bfa4f013cd88de09479cd4ac43708e37a39df3f31c72aaa6",
|
||||
"modules": [
|
||||
{
|
||||
"name": "component.wasm",
|
||||
"type": "application/wasm",
|
||||
"file": "arcrun-number-ops/component.wasm",
|
||||
"sha256": "452d1940f6e2b2b36dc97c145abcb7b5bacdbfa7631c742827c2f6f6db87b4ea"
|
||||
}
|
||||
],
|
||||
"compat_date": "2025-02-19",
|
||||
"compat_flags": [],
|
||||
"requires": {
|
||||
"kv": [],
|
||||
"d1": [],
|
||||
"vectorize": 0,
|
||||
"ai": false,
|
||||
"vars": {
|
||||
"COMPONENT_ID": "number_ops"
|
||||
}
|
||||
},
|
||||
"warnings": []
|
||||
},
|
||||
{
|
||||
"name": "arcrun-set",
|
||||
"source_dir": ".component-builds/set",
|
||||
"source_commit": "922a57fe3458dfda82bcc25d287cef93f8dab838",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-set/worker.mjs",
|
||||
"js_bytes": 67676,
|
||||
"content_sha256": "6cac587802cae7dc1651d6b461232abafe40dcf0fbfe9e0adc5b1d8e35b41cc1",
|
||||
"modules": [
|
||||
{
|
||||
"name": "component.wasm",
|
||||
"type": "application/wasm",
|
||||
"file": "arcrun-set/component.wasm",
|
||||
"sha256": "f5afcf6e57401d2d83b21fb971ec80ca56d5e8e4bdb74a004f3d118875ed57c4"
|
||||
}
|
||||
],
|
||||
"compat_date": "2025-02-19",
|
||||
"compat_flags": [],
|
||||
"requires": {
|
||||
"kv": [],
|
||||
"d1": [],
|
||||
"vectorize": 0,
|
||||
"ai": false,
|
||||
"vars": {
|
||||
"COMPONENT_ID": "set"
|
||||
}
|
||||
},
|
||||
"warnings": []
|
||||
},
|
||||
{
|
||||
"name": "arcrun-string-ops",
|
||||
"source_dir": ".component-builds/string_ops",
|
||||
"source_commit": "922a57fe3458dfda82bcc25d287cef93f8dab838",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-string-ops/worker.mjs",
|
||||
"js_bytes": 67823,
|
||||
"content_sha256": "c85166e357b76828c7020426d912b591006152ac38f024a095a4d9dcd10dee72",
|
||||
"modules": [
|
||||
{
|
||||
"name": "component.wasm",
|
||||
"type": "application/wasm",
|
||||
"file": "arcrun-string-ops/component.wasm",
|
||||
"sha256": "41f3243ee18bb9f4fcbaa417bd773347128dec2971132e240eddec19ba6a9da2"
|
||||
}
|
||||
],
|
||||
"compat_date": "2025-02-19",
|
||||
"compat_flags": [],
|
||||
"requires": {
|
||||
"kv": [],
|
||||
"d1": [],
|
||||
"vectorize": 0,
|
||||
"ai": false,
|
||||
"vars": {
|
||||
"COMPONENT_ID": "string_ops"
|
||||
}
|
||||
},
|
||||
"warnings": []
|
||||
},
|
||||
{
|
||||
"name": "arcrun-switch",
|
||||
"source_dir": ".component-builds/switch",
|
||||
"source_commit": "922a57fe3458dfda82bcc25d287cef93f8dab838",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-switch/worker.mjs",
|
||||
"js_bytes": 67739,
|
||||
"content_sha256": "e0e7689dbba735a254556272d6d233396c7fa05cc0fba5835a3e20b0ddf00fca",
|
||||
"modules": [
|
||||
{
|
||||
"name": "component.wasm",
|
||||
"type": "application/wasm",
|
||||
"file": "arcrun-switch/component.wasm",
|
||||
"sha256": "6d993dc9b1affe16b600aaa7cae5af977108cc08513bb11d9d9db908285fea8b"
|
||||
}
|
||||
],
|
||||
"compat_date": "2025-02-19",
|
||||
"compat_flags": [],
|
||||
"requires": {
|
||||
"kv": [],
|
||||
"d1": [],
|
||||
"vectorize": 0,
|
||||
"ai": false,
|
||||
"vars": {
|
||||
"COMPONENT_ID": "switch"
|
||||
}
|
||||
},
|
||||
"warnings": []
|
||||
},
|
||||
{
|
||||
"name": "arcrun-try-catch",
|
||||
"source_dir": ".component-builds/try_catch",
|
||||
"source_commit": "922a57fe3458dfda82bcc25d287cef93f8dab838",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-try-catch/worker.mjs",
|
||||
"js_bytes": 67802,
|
||||
"content_sha256": "a5fbcbf452ab00269dc857997df3059bee7eb1f2c1b322d249146ab4e5e5bac9",
|
||||
"modules": [
|
||||
{
|
||||
"name": "component.wasm",
|
||||
"type": "application/wasm",
|
||||
"file": "arcrun-try-catch/component.wasm",
|
||||
"sha256": "10eaddc5ef0d3dc68e3f009970541ac7f5fce30dcdbecec9a333afd5a29e3486"
|
||||
}
|
||||
],
|
||||
"compat_date": "2025-02-19",
|
||||
"compat_flags": [],
|
||||
"requires": {
|
||||
"kv": [],
|
||||
"d1": [],
|
||||
"vectorize": 0,
|
||||
"ai": false,
|
||||
"vars": {
|
||||
"COMPONENT_ID": "try_catch"
|
||||
}
|
||||
},
|
||||
"warnings": []
|
||||
},
|
||||
{
|
||||
"name": "arcrun-validate-json",
|
||||
"source_dir": ".component-builds/validate_json",
|
||||
"source_commit": "922a57fe3458dfda82bcc25d287cef93f8dab838",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-validate-json/worker.mjs",
|
||||
"js_bytes": 67886,
|
||||
"content_sha256": "2d0efa26e9fe38db87eb9585daf72e0f2ba8c859d15d1b65f36798656cc4e83b",
|
||||
"modules": [
|
||||
{
|
||||
"name": "component.wasm",
|
||||
"type": "application/wasm",
|
||||
"file": "arcrun-validate-json/component.wasm",
|
||||
"sha256": "8142ffd3cc57efbb9128faaf450b646111b0f2e2575cc29a7b562a7b185912ab"
|
||||
}
|
||||
],
|
||||
"compat_date": "2025-02-19",
|
||||
"compat_flags": [],
|
||||
"requires": {
|
||||
"kv": [],
|
||||
"d1": [],
|
||||
"vectorize": 0,
|
||||
"ai": false,
|
||||
"vars": {
|
||||
"COMPONENT_ID": "validate_json"
|
||||
}
|
||||
},
|
||||
"warnings": []
|
||||
},
|
||||
{
|
||||
"name": "arcrun-wait",
|
||||
"source_dir": ".component-builds/wait",
|
||||
"source_commit": "922a57fe3458dfda82bcc25d287cef93f8dab838",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-wait/worker.mjs",
|
||||
"js_bytes": 67697,
|
||||
"content_sha256": "3d1b46e7b1ff056abb2c1aeed738d23d58eaa7c8f36b674e4ea2ed1602015f9e",
|
||||
"modules": [
|
||||
{
|
||||
"name": "component.wasm",
|
||||
"type": "application/wasm",
|
||||
"file": "arcrun-wait/component.wasm",
|
||||
"sha256": "52ca2d184bf3dcc646bb94f29f7f3e69b5540276363e83a54cb435ff33ca2f3c"
|
||||
}
|
||||
],
|
||||
"compat_date": "2025-02-19",
|
||||
"compat_flags": [],
|
||||
"requires": {
|
||||
"kv": [],
|
||||
"d1": [],
|
||||
"vectorize": 0,
|
||||
"ai": false,
|
||||
"vars": {
|
||||
"COMPONENT_ID": "wait"
|
||||
}
|
||||
},
|
||||
"warnings": []
|
||||
},
|
||||
{
|
||||
"name": "arcrun-rag-ui",
|
||||
"source_dir": "console-ui/public",
|
||||
"source_commit": "614fe448124dbd25d8228e37723473f1704f3a7a",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-rag-ui/worker.mjs",
|
||||
"js_bytes": 524513,
|
||||
"content_sha256": "5825f427e4211dd4b0eee969c94c8bfa856f65b8a48fc718e137f3b5d074e1ff",
|
||||
"modules": [],
|
||||
"compat_date": "2026-07-01",
|
||||
"compat_flags": [],
|
||||
"requires": {
|
||||
"kv": [],
|
||||
"d1": [],
|
||||
"vectorize": 0,
|
||||
"ai": false,
|
||||
"vars": {}
|
||||
},
|
||||
"ui_fingerprint": "82bc636990e60172663f7ce90c75d8b434193b8532b21f1d4b57c5fb330cbe11",
|
||||
"ui_file_count": 9,
|
||||
"warnings": []
|
||||
}
|
||||
],
|
||||
"excluded": [
|
||||
{
|
||||
"name": "arcrun-claude-api",
|
||||
"dir": ".component-builds/claude_api",
|
||||
"reason": "component.wasm 不在版控(.gitignore 明文排除=這顆不是可出貨的零件)"
|
||||
}
|
||||
],
|
||||
"notes": []
|
||||
}
|
||||
@@ -65,15 +65,26 @@
|
||||
|
||||
## 🔴 第一鐵律:wiki 是判準,不准跳過(2026-07-20/21 leo 兩度點破)
|
||||
|
||||
**要查任何東西之前,先搜尋 wiki——用 grep,不是只讀開頭幾行。**
|
||||
**要查任何東西之前,先照索引走:`system-dev/wiki/INDEX.md` → 下層 `00-INDEX.md` → 卡片。**
|
||||
|
||||
🔴 **2026-08-15 leo 糾正:本條原本寫的是「用 grep」,那是錯的。**
|
||||
grep 是平面搜尋,它繞過索引,於是**索引永遠不會被驗證、也永遠不會變準**——
|
||||
而 AI 又用「索引可能不準」當理由繼續 grep。這是自我實現的。
|
||||
實測那天:`matrix/arcrun` 的 wiki 有 64 張卡,**只有 1 張走得到索引**
|
||||
(唯一那張是 leo 手寫的;63 張機器產的卡從沒加入任何索引),
|
||||
而 AI 從沒發現——**因為 grep 找得到,破損就永遠不會浮現。**
|
||||
|
||||
**grep 不是禁令,是異常訊號**:
|
||||
1. 先照索引走
|
||||
2. 索引走不到 → **那是索引壞了,先講出來**(哪一層缺、缺什麼)
|
||||
3. 講完才准 fallback 用 `grep -rin "<關鍵字>" system-dev/wiki/`
|
||||
|
||||
⇒ 每一次 grep 都該留下一筆「索引缺陷」,而不是變成習慣。
|
||||
(全面禁止 grep 也是錯的——索引壞掉那天 AI 會瞎掉,而且沒人知道。)
|
||||
|
||||
> leo:「花很多力氣去產生 wiki,最重要的就是要可以查詢,**結果要查的時候就跳過,那就白寫了**。」
|
||||
> 「重點是你自己的記憶對嗎?而你有按照規定去切實讀 wiki 嗎?」
|
||||
|
||||
```bash
|
||||
grep -rin "<本題關鍵字>" system-dev/wiki/
|
||||
```
|
||||
|
||||
**三條硬規則**:
|
||||
1. **wiki 與程式碼/歷史文件衝突 → 以 wiki 為準**。程式碼反映「還沒清乾淨」,不等於「還在用」。
|
||||
2. wiki 寫「不可動/待廢除/進行中」→ **讀它的解除條件並逐條核對**。那是當時狀態,不是永久禁令。
|
||||
|
||||
+3
-2
@@ -8,11 +8,12 @@
|
||||
"main": "./dist/index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "npm run build:harness && npm run check:harness && tsc",
|
||||
"build": "npm run build:harness && npm run check:harness && npm run check:rule && tsc",
|
||||
"build:harness": "node scripts/build-harness-skill.mjs",
|
||||
"check:harness": "node scripts/check-harness-generation.mjs",
|
||||
"check:rule": "node ../scripts/sync-resource-rule.mjs --check",
|
||||
"dev": "tsc --watch",
|
||||
"test": "node --experimental-transform-types --import ./tests/register-ts-hooks.mjs --test \"tests/**/*.test.ts\"",
|
||||
"test": "npm run check:rule && node --experimental-transform-types --import ./tests/register-ts-hooks.mjs --test \"tests/**/*.test.ts\"",
|
||||
"prepublishOnly": "npm run build && chmod +x dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -17,6 +17,7 @@ import { loadConfig } from '../lib/config.js';
|
||||
import {
|
||||
wranglerAvailable,
|
||||
downloadAndDeploy,
|
||||
namespaceHasKnowledge,
|
||||
type DeployContext,
|
||||
} from '../lib/deploy.js';
|
||||
|
||||
@@ -63,6 +64,32 @@ export async function cmdUpdate(opts: { force?: boolean } = {}): Promise<void> {
|
||||
kbdbEmbed: config.kbdb_embed !== false,
|
||||
};
|
||||
|
||||
// Arcrun#108:把「你的知識住在哪個命名空間」同步給雲端——但**先驗再寫**。
|
||||
//
|
||||
// 病灶:你 push 工作流、小幫手上傳知識、MCP 查詢,用的都是 config 的 `api_key`;
|
||||
// 而 cypher 讀藏書地圖/搜尋/工作流時,過濾用的 owner_id 來自 worker 的環境變數
|
||||
// (repo toml 帶的官方預設 `CONSOLE_TENANT = "leo"`)。兩個來源對不上 ⇒ 你的東西全被濾掉。
|
||||
//
|
||||
// 為什麼不無條件寫:一鍵安裝的實例,知識可能本來就寫在 `CONSOLE_TENANT` 底下。
|
||||
// 無條件蓋成本機 api_key,會把一台**原本正常**的實例指向空的那一格
|
||||
// ——那就是 #97/#106 那類「更新一次把人家的東西弄不見」。所以查得到才寫,查不到就不碰。
|
||||
if (config.api_key && config.cypher_executor_url) {
|
||||
process.stdout.write(chalk.gray(' → 核對雲端要用哪個知識命名空間...'));
|
||||
const hasKnowledge = await namespaceHasKnowledge(config.cypher_executor_url, config.api_key);
|
||||
if (hasKnowledge === true) {
|
||||
ctx.knowledgeNamespace = config.api_key;
|
||||
console.log(chalk.green(' ✓'));
|
||||
console.log(chalk.gray(` ARCRUN_NAMESPACE = ${config.api_key}(這個命名空間底下查得到你的知識庫)`));
|
||||
} else if (hasKnowledge === false) {
|
||||
console.log(chalk.yellow(' ⚠'));
|
||||
console.log(chalk.gray(` ${config.api_key} 底下目前查不到任何知識庫 → 這趟不動雲端的命名空間設定`));
|
||||
console.log(chalk.gray(' (若藏書地圖是空的,請把這行連同 acr update 的輸出一起回報)'));
|
||||
} else {
|
||||
console.log(chalk.yellow(' ⚠'));
|
||||
console.log(chalk.gray(' 問不到實例(可能正在啟動或版本較舊)→ 這趟不動雲端的命名空間設定'));
|
||||
}
|
||||
}
|
||||
|
||||
// mode:'update' → 資源解析在「一顆該更新的 worker 都找不到」時會停手而不是重建一整套
|
||||
//(Arcrun#97 的另一道門:名字對不上時別假裝這是全新安裝)。
|
||||
const result = await downloadAndDeploy(ctx, 'main', { force: opts.force, mode: 'update' });
|
||||
|
||||
+49
-156
@@ -3,7 +3,8 @@
|
||||
* 使用 CF REST API 直接存取用戶的 KV namespace,不依賴 Wrangler CLI
|
||||
*/
|
||||
|
||||
import type { LiveBinding, ResourceApi, ScriptBindings } from './resource-resolver.js';
|
||||
import { createCloudflareResourceApi } from './resource-rule/cf-resource-api.mjs';
|
||||
import type { ResourceApi, ScriptBindings } from './resource-resolver.js';
|
||||
|
||||
const CF_API_BASE = 'https://api.cloudflare.com/client/v4';
|
||||
|
||||
@@ -86,189 +87,81 @@ export class CfKvClient {
|
||||
* 對應 SDD:.agents/specs/arcrun/sdk-and-website/self-hosted-init.md §3 step 1-2
|
||||
*/
|
||||
export class CfAccountClient implements ResourceApi {
|
||||
private accountBase: string;
|
||||
private headers: Record<string, string>;
|
||||
/**
|
||||
* `ResourceApi` 的七個方法**全部委派**給共用規則附的那支 client
|
||||
* (`shared/resource-rule/cf-resource-api.mjs`)。
|
||||
*
|
||||
* 🔴 為什麼不是在這裡自己實作一份:判斷一致還不夠,**看到的東西**也要一致。
|
||||
* 兩條路各自寫一份 CF client,只要有一邊把 404 當錯誤、漏了 per_page、少認一種
|
||||
* 欄位名,那一邊就會「看不到既有綁定」——而看不到既有綁定的下一步,依規則就是新建。
|
||||
* Arcrun#97 不需要規則寫錯,眼睛不一樣就足以重演。
|
||||
*/
|
||||
private readonly rule: ReturnType<typeof createCloudflareResourceApi>;
|
||||
|
||||
constructor(accountId: string, apiToken: string) {
|
||||
this.accountBase = `${CF_API_BASE}/accounts/${accountId}`;
|
||||
this.headers = {
|
||||
'Authorization': `Bearer ${apiToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
this.rule = createCloudflareResourceApi({ accountId, apiToken });
|
||||
}
|
||||
|
||||
private async cf<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const { ok, status, result, error } = await this.cfRaw<T>(path, init);
|
||||
const { ok, status, result, error } = await this.rule.cfRaw(path, init);
|
||||
if (!ok) throw new Error(`CF API ${path} 失敗:${error ?? `HTTP ${status}`}`);
|
||||
return result as T;
|
||||
}
|
||||
|
||||
/** 同 cf(),但把 HTTP status 交回呼叫端自己判斷(要區分「404 不存在」和「其他錯誤」時用)。 */
|
||||
private async cfRaw<T>(
|
||||
path: string,
|
||||
init?: RequestInit,
|
||||
): Promise<{ ok: boolean; status: number; result?: T; error?: string }> {
|
||||
const res = await fetch(`${this.accountBase}${path}`, {
|
||||
...init,
|
||||
headers: { ...this.headers, ...(init?.headers ?? {}) },
|
||||
});
|
||||
const data = await res.json().catch(() => null) as
|
||||
| { success: boolean; result: T; errors?: Array<{ message: string }> }
|
||||
| null;
|
||||
if (!res.ok || !data?.success) {
|
||||
return {
|
||||
ok: false,
|
||||
status: res.status,
|
||||
error: data?.errors?.map(e => e.message).filter(Boolean).join('; ') || `HTTP ${res.status}`,
|
||||
};
|
||||
}
|
||||
return { ok: true, status: res.status, result: data.result };
|
||||
}
|
||||
|
||||
/** 驗證 token 能存取此 account(權限不足會在後續建立操作報錯,這裡先確認 account 可達)。*/
|
||||
async verifyAccess(): Promise<void> {
|
||||
// GET /accounts/{id} 能通 = token 有此 account 的基本讀權限
|
||||
await this.cf<{ id: string; name: string }>('');
|
||||
}
|
||||
|
||||
/** 列出現有 KV namespace(冪等用:已存在就重用,不重建)。回傳 title → id 對照。*/
|
||||
async listKvNamespaces(): Promise<Map<string, string>> {
|
||||
const result = await this.cf<Array<{ id: string; title: string }>>(
|
||||
'/storage/kv/namespaces?per_page=100',
|
||||
);
|
||||
const map = new Map<string, string>();
|
||||
for (const ns of result) map.set(ns.title, ns.id);
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 無條件新建一顆 KV namespace。
|
||||
*
|
||||
* 🔴 Arcrun#97:這裡**故意沒有**「找不到同名就順手建一顆」的 ensure 版本。
|
||||
* 「照名字找 → 找不到 → 新建 → 綁上去」正是把使用者實例洗成空的那條路
|
||||
* (安裝器取的名字跟我們的 binding 名不一樣,永遠對不上 ⇒ 每次更新都新建)。
|
||||
* 要不要建,一律先經過 resource-resolver 的 planResources 判斷;那裡只有在
|
||||
* 「確定沒有任何已部署的 worker 綁過這個 binding」時才會排進 create。
|
||||
*/
|
||||
async createKvNamespace(title: string): Promise<string> {
|
||||
const result = await this.cf<{ id: string; title: string }>(
|
||||
'/storage/kv/namespaces',
|
||||
{ method: 'POST', body: JSON.stringify({ title }) },
|
||||
);
|
||||
return result.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 讀一顆已部署 worker 現在綁著哪些資源——**使用者那側的事實**(Arcrun#97 的唯一真相源)。
|
||||
* CF:`GET /accounts/{id}/workers/scripts/{script}/settings` → `result.bindings[]`。
|
||||
*
|
||||
* - script 不存在(404)→ `{ deployed: false }`,這是「還沒部署」,不是錯誤。
|
||||
* - 其他任何失敗 → throw。呼叫端必須把它當「我不知道」而**不是**「它沒有」——
|
||||
* 把查不到當成不存在,就是 #97 的根因。
|
||||
*/
|
||||
async getScriptBindings(script: string): Promise<ScriptBindings> {
|
||||
const path = `/workers/scripts/${encodeURIComponent(script)}/settings`;
|
||||
const res = await this.cfRaw<{ bindings?: RawWorkerBinding[] }>(path);
|
||||
if (!res.ok) {
|
||||
if (res.status === 404) return { deployed: false, bindings: [], vars: {} };
|
||||
throw new Error(`讀 ${script} 綁定失敗:${res.error}`);
|
||||
}
|
||||
const raw = res.result?.bindings ?? [];
|
||||
// #106:同一份回應裡也帶著 plain_text var(實測 CF `/settings` 會回 `text` 值)。
|
||||
// 舊版只挑資源類、把 var 整批丟掉 → 重部署等於把它們洗掉。
|
||||
return { deployed: true, bindings: normalizeBindings(raw), vars: normalizeVars(raw) };
|
||||
}
|
||||
|
||||
/** 查 workers.dev subdomain(cypher-executor WORKER_SUBDOMAIN 用,組對內 component URL)。*/
|
||||
async getWorkersSubdomain(): Promise<string> {
|
||||
const result = await this.cf<{ subdomain: string }>('/workers/subdomain');
|
||||
return result.subdomain;
|
||||
}
|
||||
|
||||
// D1 (KBDB Base). Free on Workers Free plan, no credit card (kbdb-base Q4 verified).
|
||||
async listD1Databases(): Promise<Map<string, string>> {
|
||||
const result = await this.cf<Array<{ uuid: string; name: string }>>('/d1/database?per_page=100');
|
||||
const map = new Map<string, string>();
|
||||
for (const db of result) map.set(db.name, db.uuid);
|
||||
return map;
|
||||
// ── 以下七支=`ResourceApi`,一律委派共用規則,**這個檔案不得自己實作** ────────────
|
||||
// (`shared/resource-rule/cf-resource-api.mjs`;委派而非複製的理由見本 class 開頭)
|
||||
|
||||
/** 讀一顆已部署 worker 現在綁著哪些資源——使用者那側的事實(Arcrun#97 的唯一真相源)。 */
|
||||
getScriptBindings(script: string): Promise<ScriptBindings> {
|
||||
return this.rule.getScriptBindings(script);
|
||||
}
|
||||
|
||||
/** 無條件新建 D1。沒有 ensure 版本,理由同 createKvNamespace(Arcrun#97)。 */
|
||||
async createD1Database(name: string): Promise<string> {
|
||||
const result = await this.cf<{ uuid: string; name: string }>(
|
||||
'/d1/database',
|
||||
{ method: 'POST', body: JSON.stringify({ name }) },
|
||||
);
|
||||
return result.uuid;
|
||||
/** 帳號上現有的 KV namespace(title → id)。判斷「綁著的那顆還在不在」用。 */
|
||||
listKvNamespaces(): Promise<Map<string, string>> {
|
||||
return this.rule.listKvNamespaces();
|
||||
}
|
||||
|
||||
/** 帳號上現有的 Vectorize index 名單(判斷「綁著的那顆還在不在」用)。 */
|
||||
async listVectorizeIndexes(): Promise<string[]> {
|
||||
const result = await this.cf<Array<{ name: string }>>('/vectorize/v2/indexes');
|
||||
return (result ?? []).map(i => i.name);
|
||||
/** 帳號上現有的 D1(name → uuid)。 */
|
||||
listD1Databases(): Promise<Map<string, string>> {
|
||||
return this.rule.listD1Databases();
|
||||
}
|
||||
|
||||
/** 帳號上現有的 Vectorize index 名單。 */
|
||||
listVectorizeIndexes(): Promise<string[]> {
|
||||
return this.rule.listVectorizeIndexes();
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建 KBDB embed 用的 Vectorize index(**bge-m3 = 1024 維 / cosine**,見 deploy.ts 常數說明)。
|
||||
* 已存在(409 / already exists)視為成功——並行或重跑不該炸。沒有 ensure 版本:
|
||||
* 「要不要建」由 planResources 判斷,這裡只負責建(Arcrun#97)。
|
||||
* 無條件新建一顆 KV namespace。
|
||||
*
|
||||
* 🔴 Arcrun#97:**故意沒有**「找不到同名就順手建一顆」的 ensure 版本。
|
||||
* 「照名字找 → 找不到 → 新建 → 綁上去」正是把使用者實例洗成空的那條路。
|
||||
* 要不要建,一律先經過 planResources;那裡只有在「確定沒有任何已部署的 worker
|
||||
* 綁過這個 binding」時才會排進 create。
|
||||
*/
|
||||
async createVectorizeIndex(name: string): Promise<string> {
|
||||
const res = await this.cfRaw<{ name: string }>('/vectorize/v2/indexes', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
config: { dimensions: 1024, metric: 'cosine' },
|
||||
description: 'arcrun KBDB embed module — bge-m3 1024d (issue #7 / #59)',
|
||||
}),
|
||||
});
|
||||
if (res.ok) return name;
|
||||
const detail = (res.error ?? '').toLowerCase();
|
||||
if (res.status === 409 || /already exists|duplicate|conflict/.test(detail)) return name;
|
||||
throw new Error(`建 Vectorize index ${name} 失敗:${res.error}`);
|
||||
createKvNamespace(title: string): Promise<string> {
|
||||
return this.rule.createKvNamespace(title);
|
||||
}
|
||||
|
||||
/** 無條件新建 D1。沒有 ensure 版本,理由同 createKvNamespace(Arcrun#97)。 */
|
||||
createD1Database(name: string): Promise<string> {
|
||||
return this.rule.createD1Database(name);
|
||||
}
|
||||
|
||||
/** 新建 KBDB embed 用的 Vectorize index。沒有 ensure 版本,理由同上(Arcrun#97)。 */
|
||||
createVectorizeIndex(name: string): Promise<string> {
|
||||
return this.rule.createVectorizeIndex(name);
|
||||
}
|
||||
}
|
||||
|
||||
/** CF `/settings` 回的 binding 原始形狀(同一種資源在不同 API 版本欄位名不一,故全都收)。 */
|
||||
interface RawWorkerBinding {
|
||||
type?: string;
|
||||
name?: string;
|
||||
namespace_id?: string;
|
||||
id?: string;
|
||||
database_id?: string;
|
||||
index_name?: string;
|
||||
/** `plain_text` 綁定的值(#106;secret_text 不會回值,本來就讀不到,也不該讀)。 */
|
||||
text?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 抽出已部署 worker 上的 `plain_text` var(#106)。
|
||||
*
|
||||
* 只收 `plain_text`——**`secret_text` 一律不碰**(CF 本來就不回值,也不該被 CLI 搬來搬去;
|
||||
* wrangler deploy 不會動 secret,它們自己會留著)。
|
||||
*/
|
||||
function normalizeVars(raw: RawWorkerBinding[]): Record<string, string> {
|
||||
const out: Record<string, string> = {};
|
||||
for (const b of raw) {
|
||||
if (b?.type === 'plain_text' && b.name && typeof b.text === 'string') out[b.name] = b.text;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 把 CF 的 binding 陣列收斂成 resolver 認得的三種資源。不認得的型別直接略過。 */
|
||||
function normalizeBindings(raw: RawWorkerBinding[]): LiveBinding[] {
|
||||
const out: LiveBinding[] = [];
|
||||
for (const b of raw) {
|
||||
if (!b?.name) continue;
|
||||
if (b.type === 'kv_namespace') {
|
||||
const value = b.namespace_id ?? b.id;
|
||||
if (value) out.push({ kind: 'kv_namespace', binding: b.name, value });
|
||||
} else if (b.type === 'd1' || b.type === 'd1_database') {
|
||||
const value = b.id ?? b.database_id;
|
||||
if (value) out.push({ kind: 'd1', binding: b.name, value });
|
||||
} else if (b.type === 'vectorize') {
|
||||
if (b.index_name) out.push({ kind: 'vectorize', binding: b.name, value: b.index_name });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -298,6 +298,44 @@ export interface DeployContext {
|
||||
// [[vectorize]]+[ai] binding(取消 wrangler.toml 註解段)→ embed 模組啟用。未設/false → 不建、不注入,
|
||||
// base 維持 LIKE keyword(free-tier 友善)。
|
||||
kbdbEmbed?: boolean;
|
||||
/**
|
||||
* Arcrun#108:這台實例的知識命名空間(=`~/.arcrun/config.yaml` 的 `api_key`),
|
||||
* 會寫進 cypher worker 的 `ARCRUN_NAMESPACE` var,讓「讀」用的 owner_id 與「寫」的一致。
|
||||
*
|
||||
* **只在驗證過該 namespace 底下真的有知識時才給值**(見 `resolveKnowledgeNamespace`)——
|
||||
* 給了就會覆蓋 worker 上的既有值,沒給則原封保留(preservedVars)。
|
||||
*/
|
||||
knowledgeNamespace?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 這把 namespace 底下到底有沒有知識?(Arcrun#108 的「先驗再寫」)
|
||||
*
|
||||
* 打的是實例自己的 `GET /kbdb/map?owner_id=<ns>`(cypher 既有的純轉發端點,CLI 平常就在用
|
||||
* 這條路 + `X-Arcrun-API-Key`)。回傳:
|
||||
* true = 這個 namespace 底下查得到庫 → 寫 ARCRUN_NAMESPACE 是安全的
|
||||
* false = 查得到但是空的 → 不寫(可能知識其實在別的命名空間,蓋下去會把畫面弄空)
|
||||
* null = 問不到(實例還沒起來 / 舊版沒這條路 / 網路斷)→ 不寫,也不宣稱任何事
|
||||
*
|
||||
* 誠實邊界:這支只回答「有沒有」,不猜「應該是哪一個」。猜錯的代價是把人家的資料藏起來。
|
||||
*/
|
||||
export async function namespaceHasKnowledge(
|
||||
cypherUrl: string,
|
||||
namespace: string,
|
||||
): Promise<boolean | null> {
|
||||
if (!cypherUrl || !namespace) return null;
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${cypherUrl.replace(/\/+$/, '')}/kbdb/map?owner_id=${encodeURIComponent(namespace)}`,
|
||||
{ headers: { 'X-Arcrun-API-Key': namespace } },
|
||||
);
|
||||
if (!res.ok) return null;
|
||||
const body = (await res.json().catch(() => null)) as { libraries?: unknown } | null;
|
||||
if (!body || !Array.isArray(body.libraries)) return null;
|
||||
return body.libraries.length > 0;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -561,6 +599,21 @@ export async function downloadAndDeploy(
|
||||
vars.ARCRUN_BUNDLE_VERSION = stamp.version;
|
||||
if (stamp.commit) vars.ARCRUN_BUNDLE_COMMIT = stamp.commit;
|
||||
}
|
||||
// Arcrun#108:把「你的知識實際住在哪個命名空間」告訴雲端。
|
||||
//
|
||||
// 為什麼需要:cypher 讀藏書地圖/搜尋/工作流時要用一個 owner_id 去過濾,而它以前拿的是
|
||||
// repo toml 帶的官方預設值(`CONSOLE_TENANT = "leo"`)。寫入端(CLI push、小幫手上傳、
|
||||
// MCP)用的卻是你 `~/.arcrun/config.yaml` 的 `api_key` ⇒ 兩邊對不上就整個空掉
|
||||
//(leo 實撞:1854 條三元組被過濾成 0 個庫)。
|
||||
//
|
||||
// 🔴 **只在「這個 namespace 底下真的查得到知識」時才寫**(呼叫端已先驗過,見
|
||||
// resolveKnowledgeNamespace)。理由是反過來的那個災難:一鍵安裝的實例,知識可能
|
||||
// 本來就寫在 CONSOLE_TENANT 底下;若這裡無條件蓋成本機 api_key,會把一台**原本正常**
|
||||
// 的實例改成指向空的那一格——跟 #97/#106 同一類「更新一次把人家的東西弄不見」。
|
||||
// 驗不過就不寫;既有值由 preservedVars 原封保留,等於這趟什麼都沒改。
|
||||
if (ctx.knowledgeNamespace && script === VERSION_STAMP_WORKER) {
|
||||
vars.ARCRUN_NAMESPACE = ctx.knowledgeNamespace;
|
||||
}
|
||||
if (Object.keys(vars).length > 0) extraVarsByDir.set(dir, vars);
|
||||
}
|
||||
if (preservedTotal.length > 0) {
|
||||
@@ -668,6 +721,22 @@ export async function downloadAndDeploy(
|
||||
} else {
|
||||
failures.push(`D1 migration: 部署物缺 kbdb/migrations/0004_execution_log_template.sql(${execLogMigPath})`);
|
||||
}
|
||||
|
||||
// 3.8 樹狀 record 模型(0007,v7 定稿 2026-08-15):record 有身分、關係是唯一機制、
|
||||
// entry_values 拆表。**必須排在所有 template seed 之後**(它把 templates 表既有列
|
||||
// 鏡射成池中 sheet/field entry)。逐句套用+容錯 duplicate column:檔內三句
|
||||
// ADD COLUMN 在 SQLite 沒有 IF NOT EXISTS 形式,重跑(每次部署都會重跑本段)時
|
||||
// 那三句報 duplicate column = 已套用,其餘語句全部語句級冪等(檔頭有完整說明)。
|
||||
const treeMigPath = join(root, 'kbdb', 'migrations', '0007_tree_record_model.sql');
|
||||
if (existsSync(treeMigPath)) {
|
||||
try {
|
||||
await applyD1MigrationTolerant(ctx, readFileSync(treeMigPath, 'utf8'));
|
||||
} catch (e) {
|
||||
failures.push(`D1 migration 0007_tree_record_model (${ctx.d1DatabaseId}): ${e instanceof Error ? e.message : String(e)}`);
|
||||
}
|
||||
} else {
|
||||
failures.push(`D1 migration: 部署物缺 kbdb/migrations/0007_tree_record_model.sql(${treeMigPath})`);
|
||||
}
|
||||
}
|
||||
|
||||
const cypherExecutorUrl = ctx.workerSubdomain
|
||||
@@ -698,6 +767,35 @@ export async function downloadAndDeploy(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 逐句套 migration,容錯 duplicate column(0007 專用)。
|
||||
*
|
||||
* 為什麼不能走 applyD1Migration 整檔送:/query 端點任何一句失敗整批中止——
|
||||
* 0007 的三句 ADD COLUMN 在重跑時必然報 duplicate column(SQLite 沒有欄位級
|
||||
* IF NOT EXISTS),整檔送 ⇒ 第二次部署起 migration 永遠假紅、後面的資料搬遷
|
||||
* 語句永遠不被執行。逐句+把 duplicate column 視為「已套用」,其餘錯誤照樣拋。
|
||||
* 切句手法與安裝器 compile-migrations.mjs 同款(剝 -- 註解、依分號切;
|
||||
* 0007 的字串常值不含分號,前提成立)。
|
||||
*/
|
||||
async function applyD1MigrationTolerant(ctx: DeployContext, sql: string): Promise<void> {
|
||||
const statements = sql
|
||||
.split('\n')
|
||||
.map((l) => l.replace(/--.*$/, ''))
|
||||
.join('\n')
|
||||
.split(';')
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0);
|
||||
for (const stmt of statements) {
|
||||
try {
|
||||
await applyD1Migration(ctx, stmt);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
if (/duplicate column/i.test(msg)) continue; // ADD COLUMN 重跑=已套用
|
||||
throw new Error(`${stmt.slice(0, 60)}… → ${msg}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 對 D1 套 SQL migration(透過 CF API `/d1/database/{id}/query`,非 wrangler)。
|
||||
* 用 init 已驗的 ctx.apiToken + accountId;query 端點接受多語句檔,一次送整份 0001_base.sql。
|
||||
|
||||
@@ -1,431 +1,42 @@
|
||||
/**
|
||||
* resource-resolver.ts — 資源解析:「已部署的 worker 現在綁著什麼,那就是事實」
|
||||
* resource-resolver.ts — **這裡沒有邏輯**,只是把共用規則接到 CLI 的既有 import 路徑上。
|
||||
*
|
||||
* 🔴 Arcrun#97(2026-08-12 實害,leo 的實例中了):
|
||||
* 舊做法叫「照名字 ensure」——`acr update` 拿 **binding 名**(`WEBHOOKS`)當成 Cloudflare 上的
|
||||
* **資源標題**去找,找不到就**新建一顆空的、然後綁到 worker 上**。
|
||||
* 安裝器建的資源不叫那個名字(它叫 `arcrun-rag-<instance>-kv-webhooks`)⇒ 一次例行更新
|
||||
* 新建了 9 顆 KV、1 顆 D1,使用者的工作流/登入狀態/子庫**在畫面上全部消失**。
|
||||
* 資料沒有被刪,但 worker 被綁去空的那幾顆——從使用者的角度,他的東西就是不見了。
|
||||
* 「這個實例該用哪些資源」的規則住在 `shared/resource-rule/`(repo 根目錄),
|
||||
* 那是**唯一一份人手維護的實作**;`./resource-rule/` 是該目錄的逐位元組鏡射
|
||||
* (`scripts/sync-resource-rule.mjs` 產生,`npm run build` / `npm test` 會跑 `--check` 擋漂移)。
|
||||
* 之所以要有這份鏡射:`arcrun` 是獨立 npm 套件,`npm pack` 打不進套件目錄外的檔案。
|
||||
*
|
||||
* 根因不是「KV 那段寫錯」,是**「用名字猜使用者的資源」這個做法本身**:
|
||||
* 名字是**使用者那側的事實**(安裝器要怎麼取名由它決定,而且它有權改),
|
||||
* 我們不能拿自己的命名慣例去對號入座,更不能在對不上的時候自作主張生一顆新的。
|
||||
* ——所以修法不是「多比對幾種名字」,是**不再用名字當識別**。
|
||||
* 為什麼規則不在 CLI(leo 2026-08-12):
|
||||
* 「根本就不應該在 CLI,我要的是一個大家都可以用到的規則。」
|
||||
* ——`acr` 有這條規則、安裝器沒有,結果就是 Arcrun#97:
|
||||
* 安裝器照名字找、找不到就建一顆空的綁上去,使用者的工作流與登入狀態整片消失。
|
||||
* 規則搬到共用層之後,安裝器直接 import 同一份原稿,**不再有第二種答案**。
|
||||
*
|
||||
* ── 新規則(三句話)────────────────────────────────────────────────
|
||||
* 1. **已部署的 worker 上綁著什麼,那就是事實** → 原封不動沿用,不管那顆資源叫什麼名字。
|
||||
* 2. **只有「確定沒有任何人綁過它」才准新建**(新版本新增的 binding、或真的全新帳號)。
|
||||
* 3. **只要有一點說不準就整趟停手**(讀不到綁定/綁著的資源不見了/同一個 binding 指向兩顆/
|
||||
* 該更新的 worker 一顆都不在),**什麼都不建、什麼都不部署**,把話說清楚讓人來判斷。
|
||||
*
|
||||
* ── 為什麼拆成 plan / apply 兩段 ─────────────────────────────────────
|
||||
* `planResources()` **完全不寫入**,只回一份「要沿用什麼、要新建什麼、有什麼不敢動的」。
|
||||
* `applyResourcePlan()` 看到有任何 blocker 就直接拒絕執行。
|
||||
* ⇒「被擋下的時候一顆資源都不會被建出來」是**結構上的保證**,
|
||||
* 不是靠某個人記得在對的地方寫 early return。#97 正是死在「先動手、後判斷」。
|
||||
* 🔴 不要把任何判斷寫回這個檔案。要改規則 → 改 `shared/resource-rule/rule.mjs`。
|
||||
*/
|
||||
|
||||
/** 這支負責的資源種類。要加新種類(R2/Queue/Hyperdrive…)就加在這裡,
|
||||
* 一律走同一道門——不准任何呼叫端自己「照名字 ensure」繞過去。 */
|
||||
export type ResourceKind = 'kv_namespace' | 'd1' | 'vectorize';
|
||||
export {
|
||||
planResources,
|
||||
applyResourcePlan,
|
||||
parseWranglerRequirements,
|
||||
normalizeLiveBindings,
|
||||
normalizeLiveVars,
|
||||
bindingKey,
|
||||
ResourcePlanBlocked,
|
||||
KIND_LABEL,
|
||||
TABLE_KIND,
|
||||
} from './resource-rule/rule.mjs';
|
||||
|
||||
/** 從已部署 worker 上讀回來的一條綁定。`value`:KV/D1 是資源 id,Vectorize 是 index 名。 */
|
||||
export interface LiveBinding {
|
||||
kind: ResourceKind;
|
||||
binding: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ScriptBindings {
|
||||
/** false = 這顆 worker 在帳號上還不存在(全新部署),不是「讀取失敗」。讀取失敗要 throw。 */
|
||||
deployed: boolean;
|
||||
bindings: LiveBinding[];
|
||||
/**
|
||||
* 這顆 worker 現在掛著的 `plain_text` var(名 → 值)。
|
||||
*
|
||||
* 🔴 Arcrun#106:#97 只把「資源類」綁定當成事實沿用(KV/D1/Vectorize),
|
||||
* plain_text var 整批沒人管 ⇒ 重部署把它們洗成 repo toml 的預設值。
|
||||
* 最痛的一個是 `ARCRUN_BUNDLE_VERSION`(安裝器注入的版本標籤)——
|
||||
* 更新完就消失,Portal 設定頁變成「無法讀取目前版本」。
|
||||
* **保留了櫃子,沒保留櫃子上的標籤**。這個欄位就是那些標籤。
|
||||
*/
|
||||
vars?: Record<string, string>;
|
||||
}
|
||||
|
||||
/** resolver 需要的 CF 能力(收窄成介面,方便離線測試餵假帳號)。 */
|
||||
export interface ResourceApi {
|
||||
getScriptBindings(script: string): Promise<ScriptBindings>;
|
||||
/** title → id */
|
||||
listKvNamespaces(): Promise<Map<string, string>>;
|
||||
/** name → uuid */
|
||||
listD1Databases(): Promise<Map<string, string>>;
|
||||
listVectorizeIndexes(): Promise<string[]>;
|
||||
createKvNamespace(title: string): Promise<string>;
|
||||
createD1Database(name: string): Promise<string>;
|
||||
createVectorizeIndex(name: string): Promise<string>;
|
||||
}
|
||||
|
||||
/** 「這顆 worker 需要這個 binding」。createName 只在**真的要新建**時才會被拿來當名字用。 */
|
||||
export interface BindingRequirement {
|
||||
kind: ResourceKind;
|
||||
binding: string;
|
||||
/** 需要它的 worker script 名(= wrangler.toml 的 `name`)。 */
|
||||
worker: string;
|
||||
createName: string;
|
||||
}
|
||||
|
||||
export interface PlannedAdopt {
|
||||
kind: ResourceKind;
|
||||
binding: string;
|
||||
value: string;
|
||||
/** 從哪顆已部署的 worker 上讀到的 */
|
||||
from: string;
|
||||
}
|
||||
|
||||
export interface PlannedCreate {
|
||||
kind: ResourceKind;
|
||||
binding: string;
|
||||
createName: string;
|
||||
wantedBy: string[];
|
||||
/** 其他也指向同一顆資源的 binding(見 shareSameResource)。建一顆,大家共用。 */
|
||||
alsoBind: string[];
|
||||
}
|
||||
|
||||
export interface ResourcePlan {
|
||||
adopt: PlannedAdopt[];
|
||||
create: PlannedCreate[];
|
||||
/** 非空 = 整趟停手。applyResourcePlan 會拒絕執行。 */
|
||||
blockers: string[];
|
||||
/**
|
||||
* 每顆**已部署** worker 現在掛著的 plain_text var(script → 名/值)。未部署的不在裡面。
|
||||
*
|
||||
* Arcrun#106:讀綁定的時候本來就把整份 `bindings[]` 拿回來了,var 就在同一份回應裡——
|
||||
* 順手帶出來,**不另外打一次 API**,也不新增一種「查不到」的失敗模式
|
||||
* (讀不到綁定這件事已經在上面 blockers 那一關擋掉了)。
|
||||
*/
|
||||
liveVars: Map<string, Record<string, string>>;
|
||||
}
|
||||
|
||||
export interface ResolvedResource {
|
||||
kind: ResourceKind;
|
||||
binding: string;
|
||||
value: string;
|
||||
origin: 'adopted' | 'created';
|
||||
from?: string;
|
||||
}
|
||||
|
||||
/** plan 被擋下時丟這個,讓呼叫端能把每一條原因原文轉給使用者。 */
|
||||
export class ResourcePlanBlocked extends Error {
|
||||
constructor(readonly blockers: string[]) {
|
||||
super(`資源解析被擋下(${blockers.length} 項)`);
|
||||
this.name = 'ResourcePlanBlocked';
|
||||
}
|
||||
}
|
||||
|
||||
export function bindingKey(kind: ResourceKind, binding: string): string {
|
||||
return `${kind}:${binding}`;
|
||||
}
|
||||
|
||||
const KIND_LABEL: Record<ResourceKind, string> = {
|
||||
kv_namespace: 'KV namespace',
|
||||
d1: 'D1 資料庫',
|
||||
vectorize: 'Vectorize index',
|
||||
};
|
||||
|
||||
function msg(e: unknown): string {
|
||||
return e instanceof Error ? e.message : String(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* 決定每個 binding 要沿用哪顆資源/要不要新建,**不寫入任何東西**。
|
||||
*
|
||||
* @param mode 'update' = 這台照定義已經裝過了(見下方「一顆都不在」規則);'init' = 全新安裝,允許從零建。
|
||||
*/
|
||||
export async function planResources(
|
||||
api: ResourceApi,
|
||||
requirements: readonly BindingRequirement[],
|
||||
mode: 'update' | 'init',
|
||||
): Promise<ResourcePlan> {
|
||||
const blockers: string[] = [];
|
||||
const adopt: PlannedAdopt[] = [];
|
||||
const create: PlannedCreate[] = [];
|
||||
|
||||
// ── 1. 先讀「即將被覆蓋的每一顆 worker」現在綁著什麼 ──────────────────
|
||||
// 讀取失敗 ≠ 沒有綁。#97 的災情就是把「我查不到」當成「它不存在」。
|
||||
const scripts = [...new Set(requirements.map((r) => r.worker))].sort();
|
||||
const live = new Map<string, LiveBinding[]>();
|
||||
const liveVars = new Map<string, Record<string, string>>();
|
||||
let readFailed = false;
|
||||
for (const script of scripts) {
|
||||
try {
|
||||
const res = await api.getScriptBindings(script);
|
||||
if (res.deployed) {
|
||||
live.set(script, res.bindings);
|
||||
// #106:同一份回應裡的 plain_text var 一起收下(呼叫端要拿它決定哪些 var 該沿用)。
|
||||
liveVars.set(script, res.vars ?? {});
|
||||
}
|
||||
} catch (e) {
|
||||
readFailed = true;
|
||||
blockers.push(
|
||||
`讀不到已部署的 worker「${script}」目前綁著哪些資源(${msg(e)})。` +
|
||||
`不確定它現在用的是哪一顆,就不能重新綁——整趟更新停手,沒有動任何東西。`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 「這台照定義已經裝過了,卻一顆 worker 都找不到」= 我對不上它的實例(名字不同/token 看不到)。
|
||||
// 這種時候繼續走下去,等於把一整套資源重新生一遍再綁上去——正是 #97 的形狀,只是換一道門進來。
|
||||
if (mode === 'update' && !readFailed && live.size === 0 && scripts.length > 0) {
|
||||
blockers.push(
|
||||
`在這個 Cloudflare 帳號上找不到任何一顆要更新的 worker(找過:${scripts.join('、')})。` +
|
||||
`acr update 的前提是「這台已經裝好了」——對不上就不猜:` +
|
||||
`可能是 API token 看得到的帳號不對,或這台實例的 worker 用了別的名字。` +
|
||||
`已停手,沒有新建任何資源。`,
|
||||
);
|
||||
}
|
||||
|
||||
// ── 2. 逐個 binding 決定:沿用 / 新建 / 停手 ─────────────────────────
|
||||
const byKey = new Map<string, BindingRequirement[]>();
|
||||
for (const req of requirements) {
|
||||
const key = bindingKey(req.kind, req.binding);
|
||||
const list = byKey.get(key);
|
||||
if (list) list.push(req);
|
||||
else byKey.set(key, [req]);
|
||||
}
|
||||
|
||||
const existingCache = new Map<ResourceKind, Set<string>>();
|
||||
const listExisting = async (kind: ResourceKind): Promise<Set<string>> => {
|
||||
const hit = existingCache.get(kind);
|
||||
if (hit) return hit;
|
||||
let set: Set<string>;
|
||||
if (kind === 'kv_namespace') set = new Set((await api.listKvNamespaces()).values());
|
||||
else if (kind === 'd1') set = new Set((await api.listD1Databases()).values());
|
||||
else set = new Set(await api.listVectorizeIndexes());
|
||||
existingCache.set(kind, set);
|
||||
return set;
|
||||
};
|
||||
|
||||
for (const [, reqs] of byKey) {
|
||||
const { kind, binding } = reqs[0];
|
||||
|
||||
const found: Array<{ value: string; script: string }> = [];
|
||||
for (const [script, bindings] of live) {
|
||||
const hit = bindings.find((b) => b.kind === kind && b.binding === binding);
|
||||
if (hit) found.push({ value: hit.value, script });
|
||||
}
|
||||
const distinct = [...new Set(found.map((f) => f.value))];
|
||||
|
||||
// 2a. 同一個 binding 名在不同 worker 上指向不同資源 → 分不出哪個才是使用者要的。
|
||||
// 自己挑一個 = 有一半機率把另外那半的資料從畫面上抹掉。不猜。
|
||||
if (distinct.length > 1) {
|
||||
blockers.push(
|
||||
`綁定「${binding}」在不同 worker 上指向不同的 ${KIND_LABEL[kind]}` +
|
||||
`(${found.map((f) => `${f.script} → ${f.value}`).join('、')})。` +
|
||||
`分不出哪一顆才是你在用的,不猜——停手。`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2b. 有人綁著它 → 這就是事實,沿用。名字長什麼樣完全不看。
|
||||
if (distinct.length === 1) {
|
||||
const value = distinct[0];
|
||||
let existing: Set<string>;
|
||||
try {
|
||||
existing = await listExisting(kind);
|
||||
} catch (e) {
|
||||
blockers.push(
|
||||
`查不到帳號上的 ${KIND_LABEL[kind]} 清單,無法確認「${binding}」綁著的 ${value} 還在不在` +
|
||||
`(${msg(e)})。不確定就不動——停手。`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!existing.has(value)) {
|
||||
// 這正是 #97 的入口:舊版在這裡會安靜地新建一顆空的頂上去。
|
||||
blockers.push(
|
||||
`worker「${found[0].script}」的「${binding}」綁著 ${KIND_LABEL[kind]} ${value},` +
|
||||
`但這顆在你的 Cloudflare 帳號上找不到了。` +
|
||||
`這裡**不會**幫你新建一顆空的頂上去(Arcrun#97 的災情就是那樣來的)——` +
|
||||
`請先確認那顆資源是被刪掉了,還是這把 API token 看不到它。`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
adopt.push({ kind, binding, value, from: found[0].script });
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2c. 沒有任何已部署的 worker 綁過它 → 新版本新增的 binding,或全新帳號。
|
||||
// 這種情況下新建不會弄丟任何東西(本來就沒有東西可丟)。
|
||||
create.push({
|
||||
kind,
|
||||
binding,
|
||||
createName: reqs[0].createName,
|
||||
wantedBy: [...new Set(reqs.map((r) => r.worker))],
|
||||
alsoBind: [],
|
||||
});
|
||||
}
|
||||
|
||||
return { adopt, create: shareSameResource(adopt, create, byKey), blockers, liveVars };
|
||||
}
|
||||
|
||||
/**
|
||||
* 收斂「不同 binding 其實是同一顆資源」的情況。
|
||||
*
|
||||
* 判準是 **toml 自己宣告的名字**(`database_name` / `index_name`),不是使用者那側的資源名——
|
||||
* cypher 的 `CREDENTIALS_DB` 與 kbdb 的 `DB` 都寫 `database_name = "arcrun-kbdb"`,
|
||||
* 那是**我們**在宣告「這兩個綁定指向同一顆庫」,跟 #97 那種「拿名字去猜使用者的資源」是兩回事。
|
||||
*
|
||||
* 沒有這一步會出兩種錯:
|
||||
* ① 全新安裝時建出兩顆同名 D1,KBDB 的資料與 credential 目錄從此分家。
|
||||
* ② 一邊已部署(沿用既有)、另一邊沒有(新建一顆空的)→ 半套資料,比全壞更難查。
|
||||
*/
|
||||
function shareSameResource(
|
||||
adopt: PlannedAdopt[],
|
||||
create: PlannedCreate[],
|
||||
byKey: Map<string, BindingRequirement[]>,
|
||||
): PlannedCreate[] {
|
||||
const declaredName = (kind: ResourceKind, binding: string): string | undefined =>
|
||||
byKey.get(bindingKey(kind, binding))?.[0]?.createName;
|
||||
|
||||
const out: PlannedCreate[] = [];
|
||||
const groups = new Map<string, PlannedCreate>();
|
||||
|
||||
for (const c of create) {
|
||||
const groupKey = `${c.kind} | ||||