diff --git a/console-ui/public/portal/index.html b/console-ui/public/portal/index.html
index febd877..cd34fbc 100644
--- a/console-ui/public/portal/index.html
+++ b/console-ui/public/portal/index.html
@@ -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 = '🎉 你的知識庫開張了!接下來只有一件事:'
- + '下載同步小幫手設定'
- + '——把你的資料夾連上來,檔案就會自動變成可搜尋的知識(想用 AI 問答,金鑰也在同一頁設定)。'
- + '';
- 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 || '') + '
' + 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 '
'
+ + '
' + (done ? '✅' : '⬜') + '' + html + '
';
+ };
+ 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 = '還差 ' + (3 - (s.daemon?1:0) - (s.config?1:0) - (s.key?1:0)) + ' 步,安裝就真的完成了'
+ + row(s.daemon, 'daemon',
+ '下載同步小幫手(把資料夾變成知識庫)
'
+ + '下載 Mac 版'
+ + '(封測版未簽章,第一次請右鍵→打開)')
+ + row(s.config, 'config',
+ '下載設定檔 放到 ~/.arcrun-rag/config.json
'
+ + '下載 config.json')
+ + row(s.key, 'key',
+ '啟用 AI 問答(aistudio.google.com 免費申請)
'
+ + ' '
+ + ''
+ + '')
+ + '';
+ 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);
diff --git a/cypher-executor/src/routes/portal.ts b/cypher-executor/src/routes/portal.ts
index 5fa1148..15e63fa 100644
--- a/cypher-executor/src/routes/portal.ts
+++ b/cypher-executor/src/routes/portal.ts
@@ -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 問答(t53,leo 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;
+ try {
+ record = JSON.parse(raw) as Record;
+ } 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;
+ 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 () => {