feat(web): 前端接会话持久化——拉历史热分页 + 侧栏会话列表
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- store: 消息真源移到后端(sessions元数据map + activeSessionId,partialize 移除 messages 改存会话元数据,version:2 migrate 清旧数据)
- api: fetchSessions/createSubSession/fetchMessages(热分页before游标)/deleteSession + MessageDto→toChatMessage
- Nav: 侧栏「会话」区——主会话置顶 + 次级列表 + +新建会话(Miuix)
- 新增 HistoryList: 顶部「加载更早」向上分页 + 保守虚拟化(>200只渲染最近200)
- useChat: send 用 activeSessionId;App: token 后 fetchSessions→激活主会话→拉近50条
- Docs/api-client: v0.10 补 WS sessionId + history 回放事件契约
This commit is contained in:
Aska committed 2026-09-07 03:52:50 +08:00
1 parent 48421923f6
commit bcc53d68f3
9 files changed
+371 -32

No files matched your search

+70 -3
View File
@@ -16,6 +16,7 @@ import type {
AutomationCreateResult,
AutomationRunResult,
ChannelTestResult,
ChatMessage,
ChatSyncResult,
LlmTestRequest,
LlmTestResult,
@@ -27,6 +28,7 @@ import type {
PersonaRecord,
PersonaUpsertRequest,
Role,
SessionMeta,
SessionRecord,
} from "./types";
@@ -184,9 +186,74 @@ export function chatStream(
});
}
/** GET /api/v1/sessions — known channel sessions. */
export function fetchSessions(): Promise<SessionRecord[]> {
return request<SessionRecord[]>("/sessions");
/** One chat message as returned by GET /api/v1/sessions/{id}/messages. Mirrors
* the `chat_messages` row; `mode`/`mood`/`image`/`thinking` are optional. */
export interface MessageDto {
id: string;
sessionId: string;
role: "user" | "assistant" | "system";
content: string;
mode?: Mode;
mood?: string;
image?: string;
thinking?: string;
createdAt: number;
}
/** Map a wire message into a display ChatMessage (carrying its sessionId). */
function toChatMessage(m: MessageDto): ChatMessage {
return {
id: m.id,
sessionId: m.sessionId,
role: m.role,
content: m.content,
mode: m.mode,
mood: m.mood,
image: m.image,
thinking: m.thinking,
createdAt: m.createdAt,
};
}
/** GET /api/v1/sessions — the main + secondary sessions for this web account. */
export function fetchSessions(): Promise<SessionMeta[]> {
return request<SessionMeta[]>("/sessions");
}
/** POST /api/v1/sessions — creates a new secondary session. */
export function createSubSession(title?: string): Promise<SessionMeta> {
return request<SessionMeta>("/sessions", {
method: "POST",
body: JSON.stringify(title ? { title } : {}),
});
}
/** 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. */
export async function fetchMessages(
sessionId: string,
before?: number,
limit = 50
): Promise<ChatMessage[]> {
const qs = new URLSearchParams();
qs.set("limit", String(limit));
if (before !== undefined) qs.set("before", String(before));
const list = await request<MessageDto[]>(
`/sessions/${encodeURIComponent(sessionId)}/messages?${qs.toString()}`
);
return list.map(toChatMessage).reverse();
}
/** DELETE /api/v1/sessions/{id} — removes a secondary session (and its messages). */
export function deleteSession(
sessionId: string
): Promise<{ id: string; deleted: true }> {
return request<{ id: string; deleted: true }>(
`/sessions/${encodeURIComponent(sessionId)}`,
{ method: "DELETE" }
);
}
/** GET /api/v1/personas — persona summaries. */