feat(web): 前端接会话持久化——拉历史热分页 + 侧栏会话列表
- 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:
1 parent
48421923f6
commit
bcc53d68f3
9 files changed
+371
-32
No files matched your search
@@ -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. */
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user