feat(persona,web): 人格读侧(thinking 三态 + 角色级 mood)+ 面板表单 + mood 播种
按 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:
1 parent
6dc1f218d5
commit
2aa3233f84
20 files changed
+1042
-61
No files matched your search
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user