Files
Agent-System-Frontend/src/auth.ts
T
weichen dc70399d45 Rebuild the left nav as a task-centric sidebar (option A, R1)
Why: creation entries were split (project + at sidebar top-left, new
session at bottom-left) and the project tree forced per-project
navigation; the approved option A design makes one primary New Task
entry, organization a view mode, and identity always present.

Evidence:
- New TaskSidebar: single New Task split button (current-project context,
  Cmd-N, secondary menu for new project / new task in project), project
  <-> timeline organization with persisted collapse, pinned section,
  archive view toggle, hover row actions, account footer fed by
  /api/me (embedded mode shows local tenant; logout clears the key),
  workspace icon strip retained (D25 shared list with Cmd-1..9).
- store: tasks = cross-project list via GET /api/conversations; openTask
  switches project context; pin/archive optimistic + refresh; last
  project context persisted; stale conversations field removed.
- tasklist.ts pure helpers + 6 vitest cases; npm run build passes;
  headless walk on the real binary: cross-project open, pinned-first,
  archive hide/show/restore, new-task flow, zero console errors.

Impact: ProjectNav removed; contracts.ts regenerated from core
(pinned/archived); no composer/right-pane changes (D25/D26/D31 intact).

Refs: Agent-System-Documents docs/proposals/2026-09-30-ui-redesign-and-auth.md
2026-09-30 10:41:12 +00:00

45 lines
1.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// API key 凭证模块:登录门存取(localStorage 优先,无 DOM 环境回退内存)。
// 服务端开启认证时(PG 档默认),所有请求带 Bearer 头;SSE/静态资源链接走 ?key=。
const KEY = 'agent-system.apiKey'
interface KV { getItem(k: string): string | null; setItem(k: string, v: string): void; removeItem(k: string): void }
function pickStorage(): KV {
try {
if (typeof localStorage !== 'undefined') return localStorage
} catch { /* 隐私模式等 */ }
const mem = new Map<string, string>()
return {
getItem: k => mem.get(k) ?? null,
setItem: (k, v) => void mem.set(k, v),
removeItem: k => void mem.delete(k),
}
}
const storage = pickStorage()
export const auth = {
apiKey(): string { return storage.getItem(KEY) ?? '' },
setKey(key: string) { storage.setItem(KEY, key.trim()) },
clear() { storage.removeItem(KEY) },
// 带凭证的 fetch:未设置密钥时与原生 fetch 等价(嵌入式档无认证)。
fetch(input: string, init?: RequestInit): Promise<Response> {
const key = this.apiKey()
if (!key) return fetch(input, init)
const headers = new Headers(init?.headers ?? {})
headers.set('Authorization', `Bearer ${key}`)
return fetch(input, { ...init, headers })
},
// 退出登录:清凭证并整页回登录门(服务端会话/密钥均为无状态凭证,无需服务端注销)。
logout() {
this.clear()
try { location.reload() } catch { /* 非 DOM 环境(测试)忽略 */ }
},
// 无法带头的资源 URL(EventSource / <img> / 下载):追加 ?key=。
withKey(url: string): string {
const key = this.apiKey()
if (!key) return url
return url + (url.includes('?') ? '&' : '?') + 'key=' + encodeURIComponent(key)
},
}