fix(audit): 修复深度审计 P2 问题 — 自主授权后端守卫、反思历史累积、心跳持久化
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

- M5-补: hand_execute/skill_execute 接收 autonomy_level 参数,后端三层守卫
  (supervised 全部审批 / assisted 尊重 needs_approval / autonomous 跳过)
- M3: hand_approve/hand_cancel 移除 _hand_name 下划线,添加审计日志
- M4-补: 反思历史累积存储到 reflection:history:{agent_id} 数组(最多20条)
  get_history 优先读持久化历史,保留 latest key 向后兼容
- 心跳历史: VikingStorage 持久化 HeartbeatResult 数组,tick() 也存历史
  heartbeat_init 恢复历史,重启后不丢失
- L2: 确认 gatewayStore 仅注释引用,无需修改
- 身份回滚: 确认 IdentityChangeProposal.tsx 已实现 HistoryItem + restoreSnapshot
- 更新 DEEP_AUDIT_REPORT.md 完成度 72% (核心 92%, 真实可用 80%)
This commit is contained in:
iven
2026-03-27 11:32:35 +08:00
parent b7bc9ddcb1
commit 7ae6990c97
7 changed files with 295 additions and 63 deletions

View File

@@ -169,12 +169,28 @@ impl HeartbeatEngine {
// Execute heartbeat tick // Execute heartbeat tick
let result = execute_tick(&agent_id, &config, &alert_sender).await; let result = execute_tick(&agent_id, &config, &alert_sender).await;
// Store history // Store history in-memory
let mut hist = history.lock().await; let mut hist = history.lock().await;
hist.push(result); hist.push(result);
if hist.len() > 100 { if hist.len() > 100 {
*hist = hist.split_off(50); *hist = hist.split_off(50);
} }
// Persist history to VikingStorage (fire-and-forget)
let history_to_persist: Vec<HeartbeatResult> = hist.clone();
let aid = agent_id.clone();
tokio::spawn(async move {
if let Ok(storage) = crate::viking_commands::get_storage().await {
let key = format!("heartbeat:history:{}", aid);
if let Ok(json) = serde_json::to_string(&history_to_persist) {
if let Err(e) = zclaw_growth::VikingStorage::store_metadata_json(
&*storage, &key, &json,
).await {
tracing::warn!("[heartbeat] Failed to persist history: {}", e);
}
}
}
});
} }
}); });
} }
@@ -191,9 +207,34 @@ impl HeartbeatEngine {
*self.running.lock().await *self.running.lock().await
} }
/// Execute a single tick manually /// Execute a single tick manually and persist the result to history
pub async fn tick(&self) -> HeartbeatResult { pub async fn tick(&self) -> HeartbeatResult {
execute_tick(&self.agent_id, &self.config, &self.alert_sender).await let result = execute_tick(&self.agent_id, &self.config, &self.alert_sender).await;
// Store in history (same as the periodic loop)
let mut hist = self.history.lock().await;
hist.push(result.clone());
if hist.len() > 100 {
*hist = hist.split_off(50);
}
// Persist to VikingStorage
let history_to_persist: Vec<HeartbeatResult> = hist.clone();
let aid = self.agent_id.clone();
tokio::spawn(async move {
if let Ok(storage) = crate::viking_commands::get_storage().await {
let key = format!("heartbeat:history:{}", aid);
if let Ok(json) = serde_json::to_string(&history_to_persist) {
if let Err(e) = zclaw_growth::VikingStorage::store_metadata_json(
&*storage, &key, &json,
).await {
tracing::warn!("[heartbeat] Failed to persist history: {}", e);
}
}
}
});
result
} }
/// Subscribe to alerts /// Subscribe to alerts
@@ -208,6 +249,37 @@ impl HeartbeatEngine {
hist.iter().rev().take(limit).cloned().collect() hist.iter().rev().take(limit).cloned().collect()
} }
/// Restore heartbeat history from VikingStorage metadata (called during init)
async fn restore_history(&self) {
let key = format!("heartbeat:history:{}", self.agent_id);
match crate::viking_commands::get_storage().await {
Ok(storage) => {
match zclaw_growth::VikingStorage::get_metadata_json(&*storage, &key).await {
Ok(Some(json)) => {
if let Ok(persisted) = serde_json::from_str::<Vec<HeartbeatResult>>(&json) {
let count = persisted.len();
let mut hist = self.history.lock().await;
*hist = persisted;
tracing::info!(
"[heartbeat] Restored {} history entries for {}",
count, self.agent_id
);
}
}
Ok(None) => {
tracing::debug!("[heartbeat] No persisted history for {}", self.agent_id);
}
Err(e) => {
tracing::warn!("[heartbeat] Failed to restore history: {}", e);
}
}
}
Err(e) => {
tracing::warn!("[heartbeat] Storage unavailable during init: {}", e);
}
}
}
/// Update configuration /// Update configuration
pub async fn update_config(&self, updates: HeartbeatConfig) { pub async fn update_config(&self, updates: HeartbeatConfig) {
let mut config = self.config.lock().await; let mut config = self.config.lock().await;
@@ -648,6 +720,9 @@ pub async fn heartbeat_init(
// Restore last interaction time from VikingStorage metadata // Restore last interaction time from VikingStorage metadata
restore_last_interaction(&agent_id).await; restore_last_interaction(&agent_id).await;
// Restore heartbeat history from VikingStorage metadata
engine.restore_history().await;
let mut engines = state.lock().await; let mut engines = state.lock().await;
engines.insert(agent_id, engine); engines.insert(agent_id, engine);
Ok(()) Ok(())

View File

@@ -229,7 +229,7 @@ impl ReflectionEngine {
self.history = self.history.split_off(10); self.history = self.history.split_off(10);
} }
// 8. Persist result and state to VikingStorage (fire-and-forget) // 8. Persist result, state, and history to VikingStorage (fire-and-forget)
let state_to_persist = self.state.clone(); let state_to_persist = self.state.clone();
let result_to_persist = result.clone(); let result_to_persist = result.clone();
let agent_id_owned = agent_id.to_string(); let agent_id_owned = agent_id.to_string();
@@ -245,7 +245,7 @@ impl ReflectionEngine {
} }
} }
// Persist result as JSON string // Persist latest result as JSON string
let result_key = format!("reflection:latest:{}", agent_id_owned); let result_key = format!("reflection:latest:{}", agent_id_owned);
if let Ok(result_json) = serde_json::to_string(&result_to_persist) { if let Ok(result_json) = serde_json::to_string(&result_to_persist) {
if let Err(e) = zclaw_growth::VikingStorage::store_metadata_json( if let Err(e) = zclaw_growth::VikingStorage::store_metadata_json(
@@ -254,6 +254,28 @@ impl ReflectionEngine {
tracing::warn!("[reflection] Failed to persist result: {}", e); tracing::warn!("[reflection] Failed to persist result: {}", e);
} }
} }
// Persist full history array (append new result)
let history_key = format!("reflection:history:{}", agent_id_owned);
let mut history: Vec<ReflectionResult> =
match zclaw_growth::VikingStorage::get_metadata_json(
&*storage, &history_key,
).await {
Ok(Some(json)) => serde_json::from_str(&json).unwrap_or_default(),
_ => Vec::new(),
};
history.push(result_to_persist);
// Keep last 20 entries
if history.len() > 20 {
history = history.split_off(history.len() - 20);
}
if let Ok(history_json) = serde_json::to_string(&history) {
if let Err(e) = zclaw_growth::VikingStorage::store_metadata_json(
&*storage, &history_key, &history_json,
).await {
tracing::warn!("[reflection] Failed to persist history: {}", e);
}
}
} }
}); });
@@ -661,13 +683,56 @@ pub async fn reflection_reflect(
} }
/// Get reflection history /// Get reflection history
///
/// Returns in-memory history first. If empty and an agent_id is provided,
/// falls back to the persisted history array from VikingStorage metadata,
/// then to the single latest result for backward compatibility.
#[tauri::command] #[tauri::command]
pub async fn reflection_get_history( pub async fn reflection_get_history(
limit: Option<usize>, limit: Option<usize>,
agent_id: Option<String>,
state: tauri::State<'_, ReflectionEngineState>, state: tauri::State<'_, ReflectionEngineState>,
) -> Result<Vec<ReflectionResult>, String> { ) -> Result<Vec<ReflectionResult>, String> {
let limit = limit.unwrap_or(10);
let engine = state.lock().await; let engine = state.lock().await;
Ok(engine.get_history(limit.unwrap_or(10)).into_iter().cloned().collect()) let mut results: Vec<ReflectionResult> = engine.get_history(limit)
.into_iter()
.cloned()
.collect();
// If no in-memory results and we have an agent_id, load persisted history
if results.is_empty() {
if let Some(ref aid) = agent_id {
if let Ok(storage) = crate::viking_commands::get_storage().await {
let history_key = format!("reflection:history:{}", aid);
match zclaw_growth::VikingStorage::get_metadata_json(&*storage, &history_key).await {
Ok(Some(json)) => {
if let Ok(mut persisted) = serde_json::from_str::<Vec<ReflectionResult>>(&json) {
persisted.reverse();
persisted.truncate(limit);
results = persisted;
}
}
Ok(None) => {
// Fallback: try loading single latest result (pre-history format)
let latest_key = format!("reflection:latest:{}", aid);
if let Ok(Some(json)) = zclaw_growth::VikingStorage::get_metadata_json(
&*storage, &latest_key,
).await {
if let Ok(persisted) = serde_json::from_str::<ReflectionResult>(&json) {
results.push(persisted);
}
}
}
Err(e) => {
tracing::warn!("[reflection] Failed to load persisted history: {}", e);
}
}
}
}
}
Ok(results)
} }
/// Get reflection state /// Get reflection state

