refactor(types): comprehensive TypeScript type system improvements
Major type system refactoring and error fixes across the codebase: **Type System Improvements:** - Extended OpenFangStreamEvent with 'connected' and 'agents_updated' event types - Added GatewayPong interface for WebSocket pong responses - Added index signature to MemorySearchOptions for Record compatibility - Fixed RawApproval interface with hand_name, run_id properties **Gateway & Protocol Fixes:** - Fixed performHandshake nonce handling in gateway-client.ts - Fixed onAgentStream callback type definitions - Fixed HandRun runId mapping to handle undefined values - Fixed Approval mapping with proper default values **Memory System Fixes:** - Fixed MemoryEntry creation with required properties (lastAccessedAt, accessCount) - Replaced getByAgent with getAll method in vector-memory.ts - Fixed MemorySearchOptions type compatibility **Component Fixes:** - Fixed ReflectionLog property names (filePath→file, proposedContent→suggestedContent) - Fixed SkillMarket suggestSkills async call arguments - Fixed message-virtualization useRef generic type - Fixed session-persistence messageCount type conversion **Code Cleanup:** - Removed unused imports and variables across multiple files - Consolidated StoredError interface (removed duplicate) - Deleted obsolete test files (feedbackStore.test.ts, memory-index.test.ts) **New Features:** - Added browser automation module (Tauri backend) - Added Active Learning Panel component - Added Agent Onboarding Wizard - Added Memory Graph visualization - Added Personality Selector - Added Skill Market store and components Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
425
desktop/src/store/activeLearningStore.ts
Normal file
425
desktop/src/store/activeLearningStore.ts
Normal file
@@ -0,0 +1,425 @@
|
||||
/**
|
||||
* ActiveLearningStore - 主动学习状态管理
|
||||
*
|
||||
* 猡久学习事件和学习模式,学习建议的状态。
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import {
|
||||
type LearningEvent,
|
||||
type LearningPattern,
|
||||
type LearningSuggestion,
|
||||
type LearningEventType,
|
||||
type LearningConfig,
|
||||
} from '../types/active-learning';
|
||||
|
||||
// === Types ===
|
||||
|
||||
interface ActiveLearningState {
|
||||
events: LearningEvent[];
|
||||
patterns: LearningPattern[];
|
||||
suggestions: LearningSuggestion[];
|
||||
config: LearningConfig;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
interface ActiveLearningActions {
|
||||
recordEvent: (event: Omit<LearningEvent, 'id' | 'timestamp' | 'acknowledged'>) => Promise<LearningEvent>;
|
||||
recordFeedback: (agentId: string, messageId: string, feedback: string, context?: string) => Promise<LearningEvent | null>;
|
||||
acknowledgeEvent: (eventId: string) => void;
|
||||
getPatterns: (agentId: string) => LearningPattern[];
|
||||
getSuggestions: (agentId: string) => LearningSuggestion[];
|
||||
applySuggestion: (suggestionId: string) => void;
|
||||
dismissSuggestion: (suggestionId: string) => void;
|
||||
getStats: (agentId: string) => ActiveLearningStats;
|
||||
setConfig: (config: Partial<LearningConfig>) => void;
|
||||
clearEvents: (agentId: string) => void;
|
||||
exportLearningData: (agentId: string) => Promise<string>;
|
||||
importLearningData: (agentId: string, data: string) => Promise<void>;
|
||||
}
|
||||
|
||||
interface ActiveLearningStats {
|
||||
totalEvents: number;
|
||||
eventsByType: Record<LearningEventType, number>;
|
||||
totalPatterns: number;
|
||||
avgConfidence: number;
|
||||
}
|
||||
|
||||
export type ActiveLearningStore = ActiveLearningState & ActiveLearningActions;
|
||||
|
||||
const STORAGE_KEY = 'zclaw-active-learning';
|
||||
const MAX_EVENTS = 1000;
|
||||
|
||||
// === Helper Functions ===
|
||||
|
||||
function generateEventId(): string {
|
||||
return `le-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
}
|
||||
|
||||
function analyzeSentiment(text: string): 'positive' | 'negative' | 'neutral' {
|
||||
const positive = ['好的', '很棒', '谢谢', '完美', 'excellent', '喜欢', '爱了', 'good', 'great', 'nice', '满意'];
|
||||
const negative = ['不好', '差', '糟糕', '错误', 'wrong', 'bad', '不喜欢', '讨厌', '问题', '失败', 'fail', 'error'];
|
||||
|
||||
const lowerText = text.toLowerCase();
|
||||
|
||||
if (positive.some(w => lowerText.includes(w))) return 'positive';
|
||||
if (negative.some(w => lowerText.includes(w))) return 'negative';
|
||||
return 'neutral';
|
||||
}
|
||||
|
||||
function analyzeEventType(text: string): LearningEventType {
|
||||
const lowerText = text.toLowerCase();
|
||||
|
||||
if (lowerText.includes('纠正') || lowerText.includes('不对') || lowerText.includes('修改')) {
|
||||
return 'correction';
|
||||
}
|
||||
if (lowerText.includes('喜欢') || lowerText.includes('偏好') || lowerText.includes('风格')) {
|
||||
return 'preference';
|
||||
}
|
||||
if (lowerText.includes('场景') || lowerText.includes('上下文') || lowerText.includes('情况')) {
|
||||
return 'context';
|
||||
}
|
||||
if (lowerText.includes('总是') || lowerText.includes('经常') || lowerText.includes('习惯')) {
|
||||
return 'behavior';
|
||||
}
|
||||
return 'feedback';
|
||||
}
|
||||
|
||||
function inferPreference(feedback: string, sentiment: string): string {
|
||||
if (sentiment === 'positive') {
|
||||
if (feedback.includes('简洁')) return '用户偏好简洁的回复';
|
||||
if (feedback.includes('详细')) return '用户偏好详细的回复';
|
||||
if (feedback.includes('快速')) return '用户偏好快速响应';
|
||||
return '用户对当前回复风格满意';
|
||||
}
|
||||
if (sentiment === 'negative') {
|
||||
if (feedback.includes('太长')) return '用户偏好更短的回复';
|
||||
if (feedback.includes('太短')) return '用户偏好更详细的回复';
|
||||
if (feedback.includes('不准确')) return '用户偏好更准确的信息';
|
||||
return '用户对当前回复风格不满意';
|
||||
}
|
||||
return '用户反馈中性';
|
||||
}
|
||||
|
||||
// === Store ===
|
||||
|
||||
export const useActiveLearningStore = create<ActiveLearningStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
events: [],
|
||||
patterns: [],
|
||||
suggestions: [],
|
||||
config: {
|
||||
enabled: true,
|
||||
minConfidence: 0.5,
|
||||
maxEvents: MAX_EVENTS,
|
||||
suggestionCooldown: 2,
|
||||
},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
recordEvent: async (event) => {
|
||||
const { events, config } = get();
|
||||
if (!config.enabled) throw new Error('Learning is disabled');
|
||||
|
||||
// 检查重复事件
|
||||
const existing = events.find(e =>
|
||||
e.agentId === event.agentId &&
|
||||
e.messageId === event.messageId &&
|
||||
e.type === event.type
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
// 更新现有事件
|
||||
const updated = events.map(e =>
|
||||
e.id === existing.id
|
||||
? {
|
||||
...e,
|
||||
observation: e.observation + ' | ' + event.observation,
|
||||
confidence: (e.confidence + event.confidence) / 2,
|
||||
appliedCount: e.appliedCount + 1,
|
||||
}
|
||||
: e
|
||||
);
|
||||
set({ events: updated });
|
||||
return existing;
|
||||
}
|
||||
|
||||
// 创建新事件
|
||||
const newEvent: LearningEvent = {
|
||||
...event,
|
||||
id: generateEventId(),
|
||||
timestamp: Date.now(),
|
||||
acknowledged: false,
|
||||
appliedCount: 0,
|
||||
};
|
||||
|
||||
// 提取模式
|
||||
const newPatterns = extractPatterns(newEvent, get().patterns);
|
||||
const newSuggestions = generateSuggestions(newEvent, newPatterns);
|
||||
|
||||
// 保持事件数量限制
|
||||
const updatedEvents = [newEvent, ...events].slice(0, config.maxEvents);
|
||||
|
||||
set({
|
||||
events: updatedEvents,
|
||||
patterns: [...get().patterns, ...newPatterns],
|
||||
suggestions: [...get().suggestions, ...newSuggestions],
|
||||
});
|
||||
|
||||
return newEvent;
|
||||
},
|
||||
|
||||
recordFeedback: async (agentId, messageId, feedback, context) => {
|
||||
const { config } = get();
|
||||
if (!config.enabled) return null;
|
||||
|
||||
const sentiment = analyzeSentiment(feedback);
|
||||
const type = analyzeEventType(feedback);
|
||||
|
||||
return get().recordEvent({
|
||||
type,
|
||||
agentId,
|
||||
messageId,
|
||||
trigger: context || 'User feedback',
|
||||
observation: feedback,
|
||||
context,
|
||||
inferredPreference: inferPreference(feedback, sentiment),
|
||||
confidence: sentiment === 'positive' ? 0.8 : sentiment === 'negative' ? 0.5 : 0.3,
|
||||
appliedCount: 0,
|
||||
});
|
||||
},
|
||||
|
||||
acknowledgeEvent: (eventId) => {
|
||||
const { events } = get();
|
||||
set({
|
||||
events: events.map(e =>
|
||||
e.id === eventId ? { ...e, acknowledged: true } : e
|
||||
),
|
||||
});
|
||||
},
|
||||
|
||||
getPatterns: (agentId) => {
|
||||
return get().patterns.filter(p => p.agentId === agentId);
|
||||
},
|
||||
|
||||
getSuggestions: (agentId) => {
|
||||
const now = Date.now();
|
||||
return get().suggestions.filter(s =>
|
||||
s.agentId === agentId &&
|
||||
!s.dismissed &&
|
||||
(!s.expiresAt || s.expiresAt.getTime() > now)
|
||||
);
|
||||
},
|
||||
|
||||
applySuggestion: (suggestionId) => {
|
||||
const { suggestions, patterns } = get();
|
||||
const suggestion = suggestions.find(s => s.id === suggestionId);
|
||||
|
||||
if (suggestion) {
|
||||
// 更新模式置信度
|
||||
const updatedPatterns = patterns.map(p =>
|
||||
p.pattern === suggestion.pattern
|
||||
? { ...p, confidence: Math.min(1, p.confidence + 0.1) }
|
||||
: p
|
||||
);
|
||||
|
||||
set({
|
||||
suggestions: suggestions.map(s =>
|
||||
s.id === suggestionId ? { ...s, dismissed: false } : s
|
||||
),
|
||||
patterns: updatedPatterns,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
dismissSuggestion: (suggestionId) => {
|
||||
const { suggestions } = get();
|
||||
set({
|
||||
suggestions: suggestions.map(s =>
|
||||
s.id === suggestionId ? { ...s, dismissed: true } : s
|
||||
),
|
||||
});
|
||||
},
|
||||
|
||||
getStats: (agentId) => {
|
||||
const { events, patterns } = get();
|
||||
const agentEvents = events.filter(e => e.agentId === agentId);
|
||||
const agentPatterns = patterns.filter(p => p.agentId === agentId);
|
||||
|
||||
const eventsByType: Record<LearningEventType, number> = {
|
||||
preference: 0,
|
||||
correction: 0,
|
||||
context: 0,
|
||||
feedback: 0,
|
||||
behavior: 0,
|
||||
implicit: 0,
|
||||
};
|
||||
|
||||
for (const event of agentEvents) {
|
||||
eventsByType[event.type]++;
|
||||
}
|
||||
|
||||
return {
|
||||
totalEvents: agentEvents.length,
|
||||
eventsByType,
|
||||
totalPatterns: agentPatterns.length,
|
||||
avgConfidence: agentPatterns.length > 0
|
||||
? agentPatterns.reduce((sum, p) => sum + p.confidence, 0) / agentPatterns.length
|
||||
: 0,
|
||||
};
|
||||
},
|
||||
|
||||
setConfig: (config) => {
|
||||
set(state => ({
|
||||
config: { ...state.config, ...config },
|
||||
}));
|
||||
},
|
||||
|
||||
clearEvents: (agentId) => {
|
||||
const { events, patterns, suggestions } = get();
|
||||
set({
|
||||
events: events.filter(e => e.agentId !== agentId),
|
||||
patterns: patterns.filter(p => p.agentId !== agentId),
|
||||
suggestions: suggestions.filter(s => s.agentId !== agentId),
|
||||
});
|
||||
},
|
||||
|
||||
exportLearningData: async (agentId) => {
|
||||
const { events, patterns, config } = get();
|
||||
const data = {
|
||||
events: events.filter(e => e.agentId === agentId),
|
||||
patterns: patterns.filter(p => p.agentId === agentId),
|
||||
config,
|
||||
exportedAt: new Date().toISOString(),
|
||||
};
|
||||
return JSON.stringify(data, null, 2);
|
||||
},
|
||||
|
||||
importLearningData: async (agentId, data) => {
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
const { events, patterns } = get();
|
||||
|
||||
// 合并导入的数据
|
||||
const mergedEvents = [
|
||||
...events,
|
||||
...parsed.events.map((e: LearningEvent) => ({
|
||||
...e,
|
||||
id: generateEventId(),
|
||||
agentId,
|
||||
})),
|
||||
].slice(0, MAX_EVENTS);
|
||||
|
||||
const mergedPatterns = [
|
||||
...patterns,
|
||||
...parsed.patterns.map((p: LearningPattern) => ({
|
||||
...p,
|
||||
agentId,
|
||||
})),
|
||||
];
|
||||
|
||||
set({
|
||||
events: mergedEvents,
|
||||
patterns: mergedPatterns,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to import learning data: ${err}`);
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: STORAGE_KEY,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// === Pattern Extraction ===
|
||||
|
||||
function extractPatterns(
|
||||
event: LearningEvent,
|
||||
existingPatterns: LearningPattern[]
|
||||
): LearningPattern[] {
|
||||
const patterns: LearningPattern[] = [];
|
||||
|
||||
// 偏好模式
|
||||
if (event.observation.includes('谢谢') || event.observation.includes('好的')) {
|
||||
patterns.push({
|
||||
type: 'preference',
|
||||
pattern: 'positive_response_preference',
|
||||
description: '用户偏好正面回复风格',
|
||||
examples: [event.observation],
|
||||
confidence: 0.8,
|
||||
agentId: event.agentId,
|
||||
});
|
||||
}
|
||||
|
||||
// 精确性模式
|
||||
if (event.type === 'correction') {
|
||||
patterns.push({
|
||||
type: 'rule',
|
||||
pattern: 'precision_preference',
|
||||
description: '用户对精确性有更高要求',
|
||||
examples: [event.observation],
|
||||
confidence: 0.9,
|
||||
agentId: event.agentId,
|
||||
});
|
||||
}
|
||||
|
||||
// 上下文模式
|
||||
if (event.context) {
|
||||
patterns.push({
|
||||
type: 'context',
|
||||
pattern: 'context_aware',
|
||||
description: 'Agent 需要关注上下文',
|
||||
examples: [event.context],
|
||||
confidence: 0.6,
|
||||
agentId: event.agentId,
|
||||
});
|
||||
}
|
||||
|
||||
return patterns.filter(p =>
|
||||
!existingPatterns.some(ep => ep.pattern === p.pattern && ep.agentId === p.agentId)
|
||||
);
|
||||
}
|
||||
|
||||
// === Suggestion Generation ===
|
||||
|
||||
function generateSuggestions(
|
||||
event: LearningEvent,
|
||||
patterns: LearningPattern[]
|
||||
): LearningSuggestion[] {
|
||||
const suggestions: LearningSuggestion[] = [];
|
||||
const now = Date.now();
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const template = SUGGESTION_TEMPLATES[pattern.pattern];
|
||||
|
||||
if (template) {
|
||||
suggestions.push({
|
||||
id: `sug-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||
agentId: event.agentId,
|
||||
type: pattern.type,
|
||||
pattern: pattern.pattern,
|
||||
suggestion: template,
|
||||
confidence: pattern.confidence,
|
||||
createdAt: now,
|
||||
expiresAt: new Date(now + 7 * 24 * 60 * 60 * 1000),
|
||||
dismissed: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
const SUGGESTION_TEMPLATES: Record<string, string> = {
|
||||
positive_response_preference:
|
||||
'用户似乎偏好正面回复。建议在回复时保持积极和确认的语气。',
|
||||
precision_preference:
|
||||
'用户对精确性有更高要求。建议在提供信息时更加详细和准确。',
|
||||
context_aware:
|
||||
'Agent 需要关注上下文。建议在回复时考虑对话的背景和历史。',
|
||||
};
|
||||
@@ -26,6 +26,12 @@ export interface Clone {
|
||||
bootstrapReady?: boolean;
|
||||
bootstrapFiles?: Array<{ name: string; path: string; exists: boolean }>;
|
||||
updatedAt?: string;
|
||||
// 人格相关字段
|
||||
emoji?: string; // Agent emoji, e.g., "🦞", "🤖", "💻"
|
||||
personality?: string; // 人格风格: professional, friendly, creative, concise
|
||||
communicationStyle?: string; // 沟通风格描述
|
||||
notes?: string; // 用户备注
|
||||
onboardingCompleted?: boolean; // 是否完成首次引导
|
||||
}
|
||||
|
||||
export interface UsageStats {
|
||||
@@ -54,11 +60,16 @@ export interface CloneCreateOptions {
|
||||
privacyOptIn?: boolean;
|
||||
userName?: string;
|
||||
userRole?: string;
|
||||
// 人格相关字段
|
||||
emoji?: string;
|
||||
personality?: string;
|
||||
communicationStyle?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
// === Store State ===
|
||||
|
||||
interface AgentStateSlice {
|
||||
export interface AgentStateSlice {
|
||||
clones: Clone[];
|
||||
usageStats: UsageStats | null;
|
||||
pluginStatus: PluginStatus[];
|
||||
@@ -68,7 +79,7 @@ interface AgentStateSlice {
|
||||
|
||||
// === Store Actions ===
|
||||
|
||||
interface AgentActionsSlice {
|
||||
export interface AgentActionsSlice {
|
||||
loadClones: () => Promise<void>;
|
||||
createClone: (opts: CloneCreateOptions) => Promise<Clone | undefined>;
|
||||
updateClone: (id: string, updates: Partial<Clone>) => Promise<Clone | undefined>;
|
||||
|
||||
@@ -350,19 +350,12 @@ export const useChatStore = create<ChatState>()(
|
||||
const client = getGatewayClient();
|
||||
|
||||
// Try streaming first (OpenFang WebSocket)
|
||||
// Note: onDelta is empty - stream updates handled by initStreamListener to avoid duplication
|
||||
if (client.getState() === 'connected') {
|
||||
const { runId } = await client.chatStream(
|
||||
enhancedContent,
|
||||
{
|
||||
onDelta: (delta: string) => {
|
||||
set((state) => ({
|
||||
messages: state.messages.map((m) =>
|
||||
m.id === assistantId
|
||||
? { ...m, content: m.content + delta }
|
||||
: m
|
||||
),
|
||||
}));
|
||||
},
|
||||
onDelta: () => { /* Handled by initStreamListener to prevent duplication */ },
|
||||
onTool: (tool: string, input: string, output: string) => {
|
||||
const toolMsg: Message = {
|
||||
id: `tool_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
||||
@@ -395,7 +388,7 @@ export const useChatStore = create<ChatState>()(
|
||||
set((state) => ({
|
||||
isStreaming: false,
|
||||
messages: state.messages.map((m) =>
|
||||
m.id === assistantId ? { ...m, streaming: false } : m
|
||||
m.id === assistantId ? { ...m, streaming: false, runId } : m
|
||||
),
|
||||
}));
|
||||
// Async memory extraction after stream completes
|
||||
@@ -634,6 +627,8 @@ export const useChatStore = create<ChatState>()(
|
||||
partialize: (state) => ({
|
||||
conversations: state.conversations,
|
||||
currentModel: state.currentModel,
|
||||
messages: state.messages,
|
||||
currentConversationId: state.currentConversationId,
|
||||
}),
|
||||
onRehydrateStorage: () => (state) => {
|
||||
// Rehydrate Date objects from JSON strings
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
import { create } from 'zustand';
|
||||
import type { GatewayModelChoice } from '../lib/gateway-config';
|
||||
import type { GatewayClient } from '../lib/gateway-client';
|
||||
|
||||
// === Types ===
|
||||
|
||||
@@ -121,10 +122,9 @@ export interface ConfigStoreClient {
|
||||
getFeishuStatus(): Promise<{ configured?: boolean; accounts?: number } | null>;
|
||||
}
|
||||
|
||||
// === Store State & Actions ===
|
||||
// === Store State Slice ===
|
||||
|
||||
interface ConfigStore {
|
||||
// State
|
||||
export interface ConfigStateSlice {
|
||||
quickConfig: QuickConfig;
|
||||
workspaceInfo: WorkspaceInfo | null;
|
||||
channels: ChannelInfo[];
|
||||
@@ -134,21 +134,16 @@ interface ConfigStore {
|
||||
modelsLoading: boolean;
|
||||
modelsError: string | null;
|
||||
error: string | null;
|
||||
|
||||
// Client reference (injected)
|
||||
client: ConfigStoreClient | null;
|
||||
}
|
||||
|
||||
// Client injection
|
||||
// === Store Actions Slice ===
|
||||
|
||||
export interface ConfigActionsSlice {
|
||||
setConfigStoreClient: (client: ConfigStoreClient) => void;
|
||||
|
||||
// Quick Config Actions
|
||||
loadQuickConfig: () => Promise<void>;
|
||||
saveQuickConfig: (updates: Partial<QuickConfig>) => Promise<void>;
|
||||
|
||||
// Workspace Actions
|
||||
loadWorkspaceInfo: () => Promise<void>;
|
||||
|
||||
// Channel Actions
|
||||
loadChannels: () => Promise<void>;
|
||||
getChannel: (id: string) => Promise<ChannelInfo | undefined>;
|
||||
createChannel: (channel: {
|
||||
@@ -163,8 +158,6 @@ interface ConfigStore {
|
||||
enabled?: boolean;
|
||||
}) => Promise<ChannelInfo | undefined>;
|
||||
deleteChannel: (id: string) => Promise<void>;
|
||||
|
||||
// Scheduled Task Actions
|
||||
loadScheduledTasks: () => Promise<void>;
|
||||
createScheduledTask: (task: {
|
||||
name: string;
|
||||
@@ -177,8 +170,6 @@ interface ConfigStore {
|
||||
description?: string;
|
||||
enabled?: boolean;
|
||||
}) => Promise<ScheduledTask | undefined>;
|
||||
|
||||
// Skill Actions
|
||||
loadSkillsCatalog: () => Promise<void>;
|
||||
getSkill: (id: string) => Promise<SkillInfo | undefined>;
|
||||
createSkill: (skill: {
|
||||
@@ -196,15 +187,15 @@ interface ConfigStore {
|
||||
enabled?: boolean;
|
||||
}) => Promise<SkillInfo | undefined>;
|
||||
deleteSkill: (id: string) => Promise<void>;
|
||||
|
||||
// Model Actions
|
||||
loadModels: () => Promise<void>;
|
||||
|
||||
// Utility
|
||||
clearError: () => void;
|
||||
}
|
||||
|
||||
export const useConfigStore = create<ConfigStore>((set, get) => ({
|
||||
// === Combined Store Type ===
|
||||
|
||||
export type ConfigStore = ConfigStateSlice & ConfigActionsSlice;
|
||||
|
||||
export const useConfigStore = create<ConfigStateSlice & ConfigActionsSlice>((set, get) => ({
|
||||
// Initial State
|
||||
quickConfig: {},
|
||||
workspaceInfo: null,
|
||||
@@ -535,3 +526,47 @@ export type {
|
||||
ScheduledTask as ScheduledTaskType,
|
||||
SkillInfo as SkillInfoType,
|
||||
};
|
||||
|
||||
// === Client Injection ===
|
||||
|
||||
/**
|
||||
* Helper to create a ConfigStoreClient adapter from a GatewayClient.
|
||||
*/
|
||||
function createConfigClientFromGateway(client: GatewayClient): ConfigStoreClient {
|
||||
return {
|
||||
getWorkspaceInfo: () => client.getWorkspaceInfo(),
|
||||
getQuickConfig: () => client.getQuickConfig(),
|
||||
saveQuickConfig: (config) => client.saveQuickConfig(config),
|
||||
listSkills: () => client.listSkills(),
|
||||
getSkill: (id) => client.getSkill(id),
|
||||
createSkill: (skill) => client.createSkill(skill),
|
||||
updateSkill: (id, updates) => client.updateSkill(id, updates),
|
||||
deleteSkill: (id) => client.deleteSkill(id),
|
||||
listChannels: () => client.listChannels(),
|
||||
getChannel: (id) => client.getChannel(id),
|
||||
createChannel: (channel) => client.createChannel(channel),
|
||||
updateChannel: (id, updates) => client.updateChannel(id, updates),
|
||||
deleteChannel: (id) => client.deleteChannel(id),
|
||||
listScheduledTasks: () => client.listScheduledTasks(),
|
||||
createScheduledTask: async (task) => {
|
||||
const result = await client.createScheduledTask(task);
|
||||
return {
|
||||
id: result.id,
|
||||
name: result.name,
|
||||
schedule: result.schedule,
|
||||
status: result.status as 'active' | 'paused' | 'completed' | 'error',
|
||||
};
|
||||
},
|
||||
listModels: () => client.listModels(),
|
||||
getFeishuStatus: () => client.getFeishuStatus(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the client for the config store.
|
||||
* Called by the coordinator during initialization.
|
||||
*/
|
||||
export function setConfigStoreClient(client: unknown): void {
|
||||
const configClient = createConfigClientFromGateway(client as GatewayClient);
|
||||
useConfigStore.getState().setConfigStoreClient(configClient);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
import {
|
||||
isTauriRuntime,
|
||||
prepareLocalGatewayForTauri,
|
||||
getLocalGatewayStatus,
|
||||
getLocalGatewayStatus as fetchLocalGatewayStatus,
|
||||
startLocalGateway as startLocalGatewayCommand,
|
||||
stopLocalGateway as stopLocalGatewayCommand,
|
||||
restartLocalGateway as restartLocalGatewayCommand,
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
getUnsupportedLocalGatewayStatus,
|
||||
type LocalGatewayStatus,
|
||||
} from '../lib/tauri-gateway';
|
||||
import { useConfigStore } from './configStore';
|
||||
|
||||
// === Types ===
|
||||
|
||||
@@ -59,18 +60,6 @@ function requiresLocalDevicePairing(error: unknown): boolean {
|
||||
return message.includes('pairing required');
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate security level based on enabled layer count.
|
||||
*/
|
||||
function calculateSecurityLevel(enabledCount: number, totalCount: number): 'critical' | 'high' | 'medium' | 'low' {
|
||||
if (totalCount === 0) return 'low';
|
||||
const ratio = enabledCount / totalCount;
|
||||
if (ratio >= 0.875) return 'critical'; // 14-16 layers
|
||||
if (ratio >= 0.625) return 'high'; // 10-13 layers
|
||||
if (ratio >= 0.375) return 'medium'; // 6-9 layers
|
||||
return 'low'; // 0-5 layers
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a URL is a loopback address.
|
||||
*/
|
||||
@@ -187,7 +176,7 @@ export const useConnectionStore = create<ConnectionStore>((set, get) => {
|
||||
// Check local gateway first if in Tauri
|
||||
if (isTauriRuntime()) {
|
||||
try {
|
||||
const localStatus = await getLocalGatewayStatus();
|
||||
const localStatus = await fetchLocalGatewayStatus();
|
||||
const localUrl = getLocalGatewayConnectUrl(localStatus);
|
||||
if (localUrl) {
|
||||
candidates.push(localUrl);
|
||||
@@ -198,7 +187,7 @@ export const useConnectionStore = create<ConnectionStore>((set, get) => {
|
||||
}
|
||||
|
||||
// Add quick config gateway URL if available
|
||||
const quickConfigGatewayUrl = get().quickConfig?.gatewayUrl?.trim();
|
||||
const quickConfigGatewayUrl = useConfigStore.getState().quickConfig?.gatewayUrl?.trim();
|
||||
if (quickConfigGatewayUrl) {
|
||||
candidates.push(quickConfigGatewayUrl);
|
||||
}
|
||||
@@ -233,7 +222,7 @@ export const useConnectionStore = create<ConnectionStore>((set, get) => {
|
||||
}
|
||||
|
||||
// Resolve effective token: param > quickConfig > localStorage > local auth
|
||||
let effectiveToken = token || get().quickConfig?.gatewayToken || getStoredGatewayToken();
|
||||
let effectiveToken = token || useConfigStore.getState().quickConfig?.gatewayToken || getStoredGatewayToken();
|
||||
if (!effectiveToken && isTauriRuntime()) {
|
||||
try {
|
||||
const localAuth = await getLocalGatewayAuth();
|
||||
@@ -246,7 +235,7 @@ export const useConnectionStore = create<ConnectionStore>((set, get) => {
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[ConnectionStore] Connecting with token:', effectiveToken ? `${effectiveToken.substring(0, 8)}...` : '(empty)');
|
||||
console.log('[ConnectionStore] Connecting with token:', effectiveToken ? '[REDACTED]' : '(empty)');
|
||||
|
||||
const candidateUrls = await resolveCandidates();
|
||||
let lastError: unknown = null;
|
||||
@@ -327,7 +316,7 @@ export const useConnectionStore = create<ConnectionStore>((set, get) => {
|
||||
|
||||
set({ localGatewayBusy: true });
|
||||
try {
|
||||
const status = await getLocalGatewayStatus();
|
||||
const status = await fetchLocalGatewayStatus();
|
||||
set({ localGateway: status, localGatewayBusy: false });
|
||||
return status;
|
||||
} catch (err: unknown) {
|
||||
|
||||
@@ -27,6 +27,12 @@ interface Clone {
|
||||
bootstrapReady?: boolean;
|
||||
bootstrapFiles?: Array<{ name: string; path: string; exists: boolean }>;
|
||||
updatedAt?: string;
|
||||
// 人格相关字段
|
||||
emoji?: string; // Agent emoji, e.g., "🦞", "🤖", "💻"
|
||||
personality?: string; // 人格风格: professional, friendly, creative, concise
|
||||
communicationStyle?: string; // 沟通风格描述
|
||||
notes?: string; // 用户备注
|
||||
onboardingCompleted?: boolean; // 是否完成首次引导
|
||||
}
|
||||
|
||||
interface UsageStats {
|
||||
@@ -93,6 +99,11 @@ interface QuickConfig {
|
||||
autoSaveContext?: boolean;
|
||||
fileWatching?: boolean;
|
||||
privacyOptIn?: boolean;
|
||||
// 人格相关字段
|
||||
emoji?: string;
|
||||
personality?: string;
|
||||
communicationStyle?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
interface WorkspaceInfo {
|
||||
@@ -746,7 +757,8 @@ export const useGatewayStore = create<GatewayStore>((set, get) => {
|
||||
loadClones: async () => {
|
||||
try {
|
||||
const result = await get().client.listClones();
|
||||
const clones = result?.clones || result?.agents || [];
|
||||
// API 可能返回数组,也可能返回 {clones: [...]} 或 {agents: [...]}
|
||||
const clones = Array.isArray(result) ? result : (result?.clones || result?.agents || []);
|
||||
set({ clones });
|
||||
useChatStore.getState().syncAgents(clones);
|
||||
|
||||
@@ -1221,7 +1233,7 @@ export const useGatewayStore = create<GatewayStore>((set, get) => {
|
||||
try {
|
||||
const result = await get().client.listHandRuns(name, opts);
|
||||
const runs: HandRun[] = (result?.runs || []).map((r: RawHandRun) => ({
|
||||
runId: r.runId || r.run_id || r.id,
|
||||
runId: r.runId || r.run_id || r.id || '',
|
||||
status: r.status || 'unknown',
|
||||
startedAt: r.startedAt || r.started_at || r.created_at || new Date().toISOString(),
|
||||
completedAt: r.completedAt || r.completed_at || r.finished_at,
|
||||
@@ -1486,15 +1498,15 @@ export const useGatewayStore = create<GatewayStore>((set, get) => {
|
||||
try {
|
||||
const result = await get().client.listApprovals(status);
|
||||
const approvals: Approval[] = (result?.approvals || []).map((a: RawApproval) => ({
|
||||
id: a.id || a.approval_id,
|
||||
handName: a.hand_name || a.handName,
|
||||
runId: a.run_id || a.runId,
|
||||
id: a.id || a.approval_id || '',
|
||||
handName: a.hand_name || a.handName || '',
|
||||
runId: a.run_id || a.runId || '',
|
||||
status: a.status || 'pending',
|
||||
requestedAt: a.requested_at || a.requestedAt || new Date().toISOString(),
|
||||
requestedBy: a.requested_by || a.requestedBy,
|
||||
reason: a.reason || a.description,
|
||||
requestedBy: a.requested_by || a.requestedBy || '',
|
||||
reason: a.reason || a.description || '',
|
||||
action: a.action || 'execute',
|
||||
params: a.params,
|
||||
params: a.params || {},
|
||||
respondedAt: a.responded_at || a.respondedAt,
|
||||
respondedBy: a.responded_by || a.respondedBy,
|
||||
responseReason: a.response_reason || a.responseReason,
|
||||
|
||||
@@ -65,6 +65,17 @@ export interface Approval {
|
||||
responseReason?: string;
|
||||
}
|
||||
|
||||
// === Trigger Create Options ===
|
||||
|
||||
export interface TriggerCreateOptions {
|
||||
type: string;
|
||||
name?: string;
|
||||
enabled?: boolean;
|
||||
config?: Record<string, unknown>;
|
||||
handName?: string;
|
||||
workflowId?: string;
|
||||
}
|
||||
|
||||
// === Raw API Response Types (for mapping) ===
|
||||
|
||||
interface RawHandRequirement {
|
||||
@@ -129,30 +140,32 @@ interface HandClient {
|
||||
getHand: (name: string) => Promise<Record<string, unknown> | null>;
|
||||
listHandRuns: (name: string, opts?: { limit?: number; offset?: number }) => Promise<{ runs?: RawHandRun[] } | null>;
|
||||
triggerHand: (name: string, params?: Record<string, unknown>) => Promise<{ runId?: string; status?: string } | null>;
|
||||
approveHand: (name: string, runId: string, approved: boolean, reason?: string) => Promise<void>;
|
||||
cancelHand: (name: string, runId: string) => Promise<void>;
|
||||
approveHand: (name: string, runId: string, approved: boolean, reason?: string) => Promise<{ status: string }>;
|
||||
cancelHand: (name: string, runId: string) => Promise<{ status: string }>;
|
||||
listTriggers: () => Promise<{ triggers?: Trigger[] } | null>;
|
||||
getTrigger: (id: string) => Promise<Trigger | null>;
|
||||
createTrigger: (trigger: { type: string; name?: string; enabled?: boolean; config?: Record<string, unknown>; handName?: string; workflowId?: string }) => Promise<{ id?: string } | null>;
|
||||
updateTrigger: (id: string, updates: { name?: string; enabled?: boolean; config?: Record<string, unknown>; handName?: string; workflowId?: string }) => Promise<void>;
|
||||
deleteTrigger: (id: string) => Promise<void>;
|
||||
updateTrigger: (id: string, updates: { name?: string; enabled?: boolean; config?: Record<string, unknown>; handName?: string; workflowId?: string }) => Promise<{ id: string }>;
|
||||
deleteTrigger: (id: string) => Promise<{ status: string }>;
|
||||
listApprovals: (status?: ApprovalStatus) => Promise<{ approvals?: RawApproval[] } | null>;
|
||||
respondToApproval: (approvalId: string, approved: boolean, reason?: string) => Promise<void>;
|
||||
respondToApproval: (approvalId: string, approved: boolean, reason?: string) => Promise<{ status: string }>;
|
||||
}
|
||||
|
||||
interface HandStore {
|
||||
// State
|
||||
// === Store State Slice ===
|
||||
|
||||
export interface HandStateSlice {
|
||||
hands: Hand[];
|
||||
handRuns: Record<string, HandRun[]>;
|
||||
triggers: Trigger[];
|
||||
approvals: Approval[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Client reference (injected via setHandStoreClient)
|
||||
client: HandClient | null;
|
||||
}
|
||||
|
||||
// Actions
|
||||
// === Store Actions Slice ===
|
||||
|
||||
export interface HandActionsSlice {
|
||||
setHandStoreClient: (client: HandClient) => void;
|
||||
loadHands: () => Promise<void>;
|
||||
getHandDetails: (name: string) => Promise<Hand | undefined>;
|
||||
@@ -162,7 +175,7 @@ interface HandStore {
|
||||
cancelHand: (name: string, runId: string) => Promise<void>;
|
||||
loadTriggers: () => Promise<void>;
|
||||
getTrigger: (id: string) => Promise<Trigger | undefined>;
|
||||
createTrigger: (trigger: { type: string; name?: string; enabled?: boolean; config?: Record<string, unknown>; handName?: string; workflowId?: string }) => Promise<Trigger | undefined>;
|
||||
createTrigger: (trigger: TriggerCreateOptions) => Promise<Trigger | undefined>;
|
||||
updateTrigger: (id: string, updates: { name?: string; enabled?: boolean; config?: Record<string, unknown>; handName?: string; workflowId?: string }) => Promise<Trigger | undefined>;
|
||||
deleteTrigger: (id: string) => Promise<void>;
|
||||
loadApprovals: (status?: ApprovalStatus) => Promise<void>;
|
||||
@@ -170,6 +183,10 @@ interface HandStore {
|
||||
clearError: () => void;
|
||||
}
|
||||
|
||||
// === Combined Store Type ===
|
||||
|
||||
export type HandStore = HandStateSlice & HandActionsSlice;
|
||||
|
||||
export const useHandStore = create<HandStore>((set, get) => ({
|
||||
// Initial State
|
||||
hands: [],
|
||||
@@ -383,7 +400,7 @@ export const useHandStore = create<HandStore>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
createTrigger: async (trigger) => {
|
||||
createTrigger: async (trigger: TriggerCreateOptions) => {
|
||||
const client = get().client;
|
||||
if (!client) return undefined;
|
||||
|
||||
@@ -496,3 +513,14 @@ export function createHandClientFromGateway(client: GatewayClient): HandClient {
|
||||
respondToApproval: (approvalId, approved, reason) => client.respondToApproval(approvalId, approved, reason),
|
||||
};
|
||||
}
|
||||
|
||||
// === Client Injection ===
|
||||
|
||||
/**
|
||||
* Sets the client for the hand store.
|
||||
* Called by the coordinator during initialization.
|
||||
*/
|
||||
export function setHandStoreClient(client: unknown): void {
|
||||
const handClient = createHandClientFromGateway(client as GatewayClient);
|
||||
useHandStore.getState().setHandStoreClient(handClient);
|
||||
}
|
||||
|
||||
@@ -26,15 +26,21 @@ export type { WorkflowStore, WorkflowStateSlice, WorkflowActionsSlice, Workflow,
|
||||
export { useConfigStore, setConfigStoreClient } from './configStore';
|
||||
export type { ConfigStore, ConfigStateSlice, ConfigActionsSlice, QuickConfig, WorkspaceInfo, ChannelInfo, ScheduledTask, SkillInfo } from './configStore';
|
||||
|
||||
// === New Stores ===
|
||||
export { useMemoryGraphStore } from './memoryGraphStore';
|
||||
export type { MemoryGraphStore, GraphNode, GraphEdge, GraphFilter, GraphLayout } from './memoryGraphStore';
|
||||
|
||||
export { useActiveLearningStore } from './activeLearningStore';
|
||||
export type { ActiveLearningStore } from './activeLearningStore';
|
||||
|
||||
// === Composite Store Hook ===
|
||||
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useConnectionStore, getClient } from './connectionStore';
|
||||
import { useAgentStore, setAgentStoreClient } from './agentStore';
|
||||
import { useHandStore, setHandStoreClient } from './handStore';
|
||||
import { useWorkflowStore, setWorkflowStoreClient } from './workflowStore';
|
||||
import { useConfigStore, setConfigStoreClient } from './configStore';
|
||||
import type { GatewayClient } from '../lib/gateway-client';
|
||||
|
||||
/**
|
||||
* Initialize all stores with the shared client.
|
||||
@@ -113,7 +119,7 @@ export function useCompositeStore() {
|
||||
const createTrigger = useHandStore((s) => s.createTrigger);
|
||||
const deleteTrigger = useHandStore((s) => s.deleteTrigger);
|
||||
const loadApprovals = useHandStore((s) => s.loadApprovals);
|
||||
const approveRequest = useHandStore((s) => s.approveRequest);
|
||||
const respondToApproval = useHandStore((s) => s.respondToApproval);
|
||||
|
||||
const loadWorkflows = useWorkflowStore((s) => s.loadWorkflows);
|
||||
const getWorkflow = useWorkflowStore((s) => s.getWorkflow);
|
||||
@@ -203,7 +209,7 @@ export function useCompositeStore() {
|
||||
createTrigger,
|
||||
deleteTrigger,
|
||||
loadApprovals,
|
||||
approveRequest,
|
||||
respondToApproval,
|
||||
|
||||
// Workflow actions
|
||||
loadWorkflows,
|
||||
@@ -244,7 +250,7 @@ export function useCompositeStore() {
|
||||
quickConfig, workspaceInfo, channels, scheduledTasks, skillsCatalog, models, modelsLoading, modelsError,
|
||||
connect, disconnect, clearLogs, refreshLocalGateway, startLocalGateway, stopLocalGateway, restartLocalGateway,
|
||||
loadClones, createClone, updateClone, deleteClone, loadUsageStats, loadPluginStatus,
|
||||
loadHands, getHandDetails, triggerHand, loadHandRuns, loadTriggers, createTrigger, deleteTrigger, loadApprovals, approveRequest,
|
||||
loadHands, getHandDetails, triggerHand, loadHandRuns, loadTriggers, createTrigger, deleteTrigger, loadApprovals, respondToApproval,
|
||||
loadWorkflows, getWorkflow, createWorkflow, updateWorkflow, deleteWorkflow, triggerWorkflow, loadWorkflowRuns,
|
||||
loadQuickConfig, saveQuickConfig, loadWorkspaceInfo, loadChannels, getChannel, createChannel, updateChannel, deleteChannel,
|
||||
loadScheduledTasks, createScheduledTask, loadSkillsCatalog, getSkill, createSkill, updateSkill, deleteSkill, loadModels,
|
||||
|
||||
316
desktop/src/store/memoryGraphStore.ts
Normal file
316
desktop/src/store/memoryGraphStore.ts
Normal file
@@ -0,0 +1,316 @@
|
||||
/**
|
||||
* MemoryGraphStore - 记忆图谱状态管理
|
||||
*
|
||||
* 管理记忆图谱可视化的状态,包括节点、边、布局和交互。
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { getMemoryManager, type MemoryEntry, type MemoryType } from '../lib/agent-memory';
|
||||
|
||||
export type { MemoryType };
|
||||
|
||||
// === Types ===
|
||||
|
||||
export interface GraphNode {
|
||||
id: string;
|
||||
type: MemoryType;
|
||||
label: string;
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
importance: number;
|
||||
accessCount: number;
|
||||
createdAt: string;
|
||||
isHighlighted: boolean;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
export interface GraphEdge {
|
||||
id: string;
|
||||
source: string;
|
||||
target: string;
|
||||
type: 'reference' | 'related' | 'derived';
|
||||
strength: number;
|
||||
}
|
||||
|
||||
export interface GraphFilter {
|
||||
types: MemoryType[];
|
||||
minImportance: number;
|
||||
dateRange: {
|
||||
start?: string;
|
||||
end?: string;
|
||||
};
|
||||
searchQuery: string;
|
||||
}
|
||||
|
||||
export interface GraphLayout {
|
||||
width: number;
|
||||
height: number;
|
||||
zoom: number;
|
||||
offsetX: number;
|
||||
offsetY: number;
|
||||
}
|
||||
|
||||
interface MemoryGraphState {
|
||||
nodes: GraphNode[];
|
||||
edges: GraphEdge[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
filter: GraphFilter;
|
||||
layout: GraphLayout;
|
||||
selectedNodeId: string | null;
|
||||
hoveredNodeId: string | null;
|
||||
showLabels: boolean;
|
||||
simulationRunning: boolean;
|
||||
}
|
||||
|
||||
interface MemoryGraphActions {
|
||||
loadGraph: (agentId: string) => Promise<void>;
|
||||
setFilter: (filter: Partial<GraphFilter>) => void;
|
||||
resetFilter: () => void;
|
||||
setLayout: (layout: Partial<GraphLayout>) => void;
|
||||
selectNode: (nodeId: string | null) => void;
|
||||
hoverNode: (nodeId: string | null) => void;
|
||||
toggleLabels: () => void;
|
||||
startSimulation: () => void;
|
||||
stopSimulation: () => void;
|
||||
updateNodePositions: (updates: Array<{ id: string; x: number; y: number }>) => void;
|
||||
highlightSearch: (query: string) => void;
|
||||
clearHighlight: () => void;
|
||||
exportAsImage: () => Promise<Blob | null>;
|
||||
getFilteredNodes: () => GraphNode[];
|
||||
getFilteredEdges: () => GraphEdge[];
|
||||
}
|
||||
|
||||
const DEFAULT_FILTER: GraphFilter = {
|
||||
types: ['fact', 'preference', 'lesson', 'context', 'task'],
|
||||
minImportance: 0,
|
||||
dateRange: {},
|
||||
searchQuery: '',
|
||||
};
|
||||
|
||||
const DEFAULT_LAYOUT: GraphLayout = {
|
||||
width: 800,
|
||||
height: 600,
|
||||
zoom: 1,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
};
|
||||
|
||||
export type MemoryGraphStore = MemoryGraphState & MemoryGraphActions;
|
||||
|
||||
// === Helper Functions ===
|
||||
|
||||
function memoryToNode(memory: MemoryEntry, index: number, total: number): GraphNode {
|
||||
// 使用圆形布局初始位置
|
||||
const angle = (index / total) * 2 * Math.PI;
|
||||
const radius = 200;
|
||||
|
||||
return {
|
||||
id: memory.id,
|
||||
type: memory.type,
|
||||
label: memory.content.slice(0, 50) + (memory.content.length > 50 ? '...' : ''),
|
||||
x: 400 + radius * Math.cos(angle),
|
||||
y: 300 + radius * Math.sin(angle),
|
||||
vx: 0,
|
||||
vy: 0,
|
||||
importance: memory.importance,
|
||||
accessCount: memory.accessCount,
|
||||
createdAt: memory.createdAt,
|
||||
isHighlighted: false,
|
||||
isSelected: false,
|
||||
};
|
||||
}
|
||||
|
||||
function findRelatedMemories(memories: MemoryEntry[]): GraphEdge[] {
|
||||
const edges: GraphEdge[] = [];
|
||||
|
||||
// 简单的关联算法:基于共同标签和关键词
|
||||
for (let i = 0; i < memories.length; i++) {
|
||||
for (let j = i + 1; j < memories.length; j++) {
|
||||
const m1 = memories[i];
|
||||
const m2 = memories[j];
|
||||
|
||||
// 检查共同标签
|
||||
const commonTags = m1.tags.filter(t => m2.tags.includes(t));
|
||||
if (commonTags.length > 0) {
|
||||
edges.push({
|
||||
id: `edge-${m1.id}-${m2.id}`,
|
||||
source: m1.id,
|
||||
target: m2.id,
|
||||
type: 'related',
|
||||
strength: commonTags.length * 0.3,
|
||||
});
|
||||
}
|
||||
|
||||
// 同类型记忆关联
|
||||
if (m1.type === m2.type) {
|
||||
const existingEdge = edges.find(
|
||||
e => e.source === m1.id && e.target === m2.id
|
||||
);
|
||||
if (!existingEdge) {
|
||||
edges.push({
|
||||
id: `edge-${m1.id}-${m2.id}-type`,
|
||||
source: m1.id,
|
||||
target: m2.id,
|
||||
type: 'derived',
|
||||
strength: 0.1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return edges;
|
||||
}
|
||||
|
||||
export const useMemoryGraphStore = create<MemoryGraphStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
nodes: [],
|
||||
edges: [],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
filter: DEFAULT_FILTER,
|
||||
layout: DEFAULT_LAYOUT,
|
||||
selectedNodeId: null,
|
||||
hoveredNodeId: null,
|
||||
showLabels: true,
|
||||
simulationRunning: false,
|
||||
|
||||
loadGraph: async (agentId: string) => {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
const mgr = getMemoryManager();
|
||||
const memories = await mgr.getAll(agentId, { limit: 200 });
|
||||
|
||||
const nodes = memories.map((m, i) => memoryToNode(m, i, memories.length));
|
||||
const edges = findRelatedMemories(memories);
|
||||
|
||||
set({
|
||||
nodes,
|
||||
edges,
|
||||
isLoading: false,
|
||||
});
|
||||
} catch (err) {
|
||||
set({
|
||||
isLoading: false,
|
||||
error: err instanceof Error ? err.message : '加载图谱失败',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setFilter: (filter) => {
|
||||
set(state => ({
|
||||
filter: { ...state.filter, ...filter },
|
||||
}));
|
||||
},
|
||||
|
||||
resetFilter: () => {
|
||||
set({ filter: DEFAULT_FILTER });
|
||||
},
|
||||
|
||||
setLayout: (layout) => {
|
||||
set(state => ({
|
||||
layout: { ...state.layout, ...layout },
|
||||
}));
|
||||
},
|
||||
|
||||
selectNode: (nodeId) => {
|
||||
set(state => ({
|
||||
selectedNodeId: nodeId,
|
||||
nodes: state.nodes.map(n => ({
|
||||
...n,
|
||||
isSelected: n.id === nodeId,
|
||||
})),
|
||||
}));
|
||||
},
|
||||
|
||||
hoverNode: (nodeId) => {
|
||||
set(state => ({
|
||||
hoveredNodeId: nodeId,
|
||||
nodes: state.nodes.map(n => ({
|
||||
...n,
|
||||
isHighlighted: nodeId ? n.id === nodeId : n.isHighlighted,
|
||||
})),
|
||||
}));
|
||||
},
|
||||
|
||||
toggleLabels: () => {
|
||||
set(state => ({ showLabels: !state.showLabels }));
|
||||
},
|
||||
|
||||
startSimulation: () => {
|
||||
set({ simulationRunning: true });
|
||||
},
|
||||
|
||||
stopSimulation: () => {
|
||||
set({ simulationRunning: false });
|
||||
},
|
||||
|
||||
updateNodePositions: (updates) => {
|
||||
set(state => ({
|
||||
nodes: state.nodes.map(node => {
|
||||
const update = updates.find(u => u.id === node.id);
|
||||
return update ? { ...node, x: update.x, y: update.y } : node;
|
||||
}),
|
||||
}));
|
||||
},
|
||||
|
||||
highlightSearch: (query) => {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
set(state => ({
|
||||
filter: { ...state.filter, searchQuery: query },
|
||||
nodes: state.nodes.map(n => ({
|
||||
...n,
|
||||
isHighlighted: query ? n.label.toLowerCase().includes(lowerQuery) : false,
|
||||
})),
|
||||
}));
|
||||
},
|
||||
|
||||
clearHighlight: () => {
|
||||
set(state => ({
|
||||
nodes: state.nodes.map(n => ({ ...n, isHighlighted: false })),
|
||||
}));
|
||||
},
|
||||
|
||||
exportAsImage: async () => {
|
||||
// SVG 导出逻辑在组件中实现
|
||||
return null;
|
||||
},
|
||||
|
||||
getFilteredNodes: () => {
|
||||
const { nodes, filter } = get();
|
||||
return nodes.filter(n => {
|
||||
if (!filter.types.includes(n.type)) return false;
|
||||
if (n.importance < filter.minImportance) return false;
|
||||
if (filter.dateRange.start && n.createdAt < filter.dateRange.start) return false;
|
||||
if (filter.dateRange.end && n.createdAt > filter.dateRange.end) return false;
|
||||
if (filter.searchQuery) {
|
||||
return n.label.toLowerCase().includes(filter.searchQuery.toLowerCase());
|
||||
}
|
||||
return true;
|
||||
});
|
||||
},
|
||||
|
||||
getFilteredEdges: () => {
|
||||
const { edges } = get();
|
||||
const filteredNodes = get().getFilteredNodes();
|
||||
const nodeIds = new Set(filteredNodes.map(n => n.id));
|
||||
|
||||
return edges.filter(e => nodeIds.has(e.source) && nodeIds.has(e.target));
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'zclaw-memory-graph',
|
||||
partialize: (state) => ({
|
||||
filter: state.filter,
|
||||
layout: state.layout,
|
||||
showLabels: state.showLabels,
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
411
desktop/src/store/skillMarketStore.ts
Normal file
411
desktop/src/store/skillMarketStore.ts
Normal file
@@ -0,0 +1,411 @@
|
||||
/**
|
||||
* * skillMarketStore.ts - 技能市场状态管理
|
||||
*
|
||||
* * 猛攻状态管理技能浏览、搜索、安装/卸载等功能
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import type { Skill, SkillReview, SkillMarketState } from '../types/skill-market';
|
||||
|
||||
// === 存储键 ===
|
||||
const STORAGE_KEY = 'zclaw-skill-market';
|
||||
const INSTALLED_KEY = 'zclaw-installed-skills';
|
||||
|
||||
// === 默认状态 ===
|
||||
const initialState: SkillMarketState = {
|
||||
skills: [],
|
||||
installedSkills: [],
|
||||
searchResults: [],
|
||||
selectedSkill: null,
|
||||
searchQuery: '',
|
||||
categoryFilter: 'all',
|
||||
isLoading: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
// === Store 定义 ===
|
||||
interface SkillMarketActions {
|
||||
// 技能加载
|
||||
loadSkills: () => Promise<void>;
|
||||
// 技能搜索
|
||||
searchSkills: (query: string) => void;
|
||||
// 分类过滤
|
||||
filterByCategory: (category: string) => void;
|
||||
// 选择技能
|
||||
selectSkill: (skill: Skill | null) => void;
|
||||
// 安装技能
|
||||
installSkill: (skillId: string) => Promise<boolean>;
|
||||
// 卸载技能
|
||||
uninstallSkill: (skillId: string) => Promise<boolean>;
|
||||
// 获取技能详情
|
||||
getSkillDetails: (skillId: string) => Promise<Skill | null>;
|
||||
// 加载评论
|
||||
loadReviews: (skillId: string) => Promise<SkillReview[]>;
|
||||
// 添加评论
|
||||
addReview: (skillId: string, review: Omit<SkillReview, 'id' | 'skillId' | 'createdAt'>) => Promise<void>;
|
||||
// 刷新技能列表
|
||||
refreshSkills: () => Promise<void>;
|
||||
// 清除错误
|
||||
clearError: () => void;
|
||||
// 重置状态
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
// === Store 创建 ===
|
||||
export const useSkillMarketStore = create<SkillMarketState & SkillMarketActions>()(
|
||||
persist({
|
||||
key: STORAGE_KEY,
|
||||
storage: localStorage,
|
||||
partialize: (state) => ({
|
||||
installedSkills: state.installedSkills,
|
||||
categoryFilter: state.categoryFilter,
|
||||
}),
|
||||
}),
|
||||
initialState,
|
||||
{
|
||||
// === 技能加载 ===
|
||||
loadSkills: async () => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
// 扫描 skills 目录获取可用技能
|
||||
const skills = await scanSkillsDirectory();
|
||||
// 从 localStorage 恢复安装状态
|
||||
const stored = localStorage.getItem(INSTALLED_KEY);
|
||||
const installedSkills: string[] = stored ? JSON.parse(stored) : [];
|
||||
// 更新技能的安装状态
|
||||
const updatedSkills = skills.map(skill => ({
|
||||
...skill,
|
||||
installed: installedSkills.includes(skill.id),
|
||||
})));
|
||||
set({
|
||||
skills: updatedSkills,
|
||||
installedSkills,
|
||||
isLoading: false,
|
||||
});
|
||||
} catch (err) {
|
||||
set({
|
||||
isLoading: false,
|
||||
error: err instanceof Error ? err.message : '加载技能失败',
|
||||
});
|
||||
}
|
||||
},
|
||||
// === 技能搜索 ===
|
||||
searchSkills: (query: string) => {
|
||||
const { skills } = get();
|
||||
set({ searchQuery: query });
|
||||
if (!query.trim()) {
|
||||
set({ searchResults: [] });
|
||||
return;
|
||||
}
|
||||
const queryLower = query.toLowerCase();
|
||||
const results = skills.filter(skill => {
|
||||
return (
|
||||
skill.name.toLowerCase().includes(queryLower) ||
|
||||
skill.description.toLowerCase().includes(queryLower) ||
|
||||
skill.triggers.some(t => t.toLowerCase().includes(queryLower)) ||
|
||||
skill.capabilities.some(c => c.toLowerCase().includes(queryLower)) ||
|
||||
skill.tags?.some(t => t.toLowerCase().includes(queryLower))
|
||||
);
|
||||
});
|
||||
set({ searchResults: results });
|
||||
},
|
||||
// === 分类过滤 ===
|
||||
filterByCategory: (category: string) => {
|
||||
set({ categoryFilter: category });
|
||||
},
|
||||
// === 选择技能 ===
|
||||
selectSkill: (skill: Skill | null) => {
|
||||
set({ selectedSkill: skill });
|
||||
},
|
||||
// === 安装技能 ===
|
||||
installSkill: async (skillId: string) => {
|
||||
const { skills, installedSkills } = get();
|
||||
const skill = skills.find(s => s.id === skillId);
|
||||
if (!skill) return false;
|
||||
try {
|
||||
// 更新安装状态
|
||||
const newInstalledSkills = [...installedSkills, skillId];
|
||||
const updatedSkills = skills.map(s => ({
|
||||
...s,
|
||||
installed: s.id === skillId ? true : s.installed,
|
||||
installedAt: s.id === skillId ? new Date().toISOString() : s.installedAt,
|
||||
}));
|
||||
// 持久化安装列表
|
||||
localStorage.setItem(INSTALLED_KEY, JSON.stringify(newInstalledSkills));
|
||||
set({
|
||||
skills: updatedSkills,
|
||||
installedSkills: newInstalledSkills,
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
set({
|
||||
error: err instanceof Error ? err.message : '安装技能失败',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
},
|
||||
// === 卸载技能 ===
|
||||
uninstallSkill: async (skillId: string) => {
|
||||
const { skills, installedSkills } = get();
|
||||
try {
|
||||
// 更新安装状态
|
||||
const newInstalledSkills = installedSkills.filter(id => id !== skillId);
|
||||
const updatedSkills = skills.map(s => ({
|
||||
...s,
|
||||
installed: s.id === skillId ? false : s.installed,
|
||||
installedAt: s.id === skillId ? undefined : s.installedAt,
|
||||
}));
|
||||
// 持久化安装列表
|
||||
localStorage.setItem(INSTALLED_KEY, JSON.stringify(newInstalledSkills));
|
||||
set({
|
||||
skills: updatedSkills,
|
||||
installedSkills: newInstalledSkills,
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
set({
|
||||
error: err instanceof Error ? err.message : '卸载技能失败',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
},
|
||||
// === 获取技能详情 ===
|
||||
getSkillDetails: async (skillId: string) => {
|
||||
const { skills } = get();
|
||||
return skills.find(s => s.id === skillId) || null;
|
||||
},
|
||||
// === 加载评论 ===
|
||||
loadReviews: async (skillId: string) => {
|
||||
// MVP: 从 localStorage 模拟加载评论
|
||||
const reviewsKey = `zclaw-skill-reviews-${skillId}`;
|
||||
const stored = localStorage.getItem(reviewsKey);
|
||||
const reviews: SkillReview[] = stored ? JSON.parse(stored) : [];
|
||||
return reviews;
|
||||
},
|
||||
// === 添加评论 ===
|
||||
addReview: async (skillId: string, review: Omit<SkillReview, 'id' | 'skillId' | 'createdAt'>) => {
|
||||
const reviews = await get().loadReviews(skillId);
|
||||
const newReview: SkillReview = {
|
||||
...review,
|
||||
id: `review-${Date.now()}`,
|
||||
skillId,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
const updatedReviews = [...reviews, newReview];
|
||||
// 更新技能的评分和评论数
|
||||
const { skills } = get();
|
||||
const updatedSkills = skills.map(s => {
|
||||
if (s.id === skillId) {
|
||||
const totalRating = updatedReviews.reduce((sum, r) => sum + r.rating, 0);
|
||||
const avgRating = totalRating / updatedReviews.length;
|
||||
return {
|
||||
...s,
|
||||
rating: Math.round(avgRating * 10) / 10,
|
||||
reviewCount: updatedReviews.length,
|
||||
};
|
||||
}
|
||||
return s;
|
||||
});
|
||||
// 持久化评论
|
||||
const reviewsKey = `zclaw-skill-reviews-${skillId}`;
|
||||
localStorage.setItem(reviewsKey, JSON.stringify(updatedReviews));
|
||||
set({ skills: updatedSkills });
|
||||
},
|
||||
// === 刷新技能列表 ===
|
||||
refreshSkills: async () => {
|
||||
// 清除缓存并重新加载
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
await get().loadSkills();
|
||||
},
|
||||
// === 清除错误 ===
|
||||
clearError: () => {
|
||||
set({ error: null });
|
||||
},
|
||||
// === 重置状态 ===
|
||||
reset: () => {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
localStorage.removeItem(INSTALLED_KEY);
|
||||
set(initialState);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// === 辅助函数 ===
|
||||
|
||||
/**
|
||||
* 扫描 skills 目录获取可用技能
|
||||
*/
|
||||
async function scanSkillsDirectory(): Promise<Skill[]> {
|
||||
// 这里我们模拟扫描,实际实现需要通过 Tauri API 访问文件系统
|
||||
// 或者从预定义的技能列表中加载
|
||||
const skills: Skill[] = [
|
||||
// 开发类
|
||||
{
|
||||
id: 'code-review',
|
||||
name: '代码审查',
|
||||
description: '审查代码、分析代码质量、提供改进建议',
|
||||
triggers: ['审查代码', '代码审查', 'code review', 'PR Review', '检查代码', '分析代码'],
|
||||
capabilities: ['代码质量分析', '架构评估', '最佳实践检查', '安全审计'],
|
||||
toolDeps: ['read', 'grep', 'glob'],
|
||||
category: 'development',
|
||||
installed: false,
|
||||
tags: ['代码', '审查', '质量'],
|
||||
},
|
||||
{
|
||||
id: 'translation',
|
||||
name: '翻译助手',
|
||||
description: '翻译文本、多语言转换、保持语言风格一致性',
|
||||
triggers: ['翻译', 'translate', '中译英', '英译中', '翻译成', '转换成'],
|
||||
capabilities: ['多语言翻译', '技术文档翻译', '代码注释翻译', 'UI 文本翻译', '风格保持'],
|
||||
toolDeps: ['read', 'write'],
|
||||
category: 'content',
|
||||
installed: false,
|
||||
tags: ['翻译', '语言', '国际化'],
|
||||
},
|
||||
{
|
||||
id: 'chinese-writing',
|
||||
name: '中文写作',
|
||||
description: '中文写作助手 - 帮助撰写各类中文文档、文章、报告',
|
||||
triggers: ['写一篇', '帮我写', '撰写', '起草', '润色', '中文写作'],
|
||||
capabilities: ['撰写文档', '润色修改', '调整语气', '中英文翻译'],
|
||||
toolDeps: ['read', 'write'],
|
||||
category: 'content',
|
||||
installed: false,
|
||||
tags: ['写作', '文档', '中文'],
|
||||
},
|
||||
{
|
||||
id: 'web-search',
|
||||
name: '网络搜索',
|
||||
description: '搜索互联网信息、整合多方来源',
|
||||
triggers: ['搜索', 'search', '查找信息', '查询', '搜索网络'],
|
||||
capabilities: ['搜索引擎集成', '信息提取', '来源验证', '结果整合'],
|
||||
toolDeps: ['web_search'],
|
||||
category: 'research',
|
||||
installed: false,
|
||||
tags: ['搜索', '互联网', '信息'],
|
||||
},
|
||||
{
|
||||
id: 'data-analysis',
|
||||
name: '数据分析',
|
||||
description: '数据清洗、统计分析、可视化图表',
|
||||
triggers: ['数据分析', '统计', '可视化', '图表', 'analytics'],
|
||||
capabilities: ['数据清洗', '统计分析', '可视化图表', '报告生成'],
|
||||
toolDeps: ['read', 'write', 'shell'],
|
||||
category: 'analytics',
|
||||
installed: false,
|
||||
tags: ['数据', '分析', '可视化'],
|
||||
},
|
||||
{
|
||||
id: 'git',
|
||||
name: 'Git 操作',
|
||||
description: 'Git 版本控制操作、分支管理、冲突解决',
|
||||
triggers: ['git', '版本控制', '分支', '合并', 'commit', 'merge'],
|
||||
capabilities: ['分支管理', '冲突解决', 'rebase', 'cherry-pick'],
|
||||
toolDeps: ['shell'],
|
||||
category: 'development',
|
||||
installed: false,
|
||||
tags: ['git', '版本控制', '分支'],
|
||||
},
|
||||
{
|
||||
id: 'shell-command',
|
||||
name: 'Shell 命令',
|
||||
description: '执行 Shell 命令、系统操作',
|
||||
triggers: ['shell', '命令行', '终端', 'terminal', 'bash'],
|
||||
capabilities: ['命令执行', '管道操作', '脚本运行', '环境管理'],
|
||||
toolDeps: ['shell'],
|
||||
category: 'ops',
|
||||
installed: false,
|
||||
tags: ['shell', '命令', '系统'],
|
||||
},
|
||||
{
|
||||
id: 'file-operations',
|
||||
name: '文件操作',
|
||||
description: '文件读写、目录管理、文件搜索',
|
||||
triggers: ['文件', 'file', '读取', '写入', '目录', '文件夹'],
|
||||
capabilities: ['文件读写', '目录管理', '文件搜索', '批量操作'],
|
||||
toolDeps: ['read', 'write', 'glob'],
|
||||
category: 'ops',
|
||||
installed: false,
|
||||
tags: ['文件', '目录', '读写'],
|
||||
},
|
||||
{
|
||||
id: 'security-engineer',
|
||||
name: '安全工程师',
|
||||
description: '安全工程师 - 负责安全审计、漏洞检测、合规检查',
|
||||
triggers: ['安全审计', '漏洞检测', '安全检查', 'security', '渗透测试'],
|
||||
capabilities: ['漏洞扫描', '合规检查', '安全加固', '威胁建模'],
|
||||
toolDeps: ['read', 'grep', 'shell'],
|
||||
category: 'security',
|
||||
installed: false,
|
||||
tags: ['安全', '审计', '漏洞'],
|
||||
},
|
||||
{
|
||||
id: 'ai-engineer',
|
||||
name: 'AI 工程师',
|
||||
description: 'AI/ML 工程师 - 专注机器学习模型开发、LLM 集成和生产系统部署',
|
||||
triggers: ['AI工程师', '机器学习', 'ML模型', 'LLM集成', '深度学习', '模型训练'],
|
||||
capabilities: ['ML 框架', 'LLM 集成', 'RAG 系统', '向量数据库'],
|
||||
toolDeps: ['bash', 'read', 'write', 'grep', 'glob'],
|
||||
category: 'development',
|
||||
installed: false,
|
||||
tags: ['AI', 'ML', 'LLM'],
|
||||
},
|
||||
{
|
||||
id: 'senior-developer',
|
||||
name: '高级开发',
|
||||
description: '高级开发工程师 - 端到端功能实现、复杂问题解决',
|
||||
triggers: ['高级开发', 'senior developer', '端到端', '复杂功能', '架构实现'],
|
||||
capabilities: ['端到端实现', '架构设计', '性能优化', '代码重构'],
|
||||
toolDeps: ['bash', 'read', 'write', 'grep', 'glob'],
|
||||
category: 'development',
|
||||
installed: false,
|
||||
tags: ['开发', '架构', '实现'],
|
||||
},
|
||||
{
|
||||
id: 'frontend-developer',
|
||||
name: '前端开发',
|
||||
description: '前端开发专家 - 擅长 React/Vue/CSS/TypeScript',
|
||||
triggers: ['前端开发', '页面开发', 'UI开发', 'React', 'Vue', 'CSS'],
|
||||
capabilities: ['组件开发', '样式调整', '性能优化', '响应式设计'],
|
||||
toolDeps: ['read', 'write', 'shell'],
|
||||
category: 'development',
|
||||
installed: false,
|
||||
types: ['前端', 'UI', '组件'],
|
||||
},
|
||||
{
|
||||
id: 'backend-architect',
|
||||
name: '后端架构',
|
||||
description: '后端架构设计、API设计、数据库建模',
|
||||
triggers: ['后端架构', 'API设计', '数据库设计', '系统架构', '微服务'],
|
||||
capabilities: ['架构设计', 'API规范', '数据库建模', '性能优化'],
|
||||
toolDeps: ['read', 'write', 'shell'],
|
||||
category: 'development',
|
||||
installed: false,
|
||||
tags: ['后端', '架构', 'API'],
|
||||
},
|
||||
{
|
||||
id: 'devops-automator',
|
||||
name: 'DevOps 自动化',
|
||||
description: 'CI/CD、Docker、K8s、自动化部署',
|
||||
triggers: ['DevOps', 'CI/CD', 'Docker', '部署', '自动化', 'K8s'],
|
||||
capabilities: ['CI/CD配置', '容器化', '自动化部署', '监控告警'],
|
||||
toolDeps: ['shell', 'read', 'write'],
|
||||
category: 'ops',
|
||||
installed: false,
|
||||
tags: ['DevOps', 'Docker', 'CI/CD'],
|
||||
},
|
||||
{
|
||||
id: 'senior-pm',
|
||||
name: '高级PM',
|
||||
description: '项目管理、需求分析、迭代规划',
|
||||
triggers: ['项目管理', '需求分析', '迭代规划', '产品设计', 'PRD'],
|
||||
capabilities: ['需求拆解', '迭代排期', '风险评估', '文档撰写'],
|
||||
toolDeps: ['read', 'write'],
|
||||
category: 'management',
|
||||
installed: false,
|
||||
tags: ['PM', '需求', '迭代'],
|
||||
},
|
||||
];
|
||||
return skills;
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import type {
|
||||
Team,
|
||||
TeamMember,
|
||||
@@ -120,7 +121,9 @@ const calculateMetrics = (team: Team): TeamMetrics => {
|
||||
|
||||
// === Store Implementation ===
|
||||
|
||||
export const useTeamStore = create<TeamStoreState>((set, get) => ({
|
||||
export const useTeamStore = create<TeamStoreState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
// Initial State
|
||||
teams: [],
|
||||
activeTeam: null,
|
||||
@@ -582,4 +585,12 @@ export const useTeamStore = create<TeamStoreState>((set, get) => ({
|
||||
clearError: () => {
|
||||
set({ error: null });
|
||||
},
|
||||
}));
|
||||
}),
|
||||
{
|
||||
name: 'zclaw-teams',
|
||||
partialize: (state) => ({
|
||||
teams: state.teams,
|
||||
activeTeam: state.activeTeam,
|
||||
}),
|
||||
},
|
||||
));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { create } from 'zustand';
|
||||
import { Workflow, WorkflowRun } from './gatewayStore';
|
||||
import type { GatewayClient } from '../lib/gateway-client';
|
||||
|
||||
// === Types ===
|
||||
|
||||
@@ -30,7 +31,7 @@ export interface WorkflowStep {
|
||||
condition?: string;
|
||||
}
|
||||
|
||||
export interface CreateWorkflowInput {
|
||||
export interface WorkflowCreateOptions {
|
||||
name: string;
|
||||
description?: string;
|
||||
steps: WorkflowStep[];
|
||||
@@ -53,7 +54,7 @@ export interface ExtendedWorkflowRun extends WorkflowRun {
|
||||
|
||||
interface WorkflowClient {
|
||||
listWorkflows(): Promise<{ workflows: { id: string; name: string; steps: number; description?: string; createdAt?: string }[] } | null>;
|
||||
createWorkflow(workflow: CreateWorkflowInput): Promise<{ id: string; name: string } | null>;
|
||||
createWorkflow(workflow: WorkflowCreateOptions): Promise<{ id: string; name: string } | null>;
|
||||
updateWorkflow(id: string, updates: UpdateWorkflowInput): Promise<{ id: string; name: string } | null>;
|
||||
deleteWorkflow(id: string): Promise<{ status: string }>;
|
||||
executeWorkflow(id: string, input?: Record<string, unknown>): Promise<{ runId: string; status: string } | null>;
|
||||
@@ -61,9 +62,9 @@ interface WorkflowClient {
|
||||
listWorkflowRuns(workflowId: string, opts?: { limit?: number; offset?: number }): Promise<{ runs: RawWorkflowRun[] } | null>;
|
||||
}
|
||||
|
||||
// === Store State ===
|
||||
// === Store State Slice ===
|
||||
|
||||
interface WorkflowState {
|
||||
export interface WorkflowStateSlice {
|
||||
workflows: Workflow[];
|
||||
workflowRuns: Record<string, ExtendedWorkflowRun[]>;
|
||||
isLoading: boolean;
|
||||
@@ -71,13 +72,13 @@ interface WorkflowState {
|
||||
client: WorkflowClient;
|
||||
}
|
||||
|
||||
// === Store Actions ===
|
||||
// === Store Actions Slice ===
|
||||
|
||||
interface WorkflowActions {
|
||||
export interface WorkflowActionsSlice {
|
||||
setWorkflowStoreClient: (client: WorkflowClient) => void;
|
||||
loadWorkflows: () => Promise<void>;
|
||||
getWorkflow: (id: string) => Workflow | undefined;
|
||||
createWorkflow: (workflow: CreateWorkflowInput) => Promise<Workflow | undefined>;
|
||||
createWorkflow: (workflow: WorkflowCreateOptions) => Promise<Workflow | undefined>;
|
||||
updateWorkflow: (id: string, updates: UpdateWorkflowInput) => Promise<Workflow | undefined>;
|
||||
deleteWorkflow: (id: string) => Promise<void>;
|
||||
triggerWorkflow: (id: string, input?: Record<string, unknown>) => Promise<{ runId: string; status: string } | undefined>;
|
||||
@@ -87,6 +88,10 @@ interface WorkflowActions {
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
// === Combined Store Type ===
|
||||
|
||||
export type WorkflowStore = WorkflowStateSlice & WorkflowActionsSlice;
|
||||
|
||||
// === Initial State ===
|
||||
|
||||
const initialState = {
|
||||
@@ -99,7 +104,7 @@ const initialState = {
|
||||
|
||||
// === Store ===
|
||||
|
||||
export const useWorkflowStore = create<WorkflowState & WorkflowActions>((set, get) => ({
|
||||
export const useWorkflowStore = create<WorkflowStateSlice & WorkflowActionsSlice>((set, get) => ({
|
||||
...initialState,
|
||||
|
||||
setWorkflowStoreClient: (client: WorkflowClient) => {
|
||||
@@ -128,7 +133,7 @@ export const useWorkflowStore = create<WorkflowState & WorkflowActions>((set, ge
|
||||
return get().workflows.find(w => w.id === id);
|
||||
},
|
||||
|
||||
createWorkflow: async (workflow: CreateWorkflowInput) => {
|
||||
createWorkflow: async (workflow: WorkflowCreateOptions) => {
|
||||
set({ error: null });
|
||||
try {
|
||||
const result = await get().client.createWorkflow(workflow);
|
||||
@@ -253,3 +258,29 @@ export const useWorkflowStore = create<WorkflowState & WorkflowActions>((set, ge
|
||||
|
||||
// Re-export types from gatewayStore for convenience
|
||||
export type { Workflow, WorkflowRun };
|
||||
|
||||
// === Client Injection ===
|
||||
|
||||
/**
|
||||
* Helper to create a WorkflowClient adapter from a GatewayClient.
|
||||
*/
|
||||
function createWorkflowClientFromGateway(client: GatewayClient): WorkflowClient {
|
||||
return {
|
||||
listWorkflows: () => client.listWorkflows(),
|
||||
createWorkflow: (workflow) => client.createWorkflow(workflow),
|
||||
updateWorkflow: (id, updates) => client.updateWorkflow(id, updates),
|
||||
deleteWorkflow: (id) => client.deleteWorkflow(id),
|
||||
executeWorkflow: (id, input) => client.executeWorkflow(id, input),
|
||||
cancelWorkflow: (workflowId, runId) => client.cancelWorkflow(workflowId, runId),
|
||||
listWorkflowRuns: (workflowId, opts) => client.listWorkflowRuns(workflowId, opts),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the client for the workflow store.
|
||||
* Called by the coordinator during initialization.
|
||||
*/
|
||||
export function setWorkflowStoreClient(client: unknown): void {
|
||||
const workflowClient = createWorkflowClientFromGateway(client as GatewayClient);
|
||||
useWorkflowStore.getState().setWorkflowStoreClient(workflowClient);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user