Compare commits

...

12 Commits

Author SHA1 Message Date
uncle6me-web 6fa68c73d5 移除誤入版控的 cypher-executor/node_modules 自指 symlink
.gitignore 第 1 行本來就有 node_modules/,但 139d4c5 把它 commit 進去了,
且內容是**指向自己的 symlink**(node_modules -> .../cypher-executor/node_modules)。

害處(今天實撞兩次):任何人 checkout 或 rm -rf node_modules/<子目錄> 後,
pnpm install 會炸 ELOOP: too many symbolic links,
且 vitest 起不來(exit 194、零輸出,看不出原因)。
解法是 rm node_modules 再 pnpm install——但下次 checkout 又會回來。

⇒ 從版控移除,讓 .gitignore 真正生效。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 15:14:10 +08:00
uncle6me-web 60f481938d t181 修正:/portal/daemon/extract 認證改用 X-Arcrun-API-Key(帳密行不通)
【我自己的設計錯誤】前一版用帳密認證,但查證 daemon 實際行為後發現行不通:
**密碼只在連線精靈當下用過就丟、不落地**(config.json 沒有密碼欄,刻意的安全設計,
wiki 記為「密碼零落地」),而背景萃取是每輪自動跑的 ⇒ 根本拿不到密碼。

【改法】沿用 daemon 送卡片上雲時本來就帶的 `X-Arcrun-API-Key`(=namespace,
collector/direct.go:355)⇒ 同一把憑證、同一個身份模型,不必為此新增任何儲存。
不符即 401(租戶隔離)。

測試四則全過(沒帶 key 401/key 錯 401/缺參數 400/
**回歸守衛:錯誤訊息不得出現 gemini_api_key 或 credential**)。
全檔 38 passed,唯一 failed 與 tsc 的 1190 行 'auto' 皆為基準線既有、與本次無關。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 14:37:16 +08:00
uncle6me-web d728071a6a t181:新增 POST /portal/daemon/extract——daemon 萃取走 Workers AI(免金鑰)
【leo 08-04 列為最優先】「daemon 的 AI 改用 workers AI」
——「這是我的用戶**最大障礙**,造成首輪測試用戶的**好評或惡評**」。

【舊路徑的三種災難(實測)】要用戶自備 Gemini API Key ⇒
① 完全不知道去哪設定(台大資工碩士都卡住 ⇒ leo:「一般人就完蛋了」)
② 金鑰所屬 Google 帳號被 flag → 403 PERMISSION_DENIED,換專案也無效、申訴有死結
③ 52 檔全滅,還要把金鑰傳給總管實打才查得出真因

【修法】新端點收「已在本機轉成純文字的原稿」,用 env.AI binding 萃卡回傳
⇒ **完全不需要任何金鑰**,用的是用戶自己 CF 帳號內建的 AI,
他的 Google 帳號被封也不受影響。認證沿用 daemon/config 那把(帳密)。

為什麼放雲端:daemon 端沒有 AI binding(binding 是 Worker 專屬),
且模型選型集中在雲端才能統一換。
⚠️ 隱私邊界不變:送上來的是已轉文字的原稿、回傳知識卡,
原始檔案(docx/pdf)仍不出用戶電腦。

