docs(spec): 详情面板7问题修复设计文档
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

7个问题根因分析+修复方案:
P0: 聊天路由竞态/记忆查询缺陷/hand_trigger硬编码
P1: Agent画像断链/反思持久化多重缺陷
P2: 演化差异视图/管家Tab上下文混淆
路径B: 系统桥接修复,扩展已有命令而非新增
This commit is contained in:
iven
2026-04-11 10:50:25 +08:00
parent d871685e25
commit 449768bee9

View File

@@ -0,0 +1,285 @@
# Detail Panel 7-Issue Fix Design
> Date: 2026-04-11
> Status: Draft
> Approach: Path B — System Bridge (fix bugs + bridge disconnected systems)
> Constraint: No new SaaS endpoints, no new stores, no new middleware (stabilization compliance)
## Background
User discovered 7 issues during real usage of ZCLAW desktop app's detail panel (RightPanel). Root cause analysis revealed a pattern: 5 of 7 issues stem from "backend has capability but frontend doesn't consume it" — disconnected systems that need bridging.
## Issue Summary
| # | Problem | Severity | Root Cause | Category |
|---|---------|----------|------------|----------|
| 1 | Can't chat after restart | P0 | Heartbeat degrades to 'tauri' in localStorage, restoreSession blindly trusts it | Route race |
| 2 | "About me / How I see you" never updates | P1 | Panel reads agentStore.Clone, memory extractor writes identity files — no bridge | Data disconnect |
| 3 | Memory 311 count but empty list/graph | P0 | memory_search empty query + min_similarity=None → FTS5 returns 0 | Query logic bug |
| 4 | Only 1 reflection, effects unknowable | P1 | History only persists latest (not array); pattern thresholds too high; state restore race | Multi-factor |
| 5 | hand_trigger always rejected | P0 | autonomy-manager.ts:268 maps hand_trigger to null — hardcoded bug | Hardcoded bug |
| 6 | Evolution has data but effects unknowable | P2 | HistoryItem only shows truncated reason, no expandable diff | UX gap |
| 7 | Butler "6 messages" incorrect + empty below | P2 | Shared chat stats header confused for butler data; no pain points detected | UX confusion |
---
## Section 1: P0 Fixes
### Fix 1 — Chat Routing After Restart
**Root Cause**: `saasStore.restoreSession()` blindly trusts localStorage connectionMode written by heartbeat degrade. After restart, SaaS auth may still be valid but the persisted 'tauri' mode prevents SaaS relay usage.
**Solution**:
1. In `restoreSession()` (saasStore.ts ~line 750): Before accepting persisted mode, validate SaaS token:
- Attempt refresh token → access token exchange
- If successful, restore to 'saas' mode regardless of persisted value
- Only accept 'tauri' mode if token refresh fails
2. In `loadConnectionMode()` (saas-session.ts ~line 170): Add staleness check:
- Persist timestamp alongside mode in localStorage
- On load, if persisted mode is 'tauri' and timestamp > 5 minutes old, re-validate
3. Heartbeat degrade writes timestamp for staleness tracking
**Files**:
- `desktop/src/store/saasStore.ts` — restoreSession() ~line 750 (verify exact location before implementation)
- `desktop/src/lib/saas-session.ts` — loadConnectionMode() ~line 170
- `desktop/src/store/connectionStore.ts` — connect() routing ~line 428
**Risk**: Low. Only changes startup validation logic, doesn't affect active session behavior.
---
### Fix 3 — Memory List/Graph Empty Despite 311 Entries
**Root Cause**: `memory_search` Tauri command calls `VikingStorage::find()` with empty query string and `min_similarity: None`. FTS5 requires non-empty query to match anything. `memory_stats` works because it sets `min_similarity: Some(0.0)` which triggers table scan path.
**Solution**:
1. In `memory_commands.rs` `memory_search` (line 164): When query is empty or whitespace-only, set `min_similarity: Some(0.0)` as default fallback
2. This makes the empty-query behavior consistent with `memory_stats` — table scan returns all entries
3. No changes to VikingStorage internals — only the calling parameter
**Files**:
- `desktop/src-tauri/src/memory_commands.rs` — memory_search at line 148-184
**Risk**: Minimal. Single-line parameter change. Performance concern mitigated by existing `limit: 50` default.
---
### Fix 5 — hand_trigger Always Rejected
**Root Cause**: `autonomy-manager.ts:268` maps `hand_trigger: null` in the actionMap. When configKey is null, `isActionAllowed()` immediately returns false. No `handAutoTrigger` field exists in AutonomyConfig.allowedActions.
**Solution**:
1. Add `handAutoTrigger: boolean` to `AutonomyConfig.allowedActions` type (autonomy-manager.ts)
2. Change actionMap: `hand_trigger: null``hand_trigger: 'handAutoTrigger'`
3. Update default configs:
- `autonomous` level: `handAutoTrigger: true`
- `supervised` level: `handAutoTrigger: false`
- `guided` level: `handAutoTrigger: false`
4. Add toggle control in `AutonomyConfig.tsx` UI
**Files**:
- `desktop/src/lib/autonomy-manager.ts` — line 268 actionMap + allowedActions type + default configs
- `desktop/src/components/AutonomyConfig.tsx` — add handAutoTrigger toggle
**Risk**: Low. Pure frontend change. Backend already supports autonomous mode bypassing approval gate.
---
## Section 2: P1 Fixes
### Fix 2 — "About Me / How I See You" Auto-Update
**Root Cause**: Panel reads from `agentStore.Clone` static fields. Memory extractor writes to identity files (soul/instructions/user_profile). Backend `UserProfileStore` (Rust) has structured user modeling (industry/role/expertise/communication_style/recent_topics) but no Tauri command exposes it to frontend.
**Solution (System Bridge — Alternative Approach: Extend Existing Command)**:
To comply with stabilization constraint "no new Tauri commands" (CLAUDE.md §1.3: "已有 189 个70 个无前端调用"), we use Option (b): extend an existing command's return payload.
1. **Extend `agent_get` command**: The existing `agent_get(agent_id)` command returns `AgentConfig`. Extend its return to include an optional `user_profile` field from `UserProfileStore::get()`.
- This avoids creating a new command while exposing existing backend data.
- File: `desktop/src-tauri/src/kernel_commands/agents.rs` — extend return type
2. **Frontend bridge**:
- Use component-local state (`useState`) in RightPanel for UserProfile data (not Store — per CLAUDE.md §4.2 layered responsibilities)
- On mount and after conversation complete, re-fetch agent data which now includes profile
- "How I See You" section renders from two sources:
- Static: `Clone` fields (userName, workspaceDir, privacyOptIn)
- Dynamic: `UserProfile` fields (industry, role, expertise, communication_style, recent_topics)
- Fallback: if UserProfile empty, show static Clone data only
3. **Refresh trigger**: In `streamStore` flow completion callback (~line 446), after memory extraction, trigger RightPanel's profile refresh via a lightweight event or re-fetch
**Files**:
- `desktop/src-tauri/src/kernel_commands/agents.rs` — extend agent_get return type
- `desktop/src-tauri/src/lib.rs` — no change needed (command already registered)
- `desktop/src/components/RightPanel.tsx` — dual-source rendering (lines 523-557)
- `desktop/src/store/chat/streamStore.ts` — add profile refresh trigger ~line 446
**Risk**: Medium. Extends existing command rather than creating new one. UserProfileStore already exists. No new SaaS endpoints. No new stores.
---
### Fix 4 — Reflection Only 1 Entry, Effects Unknowable
**Root Cause**: Multi-factor — history only persists `reflection:latest` (single entry) not `reflection:history` (array); pattern detection thresholds too high (5+ same type); state restore race condition in post_conversation_hook.
**Solution**:
1. **Fix history persistence** (reflection.rs):
- Ensure `reflect()` appends to `reflection:history:{agent_id}` array (not overwrite)
- Maintain cap at 20 entries (FIFO)
- Always write both `reflection:latest` AND `reflection:history`
2. **Lower pattern detection thresholds** (reflection.rs `analyze_patterns()`):
- Pattern "many tasks": 5+ → 3+ (rationale: early pattern detection provides timely insights)
- Pattern "many preferences": 5+ → 3+ (rationale: user preferences accumulate quickly in normal use)
- Pattern "many experiences": 5+ → 3+ (rationale: experiences are high-value patterns worth detecting early)
- Pattern "high-visit memories": 3+ → 2+ (rationale: even 2 high-visit memories indicate recurring interest)
- Pattern "low-importance memories": 20+ → 15+ (rationale: conservative reduction from 20 — avoids noisy cleanup suggestions at 10)
3. **Fix state restore race** (intelligence_hooks.rs):
- Change `post_conversation_hook` state restore from "pop (read + delete)" to "read only"
- Cleanup mechanism: old state is cleaned up on next conversation hook invocation (FIFO with max 1 entry per agent), preventing indefinite accumulation
- Let `getHistory` always find complete data in VikingStorage
4. **Enhance reflection effect visibility** (ReflectionLog.tsx):
- Each history entry shows: pattern count + summary, improvement suggestion count, identity proposal count
- Timeline indicator: "Triggered after N conversations"
- Expandable detail view for patterns and suggestions
**Files**:
- `desktop/src-tauri/src/intelligence/reflection.rs` — history persistence + threshold adjustment
- `desktop/src-tauri/src/intelligence_hooks.rs` — state restore race fix
- `desktop/src/components/ReflectionLog.tsx` — enhanced visibility UI
**Risk**: Low-Medium. Reflection persistence fix is the most impactful change. Threshold changes are conservative (3+ is still meaningful).
---
## Section 3: P2 Fixes
### Fix 6 — Evolution Effects Unknowable
**Root Cause**: `HistoryItem` component only renders truncated `reason` text and timestamp. Snapshot data contains full `files` object but UI never renders diff content. `ProposalCard` already has a `DiffView` component that could be reused.
**Solution**:
1. **Expandable HistoryItem** (IdentityChangeProposal.tsx):
- Add click-to-expand behavior on history items
- When expanded, show which files changed (as tags)
- Show full reason text (no truncation)
- Reuse existing `DiffView` component for before/after comparison
2. **Evolution summary metadata**:
- Each history item header shows file change count badge
- Color coding: soul changes = purple, instructions = blue, user_profile = green
**Files**:
- `desktop/src/components/IdentityChangeProposal.tsx` — HistoryItem component (lines 269-304)
**Risk**: Low. Pure UI enhancement. Reuses existing DiffView component.
---
### Fix 7 — Butler Tab Confusion + Empty Data
**Root Cause**: "6 messages" is shared chat statistics header displayed across ALL tabs (not butler-specific). Butler tab below is empty because: (a) no pain points detected in short conversations, (b) no proposals generated, (c) Viking memory path empty.
**Solution**:
1. **Contextual header** (RightPanel.tsx):
- When butler tab is active, replace chat statistics with butler-specific summary
- Show: Butler name + pain points count + proposals count + memory count
- Other tabs keep existing chat statistics
2. **Enhanced empty state** (ButlerPanel/index.tsx):
- When all sections empty, show friendly onboarding message
- "管家正在了解您,多轮对话后会自动发现您的需求"
- Add manual "analyze now" button that calls `analyze_for_pain_signals` with current conversation history
- Button disabled when conversation < 2 messages (prevents meaningless analysis)
- Show loading state during analysis
- Display "未发现痛点" message when analysis completes with no results
3. **Lower pain point detection threshold** (pain_aggregator.rs):
- Relax keyword matching patterns
- Allow pain point extraction from shorter conversations (minimum 2 messages instead of 3+)
**Files**:
- `desktop/src/components/RightPanel.tsx` conditional header rendering (lines 374-386)
- `desktop/src/components/ButlerPanel/index.tsx` empty state enhancement
- `desktop/src-tauri/src/intelligence/pain_aggregator.rs` threshold adjustment
**Risk**: Low. UX improvements. Pain point threshold change is conservative.
---
## Implementation Priority
| Batch | Issues | Severity | Est. Complexity |
|-------|--------|----------|-----------------|
| Batch 1 | 1, 3, 5 | P0 | Low (small targeted fixes) |
| Batch 2 | 2, 4 | P1 | Medium (system bridge + persistence fix) |
| Batch 3 | 6, 7 | P2 | Low (UX enhancements) |
## Testing Plan
### Automated Tests (after each batch)
- `tsc --noEmit` TypeScript type check
- `cargo check` Rust compilation
- `vitest run` Frontend unit tests
### Rust Unit Tests (Batch 2)
- `memory_search` empty query behavior: verify `min_similarity` defaults to `Some(0.0)` when query is empty
- Reflection history persistence: verify `reflect()` appends to history array, not overwrites
### Manual Test Matrix
**Fix 1 — Chat Routing (must test both modes)**:
- [ ] SaaS mode: restart app with valid SaaS session should restore to 'saas' mode
- [ ] SaaS mode: restart app with expired SaaS session should degrade to 'tauri' mode
- [ ] Tauri mode: restart app should stay in 'tauri' mode
- [ ] Fresh install should prompt login or default to local mode
**Fix 2 — Agent Profile**:
- [ ] UserProfile empty fallback to static Clone data
- [ ] UserProfile populated dynamic fields shown alongside static
- [ ] After multi-turn conversation profile fields refresh
**Fix 3 — Memory Display**:
- [ ] Memory panel loads with entries visible (not just count)
- [ ] Memory graph renders with nodes and edges
- [ ] Pagination works for > 50 entries
**Fix 5 — Hand Trigger**:
- [ ] Existing autonomy configs migrate correctly (no handAutoTrigger → default false)
- [ ] Autonomous level → hand_trigger auto-allowed
- [ ] Supervised/guided → hand_trigger requires approval
**Fix 4 — Reflection**:
- [ ] After 3+ conversations → reflection triggers automatically
- [ ] History shows multiple entries (not just 1)
- [ ] Each entry shows pattern/improvement/proposal counts
**Fix 6 — Evolution**:
- [ ] Click history item → expandable diff view appears
- [ ] Diff view shows before/after for changed files
**Fix 7 — Butler**:
- [ ] Butler tab shows contextual header (not chat stats)
- [ ] "Analyze now" button disabled when < 2 messages
- [ ] Empty state shows onboarding message
### Documentation Updates (per CLAUDE.md §8.3)
- Update `docs/TRUTH.md` if command count changes
- Update `wiki/log.md` with change record
- Update `wiki/module-status.md` if module status affected
## Constraint Compliance
| Constraint | Status |
|------------|--------|
| No new SaaS API endpoints | OK |
| No new SKILL.md files | OK |
| No new middleware | OK |
| No new stores | OK using component-local state |
| No new admin pages | OK |
| No new Tauri commands | OK Fix 2 extends existing `agent_get` command instead |
Note: Fix 2 originally proposed a new Tauri command but was revised to extend existing `agent_get` to comply with stabilization constraint.