diff --git a/.gitignore b/.gitignore index e9da684..6391ecb 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,4 @@ backup-*.sql # GitHub 公開 mirror 工作目錄(publish-github.sh 產物) .github-public/ +wrangler.leo21c.toml diff --git a/console-ui/package.json b/console-ui/package.json new file mode 100644 index 0000000..a040af2 --- /dev/null +++ b/console-ui/package.json @@ -0,0 +1,11 @@ +{ + "name": "arcrun-console-ui", + "version": "0.1.0", + "private": true, + "description": "Arcrun Console / Portal 靜態前端(Cloudflare Pages)——從 cypher-executor 搬出的 UI 層", + "scripts": { + "build": "node scripts/build.mjs", + "deploy": "npm run build && wrangler pages deploy public --project-name arcrun-console-ui", + "preview": "npm run build && npx serve public" + } +} diff --git a/console-ui/public/config.js b/console-ui/public/config.js new file mode 100644 index 0000000..c23705f --- /dev/null +++ b/console-ui/public/config.js @@ -0,0 +1,2 @@ +// Arcrun UI runtime 組態——改這一行就能切 API 目標,不必重新 build。 +window.ARCRUN_CONFIG = { apiBase: "" }; diff --git a/console-ui/public/console/dashboard/index.html b/console-ui/public/console/dashboard/index.html new file mode 100644 index 0000000..ff5a800 --- /dev/null +++ b/console-ui/public/console/dashboard/index.html @@ -0,0 +1,295 @@ + + + + + +Arcrun 駕駛艙 + + + + + + +
+
+
Arcrun 駕駛艙
+
+
+ +
+
+
+
+
+
載入中
+
+
+
+
+
+
今日完成
+
/
+
+
+
+
收件匣未處理
+
+
來自 Telegram
+
+
+
+
等你的事
+
載入中…
+
+
+
今日路線
+ + + +
系統狀況live 健康信號
+
載入中…
+
每 60 秒自動刷新
+ 進入完整控制台 › +
+ + + diff --git a/cypher-executor/src/routes/console.ts b/console-ui/public/console/index.html similarity index 88% rename from cypher-executor/src/routes/console.ts rename to console-ui/public/console/index.html index bf12c5c..0c49dd7 100644 --- a/cypher-executor/src/routes/console.ts +++ b/console-ui/public/console/index.html @@ -1,91 +1,9 @@ -/** - * Mira Console 完整版(Arcrun#3 console 系,2026-07-04 總管派工) - * - * 視覺:claude design 定稿「紙感暖黑 2a」(system-dev/docs/6-user/Mira Console 設計規劃/ - * Mira Console.dc.html + Mira Style Guide.dc.html)——紙紋底 repeating-linear-gradient、 - * 明體標題(Songti TC 級聯)、琥珀 var(--amber) 強調、墨綠呼吸球嵌「安」字、正文 ≥16px、 - * 手機優先 390px + ≥1024px 側欄、頁面本體不出水平捲軸。 - * - * 資訊架構:brief 8 頁(docs/1-vision/mira-console-design-brief.md)= - * 1 登入/首次設定 2 駕駛艙 3 總庫搜尋 4 卡片詳頁+關聯 5 工作流 6 憑證管理 - * 7 分流台(原收件匣,Arcrun#9 改裝) 8 設定。 - * 單檔 HTML + 原生 JS hash routing(#/cockpit …),零外部資源、零 build step。 - * - * 薄殼(rule 07):零業務邏輯,只 fetch 既有 API 渲染。各頁資料源(誠實原則,無源的顯示 - * 誠實空狀態,禁假資料): - * - 駕駛艙:GET /console/dashboard-data(免登入聚合端點,console-dashboard.ts) - * - 總庫搜尋:GET /kbdb/search(mode=semantic 未開 Vectorize 會誠實降級 + capability_hint; - * chips 用 entry_type——KBDB 真能篩的欄位,不編造「電腦筆記/手機筆記」等不存在的分類); - * 頭部統計來自 GET /console/kb-scale-data(精耕層 wiki 卡/三元組/已嵌入——leo 2026-07-07 - * 裁:45.8 萬 14-E 搬遷遺產 deprecated 不再當招牌數字;搜尋本身仍搜全庫); - * 搜尋框上方=藏書地圖(library-map SDD M5/R4,Arcrun#39):GET /kbdb/map 全館庫卡片 - * (narrative+top entities+triplet 數+更新時間感),逐庫 GET /kbdb/map/:library 補 - * degree/跨庫 bridges/commit_hash(詳圖拉不到就維持名字版,不擋渲染);點庫卡片= - * 設 library filter(/kbdb/search 既有 library 參數)進該庫搜尋;/map 空陣列或錯誤= - * 誠實空狀態(指引 POST /map/recompute backfill),不擋搜尋等其他功能 - * - 卡片詳頁:GET /kbdb/entries/:id;關聯視圖走 GET /kbdb/graph/neighbors/:name - * (cypher 代轉 kbdb-graph-plugin,kbdb-proxy.ts;查無 triplet → 「尚無關聯資料」) - * - 工作流:GET /webhooks/named(list)+ POST /webhooks/named/:name/trigger(手動觸發) - * + GET /workflows/:name/executions(最近執行);釘選存 localStorage - * - 憑證:GET /credentials/catalog(D1 目錄唯讀,絕不含密文)+ POST /credentials(新增) - * + PUT /credentials/:name(整筆替換);刪除後端未接 D1(T9 範圍)→ 標「即將開通」 - * - 分流台:GET /console/triage-data(需 session——待辦/訊息原文屬機敏,計數才免登入)。 - * 資料源二合一(kb-ingest SDD R7 / Arcrun#9):entry_type=todo(Logseq 萃取)+ - * entry_type=inbox(Telegram);三欄 80/15/5(ai/collab/leo)+ per-project chips - * +已完成預設隱藏可切換。分欄判定在 lib/console-triage-model.ts(純函式),頁面只渲染。 - * 勾掉/還原走 POST /console/triage-check(同 session 保護;leo 2026-07-08 拍板)—— - * 雙向銷案語意:console 勾掉=終局(checked_via:console,萃取端絕不復活)、Logseq 改 - * DONE 由萃取端 PATCH(checked_via:logseq);console 只忠實顯示非 done 項。 - * - 設定:vectorize 狀態探測(讀 /kbdb/search 回應的 mode/capability_hint,無新端點; - * 開關本體需部署端 config kbdb_embed + acr update → 誠實文案講清楚、不假裝能遠端開) - * + MCP token TTL 誠實佔位(/console/settings-data;可調功能=Arcrun#19) - * + /console/setup/reset 換帳密 + 系統資訊(精耕層規模) - * - * 2026-07-07 fix/console-truth-audit(總管稽核派工 + leo 裁決): - * 1. 全站日期/時間顯示統一 Asia/Taipei(lib/taipei-time.ts,與駕駛艙 #36 台北日判定同套; - * 原本用瀏覽器本地 getter,換裝置就換日期;憑證頁加顯時分——7/6 22:57 建立不再被誤讀)。 - * 2. 總庫搜尋/設定頁頭部統計改精耕層(458,732 是真數字——cypher 與 base 一手同值實證—— - * 但 leo 裁 14-E 遺產 deprecated 不再顯示)。 - * 3. 駕駛艙「今日完成/今日路線」接 sprint 任務板真資料(dashboard-data 端,同 #36 模式)。 - * - * 認證模型沿 v1(console-auth.ts):單一管理員 email+password,session token 在 localStorage, - * 實際打 /kbdb/* 用後端回的固定租戶字串(CONSOLE_TENANT)。駕駛艙免登入版在 /console/dashboard。 - * - * v0 的 components/recipes 查詢區(Arcrun#3 原搜尋台)收進「工作流」頁下方折疊區,不砍功能。 - * - * 2026-07-04 leo 實測三回饋(總管派工二輪): - * 1. 深/淺主題:CSS custom properties 兩份色板(:root=淺色紙感宣紙米白+墨字, - * :root[data-theme=dark]=原定稿暖黑不動),預設淺色,選擇存 localStorage - * (key arcrun_console_theme),head 預載腳本防閃色。切換入口=登入/首次設定頁小鈕、 - * 側欄項、設定頁正式開關。金琥珀例外(按鈕漸層/狀態球/toast/gcenter)兩版不變。 - * 2. 登入/首次設定置中:.authwrap.view.on 補 display:flex(原被 .view.on 的 block 蓋掉 → 靠左)。 - * 3. fetch 韌性:friendlyErr() 把裸 Failed to fetch / Load failed 轉「連線中斷」誠實文案; - * 駕駛艙 60 秒定時器常駐,網路恢復自動刷回。 - */ -import { Hono } from 'hono'; -import type { Bindings } from '../types'; -import { TAIPEI_CLIENT_JS } from '../lib/taipei-time'; - -export const consoleRouter = new Hono<{ Bindings: Bindings }>(); - -function renderConsoleHtml(registryBase: string, brand: string, profile: string): string { - // Console profile(config 裁剪,非 auth——#24/#25 多人 Portal 是另一回事): - // full(未設即此)=全套,Mira 實例現行為一字不變。 - // rag=企業產品樣:只留 總庫搜尋/工作流/設定(card 是搜尋的詳頁,跟著留),落地=搜尋頁。 - // 隱藏頁的 view div 仍在 DOM(display:none、路由 guard 導回,永遠點不進)—— - // 因 creds/inbox 有 top-level addEventListener 綁定,抽掉 div 會讓整包 JS 啟動即炸, - // 純渲染層裁剪以「導不進去」為準,不為了抽 DOM 把 null-guard 灑滿全檔。 - const rag = profile === 'rag'; - const views = rag - ? ['search', 'card', 'workflows', 'settings'] - : ['cockpit', 'search', 'card', 'workflows', 'creds', 'inbox', 'settings']; - const home = rag ? 'search' : 'cockpit'; - return ` + -${brand} Console +Arcrun Console + @@ -308,7 +228,7 @@ function renderConsoleHtml(registryBase: string, brand: string, profile: string)
-
${brand}
+
Arcrun
私人系統・僅供擁有者進入
@@ -318,7 +238,7 @@ function renderConsoleHtml(registryBase: string, brand: string, profile: string)
這是一個人的智慧總部。
若你不是擁有者,這裡沒有你要找的東西。
- ${rag ? '' : '免登入看駕駛艙 ›'} + 免登入看駕駛艙 ›
@@ -327,7 +247,7 @@ function renderConsoleHtml(registryBase: string, brand: string, profile: string)
-
${brand}
+
Arcrun
首次設定
系統偵測到尚未設定擁有者帳號。
設定一組 Email 與密碼,之後只有你能進入。
@@ -345,12 +265,12 @@ function renderConsoleHtml(registryBase: string, brand: string, profile: string)
- - ${rag ? '' : ''} + + - ${rag ? '' : ` - `} + +
@@ -359,7 +279,7 @@ function renderConsoleHtml(registryBase: string, brand: string, profile: string)
-
${brand} 控制台
+
Arcrun 控制台
@@ -522,13 +442,13 @@ function renderConsoleHtml(registryBase: string, brand: string, profile: string)
- ${rag ? '' : `
+
憑證管理
只保存目錄與加密值,不可回看
-
`} +
系統資訊
載入中…
@@ -542,10 +462,10 @@ function renderConsoleHtml(registryBase: string, brand: string, profile: string)
- ${rag ? '' : '
駕駛艙
'} +
駕駛艙
搜尋
工作流
- ${rag ? '' : '
分流台
'} +
分流台
設定
@@ -553,11 +473,18 @@ function renderConsoleHtml(registryBase: string, brand: string, profile: string) -`; -} - -// GET /console — Mira Console 完整版(Arcrun#3 console 系)。registry base 現算:同帳號 -// arcrun-registry worker(WORKER_SUBDOMAIN 沿用既有 KBDB_BASE_URL 那套組法),沒設就退回官方公開 registry。 -consoleRouter.get('/console', (c) => { - const subdomain = c.env.WORKER_SUBDOMAIN; - const registryBase = subdomain - ? `https://arcrun-registry.${subdomain}.workers.dev` - : 'https://registry.arcrun.dev'; - // 品牌字樣(Arcrun#21):引擎預設 Arcrun,實例可用 CONSOLE_BRAND 覆蓋(如 "Arcrun RAG") - // 頁面組成(console-profile-trim):CONSOLE_PROFILE 未設=full 全套;"rag"=企業產品樣(搜尋落地) - return c.html(renderConsoleHtml(registryBase, c.env.CONSOLE_BRAND || 'Arcrun', c.env.CONSOLE_PROFILE || 'full')); -}); diff --git a/cypher-executor/src/routes/portal-ui.ts b/console-ui/public/portal/index.html similarity index 93% rename from cypher-executor/src/routes/portal-ui.ts rename to console-ui/public/portal/index.html index 74f7970..366f4e5 100644 --- a/cypher-executor/src/routes/portal-ui.ts +++ b/console-ui/public/portal/index.html @@ -1,44 +1,9 @@ -/** - * RAG Portal 前端殼 — P3:GET /portal 單檔 HTML(portal-auth design §1 D-1/§6,Gitea #24/#25) - * - * 形態(D-1):cypher-executor 的新路由、**獨立 HTML 殼**——不在 console 上加 if(「Admin - * Console 現狀不動」鐵律)。樣式重用 console 的設計語言(紙感/明體標題/琥珀強調/深淺主題), - * 取捨=**複製樣式子集而非抽共用模組**:抽共用要動 console.ts(違「console 不動」),且兩頁 - * 受眾不同(owner vs 同仁)預期各自演化;代價=樣式雙份、改版要兩邊同步(PR 說明誠實記)。 - * - * 頁面(design §6,leo 頁面級拍板): - * - 登入頁(brand=CONSOLE_BRAND;未登入只見這個殼) - * - 搜尋頁(落地頁):keyword/semantic/graph 三模式+結果含 source 溯源+卡片詳頁 - * - 設定頁:改自己密碼、看自己角色與可查庫、主題切換 - * - 工作流頁:D-8 定案 admin 可見(PORTAL_SHOW_WORKFLOWS 可調 all/off);唯讀、不開 trigger - * - graph 模式按 D-4 粗閘顯示(/portal/session 的 graph_allowed) - * - 管理頁(P4,admin-only nav):帳號管理(列表/新增/停用/重設密碼+每帳號勾選可查庫 - * 含 ["*"] 全庫)+庫目錄管理(登記/停用/graph_source 標記)。一次性密碼只在畫面顯示 - * 一次(server 不留明碼);一般用戶 nav 不顯示、直打 /portal/admin/* 也是 403(role 閘)。 - * - Mira 專屬頁(駕駛艙/分流台/憑證/專案)一律不進 - * - * 安全(design §3.3 關鍵差異 vs console): - * - 本頁 JS **沒有任何租戶字串、沒有 X-Arcrun-API-Key**——只持 portal session token - * (localStorage `arcrun_portal_session`),一切資料走 /portal/data/*(server-side enforce)。 - * - 前端的「藏」(graph 模式不顯示、工作流頁不顯示)只是 UX;真閘在 /portal/data/* 路由層 - * (403/404),curl 直打也繞不過(#48 guard 精神)。 - * - 所有動態內容經 esc() 跳脫(防 XSS,同 console 慣例);密碼欄位值不進 log、不進 URL。 - * - * 薄殼(rule 07):零業務邏輯,只 fetch /portal/* 端點渲染。 - */ -import { Hono } from 'hono'; -import type { Bindings } from '../types'; -import { TAIPEI_CLIENT_JS } from '../lib/taipei-time'; - -export const portalUiRouter = new Hono<{ Bindings: Bindings }>(); - -function renderPortalHtml(brand: string, sourceWebBase = ''): string { - return ` + -${brand} Portal +Arcrun Portal + @@ -205,7 +172,7 @@ function renderPortalHtml(brand: string, sourceWebBase = ''): string {
-
${brand}
+
Arcrun
知識入口・Portal
@@ -222,7 +189,7 @@ function renderPortalHtml(brand: string, sourceWebBase = ''): string {
- + @@ -400,9 +367,16 @@ function renderPortalHtml(brand: string, sourceWebBase = ''): string { -`; -} - -// GET /portal — Portal HTML 殼(無 auth:回的是純殼,資料全在 /portal/data/* session 閘後)。 -// 未 bootstrap 任何 portal_user 時登入必失敗(「尚未啟用」的誠實形態),不影響 console。 -portalUiRouter.get('/portal', (c) => { - const brand = c.env.CONSOLE_BRAND || 'Arcrun'; - return c.html(renderPortalHtml(brand, c.env.PORTAL_SOURCE_WEB_BASE || '')); -}); + \ No newline at end of file diff --git a/console-ui/scripts/build.mjs b/console-ui/scripts/build.mjs new file mode 100644 index 0000000..786bd77 --- /dev/null +++ b/console-ui/scripts/build.mjs @@ -0,0 +1,205 @@ +/** + * console-ui build — 把 cypher-executor 的三支 UI renderer 在「建置時」跑一次, + * 產出純靜態 HTML 到 public/,交給 Cloudflare Pages 託管。 + * + * 為什麼這樣做(cypher-ui-split 第一刀): + * 原本 console/portal/dashboard 的 HTML 由 cypher-executor Worker 在「每次請求時」 + * 用 template literal 組出來 → 5,240 行 UI 字串永遠躺在 Worker bundle 裡(748KB), + * 連 /health 這種什麼都不做的請求都要付 5-7ms CPU(免費層上限 10ms)。 + * UI 是靜態的(單檔 HTML+原生 JS、零外部資源),本來就該待在 Pages。 + * + * 保持原特性(leo 反覆強調簡化): + * - 零打包工具、零 npm 依賴:本檔只用 node 內建 fs/path,正則抽出 renderer 的 + * template literal 後求值。不引入 esbuild/vite/rollup。 + * - 產出仍是「單檔 HTML+原生 JS hash routing、零外部資源」。 + * + * 唯一的行為差異=API base: + * 原本 UI 與 API 同源,fetch 全用相對路徑('/kbdb/search')。搬上 Pages 後跨網域, + * 故注入 window.ARCRUN_API_BASE,並把 fetch 的相對路徑改成 API_BASE + path。 + * 見下方 rewriteFetchPaths()。 + */ +import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const ROOT = join(HERE, '..'); +const SRC = join(ROOT, '..', 'cypher-executor', 'src'); +const OUT = join(ROOT, 'public'); + +// ── 建置期組態(原本是 Worker 的 env var,現在是建置參數)──────────────── +// Pages 是靜態站,沒有 per-request env;品牌/profile 這類「一個部署一個值」的 +// 設定改在建置時決定(要換值=重跑 build 再部署,符合靜態站模型)。 +const CFG = { + brand: process.env.CONSOLE_BRAND || 'Arcrun', + profile: process.env.CONSOLE_PROFILE || 'full', + registryBase: process.env.REGISTRY_BASE || 'https://registry.arcrun.dev', + sourceWebBase: process.env.PORTAL_SOURCE_WEB_BASE || '', + // API base 走 runtime 注入(見 public/config.js),這裡只放預設值 + apiBase: process.env.ARCRUN_API_BASE || '', +}; + +/** + * 讀 TS 原始碼並取出整個 renderer 函式的**函式主體**(不只 template literal)。 + * + * 取整個 body 而非只取反引號區塊,是因為 renderer 在 return 之前會先算區域變數 + * (如 console.ts 的 rag/views/home 由 profile 推導)。只搬模板=把那段推導邏輯 + * 複製一份到本檔=雙份真相會漂移。連 body 一起求值 → 推導邏輯永遠只有一份。 + */ +function extractRendererBody(file, fnName) { + const code = readFileSync(join(SRC, file), 'utf8'); + const start = code.indexOf(`function ${fnName}(`); + if (start < 0) throw new Error(`找不到 ${fnName} in ${file}`); + const braceStart = code.indexOf('{', code.indexOf(')', start)); + if (braceStart < 0) throw new Error(`${fnName} 找不到函式主體`); + // 掃到配對的收尾大括號;需略過字串/template literal/註解裡的括號 + let i = braceStart + 1; + let depth = 1; + let mode = null; // null | "'" | '"' | '`' | 'line' | 'block' + let tplDepth = 0; + while (i < code.length && depth > 0) { + const ch = code[i]; + const nx = code[i + 1]; + if (mode === null) { + if (ch === '\\') { i += 2; continue; } + if (ch === '/' && nx === '/') { mode = 'line'; i += 2; continue; } + if (ch === '/' && nx === '*') { mode = 'block'; i += 2; continue; } + if (ch === "'" || ch === '"') { mode = ch; i++; continue; } + if (ch === '`') { mode = '`'; tplDepth = 0; i++; continue; } + if (ch === '{') depth++; + else if (ch === '}') depth--; + i++; + continue; + } + if (mode === 'line') { if (ch === '\n') mode = null; i++; continue; } + if (mode === 'block') { if (ch === '*' && nx === '/') { mode = null; i += 2; continue; } i++; continue; } + if (ch === '\\') { i += 2; continue; } + if (mode === '`') { + // template literal 內的 ${ … } 是真程式碼,其中的引號/括號要照常計數才不會誤判收尾 + if (ch === '$' && nx === '{') { tplDepth++; i += 2; continue; } + if (ch === '}' && tplDepth > 0) { tplDepth--; i++; continue; } + if (ch === '`' && tplDepth === 0) { mode = null; i++; continue; } + i++; + continue; + } + if (ch === mode) mode = null; + i++; + } + // 去掉 TS 的型別註記(本 body 只有 `const x: T =` 這種簡單形態) + return code.slice(braceStart + 1, i - 1).replace(/\bconst\s+(\w+):\s*[\w<>[\]|]+\s*=/g, 'const $1 ='); +} + +/** 取出 lib/taipei-time.ts 匯出的 TAIPEI_CLIENT_JS 字串常數(UI 內嵌的客戶端時間工具)。 */ +function extractTaipeiClientJs() { + const code = readFileSync(join(SRC, 'lib', 'taipei-time.ts'), 'utf8'); + // 形態=字串陣列 .join('\n')(見 lib/taipei-time.ts),直接求值該陣列表達式 + const m = code.match(/export const TAIPEI_CLIENT_JS\s*=\s*(\[[\s\S]*?\]\.join\('\\n'\));/); + if (!m) throw new Error('找不到 TAIPEI_CLIENT_JS'); + return new Function(`return ${m[1]};`)(); +} + +/** + * 求值 renderer 函式主體。用 new Function 而非 eval——只餵建置期組態, + * 輸入是本 repo 自己的原始碼(非使用者輸入),無注入面。 + */ +function render(body, vars) { + const names = Object.keys(vars); + const fn = new Function(...names, body); + return fn(...names.map((n) => vars[n])); +} + +/** + * 把 UI 內原生 JS 的相對路徑 fetch 改成打 API base。 + * + * 只改 `fetch('/...` 與 `fetch("/...`(開頭是單斜線=同源絕對路徑)這一種形態, + * 其餘(fetch(url, …) 這類變數形式)另由各檔的 url 組法在下面單獨處理。 + */ +function rewriteFetchPaths(html, file) { + // ① fetch('/xxx → fetch(API_BASE + '/xxx + let out = html.replace(/fetch\((['"])\/(?!\/)/g, 'fetch(API_BASE + $1/'); + // ② 變數式 fetch(url, ...):url 由上方 var url = '/kbdb/search?...' 組成 → + // 把這類「以單斜線開頭的路徑字面值指派」也補上 API_BASE + out = out.replace(/(\bvar\s+url\s*=\s*)(['"])\/(?!\/)/g, '$1API_BASE + $2/'); + // ③ portal 的 adminApi(method, path, body):path 由呼叫端傳字面值進來,①② + // 都掃不到(8 個呼叫點)。在 helper 內部補前綴=一處修好全部,不必改 8 個呼叫點。 + out = out.replace( + /(function adminApi\(method, path, body\) \{)/, + '$1\n path = API_BASE + path;' + ); + + // 防呆:搬完後不該再有「直接 fetch 同源相對路徑」的殘留。掃到就讓建置失敗, + // 免得漏網的呼叫點在 Pages 上打到 Pages 自己(404)才被發現。 + // 註:adminApi 的呼叫端仍是相對路徑字面值——那是對的,前綴由 helper 內部(③)加。 + const unprefixed = [...out.matchAll(/fetch\((['"])\/(?!\/)[^'"]*/g)].map((m) => m[0]); + if (unprefixed.length) { + throw new Error( + `${file}:有 ${unprefixed.length} 個相對路徑 fetch 沒被改寫成 API_BASE:\n ` + + [...new Set(unprefixed)].join('\n ') + ); + } + // adminApi 形態存在時,必須確認 helper 已被加上前綴(否則 8 個呼叫點全會打錯家) + if (/function adminApi\(method, path, body\)/.test(out) && !/path = API_BASE \+ path;/.test(out)) { + throw new Error(`${file}:偵測到 adminApi helper 但前綴注入失敗`); + } + return out; +} + +/** 在頁面 注入 config.js(runtime 決定 API base),並定義 API_BASE 供內嵌 JS 用。 */ +function injectApiBase(html) { + const snippet = ` +`; + const withCfg = html.replace('', `${snippet}\n`); + // 內嵌的 IIFE 裡宣告 API_BASE(各頁的主 - - - -
-
-
${brand} 駕駛艙
-
-
- -
-
-
-
-
-
載入中
-
-
-
-
-
-
今日完成
-
/
-
-
-
-
收件匣未處理
-
-
來自 Telegram
-
-
-
-
等你的事
-
載入中…
-
-
-
今日路線
-
  • 載入中…
- -
    -
    系統狀況live 健康信號
    -
    載入中…
    -
    每 60 秒自動刷新
    - 進入完整控制台 › -
    - - - -`; -} - -// GET /console/dashboard — 駕駛艙頁(無需登入;純渲染 dashboard-data,無互動、無說明文字) -// 品牌字樣(Arcrun#21):引擎預設 Arcrun,實例可用 CONSOLE_BRAND 覆蓋(如 "Arcrun RAG") -// CONSOLE_PROFILE=rag(console-profile-trim):駕駛艙不屬企業版頁面 → 302 回 /console。 -// 選 302 不選 404:舊書籤/外鏈直接落回產品頁,不給死路(只裁 UI 頁面,資料端點行為不動)。 -consoleDashboardRouter.get('/console/dashboard', (c) => { - if ((c.env.CONSOLE_PROFILE || 'full') === 'rag') return c.redirect('/console', 302); - return c.html(renderDashboardHtml(c.env.CONSOLE_BRAND || 'Arcrun')); -});