提示詞與 daemon 端 gemmaPrompt 同一份契約(第一行必須是「# <頁名>」),
兩邊要一起改。缺 AI binding 時回 501 並指名缺什麼(禁假綠)。

測試 4 則全過(含**回歸守衛:錯誤訊息不得出現 gemini_api_key/credential**
——若有人把它改回打 Google 會立刻紅);全檔 38 passed,
唯一失敗是基準線既有的 GET /portal 靜態資源案,與本次無關。
tsc 對 portal.ts 零錯誤。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 14:10:00 +08:00
uncle6me-web 41d63b9712 t176 套到 CIS 版 portal:刪 AI 設定區塊(修我把視覺打回舊版的錯)
【我犯的錯】08-03 出貨時用 `main` 建 UI bundle,但 CIS 新視覺在本分支
(fix/cis-round3-portal),main 上沒有 ⇒ **把 portal 打回 CIS 之前的樣子**。
leo 截圖坐實:新 AI 文案在(t176 生效)但 CIS 全不見(底色/字體/logo 都退回舊版)。

【為什麼會用錯】wiki 記「portal 真身=matrix/arcrun main」——那是 08-01 記的,
而 CIS 是 08-02 才進本分支 ⇒ **記錄過時**。且我沒照規矩「抓線上內容反查真身」驗證。

【本 commit】把 t176 的改動(移除 AI 設定區塊+其前端邏輯)套到 CIS 版上,
使兩者同時成立:
  CIS 特徵:--paper-a #FDFCFB ×8、IBM Plex Sans ×10、apple-touch-icon ×1
  t176:「這裡不需要任何設定」×1、st-ai-use-claude ×0
前端 JS 語法檢查通過(4 個 script 區塊 1344 行,node --check 綠)。

重建 UI bundle 後檔數 6 → **9**(多了 favicon.ico/favicon.svg/apple-touch-icon.png)
=先前那份 bundle 確實漏了 CIS 資產的機械證據。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 08:38:02 +08:00
uncle6me-web e570714472 portal 設定頁加版本卡:顯示目前版本+落後紅點+一鍵更新(帶 email,t154 免辨識碼)
leo 08-02:「不顯示的話用戶不知道要不要更新」「發現落後就按一下開啟 install
直接帶它的 email 和辨識碼」。
比法=自己的 bundle_version(cypher /health)vs 最新版(安裝器 /api/latest),
semver 逐段數字比(避免 1.4.10 < 1.4.9 的字串比錯誤);
舊格式版本(日期+sha)一律判為落後,舊實例才會被正確提示更新。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 19:23:12 +08:00
uncle6me-web 1e89be1ea0 portal 字形對齊 landing(9 處 Songti 明體→無襯線)+側邊欄 logo 縮小並讓出左右空白
leo 08-01:「RAG, Install 都沒有襯線,但這裡的字形帶襯線,要複製那裡的 Style」
「logo 再小一點,因為在側邊欄顯得很大,讓出左右的空白」
2026-08-01 18:54:35 +08:00
uncle6me-web bb023a12fb portal 底色對齊 landing:--paper-a #F2F1ED→#FDFCFB(Paper)、--paper-b/-bar-bg #ECEAE4→#F2F1ED(Canvas)
leo 08-01:「換了 logo 和部分配色,但整個 style 不同,如果可以修就簡單修」
差異根因:portal 用 Canvas 當主表面色、還自訂了不在 CIS 裡的 #ECEAE4,
整個背景比 landing 暗一階 ⇒ 看起來偏暖褐。改兩個變數即對齊。
其餘 9 種非 CIS 色為狀態色(成功綠/錯誤紅)與深色模式暗底,landing 同款,不動。
2026-08-01 18:18:06 +08:00
uncle6me-web a78cbbce64 portal CIS 收尾:側邊欄壞 SVG(字腔缺失)換 2x 官方圖+.logo svg→img CSS 修正+全站 logo 加 responsive clamp 2026-08-01 16:31:07 +08:00
uncle6me-web 47d90c9feb fix: portal 登入頁 lockup 換裁淨版 PNG,字太小問題修正
同 rag/install 問題:官方 PNG 畫布 1840x560 只有 32% 高度是實際字形,
height:44px 時實際字高僅約 14px。改用裁淨版(493x88,長寬比 5.604:1,
四邊已裁到字腔邊緣),height 保持 44px 不變,但現在等於實際字高。

兩處 <div class="brand"> 各兩張圖(wm-ink + wm-paper)全部換裝。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 16:02:30 +08:00
uncle6me-web a542eb5b5b CIS 緊急修正:wordmark 字腔缺失,改用官方 PNG(portal)
同 landing/install 根因(自產 SVG outline compound path,a/u/n 字腔洞
未畫進去)與修法:兩處 .brand 內嵌 svg(登入頁×2)改用官方
arcrun-cis/arcrun-lockup-h-ink.png + arcrun-lockup-h-paper-on-ink.png
as base64 data URI。本頁主題靠 data-theme 屬性切換(非 media query),
:root[data-theme="dark"] 時顯示 -paper-on-ink,預設(無屬性=light)
顯示 -ink。favicon(另外的 favicon.svg/.ico)未動。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 15:39:10 +08:00
uncle6me-web f078eba34a CIS 復盤修正 t3b:favicon chevron 筆畫加粗,修 weight mismatch
同 t1b/t2b(landing/install)根因與修法:CHEV_STROKE 46→109,重產三件套
(favicon.svg/favicon.ico 16·32·48/apple-touch-icon 180×180),三站現在
是同一份位元組。本地驗證(512px 畫布中線段寬):a 字身 78px/
chevron 85px/chevron 85px,與官方 mark-square-ink.svg 基準(85px)吻合。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 15:08:41 +08:00
uncle6me-web 8dfed921dd CIS 第三輪 t3:portal 換裝 CIS 色票 + 真向量 wordmark + favicon 三件套
leo 08-01 親驗「Portal 裡的色彩都沒改還是跟原來一樣」——實測前 CIS 色票 0
命中、favicon 0(連宣告都沒有)、<svg>=2(皆非 CIS,是既有 graph 視覺化)。

改動(console-ui/public/portal/index.html,僅視覺層,未動
cypher-executor/ 或任何後端邏輯):
- :root 色板整套換裝:--paper-a/b(舊宣紙米白)→ Canvas #F2F1ED/
  --ink(舊墨字)→ Ink #17181A/--amber(舊琥珀金)→ Relation #B04A2F
  (dark 模式對應 Relation-dark #D9784F)。
- 6 處直接寫死的舊 hex(#241804/#e8b45a/#e57373/#c0392b/#b4462f/#b98330)
  一併清零,統一走 CSS 變數或既有 err token。
- 登入頁/首次設定頁的 .brand、側邊欄 .logo:原本是 Songti 襯線純文字
  「Arcrun」+ var(--amber) 上色(違反「mark 永遠單色」+不該用 Relation
  當 logo 色)→ 換成真向量 WORDMARK_SVG(同 t1/t2 那份 IBM Plex Sans
  SemiBold outline + chevron compound path),單色 var(--ink)。
- 新增 favicon.svg(a 加雙 chevron,Ink 底 Paper 挖空)/favicon.ico
  (16/32/48)/apple-touch-icon.png(180×180)到 console-ui/public/
  根目錄,<head> 補三個 <link> 宣告(原本連宣告都沒有)。

本地 Chrome headless 截圖驗證:淺色/深色模式登入頁、側邊欄 logo 三張截圖,
wordmark 與按鈕色階層正確(登入按鈕 Relation 底 + Paper 字)。

已知未盡(誠實列出,留給下一輪):
- var(--amber) 在原設計裡被當「次要強調色」大量使用(連結色/標題色/
  hover 態,39 處),超出 CIS「≤5% 螢幕」的精神——這次只換色票本身
  沒收斂用法,需要更大範圍的互動色階層重新設計,故未動。
- console/index.html、console/dashboard/index.html 同族問題(舊宣紙+
  琥珀配色、無 favicon)未套用,任務允許但為避免半套改動造成視覺不
  一致,留待下一輪明確處理。

分支從 main 開(fix/cis-round3-portal),未動目前 checkout 的
fix/kbdb-search-deprecated-t24(那份是 7/24 舊版)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 14:57:28 +08:00
7 changed files with 262 additions and 123 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" role="img" aria-label="arcrun icon"><title>arcrun icon</title><rect width="1024" height="1024" fill="#17181A"/><path fill="#FDFCFB" fill-rule="nonzero" d="M463.01,612.91 L436.06,612.91 L436.06,485.41 L435.86,477.78 L435.27,470.46 L434.28,463.44 L432.89,456.73 L431.11,450.31 L428.93,444.20 L426.36,438.39 L423.39,432.88 L420.02,427.68 L416.26,422.78 L412.10,418.18 L407.55,413.88 L402.62,409.91 L397.31,406.28 L391.65,402.99 L385.61,400.06 L379.21,397.47 L372.44,395.22 L365.30,393.32 L357.79,391.76 L349.92,390.55 L341.68,389.69 L333.08,389.17 L324.10,389.00 L317.39,389.10 L310.90,389.40 L304.63,389.89 L298.59,390.58 L292.77,391.47 L287.17,392.56 L281.80,393.85 L276.65,395.33 L271.72,397.02 L267.02,398.90 L262.53,400.98 L258.28,403.25 L254.20,405.69 L250.26,408.26 L246.45,410.95 L242.78,413.76 L239.25,416.71 L235.86,419.77 L232.60,422.97 L229.48,426.29 L226.50,429.74 L223.65,433.31 L220.94,437.01 L218.37,440.83 L257.76,476.08 L259.43,473.77 L261.16,471.52 L262.96,469.32 L264.81,467.18 L266.73,465.09 L268.71,463.05 L270.75,461.07 L272.85,459.15 L275.01,457.27 L277.23,455.45 L279.51,453.69 L281.86,451.98 L284.30,450.36 L286.86,448.89 L289.55,447.55 L292.37,446.36 L295.31,445.31 L298.38,444.39 L301.58,443.62 L304.90,442.99 L308.34,442.50 L311.91,442.15 L315.61,441.94 L319.44,441.87 L323.74,441.95 L327.85,442.21 L331.75,442.65 L335.45,443.25 L338.95,444.03 L342.24,444.98 L345.34,446.10 L348.23,447.40 L350.93,448.87 L353.42,450.51 L355.71,452.32 L357.79,454.31 L359.70,456.45 L361.44,458.74 L363.01,461.18 L364.42,463.75 L365.66,466.47 L366.73,469.34 L367.64,472.35 L368.39,475.50 L368.97,478.80 L369.38,482.24 L369.63,485.82 L369.71,489.55 L369.71,509.25 L323.58,509.25 L314.52,509.39 L305.80,509.82 L297.44,510.53 L289.43,511.52 L281.78,512.80 L274.47,514.37 L267.52,516.22 L260.93,518.35 L254.68,520.77 L248.79,523.47 L243.25,526.45 L238.06,529.72 L233.26,533.28 L228.88,537.14 L224.91,541.30 L221.36,545.76 L218.23,550.52 L215.52,555.57 L213.22,560.93 L211.34,566.58 L209.88,572.53 L208.84,578.78 L208.21,585.33 L208.00,592.18 L208.15,598.13 L208.62,603.87 L209.39,609.41 L210.48,614.75 L211.87,619.89 L213.57,624.83 L215.58,629.57 L217.91,634.11 L220.54,638.44 L223.48,642.57 L226.73,646.50 L230.29,650.23 L234.14,653.71 L238.25,656.88 L242.63,659.75 L247.28,662.32 L252.19,664.59 L257.37,666.56 L262.82,668.22 L268.53,669.58 L274.51,670.64 L280.75,671.40 L287.26,671.85 L294.04,672.00 L299.07,671.91 L303.96,671.63 L308.72,671.17 L313.33,670.53 L317.81,669.71 L322.16,668.70 L326.37,667.50 L330.44,666.13 L334.37,664.57 L338.17,662.82 L341.83,660.89 L345.35,658.78 L348.71,656.49 L351.88,654.01 L354.85,651.35 L357.62,648.50 L360.20,645.47 L362.59,642.26 L364.78,638.87 L366.78,635.29 L368.58,631.52 L370.19,627.58 L371.60,623.45 L372.82,619.13 L375.93,619.13 L376.52,622.61 L377.24,625.98 L378.09,629.22 L379.07,632.35 L380.19,635.36 L381.44,638.24 L382.83,641.01 L384.34,643.67 L385.99,646.20 L387.78,648.61 L389.69,650.91 L391.74,653.08 L393.92,655.11 L396.23,656.96 L398.66,658.64 L401.22,660.14 L403.90,661.46 L406.71,662.61 L409.64,663.58 L412.71,664.37 L415.89,664.99 L419.21,665.43 L422.65,665.69 L426.21,665.78 L463.01,665.78 L463.01,612.91 Z M475.77,630.42 L546.23,713.58 L762.31,530.50 L546.23,347.42 L475.77,430.58 L593.69,530.50 L475.77,630.42 Z M667.77,630.42 L738.23,713.58 L954.31,530.50 L738.23,347.42 L667.77,430.58 L785.69,530.50 L667.77,630.42 Z"/></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
/Users/youlinhsieh/Documents/tech_projects/InkStoneCo/matrix/arcrun/cypher-executor/node_modules
+68
View File
@@ -715,6 +715,74 @@ function toPublicLibrary(rec: PortalRecord) {
// t52leo 2026-07-26:「用戶可以看到我有 2 個庫,地端雲端都是 2 個,如果只有一個一定被罵」):
// 小幫手回報它看守的資料夾各自對應的庫,雲端**自動登記**——庫目錄與地端資料夾一比一。
// 認證=同 /portal/daemon/config(用戶帳密)。已存在的庫略過(冪等),不覆寫顯示名。
// POST /portal/daemon/extract — 小幫手把「已轉成純文字的原稿」送上來,雲端用 Workers AI 萃成知識卡。
// body {email, password, page_name, text}。認證同 /portal/daemon/config(帳密)。
//
// 🔴 t181leo 08-04:「daemon 的 AI 改用 workers AI」,列為**最優先**——
// 「這是我的用戶最大障礙,造成首輪測試用戶的好評或惡評」):
// 舊路徑要用戶自己去 Google 申請 Gemini API Key,實測撞到三種災難:
// ① 完全不知道要去哪裡設定(台大資工碩士都卡住 ⇒ leo:「一般人就完蛋了」)
// ② 拿到的金鑰所屬 Google 帳號被 flag ⇒ 403 PERMISSION_DENIED、換專案也無效
// ③ 52 檔全滅還要把金鑰傳給別人實打才查得出真因
// ⇒ 走 Workers AI`env.AI` binding**完全不需要任何金鑰**,
// 用的是用戶自己 CF 帳號內建的 AI;他的 Google 帳號被封也不受影響。
//
// 為什麼萃取放雲端而不是 daemon 直接打:daemon 端**沒有 AI binding**
//binding 是 Worker 專屬),且模型選型集中在雲端才能統一換。
// ⚠️ 隱私邊界不變:daemon 送的是**已在本機轉成文字的原稿**,回傳的是知識卡;
// 原始檔案(docx/pdf)仍然不出用戶的電腦。
// 🔑 認證用 `X-Arcrun-API-Key`(=namespace),**不是帳密**
// daemon 的密碼**只在連線精靈當下用過就丟、不落地**(config.json 沒有密碼欄,刻意的安全設計),
// 但背景萃取是每輪自動跑的 ⇒ 根本拿不到密碼。
// 而 daemon 送卡片上雲時本來就帶這個 headercollector/direct.go:355),沿用同一把最自然。
portalRouter.post('/portal/daemon/extract', (c) =>
run(c, async () => {
const apiKey = (c.req.header('X-Arcrun-API-Key') ?? '').trim();
if (!apiKey) return c.json({ error: '缺少 X-Arcrun-API-Key header' }, 401);
if (apiKey !== portalTenant(c.env)) return c.json({ error: 'X-Arcrun-API-Key 不正確' }, 401);
const body = (await c.req.json().catch(() => null)) as
| { page_name?: string; text?: string }
| null;
const pageName = String(body?.page_name ?? '').trim();
const srcText = String(body?.text ?? '');
if (!pageName || !srcText.trim()) return c.json({ error: 'page_name 與 text 必填' }, 400);
if (!c.env.AI) {
// 誠實失敗:不假裝成功,並指名這個部署缺什麼(禁假綠)
return c.json({ error: '這個部署沒有綁定 Workers AIwrangler.toml 需有 [ai] binding),請更新知識庫版本' }, 501);
}
// 提示詞與 daemon 端 gemmaPrompt 同一份契約(第一行必須是「# <頁名>」),
// 兩邊要一起改;daemon 端在 collector/extract_gemma.go。
// 註:關聯段用的是「知識卡三元組」格式(主詞/謂詞/受詞),與 Arcrun 工作流的邊無關。
const REL = '>'.repeat(2);
const prompt =
`把以下原稿重寫成定稿知識卡(正體中文)。直接輸出卡片本身:第一行必須是「# ${pageName}」,` +
`不要任何前言、思考過程、英文草稿或說明。格式:\n# ${pageName}\n## 一句話定義\n(一行)\n` +
`## 要點\n- (3-12 條,具體、含數字條件)\n## 關鍵實體\n- **實體名** — 一句說明\n` +
`## 關聯\n- 實體A ${REL} 關係 ${REL} 實體B3-8 行,用上面實體名)\n\n原稿:\n${srcText}`;
try {
// 模型與 workers_ai_chat recipe 同一支(選型實測見 api-recipe-seeds.ts:140
// llama-4-scout 2373ms/答案最完整;對照 Gemini gemma-4-31b-it 16.87 秒且吐英文草稿)。
const out = (await c.env.AI.run('@cf/meta/llama-4-scout-17b-16e-instruct', {
messages: [{ role: 'user', content: prompt }],
max_tokens: 2048,
temperature: 0.2,
} as never)) as { response?: string } | undefined;
const card = String(out?.response ?? '').trim();
if (!card) return c.json({ error: 'Workers AI 沒有回傳內容' }, 502);
// 淨化:模型偶爾在卡片前多帶一段前言 ⇒ 取最後一個「# <頁名>」起(同 daemon cleanGemmaCard
const marker = `# ${pageName}`;
const idx = card.lastIndexOf(marker);
return c.json({ success: true, card: (idx >= 0 ? card.slice(idx) : card).trim() + '\n' });
} catch (e) {
return c.json({ error: `Workers AI 執行失敗:${e instanceof Error ? e.message : String(e)}` }, 502);
}
}),
);
portalRouter.post('/portal/daemon/libraries', (c) =>
run(c, async () => {
const body = (await c.req.json().catch(() => null)) as
@@ -811,3 +811,43 @@ describe('/portal/admin/ai + /portal/daemon/report-capabilitiest131', () =
await env.WEBHOOKS.delete(ragChatKey);
});
});
// ═══════════════ t181:daemon 萃取走 Workers AI(免金鑰)═══════════════
//
// leo 08-04 列為最優先:「daemon 的 AI 改用 workers AI」——
// 「這是我的用戶最大障礙,造成首輪測試用戶的好評或惡評」。
// 舊路徑要用戶自備 Gemini key,實測撞到「不知道去哪設定」「Google 帳號被 flag 403」
// 「52 檔全滅還要把金鑰傳給別人才查得出原因」三種災難。
describe('POST /portal/daemon/extractt181Workers AI 萃卡,免金鑰)', () => {
// 認證=X-Arcrun-API-Key(=namespacewrangler.test.toml CONSOLE_TENANT=leo),
// **不是帳密**:daemon 密碼不落地(連線精靈用完即丟),背景萃取拿不到密碼。
const KEY = { 'X-Arcrun-API-Key': 'leo' };
it('沒帶 API Key → 401', async () => {
const res = await json('POST', '/portal/daemon/extract', { page_name: 'x', text: 'y' });
expect(res.status).toBe(401);
});
it('API Key 錯 → 401(租戶隔離)', async () => {
const res = await json('POST', '/portal/daemon/extract',
{ page_name: 'x', text: 'y' }, { 'X-Arcrun-API-Key': 'someone-else' });
expect(res.status).toBe(401);
});
it('缺 page_name 或 text → 400(不打 AI、不假裝成功)', async () => {
const res = await json('POST', '/portal/daemon/extract', {}, KEY);
expect(res.status).toBe(400);
const d = (await res.json()) as { error?: string };
expect(String(d.error)).toContain('page_name');
});
// 🔴 回歸守衛:這條路**不得**要求任何 Gemini/API 金鑰——免金鑰正是它存在的理由。
// 若哪天有人把它改回打 Google,錯誤訊息會出現 credential/gemini_api_key ⇒ 這則會紅。
it('錯誤訊息不得要求任何金鑰(免金鑰是本端點存在的理由)', async () => {
const res = await json('POST', '/portal/daemon/extract', {}, KEY);
const raw = await res.text();
expect(raw).not.toContain('gemini_api_key');
expect(raw).not.toContain('credential');
});
});