Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 179dd60571 | |||
| 9ff933ba98 | |||
| 2fcae722e7 | |||
| fad5da0e17 | |||
| 13155a1d7b | |||
| bb548b6fdf |
@@ -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. 統一帳號來源(薄殼共用同一身份)
|
||||
|
||||
所有薄殼讀**同一份**身份設定:
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __esm = (fn, res) => function __init() {
|
||||
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
||||
var __esm = (fn, res, err2) => function __init() {
|
||||
if (err2) throw err2[0];
|
||||
try {
|
||||
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
||||
} catch (e) {
|
||||
throw err2 = [e], e;
|
||||
}
|
||||
};
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
@@ -102,7 +107,7 @@ function applyBaseRuntimeOptions(runtime, options) {
|
||||
function applyModuleEvalRuntimeOptions(runtime, options) {
|
||||
options.moduleLoader && runtime.setModuleLoader(options.moduleLoader), options.shouldInterrupt && runtime.setInterruptHandler(options.shouldInterrupt), options.memoryLimitBytes !== void 0 && runtime.setMemoryLimit(options.memoryLimitBytes), options.maxStackSizeBytes !== void 0 && runtime.setMaxStackSize(options.maxStackSizeBytes);
|
||||
}
|
||||
var __defProp2, __export2, QTS_DEBUG, errors_exports, QuickJSUnwrapError, QuickJSWrongOwner, QuickJSUseAfterFree, QuickJSNotImplemented, QuickJSAsyncifyError, QuickJSAsyncifySuspended, QuickJSMemoryLeakDetected, QuickJSEmscriptenModuleError, QuickJSUnknownIntrinsic, QuickJSPromisePending, QuickJSEmptyGetOwnPropertyNames, AwaitYield, UsingDisposable, SymbolDispose, prototypeAsAny, Lifetime, StaticLifetime, WeakLifetime, Scope, AbstractDisposableResult, DisposableSuccess, DisposableFail, DisposableResult, QuickJSDeferredPromise, ModuleMemory, UnstableSymbol, DefaultIntrinsics, QuickJSIterator, ContextMemory, QuickJSContext, QuickJSRuntime, QuickJSEmscriptenModuleCallbacks, QuickJSModuleCallbacks, QuickJSWASMModule;
|
||||
var __defProp2, __export2, QTS_DEBUG, errors_exports, QuickJSUnwrapError, QuickJSWrongOwner, QuickJSUseAfterFree, QuickJSNotImplemented, QuickJSAsyncifyError, QuickJSAsyncifySuspended, QuickJSMemoryLeakDetected, QuickJSEmscriptenModuleError, QuickJSUnknownIntrinsic, QuickJSPromisePending, QuickJSEmptyGetOwnPropertyNames, AwaitYield, UsingDisposable, SymbolDispose, prototypeAsAny, Lifetime, StaticLifetime, WeakLifetime, Scope, AbstractDisposableResult, DisposableSuccess, DisposableFail, DisposableResult, QuickJSDeferredPromise, ModuleMemory, DefaultIntrinsics, QuickJSIterator, ContextMemory, QuickJSContext, QuickJSRuntime, QuickJSEmscriptenModuleCallbacks, QuickJSModuleCallbacks, QuickJSWASMModule;
|
||||
var init_chunk_JTKJZQYV = __esm({
|
||||
"registry/components/code/node_modules/quickjs-emscripten-core/dist/chunk-JTKJZQYV.mjs"() {
|
||||
init_dist();
|
||||
@@ -190,7 +195,7 @@ var init_chunk_JTKJZQYV = __esm({
|
||||
return this.dispose();
|
||||
}
|
||||
};
|
||||
SymbolDispose = Symbol.dispose ?? Symbol.for("Symbol.dispose");
|
||||
SymbolDispose = Symbol.dispose ?? /* @__PURE__ */ Symbol.for("Symbol.dispose");
|
||||
prototypeAsAny = UsingDisposable.prototype;
|
||||
prototypeAsAny[SymbolDispose] || (prototypeAsAny[SymbolDispose] = function() {
|
||||
return this.dispose();
|
||||
@@ -409,7 +414,6 @@ Lifetime used`) : new QuickJSUseAfterFree("Lifetime not alive");
|
||||
return this.module._free(ptr), str;
|
||||
}
|
||||
};
|
||||
UnstableSymbol = Symbol("Unstable");
|
||||
DefaultIntrinsics = Object.freeze({ BaseObjects: true, Date: true, Eval: true, StringNormalize: true, RegExp: true, JSON: true, Proxy: true, MapSet: true, TypedArrays: true, Promise: true });
|
||||
QuickJSIterator = class extends UsingDisposable {
|
||||
constructor(handle, context) {
|
||||
@@ -773,7 +777,7 @@ ${cause.stack}Host: ${hostStack}`), Object.assign(exception, rest), exception;
|
||||
}
|
||||
return result.value;
|
||||
}
|
||||
[Symbol.for("nodejs.util.inspect.custom")]() {
|
||||
[/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
|
||||
return this.alive ? `${this.constructor.name} { ctx: ${this.ctx.value} rt: ${this.rt.value} }` : `${this.constructor.name} { disposed }`;
|
||||
}
|
||||
getFunction(fn_id) {
|
||||
@@ -909,7 +913,7 @@ ${cause.stack}Host: ${hostStack}`), Object.assign(exception, rest), exception;
|
||||
debugLog(...msg) {
|
||||
this._debugMode && console.log("quickjs-emscripten:", ...msg);
|
||||
}
|
||||
[Symbol.for("nodejs.util.inspect.custom")]() {
|
||||
[/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
|
||||
return this.alive ? `${this.constructor.name} { rt: ${this.rt.value} }` : `${this.constructor.name} { disposed }`;
|
||||
}
|
||||
getSystemContext() {
|
||||
@@ -1339,10 +1343,10 @@ async function QuickJSRaw(moduleArg = {}) {
|
||||
x ? (0 === h && (h = ra()), g[m] = x(e[m])) : g[m] = e[m];
|
||||
}
|
||||
b = a(...g);
|
||||
return b = function(k) {
|
||||
return b = (function(k) {
|
||||
0 !== h && sa(h);
|
||||
return "string" === d ? R(k) : "boolean" === d ? !!k : k;
|
||||
}(b);
|
||||
})(b);
|
||||
};
|
||||
c.wasmMemory ? r = c.wasmMemory : r = new WebAssembly.Memory({ initial: (c.INITIAL_MEMORY || 16777216) / 65536, maximum: 32768 });
|
||||
K();
|
||||
@@ -1464,7 +1468,7 @@ async function QuickJSRaw(moduleArg = {}) {
|
||||
}, t: function(a, d) {
|
||||
c.callbacks.freeHostRef(void 0, a, d);
|
||||
} }, Z;
|
||||
Z = await async function() {
|
||||
Z = await (async function() {
|
||||
function a(b) {
|
||||
b = Z = b.exports;
|
||||
c._malloc = b.v;
|
||||
@@ -1551,7 +1555,7 @@ async function QuickJSRaw(moduleArg = {}) {
|
||||
});
|
||||
M ??= c.locateFile ? c.locateFile ? c.locateFile("emscripten-module.wasm", u) : u + "emscripten-module.wasm" : new URL("emscripten-module.wasm", import.meta.url).href;
|
||||
return a((await ea(d)).instance);
|
||||
}();
|
||||
})();
|
||||
(function() {
|
||||
function a() {
|
||||
c.calledRun = true;
|
||||
@@ -3034,7 +3038,7 @@ var Hono = class _Hono {
|
||||
var emptyParam = [];
|
||||
function match(method, path) {
|
||||
const matchers = this.buildAllMatchers();
|
||||
const match2 = (method2, path2) => {
|
||||
const match2 = ((method2, path2) => {
|
||||
const matcher = matchers[method2] || matchers[METHOD_NAME_ALL];
|
||||
const staticMatch = matcher[2][path2];
|
||||
if (staticMatch) {
|
||||
@@ -3046,7 +3050,7 @@ function match(method, path) {
|
||||
}
|
||||
const index = match3.indexOf("", 1);
|
||||
return [matcher[1][index], match3];
|
||||
};
|
||||
});
|
||||
this.match = match2;
|
||||
return match2(method, path);
|
||||
}
|
||||
@@ -4008,7 +4012,7 @@ app.post("/", async (c) => {
|
||||
);
|
||||
}
|
||||
});
|
||||
var code_default = app;
|
||||
var index_default = app;
|
||||
export {
|
||||
code_default as default
|
||||
index_default as default
|
||||
};
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __esm = (fn, res) => function __init() {
|
||||
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
||||
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)
|
||||
@@ -1501,7 +1506,7 @@ var init_hono_base = __esm({
|
||||
// cypher-executor/node_modules/.pnpm/hono@4.12.10/node_modules/hono/dist/router/reg-exp-router/matcher.js
|
||||
function match(method, path) {
|
||||
const matchers = this.buildAllMatchers();
|
||||
const match2 = (method2, path2) => {
|
||||
const match2 = ((method2, path2) => {
|
||||
const matcher = matchers[method2] || matchers[METHOD_NAME_ALL];
|
||||
const staticMatch = matcher[2][path2];
|
||||
if (staticMatch) {
|
||||
@@ -1513,7 +1518,7 @@ function match(method, path) {
|
||||
}
|
||||
const index = match3.indexOf("", 1);
|
||||
return [matcher[1][index], match3];
|
||||
};
|
||||
});
|
||||
this.match = match2;
|
||||
return match2(method, path);
|
||||
}
|
||||
@@ -7433,7 +7438,7 @@ var init_lib = __esm({
|
||||
...processCreateParams(params)
|
||||
});
|
||||
};
|
||||
BRAND = Symbol("zod_brand");
|
||||
BRAND = /* @__PURE__ */ Symbol("zod_brand");
|
||||
ZodBranded = class extends ZodType {
|
||||
_parse(input) {
|
||||
const { ctx } = this._processInputParams(input);
|
||||
@@ -7607,14 +7612,14 @@ var init_lib = __esm({
|
||||
onumber = () => numberType().optional();
|
||||
oboolean = () => booleanType().optional();
|
||||
coerce = {
|
||||
string: (arg) => ZodString.create({ ...arg, coerce: true }),
|
||||
number: (arg) => ZodNumber.create({ ...arg, coerce: true }),
|
||||
boolean: (arg) => ZodBoolean.create({
|
||||
string: ((arg) => ZodString.create({ ...arg, coerce: true })),
|
||||
number: ((arg) => ZodNumber.create({ ...arg, coerce: true })),
|
||||
boolean: ((arg) => ZodBoolean.create({
|
||||
...arg,
|
||||
coerce: true
|
||||
}),
|
||||
bigint: (arg) => ZodBigInt.create({ ...arg, coerce: true }),
|
||||
date: (arg) => ZodDate.create({ ...arg, coerce: true })
|
||||
})),
|
||||
bigint: ((arg) => ZodBigInt.create({ ...arg, coerce: true })),
|
||||
date: ((arg) => ZodDate.create({ ...arg, coerce: true }))
|
||||
};
|
||||
NEVER = INVALID;
|
||||
z = /* @__PURE__ */ Object.freeze({
|
||||
@@ -7835,6 +7840,7 @@ var init_recipe_loader = __esm({
|
||||
super(message);
|
||||
this.recipe = recipe;
|
||||
}
|
||||
recipe;
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -8841,7 +8847,7 @@ function recordRecipeStats(env, recipeKeys, ok, at, ctx) {
|
||||
)
|
||||
).then(() => void 0);
|
||||
if (ctx?.waitUntil) ctx.waitUntil(promise);
|
||||
else ;
|
||||
else void promise;
|
||||
}
|
||||
function generateToken() {
|
||||
const tokenBytes = crypto.getRandomValues(new Uint8Array(16));
|
||||
@@ -8883,7 +8889,7 @@ async function executeWebhookGraph(env, graph, triggerContext, token, apiKey, ct
|
||||
result.trace
|
||||
);
|
||||
if (ctx?.waitUntil) ctx.waitUntil(statsPromise);
|
||||
else ;
|
||||
else void statsPromise;
|
||||
}
|
||||
return { success: true, data: result.data, duration_ms };
|
||||
} catch (err) {
|
||||
@@ -8907,7 +8913,7 @@ async function executeWebhookGraph(env, graph, triggerContext, token, apiKey, ct
|
||||
err.trace
|
||||
);
|
||||
if (ctx?.waitUntil) ctx.waitUntil(statsPromise);
|
||||
else ;
|
||||
else void statsPromise;
|
||||
}
|
||||
if (err instanceof ExecutionError) {
|
||||
const traceFormatted = err.trace.map((s) => ({
|
||||
@@ -9374,6 +9380,7 @@ function extractTarget(input) {
|
||||
return typeof raw2 === "string" ? raw2 : JSON.stringify(raw2);
|
||||
}
|
||||
async function writeExecutionVerdict(env, workflowId, nodes, verdict, durationMs, message, input, apiKey) {
|
||||
void nodes;
|
||||
try {
|
||||
const { base, headers } = kbdbBase(env);
|
||||
await fetch(`${base}/execution-log/record`, {
|
||||
@@ -15654,10 +15661,10 @@ app.route("/", consoleAuthRouter);
|
||||
app.route("/", consoleDashboardRouter);
|
||||
app.route("/", portalRouter);
|
||||
app.route("/", portalDataRouter);
|
||||
var src_default = {
|
||||
var index_default = {
|
||||
fetch: app.fetch,
|
||||
scheduled: handleScheduled
|
||||
};
|
||||
export {
|
||||
src_default as default
|
||||
index_default as default
|
||||
};
|
||||
|
||||
@@ -1427,7 +1427,7 @@ var Hono = class _Hono {
|
||||
var emptyParam = [];
|
||||
function match(method, path) {
|
||||
const matchers = this.buildAllMatchers();
|
||||
const match2 = (method2, path2) => {
|
||||
const match2 = ((method2, path2) => {
|
||||
const matcher = matchers[method2] || matchers[METHOD_NAME_ALL];
|
||||
const staticMatch = matcher[2][path2];
|
||||
if (staticMatch) {
|
||||
@@ -1439,7 +1439,7 @@ function match(method, path) {
|
||||
}
|
||||
const index = match3.indexOf("", 1);
|
||||
return [matcher[1][index], match3];
|
||||
};
|
||||
});
|
||||
this.match = match2;
|
||||
return match2(method, path);
|
||||
}
|
||||
@@ -2522,7 +2522,7 @@ app.post("/", async (c) => {
|
||||
);
|
||||
}
|
||||
});
|
||||
var src_default = app;
|
||||
var index_default = app;
|
||||
async function runWasm(input) {
|
||||
const hostFunctions = {
|
||||
http_request: async (url, method, headersJson, body) => {
|
||||
@@ -2568,5 +2568,5 @@ async function runWasm(input) {
|
||||
return JSON.parse(stdout);
|
||||
}
|
||||
export {
|
||||
src_default as default
|
||||
index_default as default
|
||||
};
|
||||
|
||||
@@ -1444,7 +1444,7 @@ var Hono = class _Hono {
|
||||
var emptyParam = [];
|
||||
function match(method, path) {
|
||||
const matchers = this.buildAllMatchers();
|
||||
const match2 = (method2, path2) => {
|
||||
const match2 = ((method2, path2) => {
|
||||
const matcher = matchers[method2] || matchers[METHOD_NAME_ALL];
|
||||
const staticMatch = matcher[2][path2];
|
||||
if (staticMatch) {
|
||||
@@ -1456,7 +1456,7 @@ function match(method, path) {
|
||||
}
|
||||
const index = match3.indexOf("", 1);
|
||||
return [matcher[1][index], match3];
|
||||
};
|
||||
});
|
||||
this.match = match2;
|
||||
return match2(method, path);
|
||||
}
|
||||
@@ -4157,7 +4157,7 @@ app.route("/recipe-stats", recipeStatRoutes);
|
||||
app.route("/execution-log", executionLogRoutes);
|
||||
app.route("/embed", embedRoutes);
|
||||
app.route("/map", mapRoutes);
|
||||
var src_default = app;
|
||||
var index_default = app;
|
||||
export {
|
||||
src_default as default
|
||||
index_default as default
|
||||
};
|
||||
|
||||
@@ -5,7 +5,11 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __commonJS = (cb, mod) => function __require() {
|
||||
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
||||
try {
|
||||
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
||||
} catch (e) {
|
||||
throw mod = 0, e;
|
||||
}
|
||||
};
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
@@ -8223,7 +8227,7 @@ var Hono = class _Hono {
|
||||
var emptyParam = [];
|
||||
function match(method, path) {
|
||||
const matchers = this.buildAllMatchers();
|
||||
const match2 = (method2, path2) => {
|
||||
const match2 = ((method2, path2) => {
|
||||
const matcher = matchers[method2] || matchers[METHOD_NAME_ALL];
|
||||
const staticMatch = matcher[2][path2];
|
||||
if (staticMatch) {
|
||||
@@ -8235,7 +8239,7 @@ function match(method, path) {
|
||||
}
|
||||
const index = match3.indexOf("", 1);
|
||||
return [matcher[1][index], match3];
|
||||
};
|
||||
});
|
||||
this.match = match2;
|
||||
return match2(method, path);
|
||||
}
|
||||
@@ -12959,7 +12963,7 @@ ZodNaN.create = (params) => {
|
||||
...processCreateParams(params)
|
||||
});
|
||||
};
|
||||
var BRAND = Symbol("zod_brand");
|
||||
var BRAND = /* @__PURE__ */ Symbol("zod_brand");
|
||||
var ZodBranded = class extends ZodType {
|
||||
_parse(input) {
|
||||
const { ctx } = this._processInputParams(input);
|
||||
@@ -13161,14 +13165,14 @@ var ostring = () => stringType().optional();
|
||||
var onumber = () => numberType().optional();
|
||||
var oboolean = () => booleanType().optional();
|
||||
var coerce = {
|
||||
string: (arg) => ZodString.create({ ...arg, coerce: true }),
|
||||
number: (arg) => ZodNumber.create({ ...arg, coerce: true }),
|
||||
boolean: (arg) => ZodBoolean.create({
|
||||
string: ((arg) => ZodString.create({ ...arg, coerce: true })),
|
||||
number: ((arg) => ZodNumber.create({ ...arg, coerce: true })),
|
||||
boolean: ((arg) => ZodBoolean.create({
|
||||
...arg,
|
||||
coerce: true
|
||||
}),
|
||||
bigint: (arg) => ZodBigInt.create({ ...arg, coerce: true }),
|
||||
date: (arg) => ZodDate.create({ ...arg, coerce: true })
|
||||
})),
|
||||
bigint: ((arg) => ZodBigInt.create({ ...arg, coerce: true })),
|
||||
date: ((arg) => ZodDate.create({ ...arg, coerce: true }))
|
||||
};
|
||||
var NEVER = INVALID;
|
||||
|
||||
@@ -13219,7 +13223,6 @@ function $constructor(name, initializer3, params) {
|
||||
Object.defineProperty(_, "name", { value: name });
|
||||
return _;
|
||||
}
|
||||
var $brand = Symbol("zod_brand");
|
||||
var $ZodAsyncError = class extends Error {
|
||||
constructor() {
|
||||
super(`Encountered Promise during synchronous parse. Use .parseAsync() instead.`);
|
||||
@@ -15723,8 +15726,6 @@ function en_default2() {
|
||||
}
|
||||
|
||||
// mcp/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v4/core/registries.js
|
||||
var $output = Symbol("ZodOutput");
|
||||
var $input = Symbol("ZodInput");
|
||||
var $ZodRegistry = class {
|
||||
constructor() {
|
||||
this._map = /* @__PURE__ */ new Map();
|
||||
@@ -17002,10 +17003,10 @@ var ZodMiniType = /* @__PURE__ */ $constructor("ZodMiniType", (inst, def) => {
|
||||
};
|
||||
inst.clone = (_def, params) => clone(inst, _def, params);
|
||||
inst.brand = () => inst;
|
||||
inst.register = (reg, meta) => {
|
||||
inst.register = ((reg, meta) => {
|
||||
reg.add(inst, meta);
|
||||
return inst;
|
||||
};
|
||||
});
|
||||
});
|
||||
var ZodMiniObject = /* @__PURE__ */ $constructor("ZodMiniObject", (inst, def) => {
|
||||
$ZodObject.init(inst, def);
|
||||
@@ -17268,10 +17269,10 @@ var ZodType2 = /* @__PURE__ */ $constructor("ZodType", (inst, def) => {
|
||||
};
|
||||
inst.clone = (def2, params) => clone(inst, def2, params);
|
||||
inst.brand = () => inst;
|
||||
inst.register = (reg, meta) => {
|
||||
inst.register = ((reg, meta) => {
|
||||
reg.add(inst, meta);
|
||||
return inst;
|
||||
};
|
||||
});
|
||||
inst.parse = (data, params) => parse2(inst, data, params, { callee: inst.parse });
|
||||
inst.safeParse = (data, params) => safeParse3(inst, data, params);
|
||||
inst.parseAsync = async (data, params) => parseAsync2(inst, data, params, { callee: inst.parseAsync });
|
||||
@@ -19243,11 +19244,13 @@ function assertCompleteRequestPrompt(request) {
|
||||
if (request.params.ref.type !== "ref/prompt") {
|
||||
throw new TypeError(`Expected CompleteRequestPrompt, but got ${request.params.ref.type}`);
|
||||
}
|
||||
void request;
|
||||
}
|
||||
function assertCompleteRequestResourceTemplate(request) {
|
||||
if (request.params.ref.type !== "ref/resource") {
|
||||
throw new TypeError(`Expected CompleteRequestResourceTemplate, but got ${request.params.ref.type}`);
|
||||
}
|
||||
void request;
|
||||
}
|
||||
var CompleteResultSchema = ResultSchema.extend({
|
||||
completion: looseObject({
|
||||
@@ -19400,7 +19403,7 @@ function isTerminal(status) {
|
||||
}
|
||||
|
||||
// mcp/node_modules/.pnpm/zod-to-json-schema@3.25.2_zod@3.25.76/node_modules/zod-to-json-schema/dist/esm/Options.js
|
||||
var ignoreOverride = Symbol("Let zodToJsonSchema decide on which parser to use");
|
||||
var ignoreOverride = /* @__PURE__ */ Symbol("Let zodToJsonSchema decide on which parser to use");
|
||||
var defaultOptions = {
|
||||
name: void 0,
|
||||
$refStrategy: "root",
|
||||
@@ -22376,7 +22379,7 @@ var Server = class extends Protocol {
|
||||
};
|
||||
|
||||
// mcp/node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/server/completable.js
|
||||
var COMPLETABLE_SYMBOL = Symbol.for("mcp.completable");
|
||||
var COMPLETABLE_SYMBOL = /* @__PURE__ */ Symbol.for("mcp.completable");
|
||||
function isCompletable(schema4) {
|
||||
return !!schema4 && typeof schema4 === "object" && COMPLETABLE_SYMBOL in schema4;
|
||||
}
|
||||
@@ -24691,13 +24694,13 @@ function registerAllIntrospectionTools(server, env) {
|
||||
}
|
||||
|
||||
// mcp/node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/browser/dist/nodes/identity.js
|
||||
var ALIAS = Symbol.for("yaml.alias");
|
||||
var DOC = Symbol.for("yaml.document");
|
||||
var MAP = Symbol.for("yaml.map");
|
||||
var PAIR = Symbol.for("yaml.pair");
|
||||
var SCALAR = Symbol.for("yaml.scalar");
|
||||
var SEQ = Symbol.for("yaml.seq");
|
||||
var NODE_TYPE = Symbol.for("yaml.node.type");
|
||||
var ALIAS = /* @__PURE__ */ Symbol.for("yaml.alias");
|
||||
var DOC = /* @__PURE__ */ Symbol.for("yaml.document");
|
||||
var MAP = /* @__PURE__ */ Symbol.for("yaml.map");
|
||||
var PAIR = /* @__PURE__ */ Symbol.for("yaml.pair");
|
||||
var SCALAR = /* @__PURE__ */ Symbol.for("yaml.scalar");
|
||||
var SEQ = /* @__PURE__ */ Symbol.for("yaml.seq");
|
||||
var NODE_TYPE = /* @__PURE__ */ Symbol.for("yaml.node.type");
|
||||
var isAlias = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === ALIAS;
|
||||
var isDocument = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === DOC;
|
||||
var isMap = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === MAP;
|
||||
@@ -24727,9 +24730,9 @@ function isNode(node) {
|
||||
var hasAnchor = (node) => (isScalar(node) || isCollection(node)) && !!node.anchor;
|
||||
|
||||
// mcp/node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/browser/dist/visit.js
|
||||
var BREAK = Symbol("break visit");
|
||||
var SKIP = Symbol("skip children");
|
||||
var REMOVE = Symbol("remove node");
|
||||
var BREAK = /* @__PURE__ */ Symbol("break visit");
|
||||
var SKIP = /* @__PURE__ */ Symbol("skip children");
|
||||
var REMOVE = /* @__PURE__ */ Symbol("remove node");
|
||||
function visit(node, visitor) {
|
||||
const visitor_ = initVisitor(visitor);
|
||||
if (isDocument(node)) {
|
||||
@@ -29322,9 +29325,9 @@ ${end.comment}` : end.comment;
|
||||
};
|
||||
|
||||
// mcp/node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/browser/dist/parse/cst-visit.js
|
||||
var BREAK2 = Symbol("break visit");
|
||||
var SKIP2 = Symbol("skip children");
|
||||
var REMOVE2 = Symbol("remove item");
|
||||
var BREAK2 = /* @__PURE__ */ Symbol("break visit");
|
||||
var SKIP2 = /* @__PURE__ */ Symbol("skip children");
|
||||
var REMOVE2 = /* @__PURE__ */ Symbol("remove item");
|
||||
function visit2(cst, visitor) {
|
||||
if ("type" in cst && cst.type === "document")
|
||||
cst = { start: cst.start, value: cst.value };
|
||||
@@ -33596,7 +33599,7 @@ app.post("/", partnerAuthMiddleware, async (c) => {
|
||||
const identity = resolveKnowledgeIdentity(c.get("auth_path"), c.get("portal"));
|
||||
return handleMcpRequest(c.req.raw, c.env, orgNamespace, partnerToken, identity);
|
||||
});
|
||||
var src_default = _app;
|
||||
var index_default = _app;
|
||||
export {
|
||||
src_default as default
|
||||
index_default as default
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"built_for": "arcrun-tier2-worker-artifacts",
|
||||
"generated_at": "2026-08-12T16:16:33.433Z",
|
||||
"repo_head": "b223a698844be289c1b01f99eb34a8e2ac85bb74",
|
||||
"generated_at": "2026-08-12T16:25:19.210Z",
|
||||
"repo_head": "2fcae722e7d7eab3c8b39744b87ae1344d23ba2a",
|
||||
"repo_dirty": false,
|
||||
"workers": [
|
||||
{
|
||||
@@ -11,8 +11,8 @@
|
||||
"source_commit": "b223a698844be289c1b01f99eb34a8e2ac85bb74",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-cypher-executor/worker.mjs",
|
||||
"js_bytes": 588397,
|
||||
"content_sha256": "e0026a23792f8b6b02e35c91081604b9a761c608a2cae0e6ee8ab0f34a484501",
|
||||
"js_bytes": 588587,
|
||||
"content_sha256": "c43728d21251f7835497d7dc40a3e702526a70b33406b617ed64126ffb16e1e0",
|
||||
"modules": [],
|
||||
"compat_date": "2025-02-19",
|
||||
"compat_flags": [
|
||||
@@ -61,8 +61,8 @@
|
||||
"source_commit": "f87d0e92f49690253e7c89c5badc82a08eb5d21b",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-kbdb/worker.mjs",
|
||||
"js_bytes": 149791,
|
||||
"content_sha256": "9c6d41895d78cbf3048fb539690591c86071b7d6ebf7fb0b8b51607c6b1f8ee9",
|
||||
"js_bytes": 149797,
|
||||
"content_sha256": "8b23853cbc88aee0ca15ef20ca46e92bd8e75064cd311af2847f4d51811960b1",
|
||||
"modules": [],
|
||||
"compat_date": "2025-02-19",
|
||||
"compat_flags": [
|
||||
@@ -90,8 +90,8 @@
|
||||
"source_commit": "1e85dfb49b0e8d81c0854781d93ee4e6a300c7b3",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-http-request/worker.mjs",
|
||||
"js_bytes": 80073,
|
||||
"content_sha256": "9a9dcb71879a7bdfd9fec1bd94eb9742e12cb63733d822ce63eeb1be30008d15",
|
||||
"js_bytes": 80079,
|
||||
"content_sha256": "cdd97364f277587cbade69e09bb40812c68f26a1e8bc9aa632c65b1b962b0b85",
|
||||
"modules": [
|
||||
{
|
||||
"name": "component.wasm",
|
||||
@@ -122,8 +122,8 @@
|
||||
"source_commit": "621cb8d948d61be6202063fd02effb3f538437fe",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-code/worker.mjs",
|
||||
"js_bytes": 153671,
|
||||
"content_sha256": "285a7406ec694ae47dccfaf48517f712c74d207a1689dffa15c39f1555b45be5",
|
||||
"js_bytes": 153758,
|
||||
"content_sha256": "751634a3fc9a99cc2da662026818d754c48f031d10bff3b3be2d3a8ee2311bd6",
|
||||
"modules": [
|
||||
{
|
||||
"name": "quickjs.wasm",
|
||||
@@ -151,8 +151,8 @@
|
||||
"source_commit": "10d150ac2b4385af95a457f3c411430c4a146cf9",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-mcp/worker.mjs",
|
||||
"js_bytes": 1179229,
|
||||
"content_sha256": "3ebc0d441bc04ae56a1205507da701f93bed9efa9b1e53c1777c04cbef5bdb67",
|
||||
"js_bytes": 1179487,
|
||||
"content_sha256": "1cd4c4d079d72bf7cba7c490ba6a88476f70b3ea51af7e5c93f9a184ae3c0ce6",
|
||||
"modules": [],
|
||||
"compat_date": "2024-11-27",
|
||||
"compat_flags": [
|
||||
|
||||
+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": {
|
||||
|
||||
+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;
|
||||
}
|
||||
|
||||
@@ -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} | ||||