Files
zclaw_openfang/desktop/src/lib/intelligence-client/unified-client.ts
iven 73ff5e8c5e
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
feat(desktop): DeerFlow visual redesign + stream hang fix + intelligence client
DeerFlow frontend visual overhaul:
- Card-style input box (white rounded card, textarea top, actions bottom)
- Dropdown mode selector (闪速/思考/Pro/Ultra with icons+descriptions)
- Colored quick-action chips (小惊喜/写作/研究/收集/学习)
- Minimal top bar (title + token count + export)
- Warm gray color system (#faf9f6 bg, #f5f4f1 sidebar, #e8e6e1 border)
- DeerFlow-style sidebar (新对话/对话/智能体 nav)
- Reasoning block, tool call chain, task progress visualization
- Streaming text, model selector, suggestion chips components
- Resizable artifact panel with drag handle
- Virtualized message list for 100+ messages

Bug fixes:
- Stream hang: GatewayClient onclose code 1000 now calls onComplete
- WebView2 textarea border: CSS !important override for UA styles
- Gateway stream event handling (response/phase/tool_call types)

Intelligence client:
- Unified client with fallback drivers (compactor/heartbeat/identity/memory/reflection)
- Gateway API types and type conversions
2026-04-01 22:03:07 +08:00

562 lines
18 KiB
TypeScript

/**
* Intelligence Layer Unified Client
*
* Provides a unified API for intelligence operations that:
* - Uses Rust backend (via Tauri commands) when running in Tauri environment
* - Falls back to localStorage-based implementation in browser/dev environment
*
* Degradation strategy:
* - In Tauri mode: if a Tauri invoke fails, the error is logged and re-thrown.
* The caller is responsible for handling the error. We do NOT silently fall
* back to localStorage, because that would give users degraded functionality
* (localStorage instead of SQLite, rule-based instead of LLM-based, no-op
* instead of real execution) without any indication that something is wrong.
* - In browser/dev mode: localStorage fallback is the intended behavior for
* development and testing without a Tauri backend.
*
* This replaces direct usage of:
* - agent-memory.ts
* - heartbeat-engine.ts
* - context-compactor.ts
* - reflection-engine.ts
* - agent-identity.ts
*
* Usage:
* ```typescript
* import { intelligenceClient, toFrontendMemory, toBackendMemoryInput } from './intelligence-client';
*
* // Store memory
* const id = await intelligenceClient.memory.store({
* agent_id: 'agent-1',
* memory_type: 'fact',
* content: 'User prefers concise responses',
* importance: 7,
* });
*
* // Search memories
* const memories = await intelligenceClient.memory.search({
* agent_id: 'agent-1',
* query: 'user preference',
* limit: 10,
* });
*
* // Convert to frontend format if needed
* const frontendMemories = memories.map(toFrontendMemory);
* ```
*/
import { invoke } from '@tauri-apps/api/core';
import { isTauriRuntime } from '../tauri-gateway';
import { intelligence } from './type-conversions';
import type { PersistentMemory } from '../intelligence-backend';
import type {
HeartbeatConfig,
HeartbeatResult,
CompactableMessage,
CompactionResult,
CompactionCheck,
CompactionConfig,
ReflectionConfig,
ReflectionResult,
ReflectionState,
MemoryEntryForAnalysis,
IdentityFiles,
IdentityChangeProposal,
IdentitySnapshot,
} from '../intelligence-backend';
import type { MemoryEntry, MemorySearchOptions, MemoryStats } from './types';
import { toFrontendMemory, toBackendSearchOptions, toFrontendStats } from './type-conversions';
import { fallbackMemory } from './fallback-memory';
import { fallbackCompactor } from './fallback-compactor';
import { fallbackReflection } from './fallback-reflection';
import { fallbackIdentity } from './fallback-identity';
import { fallbackHeartbeat } from './fallback-heartbeat';
/**
* Helper: wrap a Tauri invoke call so that failures are logged and re-thrown
* instead of silently falling back to localStorage implementations.
*/
function tauriInvoke<T>(label: string, fn: () => Promise<T>): Promise<T> {
return fn().catch((e: unknown) => {
console.warn(`[IntelligenceClient] Tauri invoke failed (${label}):`, e);
throw e;
});
}
/**
* Unified intelligence client that automatically selects backend or fallback.
*
* - In Tauri mode: calls Rust backend via invoke(). On failure, logs a warning
* and re-throws -- does NOT fall back to localStorage.
* - In browser/dev mode: uses localStorage-based fallback implementations.
*/
export const intelligenceClient = {
memory: {
init: async (): Promise<void> => {
if (isTauriRuntime()) {
await tauriInvoke('memory.init', () => intelligence.memory.init());
} else {
await fallbackMemory.init();
}
},
store: async (entry: import('../intelligence-backend').MemoryEntryInput): Promise<string> => {
if (isTauriRuntime()) {
return tauriInvoke('memory.store', () => intelligence.memory.store(entry));
}
return fallbackMemory.store(entry);
},
get: async (id: string): Promise<MemoryEntry | null> => {
if (isTauriRuntime()) {
const result = await tauriInvoke('memory.get', () => intelligence.memory.get(id));
return result ? toFrontendMemory(result) : null;
}
return fallbackMemory.get(id);
},
search: async (options: MemorySearchOptions): Promise<MemoryEntry[]> => {
if (isTauriRuntime()) {
const results = await tauriInvoke('memory.search', () =>
intelligence.memory.search(toBackendSearchOptions(options))
);
return results.map(toFrontendMemory);
}
return fallbackMemory.search(options);
},
delete: async (id: string): Promise<void> => {
if (isTauriRuntime()) {
await tauriInvoke('memory.delete', () => intelligence.memory.delete(id));
} else {
await fallbackMemory.delete(id);
}
},
deleteAll: async (agentId: string): Promise<number> => {
if (isTauriRuntime()) {
return tauriInvoke('memory.deleteAll', () => intelligence.memory.deleteAll(agentId));
}
return fallbackMemory.deleteAll(agentId);
},
stats: async (): Promise<MemoryStats> => {
if (isTauriRuntime()) {
const stats = await tauriInvoke('memory.stats', () => intelligence.memory.stats());
return toFrontendStats(stats);
}
return fallbackMemory.stats();
},
export: async (): Promise<MemoryEntry[]> => {
if (isTauriRuntime()) {
const results = await tauriInvoke('memory.export', () => intelligence.memory.export());
return results.map(toFrontendMemory);
}
return fallbackMemory.export();
},
import: async (memories: MemoryEntry[]): Promise<number> => {
if (isTauriRuntime()) {
const backendMemories = memories.map(m => ({
...m,
agent_id: m.agentId,
memory_type: m.type,
last_accessed_at: m.lastAccessedAt,
created_at: m.createdAt,
access_count: m.accessCount,
conversation_id: m.conversationId ?? null,
tags: JSON.stringify(m.tags),
embedding: null,
}));
return tauriInvoke('memory.import', () =>
intelligence.memory.import(backendMemories as PersistentMemory[])
);
}
return fallbackMemory.import(memories);
},
dbPath: async (): Promise<string> => {
if (isTauriRuntime()) {
return tauriInvoke('memory.dbPath', () => intelligence.memory.dbPath());
}
return fallbackMemory.dbPath();
},
buildContext: async (
agentId: string,
query: string,
maxTokens?: number,
): Promise<{ systemPromptAddition: string; totalTokens: number; memoriesUsed: number }> => {
if (isTauriRuntime()) {
return tauriInvoke('memory.buildContext', () =>
intelligence.memory.buildContext(agentId, query, maxTokens ?? null)
);
}
// Browser/dev fallback: use basic search
const memories = await fallbackMemory.search({
agentId,
query,
limit: 8,
minImportance: 3,
});
const addition = memories.length > 0
? `## 相关记忆\n${memories.map(m => `- [${m.type}] ${m.content}`).join('\n')}`
: '';
return { systemPromptAddition: addition, totalTokens: 0, memoriesUsed: memories.length };
},
},
heartbeat: {
init: async (agentId: string, config?: HeartbeatConfig): Promise<void> => {
if (isTauriRuntime()) {
await tauriInvoke('heartbeat.init', () => intelligence.heartbeat.init(agentId, config));
} else {
await fallbackHeartbeat.init(agentId, config);
}
},
start: async (agentId: string): Promise<void> => {
if (isTauriRuntime()) {
await tauriInvoke('heartbeat.start', () => intelligence.heartbeat.start(agentId));
} else {
await fallbackHeartbeat.start(agentId);
}
},
stop: async (agentId: string): Promise<void> => {
if (isTauriRuntime()) {
await tauriInvoke('heartbeat.stop', () => intelligence.heartbeat.stop(agentId));
} else {
await fallbackHeartbeat.stop(agentId);
}
},
tick: async (agentId: string): Promise<HeartbeatResult> => {
if (isTauriRuntime()) {
return tauriInvoke('heartbeat.tick', () => intelligence.heartbeat.tick(agentId));
}
return fallbackHeartbeat.tick(agentId);
},
getConfig: async (agentId: string): Promise<HeartbeatConfig> => {
if (isTauriRuntime()) {
return tauriInvoke('heartbeat.getConfig', () => intelligence.heartbeat.getConfig(agentId));
}
return fallbackHeartbeat.getConfig(agentId);
},
updateConfig: async (agentId: string, config: HeartbeatConfig): Promise<void> => {
if (isTauriRuntime()) {
await tauriInvoke('heartbeat.updateConfig', () =>
intelligence.heartbeat.updateConfig(agentId, config)
);
} else {
await fallbackHeartbeat.updateConfig(agentId, config);
}
},
getHistory: async (agentId: string, limit?: number): Promise<HeartbeatResult[]> => {
if (isTauriRuntime()) {
return tauriInvoke('heartbeat.getHistory', () =>
intelligence.heartbeat.getHistory(agentId, limit)
);
}
return fallbackHeartbeat.getHistory(agentId, limit);
},
updateMemoryStats: async (
agentId: string,
taskCount: number,
totalEntries: number,
storageSizeBytes: number
): Promise<void> => {
if (isTauriRuntime()) {
await tauriInvoke('heartbeat.updateMemoryStats', () =>
invoke('heartbeat_update_memory_stats', {
agent_id: agentId,
task_count: taskCount,
total_entries: totalEntries,
storage_size_bytes: storageSizeBytes,
})
);
} else {
// Browser/dev fallback only
const cache = {
taskCount,
totalEntries,
storageSizeBytes,
lastUpdated: new Date().toISOString(),
};
localStorage.setItem(`zclaw-memory-stats-${agentId}`, JSON.stringify(cache));
}
},
recordCorrection: async (agentId: string, correctionType: string): Promise<void> => {
if (isTauriRuntime()) {
await tauriInvoke('heartbeat.recordCorrection', () =>
invoke('heartbeat_record_correction', {
agent_id: agentId,
correction_type: correctionType,
})
);
} else {
// Browser/dev fallback only
const key = `zclaw-corrections-${agentId}`;
const stored = localStorage.getItem(key);
const counters = stored ? JSON.parse(stored) : {};
counters[correctionType] = (counters[correctionType] || 0) + 1;
localStorage.setItem(key, JSON.stringify(counters));
}
},
recordInteraction: async (agentId: string): Promise<void> => {
if (isTauriRuntime()) {
await tauriInvoke('heartbeat.recordInteraction', () =>
invoke('heartbeat_record_interaction', {
agent_id: agentId,
})
);
} else {
// Browser/dev fallback only
localStorage.setItem(`zclaw-last-interaction-${agentId}`, new Date().toISOString());
}
},
},
compactor: {
estimateTokens: async (text: string): Promise<number> => {
if (isTauriRuntime()) {
return tauriInvoke('compactor.estimateTokens', () =>
intelligence.compactor.estimateTokens(text)
);
}
return fallbackCompactor.estimateTokens(text);
},
estimateMessagesTokens: async (messages: CompactableMessage[]): Promise<number> => {
if (isTauriRuntime()) {
return tauriInvoke('compactor.estimateMessagesTokens', () =>
intelligence.compactor.estimateMessagesTokens(messages)
);
}
return fallbackCompactor.estimateMessagesTokens(messages);
},
checkThreshold: async (
messages: CompactableMessage[],
config?: CompactionConfig
): Promise<CompactionCheck> => {
if (isTauriRuntime()) {
return tauriInvoke('compactor.checkThreshold', () =>
intelligence.compactor.checkThreshold(messages, config)
);
}
return fallbackCompactor.checkThreshold(messages, config);
},
compact: async (
messages: CompactableMessage[],
agentId: string,
conversationId?: string,
config?: CompactionConfig
): Promise<CompactionResult> => {
if (isTauriRuntime()) {
return tauriInvoke('compactor.compact', () =>
intelligence.compactor.compact(messages, agentId, conversationId, config)
);
}
return fallbackCompactor.compact(messages, agentId, conversationId, config);
},
},
reflection: {
init: async (config?: ReflectionConfig): Promise<void> => {
if (isTauriRuntime()) {
await tauriInvoke('reflection.init', () => intelligence.reflection.init(config));
} else {
await fallbackReflection.init(config);
}
},
recordConversation: async (): Promise<void> => {
if (isTauriRuntime()) {
await tauriInvoke('reflection.recordConversation', () =>
intelligence.reflection.recordConversation()
);
} else {
await fallbackReflection.recordConversation();
}
},
shouldReflect: async (): Promise<boolean> => {
if (isTauriRuntime()) {
return tauriInvoke('reflection.shouldReflect', () =>
intelligence.reflection.shouldReflect()
);
}
return fallbackReflection.shouldReflect();
},
reflect: async (agentId: string, memories: MemoryEntryForAnalysis[]): Promise<ReflectionResult> => {
if (isTauriRuntime()) {
return tauriInvoke('reflection.reflect', () =>
intelligence.reflection.reflect(agentId, memories)
);
}
return fallbackReflection.reflect(agentId, memories);
},
getHistory: async (limit?: number, agentId?: string): Promise<ReflectionResult[]> => {
if (isTauriRuntime()) {
return tauriInvoke('reflection.getHistory', () =>
intelligence.reflection.getHistory(limit, agentId)
);
}
return fallbackReflection.getHistory(limit, agentId);
},
getState: async (): Promise<ReflectionState> => {
if (isTauriRuntime()) {
return tauriInvoke('reflection.getState', () => intelligence.reflection.getState());
}
return fallbackReflection.getState();
},
},
identity: {
get: async (agentId: string): Promise<IdentityFiles> => {
if (isTauriRuntime()) {
return tauriInvoke('identity.get', () => intelligence.identity.get(agentId));
}
return fallbackIdentity.get(agentId);
},
getFile: async (agentId: string, file: string): Promise<string> => {
if (isTauriRuntime()) {
return tauriInvoke('identity.getFile', () => intelligence.identity.getFile(agentId, file));
}
return fallbackIdentity.getFile(agentId, file);
},
buildPrompt: async (agentId: string, memoryContext?: string): Promise<string> => {
if (isTauriRuntime()) {
return tauriInvoke('identity.buildPrompt', () =>
intelligence.identity.buildPrompt(agentId, memoryContext)
);
}
return fallbackIdentity.buildPrompt(agentId, memoryContext);
},
updateUserProfile: async (agentId: string, content: string): Promise<void> => {
if (isTauriRuntime()) {
await tauriInvoke('identity.updateUserProfile', () =>
intelligence.identity.updateUserProfile(agentId, content)
);
} else {
await fallbackIdentity.updateUserProfile(agentId, content);
}
},
appendUserProfile: async (agentId: string, addition: string): Promise<void> => {
if (isTauriRuntime()) {
await tauriInvoke('identity.appendUserProfile', () =>
intelligence.identity.appendUserProfile(agentId, addition)
);
} else {
await fallbackIdentity.appendUserProfile(agentId, addition);
}
},
proposeChange: async (
agentId: string,
file: 'soul' | 'instructions',
suggestedContent: string,
reason: string
): Promise<IdentityChangeProposal> => {
if (isTauriRuntime()) {
return tauriInvoke('identity.proposeChange', () =>
intelligence.identity.proposeChange(agentId, file, suggestedContent, reason)
);
}
return fallbackIdentity.proposeChange(agentId, file, suggestedContent, reason);
},
approveProposal: async (proposalId: string): Promise<IdentityFiles> => {
if (isTauriRuntime()) {
return tauriInvoke('identity.approveProposal', () =>
intelligence.identity.approveProposal(proposalId)
);
}
return fallbackIdentity.approveProposal(proposalId);
},
rejectProposal: async (proposalId: string): Promise<void> => {
if (isTauriRuntime()) {
await tauriInvoke('identity.rejectProposal', () =>
intelligence.identity.rejectProposal(proposalId)
);
} else {
await fallbackIdentity.rejectProposal(proposalId);
}
},
getPendingProposals: async (agentId?: string): Promise<IdentityChangeProposal[]> => {
if (isTauriRuntime()) {
return tauriInvoke('identity.getPendingProposals', () =>
intelligence.identity.getPendingProposals(agentId)
);
}
return fallbackIdentity.getPendingProposals(agentId);
},
updateFile: async (agentId: string, file: string, content: string): Promise<void> => {
if (isTauriRuntime()) {
await tauriInvoke('identity.updateFile', () =>
intelligence.identity.updateFile(agentId, file, content)
);
} else {
await fallbackIdentity.updateFile(agentId, file, content);
}
},
getSnapshots: async (agentId: string, limit?: number): Promise<IdentitySnapshot[]> => {
if (isTauriRuntime()) {
return tauriInvoke('identity.getSnapshots', () =>
intelligence.identity.getSnapshots(agentId, limit)
);
}
return fallbackIdentity.getSnapshots(agentId, limit);
},
restoreSnapshot: async (agentId: string, snapshotId: string): Promise<void> => {
if (isTauriRuntime()) {
await tauriInvoke('identity.restoreSnapshot', () =>
intelligence.identity.restoreSnapshot(agentId, snapshotId)
);
} else {
await fallbackIdentity.restoreSnapshot(agentId, snapshotId);
}
},
listAgents: async (): Promise<string[]> => {
if (isTauriRuntime()) {
return tauriInvoke('identity.listAgents', () => intelligence.identity.listAgents());
}
return fallbackIdentity.listAgents();
},
deleteAgent: async (agentId: string): Promise<void> => {
if (isTauriRuntime()) {
await tauriInvoke('identity.deleteAgent', () => intelligence.identity.deleteAgent(agentId));
} else {
await fallbackIdentity.deleteAgent(agentId);
}
},
},
};
export default intelligenceClient;