feat(t52): 資料夾=庫端到端——kbdb 加 /entries/libraries(資料面 distinct 庫);portal 庫目錄合併「登記簿+蓋章自動出現」(auto);auto 庫改安全渲染(無 record_id 不放死按鈕,改給『登記到目錄』)+daemon/libraries 自動登記端點。leo 07-26:地端 2 個資料夾雲端就要 2 個庫
This commit is contained in:
@@ -1306,6 +1306,19 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
|
|||||||
if (!adminLibs.length) { $('ad-libs').innerHTML = '<div class="muted" style="padding:16px 4px">還沒有登記任何庫。未登記時整個系統視同單一 general 庫。</div>'; return; }
|
if (!adminLibs.length) { $('ad-libs').innerHTML = '<div class="muted" style="padding:16px 4px">還沒有登記任何庫。未登記時整個系統視同單一 general 庫。</div>'; return; }
|
||||||
$('ad-libs').innerHTML = adminLibs.map(function (l) {
|
$('ad-libs').innerHTML = adminLibs.map(function (l) {
|
||||||
var disabled = l.status === 'disabled';
|
var disabled = l.status === 'disabled';
|
||||||
|
// t52:auto 庫=資料蓋章自動出現、還沒登記進登記簿(沒有 record_id)——
|
||||||
|
// 那兩顆按鈕會帶空 id 打 API=按了就錯,改給「登記到目錄」一顆(leo 判準:不放死按鈕)。
|
||||||
|
if (l.auto) {
|
||||||
|
return '<div class="card-item">' +
|
||||||
|
'<div style="display:flex;align-items:baseline;gap:10px;flex-wrap:wrap">' +
|
||||||
|
'<span class="serif" style="font-size:17px;font-weight:600">' + esc(l.display_name || l.name) + '</span>' +
|
||||||
|
'<span class="dim mono" style="font-size:13px">' + esc(l.name) + '</span>' +
|
||||||
|
'<span class="tag green">啟用中</span><span class="tag">同步自動出現</span></div>' +
|
||||||
|
'<div style="margin-top:8px;font-size:14.5px;line-height:1.65;color:rgba(var(--ink-rgb),.65)">'
|
||||||
|
+ '這個庫來自同步小幫手的資料夾,已經可以搜尋與設權限。登記到目錄後可以改顯示名、設為圖譜來源。</div>' +
|
||||||
|
'<div class="btnrow"><button class="btn2" data-act="lib-adopt" data-name="' + esc(l.name) + '">登記到目錄</button></div>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
return '<div class="card-item"' + (disabled ? ' style="opacity:.6"' : '') + '>' +
|
return '<div class="card-item"' + (disabled ? ' style="opacity:.6"' : '') + '>' +
|
||||||
'<div style="display:flex;align-items:baseline;gap:10px;flex-wrap:wrap">' +
|
'<div style="display:flex;align-items:baseline;gap:10px;flex-wrap:wrap">' +
|
||||||
'<span class="serif" style="font-size:17px;font-weight:600">' + esc(l.display_name || l.name) + '</span>' +
|
'<span class="serif" style="font-size:17px;font-weight:600">' + esc(l.display_name || l.name) + '</span>' +
|
||||||
@@ -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)); });
|
.catch(function (e) { t.disabled = false; toast(friendlyErr(e)); });
|
||||||
return;
|
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') {
|
if (act === 'lib-status' || act === 'lib-graph') {
|
||||||
var lid = t.getAttribute('data-lid');
|
var lid = t.getAttribute('data-lid');
|
||||||
var body2 = {};
|
var body2 = {};
|
||||||
|
|||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
/Users/youlinhsieh/Documents/tech_projects/InkStoneCo/matrix/arcrun/cypher-executor/node_modules
|
||||||
@@ -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}。同步小幫手憑「用戶剛設的帳密」
|
// POST /portal/daemon/config — body {email, password}。同步小幫手憑「用戶剛設的帳密」
|
||||||
// 直接換到自己的設定(t54,leo 07-25:「最好的就是把它的帳密直接輸入」)——
|
// 直接換到自己的設定(t54,leo 07-25:「最好的就是把它的帳密直接輸入」)——
|
||||||
// 用戶不必再下載 config.json 丟隱藏資料夾,托盤第一次開啟輸入網址+帳密就上工。
|
// 用戶不必再下載 config.json 丟隱藏資料夾,托盤第一次開啟輸入網址+帳密就上工。
|
||||||
@@ -782,12 +838,32 @@ portalRouter.post('/portal/admin/chat-key', (c) =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
// GET /portal/admin/libraries — 庫目錄列表。
|
// GET /portal/admin/libraries — 庫目錄列表。
|
||||||
|
// t52(leo 2026-07-26:「地端 2 個資料夾、雲端就要 2 個庫,只有一個一定被罵」):
|
||||||
|
// 除了登記簿裡的庫,**也把資料裡實際蓋過章的庫一併列出**(標 auto:true)——
|
||||||
|
// 蓋章即現身,用戶不必先去登記;登記簿只負責顯示名/圖譜來源這些額外設定。
|
||||||
portalRouter.get('/portal/admin/libraries', (c) =>
|
portalRouter.get('/portal/admin/libraries', (c) =>
|
||||||
run(c, async () => {
|
run(c, async () => {
|
||||||
const auth = await requirePortalAdmin(c);
|
const auth = await requirePortalAdmin(c);
|
||||||
if (!auth.ok) return auth.res;
|
if (!auth.ok) return auth.res;
|
||||||
const libs = await listRecordsByTemplate(c.env, LIBRARY_TEMPLATE);
|
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 });
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
/Users/youlinhsieh/Documents/tech_projects/InkStoneCo/matrix/arcrun/kbdb/node_modules
|
||||||
@@ -31,6 +31,25 @@ entryRoutes.post('/', async (c) => {
|
|||||||
return c.json({ success: true, entry });
|
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)
|
// 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. list workflows under a project: ?parent_id=PROJECT&entry_type=workflow
|
||||||
// e.g. get one by idempotency key: ?page_name=skill-rag_with_arcrun
|
// e.g. get one by idempotency key: ?page_name=skill-rag_with_arcrun
|
||||||
|
|||||||
Reference in New Issue
Block a user