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:
@@ -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);
|
||||
|
||||
@@ -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<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 () => {
|
||||
|
||||
Reference in New Issue
Block a user