Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a24f2912eb | |||
| 1791ffa497 | |||
| 296fb01247 | |||
| d58a6e152d |
@@ -2650,7 +2650,7 @@ var init_recipes = __esm({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// cypher-executor/src/lib/constants.ts
|
// cypher-executor/src/lib/constants.ts
|
||||||
var VALID_EDGE_TYPES, SEMANTIC_EDGE_MAP, BUILTIN_COMPONENTS;
|
var VALID_EDGE_TYPES, SEMANTIC_EDGE_MAP, WAIT_MAX_MS, BUILTIN_COMPONENTS;
|
||||||
var init_constants3 = __esm({
|
var init_constants3 = __esm({
|
||||||
"cypher-executor/src/lib/constants.ts"() {
|
"cypher-executor/src/lib/constants.ts"() {
|
||||||
"use strict";
|
"use strict";
|
||||||
@@ -2698,6 +2698,7 @@ var init_constants3 = __esm({
|
|||||||
"CLICK": "ON_CLICK",
|
"CLICK": "ON_CLICK",
|
||||||
"SUBFLOW": "CALLS_SUBFLOW"
|
"SUBFLOW": "CALLS_SUBFLOW"
|
||||||
};
|
};
|
||||||
|
WAIT_MAX_MS = 3e4;
|
||||||
BUILTIN_COMPONENTS = /* @__PURE__ */ new Map([
|
BUILTIN_COMPONENTS = /* @__PURE__ */ new Map([
|
||||||
["comp_passthrough", (ctx) => ctx],
|
["comp_passthrough", (ctx) => ctx],
|
||||||
["comp_uppercase", (ctx) => {
|
["comp_uppercase", (ctx) => {
|
||||||
@@ -2707,6 +2708,54 @@ var init_constants3 = __esm({
|
|||||||
["comp_counter", (ctx) => {
|
["comp_counter", (ctx) => {
|
||||||
const c = ctx;
|
const c = ctx;
|
||||||
return { ...c, count: (Number(c.count) || 0) + 1 };
|
return { ...c, count: (Number(c.count) || 0) + 1 };
|
||||||
|
}],
|
||||||
|
// ── wait:等待 N 毫秒後繼續(Arcrun#101,2026-08-12)────────────────────────
|
||||||
|
//
|
||||||
|
// 為什麼「等待」搬進引擎,而不是修那顆 WASM:
|
||||||
|
//
|
||||||
|
// 舊實作是 registry/components/wait/main.go(TinyGo → WASM),用 time.Sleep。
|
||||||
|
// TinyGo 的 sleep 走 WASI `poll_oneoff`;而每顆 component worker 的 WASI shim 把
|
||||||
|
// poll_oneoff 實作成 ENOSYS(`.component-builds/*/src/index.ts`:`poll_oneoff: () => 76`)
|
||||||
|
// ⇒ TinyGo 排程器拿不到「睡到某個時間」的手段,退化成迴圈重讀 `clock_time_get`
|
||||||
|
// 自旋等時間到(wasm 內可見 runtime.sleepTicks / sleepQueue / runtime.ticks 符號)。
|
||||||
|
//
|
||||||
|
// 🔴 到這裡為止是**查得到原始碼的事實**。再往下「所以那個自旋迴圈的結束條件永遠
|
||||||
|
// 不成立」曾被當成結論寫在這裡,但**寫了測試去證,反而被打臉**:在
|
||||||
|
// vitest-pool-workers 的 workerd 裡,同步自旋 2553 圈之後 Date.now() 就前進了
|
||||||
|
// ⇒ 時鐘並沒有全程凍結。
|
||||||
|
// ⇒ 「為什麼三秒的等待會拖到 35 秒才死」的完整機制**目前仍是推測**,
|
||||||
|
// 證據只有下面 leo 的四次實測。別把它當定論往外傳。
|
||||||
|
//
|
||||||
|
// 所以症狀不是「等 N 秒花 N 秒 CPU」,而是「不管 ms 填多少都跑到 CPU 上限被砍」。
|
||||||
|
// leo 2026-08-12 在 youlin stage 實測(只有 input >> wait 兩個節點):
|
||||||
|
// ms=3000 → 38.9s 後 503 / ms=20000 → 34.0s / ms=30000 → 34.9s / 寫死 3000 → 34.8s
|
||||||
|
// 四個值同一個死法、與 ms 無關 —— 3 秒的等待撐到 35 秒才死,就是「迴圈根本沒結束」
|
||||||
|
// 的證據(若成本與時長成正比,ms=3000 只會花 3 秒 CPU,根本不該死)。
|
||||||
|
// 也就是說 wait 零件在 Workers 上從來沒有真的等待成功過,不只是貴。
|
||||||
|
//
|
||||||
|
// 純 WASI 沙箱(stdin→stdout、無 socket、同步呼叫)本來就沒有「不花 CPU 地等」這種
|
||||||
|
// 東西 —— 會等的只有宿主。故 wait 與 trigger_workflow 同類:**是 orchestrator 的
|
||||||
|
// 執行排程職責,不是業務邏輯**(rule 02 §2.3 明列「workflow 執行排程」屬 cypher-executor
|
||||||
|
// 合法職責;§2.2 禁的是解密/簽章/template 展開/具體 API 呼叫,等待都不是)。
|
||||||
|
// 搬進引擎不違反「業務邏輯走 WASM」鐵律。引擎這側 await 一個 timer 只花 wall-clock、
|
||||||
|
// 不記 CPU ⇒ 等 30 秒與等 3 秒同價(皆 ≈0)。
|
||||||
|
//
|
||||||
|
// I/O 契約沿用 component.contract.yaml,既有 workflow 的 wait 節點定義不必改:
|
||||||
|
// 吃 ms(必填 > 0)+可選 context;ms > WAIT_MAX_MS 截斷;
|
||||||
|
// 回 { success: true, data: { ...context, waited_ms } };ms <= 0 回 success:false。
|
||||||
|
// 唯一刻意的放寬:ms 允許數字字串("3000")。WASM 版 json.Unmarshal 進 int 會直接
|
||||||
|
// 失敗,但 node.data 走 interpolateData 後 `ms: "{{input.delay}}"` 必然是字串
|
||||||
|
// ⇒ 收字串只會把「本來就跑不動的」變成跑得動,不會改變任何既有成功案例的行為。
|
||||||
|
["wait", async (ctx) => {
|
||||||
|
const c = ctx && typeof ctx === "object" ? ctx : {};
|
||||||
|
const requested = typeof c.ms === "number" ? c.ms : Number(c.ms);
|
||||||
|
if (!Number.isFinite(requested) || requested <= 0) {
|
||||||
|
return { success: false, error: "ms \u5FC5\u9808\u5927\u65BC 0" };
|
||||||
|
}
|
||||||
|
const ms = Math.min(Math.floor(requested), WAIT_MAX_MS);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
const passthrough = c.context && typeof c.context === "object" && !Array.isArray(c.context) ? c.context : {};
|
||||||
|
return { success: true, data: { ...passthrough, waited_ms: ms } };
|
||||||
}]
|
}]
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -3060,7 +3109,12 @@ var init_component_loader = __esm({
|
|||||||
filter: "SVC_FILTER",
|
filter: "SVC_FILTER",
|
||||||
merge: "SVC_MERGE",
|
merge: "SVC_MERGE",
|
||||||
try_catch: "SVC_TRY_CATCH",
|
try_catch: "SVC_TRY_CATCH",
|
||||||
wait: "SVC_WAIT",
|
// wait 已於 Arcrun#101(2026-08-12)移進 BUILTIN_COMPONENTS(step 1)——
|
||||||
|
// 等待是 orchestrator 的排程職責,WASI 沙箱裡做不到「不花 CPU 地等」。理由全文見
|
||||||
|
// constants.ts 的 wait 註解。這裡刻意**移除**而非留著:step 1 本來就先於 step 5 命中,
|
||||||
|
// 留下這行只會讓讀者以為 wait 還走 SVC_WAIT(實際永遠走不到)=誤導人的死路由。
|
||||||
|
// wrangler.toml 的 SVC_WAIT binding 不動(rule 3.1:13 個既有 binding 保留不新增),
|
||||||
|
// 拆綁定要重新部署、與本票無關。
|
||||||
set: "SVC_SET",
|
set: "SVC_SET",
|
||||||
array_ops: "SVC_ARRAY_OPS",
|
array_ops: "SVC_ARRAY_OPS",
|
||||||
string_ops: "SVC_STRING_OPS",
|
string_ops: "SVC_STRING_OPS",
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
{
|
{
|
||||||
"schema": 1,
|
"schema": 1,
|
||||||
"built_for": "arcrun-tier2-worker-artifacts",
|
"built_for": "arcrun-tier2-worker-artifacts",
|
||||||
"generated_at": "2026-08-12T05:36:26.216Z",
|
"generated_at": "2026-08-12T07:29:58.626Z",
|
||||||
"repo_head": "cbeddf753537fbf836e20b7efce5b47b50f30d06",
|
"repo_head": "1791ffa4972b4135dacd4208e805f67b747479c4",
|
||||||
"repo_dirty": false,
|
"repo_dirty": false,
|
||||||
"workers": [
|
"workers": [
|
||||||
{
|
{
|
||||||
"name": "arcrun-cypher-executor",
|
"name": "arcrun-cypher-executor",
|
||||||
"source_dir": "cypher-executor",
|
"source_dir": "cypher-executor",
|
||||||
"source_commit": "a5e4caf5cb38c376f892708d8a46ad96a9cc23cc",
|
"source_commit": "f1370e2275eea62b64a88821a096f2c2cfe76fb0",
|
||||||
"main_module": "worker.mjs",
|
"main_module": "worker.mjs",
|
||||||
"main_file": "arcrun-cypher-executor/worker.mjs",
|
"main_file": "arcrun-cypher-executor/worker.mjs",
|
||||||
"js_bytes": 572842,
|
"js_bytes": 577374,
|
||||||
"content_sha256": "d1765930ce157de07400dcff21d620a3b5549e0a66f0d6428cc6901631ba73b2",
|
"content_sha256": "8411ed59b7ad9e1a74ac0d8e3b620d7166e7d0178ac5939e6cc736f2e8d1d2be",
|
||||||
"modules": [],
|
"modules": [],
|
||||||
"compat_date": "2025-02-19",
|
"compat_date": "2025-02-19",
|
||||||
"compat_flags": [
|
"compat_flags": [
|
||||||
|
|||||||
@@ -1108,7 +1108,9 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// ── t53 完成安裝清單(進站必見,三件做完才消失)─────────────────────────────
|
// ── t53 完成安裝清單(進站必見,做完才消失)───────────────────────────────
|
||||||
|
// 件數演進:t53 三件 → t54 兩件(設定檔改由小幫手憑帳密自取)
|
||||||
|
// → arcrun-rag#81 一件(AI 問答改走 Workers AI,不再要用戶自備金鑰)。
|
||||||
function setupSteps() {
|
function setupSteps() {
|
||||||
try { return JSON.parse(localStorage.getItem('arcrun_setup_steps') || '{}'); } catch (e) { return {}; }
|
try { return JSON.parse(localStorage.getItem('arcrun_setup_steps') || '{}'); } catch (e) { return {}; }
|
||||||
}
|
}
|
||||||
@@ -1123,7 +1125,10 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
|
|||||||
var p = S.profile || {};
|
var p = S.profile || {};
|
||||||
if (p.role !== 'admin') return;
|
if (p.role !== 'admin') return;
|
||||||
var s = setupSteps();
|
var s = setupSteps();
|
||||||
if (s.daemon && s.key) return; // t54 起只剩兩件:設定改由小幫手輸入帳密自取,不再下載檔案
|
// arcrun-rag#81(leo 08-12:「已經改用 workers AI,刪掉」):只剩「下載小幫手」一件。
|
||||||
|
// 舊的 s.key(貼 Google AI 金鑰)已整條移除,見下方 innerHTML 處的說明。
|
||||||
|
// 相容:舊瀏覽器 localStorage 裡殘留的 s.key 不再被讀 —— 沒設過 key 的人也不會被卡住。
|
||||||
|
if (s.daemon) return; // t54 起設定改由小幫手輸入帳密自取,不再下載檔案
|
||||||
var cfg = window.ARCRUN_CONFIG || {};
|
var cfg = window.ARCRUN_CONFIG || {};
|
||||||
var dpick = daemonPick(); // t72 OS 分流(同一組判定,見上面 daemonPick)
|
var dpick = daemonPick(); // t72 OS 分流(同一組判定,見上面 daemonPick)
|
||||||
var daemonUrl = dpick.sure ? dpick.pick.url : dpick.mac.url;
|
var daemonUrl = dpick.sure ? dpick.pick.url : dpick.mac.url;
|
||||||
@@ -1134,7 +1139,20 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
|
|||||||
var el = document.createElement('div');
|
var el = document.createElement('div');
|
||||||
el.id = 'setup-checklist';
|
el.id = 'setup-checklist';
|
||||||
el.style.cssText = 'position:fixed;right:20px;bottom:20px;z-index:60;max-width:400px;width:calc(100% - 40px);padding:18px 20px;border-radius:14px;background:rgba(var(--amber-rgb),.10);border:1px solid rgba(var(--amber-rgb),.45);backdrop-filter:blur(8px);font-size:14px;line-height:1.65';
|
el.style.cssText = 'position:fixed;right:20px;bottom:20px;z-index:60;max-width:400px;width:calc(100% - 40px);padding:18px 20px;border-radius:14px;background:rgba(var(--amber-rgb),.10);border:1px solid rgba(var(--amber-rgb),.45);backdrop-filter:blur(8px);font-size:14px;line-height:1.65';
|
||||||
el.innerHTML = '<b>還差 ' + (2 - (s.daemon?1:0) - (s.key?1:0)) + ' 步,安裝就真的完成了</b>'
|
// 🔴 arcrun-rag#81(leo 08-12):第二項「啟用 AI 問答(貼 Google AI 金鑰)」整條刪除。
|
||||||
|
// 為什麼不是「一個沒用的欄位」而已:它長在**安裝完成清單裡而且是勾選項**
|
||||||
|
// ⇒ 用戶會以為不做這步就沒裝完,而它要人離開流程、去第三方網站申請帳號、
|
||||||
|
// 把金鑰貼進表單——整條安裝路徑上最重的一個動作,而且是白做的。
|
||||||
|
// 真相:雲端問答走 t181(08-04)改好的 Workers AI(`env.AI` binding,免金鑰)——
|
||||||
|
// /portal/data/chat → tenant 的 rag_chat workflow → `workers_ai_chat` recipe
|
||||||
|
// (api-recipe-seeds.ts:150,endpoint `@cf/meta/llama-4-scout-17b-16e-instruct`)。
|
||||||
|
// 這裡貼的金鑰是打 POST /portal/admin/ai 存一筆雲端 gemini_api_key credential,
|
||||||
|
// **現行問答鏈路一個地方都沒有讀它**。
|
||||||
|
// 後端 route 本身不動(同 08-09 拿掉檢修孔按鈕的處置:端點無害、已無任何 UI 呼叫,
|
||||||
|
// 純粹清路標);已經存過金鑰的人那筆 credential 也原封不動,不做刪除。
|
||||||
|
// ⚠️ 別把這個跟設定頁「AI 設定」面板講的地端萃取金鑰搞混——那把是填在
|
||||||
|
// **同步小幫手**托盤裡的,從來不經過這個清單。
|
||||||
|
el.innerHTML = '<b>還差 1 步,安裝就真的完成了</b>'
|
||||||
+ row(s.daemon, 'daemon',
|
+ row(s.daemon, 'daemon',
|
||||||
'<b>下載同步小幫手</b>(把資料夾變成知識庫)<br>'
|
'<b>下載同步小幫手</b>(把資料夾變成知識庫)<br>'
|
||||||
+ (daemonUrl
|
+ (daemonUrl
|
||||||
@@ -1156,34 +1174,13 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
|
|||||||
// t54(leo:「最好的就是把它的帳密直接輸入」):設定不再是一個要下載的檔案——
|
// t54(leo:「最好的就是把它的帳密直接輸入」):設定不再是一個要下載的檔案——
|
||||||
// 小幫手第一次開啟會問網址+帳密,自己去換設定。
|
// 小幫手第一次開啟會問網址+帳密,自己去換設定。
|
||||||
+ '<div class="muted" style="font-size:12.5px;margin-top:4px">裝好第一次開啟時,貼上這個網址+你的帳號密碼就連上了,不用下載設定檔。</div>')
|
+ '<div class="muted" style="font-size:12.5px;margin-top:4px">裝好第一次開啟時,貼上這個網址+你的帳號密碼就連上了,不用下載設定檔。</div>')
|
||||||
+ row(s.key, 'key',
|
|
||||||
'<b>啟用 AI 問答</b>(<a href="https://aistudio.google.com" target="_blank" rel="noopener">aistudio.google.com</a> 免費申請)<br>'
|
|
||||||
+ '<input id="sc-key" type="password" placeholder="貼上 Google AI 金鑰" style="width:60%;padding:6px 8px;border-radius:8px;border:1px solid rgba(var(--ink-rgb),.25);background:rgba(var(--ink-rgb),.04);color:var(--ink)"> '
|
|
||||||
+ '<button class="btn3" id="sc-key-save" style="padding:6px 12px;border-radius:8px;cursor:pointer">啟用</button>'
|
|
||||||
+ '<div id="sc-key-msg" style="font-size:12.5px;min-height:1.1em"></div>')
|
|
||||||
+ '<div style="margin-top:10px;text-align:right"><button id="sc-later" style="border:none;background:none;color:inherit;cursor:pointer;font-size:12.5px;text-decoration:underline;opacity:.65">稍後再說</button></div>';
|
+ '<div style="margin-top:10px;text-align:right"><button id="sc-later" style="border:none;background:none;color:inherit;cursor:pointer;font-size:12.5px;text-decoration:underline;opacity:.65">稍後再說</button></div>';
|
||||||
document.body.appendChild(el);
|
document.body.appendChild(el);
|
||||||
var dl = document.getElementById('sc-daemon');
|
var dl = document.getElementById('sc-daemon');
|
||||||
if (dl) dl.addEventListener('click', function () { markStep('daemon'); });
|
if (dl) dl.addEventListener('click', function () { markStep('daemon'); });
|
||||||
// t54:config.json 下載鈕已移除(設定由小幫手憑帳密自取)
|
// t54:config.json 下載鈕已移除(設定由小幫手憑帳密自取)
|
||||||
var kb = document.getElementById('sc-key-save');
|
// arcrun-rag#81:金鑰輸入框的送出邏輯(POST /portal/admin/ai)一併移除——
|
||||||
if (kb) kb.addEventListener('click', function () {
|
// 只藏畫面留著那條路,等於這個要求還在,只是變得更難發現。
|
||||||
var k = (document.getElementById('sc-key').value || '').trim();
|
|
||||||
var m = document.getElementById('sc-key-msg');
|
|
||||||
if (!k) { m.textContent = '請先貼上金鑰'; return; }
|
|
||||||
kb.disabled = true; m.textContent = '啟用中…';
|
|
||||||
fetch(API_BASE + '/portal/admin/ai', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: Object.assign({ 'Content-Type': 'application/json' }, authHeaders()),
|
|
||||||
body: JSON.stringify({ gemini_api_key: k })
|
|
||||||
}).then(function (r) { return safeJson(r).then(function (d) { return { ok: r.ok, status: r.status, d: d }; }); })
|
|
||||||
.then(function (x) {
|
|
||||||
kb.disabled = false;
|
|
||||||
if (!x.ok || !x.d.success) { m.textContent = (x.d && x.d.error) || '啟用失敗,請再試一次'; return; }
|
|
||||||
markStep('key');
|
|
||||||
})
|
|
||||||
.catch(function () { kb.disabled = false; m.textContent = '網路好像有問題,請再試一次'; });
|
|
||||||
});
|
|
||||||
var later = document.getElementById('sc-later');
|
var later = document.getElementById('sc-later');
|
||||||
if (later) later.addEventListener('click', function () { el.remove(); }); // 只藏本次,下次進站再提醒
|
if (later) later.addEventListener('click', function () { el.remove(); }); // 只藏本次,下次進站再提醒
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user