- 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冒烟由主会话跑)
47 lines
1.9 KiB
TypeScript
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;
|
|
}
|