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

+11 -10
View File
@@ -22,7 +22,7 @@ export default function App(): React.ReactNode {
// when we hold a token — otherwise the gateway rejects the handshake with 401.
const [route, setRoute] = useState(() => parseAdminHash(window.location.hash));
const [menuOpen, setMenuOpen] = useState(false);
const setCurrentPersona = useAppStore((s) => s.setCurrentPersona);
const setPersonas = useAppStore((s) => s.setPersonas);
useEffect(() => {
initTheme();
@@ -35,13 +35,14 @@ export default function App(): React.ReactNode {
};
}, []);
// Load personas and pick the default one as the web main-session persona. This
// must not hinge on a single mount-time fetch: a token may appear (login) after
// mount, or the fetch may transiently fail — in which case currentPersona would
// stay null and the UI would show a neutral placeholder (or worse, silently
// persist a null). We re-run whenever the token appears, and retry on failure.
// The resolved persona is also persisted by useAppStore so a refresh keeps the
// correct avatar/background.
// Load the role catalog (design/22 §4.6). The rendered role is no longer one
// global value: it is derived per active session from this catalog plus that
// session's own `personaId` (see useAppStore's selectCurrentPersona), so this
// fetch only has to fill the catalog. It must not hinge on a single mount-time
// request: a token may appear (login) after mount, or the fetch may transiently
// fail — in which case the avatar/name would stay on the neutral placeholder. We
// re-run when the token appears, and retry on failure. The catalog is persisted
// by useAppStore, so a refresh renders the right role before the fetch returns.
const token = getAuthToken();
useEffect(() => {
if (!token) return;
@@ -53,7 +54,7 @@ export default function App(): React.ReactNode {
fetchPersonas()
.then((list) => {
if (cancelled) return;
setCurrentPersona(list.find((p) => p.isDefault) ?? list[0] ?? null);
setPersonas(list);
})
.catch(() => {
// Transient failure — retry rather than leaving the placeholder forever.
@@ -66,7 +67,7 @@ export default function App(): React.ReactNode {
cancelled = true;
if (retry !== null) window.clearTimeout(retry);
};
}, [token, setCurrentPersona]);
}, [token, setPersonas]);
const sessions = useAppStore((s) => s.sessions);
const activeSessionId = useAppStore((s) => s.activeSessionId);