From a8d246ebe96f11eb7e676c750a09f1fc2cc35f4f Mon Sep 17 00:00:00 2001 From: uncle6me-web Date: Sat, 25 Jul 2026 23:26:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(portal/t53):=20=E9=80=B2=E7=AB=99=E5=AE=8C?= =?UTF-8?q?=E6=88=90=E5=AE=89=E8=A3=9D=E6=B8=85=E5=96=AE=EF=BC=88leo=2007-?= =?UTF-8?q?25=EF=BC=9A=E9=87=91=E9=91=B0=E8=88=87=20daemon=20=E8=A6=81?= =?UTF-8?q?=E5=9C=A8=E7=AB=99=E5=85=A7=E5=81=9A=EF=BC=8C=E4=B8=8D=E7=84=B6?= =?UTF-8?q?=E5=AE=89=E8=A3=9D=E6=B2=92=E5=AE=8C=E6=88=90=EF=BC=89=E2=80=94?= =?UTF-8?q?=E2=80=94=E5=B8=B8=E9=A7=90=E4=B8=89=E9=A0=85=E6=B8=85=E5=96=AE?= =?UTF-8?q?=EF=BC=88=E4=B8=8B=E8=BC=89=E5=B0=8F=E5=B9=AB=E6=89=8B/?= =?UTF-8?q?=E4=B8=8B=E8=BC=89=20config.json=20=E7=AB=99=E5=85=A7=E7=94=9F?= =?UTF-8?q?=E6=88=90/=E8=B2=BC=20Gemini=20=E9=87=91=E9=91=B0=E5=8D=B3?= =?UTF-8?q?=E6=99=82=E5=95=9F=E7=94=A8=EF=BC=89=EF=BC=8B=E6=96=B0=E7=AB=AF?= =?UTF-8?q?=E9=BB=9E=20POST=20/portal/admin/chat-key=EF=BC=88=E6=94=B9?= =?UTF-8?q?=E5=AF=AB=20tenant=20rag=5Fchat=20=E7=9A=84=20x-goog-api-key?= =?UTF-8?q?=EF=BC=8Cadmin=20=E9=96=98=E3=80=81=E9=87=91=E9=91=B0=E4=B8=8D?= =?UTF-8?q?=E8=90=BD=20log=EF=BC=89=EF=BC=8Bsession=20=E5=9B=9E=20email=20?= =?UTF-8?q?=E4=BE=9B=20config=20=E7=94=9F=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- console-ui/public/portal/index.html | 107 +++++++++++++++++++++------ cypher-executor/src/routes/portal.ts | 43 +++++++++++ 2 files changed, 129 insertions(+), 21 deletions(-) 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 () => {