diff --git a/console-ui/public/portal/index.html b/console-ui/public/portal/index.html index b484d37..d1669f4 100644 --- a/console-ui/public/portal/index.html +++ b/console-ui/public/portal/index.html @@ -1306,6 +1306,19 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return { if (!adminLibs.length) { $('ad-libs').innerHTML = '
還沒有登記任何庫。未登記時整個系統視同單一 general 庫。
'; return; } $('ad-libs').innerHTML = adminLibs.map(function (l) { var disabled = l.status === 'disabled'; + // t52:auto 庫=資料蓋章自動出現、還沒登記進登記簿(沒有 record_id)—— + // 那兩顆按鈕會帶空 id 打 API=按了就錯,改給「登記到目錄」一顆(leo 判準:不放死按鈕)。 + if (l.auto) { + return '
' + + '
' + + '' + esc(l.display_name || l.name) + '' + + '' + esc(l.name) + '' + + '啟用中同步自動出現
' + + '
' + + '這個庫來自同步小幫手的資料夾,已經可以搜尋與設權限。登記到目錄後可以改顯示名、設為圖譜來源。
' + + '
' + + '
'; + } return '
' + '
' + '' + esc(l.display_name || l.name) + '' + @@ -1455,6 +1468,22 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return { .catch(function (e) { t.disabled = false; toast(friendlyErr(e)); }); return; } + // t52:把「自動出現的庫」正式登記進目錄(之後就能改顯示名/設圖譜來源) + if (act === 'lib-adopt') { + var lname = t.getAttribute('data-name') || ''; + if (!lname) return; + t.disabled = true; + adminApi('POST', '/portal/admin/libraries', { name: lname, display_name: lname, description: '同步小幫手看守的資料夾' }) + .then(function (x) { + t.disabled = false; + if (guard401(x.status)) return; + if (!x.ok && x.status !== 409) { toast(x.d.error || ('登記失敗(HTTP ' + x.status + ')')); return; } + toast('已登記到目錄'); + loadAdmin(); + }) + .catch(function (e) { t.disabled = false; toast(friendlyErr(e)); }); + return; + } if (act === 'lib-status' || act === 'lib-graph') { var lid = t.getAttribute('data-lid'); var body2 = {}; diff --git a/cypher-executor/node_modules b/cypher-executor/node_modules new file mode 120000 index 0000000..5b81570 --- /dev/null +++ b/cypher-executor/node_modules @@ -0,0 +1 @@ +/Users/youlinhsieh/Documents/tech_projects/InkStoneCo/matrix/arcrun/cypher-executor/node_modules \ No newline at end of file diff --git a/cypher-executor/src/routes/portal.ts b/cypher-executor/src/routes/portal.ts index 8c1d872..2a99f49 100644 --- a/cypher-executor/src/routes/portal.ts +++ b/cypher-executor/src/routes/portal.ts @@ -699,6 +699,62 @@ function toPublicLibrary(rec: PortalRecord) { }; } +// POST /portal/daemon/libraries — body {email, password, libraries:[{name, display_name?}]}。 +// t52(leo 2026-07-26:「用戶可以看到我有 2 個庫,地端雲端都是 2 個,如果只有一個一定被罵」): +// 小幫手回報它看守的資料夾各自對應的庫,雲端**自動登記**——庫目錄與地端資料夾一比一。 +// 認證=同 /portal/daemon/config(用戶帳密)。已存在的庫略過(冪等),不覆寫顯示名。 +portalRouter.post('/portal/daemon/libraries', (c) => + run(c, async () => { + const body = (await c.req.json().catch(() => null)) as + | { email?: string; password?: string; libraries?: { name?: string; display_name?: string }[] } + | null; + const email = String(body?.email ?? '').trim().toLowerCase(); + const password = String(body?.password ?? ''); + if (!email || !password) return c.json({ error: 'email 與 password 必填' }, 400); + 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 wanted = Array.isArray(body?.libraries) ? body!.libraries! : []; + 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) => String(l.values.name ?? ''))); + const ns = portalNamespace(c.env); + const created: string[] = []; + for (const item of wanted) { + const name = String(item?.name ?? '').trim(); + if (!isValidLibraryName(name) || name === '*' || have.has(name)) continue; + const res = await kbdbFetch(c.env, '/records', { + method: 'POST', + body: JSON.stringify({ + template: LIBRARY_TEMPLATE, + owner_id: ns, + values: { + name, + display_name: String(item?.display_name ?? '').trim() || name, + description: '同步小幫手看守的資料夾', + status: 'active', + }, + }), + }); + if (!res.ok) throw new KbdbError(`POST /records(portal_library)→ ${res.status}`); + have.add(name); + created.push(name); + } + const after = await listRecordsByTemplate(c.env, LIBRARY_TEMPLATE); + return c.json({ success: true, created, libraries: after.map(toPublicLibrary) }); + }), +); + // POST /portal/daemon/config — body {email, password}。同步小幫手憑「用戶剛設的帳密」 // 直接換到自己的設定(t54,leo 07-25:「最好的就是把它的帳密直接輸入」)—— // 用戶不必再下載 config.json 丟隱藏資料夾,托盤第一次開啟輸入網址+帳密就上工。 @@ -782,12 +838,32 @@ portalRouter.post('/portal/admin/chat-key', (c) => ); // GET /portal/admin/libraries — 庫目錄列表。 +// t52(leo 2026-07-26:「地端 2 個資料夾、雲端就要 2 個庫,只有一個一定被罵」): +// 除了登記簿裡的庫,**也把資料裡實際蓋過章的庫一併列出**(標 auto:true)—— +// 蓋章即現身,用戶不必先去登記;登記簿只負責顯示名/圖譜來源這些額外設定。 portalRouter.get('/portal/admin/libraries', (c) => run(c, async () => { const auth = await requirePortalAdmin(c); if (!auth.ok) return auth.res; const libs = await listRecordsByTemplate(c.env, LIBRARY_TEMPLATE); - return c.json({ success: true, libraries: libs.map(toPublicLibrary), count: libs.length }); + const out = libs.map(toPublicLibrary); + const known = new Set(out.map((l) => l.name)); + // 資料面實際出現的庫(來自 ingest 蓋章的 metadata.library) + try { + const res = await kbdbFetch(c.env, `/entries/libraries?owner_id=${encodeURIComponent(portalTenant(c.env))}`); + if (res.ok) { + const body = (await res.json()) as { libraries?: string[] }; + for (const name of body.libraries ?? []) { + const n = String(name ?? '').trim(); + if (!n || known.has(n)) continue; + known.add(n); + out.push({ record_id: '', name: n, display_name: n, description: '資料同步時自動出現(可在此補顯示名)', status: 'active', graph_source: false, auto: true }); + } + } + } catch { + // 資料面查不到不擋登記簿(誠實降級:至少顯示已登記的庫) + } + return c.json({ success: true, libraries: out, count: out.length }); }), ); diff --git a/kbdb/node_modules b/kbdb/node_modules new file mode 120000 index 0000000..f6f53f2 --- /dev/null +++ b/kbdb/node_modules @@ -0,0 +1 @@ +/Users/youlinhsieh/Documents/tech_projects/InkStoneCo/matrix/arcrun/kbdb/node_modules \ No newline at end of file diff --git a/kbdb/src/routes/entries.ts b/kbdb/src/routes/entries.ts index b578c7d..e02f928 100644 --- a/kbdb/src/routes/entries.ts +++ b/kbdb/src/routes/entries.ts @@ -31,6 +31,25 @@ entryRoutes.post('/', async (c) => { return c.json({ success: true, entry }); }); +// GET /entries/libraries?owner_id=... — 這個租戶的資料裡實際出現過哪些庫(distinct)。 +// t52(leo 2026-07-26:地端幾個資料夾=雲端幾個庫):庫由 ingest 蓋章決定,這裡直接從 +// 資料反查,讓「蓋了章的庫」一定看得到,不必依賴任何登記動作。未蓋章的舊資料=general。 +// 註冊在 '/' 之前——Hono 路由先到先比,放後面會被 '/:id' 之類的樣式吃掉。 +entryRoutes.get('/libraries', async (c) => { + const owner = c.req.query('owner_id') || ''; + const rows = await c.env.DB.prepare( + `SELECT DISTINCT COALESCE(NULLIF(json_extract(metadata_json, '$.library'), ''), 'general') AS library + FROM entries + WHERE (?1 = '' OR owner_id = ?1) + AND COALESCE(json_extract(metadata_json, '$.status'), '') != 'deprecated' + ORDER BY library`, + ) + .bind(owner) + .all<{ library: string }>(); + const libraries = (rows.results ?? []).map((r) => r.library).filter(Boolean); + return c.json({ success: true, libraries, count: libraries.length }); +}); + // GET /entries — list with filters (entry_type, owner_id, parent_id, page_name, source, q/search) // e.g. list workflows under a project: ?parent_id=PROJECT&entry_type=workflow // e.g. get one by idempotency key: ?page_name=skill-rag_with_arcrun