feat(portal/t53): 進站完成安裝清單(leo 07-25:金鑰與 daemon 要在站內做,不然安裝沒完成)——常駐三項清單(下載小幫手/下載 config.json 站內生成/貼 Gemini 金鑰即時啟用)+新端點 POST /portal/admin/chat-key(改寫 tenant rag_chat 的 x-goog-api-key,admin 閘、金鑰不落 log)+session 回 email 供 config 生成

This commit is contained in:
uncle6me-web
2026-07-25 23:26:44 +08:00
parent 11ffd42899
commit a8d246ebe9
2 changed files with 129 additions and 21 deletions
+86 -21
View File
@@ -590,27 +590,10 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
$('v-firstsetup').classList.remove('on');
$('shell').classList.add('on');
$('tabbar').classList.add('on');
// t49 一次性歡迎引導(首次設定完成那一刻才出現;關掉就不再出現):
// 下一步只有兩件——裝同步小幫手(資料自動進來)、想問答就設金鑰。連去 installer /setup。
try {
if (localStorage.getItem('arcrun_onboarding') === '1' && !document.getElementById('onboard-card')) {
var setupUrl = (window.ARCRUN_CONFIG && window.ARCRUN_CONFIG.installerBase
? String(window.ARCRUN_CONFIG.installerBase).replace(/\/+$/, '')
: 'https://arcrun-installer.youlin-hsieh-dev.workers.dev') + '/setup';
var el = document.createElement('div');
el.id = 'onboard-card';
el.style.cssText = 'position:fixed;left:50%;bottom:24px;transform:translateX(-50%);z-index:60;max-width:560px;width:calc(100% - 32px);padding:18px 20px;border-radius:14px;background:rgba(var(--amber-rgb),.10);border:1px solid rgba(var(--amber-rgb),.45);backdrop-filter:blur(6px);font-size:14.5px;line-height:1.7';
el.innerHTML = '<b>🎉 你的知識庫開張了!</b>接下來只有一件事:'
+ '<a href="' + setupUrl + '" target="_blank" rel="noopener">下載同步小幫手設定</a>'
+ '——把你的資料夾連上來,檔案就會自動變成可搜尋的知識(想用 AI 問答,金鑰也在同一頁設定)。'
+ '<button id="onboard-close" style="margin-left:10px;border:none;background:none;color:inherit;cursor:pointer;font-size:13px;text-decoration:underline">知道了</button>';
document.body.appendChild(el);
document.getElementById('onboard-close').addEventListener('click', function () {
try { localStorage.removeItem('arcrun_onboarding'); } catch (e) { /* noop */ }
el.remove();
});
}
} catch (e) { /* 引導卡失敗不擋主流程 */ }
// t53(leo 07-25:「進站要做的事——給金鑰、下載 daemon——不然這個安裝沒完成」):
// 常駐「完成安裝」清單卡,三件做完才消失(localStorage 記進度;admin 才看得到——
// 金鑰與 daemon 設定是裝機者的事,同事帳號不顯示)。
try { renderSetupChecklist(); } catch (e) { /* 清單卡失敗不擋主流程 */ }
var p = S.profile || {};
$('side-foot').innerHTML = esc(p.display_name || '') + '<br>' + esc(location.host);
$('nav-workflows').classList.toggle('hide', !p.workflows_visible);
@@ -678,6 +661,88 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
dropSession();
});
// ── t53 完成安裝清單(進站必見,三件做完才消失)─────────────────────────────
function setupSteps() {
try { return JSON.parse(localStorage.getItem('arcrun_setup_steps') || '{}'); } catch (e) { return {}; }
}
function markStep(k) {
var s = setupSteps(); s[k] = 1;
try { localStorage.setItem('arcrun_setup_steps', JSON.stringify(s)); } catch (e) { /* noop */ }
renderSetupChecklist();
}
function renderSetupChecklist() {
var old = document.getElementById('setup-checklist');
if (old) old.remove();
var p = S.profile || {};
if (p.role !== 'admin') return;
var s = setupSteps();
if (s.daemon && s.config && s.key) return; // 三件齊=安裝真的完成,不再打擾
var cfg = window.ARCRUN_CONFIG || {};
var daemonUrl = cfg.daemonDownload || 'https://raw.githubusercontent.com/youlinhsieh/arcrun-rag-bundles/main/daemon/ArcrunRAG-mac-unsigned.zip';
var row = function (done, id, html) {
return '<div style="display:flex;gap:10px;align-items:baseline;margin-top:10px">'
+ '<span>' + (done ? '✅' : '⬜') + '</span><div style="flex:1">' + html + '</div></div>';
};
var el = document.createElement('div');
el.id = 'setup-checklist';
el.style.cssText = 'position:fixed;right:20px;bottom:20px;z-index:60;max-width:400px;width:calc(100% - 40px);padding:18px 20px;border-radius:14px;background:rgba(var(--amber-rgb),.10);border:1px solid rgba(var(--amber-rgb),.45);backdrop-filter:blur(8px);font-size:14px;line-height:1.65';
el.innerHTML = '<b>還差 ' + (3 - (s.daemon?1:0) - (s.config?1:0) - (s.key?1:0)) + ' 步,安裝就真的完成了</b>'
+ row(s.daemon, 'daemon',
'<b>下載同步小幫手</b>(把資料夾變成知識庫)<br>'
+ '<a href="' + esc(daemonUrl) + '" id="sc-daemon">下載 Mac 版</a>'
+ '<span class="muted" style="font-size:12.5px">(封測版未簽章,第一次請右鍵→打開)</span>')
+ row(s.config, 'config',
'<b>下載設定檔</b> 放到 <code style="font-size:12px">~/.arcrun-rag/config.json</code><br>'
+ '<a href="#" id="sc-config">下載 config.json</a>')
+ row(s.key, 'key',
'<b>啟用 AI 問答</b><a href="https://aistudio.google.com" target="_blank" rel="noopener">aistudio.google.com</a> 免費申請)<br>'
+ '<input id="sc-key" type="password" placeholder="貼上 Google AI 金鑰" style="width:60%;padding:6px 8px;border-radius:8px;border:1px solid rgba(var(--ink-rgb),.25);background:rgba(var(--ink-rgb),.04);color:var(--ink)"> '
+ '<button class="btn3" id="sc-key-save" style="padding:6px 12px;border-radius:8px;cursor:pointer">啟用</button>'
+ '<div id="sc-key-msg" style="font-size:12.5px;min-height:1.1em"></div>')
+ '<div style="margin-top:10px;text-align:right"><button id="sc-later" style="border:none;background:none;color:inherit;cursor:pointer;font-size:12.5px;text-decoration:underline;opacity:.65">稍後再說</button></div>';
document.body.appendChild(el);
var dl = document.getElementById('sc-daemon');
if (dl) dl.addEventListener('click', function () { markStep('daemon'); });
var cb = document.getElementById('sc-config');
if (cb) cb.addEventListener('click', function (ev) {
ev.preventDefault();
var conf = {
watch_folders: [],
manifest: '~/.arcrun-rag/manifest.json',
cypher_url: cfg.apiBase || '',
namespace: cfg.tenant || '',
library: 'kb',
extractor: 'claude',
email: (S.profile && S.profile.email) || ''
};
var a = document.createElement('a');
a.href = 'data:application/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(conf, null, 2));
a.download = 'config.json';
document.body.appendChild(a); a.click(); a.remove();
markStep('config');
});
var kb = document.getElementById('sc-key-save');
if (kb) kb.addEventListener('click', function () {
var k = (document.getElementById('sc-key').value || '').trim();
var m = document.getElementById('sc-key-msg');
if (!k) { m.textContent = '請先貼上金鑰'; return; }
kb.disabled = true; m.textContent = '啟用中…';
fetch(API_BASE + '/portal/admin/chat-key', {
method: 'POST',
headers: Object.assign({ 'Content-Type': 'application/json' }, authHeaders()),
body: JSON.stringify({ key: k })
}).then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
.then(function (x) {
kb.disabled = false;
if (!x.ok || !x.d.success) { m.textContent = x.d.error || '啟用失敗,請再試一次'; return; }
markStep('key');
})
.catch(function () { kb.disabled = false; m.textContent = '網路好像有問題,請再試一次'; });
});
var later = document.getElementById('sc-later');
if (later) later.addEventListener('click', function () { el.remove(); }); // 只藏本次,下次進站再提醒
}
// ── t49 首次設定(leo 07-25):console/setup → portal/admin/bootstrap → 預設庫登記 → portal/login
// 四發全是既有端點,一顆按鈕做完;用戶感受=「建一組帳密就進來了」。
$('fs-submit').addEventListener('click', doFirstSetup);
+43
View File
@@ -487,6 +487,7 @@ portalRouter.get('/portal/session', (c) =>
return c.json({
valid: true,
display_name: v.display_name ?? '',
email: v.email ?? '', // t53:完成安裝清單在站內生 daemon config.json 要用(身分顯示欄)
role,
libraries,
graph_allowed: await hasGraphAccess(c.env, libraries),
@@ -698,6 +699,48 @@ function toPublicLibrary(rec: PortalRecord) {
};
}
// POST /portal/admin/chat-key — body {key}。站內啟用 AI 問答(t53leo 07-25
// 「他進到網站要去給 gemini API Key」——不再回安裝器)。手法=G10 直嵌同款:
// 改寫 tenant 的 rag_chat workflow 記錄,把 x-goog-api-key 換成實值(KV 覆寫=重推)。
// 金鑰只過境不落 log;admin 閘(與庫登記同級)。
portalRouter.post('/portal/admin/chat-key', (c) =>
run(c, async () => {
const auth = await requirePortalAdmin(c);
if (!auth.ok) return auth.res;
const body = (await c.req.json().catch(() => null)) as { key?: string } | null;
const key = String(body?.key ?? '').trim();
if (!key) return c.json({ error: '請貼上你的 Google AI 金鑰' }, 400);
const tenant = portalTenant(c.env);
const kvKey = `${tenant}:wf:rag_chat`;
const raw = await c.env.WEBHOOKS.get(kvKey, 'text');
if (!raw) return c.json({ error: '這個實例沒有安裝 AI 問答工作流' }, 404);
let record: Record<string, unknown>;
try {
record = JSON.parse(raw) as Record<string, unknown>;
} catch {
return c.json({ error: 'AI 問答工作流記錄損壞,請重新安裝' }, 500);
}
// 結構不動、只換金鑰值:走遍 graph/config,凡 x-goog-api-key 欄一律設為新值
//(現值可能是 {{credential.gemini_api_key}} 佔位、空字串或舊 key,都直接覆蓋)。
let replaced = 0;
const visit = (o: unknown): void => {
if (Array.isArray(o)) { o.forEach(visit); return; }
if (o && typeof o === 'object') {
const rec = o as Record<string, unknown>;
for (const k of Object.keys(rec)) {
if (k.toLowerCase() === 'x-goog-api-key') { rec[k] = key; replaced += 1; }
else visit(rec[k]);
}
}
};
visit(record['graph']);
visit(record['config']);
if (replaced === 0) return c.json({ error: '工作流裡找不到金鑰欄位,請重新安裝後再試' }, 500);
await c.env.WEBHOOKS.put(kvKey, JSON.stringify(record));
return c.json({ success: true, replaced });
}),
);
// GET /portal/admin/libraries — 庫目錄列表。
portalRouter.get('/portal/admin/libraries', (c) =>
run(c, async () => {