feat(portal/t49): 首登建帳一顆按鈕——auth-status 偵測未初始化→v-firstsetup(console/setup→portal/admin/bootstrap→自動登記預設庫 kb→portal/login 四發連鎖)+進站一次性歡迎卡(下載小幫手/金鑰 → installer /setup)。leo 07-25:『加入的體驗要搞定』——用戶打開專屬網址第一眼就是建帳號,不再回安裝頁等

This commit is contained in:
uncle6me-web
2026-07-25 20:35:33 +08:00
parent 76b89be6d7
commit 9bbd25fdfc
+106
View File
@@ -186,6 +186,25 @@
</div>
</div>
<!-- 首次設定(t49leo 07-25:一鍵安裝的用戶打開專屬網址,第一件事就是在這裡建帳號,
不再需要回安裝器那頁。auth-status 說 configured:false 才會出現。)-->
<div class="authwrap view" id="v-firstsetup">
<div class="authbox">
<div>
<div class="brand">Arcrun</div>
<div class="brand-sub">歡迎!先建立你的帳號</div>
</div>
<div style="display:flex;flex-direction:column;gap:12px;text-align:left">
<input type="email" id="fs-email" class="txt" placeholder="你的 Email" autocomplete="username" style="padding:15px 16px;font-size:17px;border-radius:11px">
<input type="password" id="fs-password" class="txt" placeholder="設定密碼(至少 8 碼)" autocomplete="new-password" style="padding:15px 16px;font-size:17px;border-radius:11px">
<input type="password" id="fs-password2" class="txt" placeholder="再輸入一次密碼" autocomplete="new-password" style="padding:15px 16px;font-size:17px;border-radius:11px">
<button class="btn" id="fs-submit" style="margin-top:6px;padding:15px;font-size:17px;letter-spacing:.12em">建立帳號並進入</button>
<div id="fs-status" class="err" style="font-size:14px;min-height:1.2em"></div>
</div>
<div style="font-size:13.5px;color:rgba(var(--ink-rgb),.4);line-height:1.7">這組帳密就是之後登入知識庫用的,只需要設定這一次。</div>
</div>
</div>
<!-- 主應用 -->
<div id="shell">
<div id="sidenav">
@@ -548,14 +567,48 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
// ── 認證流 ──
function showAuth() {
$('v-firstsetup').classList.remove('on');
$('v-login').classList.add('on');
$('shell').classList.remove('on');
$('tabbar').classList.remove('on');
// t49:全新實例(還沒有任何帳號)→ 換顯示「首次設定」,不讓用戶對著登入殼發呆。
// 探測走 console/auth-statusconfigured 布林,console 首設同一顆;失敗就保持登入殼=誠實降級)。
fetch(API_BASE + '/console/auth-status')
.then(function (r) { return r.json(); })
.then(function (d) {
if (d && d.configured === false) {
$('v-login').classList.remove('on');
$('v-firstsetup').classList.add('on');
}
})
.catch(function () { /* 探測不到就維持登入殼 */ });
}
function showApp() {
$('v-login').classList.remove('on');
$('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) { /* 引導卡失敗不擋主流程 */ }
var p = S.profile || {};
$('side-foot').innerHTML = esc(p.display_name || '') + '<br>' + esc(location.host);
$('nav-workflows').classList.toggle('hide', !p.workflows_visible);
@@ -623,6 +676,59 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
dropSession();
});
// ── t49 首次設定(leo 07-25):console/setup → portal/admin/bootstrap → 預設庫登記 → portal/login
// 四發全是既有端點,一顆按鈕做完;用戶感受=「建一組帳密就進來了」。
$('fs-submit').addEventListener('click', doFirstSetup);
$('fs-password2').addEventListener('keydown', function (ev) { if (ev.key === 'Enter') doFirstSetup(); });
function doFirstSetup() {
var email = $('fs-email').value.trim();
var pw = $('fs-password').value;
var st = $('fs-status');
if (!email || pw.length < 8) { st.textContent = '請填 Email,密碼至少 8 碼'; return; }
if (pw !== $('fs-password2').value) { st.textContent = '兩次密碼不一樣'; return; }
$('fs-submit').disabled = true; st.textContent = '';
var post = function (path, body, hdrs) {
return fetch(API_BASE + path, {
method: 'POST',
headers: Object.assign({ 'Content-Type': 'application/json' }, hdrs || {}),
body: JSON.stringify(body)
}).then(function (r) { return r.json().catch(function () { return {}; }).then(function (d) { return { ok: r.ok, status: r.status, d: d }; }); });
};
post('/console/setup', { email: email, password: pw })
.then(function (x) {
if (!x.ok || !x.d.session_token) throw new Error(x.d.error || '設定沒有成功,請再試一次');
var ownerTok = x.d.session_token;
return post('/portal/admin/bootstrap', { email: email, password: pw, display_name: email.split('@')[0] }, { Authorization: 'Bearer ' + ownerTok })
.then(function (b) {
// 409=已 bootstrap 過(冪等視為就緒);其餘失敗誠實丟出
if (!b.ok && b.status !== 409) throw new Error(b.d.error || '初始化沒有成功,請再試一次');
// 預設庫 kb 自動登記(leo:庫要自動出現,不是叫用戶去登記)——
// 同步小幫手預設就寫進 kb 庫,這裡把登記簿先蓋好章;已存在(409/重複)不吵。
return post('/portal/login', { email: email, password: pw });
});
})
.then(function (l) {
if (!l.ok || !l.d.session_token) throw new Error(l.d.error || '帳號建好了,但自動登入沒成功——請用剛設定的帳密登入');
S.token = l.d.session_token;
try { localStorage.setItem('arcrun_portal_session', S.token); } catch (e) { /* noop */ }
// 預設庫登記(用 portal session;失敗不擋進入)
return fetch(API_BASE + '/portal/admin/libraries', {
method: 'POST',
headers: Object.assign({ 'Content-Type': 'application/json' }, authHeaders()),
body: JSON.stringify({ name: 'kb', display_name: '知識庫', description: '同步小幫手預設寫入的庫' })
}).catch(function () { /* 已存在或無權限都不擋 */ });
})
.then(function () {
$('fs-submit').disabled = false;
try { localStorage.setItem('arcrun_onboarding', '1'); } catch (e) { /* noop */ }
boot();
})
.catch(function (e) {
$('fs-submit').disabled = false;
st.textContent = (e && e.message) || friendlyErr(e);
});
}
// 任何 data 請求收到 401 → session 失效 → 回登入殼
function guard401(status) {
if (status === 401) { dropSession(); return true; }