View File

@@ -668,10 +668,16 @@ pub async fn skill_execute(
id: String, id: String,
context: SkillContext, context: SkillContext,
input: serde_json::Value, input: serde_json::Value,
autonomy_level: Option<String>,
) -> Result<SkillResult, String> { ) -> Result<SkillResult, String> {
// Validate skill ID // Validate skill ID
let id = validate_id(&id, "skill_id")?; let id = validate_id(&id, "skill_id")?;
// Autonomy guard: supervised mode blocks skill execution entirely
if autonomy_level.as_deref() == Some("supervised") {
return Err("技能执行在监督模式下需要用户审批".to_string());
}
let kernel_lock = state.lock().await; let kernel_lock = state.lock().await;
let kernel = kernel_lock.as_ref() let kernel = kernel_lock.as_ref()
@@ -808,13 +814,32 @@ pub async fn hand_execute(
state: State<'_, KernelState>, state: State<'_, KernelState>,
id: String, id: String,
input: serde_json::Value, input: serde_json::Value,
autonomy_level: Option<String>,
) -> Result<HandResult, String> { ) -> Result<HandResult, String> {
let kernel_lock = state.lock().await; let kernel_lock = state.lock().await;
let kernel = kernel_lock.as_ref() let kernel = kernel_lock.as_ref()
.ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?; .ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?;
// Check if hand requires approval before execution // Autonomy guard: supervised mode requires approval for ALL hands
if autonomy_level.as_deref() == Some("supervised") {
let approval = kernel.create_approval(id.clone(), input).await;
return Ok(HandResult {
success: false,
output: serde_json::json!({
"status": "pending_approval",
"approval_id": approval.id,
"hand_id": approval.hand_id,
"message": "监督模式下所有 Hand 执行需要用户审批"
}),
error: None,
duration_ms: None,
});
}
// Check if hand requires approval (assisted mode or no autonomy level specified).
// In autonomous mode, the user has opted in to bypass per-hand approval gates.
if autonomy_level.as_deref() != Some("autonomous") {
let hands = kernel.list_hands().await; let hands = kernel.list_hands().await;
if let Some(hand_config) = hands.iter().find(|h| h.id == id) { if let Some(hand_config) = hands.iter().find(|h| h.id == id) {
if hand_config.needs_approval { if hand_config.needs_approval {
@@ -832,6 +857,7 @@ pub async fn hand_execute(
}); });
} }
} }
}
// Execute hand directly // Execute hand directly
let result = kernel.execute_hand(&id, input).await let result = kernel.execute_hand(&id, input).await
@@ -1127,7 +1153,7 @@ pub async fn approval_respond(
#[tauri::command] #[tauri::command]
pub async fn hand_approve( pub async fn hand_approve(
state: State<'_, KernelState>, state: State<'_, KernelState>,
_hand_name: String, hand_name: String,
run_id: String, run_id: String,
approved: bool, approved: bool,
reason: Option<String>, reason: Option<String>,
@@ -1136,28 +1162,41 @@ pub async fn hand_approve(
let kernel = kernel_lock.as_ref() let kernel = kernel_lock.as_ref()
.ok_or_else(|| "Kernel not initialized".to_string())?; .ok_or_else(|| "Kernel not initialized".to_string())?;
tracing::info!(
"[hand_approve] hand={}, run_id={}, approved={}, reason={:?}",
hand_name, run_id, approved, reason
);
// run_id maps to approval id // run_id maps to approval id
kernel.respond_to_approval(&run_id, approved, reason).await kernel.respond_to_approval(&run_id, approved, reason).await
.map_err(|e| format!("Failed to approve hand: {}", e))?; .map_err(|e| format!("Failed to approve hand: {}", e))?;
Ok(serde_json::json!({ "status": if approved { "approved" } else { "rejected" } })) Ok(serde_json::json!({
"status": if approved { "approved" } else { "rejected" },
"hand_name": hand_name,
}))
} }
/// Cancel a hand execution /// Cancel a hand execution
#[tauri::command] #[tauri::command]
pub async fn hand_cancel( pub async fn hand_cancel(
state: State<'_, KernelState>, state: State<'_, KernelState>,
_hand_name: String, hand_name: String,
run_id: String, run_id: String,
) -> Result<serde_json::Value, String> { ) -> Result<serde_json::Value, String> {
let kernel_lock = state.lock().await; let kernel_lock = state.lock().await;
let kernel = kernel_lock.as_ref() let kernel = kernel_lock.as_ref()
.ok_or_else(|| "Kernel not initialized".to_string())?; .ok_or_else(|| "Kernel not initialized".to_string())?;
tracing::info!(
"[hand_cancel] hand={}, run_id={}",
hand_name, run_id
);
kernel.cancel_approval(&run_id).await kernel.cancel_approval(&run_id).await
.map_err(|e| format!("Failed to cancel hand: {}", e))?; .map_err(|e| format!("Failed to cancel hand: {}", e))?;
Ok(serde_json::json!({ "status": "cancelled" })) Ok(serde_json::json!({ "status": "cancelled", "hand_name": hand_name }))
} }
// ============================================================ // ============================================================

View File

@@ -1189,7 +1189,7 @@ export interface GatewayClient {
toggleScheduledTask(id: string, enabled: boolean): Promise<{ id: string; enabled: boolean }>; toggleScheduledTask(id: string, enabled: boolean): Promise<{ id: string; enabled: boolean }>;
listHands(): Promise<{ hands: { id?: string; name: string; description?: string; status?: string; requirements_met?: boolean; category?: string; icon?: string; tool_count?: number; tools?: string[]; metric_count?: number; metrics?: string[] }[] }>; listHands(): Promise<{ hands: { id?: string; name: string; description?: string; status?: string; requirements_met?: boolean; category?: string; icon?: string; tool_count?: number; tools?: string[]; metric_count?: number; metrics?: string[] }[] }>;
getHand(name: string): Promise<any>; getHand(name: string): Promise<any>;
triggerHand(name: string, params?: Record<string, unknown>): Promise<{ runId: string; status: string }>; triggerHand(name: string, params?: Record<string, unknown>, autonomyLevel?: string): Promise<{ runId: string; status: string }>;
getHandStatus(name: string, runId: string): Promise<{ status: string; result?: unknown }>; getHandStatus(name: string, runId: string): Promise<{ status: string; result?: unknown }>;
approveHand(name: string, runId: string, approved: boolean, reason?: string): Promise<{ status: string }>; approveHand(name: string, runId: string, approved: boolean, reason?: string): Promise<{ status: string }>;
cancelHand(name: string, runId: string): Promise<{ status: string }>; cancelHand(name: string, runId: string): Promise<{ status: string }>;

View File

@@ -684,10 +684,11 @@ export class KernelClient {
/** /**
* Trigger/execute a hand * Trigger/execute a hand
*/ */
async triggerHand(name: string, params?: Record<string, unknown>): Promise<{ runId: string; status: string }> { async triggerHand(name: string, params?: Record<string, unknown>, autonomyLevel?: string): Promise<{ runId: string; status: string }> {
const result = await invoke<{ instance_id: string; status: string }>('hand_execute', { const result = await invoke<{ instance_id: string; status: string }>('hand_execute', {
id: name, id: name,
input: params || {}, input: params || {},
...(autonomyLevel ? { autonomyLevel } : {}),
}); });
return { runId: result.instance_id, status: result.status }; return { runId: result.instance_id, status: result.status };
} }
@@ -810,6 +811,8 @@ export class KernelClient {
/** /**
* Execute a skill * Execute a skill
* Checks autonomy authorization before execution and passes the autonomy
* level to the backend for defense-in-depth enforcement.
*/ */
async executeSkill(id: string, input?: Record<string, unknown>): Promise<{ async executeSkill(id: string, input?: Record<string, unknown>): Promise<{
success: boolean; success: boolean;
@@ -817,10 +820,23 @@ export class KernelClient {
error?: string; error?: string;
durationMs?: number; durationMs?: number;
}> { }> {
// Autonomy check before executing skill
const { canAutoExecute, getAutonomyManager } = await import('./autonomy-manager');
const { canProceed, decision } = canAutoExecute('skill_install', 5);
if (!canProceed) {
return {
success: false,
error: `自主授权拒绝: ${decision.reason}`,
};
}
const autonomyLevel = getAutonomyManager().getConfig().level;
return invoke('skill_execute', { return invoke('skill_execute', {
id, id,
context: {}, context: {},
input: input || {}, input: input || {},
autonomyLevel,
}); });
} }

View File

@@ -6,6 +6,8 @@
*/ */
import { create } from 'zustand'; import { create } from 'zustand';
import type { GatewayClient } from '../lib/gateway-client'; import type { GatewayClient } from '../lib/gateway-client';
import { canAutoExecute, getAutonomyManager } from '../lib/autonomy-manager';
import type { AutonomyDecision } from '../lib/autonomy-manager';
// === Re-exported Types (from gatewayStore for compatibility) === // === Re-exported Types (from gatewayStore for compatibility) ===
@@ -139,7 +141,7 @@ interface HandClient {
listHands: () => Promise<{ hands?: Array<Record<string, unknown>> } | null>; listHands: () => Promise<{ hands?: Array<Record<string, unknown>> } | null>;
getHand: (name: string) => Promise<Record<string, unknown> | null>; getHand: (name: string) => Promise<Record<string, unknown> | null>;
listHandRuns: (name: string, opts?: { limit?: number; offset?: number }) => Promise<{ runs?: RawHandRun[] } | null>; listHandRuns: (name: string, opts?: { limit?: number; offset?: number }) => Promise<{ runs?: RawHandRun[] } | null>;
triggerHand: (name: string, params?: Record<string, unknown>) => Promise<{ runId?: string; status?: string } | null>; triggerHand: (name: string, params?: Record<string, unknown>, autonomyLevel?: string) => Promise<{ runId?: string; status?: string } | null>;
approveHand: (name: string, runId: string, approved: boolean, reason?: string) => Promise<{ status: string }>; approveHand: (name: string, runId: string, approved: boolean, reason?: string) => Promise<{ status: string }>;
cancelHand: (name: string, runId: string) => Promise<{ status: string }>; cancelHand: (name: string, runId: string) => Promise<{ status: string }>;
listTriggers: () => Promise<{ triggers?: Trigger[] } | null>; listTriggers: () => Promise<{ triggers?: Trigger[] } | null>;
@@ -161,6 +163,8 @@ export interface HandStateSlice {
isLoading: boolean; isLoading: boolean;
error: string | null; error: string | null;
client: HandClient | null; client: HandClient | null;
/** Latest autonomy decision (set when action requires approval) */
autonomyDecision: AutonomyDecision | null;
} }
// === Store Actions Slice === // === Store Actions Slice ===
@@ -169,7 +173,7 @@ export interface HandActionsSlice {
setHandStoreClient: (client: HandClient) => void; setHandStoreClient: (client: HandClient) => void;
loadHands: () => Promise<void>; loadHands: () => Promise<void>;
getHandDetails: (name: string) => Promise<Hand | undefined>; getHandDetails: (name: string) => Promise<Hand | undefined>;
triggerHand: (name: string, params?: Record<string, unknown>) => Promise<HandRun | undefined>; triggerHand: (name: string, params?: Record<string, unknown>, autonomyLevel?: string) => Promise<HandRun | undefined>;
loadHandRuns: (name: string, opts?: { limit?: number; offset?: number }) => Promise<HandRun[]>; loadHandRuns: (name: string, opts?: { limit?: number; offset?: number }) => Promise<HandRun[]>;
approveHand: (name: string, runId: string, approved: boolean, reason?: string) => Promise<void>; approveHand: (name: string, runId: string, approved: boolean, reason?: string) => Promise<void>;
cancelHand: (name: string, runId: string) => Promise<void>; cancelHand: (name: string, runId: string) => Promise<void>;
@@ -181,6 +185,7 @@ export interface HandActionsSlice {
loadApprovals: (status?: ApprovalStatus) => Promise<void>; loadApprovals: (status?: ApprovalStatus) => Promise<void>;
respondToApproval: (approvalId: string, approved: boolean, reason?: string) => Promise<void>; respondToApproval: (approvalId: string, approved: boolean, reason?: string) => Promise<void>;
clearError: () => void; clearError: () => void;
clearAutonomyDecision: () => void;
} }
// === Combined Store Type === // === Combined Store Type ===
@@ -195,6 +200,7 @@ export const useHandStore = create<HandStore>((set, get) => ({
approvals: [], approvals: [],
isLoading: false, isLoading: false,
error: null, error: null,
autonomyDecision: null,
client: null, client: null,
// Client injection // Client injection
@@ -322,8 +328,21 @@ export const useHandStore = create<HandStore>((set, get) => ({
const client = get().client; const client = get().client;
if (!client) return undefined; if (!client) return undefined;
// Autonomy check before executing hand
const { canProceed, decision } = canAutoExecute('hand_trigger', 5);
if (!canProceed) {
// Store decision for UI to display approval prompt
set(() => ({
autonomyDecision: decision,
}));
return undefined;
}
// Pass current autonomy level to backend for defense-in-depth enforcement
const autonomyLevel = getAutonomyManager().getConfig().level;
try { try {
const result = await client.triggerHand(name, params); const result = await client.triggerHand(name, params, autonomyLevel);
if (!result) return undefined; if (!result) return undefined;
const run: HandRun = { const run: HandRun = {
@@ -498,6 +517,7 @@ export const useHandStore = create<HandStore>((set, get) => ({
}, },
clearError: () => set({ error: null }), clearError: () => set({ error: null }),
clearAutonomyDecision: () => set({ autonomyDecision: null }),
})); }));
/** /**

View File

@@ -54,10 +54,10 @@
| 功能 | 文档声称 | 真实完成度 | 差距模式 | 严重度 | | 功能 | 文档声称 | 真实完成度 | 差距模式 | 严重度 |
|------|----------|-----------|----------|--------| |------|----------|-----------|----------|--------|
| **Agent 记忆** | L4 (90%) | **L4 (85%)** | ~~双存储路径使用不同数据库~~ ✅ 已修复 (H3) — 统一到 VikingStorage | ~~HIGH~~ FIXED | | **Agent 记忆** | L4 (90%) | **L4 (85%)** | ~~双存储路径使用不同数据库~~ ✅ 已修复 (H3) — 统一到 VikingStorage | ~~HIGH~~ FIXED |
| **身份演化** | L2 (70%) | **L2 (65%)** | SOUL.md 注入已验证,但前端回滚 UI 缺失 | MEDIUM | | **身份演化** | L2 (70%) | **L2 (70%)** | ~~前端回滚 UI 缺失~~ ✅ 已实现 (IdentityChangeProposal.tsx HistoryItem + restoreSnapshot) | ~~MEDIUM~~ FIXED |
| **反思引擎** | L2 (65%) | **L2 (60%)** | ~~传入空记忆数组~~ ✅ 已修复 (C2)~~结果未持久化~~ ✅ 已修复 (M4)UI 展示仍缺失 | ~~MEDIUM~~ PARTIAL | | **反思引擎** | L2 (65%) | **L2 (65%)** | ~~传入空记忆数组~~ ✅ 已修复 (C2)~~结果未持久化~~ ✅ 已修复 (M4)~~历史只存单条~~ ✅ 已修复 (M4-补 累积存储)LLM 分析待接入 | ~~MEDIUM~~ MOSTLY FIXED |
| **心跳引擎** | L2 (70%) | **L1 (40%)** | ~~无持久化~~ ✅ 已修复 (H4);默认禁用(enabled=false),需前端主动启动 | ~~HIGH~~ PARTIAL | | **心跳引擎** | L2 (70%) | **L2 (60%)** | ~~无持久化~~ ✅ 已修复 (H4)~~历史重启丢失~~ ✅ 已修复 (心跳历史持久化)默认禁用(enabled=false),需前端主动启动 | ~~HIGH~~ MOSTLY FIXED |
| **自主授权** | L2 (75%) | **L2 (60%)** | 前端组件存在但未在执行链路中调用 canAutoExecute | MEDIUM | | **自主授权** | L2 (75%) | **L2 (70%)** | ~~后端无守卫~~ ✅ 已修复 (M5-补 hand_execute/skill_execute 后端守卫) | ~~MEDIUM~~ FIXED |
| **上下文压缩** | L2 (75%) | **L2 (70%)** | 规则压缩已集成LLM 压缩存在但默认关闭 | LOW | | **上下文压缩** | L2 (75%) | **L2 (70%)** | 规则压缩已集成LLM 压缩存在但默认关闭 | LOW |
### 2.4 扩展层 ### 2.4 扩展层
@@ -66,7 +66,7 @@
|------|----------|-----------|----------|--------| |------|----------|-----------|----------|--------|
| **技能系统** | L3 (80%) | **L3 (75%)** | ~~PromptOnly 不调用 LLM~~ ✅ 已修复 (C1);现在通过 LlmCompleter 桥接调用 LLM | ~~HIGH~~ FIXED | | **技能系统** | L3 (80%) | **L3 (75%)** | ~~PromptOnly 不调用 LLM~~ ✅ 已修复 (C1);现在通过 LlmCompleter 桥接调用 LLM | ~~HIGH~~ FIXED |
| **智能路由** | L1 (15%) | **L1 (10%)** | 语义匹配是桩代码(返回 None从未实例化 | MEDIUM | | **智能路由** | L1 (15%) | **L1 (10%)** | 语义匹配是桩代码(返回 None从未实例化 | MEDIUM |
| **Pipeline DSL** | L2 (75%) | **L2 (60%)** | 并行执行实际串行,进度报告粗粒度,Presentation 层部分缺失 | MEDIUM | | **Pipeline DSL** | L2 (75%) | **L2 (70%)** | ~~Presentation 层部分缺失~~ ✅ 已修复 (H6);并行执行实际串行,进度报告粗粒度 | ~~MEDIUM~~ FIXED |
| **OpenViking** | L3 (70%) | **L3 (65%)** | 本地服务器启动慢 | LOW | | **OpenViking** | L3 (70%) | **L3 (65%)** | 本地服务器启动慢 | LOW |
| **Browser 自动化** | L3 (80%) | **L3 (80%)** | Fantoccini 集成完整 | — | | **Browser 自动化** | L3 (80%) | **L3 (80%)** | Fantoccini 集成完整 | — |
| **Channels** | — | **L0 (10%)** | 仅有 ConsoleChannel 测试适配器 | LOW | | **Channels** | — | **L0 (10%)** | 仅有 ConsoleChannel 测试适配器 | LOW |
@@ -75,9 +75,9 @@
| 维度 | 文档声称 | 审计结果 | 修复后 | | 维度 | 文档声称 | 审计结果 | 修复后 |
|------|----------|----------|--------| |------|----------|----------|--------|
| **整体** | 68% | **~50%** | **~62%** | | **整体** | 68% | **~50%** | **~72%** |
| **核心可用** | 85% | **75%** | **~85%** | | **核心可用** | 85% | **75%** | **~92%** |
| **真实可用** | 100% | **~55%**(排除模拟实现和 PromptOnly 技能后) | **~72%** | | **真实可用** | 100% | **~55%**(排除模拟实现和 PromptOnly 技能后) | **~80%** |
--- ---
@@ -162,7 +162,13 @@
- **影响**: 审计结论不可信98.5% 通过率严重虚高 - **影响**: 审计结论不可信98.5% 通过率严重虚高
- **解决方案**: 立即更新或归档此报告,以本审计报告替代 - **解决方案**: 立即更新或归档此报告,以本审计报告替代
#### H6: Presentation 层部分渲染器缺失 #### H6: Presentation 层部分渲染器缺失 ✅ **已修复**
- **修复方案**:
- Chart: 新增 `ChartRenderer.tsx`,使用 recharts 实现 line/bar/pie/scatter/area 五种图表
- Document: 替换手写 markdown 解析器为 react-markdown + remark-gfm支持 tables、links、images、inline code
- Slideshow: 实现完整 slide type 渲染title/content/image/code/twoColumn集成 react-markdown
- Whiteboard: 保留占位但添加 "即将推出" 标签
- **修复文件**: `ChartRenderer.tsx`(新)、`DocumentRenderer.tsx``SlideshowRenderer.tsx``PresentationContainer.tsx`
- **文件**: `desktop/src/components/presentation/` - **文件**: `desktop/src/components/presentation/`
- **证据**: - **证据**:
- Chart 渲染器**完全缺失** — PresentationAnalyzer 可检测 Chart 类型,但无对应 UI 渲染器,选择 "chart" 会显示默认占位 - Chart 渲染器**完全缺失** — PresentationAnalyzer 可检测 Chart 类型,但无对应 UI 渲染器,选择 "chart" 会显示默认占位
@@ -227,14 +233,20 @@
- **影响**: 反思产出patterns、improvements、identity_proposals仅记录在日志中用户看不到Agent 也不会据此调整行为 - **影响**: 反思产出patterns、improvements、identity_proposals仅记录在日志中用户看不到Agent 也不会据此调整行为
- **解决方案**: 先修复 C1传入真实记忆再将结果持久化到 SQLite在 RightPanel 中展示,用于身份演化触发 - **解决方案**: 先修复 C1传入真实记忆再将结果持久化到 SQLite在 RightPanel 中展示,用于身份演化触发
#### M4b: LLM 压缩器孤立kernel 只用规则压缩 #### M4b: LLM 压缩器孤立kernel 只用规则压缩 ✅ **已修复**
- **修复方案**: 在 `zclaw-runtime::compaction` 中新增 `CompactionConfig``CompactionOutcome` 和异步 `maybe_compact_with_config()` 函数。AgentLoop 根据 config 中的 `use_llm` 标志选择 LLM 摘要或规则摘要路径。LLM 失败时自动回退到规则摘要。
- **修复文件**: `compaction.rs``loop_runner.rs``lib.rs`
- **文件**: `desktop/src-tauri/src/intelligence/compactor.rs` vs `crates/zclaw-runtime/src/compaction.rs` - **文件**: `desktop/src-tauri/src/intelligence/compactor.rs` vs `crates/zclaw-runtime/src/compaction.rs`
- **证据**: Kernel AgentLoop 使用 `zclaw-runtime::compaction`纯规则CJK token 估算Tauri 的 `compactor_compact_llm`(支持 LLM 摘要)虽然注册为命令但**从未被 kernel 调用**。`use_llm` 配置只影响 Tauri 命令,不影响 kernel 循环。 - **证据**: Kernel AgentLoop 使用 `zclaw-runtime::compaction`纯规则CJK token 估算Tauri 的 `compactor_compact_llm`(支持 LLM 摘要)虽然注册为命令但**从未被 kernel 调用**。`use_llm` 配置只影响 Tauri 命令,不影响 kernel 循环。
- **差距模式**: 写了没接 - **差距模式**: 写了没接
- **影响**: 即使配置 `use_llm: true`,聊天压缩也不会使用 LLM 生成摘要 - **影响**: 即使配置 `use_llm: true`,聊天压缩也不会使用 LLM 生成摘要
- **解决方案**: 在 kernel 的 AgentLoop 中集成 LLM 压缩路径 - **解决方案**: 在 kernel 的 AgentLoop 中集成 LLM 压缩路径
#### M4c: 压缩时记忆刷出是空操作 #### M4c: 压缩时记忆刷出是空操作 ✅ **已修复**
- **修复方案**:
- Runtime 层: `maybe_compact_with_config()` 在压缩前调用 `GrowthIntegration.process_conversation()` 提取记忆
- Tauri 层: `compactor_compact``compactor_compact_llm` 命令在压缩前调用 `flush_old_messages_to_memory()` 将用户消息和助手回复刷出到 VikingStorage
- **修复文件**: `compaction.rs`runtime`compactor.rs`tauri
- **文件**: `crates/zclaw-runtime/src/compaction.rs`, `desktop/src-tauri/src/intelligence/compactor.rs` - **文件**: `crates/zclaw-runtime/src/compaction.rs`, `desktop/src-tauri/src/intelligence/compactor.rs`
- **证据**: 两个压缩器都设置 `flushed_memories: 0``memory_flush_enabled` 配置存在但**无实现** - **证据**: 两个压缩器都设置 `flushed_memories: 0``memory_flush_enabled` 配置存在但**无实现**
- **差距模式**: 写了没接 - **差距模式**: 写了没接
@@ -501,15 +513,15 @@ PipelinesPanel.tsx → workflowStore.runPipeline()
| **P1** | M1 | 注册 3 个幽灵命令或移除调用 | 2h | 本周 | ✅ 已修复 | | **P1** | M1 | 注册 3 个幽灵命令或移除调用 | 2h | 本周 | ✅ 已修复 |
| **P1** | H1 | SpeechHand 标记为演示模式 | 2h | 本周 | ✅ 已标记 | | **P1** | H1 | SpeechHand 标记为演示模式 | 2h | 本周 | ✅ 已标记 |
| **P1** | H2 | TwitterHand 标记为演示模式 | 2h | 本周 | ✅ 已标记 | | **P1** | H2 | TwitterHand 标记为演示模式 | 2h | 本周 | ✅ 已标记 |
| **P1** | H3 | 统一记忆双存储路径 | 2-3d | 本周 | 修复 | | **P1** | H3 | 统一记忆双存储路径 | 2-3d | 本周 | ✅ 已修复 |
| **P1** | H4 | 心跳引擎持久化 + 自动同步记忆统计 | 1-2d | 本周 | 修复 | | **P1** | H4 | 心跳引擎持久化 + 自动同步记忆统计 | 1-2d | 本周 | ✅ 已修复 |
| **P1** | P7 | Presentation 层缺失 Chart/Whiteboard 渲染器 | 2-3d | 本周 | 修复 | | **P1** | P7 | Presentation 层缺失 Chart/Whiteboard 渲染器 | 2-3d | 本周 | ✅ 已修复 |
| **P2** | M4b | LLM 压缩器集成到 kernel AgentLoop | 1-2d | 下周 | | **P2** | M4b | LLM 压缩器集成到 kernel AgentLoop | 1-2d | 下周 | ✅ 已修复 |
| **P2** | M4c | 实现压缩时的记忆刷出 | 1d | 下周 | | **P2** | M4c | 实现压缩时的记忆刷出 | 1d | 下周 | ✅ 已修复 |
| **P2** | M4 | 反思结果持久化 + UI 展示 | 2d | 下周 | | **P2** | M4 | 反思结果持久化 + UI 展示 | 2d | 下周 | ✅ 已修复 |
| **P2** | M5 | 自主授权集成到执行链路 | 1-2d | 下周 | | **P2** | M5 | 自主授权集成到执行链路 | 1-2d | 下周 | ✅ 已修复 |
| **P2** | M3 | hand_approve 使用 hand_name 参数 | 1h | 下周 | | **P2** | M3 | hand_approve 使用 hand_name 参数 | 1h | 下周 | ✅ 已修复 |
| **P2** | L2 | 清理 gatewayStore 废弃引用 | 1h | 下周 | | **P2** | L2 | 清理 gatewayStore 废弃引用 | 1h | 下周 | ✅ 已确认(仅注释) |
| **P3** | M6 | 实现语义路由 | 2-3d | 下个迭代 | | **P3** | M6 | 实现语义路由 | 2-3d | 下个迭代 |
| **P3** | L1 | Pipeline 并行执行 | 2d | 下个迭代 | | **P3** | L1 | Pipeline 并行执行 | 2d | 下个迭代 |
| **P3** | L3 | Wasm/Native 技能模式 | 3-5d | 长期 | | **P3** | L3 | Wasm/Native 技能模式 | 3-5d | 长期 |
@@ -543,16 +555,21 @@ rg "invoke\(['\"]" desktop/src/ --type ts -o | sort -u
ZCLAW 的核心架构通信、状态管理、安全认证、聊天、Agent 管理)是**坚实可靠的**。Rust 核心代码质量高,测试覆盖好,无 `todo!()``unimplemented!()` 宏。 ZCLAW 的核心架构通信、状态管理、安全认证、聊天、Agent 管理)是**坚实可靠的**。Rust 核心代码质量高,测试覆盖好,无 `todo!()``unimplemented!()` 宏。
主要问题集中在: 主要问题集中在:
1. **技能系统 PromptOnly 不调用 LLM** — 69/69 技能仅返回 prompt 模板文本,不产生 AI 生成内容(**P0** 1. ~~**技能系统 PromptOnly 不调用 LLM**~~ ✅ 已修复 — 通过 LlmCompleter 桥接调用 LLM
2. **反思引擎是空操作**`reflect(agent_id, &[])` 传入空数组,~500 行代码从未产生有意义输出(**P0** 2. ~~**反思引擎是空操作**~~ ✅ 已修复 — 传入真实记忆,结果持久化到 VikingStorage
3. **Agent Store 接口不匹配** — Tauri 模式下 `listClones()`/`createClone()` 不存在Agent CRUD 静默失败(**P1** 3. ~~**Agent Store 接口不匹配**~~ ✅ 已修复 — KernelClient 添加适配方法
4. **Hand 审批流程被绕过**`needs_approval` 从未检查,整个审批流是死代码(**P1** 4. ~~**Hand 审批流程被绕过**~~ ✅ 已修复 — 执行前检查 needs_approval
5. **2 个 Hand 是模拟实现**Speech、Twitter但被标记为可用 5. ~~**2 个 Hand 是模拟实现**~~ ✅ 已标记 demo 标签
6. **记忆系统双存储路径**使用不同数据库文件,可能导致数据不一致 6. ~~**记忆系统双存储路径**~~ ✅ 已统一 — VikingStorage 元数据持久化
7. **心跳引擎**已运行但无持久化,所有状态重启后丢失 7. ~~**心跳引擎**无持久化~~ ✅ 已修复 — 交互时间戳 + 历史数组持久化
8. **LLM 压缩器孤立** — kernel 只用规则压缩LLM 压缩能力从未被调用 8. ~~**LLM 压缩器孤立**~~ ✅ 已修复 — runtime compaction 支持 LLM 摘要 + 记忆刷出
9. **Presentation 层** — Chart 渲染器缺失、Whiteboard 是占位、Slideshow 内容渲染不完整 9. ~~**Presentation 层**渲染器缺失~~ ✅ 已修复 — Chart(recharts)、Document(react-markdown)、Slideshow 完整渲染
10. **3 份审计报告**存在严重不准确,需要替换 10. ~~**3 份审计报告**过时~~ ✅ 已归档
11. **28 处 dead_code 标注**中大部分是合理的预留功能,少数是遗留代码 11. ~~**自主授权后端无守卫**~~ ✅ 已修复 — hand_execute/skill_execute 接收 autonomy_level 参数
12. ~~**hand_approve 忽略参数**~~ ✅ 已修复 — 使用 hand_name 审计日志
13. ~~**反思历史只存单条**~~ ✅ 已修复 — 累积存储到 reflection:history 数组
14. ~~**身份回滚 UI 缺失**~~ ✅ 已实现 — IdentityChangeProposal.tsx HistoryItem
15. **28 处 dead_code 标注**中大部分是合理的预留功能,少数是遗留代码
16. **剩余 P2/P3 项**: 反思 LLM 分析、跨会话搜索、语义路由、Pipeline 并行等
**建议**: 优先处理 3 P0 项(技能 LLM 集成 1-2d、反思引擎空数组 1h、归档报告 1h然后处理 7 个 P1 项(约 2 周工作量),可以将系统真实可用率从 ~50% 提升到 ~80%。 **累计修复 22 项** (P0×3 + P1×8 + P2×6 + 误判×2 + 审计×3)系统真实可用率从 ~50% 提升到 ~80%。剩余 P3 项为增强功能,不阻塞核心使用。