feat: IM平台列 + deviceID标识 + 会话持久化完善
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 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:
Aska committed 2026-09-07 04:51:41 +08:00
1 parent 1bf60c1ae4
commit 37e98ec584
26 files changed
+828 -116

No files matched your search

+24 -9
View File
@@ -31,6 +31,7 @@ import type {
SessionMeta,
SessionRecord,
} from "./types";
import { getClientId } from "./client";
/**
* REST client for the Cyrene v2 gateway. The gateway wraps every success in
@@ -160,21 +161,24 @@ export function health(): Promise<{ status: string }> {
return request<{ status: string }>("/health");
}
/** POST /api/v1/chat — one synchronous agent turn. */
/** POST /api/v1/chat — one synchronous agent turn. The body carries the sender's
* persistent per-browser deviceId (design/16 §4.5) so the persisted user and
* assistant messages are tagged with the concrete client. */
export function chatSync(
sessionId: string,
text: string
): Promise<ChatSyncResult> {
return request<ChatSyncResult>("/chat", {
method: "POST",
body: JSON.stringify({ sessionId, text }),
body: JSON.stringify({ sessionId, text, deviceId: getClientId() }),
});
}
/** POST /api/v1/chat with stream:true — triggers streaming over the WS channel.
* `image` (a base64 data: URI or URL) is attached for the vision path; the
* gateway's chat body currently has no image field, so an unconfigured backend
* simply ignores it. */
* simply ignores it. `deviceId` mirrors the client's per-browser id onto the
* persisted message (design/16 §4.5). */
export function chatStream(
sessionId: string,
text: string,
@@ -182,7 +186,7 @@ export function chatStream(
): Promise<{ stream: true }> {
return request<{ stream: true }>("/chat", {
method: "POST",
body: JSON.stringify({ sessionId, text, image, stream: true }),
body: JSON.stringify({ sessionId, text, image, stream: true, deviceId: getClientId() }),
});
}
@@ -197,6 +201,10 @@ export interface MessageDto {
mood?: string;
image?: string;
thinking?: string;
/** Source-platform tag (design/16 §4.4): "web" or an IM channel platform. */
platform?: string;
/** Concrete client/device that produced the message (design/16 §4.5). */
deviceId?: string;
createdAt: number;
}
@@ -211,6 +219,8 @@ function toChatMessage(m: MessageDto): ChatMessage {
mood: m.mood,
image: m.image,
thinking: m.thinking,
platform: m.platform,
deviceId: m.deviceId,
createdAt: m.createdAt,
};
}
@@ -228,18 +238,23 @@ export function createSubSession(title?: string): Promise<SessionMeta> {
});
}
/** GET /api/v1/sessions/{id}/messages?before=&limit= — hot-paginated history.
* The wire returns newest-first, but callers render chronologically (oldest
* first), so we reverse here: page 1 is the most recent `limit` messages in
* display order, and `before` (the oldest message's createdAt) walks upward. */
/** GET /api/v1/sessions/{id}/messages?before=&limit=&platform= — hot-paginated
* history, optionally filtered to one source platform (design/16 §4.4). The wire
* returns newest-first, but callers render chronologically (oldest first), so we
* reverse here: page 1 is the most recent `limit` messages in display order, and
* `before` (the oldest message's createdAt) walks upward. The Web client omits
* `platform` for the full shared main session; an IM adapter passes its own
* platform to see only its lane. */
export async function fetchMessages(
sessionId: string,
before?: number,
limit = 50
limit = 50,
platform?: string
): Promise<ChatMessage[]> {
const qs = new URLSearchParams();
qs.set("limit", String(limit));
if (before !== undefined) qs.set("before", String(before));
if (platform) qs.set("platform", platform);
const list = await request<MessageDto[]>(
`/sessions/${encodeURIComponent(sessionId)}/messages?${qs.toString()}`
);