Files
Cyrene_v2/frontend/web/src/lib/client.ts
T
Aska 37e98ec584
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat: IM平台列 + deviceID标识 + 会话持久化完善
- agent-96: chat_messages 加 platform 列(迁移0004) + IM主人私聊落主会话web(带platform) + 前端按平台过滤契约 + message_persisted 带 platform(复用 BroadcastPersisted)
- agent-97: chat_messages 加 device_id 列(迁移0005) + history.Message.DeviceID + Web发消息带deviceId(localStorage getClientId) + IM persistToMain 带 deviceID + 前端气泡展示 SourceLabel + 修主会话基础2个bug(fn scanMessage语法/DDL缺列)
- design/16 §4.4(平台) + §4.5(deviceID) 已入档
- 实机: POST /chat 带 deviceId 读回 user/assistant 均带 deviceId+platform; 后端 build/vet/test 绿 + 前端 build 绿
- 红线: 身份靠 actor 元数据; 禁止 shell 读 .env(JWT冒烟由主会话跑)
2026-09-07 04:51:41 +08:00

47 lines
1.9 KiB
TypeScript

// Per-browser persistent client id (design/16 §4.5). Every chat message the Web
// client sends carries this id so the agent knows which browser/device the owner
// is chatting from — e.g. "web:chrome:a1b2c3". It survives reloads and is shared
// by all tabs of the same browser (localStorage), distinguishing multiple browsers
// logged into the same account.
const CLIENT_ID_KEY = "cyrene.clientId";
/** Short, human-readable browser key derived from the navigator user agent, so the
* deviceID reads like "chrome" rather than a raw UA string. */
function browserKey(): string {
const ua = navigator.userAgent;
if (/edg\//i.test(ua)) return "edge";
if (/firefox\//i.test(ua)) return "firefox";
if (/chrome\//i.test(ua)) return "chrome";
if (/safari\//i.test(ua)) return "safari";
return "browser";
}
/** Eight lowercase hex characters of random entropy (one short id token). */
function randomToken(): string {
if (typeof crypto !== "undefined" && "getRandomValues" in crypto) {
const bytes = new Uint8Array(4);
crypto.getRandomValues(bytes);
let out = "";
for (const b of bytes) out += b.toString(16).padStart(2, "0");
return out;
}
return Math.random().toString(16).slice(2, 10);
}
/** Persistent, per-browser client id: "web:<browser>:<randomToken>". Stored in
* localStorage so every tab of the same browser shares one id and it survives
* reloads. Degrades to a fresh id when localStorage is unavailable (privacy mode
* / a page that blocked storage): the message is still tagged, just not stable. */
export function getClientId(): string {
const existing = localStorage.getItem(CLIENT_ID_KEY);
if (existing && existing.startsWith("web:")) return existing;
const id = `web:${browserKey()}:${randomToken()}`;
try {
localStorage.setItem(CLIENT_ID_KEY, id);
} catch {
// Storage unavailable: fall back to the freshly generated id for this call.
}
return id;
}