Compare commits
3 Commits
13a40dbbf5
...
b84a503500
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b84a503500 | ||
|
|
fb0b8d2af3 | ||
|
|
82842c4258 |
@@ -54,7 +54,7 @@ ZCLAW/
|
||||
│ ├── zclaw-memory/ # L2: 存储层 (SQLite, KV, 会话管理)
|
||||
│ ├── zclaw-runtime/ # L3: 运行时 (4 Driver, 7 工具, 11 层中间件)
|
||||
│ ├── zclaw-kernel/ # L4: 核心协调 (171 Tauri 命令)
|
||||
│ ├── zclaw-skills/ # 技能系统 (76 SKILL.md 解析, 语义路由)
|
||||
│ ├── zclaw-skills/ # 技能系统 (75 SKILL.md 解析, 语义路由)
|
||||
│ ├── zclaw-hands/ # 自主能力 (9 启用, 155 Rust 测试)
|
||||
│ ├── zclaw-protocols/ # 协议支持 (MCP 完整, A2A feature-gated)
|
||||
│ ├── zclaw-pipeline/ # Pipeline DSL (v1/v2, 10 行业模板)
|
||||
|
||||
@@ -113,8 +113,8 @@ pub async fn classroom_chat(
|
||||
Ok(agent_responses)
|
||||
}
|
||||
|
||||
/// @reserved — no frontend UI yet
|
||||
/// Retrieve chat history for a classroom
|
||||
// @reserved: no frontend consumer yet
|
||||
#[tauri::command]
|
||||
pub async fn classroom_chat_history(
|
||||
chat_store: State<'_, ChatStore>,
|
||||
|
||||
@@ -239,8 +239,8 @@ pub async fn classroom_generate(
|
||||
})
|
||||
}
|
||||
|
||||
/// @reserved — no frontend UI yet
|
||||
/// Get current generation progress for a topic
|
||||
// @reserved: 暂无前端集成
|
||||
#[tauri::command]
|
||||
pub async fn classroom_generation_progress(
|
||||
tasks: State<'_, GenerationTasks>,
|
||||
@@ -281,8 +281,8 @@ pub async fn classroom_get(
|
||||
.ok_or_else(|| format!("Classroom '{}' not found", classroom_id))
|
||||
}
|
||||
|
||||
/// @reserved — no frontend UI yet
|
||||
/// List all generated classrooms (id + title only)
|
||||
// @reserved: 暂无前端集成
|
||||
#[tauri::command]
|
||||
pub async fn classroom_list(
|
||||
store: State<'_, ClassroomStore>,
|
||||
|
||||
@@ -113,8 +113,8 @@ pub fn zclaw_approve_device_pairing(
|
||||
approve_local_device_pairing(&app, &device_id, &public_key_base64, url.as_deref())
|
||||
}
|
||||
|
||||
/// @reserved — no frontend UI yet
|
||||
/// Run ZCLAW doctor to diagnose issues
|
||||
// @connected
|
||||
#[tauri::command]
|
||||
pub fn zclaw_doctor(app: AppHandle) -> Result<String, String> {
|
||||
let result = run_zclaw(&app, &["doctor", "--json"])?;
|
||||
|
||||
@@ -266,8 +266,8 @@ pub fn zclaw_health_check(
|
||||
})
|
||||
}
|
||||
|
||||
/// @reserved — no frontend UI yet
|
||||
/// Quick ping to check if ZCLAW is alive (lightweight check)
|
||||
// @reserved: 暂无前端集成
|
||||
#[tauri::command]
|
||||
pub fn zclaw_ping(app: AppHandle) -> Result<bool, String> {
|
||||
let port_check = check_port_accessibility("127.0.0.1", ZCLAW_DEFAULT_PORT, 1000);
|
||||
|
||||
@@ -231,8 +231,8 @@ pub async fn agent_update(
|
||||
.ok_or_else(|| format!("Agent not found after update: {}", agent_id))
|
||||
}
|
||||
|
||||
/// @reserved — no frontend UI yet
|
||||
/// Export an agent configuration as JSON
|
||||
// @reserved: 暂无前端集成
|
||||
#[tauri::command]
|
||||
pub async fn agent_export(
|
||||
state: State<'_, KernelState>,
|
||||
@@ -254,8 +254,8 @@ pub async fn agent_export(
|
||||
.map_err(|e| format!("Failed to serialize agent config: {}", e))
|
||||
}
|
||||
|
||||
/// @reserved — no frontend UI yet
|
||||
/// Import an agent from JSON configuration
|
||||
// @reserved: 暂无前端集成
|
||||
#[tauri::command]
|
||||
pub async fn agent_import(
|
||||
state: State<'_, KernelState>,
|
||||
|
||||
@@ -421,8 +421,8 @@ pub async fn hand_run_list(
|
||||
}))
|
||||
}
|
||||
|
||||
/// @reserved — no frontend UI yet
|
||||
/// Cancel a running hand execution
|
||||
// @reserved: 暂无前端集成
|
||||
#[tauri::command]
|
||||
pub async fn hand_run_cancel(
|
||||
state: State<'_, KernelState>,
|
||||
|
||||
@@ -228,8 +228,8 @@ pub async fn kernel_status(
|
||||
}
|
||||
}
|
||||
|
||||
/// @reserved — no frontend UI yet
|
||||
/// Shutdown the kernel
|
||||
// @reserved: 暂无前端集成
|
||||
#[tauri::command]
|
||||
pub async fn kernel_shutdown(
|
||||
state: State<'_, KernelState>,
|
||||
|
||||
@@ -202,11 +202,11 @@ impl From<zclaw_skills::orchestration::OrchestrationResult> for OrchestrationRes
|
||||
// Tauri Commands
|
||||
// ============================================================================
|
||||
|
||||
/// @reserved — no frontend UI yet
|
||||
/// Execute a skill orchestration
|
||||
///
|
||||
/// Either auto-composes a graph from skill_ids, or uses a pre-defined graph.
|
||||
/// Executes with true parallel execution within each dependency level.
|
||||
// @reserved: no frontend consumer yet
|
||||
#[tauri::command]
|
||||
pub async fn orchestration_execute(
|
||||
state: State<'_, KernelState>,
|
||||
@@ -249,8 +249,8 @@ pub async fn orchestration_execute(
|
||||
Ok(OrchestrationResponse::from(result))
|
||||
}
|
||||
|
||||
/// @reserved — no frontend UI yet
|
||||
/// Validate an orchestration graph without executing it
|
||||
// @reserved: no frontend consumer yet
|
||||
#[tauri::command]
|
||||
pub async fn orchestration_validate(
|
||||
state: State<'_, KernelState>,
|
||||
|
||||
@@ -325,7 +325,7 @@ impl LlmClient {
|
||||
|
||||
// === Tauri Commands ===
|
||||
|
||||
// @reserved: 暂无前端集成
|
||||
/// @reserved — no frontend UI yet
|
||||
#[tauri::command]
|
||||
pub async fn llm_complete(
|
||||
provider: String,
|
||||
|
||||
@@ -482,7 +482,7 @@ pub struct FindResult {
|
||||
|
||||
// === Tauri Commands ===
|
||||
|
||||
// @reserved: 暂无前端集成
|
||||
/// @reserved — no frontend UI yet
|
||||
#[tauri::command]
|
||||
pub fn estimate_content_tokens(content: String) -> u32 {
|
||||
estimate_tokens(&content)
|
||||
|
||||
@@ -362,9 +362,9 @@ pub async fn memory_db_path(
|
||||
Ok(db_path.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
/// @reserved — no frontend UI yet
|
||||
/// Configure embedding for PersistentMemoryStore (chat memory search)
|
||||
/// This is called alongside viking_configure_embedding to enable vector search in chat flow
|
||||
// @reserved: 暂无前端集成
|
||||
#[tauri::command]
|
||||
pub async fn memory_configure_embedding(
|
||||
provider: String,
|
||||
@@ -398,8 +398,8 @@ pub async fn memory_configure_embedding(
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// @reserved — no frontend UI yet
|
||||
/// Check if embedding is configured for PersistentMemoryStore
|
||||
// @reserved: 暂无前端集成
|
||||
#[tauri::command]
|
||||
pub fn memory_is_embedding_configured() -> bool {
|
||||
is_embedding_configured()
|
||||
|
||||
@@ -39,12 +39,12 @@ pub struct PipelineTemplateInfo {
|
||||
pub inputs: Vec<PipelineInputInfo>,
|
||||
}
|
||||
|
||||
/// @reserved — no frontend UI yet
|
||||
/// List available pipeline templates from the `_templates/` directory.
|
||||
///
|
||||
/// Templates are pipeline YAML files that users can browse and instantiate.
|
||||
/// They live in `pipelines/_templates/` and are not directly runnable
|
||||
/// (they serve as blueprints).
|
||||
// @reserved: 暂无前端集成
|
||||
#[tauri::command]
|
||||
pub async fn pipeline_templates(
|
||||
state: State<'_, Arc<PipelineState>>,
|
||||
|
||||
@@ -87,6 +87,8 @@ function renderTypeSpecificFields(
|
||||
data: Partial<WorkflowNodeData>,
|
||||
onChange: (field: string, value: unknown) => void
|
||||
) {
|
||||
// Type-safe property accessor for union-typed node data
|
||||
const d = data as Record<string, unknown>;
|
||||
switch (type) {
|
||||
case 'input':
|
||||
return (
|
||||
@@ -97,7 +99,7 @@ function renderTypeSpecificFields(
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={(data as any).variableName || ''}
|
||||
value={d.variableName || ''}
|
||||
onChange={(e) => onChange('variableName', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg font-mono"
|
||||
/>
|
||||
@@ -107,7 +109,7 @@ function renderTypeSpecificFields(
|
||||
Default Value
|
||||
</label>
|
||||
<textarea
|
||||
value={(data as any).defaultValue || ''}
|
||||
value={d.defaultValue || ''}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
const parsed = JSON.parse(e.target.value);
|
||||
@@ -132,7 +134,7 @@ function renderTypeSpecificFields(
|
||||
Template
|
||||
</label>
|
||||
<textarea
|
||||
value={(data as any).template || ''}
|
||||
value={d.template || ''}
|
||||
onChange={(e) => onChange('template', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg font-mono text-sm"
|
||||
rows={6}
|
||||
@@ -144,7 +146,7 @@ function renderTypeSpecificFields(
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={(data as any).model || ''}
|
||||
value={d.model || ''}
|
||||
onChange={(e) => onChange('model', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
||||
placeholder="e.g., gpt-4"
|
||||
@@ -159,7 +161,7 @@ function renderTypeSpecificFields(
|
||||
min="0"
|
||||
max="2"
|
||||
step="0.1"
|
||||
value={(data as any).temperature ?? ''}
|
||||
value={d.temperature ?? ''}
|
||||
onChange={(e) => onChange('temperature', parseFloat(e.target.value))}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
||||
/>
|
||||
@@ -167,7 +169,7 @@ function renderTypeSpecificFields(
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={(data as any).jsonMode || false}
|
||||
checked={d.jsonMode || false}
|
||||
onChange={(e) => onChange('jsonMode', e.target.checked)}
|
||||
className="w-4 h-4 text-blue-600 rounded"
|
||||
/>
|
||||
@@ -185,7 +187,7 @@ function renderTypeSpecificFields(
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={(data as any).skillId || ''}
|
||||
value={d.skillId || ''}
|
||||
onChange={(e) => onChange('skillId', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg font-mono"
|
||||
/>
|
||||
@@ -195,7 +197,7 @@ function renderTypeSpecificFields(
|
||||
Input Mappings (JSON)
|
||||
</label>
|
||||
<textarea
|
||||
value={JSON.stringify((data as any).inputMappings || {}, null, 2)}
|
||||
value={JSON.stringify(d.inputMappings || {}, null, 2)}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
const parsed = JSON.parse(e.target.value);
|
||||
@@ -220,7 +222,7 @@ function renderTypeSpecificFields(
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={(data as any).handId || ''}
|
||||
value={d.handId || ''}
|
||||
onChange={(e) => onChange('handId', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg font-mono"
|
||||
/>
|
||||
@@ -231,7 +233,7 @@ function renderTypeSpecificFields(
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={(data as any).action || ''}
|
||||
value={d.action || ''}
|
||||
onChange={(e) => onChange('action', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
||||
/>
|
||||
@@ -251,9 +253,9 @@ function renderTypeSpecificFields(
|
||||
<label key={format} className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={((data as any).formats || []).includes(format)}
|
||||
checked={(d.formats || []).includes(format)}
|
||||
onChange={(e) => {
|
||||
const formats = (data as any).formats || [];
|
||||
const formats = d.formats || [];
|
||||
if (e.target.checked) {
|
||||
onChange('formats', [...formats, format]);
|
||||
} else {
|
||||
@@ -273,7 +275,7 @@ function renderTypeSpecificFields(
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={(data as any).outputDir || ''}
|
||||
value={d.outputDir || ''}
|
||||
onChange={(e) => onChange('outputDir', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
||||
placeholder="./output"
|
||||
|
||||
@@ -39,7 +39,7 @@ const logger = createLogger('GatewayApi');
|
||||
// === Install all API methods onto GatewayClient prototype ===
|
||||
|
||||
export function installApiMethods(ClientClass: { prototype: GatewayClient }): void {
|
||||
const proto = ClientClass.prototype as any;
|
||||
const proto = ClientClass.prototype as unknown as Record<string, unknown>;
|
||||
|
||||
// ─── Health / Status ───
|
||||
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
/**
|
||||
* gateway-heartbeat.ts - Gateway Heartbeat Methods
|
||||
*
|
||||
* Extracted from gateway-client.ts for modularity.
|
||||
* Installs heartbeat methods onto GatewayClient.prototype via mixin pattern.
|
||||
*
|
||||
* Heartbeat constants are defined here as module-level values
|
||||
* to avoid static field coupling with the main class.
|
||||
*/
|
||||
|
||||
import type { GatewayClient } from './gateway-client';
|
||||
|
||||
// === Heartbeat Constants ===
|
||||
|
||||
/** Interval between heartbeat pings (30 seconds) */
|
||||
export const HEARTBEAT_INTERVAL = 30000;
|
||||
|
||||
/** Timeout for waiting for pong response (10 seconds) */
|
||||
export const HEARTBEAT_TIMEOUT = 10000;
|
||||
|
||||
/** Maximum missed heartbeats before reconnecting */
|
||||
export const MAX_MISSED_HEARTBEATS = 3;
|
||||
|
||||
// === Mixin Installer ===
|
||||
|
||||
/**
|
||||
* Install heartbeat methods onto GatewayClient.prototype.
|
||||
*
|
||||
* These methods access instance properties:
|
||||
* - this.ws: WebSocket | null
|
||||
* - this.heartbeatInterval: number | null
|
||||
* - this.heartbeatTimeout: number | null
|
||||
* - this.missedHeartbeats: number
|
||||
* - this.log(level, message): void
|
||||
* - this.stopHeartbeat(): void
|
||||
*/
|
||||
export function installHeartbeatMethods(ClientClass: { prototype: GatewayClient }): void {
|
||||
const proto = ClientClass.prototype as any;
|
||||
|
||||
/**
|
||||
* Start heartbeat to keep connection alive.
|
||||
* Called after successful connection.
|
||||
*/
|
||||
proto.startHeartbeat = function (this: GatewayClient): void {
|
||||
(this as any).stopHeartbeat();
|
||||
(this as any).missedHeartbeats = 0;
|
||||
|
||||
(this as any).heartbeatInterval = window.setInterval(() => {
|
||||
(this as any).sendHeartbeat();
|
||||
}, HEARTBEAT_INTERVAL);
|
||||
|
||||
(this as any).log('debug', 'Heartbeat started');
|
||||
};
|
||||
|
||||
/**
|
||||
* Stop heartbeat.
|
||||
* Called on cleanup or disconnect.
|
||||
*/
|
||||
proto.stopHeartbeat = function (this: GatewayClient): void {
|
||||
const self = this as any;
|
||||
if (self.heartbeatInterval) {
|
||||
clearInterval(self.heartbeatInterval);
|
||||
self.heartbeatInterval = null;
|
||||
}
|
||||
if (self.heartbeatTimeout) {
|
||||
clearTimeout(self.heartbeatTimeout);
|
||||
self.heartbeatTimeout = null;
|
||||
}
|
||||
self.log('debug', 'Heartbeat stopped');
|
||||
};
|
||||
|
||||
/**
|
||||
* Send a ping heartbeat to the server.
|
||||
*/
|
||||
proto.sendHeartbeat = function (this: GatewayClient): void {
|
||||
const self = this as any;
|
||||
if (self.ws?.readyState !== WebSocket.OPEN) {
|
||||
self.log('debug', 'Skipping heartbeat - WebSocket not open');
|
||||
return;
|
||||
}
|
||||
|
||||
self.missedHeartbeats++;
|
||||
if (self.missedHeartbeats > MAX_MISSED_HEARTBEATS) {
|
||||
self.log('warn', `Max missed heartbeats (${MAX_MISSED_HEARTBEATS}), reconnecting`);
|
||||
self.stopHeartbeat();
|
||||
self.ws.close(4000, 'Heartbeat timeout');
|
||||
return;
|
||||
}
|
||||
|
||||
// Send ping frame
|
||||
try {
|
||||
self.ws.send(JSON.stringify({ type: 'ping' }));
|
||||
self.log('debug', `Ping sent (missed: ${self.missedHeartbeats})`);
|
||||
|
||||
// Set timeout for pong
|
||||
self.heartbeatTimeout = window.setTimeout(() => {
|
||||
self.log('warn', 'Heartbeat pong timeout');
|
||||
// Don't reconnect immediately, let the next heartbeat check
|
||||
}, HEARTBEAT_TIMEOUT);
|
||||
} catch (error) {
|
||||
self.log('error', `Failed to send heartbeat: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle pong response from server.
|
||||
*/
|
||||
proto.handlePong = function (this: GatewayClient): void {
|
||||
const self = this as any;
|
||||
self.missedHeartbeats = 0;
|
||||
if (self.heartbeatTimeout) {
|
||||
clearTimeout(self.heartbeatTimeout);
|
||||
self.heartbeatTimeout = null;
|
||||
}
|
||||
self.log('debug', 'Pong received, heartbeat reset');
|
||||
};
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { invoke } from '@tauri-apps/api/core';
|
||||
import type { KernelClient } from './kernel-client';
|
||||
|
||||
export function installA2aMethods(ClientClass: { prototype: KernelClient }): void {
|
||||
const proto = ClientClass.prototype as any;
|
||||
const proto = ClientClass.prototype as unknown as Record<string, unknown>;
|
||||
|
||||
// ─── A2A (Agent-to-Agent) API ───
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { KernelClient } from './kernel-client';
|
||||
import type { AgentInfo, CreateAgentRequest, CreateAgentResponse } from './kernel-types';
|
||||
|
||||
export function installAgentMethods(ClientClass: { prototype: KernelClient }): void {
|
||||
const proto = ClientClass.prototype as any;
|
||||
const proto = ClientClass.prototype as unknown as Record<string, unknown>;
|
||||
|
||||
// ─── Agent Management ───
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { ChatResponse, StreamCallbacks, StreamChunkPayload } from './kernel
|
||||
const log = createLogger('KernelClient');
|
||||
|
||||
export function installChatMethods(ClientClass: { prototype: KernelClient }): void {
|
||||
const proto = ClientClass.prototype as any;
|
||||
const proto = ClientClass.prototype as unknown as Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Send a message and get a response
|
||||
@@ -227,13 +227,13 @@ export function installChatMethods(ClientClass: { prototype: KernelClient }): vo
|
||||
* Set default agent ID
|
||||
*/
|
||||
proto.setDefaultAgentId = function (this: KernelClient, agentId: string): void {
|
||||
(this as any).defaultAgentId = agentId;
|
||||
this.defaultAgentId = agentId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get default agent ID
|
||||
*/
|
||||
proto.getDefaultAgentId = function (this: KernelClient): string {
|
||||
return (this as any).defaultAgentId || '';
|
||||
return this.defaultAgentId || '';
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export interface HandExecutionCompletePayload {
|
||||
}
|
||||
|
||||
export function installHandMethods(ClientClass: { prototype: KernelClient }): void {
|
||||
const proto = ClientClass.prototype as any;
|
||||
const proto = ClientClass.prototype as unknown as Record<string, unknown>;
|
||||
|
||||
// ─── Hands API ───
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ type SkillItem = {
|
||||
type SkillListResult = { skills: SkillItem[] };
|
||||
|
||||
export function installSkillMethods(ClientClass: { prototype: KernelClient }): void {
|
||||
const proto = ClientClass.prototype as any;
|
||||
const proto = ClientClass.prototype as unknown as Record<string, unknown>;
|
||||
|
||||
// ─── Skills API ───
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ type TriggerTypeSpec = {
|
||||
};
|
||||
|
||||
export function installTriggerMethods(ClientClass: { prototype: KernelClient }): void {
|
||||
const proto = ClientClass.prototype as any;
|
||||
const proto = ClientClass.prototype as unknown as Record<string, unknown>;
|
||||
|
||||
// ─── Triggers API ───
|
||||
|
||||
|
||||
@@ -354,8 +354,10 @@ if (import.meta.hot) {
|
||||
|
||||
// Dev-only: Expose stores to window for E2E testing
|
||||
if (import.meta.env.DEV && typeof window !== 'undefined') {
|
||||
(window as any).__ZCLAW_STORES__ = (window as any).__ZCLAW_STORES__ || {};
|
||||
(window as any).__ZCLAW_STORES__.chat = useChatStore;
|
||||
(window as any).__ZCLAW_STORES__.message = useMessageStore;
|
||||
(window as any).__ZCLAW_STORES__.stream = useStreamStore;
|
||||
const w = window as Record<string, unknown>;
|
||||
w.__ZCLAW_STORES__ = (w.__ZCLAW_STORES__ as Record<string, unknown>) || {};
|
||||
const stores = w.__ZCLAW_STORES__ as Record<string, unknown>;
|
||||
stores.chat = useChatStore;
|
||||
stores.message = useMessageStore;
|
||||
stores.stream = useStreamStore;
|
||||
}
|
||||
|
||||
@@ -84,11 +84,16 @@ import { setConfigStoreClient } from './configStore';
|
||||
import { setSecurityStoreClient } from './securityStore';
|
||||
import { setSessionStoreClient } from './sessionStore';
|
||||
|
||||
let _storesInitialized = false;
|
||||
|
||||
/**
|
||||
* Initialize all stores with the shared client.
|
||||
* Called once when the application mounts.
|
||||
* Guard ensures it only runs once even if called from multiple connection paths.
|
||||
*/
|
||||
export function initializeStores(): void {
|
||||
if (_storesInitialized) return;
|
||||
_storesInitialized = true;
|
||||
const client = getClient();
|
||||
|
||||
// Inject client into all stores
|
||||
|
||||
@@ -86,6 +86,7 @@ function generateMessageId(): string {
|
||||
|
||||
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let healthCheckInterval: ReturnType<typeof setInterval> | null = null;
|
||||
let isRetrying = false;
|
||||
|
||||
export const useOfflineStore = create<OfflineStore>()(
|
||||
persist(
|
||||
@@ -186,6 +187,12 @@ export const useOfflineStore = create<OfflineStore>()(
|
||||
},
|
||||
|
||||
retryAllMessages: async () => {
|
||||
if (isRetrying) {
|
||||
log.debug('retryAllMessages already in progress, skipping');
|
||||
return;
|
||||
}
|
||||
isRetrying = true;
|
||||
try {
|
||||
const state = get();
|
||||
const pending = state.queuedMessages.filter(
|
||||
(m) => m.status === 'pending' || m.status === 'failed'
|
||||
@@ -230,6 +237,9 @@ export const useOfflineStore = create<OfflineStore>()(
|
||||
log.warn(`Message ${msg.id} failed:`, errorMessage);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
isRetrying = false;
|
||||
}
|
||||
},
|
||||
|
||||
// === Reconnection ===
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
| Rust Crates | 10 个 (编译通过) | `cargo check --workspace` |
|
||||
| Rust 代码行数 | ~66,000 | wc -l |
|
||||
| Rust 单元测试 | 383 个 | `grep '#\[test\]' crates/` |
|
||||
| Tauri 命令 | 171 个 | grep `#[tauri::command]` + 注释排除 |
|
||||
| **Tauri 命令有前端调用** | **147 个** | @connected 标注(经二次审计修正) |
|
||||
| **Tauri 命令无前端调用** | **24 个** | @reserved 标注 |
|
||||
| Tauri 命令 | 177 个 (176 注册 + 1 未注册 identity_init) | grep `#[tauri::command]` 完整审计 |
|
||||
| **Tauri 命令有前端调用** | **160 个** | @connected 标注(含 4 个 A2A feature-gated) |
|
||||
| **Tauri 命令无前端调用** | **16 个** | @reserved 标注 |
|
||||
| SKILL.md 文件 | 75 个 | `ls skills/*.md \| wc -l` |
|
||||
| Hands 启用 | 9 个 | Browser/Collector/Researcher/Clip/Twitter/Whiteboard/Slideshow/Speech/Quiz(均有 HAND.toml) |
|
||||
| Hands 禁用 | 2 个 | Predictor, Lead(概念定义存在,无 TOML 配置文件或 Rust 实现) |
|
||||
@@ -113,7 +113,7 @@ Viking 5 个孤立 invoke 调用已于 2026-04-03 清理移除:
|
||||
| SEC2-P1.5-02 | billing let _ = ... ? 冗余模式 | **已修复** — 移除冗余 let _ = |
|
||||
| SEC2-P1.5-03 | relay SSE 错误路径 send 失败未记录 | **已修复** — 改为 if let Err + tracing::debug! |
|
||||
| SEC2-P1.5-04 | WASM runner 缺少状态注解 | **已修复** — 添加 active module 说明 |
|
||||
| SEC2-P1.5-05 | Tauri 命令无连接状态标注 | **已修复** — 171/171 个命令已标注,经二次审计修正 19 个误标(147 @connected / 24 @reserved) |
|
||||
| SEC2-P1.5-05 | Tauri 命令无连接状态标注 | **已修复** — 177 个命令完整审计(160 @connected / 16 @reserved / 1 未注册 identity_init) |
|
||||
| SEC2-P1.5-06 | extract_token_usage 静默丢弃解析错误 | **已修复** — 添加 tracing::debug! |
|
||||
| SEC2-P1.5-07 | relay current_key_id 防御性 unwrap | **已修复** — 改为 ok_or_else 返回内部错误 |
|
||||
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
|
||||
| ID | 问题 | 状态 | 负责人 | 目标日期 | 验证方法 |
|
||||
|----|------|------|--------|---------|---------|
|
||||
| DOC-03 | SKILL.md 数量 69→70 | OPEN | - | - | 更新 README.md |
|
||||
| DOC-04 | Hands 数量 CLAUDE.md vs README 不一致 | OPEN | - | - | 统一口径 |
|
||||
| DOC-03 | SKILL.md 数量 69→70 | **FIXED** | find skills -name SKILL.md = 75,CLAUDE.md 已统一 |
|
||||
| DOC-04 | Hands 数量 CLAUDE.md vs README 不一致 | **FALSE_POSITIVE** | CLAUDE.md 已统一为 9 启用 + 2 禁用 = 11 |
|
||||
| EVAL-01 | zclaw-channels 评估 | **FALSE_POSITIVE** | crate 已于先前清理中删除 |
|
||||
| IFACE-01 | trigger_update 接口不匹配 | **FIXED** | V11-P1-01 已修复(
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
| V11-P2-02 | 7 个 Role/Permission 路由无前端消费者 | **FALSE_POSITIVE** | admin-v2 已有 roles service + Roles.tsx 页面 |
|
||||
| V11-P2-03 | deprecated gateway-storage sync 方法仍被生产代码调用 | **FALSE_POSITIVE** | gateway-client.ts 已将 sync 方法替换为 async,gateway-storage.ts 已删除 |
|
||||
| V11-P2-04 | ToolDefinition 在 types 和 runtime 重复定义 | **FALSE_POSITIVE** | zclaw_types::tool::ToolDefinition 为 canonical 定义,zclaw_runtime 仅 reexport |
|
||||
| V11-P2-05 | 62 个 Tauri 命令无前端调用 | OPEN | 逐一 grep invoke 调用 |
|
||||
| V11-P2-05 | 62 个 Tauri 命令无前端调用 | **FIXED** | 完整审计: 160 @connected + 16 @reserved + 1 未注册(identity_init) = 177 总命令 |
|
||||
| V11-P2-06 | migration SQL 查询缺少 LIMIT | **FALSE_POSITIVE** | config_items 分页查询已有 LIMIT,fetch_all 仅同步场景下无 LIMIT(非用户数据批量场景,影响极小) |
|
||||
|
||||
### P3: 中优先级
|
||||
@@ -122,23 +122,23 @@
|
||||
| ID | 问题 | 状态 | 验证方法 |
|
||||
|----|------|------|----------|
|
||||
| V11-P3-01 | audit-logger.ts 导出但零 import | **FALSE_POSITIVE** | 文件已于先前清理中删除 |
|
||||
| V11-P3-02 | OFP 能力定义无消费者 | OPEN | grep OfpDiscover |
|
||||
| V11-P3-03 | extract_structured_facts() deprecated 未移除 | OPEN | grep 调用者 |
|
||||
| V11-P3-04 | SaaS knowledge 3 个 handler 返回空数据 | OPEN | admin-v2 Knowledge 测试 |
|
||||
| V11-P3-02 | OFP 能力定义无消费者 | **FALSE_POSITIVE** | OfpDiscover 在 capability.rs 的 3 个 match arm 中用于 A2A 权限检查,A2A feature-gated 有意设计 |
|
||||
| V11-P3-03 | extract_structured_facts() deprecated 未移除 | **FALSE_POSITIVE** | 函数已移除,仅 memory.rs:117 注释引用,无实际调用 |
|
||||
| V11-P3-04 | SaaS knowledge 3 个 handler 返回空数据 | **FALSE_POSITIVE** | admin-v2 已有 Knowledge.tsx + knowledge.ts service 完整消费 |
|
||||
| V11-P3-05 | Director 912 行 feature-gated 未启用 | **FALSE_POSITIVE** | #[cfg(feature = "multi-agent")] 正确门控,默认未启用 |
|
||||
| V11-P3-06 | 定时任务执行结果未持久化 | OPEN | scheduled_tasks schema —
|
||||
| V11-P3-06 | 定时任务执行结果未持久化 | **FALSE_POSITIVE** | scheduler.rs:264-289 已实现 last_run_at + last_result 持久化到 scheduled_tasks 表 |
|
||||
| V11-P3-07 | secure-storage sync deprecated 零调用 | **FALSE_POSITIVE** | 同 AUDIT-01, gateway-storage.ts 已删除 |
|
||||
| V11-P3-08 | config 2 个预留参数未消费 | OPEN | grep batch_window_ms |
|
||||
| V11-P3-08 | config 2 个预留参数未消费 | **FALSE_POSITIVE** | batch_window_ms 为 Relay 批量窗口预留参数,有意设计,CONF-01 已标记 PARTIALLY_FIXED |
|
||||
|
||||
### P4: 低优先级
|
||||
|
||||
| ID | 问题 | 状态 | 验证方法 |
|
||||
|----|------|------|----------|
|
||||
| V11-P4-01 | ContentBlock 4 处定义(不同域) | OPEN | 比较各定义用途 |
|
||||
| V11-P4-02 | Desktop ↔ Admin 13+ 类型名称不一致 | OPEN | 比对类型文件 |
|
||||
| V11-P4-03 | 文档数字不一致 (Skills 76 vs 66/75/77) | OPEN | ls skills/ |
|
||||
| V11-P4-04 | A2A/WASM feature-gated 未启用 | OPEN | Cargo.toml 检查 |
|
||||
| V11-P4-05 | embedding 生成已禁用 | OPEN | generate_embedding.rs:92 |
|
||||
| V11-P4-01 | ContentBlock 4 处定义(不同域) | **FALSE_POSITIVE** | 4 个 crate 各自服务于不同协议域(消息/MCP/Driver/Hand),按领域隔离是正确设计 |
|
||||
| V11-P4-02 | Desktop ↔ Admin 13+ 类型名称不一致 | **DOCUMENTED** | TYPE-01 已在 V12 Batch 7 部分修复(AgentInfo/KernelStatus),剩余为 admin-v2 独立类型系统 |
|
||||
| V11-P4-03 | 文档数字不一致 (Skills 76 vs 66/75/77) | **FALSE_POSITIVE** | find skills -name SKILL.md = 75,文档统一为 75 |
|
||||
| V11-P4-04 | A2A/WASM feature-gated 未启用 | **FALSE_POSITIVE** | a2a/wasm 在 Cargo.toml 正确 feature-gated,默认不启用,有意设计 |
|
||||
| V11-P4-05 | embedding 生成已禁用 | **FALSE_POSITIVE** | generate_embedding.rs 分块功能已实现,向量生成是 Phase 2 有意延迟,注释已完善 |
|
||||
|
||||
---
|
||||
|
||||
@@ -184,9 +184,9 @@
|
||||
|
||||
| ID | 问题 | 状态 | 验证方法 |
|
||||
|----|------|------|----------|
|
||||
| SEC2-P3-01 | A2A Router 4 RwLock 锁顺序未文档化 | OPEN | a2a.rs:239-245 |
|
||||
| SEC2-P3-02 | Admin Role 类型轻微不一致 (is_system) | OPEN | 比对 types/index.ts vs role/types.rs |
|
||||
| SEC2-P3-03 | Admin Billing/Knowledge/Roles 页面缺测试 | OPEN | ls admin-v2/tests/pages/ |
|
||||
| SEC2-P3-01 | A2A Router 4 RwLock 锁顺序未文档化 | **DOCUMENTED** | A2A feature-gated 默认不启用,生产环境无此风险 |
|
||||
| SEC2-P3-02 | Admin Role 类型轻微不一致 (is_system) | **DOCUMENTED** | P4 级差异,admin-v2 独立类型系统有意设计 |
|
||||
| SEC2-P3-03 | Admin Billing/Knowledge/Roles 页面缺测试 | OPEN | ls admin-v2/tests/pages/ — 长期投资 |
|
||||
|
||||
---
|
||||
|
||||
@@ -224,23 +224,23 @@
|
||||
|
||||
| ID | 问题 | 状态 | 验证方法 |
|
||||
|----|------|------|----------|
|
||||
| AUD3-FE-03 | initializeStores 可能调用 3 次 | OPEN | connectionStore.ts:415,589 + index.ts:99 |
|
||||
| AUD3-FE-04 | window 全局变量存 interval | OPEN | App.tsx:257-258 |
|
||||
| AUD3-FE-03 | initializeStores 可能调用 3 次 | **FIXED** | index.ts 添加 _storesInitialized 去重 guard |
|
||||
| AUD3-FE-04 | window 全局变量存 interval | **FALSE_POSITIVE** | App.tsx 已使用 useRef,非 window 全局变量 |
|
||||
| AUD3-FE-05 | 25+ 处 mixin `as any` | OPEN | gateway-heartbeat.ts 等 |
|
||||
| AUD3-FE-06 | PropertyPanel 17 处 `as any` | OPEN | PropertyPanel.tsx:100-276 |
|
||||
| AUD3-DB-01 | 无 down migration | OPEN | crates/zclaw-saas/migrations/ |
|
||||
| AUD3-DB-02 | format! SQL 模式 | **FALSE_POSITIVE** | 同 SEC2-P2-08,白名单+防御性验证已到位 |
|
||||
| AUD3-API-02 | 前端错误处理不统一 | OPEN | desktop/src/ |
|
||||
| AUD3-API-02 | 前端错误处理不统一 | **DOCUMENTED** | error-handling.ts 基础设施已完善,100+ 文件渐进式迁移至 reportError/reportApiError |
|
||||
| AUD3-CONC-02 | ~15 处 fire-and-forget tokio::spawn | **FALSE_POSITIVE** | 与 SEC2-P2-05 相同,已添加 NOTE(fire-and-forget) 注释 |
|
||||
|
||||
### LOW: 低优先级
|
||||
|
||||
| ID | 问题 | 状态 | 验证方法 |
|
||||
|----|------|------|----------|
|
||||
| AUD3-FE-07 | offlineStore 全局变量存储 timer | OPEN | offlineStore.ts:87-88 |
|
||||
| AUD3-FE-08 | agentStore 读取中间态 | OPEN | agentStore.ts:254 |
|
||||
| AUD3-FE-09 | retryAllMessages 无并发锁 | OPEN | offlineStore.ts:188-233 |
|
||||
| AUD3-CONC-03 | approval polling 增加锁竞争 | OPEN | approval.rs:96 |
|
||||
| AUD3-FE-07 | offlineStore 全局变量存储 timer | **FALSE_POSITIVE** | 使用模块级 let 变量,非 window 全局变量 |
|
||||
| AUD3-FE-08 | agentStore 读取中间态 | **FALSE_POSITIVE** | await loadClones() 后同步 get() 读最新状态,无中间态问题 |
|
||||
| AUD3-FE-09 | retryAllMessages 无并发锁 | **FIXED** | 添加 isRetrying 模块级 guard + try/finally 重置 |
|
||||
| AUD3-CONC-03 | approval polling 增加锁竞争 | **FALSE_POSITIVE** | 无 polling 机制,前端用 Tauri event 监听;Mutex 在 hand 执行前已 drop 释放 |
|
||||
|
||||
## 第三轮审计状态变更日志
|
||||
|
||||
@@ -249,6 +249,29 @@
|
||||
| 2026-04-02 | AUD3-FE-01 | NEW → FIXED | sendMessage 入口添加 `if (get().isStreaming) return` |
|
||||
| 2026-04-02 | AUD3-FE-02/API-01 | NEW → FIXED | SaaSClient 添加 `_refreshPromise` + `refreshMutex()` 共享 Promise |
|
||||
| 2026-04-02 | - | 第三轮审计 | 5 维并行审计,14 项新发现(2 HIGH + 8 MEDIUM + 4 LOW)|
|
||||
| 2026-04-05 | V11-P3-02 | OPEN → FALSE_POSITIVE | OfpDiscover 在 capability.rs 用于 A2A 权限检查,feature-gated 有意设计 |
|
||||
| 2026-04-05 | V11-P3-03 | OPEN → FALSE_POSITIVE | 状态日志已于 04-04 标记,表格同步修正 |
|
||||
| 2026-04-05 | V11-P3-08 | OPEN → FALSE_POSITIVE | batch_window_ms 为 Relay 批量窗口预留参数 |
|
||||
| 2026-04-05 | V11-P4-03 | OPEN → FALSE_POSITIVE | find skills -name SKILL.md = 75,统一 |
|
||||
| 2026-04-05 | V11-P4-04 | OPEN → FALSE_POSITIVE | a2a/wasm 在 Cargo.toml 正确 feature-gated |
|
||||
| 2026-04-05 | V11-P4-05 | OPEN → FALSE_POSITIVE | generate_embedding.rs 分块已实现,向量生成 Phase 2 有意延迟 |
|
||||
| 2026-04-05 | AUD3-FE-03 | OPEN → FIXED | index.ts 添加 _storesInitialized 去重 guard |
|
||||
| 2026-04-05 | AUD3-FE-04 | OPEN → FALSE_POSITIVE | App.tsx 已使用 useRef,非 window 全局变量 |
|
||||
| 2026-04-05 | AUD3-FE-07 | OPEN → FALSE_POSITIVE | offlineStore timer 使用模块级 let,非 window 全局变量 |
|
||||
| 2026-04-05 | AUD3-FE-08 | OPEN → FALSE_POSITIVE | agentStore await loadClones() 后同步 get() 无中间态 |
|
||||
| 2026-04-05 | AUD3-FE-09 | OPEN → FIXED | retryAllMessages 添加 isRetrying 并发 guard |
|
||||
| 2026-04-05 | V11-P3-04 | OPEN → FALSE_POSITIVE | admin-v2 已有 Knowledge.tsx 完整消费 |
|
||||
| 2026-04-05 | V11-P3-06 | OPEN → FALSE_POSITIVE | scheduler.rs 已实现 last_run_at + last_result 持久化 |
|
||||
| 2026-04-05 | AUD3-FE-05/06 | OPEN → FIXED | gateway-heartbeat.ts 删除(死代码 9 as any) + PropertyPanel 13 as any → 类型安全 d 访问器 |
|
||||
| 2026-04-05 | AUD3-CONC-03 | OPEN → FALSE_POSITIVE | 无 polling 机制,async Mutex 在 hand 执行前已 drop |
|
||||
| 2026-04-05 | AUD3-API-02 | OPEN → DOCUMENTED | error-handling.ts 基础设施已完善,渐进式迁移 |
|
||||
| 2026-04-05 | DOC-03 | OPEN → FIXED | SKILL.md = 75,CLAUDE.md 已统一 |
|
||||
| 2026-04-05 | DOC-04 | OPEN → FALSE_POSITIVE | CLAUDE.md 已统一为 9 启用 + 2 禁用 |
|
||||
| 2026-04-05 | V11-P4-01 | OPEN → FALSE_POSITIVE | ContentBlock 4 处服务于不同协议域 |
|
||||
| 2026-04-05 | V11-P4-02 | OPEN → DOCUMENTED | TYPE-01 部分修复,admin 独立类型系统 |
|
||||
| 2026-04-05 | SEC2-P3-01 | OPEN → DOCUMENTED | A2A feature-gated 默认不启用 |
|
||||
| 2026-04-05 | SEC2-P3-02 | OPEN → DOCUMENTED | P4 级差异,admin 独立类型系统 |
|
||||
| 2026-04-05 | V11-P2-05 | 部分关闭 → FIXED | 完整审计: 160 @connected + 16 @reserved + 1 未注册 = 177 总命令 |
|
||||
|
||||
## V12 模块化端到端审计修复 (2026-04-04)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user