feat(session,web): 会话角色不可变 — PATCH 拒 personaId + 移除前端换角色入口
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

design/22 §8-1(主人 2026-10-03 拍板):会话角色在创建时确定、之后不可变,
要换角色必须新开会话。

后端:
- PATCH /api/v1/sessions/{id} 的 personaId 改为 json.RawMessage 判「键出现」,
  出现即 400(null/""/未知 id 一律拒),且先于会话存在性检查;错误消息点名 personaId
- 删除 Server.setSessionPersona 与 PATCH 的 501 分支;title/pinned 行为逐字不变
- sessionPersonaWriter 接口与 history.Store.SetSessionPersona 保留(存储能力)

前端:
- 移除会话行「切换角色」入口与面板「挂角色」下拉(含 setSessionPersona action、
  lib/sessionPersona.ts);保留「新建会话选角色」;面板「角色」列改为只读展示

契约:Docs/api-client.md -> v0.46(破坏性:依赖 PATCH 换角色的客户端须改为新建会话)

验证:go build/vet ./... 绿;gateway+history 包测试绿(-count=1);前端 npm run build
(tsc --noEmit + vite build)绿;9 个前端逻辑测试全绿。
变异验证:改回「静默忽略」/「写绑定」两种反向实现,新断言均 FAIL。
This commit is contained in:
AskaEth committed 2026-10-03 21:24:51 +08:00
1 parent f9430dee81
commit c7ecc3a249
10 files changed
+193 -448

No files matched your search

-13
View File
@@ -81,11 +81,6 @@ export interface AppStore {
/** Replace the session metadata list. */
setSessions: (list: SessionMeta[]) => void;
/** Re-bind one session to a role locally — the optimistic write and the rollback
* target of `lib/sessionPersona.ts#setSessionPersona`. `""` re-binds it to
* "inherit the default role". A no-op for an unknown session or one already
* bound to that role. */
setSessionPersona: (sessionId: string, personaId: string) => void;
/** Zero one session's unread badge locally (Android 协同 B3/D4). Called when the
* user opens the session and its page has loaded, and on the `session_read`
* frame another end broadcasts — a no-op for an already-read or unknown
@@ -179,14 +174,6 @@ export const useAppStore = create<AppStore>()(
set({
sessions: Object.fromEntries(list.map((s) => [s.id, s])),
}),
setSessionPersona: (sessionId, personaId) =>
set((s) => {
const meta = s.sessions[sessionId];
if (!meta || (meta.personaId ?? "") === personaId) return s;
return {
sessions: { ...s.sessions, [sessionId]: { ...meta, personaId } },
};
}),
setActiveSession: (id) => set({ activeSessionId: id, messages: [] }),
clearUnread: (sessionId) =>
set((s) => {