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冒烟由主会话跑)
This commit is contained in:
1 parent
1bf60c1ae4
commit
37e98ec584
26 files changed
+828
-116
No files matched your search
@@ -0,0 +1,46 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in new issue
Block a user