feat(web): 会话级角色 — 角色跟随激活会话 + 选角入口 + 面板挂角色(B 批次 1 前端)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

按 DevDocs/design/22 §4.6/§5(7) 实现(后端由并行代理在做,按已定契约先行)。
- currentPersona 由「全局单值」改为「跟随激活会话」:新增 lib/sessionPersona.ts 解析(personaId 空 → 回落默认角色),App/TopBar/chat/lib/persona.ts 全部改为按激活会话取角色(头像/背景/名字/心情随会话切换)
- 交互:新建次级会话可选角色;会话换角色入口;管理面板 Sessions 页加「挂角色」下拉(走 PATCH /sessions/{id})
- 组件拆分:Nav.tsx 228→143(抽出 NavItem.tsx 53)、新增 SessionRail.tsx 275(会话侧栏)
- 契约:types.ts / lib/api/sessions.ts 加 personaId?(建会话/改会话支持)
- mood 改为**按角色分桶**(同一角色的多个会话共享该角色心情);升级后旧 mood 值不保留(刷新即空)——属行为变化,已在报告注明
- 测试:新增 personaSession.test.ts 40 checks(角色解析/回落/乐观更新与回滚)+ 既有 messageSync 54 checks 回归
- 验收:npm run build 绿;**未做浏览器端到端**(无 jsdom/渲染测试底座),UI 交互与失败路径提示未渲染验证
- 已知余量:useAppStore.ts 494/500 行(下次改动前应先拆 selectCurrentPersona/useCurrentPersona/useCurrentMood)
This commit is contained in:
AskaEth committed 2026-10-03 20:05:40 +08:00
1 parent 38a403afca
commit 66a96d2f93
16 files changed
+1044 -205

No files matched your search

+48 -4
View File
@@ -18,6 +18,9 @@ export interface MessageDto {
deviceId?: string;
/** Sender display name of a third-party message (design/16 §4.6). Blank for the owner. */
senderName?: string;
/** The role that spoke this message (design/22 §7-5, backing column
* `chat_messages.persona_id`); `''` means "inherit the current default role". */
personaId?: string;
createdAt: number;
}
@@ -35,6 +38,7 @@ function toChatMessage(m: MessageDto): ChatMessage {
platform: m.platform,
deviceId: m.deviceId,
senderName: m.senderName,
personaId: m.personaId,
createdAt: m.createdAt,
};
}
@@ -44,14 +48,54 @@ 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", {
/** `POST /api/v1/sessions` reply: the new session's id, plus the bound role when
* one was requested (docs/api-client.md §5) — the full summary is not echoed. `id`
* is accepted too so a backend that returns a summary still reads correctly. */
export interface CreateSessionResult {
sessionId?: string;
id?: string;
personaId?: string;
}
/** POST /api/v1/sessions — creates a new secondary session. `title` is the
* optional display name; `personaId` binds the session to a role (design/22
* §4.5). An omitted/empty `personaId` leaves the session inheriting the global
* default role. The reply carries only the new id, so callers seed the new row
* locally and let the next `GET /sessions` supply the metadata. */
export function createSubSession(opts: {
title?: string;
personaId?: string;
} = {}): Promise<CreateSessionResult> {
return request<CreateSessionResult>("/sessions", {
method: "POST",
body: JSON.stringify(title ? { title } : {}),
body: JSON.stringify({
...(opts.title ? { title: opts.title } : {}),
...(opts.personaId ? { personaId: opts.personaId } : {}),
}),
});
}
/** One PATCH /api/v1/sessions/{id} change set. At least one field must be set;
* the gateway rejects an empty body with 400. `personaId: ""` explicitly
* re-binds the session to "inherit the default role" (design/22 §4.5). */
export interface SessionPatch {
title?: string;
pinned?: boolean;
personaId?: string;
}
/** PATCH /api/v1/sessions/{id} — updates a session's display metadata and/or its
* bound role. The reply echoes only the fields that were changed. */
export function patchSession(
sessionId: string,
patch: SessionPatch
): Promise<{ id: string } & SessionPatch> {
return request<{ id: string } & SessionPatch>(
`/sessions/${encodeURIComponent(sessionId)}`,
{ method: "PATCH", body: JSON.stringify(patch) }
);
}
/** 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