feat(persona,web): 人格读侧(thinking 三态 + 角色级 mood)+ 面板表单 + mood 播种
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

按 design/04 §3.2(thinking 语义,showToUser 三态)+ design/01 §2.1 + 主人两条定论(心情跟角色不跟会话;mood 初值走口径 ①:由角色读端点暴露、前端加载播种)。
- 管理侧 GET /admin/personas 的 PersonaSummary 增 thinking(budgetTokens/effort/mode/showToUser)——**showToUser 保持三态**(未配置 ≠ false)
- 用户侧 GET /personas 增**角色级 mood**(取自 emotion 层该角色当前状态,非从消息推断)→ 前端加载播种 currentMood,消除「刷新后心情徽标短暂为空」
- 前端 Personas 页加 thinking 四控件(预算/effort/模式/showToUser 三态):只做输入校验,**provider 是否真支持由后端 WARN 决定,前端不假装能力**
- 先拆 useAppStore 选择器(原 494/500)到独立模块再做,避免越线
- Docs/api-client 版本号 +1(两处契约 + 三态与角色级 mood 语义)
- 验收:go build/vet/test 全绿 + linux/darwin 交叉编译 + 前端 build 绿 + personaSession/messageSync 逻辑测试通过
This commit is contained in:
AskaEth committed 2026-10-03 20:40:44 +08:00
1 parent 6dc1f218d5
commit 2aa3233f84
20 files changed
+1042 -61

No files matched your search

+12 -3
View File
@@ -16,6 +16,7 @@ import {
} from "./lib/api";
import type { SessionMeta } from "./lib/types";
import { useAppStore } from "./store/useAppStore";
import { personaMoodSeed } from "./store/personaSelectors";
export default function App(): React.ReactNode {
// Apply the persisted (dark-first) theme and open the realtime channel only
@@ -23,6 +24,7 @@ export default function App(): React.ReactNode {
const [route, setRoute] = useState(() => parseAdminHash(window.location.hash));
const [menuOpen, setMenuOpen] = useState(false);
const setPersonas = useAppStore((s) => s.setPersonas);
const seedMoods = useAppStore((s) => s.seedMoods);
useEffect(() => {
initTheme();
@@ -37,12 +39,18 @@ export default function App(): React.ReactNode {
// 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
// session's own `personaId` (see store/personaSelectors), 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.
//
// The catalog also carries each role's *role-level* mood (design/01 §2: emotion
// follows the role), which seeds the mood bucket of every role it names — that is
// what stops the 心情 badge from flashing empty after a refresh. Seeding merges
// (see `seedMoods`): a role the backend reports no mood for is skipped, so a WS
// `mood` frame that landed first is never cleared.
const token = getAuthToken();
useEffect(() => {
if (!token) return;
@@ -55,6 +63,7 @@ export default function App(): React.ReactNode {
.then((list) => {
if (cancelled) return;
setPersonas(list);
seedMoods(personaMoodSeed(list));
})
.catch(() => {
// Transient failure — retry rather than leaving the placeholder forever.
@@ -67,7 +76,7 @@ export default function App(): React.ReactNode {
cancelled = true;
if (retry !== null) window.clearTimeout(retry);
};
}, [token, setPersonas]);
}, [token, setPersonas, seedMoods]);
const sessions = useAppStore((s) => s.sessions);
const activeSessionId = useAppStore((s) => s.activeSessionId);