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. */
+18
View File
@@ -59,6 +59,9 @@ export interface SessionStatePayload {
export interface ChatMessage {
id: string;
/** The owning session (backend `session_id`). Set when loaded via the sessions
* API; absent on a client-side optimistic bubble until it is persisted. */
sessionId?: string;
role: "user" | "assistant" | "system";
content: string;
/** Behavior mode this message was produced in — drives its bubble rendering.
@@ -76,6 +79,21 @@ export interface ChatMessage {
createdAt: number;
}
/** The two web session kinds (design/16 §2.1): the fixed main session that every
* turn falls into by default, and a user-created secondary session that shares
* the same persona memory. */
export type SessionType = "main" | "sub";
/** A web conversation session as returned by GET /api/v1/sessions. The main
* session has a fixed id; a secondary session's id is minted on creation. The
* message truth lives server-side, so this is metadata only. */
export interface SessionMeta {
id: string;
type: SessionType;
title?: string;
createdAt: number;
}
export type AgentStatus =
| "idle"
| "running"
+1 -1
View File
@@ -19,7 +19,7 @@ export function useSendChat(): {
sending: boolean;
} {
const [sending, setSending] = useState(false);
const sessionId = useAppStore((s) => s.sessionId);
const sessionId = useAppStore((s) => s.activeSessionId);
const addUserMessage = useAppStore((s) => s.addUserMessage);
const beginAssistant = useAppStore((s) => s.beginAssistant);
const endStream = useAppStore((s) => s.endStream);