release(v0.2.0): streaming, MCP protocol, Browser Hand, security enhancements
## Major Features ### Streaming Response System - Implement LlmDriver trait with `stream()` method returning async Stream - Add SSE parsing for Anthropic and OpenAI API streaming - Integrate Tauri event system for frontend streaming (`stream:chunk` events) - Add StreamChunk types: Delta, ToolStart, ToolEnd, Complete, Error ### MCP Protocol Implementation - Add MCP JSON-RPC 2.0 types (mcp_types.rs) - Implement stdio-based MCP transport (mcp_transport.rs) - Support tool discovery, execution, and resource operations ### Browser Hand Implementation - Complete browser automation with Playwright-style actions - Support Navigate, Click, Type, Scrape, Screenshot, Wait actions - Add educational Hands: Whiteboard, Slideshow, Speech, Quiz ### Security Enhancements - Implement command whitelist/blacklist for shell_exec tool - Add SSRF protection with private IP blocking - Create security.toml configuration file ## Test Improvements - Fix test import paths (security-utils, setup) - Fix vi.mock hoisting issues with vi.hoisted() - Update test expectations for validateUrl and sanitizeFilename - Add getUnsupportedLocalGatewayStatus mock ## Documentation Updates - Update architecture documentation - Improve configuration reference - Add quick-start guide updates Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,9 @@ import { Users, Loader2, Settings } from 'lucide-react';
|
||||
import { EmptyState } from './components/ui';
|
||||
import { isTauriRuntime, getLocalGatewayStatus, startLocalGateway } from './lib/tauri-gateway';
|
||||
import { useOnboarding } from './lib/use-onboarding';
|
||||
import { intelligenceClient } from './lib/intelligence-client';
|
||||
import { useProposalNotifications, ProposalNotificationHandler } from './lib/useProposalNotifications';
|
||||
import { useToast } from './components/ui/Toast';
|
||||
import type { Clone } from './store/agentStore';
|
||||
|
||||
type View = 'main' | 'settings';
|
||||
@@ -63,6 +66,24 @@ function App() {
|
||||
const { setCurrentAgent, newConversation } = useChatStore();
|
||||
const { isNeeded: onboardingNeeded, isLoading: onboardingLoading, markCompleted } = useOnboarding();
|
||||
|
||||
// Proposal notifications
|
||||
const { toast } = useToast();
|
||||
useProposalNotifications(); // Sets up polling for pending proposals
|
||||
|
||||
// Show toast when new proposals are available
|
||||
useEffect(() => {
|
||||
const handleProposalAvailable = (event: Event) => {
|
||||
const customEvent = event as CustomEvent<{ count: number }>;
|
||||
const { count } = customEvent.detail;
|
||||
toast(`${count} 个新的人格变更提案待审批`, 'info');
|
||||
};
|
||||
|
||||
window.addEventListener('zclaw:proposal-available', handleProposalAvailable);
|
||||
return () => {
|
||||
window.removeEventListener('zclaw:proposal-available', handleProposalAvailable);
|
||||
};
|
||||
}, [toast]);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = 'ZCLAW';
|
||||
}, []);
|
||||
@@ -160,6 +181,41 @@ function App() {
|
||||
// Step 4: Initialize stores with gateway client
|
||||
initializeStores();
|
||||
|
||||
// Step 4.5: Auto-start heartbeat engine for self-evolution
|
||||
try {
|
||||
const defaultAgentId = 'zclaw-main';
|
||||
await intelligenceClient.heartbeat.init(defaultAgentId, {
|
||||
enabled: true,
|
||||
interval_minutes: 30,
|
||||
quiet_hours_start: '22:00',
|
||||
quiet_hours_end: '08:00',
|
||||
notify_channel: 'ui',
|
||||
proactivity_level: 'standard',
|
||||
max_alerts_per_tick: 5,
|
||||
});
|
||||
|
||||
// Sync memory stats to heartbeat engine
|
||||
try {
|
||||
const stats = await intelligenceClient.memory.stats();
|
||||
const taskCount = stats.byType?.['task'] || 0;
|
||||
await intelligenceClient.heartbeat.updateMemoryStats(
|
||||
defaultAgentId,
|
||||
taskCount,
|
||||
stats.totalEntries,
|
||||
stats.storageSizeBytes
|
||||
);
|
||||
console.log('[App] Memory stats synced to heartbeat engine');
|
||||
} catch (statsErr) {
|
||||
console.warn('[App] Failed to sync memory stats:', statsErr);
|
||||
}
|
||||
|
||||
await intelligenceClient.heartbeat.start(defaultAgentId);
|
||||
console.log('[App] Heartbeat engine started for self-evolution');
|
||||
} catch (err) {
|
||||
console.warn('[App] Failed to start heartbeat engine:', err);
|
||||
// Non-critical, continue without heartbeat
|
||||
}
|
||||
|
||||
// Step 5: Bootstrap complete
|
||||
setBootstrapping(false);
|
||||
} catch (err) {
|
||||
@@ -364,6 +420,9 @@ function App() {
|
||||
onReject={handleRejectHand}
|
||||
onClose={handleCloseApprovalModal}
|
||||
/>
|
||||
|
||||
{/* Proposal Notifications Handler */}
|
||||
<ProposalNotificationHandler />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,8 +29,7 @@ import {
|
||||
Loader2
|
||||
} from 'lucide-react';
|
||||
import { useSecurityStore, AuditLogEntry } from '../store/securityStore';
|
||||
|
||||
import { getGatewayClient } from '../lib/gateway-client';
|
||||
import { getClient } from '../store/connectionStore';
|
||||
|
||||
// === Types ===
|
||||
|
||||
@@ -514,7 +513,7 @@ export function AuditLogsPanel() {
|
||||
const auditLogs = useSecurityStore((s) => s.auditLogs);
|
||||
const loadAuditLogs = useSecurityStore((s) => s.loadAuditLogs);
|
||||
const isLoading = useSecurityStore((s) => s.auditLogsLoading);
|
||||
const client = getGatewayClient();
|
||||
const client = getClient();
|
||||
|
||||
// State
|
||||
const [limit, setLimit] = useState(50);
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Wifi, WifiOff, Loader2, RefreshCw, Heart, HeartPulse } from 'lucide-react';
|
||||
import { useConnectionStore } from '../store/connectionStore';
|
||||
import { getGatewayClient } from '../lib/gateway-client';
|
||||
import { useConnectionStore, getClient } from '../store/connectionStore';
|
||||
import {
|
||||
createHealthCheckScheduler,
|
||||
getHealthStatusLabel,
|
||||
@@ -90,7 +89,7 @@ export function ConnectionStatus({
|
||||
|
||||
// Listen for reconnect events
|
||||
useEffect(() => {
|
||||
const client = getGatewayClient();
|
||||
const client = getClient();
|
||||
|
||||
const unsubReconnecting = client.on('reconnecting', (info) => {
|
||||
setReconnectInfo(info as ReconnectInfo);
|
||||
|
||||
@@ -331,7 +331,8 @@ export function IdentityChangeProposalPanel() {
|
||||
setSnapshots(agentSnapshots);
|
||||
} catch (err) {
|
||||
console.error('[IdentityChangeProposal] Failed to approve:', err);
|
||||
setError('审批失败');
|
||||
const message = err instanceof Error ? err.message : '审批失败,请重试';
|
||||
setError(`审批失败: ${message}`);
|
||||
} finally {
|
||||
setProcessingId(null);
|
||||
}
|
||||
@@ -348,7 +349,8 @@ export function IdentityChangeProposalPanel() {
|
||||
setProposals(pendingProposals);
|
||||
} catch (err) {
|
||||
console.error('[IdentityChangeProposal] Failed to reject:', err);
|
||||
setError('拒绝失败');
|
||||
const message = err instanceof Error ? err.message : '拒绝失败,请重试';
|
||||
setError(`拒绝失败: ${message}`);
|
||||
} finally {
|
||||
setProcessingId(null);
|
||||
}
|
||||
@@ -365,7 +367,8 @@ export function IdentityChangeProposalPanel() {
|
||||
setSnapshots(agentSnapshots);
|
||||
} catch (err) {
|
||||
console.error('[IdentityChangeProposal] Failed to restore:', err);
|
||||
setError('恢复失败');
|
||||
const message = err instanceof Error ? err.message : '恢复失败,请重试';
|
||||
setError(`恢复失败: ${message}`);
|
||||
} finally {
|
||||
setProcessingId(null);
|
||||
}
|
||||
|
||||
@@ -116,6 +116,58 @@ const PRIORITY_CONFIG: Record<string, { color: string; bgColor: string }> = {
|
||||
},
|
||||
};
|
||||
|
||||
// === Field to File Mapping ===
|
||||
|
||||
/**
|
||||
* Maps reflection field names to identity file types.
|
||||
* This ensures correct routing of identity change proposals.
|
||||
*/
|
||||
function mapFieldToFile(field: string): 'soul' | 'instructions' {
|
||||
// Direct matches
|
||||
if (field === 'soul' || field === 'instructions') {
|
||||
return field;
|
||||
}
|
||||
|
||||
// Known soul fields (core personality traits)
|
||||
const soulFields = [
|
||||
'personality',
|
||||
'traits',
|
||||
'values',
|
||||
'identity',
|
||||
'character',
|
||||
'essence',
|
||||
'core_behavior',
|
||||
];
|
||||
|
||||
// Known instructions fields (operational guidelines)
|
||||
const instructionsFields = [
|
||||
'guidelines',
|
||||
'rules',
|
||||
'behavior_rules',
|
||||
'response_format',
|
||||
'communication_guidelines',
|
||||
'task_handling',
|
||||
];
|
||||
|
||||
const lowerField = field.toLowerCase();
|
||||
|
||||
// Check explicit mappings
|
||||
if (soulFields.some((f) => lowerField.includes(f))) {
|
||||
return 'soul';
|
||||
}
|
||||
if (instructionsFields.some((f) => lowerField.includes(f))) {
|
||||
return 'instructions';
|
||||
}
|
||||
|
||||
// Fallback heuristics
|
||||
if (lowerField.includes('soul') || lowerField.includes('personality') || lowerField.includes('trait')) {
|
||||
return 'soul';
|
||||
}
|
||||
|
||||
// Default to instructions for operational changes
|
||||
return 'instructions';
|
||||
}
|
||||
|
||||
// === Components ===
|
||||
|
||||
function SentimentBadge({ sentiment }: { sentiment: string }) {
|
||||
@@ -419,6 +471,7 @@ export function ReflectionLog({
|
||||
const [isReflecting, setIsReflecting] = useState(false);
|
||||
const [showConfig, setShowConfig] = useState(false);
|
||||
const [config, setConfig] = useState<ReflectionConfig>(() => loadConfig());
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Persist config changes
|
||||
useEffect(() => {
|
||||
@@ -446,8 +499,24 @@ export function ReflectionLog({
|
||||
|
||||
const handleReflect = useCallback(async () => {
|
||||
setIsReflecting(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await intelligenceClient.reflection.reflect(agentId, []);
|
||||
// Fetch recent memories for analysis
|
||||
const memories = await intelligenceClient.memory.search({
|
||||
agentId,
|
||||
limit: 50, // Get enough memories for pattern analysis
|
||||
});
|
||||
|
||||
// Convert to analysis format
|
||||
const memoriesForAnalysis = memories.map((m) => ({
|
||||
memory_type: m.type,
|
||||
content: m.content,
|
||||
importance: m.importance,
|
||||
access_count: m.accessCount,
|
||||
tags: m.tags,
|
||||
}));
|
||||
|
||||
const result = await intelligenceClient.reflection.reflect(agentId, memoriesForAnalysis);
|
||||
setHistory((prev) => [result, ...prev]);
|
||||
|
||||
// Convert reflection identity_proposals to actual identity proposals
|
||||
@@ -455,13 +524,8 @@ export function ReflectionLog({
|
||||
if (result.identity_proposals && result.identity_proposals.length > 0) {
|
||||
for (const proposal of result.identity_proposals) {
|
||||
try {
|
||||
// Determine which file to modify based on the field
|
||||
const file: 'soul' | 'instructions' =
|
||||
proposal.field === 'soul' || proposal.field === 'instructions'
|
||||
? (proposal.field as 'soul' | 'instructions')
|
||||
: proposal.field.toLowerCase().includes('soul')
|
||||
? 'soul'
|
||||
: 'instructions';
|
||||
// Map field to file type with explicit mapping rules
|
||||
const file = mapFieldToFile(proposal.field);
|
||||
|
||||
// Persist the proposal to the identity system
|
||||
await intelligenceClient.identity.proposeChange(
|
||||
@@ -479,8 +543,10 @@ export function ReflectionLog({
|
||||
const proposals = await intelligenceClient.identity.getPendingProposals(agentId);
|
||||
setPendingProposals(proposals);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ReflectionLog] Reflection failed:', error);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
console.error('[ReflectionLog] Reflection failed:', err);
|
||||
setError(`反思失败: ${errorMessage}`);
|
||||
} finally {
|
||||
setIsReflecting(false);
|
||||
}
|
||||
@@ -559,6 +625,31 @@ export function ReflectionLog({
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Error Banner */}
|
||||
<AnimatePresence>
|
||||
{error && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2 px-4 py-2 bg-red-50 dark:bg-red-900/20 border-b border-red-200 dark:border-red-800">
|
||||
<div className="flex items-center gap-2 text-red-700 dark:text-red-300 text-sm">
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setError(null)}
|
||||
className="p-1 text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-200"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Config Panel */}
|
||||
<AnimatePresence>
|
||||
{showConfig && (
|
||||
|
||||
@@ -82,7 +82,7 @@ export const chatStore = proxy<ChatStore>({
|
||||
agents: [DEFAULT_AGENT],
|
||||
currentAgent: DEFAULT_AGENT,
|
||||
isStreaming: false,
|
||||
currentModel: 'glm-5',
|
||||
currentModel: 'glm-4-flash',
|
||||
sessionKey: null,
|
||||
|
||||
// === Actions ===
|
||||
|
||||
@@ -163,6 +163,7 @@ export const intelligenceStore = proxy<IntelligenceStore>({
|
||||
byAgent: rawStats.byAgent,
|
||||
oldestEntry: rawStats.oldestEntry,
|
||||
newestEntry: rawStats.newestEntry,
|
||||
storageSizeBytes: rawStats.storageSizeBytes ?? 0,
|
||||
};
|
||||
intelligenceStore.memoryStats = stats;
|
||||
} catch (err) {
|
||||
|
||||
@@ -74,6 +74,7 @@ export interface MemoryStats {
|
||||
byAgent: Record<string, number>;
|
||||
oldestEntry: string | null;
|
||||
newestEntry: string | null;
|
||||
storageSizeBytes: number;
|
||||
}
|
||||
|
||||
// === Cache Types ===
|
||||
|
||||
@@ -71,6 +71,7 @@ export interface MemoryStats {
|
||||
by_agent: Record<string, number>;
|
||||
oldest_memory: string | null;
|
||||
newest_memory: string | null;
|
||||
storage_size_bytes: number;
|
||||
}
|
||||
|
||||
// Heartbeat types
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
import {
|
||||
intelligence,
|
||||
type MemoryEntryInput,
|
||||
@@ -49,6 +51,9 @@ import {
|
||||
type CompactionCheck,
|
||||
type CompactionConfig,
|
||||
type MemoryEntryForAnalysis,
|
||||
type PatternObservation,
|
||||
type ImprovementSuggestion,
|
||||
type ReflectionIdentityProposal,
|
||||
type ReflectionResult,
|
||||
type ReflectionState,
|
||||
type ReflectionConfig,
|
||||
@@ -101,6 +106,7 @@ export interface MemoryStats {
|
||||
byAgent: Record<string, number>;
|
||||
oldestEntry: string | null;
|
||||
newestEntry: string | null;
|
||||
storageSizeBytes: number;
|
||||
}
|
||||
|
||||
// === Re-export types from intelligence-backend ===
|
||||
@@ -184,6 +190,7 @@ export function toFrontendStats(backend: BackendMemoryStats): MemoryStats {
|
||||
byAgent: backend.by_agent,
|
||||
oldestEntry: backend.oldest_memory,
|
||||
newestEntry: backend.newest_memory,
|
||||
storageSizeBytes: backend.storage_size_bytes ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -324,6 +331,7 @@ const fallbackMemory = {
|
||||
byAgent,
|
||||
oldestEntry: sorted[0]?.createdAt ?? null,
|
||||
newestEntry: sorted[sorted.length - 1]?.createdAt ?? null,
|
||||
storageSizeBytes: 0, // localStorage-based fallback doesn't track storage size
|
||||
};
|
||||
},
|
||||
|
||||
@@ -403,6 +411,7 @@ const fallbackCompactor = {
|
||||
const fallbackReflection = {
|
||||
_conversationCount: 0,
|
||||
_lastReflection: null as string | null,
|
||||
_history: [] as ReflectionResult[],
|
||||
|
||||
async init(_config?: ReflectionConfig): Promise<void> {
|
||||
// No-op
|
||||
@@ -416,21 +425,130 @@ const fallbackReflection = {
|
||||
return fallbackReflection._conversationCount >= 5;
|
||||
},
|
||||
|
||||
async reflect(_agentId: string, _memories: MemoryEntryForAnalysis[]): Promise<ReflectionResult> {
|
||||
async reflect(agentId: string, memories: MemoryEntryForAnalysis[]): Promise<ReflectionResult> {
|
||||
fallbackReflection._conversationCount = 0;
|
||||
fallbackReflection._lastReflection = new Date().toISOString();
|
||||
|
||||
return {
|
||||
patterns: [],
|
||||
improvements: [],
|
||||
identity_proposals: [],
|
||||
new_memories: 0,
|
||||
// Analyze patterns (simple rule-based implementation)
|
||||
const patterns: PatternObservation[] = [];
|
||||
const improvements: ImprovementSuggestion[] = [];
|
||||
const identityProposals: ReflectionIdentityProposal[] = [];
|
||||
|
||||
// Count memory types
|
||||
const typeCounts: Record<string, number> = {};
|
||||
for (const m of memories) {
|
||||
typeCounts[m.memory_type] = (typeCounts[m.memory_type] || 0) + 1;
|
||||
}
|
||||
|
||||
// Pattern: Too many tasks
|
||||
const taskCount = typeCounts['task'] || 0;
|
||||
if (taskCount >= 5) {
|
||||
const taskMemories = memories.filter(m => m.memory_type === 'task').slice(0, 3);
|
||||
patterns.push({
|
||||
observation: `积累了 ${taskCount} 个待办任务,可能存在任务管理不善`,
|
||||
frequency: taskCount,
|
||||
sentiment: 'negative',
|
||||
evidence: taskMemories.map(m => m.content),
|
||||
});
|
||||
improvements.push({
|
||||
area: '任务管理',
|
||||
suggestion: '清理已完成的任务记忆,对长期未处理的任务降低重要性',
|
||||
priority: 'high',
|
||||
});
|
||||
}
|
||||
|
||||
// Pattern: Strong preference accumulation
|
||||
const prefCount = typeCounts['preference'] || 0;
|
||||
if (prefCount >= 5) {
|
||||
const prefMemories = memories.filter(m => m.memory_type === 'preference').slice(0, 3);
|
||||
patterns.push({
|
||||
observation: `已记录 ${prefCount} 个用户偏好,对用户习惯有较好理解`,
|
||||
frequency: prefCount,
|
||||
sentiment: 'positive',
|
||||
evidence: prefMemories.map(m => m.content),
|
||||
});
|
||||
}
|
||||
|
||||
// Pattern: Lessons learned
|
||||
const lessonCount = typeCounts['lesson'] || 0;
|
||||
if (lessonCount >= 5) {
|
||||
patterns.push({
|
||||
observation: `积累了 ${lessonCount} 条经验教训,知识库在成长`,
|
||||
frequency: lessonCount,
|
||||
sentiment: 'positive',
|
||||
evidence: memories.filter(m => m.memory_type === 'lesson').slice(0, 3).map(m => m.content),
|
||||
});
|
||||
}
|
||||
|
||||
// Pattern: High-access important memories
|
||||
const highAccessMemories = memories.filter(m => m.access_count >= 5 && m.importance >= 7);
|
||||
if (highAccessMemories.length >= 3) {
|
||||
patterns.push({
|
||||
observation: `有 ${highAccessMemories.length} 条高频访问的重要记忆,核心知识正在形成`,
|
||||
frequency: highAccessMemories.length,
|
||||
sentiment: 'positive',
|
||||
evidence: highAccessMemories.slice(0, 3).map(m => m.content),
|
||||
});
|
||||
}
|
||||
|
||||
// Pattern: Low importance memories accumulating
|
||||
const lowImportanceCount = memories.filter(m => m.importance <= 3).length;
|
||||
if (lowImportanceCount > 20) {
|
||||
patterns.push({
|
||||
observation: `有 ${lowImportanceCount} 条低重要性记忆,建议清理`,
|
||||
frequency: lowImportanceCount,
|
||||
sentiment: 'neutral',
|
||||
evidence: [],
|
||||
});
|
||||
improvements.push({
|
||||
area: '记忆管理',
|
||||
suggestion: '执行记忆清理,移除30天以上未访问且重要性低于3的记忆',
|
||||
priority: 'medium',
|
||||
});
|
||||
}
|
||||
|
||||
// Generate identity proposal if negative patterns exist
|
||||
const negativePatterns = patterns.filter(p => p.sentiment === 'negative');
|
||||
if (negativePatterns.length >= 2) {
|
||||
const additions = negativePatterns.map(p => `- 注意: ${p.observation}`).join('\n');
|
||||
identityProposals.push({
|
||||
agent_id: agentId,
|
||||
field: 'instructions',
|
||||
current_value: '...',
|
||||
proposed_value: `\n\n## 自我反思改进\n${additions}`,
|
||||
reason: `基于 ${negativePatterns.length} 个负面模式观察,建议在指令中增加自我改进提醒`,
|
||||
});
|
||||
}
|
||||
|
||||
// Suggestion: User profile enrichment
|
||||
if (prefCount < 3) {
|
||||
improvements.push({
|
||||
area: '用户理解',
|
||||
suggestion: '主动在对话中了解用户偏好(沟通风格、技术栈、工作习惯),丰富用户画像',
|
||||
priority: 'medium',
|
||||
});
|
||||
}
|
||||
|
||||
const result: ReflectionResult = {
|
||||
patterns,
|
||||
improvements,
|
||||
identity_proposals: identityProposals,
|
||||
new_memories: patterns.filter(p => p.frequency >= 3).length + improvements.filter(i => i.priority === 'high').length,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Store in history
|
||||
fallbackReflection._history.push(result);
|
||||
if (fallbackReflection._history.length > 20) {
|
||||
fallbackReflection._history = fallbackReflection._history.slice(-10);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
async getHistory(_limit?: number): Promise<ReflectionResult[]> {
|
||||
return [];
|
||||
async getHistory(limit?: number): Promise<ReflectionResult[]> {
|
||||
const l = limit ?? 10;
|
||||
return fallbackReflection._history.slice(-l).reverse();
|
||||
},
|
||||
|
||||
async getState(): Promise<ReflectionState> {
|
||||
@@ -442,18 +560,87 @@ const fallbackReflection = {
|
||||
},
|
||||
};
|
||||
|
||||
// Fallback Identity API
|
||||
const fallbackIdentities = new Map<string, IdentityFiles>();
|
||||
const fallbackProposals: IdentityChangeProposal[] = [];
|
||||
// Fallback Identity API with localStorage persistence
|
||||
const IDENTITY_STORAGE_KEY = 'zclaw-fallback-identities';
|
||||
const PROPOSALS_STORAGE_KEY = 'zclaw-fallback-proposals';
|
||||
const SNAPSHOTS_STORAGE_KEY = 'zclaw-fallback-snapshots';
|
||||
|
||||
function loadIdentitiesFromStorage(): Map<string, IdentityFiles> {
|
||||
try {
|
||||
const stored = localStorage.getItem(IDENTITY_STORAGE_KEY);
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored) as Record<string, IdentityFiles>;
|
||||
return new Map(Object.entries(parsed));
|
||||
}
|
||||
} catch {
|
||||
console.warn('[IntelligenceClient] Failed to load identities from localStorage');
|
||||
}
|
||||
return new Map();
|
||||
}
|
||||
|
||||
function saveIdentitiesToStorage(identities: Map<string, IdentityFiles>): void {
|
||||
try {
|
||||
const obj = Object.fromEntries(identities);
|
||||
localStorage.setItem(IDENTITY_STORAGE_KEY, JSON.stringify(obj));
|
||||
} catch {
|
||||
console.warn('[IntelligenceClient] Failed to save identities to localStorage');
|
||||
}
|
||||
}
|
||||
|
||||
function loadProposalsFromStorage(): IdentityChangeProposal[] {
|
||||
try {
|
||||
const stored = localStorage.getItem(PROPOSALS_STORAGE_KEY);
|
||||
if (stored) {
|
||||
return JSON.parse(stored) as IdentityChangeProposal[];
|
||||
}
|
||||
} catch {
|
||||
console.warn('[IntelligenceClient] Failed to load proposals from localStorage');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function saveProposalsToStorage(proposals: IdentityChangeProposal[]): void {
|
||||
try {
|
||||
localStorage.setItem(PROPOSALS_STORAGE_KEY, JSON.stringify(proposals));
|
||||
} catch {
|
||||
console.warn('[IntelligenceClient] Failed to save proposals to localStorage');
|
||||
}
|
||||
}
|
||||
|
||||
function loadSnapshotsFromStorage(): IdentitySnapshot[] {
|
||||
try {
|
||||
const stored = localStorage.getItem(SNAPSHOTS_STORAGE_KEY);
|
||||
if (stored) {
|
||||
return JSON.parse(stored) as IdentitySnapshot[];
|
||||
}
|
||||
} catch {
|
||||
console.warn('[IntelligenceClient] Failed to load snapshots from localStorage');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function saveSnapshotsToStorage(snapshots: IdentitySnapshot[]): void {
|
||||
try {
|
||||
localStorage.setItem(SNAPSHOTS_STORAGE_KEY, JSON.stringify(snapshots));
|
||||
} catch {
|
||||
console.warn('[IntelligenceClient] Failed to save snapshots to localStorage');
|
||||
}
|
||||
}
|
||||
|
||||
const fallbackIdentities = loadIdentitiesFromStorage();
|
||||
let fallbackProposals = loadProposalsFromStorage();
|
||||
let fallbackSnapshots = loadSnapshotsFromStorage();
|
||||
|
||||
const fallbackIdentity = {
|
||||
async get(agentId: string): Promise<IdentityFiles> {
|
||||
if (!fallbackIdentities.has(agentId)) {
|
||||
fallbackIdentities.set(agentId, {
|
||||
const defaults: IdentityFiles = {
|
||||
soul: '# Agent Soul\n\nA helpful AI assistant.',
|
||||
instructions: '# Instructions\n\nBe helpful and concise.',
|
||||
user_profile: '# User Profile\n\nNo profile yet.',
|
||||
});
|
||||
};
|
||||
fallbackIdentities.set(agentId, defaults);
|
||||
saveIdentitiesToStorage(fallbackIdentities);
|
||||
}
|
||||
return fallbackIdentities.get(agentId)!;
|
||||
},
|
||||
@@ -476,12 +663,14 @@ const fallbackIdentity = {
|
||||
const files = await fallbackIdentity.get(agentId);
|
||||
files.user_profile = content;
|
||||
fallbackIdentities.set(agentId, files);
|
||||
saveIdentitiesToStorage(fallbackIdentities);
|
||||
},
|
||||
|
||||
async appendUserProfile(agentId: string, addition: string): Promise<void> {
|
||||
const files = await fallbackIdentity.get(agentId);
|
||||
files.user_profile += `\n\n${addition}`;
|
||||
fallbackIdentities.set(agentId, files);
|
||||
saveIdentitiesToStorage(fallbackIdentities);
|
||||
},
|
||||
|
||||
async proposeChange(
|
||||
@@ -502,6 +691,7 @@ const fallbackIdentity = {
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
fallbackProposals.push(proposal);
|
||||
saveProposalsToStorage(fallbackProposals);
|
||||
return proposal;
|
||||
},
|
||||
|
||||
@@ -509,10 +699,30 @@ const fallbackIdentity = {
|
||||
const proposal = fallbackProposals.find(p => p.id === proposalId);
|
||||
if (!proposal) throw new Error('Proposal not found');
|
||||
|
||||
proposal.status = 'approved';
|
||||
const files = await fallbackIdentity.get(proposal.agent_id);
|
||||
|
||||
// Create snapshot before applying change
|
||||
const snapshot: IdentitySnapshot = {
|
||||
id: `snap_${Date.now()}`,
|
||||
agent_id: proposal.agent_id,
|
||||
files: { ...files },
|
||||
timestamp: new Date().toISOString(),
|
||||
reason: `Before applying: ${proposal.reason}`,
|
||||
};
|
||||
fallbackSnapshots.unshift(snapshot);
|
||||
// Keep only last 20 snapshots per agent
|
||||
const agentSnapshots = fallbackSnapshots.filter(s => s.agent_id === proposal.agent_id);
|
||||
if (agentSnapshots.length > 20) {
|
||||
const toRemove = agentSnapshots.slice(20);
|
||||
fallbackSnapshots = fallbackSnapshots.filter(s => !toRemove.includes(s));
|
||||
}
|
||||
saveSnapshotsToStorage(fallbackSnapshots);
|
||||
|
||||
proposal.status = 'approved';
|
||||
files[proposal.file] = proposal.suggested_content;
|
||||
fallbackIdentities.set(proposal.agent_id, files);
|
||||
saveIdentitiesToStorage(fallbackIdentities);
|
||||
saveProposalsToStorage(fallbackProposals);
|
||||
return files;
|
||||
},
|
||||
|
||||
@@ -520,6 +730,7 @@ const fallbackIdentity = {
|
||||
const proposal = fallbackProposals.find(p => p.id === proposalId);
|
||||
if (proposal) {
|
||||
proposal.status = 'rejected';
|
||||
saveProposalsToStorage(fallbackProposals);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -537,16 +748,35 @@ const fallbackIdentity = {
|
||||
if (key in files) {
|
||||
files[key] = content;
|
||||
fallbackIdentities.set(agentId, files);
|
||||
saveIdentitiesToStorage(fallbackIdentities);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async getSnapshots(_agentId: string, _limit?: number): Promise<IdentitySnapshot[]> {
|
||||
return [];
|
||||
async getSnapshots(agentId: string, limit?: number): Promise<IdentitySnapshot[]> {
|
||||
const agentSnapshots = fallbackSnapshots.filter(s => s.agent_id === agentId);
|
||||
return agentSnapshots.slice(0, limit ?? 10);
|
||||
},
|
||||
|
||||
async restoreSnapshot(_agentId: string, _snapshotId: string): Promise<void> {
|
||||
// No-op for fallback
|
||||
async restoreSnapshot(agentId: string, snapshotId: string): Promise<void> {
|
||||
const snapshot = fallbackSnapshots.find(s => s.id === snapshotId && s.agent_id === agentId);
|
||||
if (!snapshot) throw new Error('Snapshot not found');
|
||||
|
||||
// Create a snapshot of current state before restore
|
||||
const currentFiles = await fallbackIdentity.get(agentId);
|
||||
const beforeRestoreSnapshot: IdentitySnapshot = {
|
||||
id: `snap_${Date.now()}`,
|
||||
agent_id: agentId,
|
||||
files: { ...currentFiles },
|
||||
timestamp: new Date().toISOString(),
|
||||
reason: 'Auto-backup before restore',
|
||||
};
|
||||
fallbackSnapshots.unshift(beforeRestoreSnapshot);
|
||||
saveSnapshotsToStorage(fallbackSnapshots);
|
||||
|
||||
// Restore the snapshot
|
||||
fallbackIdentities.set(agentId, { ...snapshot.files });
|
||||
saveIdentitiesToStorage(fallbackIdentities);
|
||||
},
|
||||
|
||||
async listAgents(): Promise<string[]> {
|
||||
@@ -755,6 +985,42 @@ export const intelligenceClient = {
|
||||
}
|
||||
return fallbackHeartbeat.getHistory(agentId, limit);
|
||||
},
|
||||
|
||||
updateMemoryStats: async (
|
||||
agentId: string,
|
||||
taskCount: number,
|
||||
totalEntries: number,
|
||||
storageSizeBytes: number
|
||||
): Promise<void> => {
|
||||
if (isTauriEnv()) {
|
||||
await invoke('heartbeat_update_memory_stats', {
|
||||
agentId,
|
||||
taskCount,
|
||||
totalEntries,
|
||||
storageSizeBytes,
|
||||
});
|
||||
}
|
||||
// Fallback: store in localStorage for non-Tauri environment
|
||||
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 (isTauriEnv()) {
|
||||
await invoke('heartbeat_record_correction', { agentId, correctionType });
|
||||
}
|
||||
// Fallback: store in localStorage for non-Tauri environment
|
||||
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));
|
||||
},
|
||||
},
|
||||
|
||||
compactor: {
|
||||
|
||||
183
desktop/src/lib/useProposalNotifications.ts
Normal file
183
desktop/src/lib/useProposalNotifications.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* Proposal Notifications Hook
|
||||
*
|
||||
* Periodically polls for pending identity change proposals and shows
|
||||
* notifications when new proposals are available.
|
||||
*
|
||||
* Usage:
|
||||
* ```tsx
|
||||
* // In App.tsx or a top-level component
|
||||
* useProposalNotifications();
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import { useChatStore } from '../store/chatStore';
|
||||
import { intelligenceClient, type IdentityChangeProposal } from './intelligence-client';
|
||||
|
||||
// Configuration
|
||||
const POLL_INTERVAL_MS = 60_000; // 1 minute
|
||||
const NOTIFICATION_COOLDOWN_MS = 300_000; // 5 minutes - don't spam notifications
|
||||
|
||||
// Storage key for tracking notified proposals
|
||||
const NOTIFIED_PROPOSALS_KEY = 'zclaw-notified-proposals';
|
||||
|
||||
/**
|
||||
* Get set of already notified proposal IDs
|
||||
*/
|
||||
function getNotifiedProposals(): Set<string> {
|
||||
try {
|
||||
const stored = localStorage.getItem(NOTIFIED_PROPOSALS_KEY);
|
||||
if (stored) {
|
||||
return new Set(JSON.parse(stored) as string[]);
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
return new Set();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save notified proposal IDs
|
||||
*/
|
||||
function saveNotifiedProposals(ids: Set<string>): void {
|
||||
try {
|
||||
// Keep only last 100 IDs to prevent storage bloat
|
||||
const arr = Array.from(ids).slice(-100);
|
||||
localStorage.setItem(NOTIFIED_PROPOSALS_KEY, JSON.stringify(arr));
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for showing proposal notifications
|
||||
*
|
||||
* This hook:
|
||||
* 1. Polls for pending proposals every minute
|
||||
* 2. Shows a toast notification when new proposals are found
|
||||
* 3. Tracks which proposals have already been notified to avoid spam
|
||||
*/
|
||||
export function useProposalNotifications(): {
|
||||
pendingCount: number;
|
||||
refresh: () => Promise<void>;
|
||||
} {
|
||||
const { currentAgent } = useChatStore();
|
||||
const agentId = currentAgent?.id;
|
||||
|
||||
const pendingCountRef = useRef(0);
|
||||
const lastNotificationTimeRef = useRef(0);
|
||||
const notifiedProposalsRef = useRef(getNotifiedProposals());
|
||||
const isPollingRef = useRef(false);
|
||||
|
||||
const checkForNewProposals = useCallback(async () => {
|
||||
if (!agentId || isPollingRef.current) return;
|
||||
|
||||
isPollingRef.current = true;
|
||||
|
||||
try {
|
||||
const proposals = await intelligenceClient.identity.getPendingProposals(agentId);
|
||||
pendingCountRef.current = proposals.length;
|
||||
|
||||
// Find proposals we haven't notified about
|
||||
const newProposals = proposals.filter(
|
||||
(p: IdentityChangeProposal) => !notifiedProposalsRef.current.has(p.id)
|
||||
);
|
||||
|
||||
if (newProposals.length > 0) {
|
||||
const now = Date.now();
|
||||
|
||||
// Check cooldown to avoid spam
|
||||
if (now - lastNotificationTimeRef.current >= NOTIFICATION_COOLDOWN_MS) {
|
||||
// Dispatch custom event for the app to handle
|
||||
// This allows the app to show toast, play sound, etc.
|
||||
const event = new CustomEvent('zclaw:proposal-available', {
|
||||
detail: {
|
||||
count: newProposals.length,
|
||||
proposals: newProposals,
|
||||
},
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
|
||||
lastNotificationTimeRef.current = now;
|
||||
}
|
||||
|
||||
// Mark these proposals as notified
|
||||
for (const p of newProposals) {
|
||||
notifiedProposalsRef.current.add(p.id);
|
||||
}
|
||||
saveNotifiedProposals(notifiedProposalsRef.current);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[ProposalNotifications] Failed to check proposals:', err);
|
||||
} finally {
|
||||
isPollingRef.current = false;
|
||||
}
|
||||
}, [agentId]);
|
||||
|
||||
// Set up polling
|
||||
useEffect(() => {
|
||||
if (!agentId) return;
|
||||
|
||||
// Initial check
|
||||
checkForNewProposals();
|
||||
|
||||
// Set up interval
|
||||
const intervalId = setInterval(checkForNewProposals, POLL_INTERVAL_MS);
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, [agentId, checkForNewProposals]);
|
||||
|
||||
// Listen for visibility change to refresh when app becomes visible
|
||||
useEffect(() => {
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
checkForNewProposals();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
};
|
||||
}, [checkForNewProposals]);
|
||||
|
||||
return {
|
||||
pendingCount: pendingCountRef.current,
|
||||
refresh: checkForNewProposals,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that sets up proposal notification handling
|
||||
*
|
||||
* Place this near the root of the app to enable proposal notifications
|
||||
*/
|
||||
export function ProposalNotificationHandler(): null {
|
||||
// This effect sets up the global event listener for proposal notifications
|
||||
useEffect(() => {
|
||||
const handleProposalAvailable = (event: Event) => {
|
||||
const customEvent = event as CustomEvent<{ count: number }>;
|
||||
const { count } = customEvent.detail;
|
||||
|
||||
// You can integrate with a toast system here
|
||||
console.log(`[ProposalNotifications] ${count} new proposal(s) available`);
|
||||
|
||||
// If using the Toast system from Toast.tsx, you would do:
|
||||
// toast(`${count} 个新的人格变更提案待审批`, 'info');
|
||||
};
|
||||
|
||||
window.addEventListener('zclaw:proposal-available', handleProposalAvailable);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('zclaw:proposal-available', handleProposalAvailable);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default useProposalNotifications;
|
||||
@@ -192,8 +192,8 @@ function mapEventType(eventType: TeamEventType): CollaborationEvent['type'] {
|
||||
function getGatewayClientSafe() {
|
||||
try {
|
||||
// Dynamic import to avoid circular dependency
|
||||
const { getGatewayClient } = require('../lib/gateway-client');
|
||||
return getGatewayClient();
|
||||
const { getClient } = require('../store/connectionStore');
|
||||
return getClient();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { getGatewayClient, AgentStreamDelta } from '../lib/gateway-client';
|
||||
import type { AgentStreamDelta } from '../lib/gateway-client';
|
||||
import { getClient } from './connectionStore';
|
||||
import { intelligenceClient } from '../lib/intelligence-client';
|
||||
import { getMemoryExtractor } from '../lib/memory-extractor';
|
||||
import { getAgentSwarm } from '../lib/agent-swarm';
|
||||
@@ -190,7 +191,7 @@ export const useChatStore = create<ChatState>()(
|
||||
currentAgent: DEFAULT_AGENT,
|
||||
isStreaming: false,
|
||||
isLoading: false,
|
||||
currentModel: 'glm-5',
|
||||
currentModel: 'glm-4-flash',
|
||||
sessionKey: null,
|
||||
|
||||
addMessage: (message) =>
|
||||
@@ -399,7 +400,8 @@ export const useChatStore = create<ChatState>()(
|
||||
set({ isStreaming: true });
|
||||
|
||||
try {
|
||||
const client = getGatewayClient();
|
||||
// Use the connected client from connectionStore (supports both GatewayClient and KernelClient)
|
||||
const client = getClient();
|
||||
|
||||
// Check connection state first
|
||||
const connectionState = useConnectionStore.getState().connectionState;
|
||||
@@ -409,11 +411,23 @@ export const useChatStore = create<ChatState>()(
|
||||
throw new Error(`Not connected (state: ${connectionState})`);
|
||||
}
|
||||
|
||||
// Declare runId before chatStream so callbacks can access it
|
||||
let runId = `run_${Date.now()}`;
|
||||
|
||||
// Try streaming first (OpenFang WebSocket)
|
||||
const { runId } = await client.chatStream(
|
||||
const result = await client.chatStream(
|
||||
enhancedContent,
|
||||
{
|
||||
onDelta: () => { /* Handled by initStreamListener to prevent duplication */ },
|
||||
onDelta: (delta: string) => {
|
||||
// Update message content directly (works for both KernelClient and GatewayClient)
|
||||
set((s) => ({
|
||||
messages: s.messages.map((m) =>
|
||||
m.id === assistantId
|
||||
? { ...m, content: m.content + delta }
|
||||
: m
|
||||
),
|
||||
}));
|
||||
},
|
||||
onTool: (tool: string, input: string, output: string) => {
|
||||
const toolMsg: Message = {
|
||||
id: `tool_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
||||
@@ -494,6 +508,11 @@ export const useChatStore = create<ChatState>()(
|
||||
}
|
||||
);
|
||||
|
||||
// Update runId from the result if available
|
||||
if (result?.runId) {
|
||||
runId = result.runId;
|
||||
}
|
||||
|
||||
if (!sessionKey) {
|
||||
set({ sessionKey: effectiveSessionKey });
|
||||
}
|
||||
@@ -530,9 +549,9 @@ export const useChatStore = create<ChatState>()(
|
||||
communicationStyle: style || 'parallel',
|
||||
});
|
||||
|
||||
// Set up executor that uses gateway client
|
||||
// Set up executor that uses the connected client
|
||||
swarm.setExecutor(async (agentId: string, prompt: string, context?: string) => {
|
||||
const client = getGatewayClient();
|
||||
const client = getClient();
|
||||
const fullPrompt = context ? `${context}\n\n${prompt}` : prompt;
|
||||
const result = await client.chat(fullPrompt, { agentId: agentId.startsWith('clone_') ? undefined : agentId });
|
||||
return result?.response || '(无响应)';
|
||||
@@ -566,7 +585,13 @@ export const useChatStore = create<ChatState>()(
|
||||
},
|
||||
|
||||
initStreamListener: () => {
|
||||
const client = getGatewayClient();
|
||||
const client = getClient();
|
||||
|
||||
// Check if client supports onAgentStream (GatewayClient does, KernelClient doesn't)
|
||||
if (!('onAgentStream' in client)) {
|
||||
// KernelClient handles streaming via chatStream callbacks, no separate listener needed
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const unsubscribe = client.onAgentStream((delta: AgentStreamDelta) => {
|
||||
const state = get();
|
||||
|
||||
@@ -25,6 +25,7 @@ import { useSecurityStore } from './securityStore';
|
||||
import { useSessionStore } from './sessionStore';
|
||||
import { useChatStore } from './chatStore';
|
||||
import type { GatewayClient, ConnectionState } from '../lib/gateway-client';
|
||||
import type { KernelClient } from '../lib/kernel-client';
|
||||
import type { GatewayModelChoice } from '../lib/gateway-config';
|
||||
import type { LocalGatewayStatus } from '../lib/tauri-gateway';
|
||||
import type { Hand, HandRun, Trigger, Approval, ApprovalStatus } from './handStore';
|
||||
@@ -233,7 +234,7 @@ interface GatewayFacade {
|
||||
localGateway: LocalGatewayStatus;
|
||||
localGatewayBusy: boolean;
|
||||
isLoading: boolean;
|
||||
client: GatewayClient;
|
||||
client: GatewayClient | KernelClient;
|
||||
|
||||
// Data
|
||||
clones: Clone[];
|
||||
|
||||
@@ -207,9 +207,9 @@ export const useOfflineStore = create<OfflineStore>()(
|
||||
get().updateMessageStatus(msg.id, 'sending');
|
||||
|
||||
try {
|
||||
// Import gateway client dynamically to avoid circular dependency
|
||||
const { getGatewayClient } = await import('../lib/gateway-client');
|
||||
const client = getGatewayClient();
|
||||
// Use connected client from connectionStore (supports both GatewayClient and KernelClient)
|
||||
const { getClient } = await import('./connectionStore');
|
||||
const client = getClient();
|
||||
|
||||
await client.chat(msg.content, {
|
||||
sessionKey: msg.sessionKey,
|
||||
|
||||
Reference in New Issue
Block a user