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
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
562 lines
18 KiB
TypeScript
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;
|