@chatcode/cco-better-sidebar 0.18.0-rc.4 → 0.18.0-rc.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,107 @@
1
+ /** Standalone sign-in recovery; each document starts once and always keeps a usable reopen action. */
2
+ export const CHATCODE_LOGIN_PAGE_SCRIPT = String.raw`
3
+ (() => {
4
+ const message = document.getElementById('message');
5
+ const button = document.getElementById('login');
6
+ const fallback = document.getElementById('login-fallback');
7
+ const link = document.getElementById('login-link');
8
+ const current = new URL(location.href);
9
+ const returnTo = (() => {
10
+ const value = current.searchParams.get('return');
11
+ if (!value || !value.startsWith('/') || value.startsWith('//')) return '/';
12
+ const target = new URL(value, location.origin);
13
+ return target.origin === location.origin ? target.pathname + target.search + target.hash : '/';
14
+ })();
15
+ let busy = false, checking = false, attempted = false, recheck = false, attempt, popup;
16
+ const call = async name => {
17
+ const response = await fetch('/sidebar/api/' + name, {
18
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}',
19
+ });
20
+ const data = await response.json();
21
+ if (!response.ok || !data.ok) throw new Error('操作失败');
22
+ return data.value;
23
+ };
24
+ const showMessage = (text, error = false) => {
25
+ message.textContent = text;
26
+ message.classList.toggle('error', error);
27
+ };
28
+ async function login() {
29
+ if (busy) return;
30
+ attempted = true;
31
+ busy = true;
32
+ button.disabled = true;
33
+ button.textContent = '正在打开登录页…';
34
+ let tab = null;
35
+ try { tab = window.open('', '_blank'); if (tab) tab.opener = null; } catch {}
36
+ try {
37
+ if (!attempt) {
38
+ const started = await call('chatcode.start');
39
+ const url = new URL(started.url);
40
+ if (!['http:', 'https:'].includes(url.protocol) || typeof started.sessionId !== 'string') throw new Error('登录地址无效');
41
+ attempt = { sessionId: started.sessionId, url: url.href };
42
+ }
43
+ link.href = attempt.url;
44
+ link.textContent = attempt.url;
45
+ fallback.hidden = false;
46
+ if (tab && !tab.closed) {
47
+ try { tab.location.href = attempt.url; popup = tab; } catch { try { tab.close(); } catch {} }
48
+ }
49
+ showMessage('请在登录页完成登录;若网页已关闭,可随时重新打开。');
50
+ } catch {
51
+ try { if (tab) tab.close(); } catch {}
52
+ showMessage('登录页打开失败,请重试。', true);
53
+ } finally {
54
+ busy = false;
55
+ button.disabled = false;
56
+ button.textContent = attempt ? '重新打开登录页' : '去登录';
57
+ }
58
+ }
59
+ async function status() {
60
+ if (checking || busy) return;
61
+ checking = true;
62
+ const observedAttempt = attempt, observedStarted = attempted;
63
+ try {
64
+ const state = await call('chatcode.status');
65
+ if (busy || attempt !== observedAttempt || attempted !== observedStarted) return;
66
+ if (state.available && state.validation === 'valid' && !state.expired) {
67
+ try { if (popup) popup.close(); window.focus(); } catch {}
68
+ location.replace(returnTo);
69
+ return;
70
+ }
71
+ if (!state.available) {
72
+ recheck = true;
73
+ button.textContent = '重新检测';
74
+ showMessage('登录服务暂不可用,请稍后重试。', true);
75
+ return;
76
+ }
77
+ if (state.configured && state.validation === 'unavailable') {
78
+ recheck = true;
79
+ button.textContent = '重新检测';
80
+ showMessage('暂时无法验证已有账号,请检查网络后重新检测。', true);
81
+ return;
82
+ }
83
+ recheck = false;
84
+ button.textContent = attempt ? '重新打开登录页' : '去登录';
85
+ if (attempt && (state.login?.sessionId !== attempt.sessionId || state.login.state !== 'pending')) {
86
+ attempt = undefined;
87
+ fallback.hidden = true;
88
+ link.removeAttribute('href');
89
+ link.textContent = '';
90
+ button.textContent = '去登录';
91
+ showMessage('登录请求已失效,请重新登录。');
92
+ }
93
+ if (!attempted) await login();
94
+ } catch {
95
+ recheck = true;
96
+ button.textContent = '重新检测';
97
+ showMessage('无法读取登录状态,请稍后重试。', true);
98
+ } finally {
99
+ checking = false;
100
+ if (!busy) button.disabled = false;
101
+ }
102
+ }
103
+ button.addEventListener('click', () => { void (recheck ? status() : login()); });
104
+ void status();
105
+ setInterval(() => { void status(); }, 2000);
106
+ })();
107
+ `
@@ -2,14 +2,17 @@ import { readFile } from 'node:fs/promises'
2
2
  import { dirname, join } from 'node:path'
3
3
  import { fileURLToPath } from 'node:url'
4
4
  import type { Context, SidebarHttpRequest, SidebarHttpResponse } from './context-types.ts'
5
+ import { CHATCODE_LOGIN_PAGE_SCRIPT } from './login-page-script.ts'
5
6
 
6
7
  const assetPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'assets', 'logo.svg')
7
8
 
