feat(web): 会话级角色 — 角色跟随激活会话 + 选角入口 + 面板挂角色(B 批次 1 前端)
按 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:
1 parent
38a403afca
commit
66a96d2f93
16 files changed
+1044
-205
No files matched your search
+11
-10
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user