From 062757f1b15fb6181e61633ea41c3a2c01edcbbd Mon Sep 17 00:00:00 2001 From: uncle6me-web Date: Fri, 31 Jul 2026 14:07:05 +0800 Subject: [PATCH] =?UTF-8?q?t159=20=E4=BF=AE=20portal=20=E5=BA=AB=E7=9B=AE?= =?UTF-8?q?=E9=8C=84=E7=A9=BA=EF=BC=9A=E8=A3=9C=E4=B8=8A=20daemon=20?= =?UTF-8?q?=E4=B8=80=E7=9B=B4=E5=9C=A8=E6=89=93=E3=80=81=E4=BD=86=E5=BE=9E?= =?UTF-8?q?=E6=9C=AA=E5=AD=98=E5=9C=A8=E7=9A=84=E7=99=BB=E8=A8=98=E7=AB=AF?= =?UTF-8?q?=E9=BB=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 診斷更正(重要):kbdb 裡 50+ 筆 entry_type=value(content=資料夾名)**不是孤兒**—— 那是 91 筆 triplet records 的 library slot 正常構造(records 全部組裝完整,資料同步 一切正常,無失敗無重試)。真病灶=**POST /portal/daemon/libraries 這個 route 從來不存在**:daemon registerLibraries(arcrun-tray main.go:505,t52「用戶可以看到 我有 2 個庫」)連線精靈時打它 → 404 → 被 daemon「失敗不擋連線」設計靜默吞掉 → portal_library 登記簿永遠 0 筆 → portal「庫目錄管理」空。 - 新 route 契約照 daemon 既有呼叫:{email, password, libraries:[{name, display_name}]} - 帳密驗證沿用 /portal/session 同一套(isLocked/findUserRecordId/verifyPassword/ recordLoginFail;daemon 只在精靈那刻拿帳密不存) - 冪等:listRecordsByTemplate 比 name,已登記跳過(重跑精靈不堆重複) - 庫名走 isValidLibraryName 同閘(拒 "*" 與非法字元) 驗:tsc 全綠;youlin 實例資料層已手動補登記(探針 record PATCH 成 youlinhsieh-test1+新建 test2,portal_library count=2);route 部署驗證 隨下批 staging bundle(假帳密應回 401 而非 404)。 Co-Authored-By: Claude Fable 5 --- cypher-executor/src/routes/portal.ts | 58 ++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/cypher-executor/src/routes/portal.ts b/cypher-executor/src/routes/portal.ts index 5fa1148..d839da5 100644 --- a/cypher-executor/src/routes/portal.ts +++ b/cypher-executor/src/routes/portal.ts @@ -746,6 +746,64 @@ portalRouter.post('/portal/admin/libraries', (c) => }), ); +// POST /portal/daemon/libraries — 小幫手(daemon)連線精靈時把看守資料夾的庫報上來自動登記。 +// t159(2026-07-31 leo prod 實走揪出):daemon registerLibraries(arcrun-tray main.go:505,t52) +// 一直在打這個端點,但 cypher 從來沒有它 ⇒ 404 被 daemon「失敗不擋連線」靜默吞掉 +// ⇒ portal_library 登記簿永遠空 ⇒ portal「庫目錄管理」空(資料同步倒是全正常—— +// triplet records 的 library slot 都在,病只在登記簿沒人寫)。 +// 契約照 daemon 既有呼叫:body {email, password, libraries:[{name, display_name}]}。 +// 帳密驗證=與 /portal/session 同一套(daemon 只在精靈那一刻拿到帳密,不存)。 +// 冪等:已登記(同 name)跳過——重跑精靈不堆重複。 +portalRouter.post('/portal/daemon/libraries', (c) => + run(c, async () => { + const body = await c.req.json().catch(() => null); + const email = String(body?.email ?? '').trim().toLowerCase(); + const password = String(body?.password ?? ''); + if (!email || !password) return c.json({ error: 'email 與 password 必填' }, 400); + const items = Array.isArray(body?.libraries) ? body.libraries : []; + if (items.length === 0) return c.json({ success: true, registered: [], skipped: [] }); + + // 帳密驗證(沿用 /portal/session 的鎖定與驗證機制) + if (await isLocked(c.env, email)) return c.json({ error: '登入失敗次數過多,請稍後再試' }, 429); + const recordId = await findUserRecordId(c.env, email); + const rec = recordId ? await getRecordById(c.env, recordId) : null; + if (!rec || (rec.values.status ?? '') !== 'active' + || !(await verifyPassword(password, rec.values.password_hash ?? ''))) { + await recordLoginFail(c.env, email); + return c.json({ error: 'email 或密碼錯誤' }, 401); + } + await clearLoginFail(c.env, email); + + const seeded = await ensurePortalTemplates(c.env); + if (seeded.errors.length > 0) { + return c.json({ error: `portal templates seed 失敗:${seeded.errors.join('; ')}` }, 502); + } + const existing = await listRecordsByTemplate(c.env, LIBRARY_TEMPLATE); + const have = new Set(existing.map((l) => l.values.name ?? '')); + const ns = portalNamespace(c.env); + const registered: string[] = []; + const skipped: string[] = []; + for (const it of items) { + const name = String(it?.name ?? '').trim(); + const displayName = String(it?.display_name ?? '').trim() || name; + if (!isValidLibraryName(name) || name === '*') { skipped.push(name || '(空)'); continue; } + if (have.has(name)) { skipped.push(name); continue; } + const res = await kbdbFetch(c.env, '/records', { + method: 'POST', + body: JSON.stringify({ + template: LIBRARY_TEMPLATE, + owner_id: ns, + values: { name, display_name: displayName, description: '', status: 'active' }, + }), + }); + if (!res.ok) throw new KbdbError(`POST /records(portal_library,daemon 登記)→ ${res.status}`); + have.add(name); + registered.push(name); + } + return c.json({ success: true, registered, skipped }); + }), +); + // PATCH /portal/admin/libraries/:id — 改 display_name/description/status(停用庫=翻 status slot)。 portalRouter.patch('/portal/admin/libraries/:id', (c) => run(c, async () => {