8
- function page(logo: string): string {
9
+ /** Render the standalone login gate with one automatic attempt and persistent manual recovery. */
10
+ export function renderChatcodeLoginPage(logo: string): string {
9
11
  return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>请登录 ChatCode</title><style>
10
12
  *{box-sizing:border-box}body{margin:0;min-width:320px;min-height:100vh;display:grid;place-items:center;font-family:Inter,"Microsoft YaHei",system-ui,sans-serif;color:#172b4d;background:radial-gradient(circle at 18% 18%,#e5edff 0,transparent 30rem),linear-gradient(135deg,#edf2fb,#dfe8f7)}.card{width:min(490px,calc(100vw - 40px));padding:34px 44px 36px;border:1px solid #dce4ef;border-radius:18px;background:#fff;box-shadow:0 20px 48px rgba(36,62,103,.18),0 2px 8px rgba(36,62,103,.06)}.brand{display:flex;align-items:center;justify-content:center;gap:12px;padding-bottom:27px;border-bottom:1px solid #edf0f5}.brand img{width:34px;height:34px}.brand strong{font-size:22px;letter-spacing:-.02em}.content{padding-top:40px;text-align:center}.content h1{margin:0;font-size:28px;line-height:1.3;font-weight:650}.content p{min-height:24px;margin:12px 0 30px;color:#71809a;font-size:14px;line-height:1.7}.action{width:100%;min-height:46px;border:0;border-radius:8px;background:#9B0B09;color:#fff;font:600 15px/1 Inter,"Microsoft YaHei",sans-serif;cursor:pointer;box-shadow:0 8px 18px rgba(155,11,9,.25);transition:transform .18s ease,box-shadow .18s ease,background .18s ease}.action:hover:not(:disabled){transform:translateY(-2px);background:#b20d0a;box-shadow:0 12px 24px rgba(155,11,9,.38)}.action:focus-visible{outline:3px solid #f0b7b5;outline-offset:3px}.action:disabled{cursor:default;opacity:.7}.error{color:#b42318}@media(max-width:480px){.card{padding:30px 26px}.content{padding-top:32px}}
11
- </style></head><body><main class="card"><div class="brand"><img src="data:image/svg+xml;base64,${logo}" alt=""><strong>ChatCode</strong></div><section class="content"><h1>请登录 ChatCode</h1><p id="message">请登录后继续使用。</p><button id="login" class="action" type="button">去登录</button></section></main><script>
12
- const message=document.getElementById('message'),button=document.getElementById('login');const current=new URL(location.href);const returnTo=(()=>{const v=current.searchParams.get('return');return v&&v.startsWith('/')&&!v.startsWith('//')?v:'/'} )();const call=async(name,body={})=>{const r=await fetch('/sidebar/api/'+name,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(body)});const d=await r.json();if(!r.ok||!d.ok)throw new Error(d.error?.message||'操作失败');return d.value};let waiting=false;async function status(){try{const s=await call('chatcode.status');if(s.available&&s.validation==='valid'&&!s.expired){location.replace(returnTo);return}if(waiting)message.textContent='正在等待浏览器完成登录…'}catch{message.textContent='无法读取登录状态,请稍后重试。';message.classList.add('error')}}async function login(){const tab=window.open('','_blank');button.disabled=true;button.textContent='正在打开登录页…';message.classList.remove('error');try{const s=await call('chatcode.start');waiting=true;if(tab&&!tab.closed){tab.opener=null;tab.location.href=s.url;message.textContent='请在新打开的浏览器窗口完成登录…'}else{message.innerHTML='浏览器拦截了登录窗口,请 <a href="'+s.url+'" target="_blank" rel="noopener noreferrer">打开登录页</a> 后完成登录。'}}catch{try{tab&&tab.close()}catch{}button.disabled=false;button.textContent='去登录';message.textContent='登录页打开失败,请重试。';message.classList.add('error')}}button.addEventListener('click',login);status();setInterval(status,2000);
13
+ .login-fallback{margin:18px 0 0;color:#71809a;font-size:12px;line-height:1.7;overflow-wrap:anywhere}.login-fallback a{display:block;margin-top:4px;color:#315da8;text-underline-offset:3px}
14
+ </style></head><body><main class="card"><div class="brand"><img src="data:image/svg+xml;base64,${logo}" alt=""><strong>ChatCode</strong></div><section class="content"><h1>请登录 ChatCode</h1><p id="message" aria-live="polite">正在检查登录状态…</p><button id="login" class="action" type="button" disabled>正在检查登录状态…</button><div id="login-fallback" class="login-fallback" hidden>若登录网页已关闭或未弹出,可再次访问:<a id="login-link" target="_blank" rel="noopener noreferrer"></a></div></section></main><script>
15
+ ${CHATCODE_LOGIN_PAGE_SCRIPT}
13
16
  </script></body></html>`
14
17
  }
15
18
 
@@ -20,7 +23,7 @@ export function registerLoginRoute(ctx: Context, fence: (req: SidebarHttpRequest
20
23
  try {
21
24
  const logo = (await readFile(assetPath)).toString('base64')
22
25
  res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store', 'content-security-policy': "default-src 'self'; img-src 'self' data:; style-src 'unsafe-inline'; script-src 'unsafe-inline'" })
23
- res.end(req.method === 'HEAD' ? undefined : page(logo))
26
+ res.end(req.method === 'HEAD' ? undefined : renderChatcodeLoginPage(logo))
24
27
  } catch { res.writeHead(500); res.end('login page unavailable') }
25
28
  } })
26
29
  }