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

+56 -1
View File
@@ -6,7 +6,13 @@ import { AdminApp } from "./admin/AdminApp";
import { parseAdminHash } from "./admin/router";
import { connectWs, disconnectWs } from "./lib/ws";
import { initTheme } from "./lib/theme";
import { fetchPersonas, getAuthToken } from "./lib/api";
import {
fetchMessages,
fetchPersonas,
fetchSessions,
getAuthToken,
} from "./lib/api";
import type { SessionMeta } from "./lib/types";
import { useAppStore } from "./store/useAppStore";
export default function App(): React.ReactNode {
@@ -60,6 +66,55 @@ export default function App(): React.ReactNode {
};
}, [token, setCurrentPersona]);
const sessions = useAppStore((s) => s.sessions);
const activeSessionId = useAppStore((s) => s.activeSessionId);
const setSessions = useAppStore((s) => s.setSessions);
const setActiveSession = useAppStore((s) => s.setActiveSession);
const setMessages = useAppStore((s) => s.setMessages);
// Load the session list once we hold a token, then focus a session: a persisted
// (still-present) session id, else the main session (design/16 §4.1). The main
// session has `type === "main"`; fall back to the first entry, then clear.
useEffect(() => {
if (!token) return;
let cancelled = false;
fetchSessions()
.then((list) => {
if (cancelled) return;
const byId = Object.fromEntries(
list.map((s) => [s.id, s] as const)
) as Record<string, SessionMeta>;
setSessions(list);
const main = list.find((s) => s.type === "main") ?? list[0];
const next =
activeSessionId && byId[activeSessionId] ? activeSessionId : main?.id;
setActiveSession(next ?? "");
})
.catch(() => {
// Backend not wired yet — leave the shell in its untouched state.
});
return () => {
cancelled = true;
};
}, [token, setSessions, setActiveSession]);
// Whenever the focused session changes (switch, new, or first load) and it is
// a session we know about, pull its initial page of history into the panel.
useEffect(() => {
if (!activeSessionId || !sessions[activeSessionId]) return;
let cancelled = false;
fetchMessages(activeSessionId, undefined, 50)
.then((list) => {
if (!cancelled) setMessages(list);
})
.catch(() => {
if (!cancelled) setMessages([]);
});
return () => {
cancelled = true;
};
}, [activeSessionId, sessions, setMessages]);
// The M9 modal UI and the admin subsystem (design/10) are two separate shells.
// `/admin` is reached via `#/admin`; every admin module is a hash segment.
if (route.admin) {