cc工作前备份

This commit is contained in:
iven
2026-03-12 00:23:42 +08:00
parent f75a2b798b
commit ef849c62ab
98 changed files with 12110 additions and 568 deletions

144
src/api/index.ts Normal file
View File

@@ -0,0 +1,144 @@
// ZCLAW API 层 - 供 Tauri 前端调用的接口
import type { ZClawApp } from '../app';
import { createLogger } from '../utils/logger';
const log = createLogger('API');
export interface APIResponse<T = any> {
success: boolean;
data?: T;
error?: string;
}
// API 处理器集合 - 供 Tauri Commands 调用
export class ZClawAPI {
constructor(private app: ZClawApp) {}
// === 聊天 ===
async sendMessage(userId: string, content: string): Promise<APIResponse<{ reply: string }>> {
try {
const reply = await this.app.handleUserMessage(userId, content);
return { success: true, data: { reply } };
} catch (error: any) {
log.error(`sendMessage error: ${error.message}`);
return { success: false, error: error.message };
}
}
// === 任务 ===
async submitTask(userId: string, payload: any): Promise<APIResponse<{ taskId: string }>> {
try {
const engine = this.app.getRemoteExecution();
const taskId = await engine.submitTask({
id: '',
userId,
deviceId: 'local',
channel: 'desktop',
type: 'immediate',
priority: 'normal',
payload,
status: 'pending',
createdAt: new Date(),
});
return { success: true, data: { taskId } };
} catch (error: any) {
return { success: false, error: error.message };
}
}
async getTaskStatus(taskId: string): Promise<APIResponse<{ status: string }>> {
try {
const engine = this.app.getRemoteExecution();
const status = await engine.getStatus(taskId);
return { success: true, data: { status } };
} catch (error: any) {
return { success: false, error: error.message };
}
}
async getTaskStats(): Promise<APIResponse> {
try {
const stats = this.app.getRemoteExecution().getStats();
return { success: true, data: stats };
} catch (error: any) {
return { success: false, error: error.message };
}
}
// === 多 Agent ===
async executeGoal(userId: string, goal: string): Promise<APIResponse> {
try {
const result = await this.app.getAgentOrchestrator().executeGoal(goal);
return { success: true, data: result };
} catch (error: any) {
return { success: false, error: error.message };
}
}
// === 记忆 ===
async getUserProfile(userId: string): Promise<APIResponse> {
try {
const profile = await this.app.getMemory().getProfile(userId);
return { success: true, data: profile || null };
} catch (error: any) {
return { success: false, error: error.message };
}
}
async getMemoryEvents(userId: string, query: string = '', limit: number = 20): Promise<APIResponse> {
try {
const events = await this.app.getMemory().recall(userId, query, limit);
return { success: true, data: events };
} catch (error: any) {
return { success: false, error: error.message };
}
}
// === 定时任务 ===
async listScheduledTasks(userId: string): Promise<APIResponse> {
try {
const tasks = await this.app.getProactive().listTasks(userId);
return { success: true, data: tasks };
} catch (error: any) {
return { success: false, error: error.message };
}
}
async scheduleTask(userId: string, schedule: any, taskConfig: any): Promise<APIResponse> {
try {
await this.app.getProactive().scheduleTask({
id: '',
userId,
channel: 'desktop',
schedule,
task: taskConfig,
status: 'active',
});
return { success: true };
} catch (error: any) {
return { success: false, error: error.message };
}
}
// === 系统状态 ===
async getSystemStatus(): Promise<APIResponse> {
try {
const taskStats = this.app.getRemoteExecution().getStats();
const agents = this.app.getAgentOrchestrator().getAgents().map(a => a.getInfo());
const activePlans = this.app.getAgentOrchestrator().getActivePlans();
const connectedIMs = this.app.getIMGateway().getConnectedAdapters();
return {
success: true,
data: {
taskStats,
activeAgents: agents.length,
activePlans: activePlans.length,
connectedIMs,
},
};
} catch (error: any) {
return { success: false, error: error.message };
}
}
}

173
src/app.ts Normal file
View File

@@ -0,0 +1,173 @@
// ZCLAW 主应用类 - 统一协调所有系统
import { loadConfig, getConfig } from './config';
import { initDatabase, closeDatabase } from './db';
import { AIManager } from './core/ai/manager';
import { AgentOrchestrator } from './core/multi-agent/orchestrator';
import { RemoteExecutionEngine } from './core/remote-execution/engine';
import { TaskOrchestrator } from './core/task-orchestration/orchestrator';
import { PersistentMemorySystem } from './core/memory/memory';
import { ProactiveServiceSystem } from './core/proactive/proactive';
import { IMGateway } from './im/gateway';
import { FeishuAdapter } from './im/adapters/feishu';
import { createLogger, setLogLevel } from './utils/logger';
const log = createLogger('ZCLAW');
export class ZClawApp {
private ai!: AIManager;
private agentOrchestrator!: AgentOrchestrator;
private remoteExecution!: RemoteExecutionEngine;
private taskOrchestrator!: TaskOrchestrator;
private memory!: PersistentMemorySystem;
private proactive!: ProactiveServiceSystem;
private imGateway!: IMGateway;
async start(): Promise<void> {
log.info('========================================');
log.info(' ZCLAW - AI Agent Platform');
log.info(' Version: 0.1.0');
log.info('========================================');
// 1. 加载配置
const config = loadConfig();
setLogLevel(config.log.level);
log.info('Configuration loaded');
// 2. 初始化数据库
initDatabase(config.db.path);
log.info('Database initialized');
// 3. 初始化 AI 管理器
this.ai = new AIManager();
log.info('AI Manager initialized');
// 4. 初始化核心系统
this.remoteExecution = new RemoteExecutionEngine();
this.taskOrchestrator = new TaskOrchestrator();
this.memory = new PersistentMemorySystem();
this.proactive = new ProactiveServiceSystem();
log.info('Core systems initialized');
// 5. 初始化多 Agent 协作系统
this.agentOrchestrator = new AgentOrchestrator(this.ai);
log.info('Agent Orchestrator initialized');
// 6. 初始化 IM 网关
this.imGateway = new IMGateway();
this.setupIMAdapters(config);
this.setupMessageRouting();
log.info('IM Gateway initialized');
// 7. 连接 IM
await this.imGateway.connectAll();
log.info('ZCLAW started successfully! 🦞');
log.info(`Server: ${config.server.host}:${config.server.port}`);
log.info(`AI Provider: ${config.ai.provider} (${config.ai.defaultModel})`);
log.info(`Connected IMs: ${this.imGateway.getConnectedAdapters().join(', ') || 'none'}`);
}
private setupIMAdapters(config: ReturnType<typeof getConfig>): void {
// 飞书
if (config.im.feishu.enabled || config.im.feishu.appId) {
const feishu = new FeishuAdapter(config.im.feishu.appId, config.im.feishu.appSecret);
this.imGateway.registerAdapter(feishu);
}
// TODO: 其他 IM 适配器 (Telegram, QQ, WeChat)
}
private setupMessageRouting(): void {
this.imGateway.onMessage(async (message) => {
log.info(`Processing message from ${message.channelType}/${message.userId}: ${message.content.slice(0, 80)}`);
try {
// 记忆:记录用户消息
await this.memory.remember(message.userId, {
id: message.id,
userId: message.userId,
type: 'im_message',
content: { text: message.content, channel: message.channelType },
timestamp: new Date(),
});
// 判断消息类型并路由
const response = await this.handleUserMessage(message.userId, message.content);
// 回复
await this.imGateway.send({
channelType: message.channelType,
channelId: message.channelId,
userId: message.userId,
content: response,
contentType: 'text',
});
} catch (error: any) {
log.error(`Message processing error: ${error.message}`);
await this.imGateway.send({
channelType: message.channelType,
channelId: message.channelId,
content: `处理消息时出错: ${error.message}`,
contentType: 'text',
});
}
});
}
async handleUserMessage(userId: string, content: string): Promise<string> {
// 获取用户记忆上下文
const recentMemories = await this.memory.recall(userId, content, 5);
const context: Record<string, any> = {};
if (recentMemories.length > 0) {
context.recentHistory = recentMemories.map(m => m.content);
}
// 判断是否需要多步骤编排
const isComplex = this.isComplexTask(content);
if (isComplex) {
// 复杂任务 -> 多 Agent 协作
log.info(`Complex task detected, starting multi-agent execution`);
const result = await this.agentOrchestrator.executeGoal(content, context);
return result.success
? result.data?.report || '任务已完成'
: `任务执行失败: ${result.error}`;
} else {
// 简单对话 -> 直接 AI 回复
const profile = await this.memory.getProfile(userId);
const systemPrompt = `你是 ZCLAW一个智能 AI 助手 🦞。你友善、专业、高效。
${profile ? `用户偏好: ${JSON.stringify(profile.preferences)}` : ''}
请用中文简洁回复。`;
return await this.ai.ask(content, systemPrompt);
}
}
private isComplexTask(content: string): boolean {
const complexIndicators = [
'帮我做', '市场调研', '写报告', '分析',
'自动化', '批量', '多步骤', '流程',
'调研', '搜索并整理', '对比', '综合',
'项目管理', '代码生成', '部署',
];
return complexIndicators.some(indicator => content.includes(indicator));
}
// 公开接口:供 Tauri 前端调用
getAI(): AIManager { return this.ai; }
getAgentOrchestrator(): AgentOrchestrator { return this.agentOrchestrator; }
getRemoteExecution(): RemoteExecutionEngine { return this.remoteExecution; }
getTaskOrchestrator(): TaskOrchestrator { return this.taskOrchestrator; }
getMemory(): PersistentMemorySystem { return this.memory; }
getProactive(): ProactiveServiceSystem { return this.proactive; }
getIMGateway(): IMGateway { return this.imGateway; }
async shutdown(): Promise<void> {
log.info('Shutting down ZCLAW...');
await this.agentOrchestrator.shutdown();
await this.imGateway.disconnectAll();
closeDatabase();
log.info('ZCLAW shutdown complete');
}
}

118
src/config/index.ts Normal file
View File

@@ -0,0 +1,118 @@
// ZCLAW 配置管理
import { z } from 'zod';
const ConfigSchema = z.object({
// AI Provider
ai: z.object({
provider: z.enum(['zhipu', 'openai', 'local']).default('zhipu'),
zhipuApiKey: z.string().default(''),
openaiApiKey: z.string().default(''),
openaiBaseUrl: z.string().default('https://api.openai.com/v1'),
defaultModel: z.string().default('glm-4-flash'),
maxTokens: z.number().default(4096),
temperature: z.number().default(0.7),
}),
// Database
db: z.object({
path: z.string().default('./data/zclaw.db'),
}),
// Server
server: z.object({
port: z.number().default(3721),
host: z.string().default('127.0.0.1'),
}),
// IM Channels
im: z.object({
feishu: z.object({
appId: z.string().default(''),
appSecret: z.string().default(''),
enabled: z.boolean().default(false),
}),
telegram: z.object({
botToken: z.string().default(''),
enabled: z.boolean().default(false),
}),
}),
// Execution
execution: z.object({
maxConcurrent: z.number().default(5),
taskTimeout: z.number().default(300000), // 5 minutes
retryAttempts: z.number().default(3),
}),
// Memory
memory: z.object({
maxEventsPerUser: z.number().default(10000),
embeddingModel: z.string().default('text-embedding-3-small'),
}),
// Logging
log: z.object({
level: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
}),
});
export type ZClawConfig = z.infer<typeof ConfigSchema>;
let _config: ZClawConfig | null = null;
export function loadConfig(overrides?: Partial<Record<string, any>>): ZClawConfig {
const env = process.env;
const raw = {
ai: {
provider: env.ZCLAW_AI_PROVIDER || 'zhipu',
zhipuApiKey: env.ZCLAW_ZHIPU_API_KEY || env.ZHIPU_API_KEY || '',
openaiApiKey: env.ZCLAW_OPENAI_API_KEY || env.OPENAI_API_KEY || '',
openaiBaseUrl: env.ZCLAW_OPENAI_BASE_URL || 'https://api.openai.com/v1',
defaultModel: env.ZCLAW_DEFAULT_MODEL || 'glm-4-flash',
maxTokens: parseInt(env.ZCLAW_MAX_TOKENS || '4096'),
temperature: parseFloat(env.ZCLAW_TEMPERATURE || '0.7'),
},
db: {
path: env.ZCLAW_DB_PATH || './data/zclaw.db',
},
server: {
port: parseInt(env.ZCLAW_PORT || '3721'),
host: env.ZCLAW_HOST || '127.0.0.1',
},
im: {
feishu: {
appId: env.ZCLAW_FEISHU_APP_ID || '',
appSecret: env.ZCLAW_FEISHU_APP_SECRET || '',
enabled: env.ZCLAW_FEISHU_ENABLED === 'true',
},
telegram: {
botToken: env.ZCLAW_TELEGRAM_BOT_TOKEN || '',
enabled: env.ZCLAW_TELEGRAM_ENABLED === 'true',
},
},
execution: {
maxConcurrent: parseInt(env.ZCLAW_MAX_CONCURRENT || '5'),
taskTimeout: parseInt(env.ZCLAW_TASK_TIMEOUT || '300000'),
retryAttempts: parseInt(env.ZCLAW_RETRY_ATTEMPTS || '3'),
},
memory: {
maxEventsPerUser: parseInt(env.ZCLAW_MAX_EVENTS || '10000'),
embeddingModel: env.ZCLAW_EMBEDDING_MODEL || 'text-embedding-3-small',
},
log: {
level: env.ZCLAW_LOG_LEVEL || 'info',
},
...overrides,
};
_config = ConfigSchema.parse(raw);
return _config;
}
export function getConfig(): ZClawConfig {
if (!_config) {
return loadConfig();
}
return _config;
}

4
src/core/ai/index.ts Normal file
View File

@@ -0,0 +1,4 @@
export type { AIProvider, ChatMessage, ChatRequest, ChatResponse, StreamChunk, EmbeddingRequest, EmbeddingResponse } from './types';
export { AIManager, getAIManager } from './manager';
export { ZhipuProvider } from './providers/zhipu';
export { OpenAIProvider } from './providers/openai';

142
src/core/ai/manager.ts Normal file
View File

@@ -0,0 +1,142 @@
// AI 模型管理器 - 统一调用接口,支持多 Provider fallback
import type { AIProvider, ChatRequest, ChatResponse, ChatMessage, StreamChunk, EmbeddingRequest, EmbeddingResponse } from './types';
import { ZhipuProvider } from './providers/zhipu';
import { OpenAIProvider } from './providers/openai';
import { getConfig } from '../../config';
import { createLogger } from '../../utils/logger';
const log = createLogger('AIManager');
export class AIManager {
private providers: Map<string, AIProvider> = new Map();
private defaultProvider: string;
constructor() {
const config = getConfig();
this.defaultProvider = config.ai.provider;
// 注册智谱 GLM
if (config.ai.zhipuApiKey) {
this.providers.set('zhipu', new ZhipuProvider(config.ai.zhipuApiKey));
log.info('Zhipu GLM provider registered');
}
// 注册 OpenAI 兼容
if (config.ai.openaiApiKey) {
this.providers.set('openai', new OpenAIProvider(config.ai.openaiApiKey, config.ai.openaiBaseUrl));
log.info('OpenAI provider registered');
}
if (this.providers.size === 0) {
log.warn('No AI providers configured. Set ZCLAW_ZHIPU_API_KEY or ZCLAW_OPENAI_API_KEY.');
}
}
getProvider(name?: string): AIProvider {
const providerName = name || this.defaultProvider;
const provider = this.providers.get(providerName);
if (!provider) {
// fallback: 尝试其他可用 provider
const fallback = this.providers.values().next().value;
if (fallback) {
log.warn(`Provider '${providerName}' not available, falling back to '${fallback.name}'`);
return fallback;
}
throw new Error(`No AI provider available. Configure at least one API key.`);
}
return provider;
}
async chat(request: ChatRequest, providerName?: string): Promise<ChatResponse> {
const provider = this.getProvider(providerName);
const config = getConfig();
const enrichedRequest: ChatRequest = {
...request,
model: request.model || config.ai.defaultModel,
temperature: request.temperature ?? config.ai.temperature,
maxTokens: request.maxTokens ?? config.ai.maxTokens,
};
log.debug(`Chat via ${provider.name}`, { model: enrichedRequest.model, messages: enrichedRequest.messages.length });
try {
const response = await provider.chat(enrichedRequest);
log.debug(`Chat response: ${response.usage.totalTokens} tokens`);
return response;
} catch (error: any) {
log.error(`Chat failed via ${provider.name}: ${error.message}`);
// 尝试 fallback
for (const [name, fallbackProvider] of this.providers) {
if (name !== provider.name) {
log.info(`Retrying with fallback provider: ${name}`);
try {
return await fallbackProvider.chat(enrichedRequest);
} catch {
continue;
}
}
}
throw error;
}
}
async chatStream(request: ChatRequest, providerName?: string): Promise<AsyncIterable<StreamChunk>> {
const provider = this.getProvider(providerName);
if (!provider.chatStream) {
throw new Error(`Provider '${provider.name}' does not support streaming`);
}
return provider.chatStream(request);
}
async embed(request: EmbeddingRequest, providerName?: string): Promise<EmbeddingResponse> {
const provider = this.getProvider(providerName);
if (!provider.embed) {
throw new Error(`Provider '${provider.name}' does not support embeddings`);
}
return provider.embed(request);
}
// 便捷方法:单次对话
async ask(prompt: string, systemPrompt?: string): Promise<string> {
const messages: ChatMessage[] = [];
if (systemPrompt) {
messages.push({ role: 'system', content: systemPrompt });
}
messages.push({ role: 'user', content: prompt });
const response = await this.chat({ messages });
return response.content;
}
// 便捷方法:带上下文的多轮对话
async chatWithHistory(messages: ChatMessage[], systemPrompt?: string): Promise<string> {
const allMessages: ChatMessage[] = [];
if (systemPrompt) {
allMessages.push({ role: 'system', content: systemPrompt });
}
allMessages.push(...messages);
const response = await this.chat({ messages: allMessages });
return response.content;
}
// 便捷方法JSON 输出
async askJson<T = any>(prompt: string, systemPrompt?: string): Promise<T> {
const jsonSystemPrompt = (systemPrompt || '') + '\n\n请严格以 JSON 格式输出,不要包含 markdown 代码块标记。';
const content = await this.ask(prompt, jsonSystemPrompt);
// 清理可能的 markdown 代码块包裹
const cleaned = content.replace(/^```(?:json)?\s*\n?/i, '').replace(/\n?```\s*$/i, '').trim();
return JSON.parse(cleaned) as T;
}
}
let _manager: AIManager | null = null;
export function getAIManager(): AIManager {
if (!_manager) {
_manager = new AIManager();
}
return _manager;
}

View File

@@ -0,0 +1,139 @@
// OpenAI Compatible Provider (also works for local models via API)
import type { AIProvider, ChatRequest, ChatResponse, StreamChunk, EmbeddingRequest, EmbeddingResponse } from '../types';
import { createLogger } from '../../../utils/logger';
const log = createLogger('OpenAIProvider');
export class OpenAIProvider implements AIProvider {
name = 'openai';
private apiKey: string;
private baseUrl: string;
constructor(apiKey: string, baseUrl: string = 'https://api.openai.com/v1') {
this.apiKey = apiKey;
this.baseUrl = baseUrl.replace(/\/$/, '');
}
async chat(request: ChatRequest): Promise<ChatResponse> {
const model = request.model || 'gpt-4o-mini';
log.debug(`Chat request to ${model}`);
const response = await fetch(`${this.baseUrl}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
},
body: JSON.stringify({
model,
messages: request.messages,
temperature: request.temperature ?? 0.7,
max_tokens: request.maxTokens ?? 4096,
stream: false,
}),
});
if (!response.ok) {
const errorText = await response.text();
log.error(`OpenAI API error: ${response.status}`, errorText);
throw new Error(`OpenAI API error: ${response.status} - ${errorText}`);
}
const data: any = await response.json();
const choice = data.choices?.[0];
return {
content: choice?.message?.content || '',
model: data.model || model,
usage: {
promptTokens: data.usage?.prompt_tokens || 0,
completionTokens: data.usage?.completion_tokens || 0,
totalTokens: data.usage?.total_tokens || 0,
},
finishReason: choice?.finish_reason || 'stop',
};
}
async *chatStream(request: ChatRequest): AsyncIterable<StreamChunk> {
const model = request.model || 'gpt-4o-mini';
const response = await fetch(`${this.baseUrl}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
},
body: JSON.stringify({
model,
messages: request.messages,
temperature: request.temperature ?? 0.7,
max_tokens: request.maxTokens ?? 4096,
stream: true,
}),
});
if (!response.ok) {
throw new Error(`OpenAI API error: ${response.status}`);
}
const reader = response.body?.getReader();
if (!reader) throw new Error('No response body');
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6).trim();
if (data === '[DONE]') {
yield { content: '', done: true };
return;
}
try {
const parsed = JSON.parse(data);
const delta = parsed.choices?.[0]?.delta?.content || '';
if (delta) {
yield { content: delta, done: false };
}
} catch {
// skip
}
}
}
}
}
async embed(request: EmbeddingRequest): Promise<EmbeddingResponse> {
const input = Array.isArray(request.input) ? request.input : [request.input];
const model = request.model || 'text-embedding-3-small';
const response = await fetch(`${this.baseUrl}/embeddings`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
},
body: JSON.stringify({ model, input }),
});
if (!response.ok) {
throw new Error(`OpenAI Embedding API error: ${response.status}`);
}
const data: any = await response.json();
return {
embeddings: data.data?.map((d: any) => d.embedding) || [],
model: data.model || model,
usage: { totalTokens: data.usage?.total_tokens || 0 },
};
}
}

View File

@@ -0,0 +1,138 @@
// 智谱 GLM AI Provider
import type { AIProvider, ChatRequest, ChatResponse, StreamChunk, EmbeddingRequest, EmbeddingResponse } from '../types';
import { createLogger } from '../../../utils/logger';
const log = createLogger('ZhipuProvider');
export class ZhipuProvider implements AIProvider {
name = 'zhipu';
private apiKey: string;
private baseUrl = 'https://open.bigmodel.cn/api/paas/v4';
constructor(apiKey: string) {
this.apiKey = apiKey;
}
async chat(request: ChatRequest): Promise<ChatResponse> {
const model = request.model || 'glm-4-flash';
log.debug(`Chat request to ${model}`, { messageCount: request.messages.length });
const response = await fetch(`${this.baseUrl}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
},
body: JSON.stringify({
model,
messages: request.messages,
temperature: request.temperature ?? 0.7,
max_tokens: request.maxTokens ?? 4096,
stream: false,
}),
});
if (!response.ok) {
const errorText = await response.text();
log.error(`Zhipu API error: ${response.status}`, errorText);
throw new Error(`Zhipu API error: ${response.status} - ${errorText}`);
}
const data: any = await response.json();
const choice = data.choices?.[0];
return {
content: choice?.message?.content || '',
model: data.model || model,
usage: {
promptTokens: data.usage?.prompt_tokens || 0,
completionTokens: data.usage?.completion_tokens || 0,
totalTokens: data.usage?.total_tokens || 0,
},
finishReason: choice?.finish_reason || 'stop',
};
}
async *chatStream(request: ChatRequest): AsyncIterable<StreamChunk> {
const model = request.model || 'glm-4-flash';
const response = await fetch(`${this.baseUrl}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
},
body: JSON.stringify({
model,
messages: request.messages,
temperature: request.temperature ?? 0.7,
max_tokens: request.maxTokens ?? 4096,
stream: true,
}),
});
if (!response.ok) {
throw new Error(`Zhipu API error: ${response.status}`);
}
const reader = response.body?.getReader();
if (!reader) throw new Error('No response body');
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6).trim();
if (data === '[DONE]') {
yield { content: '', done: true };
return;
}
try {
const parsed = JSON.parse(data);
const delta = parsed.choices?.[0]?.delta?.content || '';
if (delta) {
yield { content: delta, done: false };
}
} catch {
// skip invalid JSON
}
}
}
}
}
async embed(request: EmbeddingRequest): Promise<EmbeddingResponse> {
const input = Array.isArray(request.input) ? request.input : [request.input];
const model = request.model || 'embedding-3';
const response = await fetch(`${this.baseUrl}/embeddings`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
},
body: JSON.stringify({ model, input }),
});
if (!response.ok) {
throw new Error(`Zhipu Embedding API error: ${response.status}`);
}
const data: any = await response.json();
return {
embeddings: data.data?.map((d: any) => d.embedding) || [],
model: data.model || model,
usage: { totalTokens: data.usage?.total_tokens || 0 },
};
}
}

48
src/core/ai/types.ts Normal file
View File

@@ -0,0 +1,48 @@
// AI 模型集成层 - 类型定义
export interface ChatMessage {
role: 'system' | 'user' | 'assistant';
content: string;
}
export interface ChatRequest {
messages: ChatMessage[];
model?: string;
temperature?: number;
maxTokens?: number;
stream?: boolean;
}
export interface ChatResponse {
content: string;
model: string;
usage: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
};
finishReason: string;
}
export interface StreamChunk {
content: string;
done: boolean;
}
export interface EmbeddingRequest {
input: string | string[];
model?: string;
}
export interface EmbeddingResponse {
embeddings: number[][];
model: string;
usage: { totalTokens: number };
}
export interface AIProvider {
name: string;
chat(request: ChatRequest): Promise<ChatResponse>;
chatStream?(request: ChatRequest): AsyncIterable<StreamChunk>;
embed?(request: EmbeddingRequest): Promise<EmbeddingResponse>;
}

View File

@@ -1,4 +1,9 @@
// 持续记忆系统
import { generateId } from '../../utils/id';
import { createLogger } from '../../utils/logger';
const log = createLogger('Memory');
export interface UserProfile {
id: string;
preferences: {
@@ -29,7 +34,7 @@ export class PersistentMemorySystem {
event.id = event.id || this.generateId();
event.timestamp = new Date();
this.events.push(event);
console.log([Memory] Event remembered: );
log.debug(`Event remembered: ${event.id} for user ${userId}`);
}
async recall(userId: string, query: string, limit: number = 10): Promise<MemoryEvent[]> {
@@ -42,13 +47,24 @@ export class PersistentMemorySystem {
}
async updateProfile(userId: string, updates: Partial<UserProfile>): Promise<void> {
const profile = this.profiles.get(userId);
if (profile) {
Object.assign(profile, updates);
let profile = this.profiles.get(userId);
if (!profile) {
profile = {
id: userId,
preferences: { language: 'zh', timezone: 'Asia/Shanghai', responseStyle: 'concise' },
patterns: { activeHours: [], frequentCommands: [] },
};
this.profiles.set(userId, profile);
}
Object.assign(profile, updates);
log.debug(`Profile updated for user ${userId}`);
}
async getEventCount(userId: string): Promise<number> {
return this.events.filter(e => e.userId === userId).length;
}
private generateId(): string {
return mem__;
return generateId('mem');
}
}

View File

@@ -0,0 +1,40 @@
// Combiner Agent - 整合多个 Agent 的执行结果
import { BaseAgent } from '../base-agent';
import type { AgentTask } from '../types';
const COMBINER_SYSTEM_PROMPT = `你是一个结果整合专家。你的职责是将多个执行步骤的结果整合为一份完整、结构化的报告。
要求:
1. 综合分析所有步骤的执行结果
2. 提取关键信息
3. 生成简洁清晰的最终报告
4. 如果某些步骤失败,说明缺失的信息
5. 输出格式清晰,适合通过 IM 发送给用户`;
export class CombinerAgent extends BaseAgent {
protected getCapabilities(): string[] {
return ['result_combination', 'report_generation', 'data_synthesis'];
}
protected async run(task: AgentTask): Promise<any> {
const results = task.params.results || [];
const goal = task.params.goal || task.description;
this.log.info(`Combining ${results.length} results for: ${goal}`);
const prompt = `原始目标: ${goal}
各步骤执行结果:
${results.map((r: any, i: number) => `
--- 步骤 ${i + 1} ---
描述: ${r.description || '未知'}
状态: ${r.success ? '成功' : '失败'}
结果: ${JSON.stringify(r.data || r.error, null, 2)}
`).join('\n')}
请整合上述结果,生成一份完整的报告。输出应该清晰、有条理,适合通过即时通讯发送给用户。`;
const report = await this.ai.ask(prompt, COMBINER_SYSTEM_PROMPT);
return { report, stepsCount: results.length, successCount: results.filter((r: any) => r.success).length };
}
}

View File

@@ -0,0 +1,83 @@
// Executor Agent - 通用执行器,根据工具类型执行具体操作
import { BaseAgent } from '../base-agent';
import type { AgentTask } from '../types';
export class BrowserAgent extends BaseAgent {
protected getCapabilities(): string[] {
return ['web_browse', 'web_search', 'screenshot', 'form_fill'];
}
protected async run(task: AgentTask): Promise<any> {
this.log.info(`Browser task: ${task.description}`);
// 使用 AI 模拟浏览器操作结果(后续集成真实浏览器控制)
const prompt = `你需要模拟执行以下浏览器操作并生成合理的结果:
操作描述: ${task.description}
参数: ${JSON.stringify(task.params)}
上下文: ${JSON.stringify(task.context)}
请以 JSON 格式输出执行结果,包含 status、data 字段。`;
return await this.ai.askJson(prompt, '你是一个浏览器操作模拟器,请生成合理的操作结果。');
}
}
export class FileAgent extends BaseAgent {
protected getCapabilities(): string[] {
return ['file_read', 'file_write', 'file_search', 'file_organize'];
}
protected async run(task: AgentTask): Promise<any> {
this.log.info(`File task: ${task.description}`);
const prompt = `你需要执行以下文件操作并生成结果:
操作描述: ${task.description}
参数: ${JSON.stringify(task.params)}
上下文: ${JSON.stringify(task.context)}
请以 JSON 格式输出执行结果。`;
return await this.ai.askJson(prompt, '你是一个文件操作执行器。');
}
}
export class TerminalAgent extends BaseAgent {
protected getCapabilities(): string[] {
return ['command_execute', 'script_run', 'system_info'];
}
protected async run(task: AgentTask): Promise<any> {
this.log.info(`Terminal task: ${task.description}`);
const prompt = `你需要模拟执行以下终端命令操作并生成合理结果:
操作描述: ${task.description}
参数: ${JSON.stringify(task.params)}
上下文: ${JSON.stringify(task.context)}
请以 JSON 格式输出执行结果,包含 stdout、stderr、exitCode 字段。`;
return await this.ai.askJson(prompt, '你是一个终端命令模拟器。');
}
}
export class AIAnalysisAgent extends BaseAgent {
protected getCapabilities(): string[] {
return ['text_analysis', 'content_generation', 'data_processing', 'summarization'];
}
protected async run(task: AgentTask): Promise<any> {
this.log.info(`AI analysis task: ${task.description}`);
const systemPrompt = task.params.systemPrompt || '你是一个智能分析助手,请根据要求完成分析任务。';
const prompt = `${task.description}
${task.context ? `上下文数据:\n${JSON.stringify(task.context, null, 2)}` : ''}
${task.params.data ? `输入数据:\n${JSON.stringify(task.params.data, null, 2)}` : ''}`;
const result = await this.ai.ask(prompt, systemPrompt);
return { analysis: result };
}
}

View File

@@ -0,0 +1,64 @@
// Planner Agent - 任务规划,将用户目标拆解为可执行步骤
import { BaseAgent } from '../base-agent';
import type { AgentTask } from '../types';
const PLANNER_SYSTEM_PROMPT = `你是一个高级任务规划专家。你的职责是将用户的目标拆解为可执行的步骤序列。
可用工具类型:
- browser: 浏览器操作(访问网页、截图、填表、搜索)
- file: 文件操作(读写、搜索、整理、生成文档)
- terminal: 终端命令(代码执行、系统操作、安装软件)
- api: API 调用(搜索引擎、天气、翻译等)
- ai: AI 分析(文本分析、内容生成、数据处理)
输出要求:
严格输出 JSON 格式,不要包含 markdown 代码块标记。
{
"steps": [
{
"id": "step_1",
"description": "步骤描述",
"tool": "browser",
"params": { "url": "https://example.com", "action": "search" },
"dependencies": []
},
{
"id": "step_2",
"description": "步骤描述",
"tool": "ai",
"params": { "action": "analyze" },
"dependencies": ["step_1"]
}
]
}
规则:
1. 每个步骤必须明确、可执行
2. 标注步骤之间的依赖关系dependencies 数组)
3. 无依赖的步骤可以并行执行
4. 步骤数量控制在 3-10 个之间
5. 最后一个步骤通常是整合/汇总结果`;
export class PlannerAgent extends BaseAgent {
protected getCapabilities(): string[] {
return ['task_planning', 'task_decomposition', 'dependency_analysis'];
}
protected async run(task: AgentTask): Promise<any> {
const goal = task.params.goal || task.description;
const context = task.context || {};
this.log.info(`Planning task: ${goal}`);
const prompt = `目标: ${goal}
${Object.keys(context).length > 0 ? `上下文信息:\n${JSON.stringify(context, null, 2)}` : ''}
请将上述目标拆解为可执行步骤。`;
const response = await this.ai.askJson<{ steps: any[] }>(prompt, PLANNER_SYSTEM_PROMPT);
this.log.info(`Plan generated: ${response.steps.length} steps`);
return response;
}
}

View File

@@ -0,0 +1,126 @@
// 多 Agent 协作系统 - Agent 基类
import type { AgentType, AgentConfig, AgentInfo, AgentTask, AgentResult, AgentMessage, AgentStatus } from './types';
import type { MessageBus } from './message-bus';
import type { AIManager } from '../ai/manager';
import { generateId } from '../../utils/id';
import { createLogger, type Logger } from '../../utils/logger';
export abstract class BaseAgent {
readonly id: string;
readonly type: AgentType;
readonly name: string;
protected status: AgentStatus = 'idle';
protected config: AgentConfig;
protected messageBus: MessageBus;
protected ai: AIManager;
protected log: Logger;
protected currentTaskId?: string;
protected createdAt: Date = new Date();
constructor(
type: AgentType,
config: AgentConfig,
messageBus: MessageBus,
ai: AIManager,
) {
this.id = generateId('agent');
this.type = type;
this.name = config.name || `${type}-agent`;
this.config = config;
this.messageBus = messageBus;
this.ai = ai;
this.log = createLogger(`Agent:${this.name}`);
// 订阅消息
this.messageBus.subscribe(this.id, (msg) => this.handleMessage(msg));
this.log.info(`Agent created: ${this.id}`);
}
getInfo(): AgentInfo {
return {
id: this.id,
type: this.type,
name: this.name,
status: this.status,
capabilities: this.getCapabilities(),
currentTaskId: this.currentTaskId,
createdAt: this.createdAt,
};
}
async execute(task: AgentTask): Promise<AgentResult> {
this.status = 'busy';
this.currentTaskId = task.id;
const startTime = Date.now();
this.log.info(`Executing task: ${task.id} - ${task.description}`);
// 通知开始执行
await this.messageBus.send({
from: this.id,
to: '*',
type: 'status',
payload: { taskId: task.id, status: 'running' },
});
try {
const data = await this.run(task);
const result: AgentResult = {
taskId: task.id,
agentId: this.id,
success: true,
data,
duration: Date.now() - startTime,
};
// 通知完成
await this.messageBus.send({
from: this.id,
to: '*',
type: 'result',
payload: result,
});
this.status = 'idle';
this.currentTaskId = undefined;
this.log.info(`Task completed: ${task.id} (${result.duration}ms)`);
return result;
} catch (error: any) {
const result: AgentResult = {
taskId: task.id,
agentId: this.id,
success: false,
error: error.message,
duration: Date.now() - startTime,
};
await this.messageBus.send({
from: this.id,
to: '*',
type: 'error',
payload: result,
});
this.status = 'error';
this.currentTaskId = undefined;
this.log.error(`Task failed: ${task.id} - ${error.message}`);
return result;
}
}
async terminate(): Promise<void> {
this.status = 'terminated';
this.messageBus.unsubscribe(this.id);
this.log.info(`Agent terminated: ${this.id}`);
}
// 子类必须实现
protected abstract run(task: AgentTask): Promise<any>;
protected abstract getCapabilities(): string[];
protected async handleMessage(message: AgentMessage): Promise<void> {
this.log.debug(`Received message from ${message.from}: ${message.type}`);
}
}

View File

@@ -0,0 +1,7 @@
export type { AgentType, AgentConfig, AgentInfo, AgentTask, AgentResult, AgentMessage, AgentStatus, MultiAgentPlan } from './types';
export { BaseAgent } from './base-agent';
export { MessageBus } from './message-bus';
export { AgentOrchestrator } from './orchestrator';
export { PlannerAgent } from './agents/planner-agent';
export { BrowserAgent, FileAgent, TerminalAgent, AIAnalysisAgent } from './agents/executor-agent';
export { CombinerAgent } from './agents/combiner-agent';

View File

@@ -0,0 +1,71 @@
// 多 Agent 消息总线 - Agent 间通信的核心
import type { AgentMessage, MessageHandler } from './types';
import { generateId } from '../../utils/id';
import { createLogger } from '../../utils/logger';
const log = createLogger('MessageBus');
export class MessageBus {
private subscribers: Map<string, MessageHandler[]> = new Map();
private broadcastHandlers: MessageHandler[] = [];
private messageLog: AgentMessage[] = [];
subscribe(agentId: string, handler: MessageHandler): void {
if (!this.subscribers.has(agentId)) {
this.subscribers.set(agentId, []);
}
this.subscribers.get(agentId)!.push(handler);
log.debug(`Agent ${agentId} subscribed to message bus`);
}
unsubscribe(agentId: string): void {
this.subscribers.delete(agentId);
log.debug(`Agent ${agentId} unsubscribed from message bus`);
}
onBroadcast(handler: MessageHandler): void {
this.broadcastHandlers.push(handler);
}
async send(message: Omit<AgentMessage, 'id' | 'timestamp'>): Promise<void> {
const fullMessage: AgentMessage = {
...message,
id: generateId('msg'),
timestamp: new Date(),
};
this.messageLog.push(fullMessage);
log.debug(`Message: ${fullMessage.from} -> ${fullMessage.to} [${fullMessage.type}]`);
if (fullMessage.to === '*') {
// 广播
const allHandlers = [...this.broadcastHandlers];
for (const [, handlers] of this.subscribers) {
allHandlers.push(...handlers);
}
await Promise.allSettled(allHandlers.map(h => h(fullMessage)));
} else {
// 定向发送
const handlers = this.subscribers.get(fullMessage.to);
if (handlers) {
await Promise.allSettled(handlers.map(h => h(fullMessage)));
} else {
log.warn(`No handlers for agent ${fullMessage.to}`);
}
}
}
getMessages(agentId?: string, limit: number = 50): AgentMessage[] {
let messages = this.messageLog;
if (agentId) {
messages = messages.filter(m => m.from === agentId || m.to === agentId || m.to === '*');
}
return messages.slice(-limit);
}
clear(): void {
this.subscribers.clear();
this.broadcastHandlers = [];
this.messageLog = [];
}
}

View File

@@ -0,0 +1,262 @@
// 多 Agent 协作系统 - 编排器(核心协调器)
import type { AgentType, AgentConfig, AgentTask, AgentResult, MultiAgentPlan } from './types';
import { BaseAgent } from './base-agent';
import { MessageBus } from './message-bus';
import { PlannerAgent } from './agents/planner-agent';
import { BrowserAgent, FileAgent, TerminalAgent, AIAnalysisAgent } from './agents/executor-agent';
import { CombinerAgent } from './agents/combiner-agent';
import { AIManager } from '../ai/manager';
import { generateId } from '../../utils/id';
import { createLogger } from '../../utils/logger';
const log = createLogger('AgentOrchestrator');
type ProgressCallback = (plan: MultiAgentPlan, currentStep?: string) => void;
export class AgentOrchestrator {
private agents: Map<string, BaseAgent> = new Map();
private messageBus: MessageBus;
private ai: AIManager;
private activePlans: Map<string, MultiAgentPlan> = new Map();
constructor(ai: AIManager) {
this.messageBus = new MessageBus();
this.ai = ai;
log.info('AgentOrchestrator initialized');
}
// 创建 Agent
spawnAgent(type: AgentType, config: AgentConfig = {}): BaseAgent {
let agent: BaseAgent;
switch (type) {
case 'planner':
agent = new PlannerAgent(type, config, this.messageBus, this.ai);
break;
case 'browser':
agent = new BrowserAgent(type, config, this.messageBus, this.ai);
break;
case 'file':
agent = new FileAgent(type, config, this.messageBus, this.ai);
break;
case 'terminal':
agent = new TerminalAgent(type, config, this.messageBus, this.ai);
break;
case 'combiner':
agent = new CombinerAgent(type, config, this.messageBus, this.ai);
break;
case 'custom':
default:
agent = new AIAnalysisAgent(type, config, this.messageBus, this.ai);
break;
}
this.agents.set(agent.id, agent);
log.info(`Agent spawned: ${agent.id} (${type})`);
return agent;
}
// 终止 Agent
async terminateAgent(agentId: string): Promise<void> {
const agent = this.agents.get(agentId);
if (agent) {
await agent.terminate();
this.agents.delete(agentId);
}
}
// 核心方法:执行多 Agent 协作任务
async executeGoal(goal: string, context: Record<string, any> = {}, onProgress?: ProgressCallback): Promise<AgentResult> {
const planId = generateId('plan');
log.info(`Starting multi-agent execution: ${goal} (${planId})`);
const plan: MultiAgentPlan = {
id: planId,
goal,
steps: [],
agentAssignments: new Map(),
status: 'planning',
results: [],
createdAt: new Date(),
};
this.activePlans.set(planId, plan);
try {
// Phase 1: 规划 - 使用 Planner Agent 拆解任务
plan.status = 'planning';
onProgress?.(plan, '正在规划任务...');
const planner = this.spawnAgent('planner', { name: 'task-planner' });
const planResult = await planner.execute({
id: generateId('task'),
type: 'plan',
description: '规划任务',
params: { goal },
context,
dependencies: [],
});
if (!planResult.success || !planResult.data?.steps) {
throw new Error(`Planning failed: ${planResult.error || 'No steps generated'}`);
}
const steps: AgentTask[] = planResult.data.steps.map((s: any) => ({
id: s.id || generateId('step'),
type: s.tool || 'ai',
description: s.description,
params: s.params || {},
context: {},
dependencies: s.dependencies || [],
}));
plan.steps = steps;
log.info(`Plan created with ${steps.length} steps`);
onProgress?.(plan, `已规划 ${steps.length} 个步骤`);
// Phase 2: 执行 - 按依赖顺序执行各步骤
plan.status = 'executing';
const stepResults: Map<string, AgentResult> = new Map();
// 拓扑排序
const sortedSteps = this.topologicalSort(steps);
for (const step of sortedSteps) {
// 等待依赖完成
for (const depId of step.dependencies) {
const depResult = stepResults.get(depId);
if (depResult?.data) {
step.context[depId] = depResult.data;
}
}
// 根据工具类型选择 Agent
const agentType = this.mapToolToAgentType(step.type);
const executor = this.spawnAgent(agentType, { name: `executor-${step.id}` });
plan.agentAssignments.set(step.id, executor.id);
onProgress?.(plan, `正在执行: ${step.description}`);
const result = await executor.execute(step);
stepResults.set(step.id, result);
plan.results.push(result);
// 执行完毕,终止 executor
await this.terminateAgent(executor.id);
if (!result.success) {
log.warn(`Step failed: ${step.id} - ${result.error}`);
// 继续执行其他不依赖此步骤的任务
}
}
// Phase 3: 整合 - 使用 Combiner Agent 汇总结果
onProgress?.(plan, '正在整合结果...');
const combiner = this.spawnAgent('combiner', { name: 'result-combiner' });
const combineResult = await combiner.execute({
id: generateId('task'),
type: 'combine',
description: '整合所有步骤的结果',
params: {
goal,
results: plan.results.map((r, i) => ({
...r,
description: steps[i]?.description,
})),
},
context: {},
dependencies: [],
});
// 清理
await this.terminateAgent(planner.id);
await this.terminateAgent(combiner.id);
plan.status = 'completed';
onProgress?.(plan, '任务完成');
log.info(`Multi-agent execution completed: ${planId}`);
return {
taskId: planId,
agentId: 'orchestrator',
success: combineResult.success,
data: {
plan: plan.goal,
stepsExecuted: plan.results.length,
stepsSucceeded: plan.results.filter(r => r.success).length,
report: combineResult.data?.report || '',
stepResults: plan.results,
},
duration: Date.now() - plan.createdAt.getTime(),
};
} catch (error: any) {
plan.status = 'failed';
log.error(`Multi-agent execution failed: ${error.message}`);
// 清理所有 Agent
for (const [id] of this.agents) {
await this.terminateAgent(id);
}
return {
taskId: planId,
agentId: 'orchestrator',
success: false,
error: error.message,
duration: Date.now() - plan.createdAt.getTime(),
};
}
}
// 获取活跃计划
getActivePlans(): MultiAgentPlan[] {
return Array.from(this.activePlans.values());
}
// 获取所有 Agent
getAgents(): BaseAgent[] {
return Array.from(this.agents.values());
}
private mapToolToAgentType(tool: string): AgentType {
switch (tool) {
case 'browser': return 'browser';
case 'file': return 'file';
case 'terminal': return 'terminal';
case 'ai':
case 'api':
default: return 'custom'; // AIAnalysisAgent
}
}
private topologicalSort(steps: AgentTask[]): AgentTask[] {
const sorted: AgentTask[] = [];
const visited = new Set<string>();
const visit = (step: AgentTask) => {
if (visited.has(step.id)) return;
visited.add(step.id);
for (const depId of step.dependencies) {
const dep = steps.find(s => s.id === depId);
if (dep) visit(dep);
}
sorted.push(step);
};
steps.forEach(visit);
return sorted;
}
async shutdown(): Promise<void> {
for (const [id] of this.agents) {
await this.terminateAgent(id);
}
this.messageBus.clear();
this.activePlans.clear();
log.info('AgentOrchestrator shutdown');
}
}

View File

@@ -0,0 +1,63 @@
// 多 Agent 协作系统 - 类型定义
export type AgentType = 'planner' | 'browser' | 'file' | 'terminal' | 'combiner' | 'custom';
export type AgentStatus = 'idle' | 'busy' | 'error' | 'terminated';
export interface AgentConfig {
name?: string;
systemPrompt?: string;
model?: string;
maxRetries?: number;
timeout?: number;
}
export interface AgentInfo {
id: string;
type: AgentType;
name: string;
status: AgentStatus;
capabilities: string[];
currentTaskId?: string;
createdAt: Date;
}
export interface AgentTask {
id: string;
type: string;
description: string;
params: Record<string, any>;
context: Record<string, any>;
dependencies: string[];
}
export interface AgentResult {
taskId: string;
agentId: string;
success: boolean;
data?: any;
error?: string;
duration: number;
}
// 消息总线
export interface AgentMessage {
id: string;
from: string;
to: string; // agent ID 或 '*' 表示广播
type: 'task' | 'result' | 'status' | 'data' | 'error' | 'control';
payload: any;
timestamp: Date;
}
export type MessageHandler = (message: AgentMessage) => void | Promise<void>;
// 多 Agent 任务
export interface MultiAgentPlan {
id: string;
goal: string;
steps: AgentTask[];
agentAssignments: Map<string, string>; // taskId -> agentId
status: 'planning' | 'executing' | 'completed' | 'failed';
results: AgentResult[];
createdAt: Date;
}

View File

@@ -1,4 +1,9 @@
// 主动服务系统
import { generateId } from '../../utils/id';
import { createLogger } from '../../utils/logger';
const log = createLogger('Proactive');
export interface ScheduledTask {
id: string;
userId: string;
@@ -26,9 +31,24 @@ export class ProactiveServiceSystem {
task.status = 'active';
this.tasks.set(task.id, task);
// TODO: 使用 node-cron 设置定时任务(后续实现
console.log([Proactive] Task scheduled: );
// node-cron 集成(可选依赖
try {
const cron = require('node-cron');
const cronExpr = this.toCronExpression(task);
if (cronExpr) {
const job = cron.schedule(cronExpr, () => {
log.info(`Cron triggered: ${task.id}`);
// TODO: 执行实际任务逻辑
task.lastRun = new Date();
}, { timezone: task.schedule.timezone });
this.cronJobs.set(task.id, job);
}
} catch {
log.debug('node-cron not available, scheduling in-memory only');
}
log.info(`Task scheduled: ${task.id} (${task.schedule.type} at ${task.schedule.time})`);
}
async cancelTask(taskId: string): Promise<void> {
@@ -43,7 +63,28 @@ export class ProactiveServiceSystem {
return Array.from(this.tasks.values()).filter(t => t.userId === userId);
}
private toCronExpression(task: ScheduledTask): string | null {
const { type, time } = task.schedule;
if (type === 'cron') return time; // already a cron expression
// Parse HH:MM format
const parts = time.split(':');
if (parts.length !== 2) return null;
const [hour, minute] = parts;
switch (type) {
case 'daily':
return `${minute} ${hour} * * *`;
case 'weekly':
return `${minute} ${hour} * * 1`; // Monday
case 'once':
return null; // handled separately via setTimeout
default:
return null;
}
}
private generateId(): string {
return cron__;
return generateId('cron');
}
}

View File

@@ -8,63 +8,88 @@ import type {
Result,
DeviceStatus
} from './types';
import { generateId } from '../../utils/id';
import { createLogger } from '../../utils/logger';
const log = createLogger('RemoteExecution');
export class RemoteExecutionEngine implements RemoteExecutionSystem {
private devices: Map<string, Device> = new Map();
private tasks: Map<string, Task> = new Map();
private subscriptions: Map<string, StatusHandler[]> = new Map();
private taskQueue: Task[] = [];
private runningCount = 0;
private maxConcurrent: number;
private pendingQueue: Task[] = [];
constructor(maxConcurrent: number = 5) {
this.maxConcurrent = maxConcurrent;
}
async registerDevice(device: Device): Promise<void> {
this.devices.set(device.id, device);
console.log([RemoteExecution] Device registered: ());
log.info(`Device registered: ${device.id} (${device.platform})`);
}
async heartbeat(deviceId: string): Promise<DeviceStatus> {
const device = this.devices.get(deviceId);
if (!device) {
throw new Error(Device not found: );
throw new Error(`Device not found: ${deviceId}`);
}
device.lastHeartbeat = new Date();
return device.status;
}
async submitTask(task: Task): Promise<string> {
task.id = task.id || this.generateId();
task.id = task.id || generateId('task');
task.status = 'pending';
task.createdAt = new Date();
this.tasks.set(task.id, task);
this.taskQueue.push(task);
console.log([RemoteExecution] Task submitted: );
// 立即执行(后续会改为队列处理)
this.executeTask(task).catch(console.error);
log.info(`Task submitted: ${task.id} (priority: ${task.priority})`);
if (this.runningCount < this.maxConcurrent) {
this.executeTask(task).catch(err => log.error(`Task execution error: ${err.message}`));
} else {
this.pendingQueue.push(task);
log.debug(`Task queued (${this.pendingQueue.length} pending)`);
}
return task.id;
}
async cancelTask(taskId: string): Promise<void> {
const task = this.tasks.get(taskId);
if (!task) {
throw new Error(Task not found: );
}
if (!task) throw new Error(`Task not found: ${taskId}`);
task.status = 'cancelled';
this.notifySubscribers(taskId, 'cancelled');
log.info(`Task cancelled: ${taskId}`);
}
async getStatus(taskId: string): Promise<TaskStatus> {
const task = this.tasks.get(taskId);
if (!task) {
throw new Error(Task not found: );
}
if (!task) throw new Error(`Task not found: ${taskId}`);
return task.status;
}
getTask(taskId: string): Task | undefined {
return this.tasks.get(taskId);
}
getDevice(deviceId: string): Device | undefined {
return this.devices.get(deviceId);
}
listDevices(): Device[] {
return Array.from(this.devices.values());
}
listTasks(filter?: { status?: TaskStatus; userId?: string }): Task[] {
let tasks = Array.from(this.tasks.values());
if (filter?.status) tasks = tasks.filter(t => t.status === filter.status);
if (filter?.userId) tasks = tasks.filter(t => t.userId === filter.userId);
return tasks.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
}
subscribe(taskId: string, handler: StatusHandler): void {
if (!this.subscriptions.has(taskId)) {
this.subscriptions.set(taskId, []);
@@ -74,41 +99,61 @@ export class RemoteExecutionEngine implements RemoteExecutionSystem {
async pushResult(taskId: string, result: Result): Promise<void> {
const task = this.tasks.get(taskId);
if (!task) {
throw new Error(Task not found: );
}
if (!task) throw new Error(`Task not found: ${taskId}`);
task.result = result;
task.status = result.success ? 'completed' : 'failed';
task.completedAt = new Date();
this.notifySubscribers(taskId, task.status);
}
getStats(): { total: number; running: number; pending: number; completed: number; failed: number } {
const tasks = Array.from(this.tasks.values());
return {
total: tasks.length,
running: tasks.filter(t => t.status === 'running').length,
pending: tasks.filter(t => t.status === 'pending').length,
completed: tasks.filter(t => t.status === 'completed').length,
failed: tasks.filter(t => t.status === 'failed').length,
};
}
private async executeTask(task: Task): Promise<void> {
this.runningCount++;
try {
task.status = 'running';
task.startedAt = new Date();
this.notifySubscribers(task.id, 'running');
// TODO: 实际执行逻辑(调用 OpenClaw SDK
console.log([RemoteExecution] Executing task: );
// 模拟执行
log.info(`Executing task: ${task.id}`);
// TODO: 集成 OpenClaw SDK 实际执行
await new Promise(resolve => setTimeout(resolve, 1000));
await this.pushResult(task.id, {
taskId: task.id,
success: true,
data: { message: 'Task completed successfully' }
});
} catch (error: any) {
await this.pushResult(task.id, {
taskId: task.id,
success: false,
error: error.message
});
} finally {
this.runningCount--;
this.processNextInQueue();
}
}
private processNextInQueue(): void {
if (this.pendingQueue.length > 0 && this.runningCount < this.maxConcurrent) {
// 按优先级排序
this.pendingQueue.sort((a, b) => {
const priority: Record<string, number> = { high: 0, normal: 1, low: 2 };
return (priority[a.priority] ?? 1) - (priority[b.priority] ?? 1);
});
const next = this.pendingQueue.shift()!;
this.executeTask(next).catch(err => log.error(`Queued task error: ${err.message}`));
}
}
@@ -118,8 +163,4 @@ export class RemoteExecutionEngine implements RemoteExecutionSystem {
handlers.forEach(handler => handler(status, progress));
}
}
private generateId(): string {
return ask__;
}
}

View File

@@ -7,6 +7,10 @@ import type {
Progress,
StepStatus
} from './types';
import { generateId } from '../../utils/id';
import { createLogger } from '../../utils/logger';
const log = createLogger('TaskOrchestrator');
export class TaskOrchestrator implements TaskOrchestrationEngine {
private plans: Map<string, TaskPlan> = new Map();
@@ -52,7 +56,7 @@ export class TaskOrchestrator implements TaskOrchestrationEngine {
};
this.plans.set(plan.id, plan);
console.log([TaskOrchestrator] Plan created: );
log.info(`Plan created: ${plan.id} with ${steps.length} steps`);
return plan;
}
@@ -72,7 +76,7 @@ export class TaskOrchestrator implements TaskOrchestrationEngine {
step.status = 'running';
plan.progress = this.calculateProgress(plan);
console.log([TaskOrchestrator] Executing step: );
log.info(`Executing step: ${step.id} - ${step.description}`);
// TODO: 实际执行逻辑(后续实现)
await new Promise(resolve => setTimeout(resolve, 500));
@@ -101,7 +105,7 @@ export class TaskOrchestrator implements TaskOrchestrationEngine {
async getProgress(planId: string): Promise<Progress> {
const plan = this.plans.get(planId);
if (!plan) {
throw new Error(Plan not found: );
throw new Error(`Plan not found: ${planId}`);
}
const completed = plan.steps.filter(s => s.status === 'completed').length;
@@ -180,7 +184,13 @@ export class TaskOrchestrator implements TaskOrchestrationEngine {
return completed / plan.steps.length;
}
listPlans(filter?: { status?: string }): TaskPlan[] {
let plans = Array.from(this.plans.values());
if (filter?.status) plans = plans.filter(p => p.status === filter.status);
return plans;
}
private generateId(): string {
return plan__;
return generateId('plan');
}
}

91
src/db/database.ts Normal file
View File

@@ -0,0 +1,91 @@
// ZCLAW 数据库管理
import Database from 'better-sqlite3';
import { existsSync, mkdirSync } from 'fs';
import { dirname } from 'path';
import { SCHEMA_SQL } from './schema';
import { createLogger } from '../utils/logger';
const log = createLogger('Database');
let _db: Database.Database | null = null;
export function initDatabase(dbPath: string): Database.Database {
// 确保目录存在
const dir = dirname(dbPath);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
_db = new Database(dbPath);
// 启用 WAL 模式提升性能
_db.pragma('journal_mode = WAL');
_db.pragma('foreign_keys = ON');
// 创建表
_db.exec(SCHEMA_SQL);
log.info(`Database initialized at ${dbPath}`);
return _db;
}
export function getDatabase(): Database.Database {
if (!_db) {
throw new Error('Database not initialized. Call initDatabase() first.');
}
return _db;
}
export function closeDatabase(): void {
if (_db) {
_db.close();
_db = null;
log.info('Database closed');
}
}
// 通用 CRUD 辅助
export class BaseDAO<T extends Record<string, any>> {
constructor(
protected tableName: string,
protected db: Database.Database = getDatabase()
) {}
findById(id: string): T | undefined {
return this.db.prepare(`SELECT * FROM ${this.tableName} WHERE id = ?`).get(id) as T | undefined;
}
findAll(where?: string, params?: any[]): T[] {
const sql = where
? `SELECT * FROM ${this.tableName} WHERE ${where}`
: `SELECT * FROM ${this.tableName}`;
return (params ? this.db.prepare(sql).all(...params) : this.db.prepare(sql).all()) as T[];
}
insert(data: Partial<T>): void {
const keys = Object.keys(data);
const placeholders = keys.map(() => '?').join(', ');
const sql = `INSERT OR REPLACE INTO ${this.tableName} (${keys.join(', ')}) VALUES (${placeholders})`;
this.db.prepare(sql).run(...Object.values(data));
}
update(id: string, data: Partial<T>): void {
const sets = Object.keys(data).map(k => `${k} = ?`).join(', ');
const sql = `UPDATE ${this.tableName} SET ${sets} WHERE id = ?`;
this.db.prepare(sql).run(...Object.values(data), id);
}
delete(id: string): void {
this.db.prepare(`DELETE FROM ${this.tableName} WHERE id = ?`).run(id);
}
count(where?: string, params?: any[]): number {
const sql = where
? `SELECT COUNT(*) as count FROM ${this.tableName} WHERE ${where}`
: `SELECT COUNT(*) as count FROM ${this.tableName}`;
const result = params
? this.db.prepare(sql).get(...params) as { count: number }
: this.db.prepare(sql).get() as { count: number };
return result.count;
}
}

2
src/db/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export { initDatabase, getDatabase, closeDatabase, BaseDAO } from './database';
export { SCHEMA_SQL } from './schema';

118
src/db/schema.ts Normal file
View File

@@ -0,0 +1,118 @@
// ZCLAW 数据库 Schema 定义
export const SCHEMA_SQL = `
-- 用户表
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
avatar TEXT DEFAULT '',
preferences TEXT DEFAULT '{}',
patterns TEXT DEFAULT '{}',
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
-- 设备表
CREATE TABLE IF NOT EXISTS devices (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
platform TEXT NOT NULL CHECK (platform IN ('macos', 'windows', 'linux')),
capabilities TEXT DEFAULT '[]',
status TEXT DEFAULT 'offline' CHECK (status IN ('online', 'offline', 'busy')),
last_heartbeat TEXT,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- 任务表
CREATE TABLE IF NOT EXISTS tasks (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
device_id TEXT,
channel TEXT DEFAULT '',
type TEXT NOT NULL CHECK (type IN ('immediate', 'scheduled')),
priority TEXT DEFAULT 'normal' CHECK (priority IN ('high', 'normal', 'low')),
payload TEXT DEFAULT '{}',
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'running', 'completed', 'failed', 'cancelled')),
result TEXT,
error TEXT,
created_at TEXT DEFAULT (datetime('now')),
started_at TEXT,
completed_at TEXT,
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- 任务计划表
CREATE TABLE IF NOT EXISTS task_plans (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
goal TEXT NOT NULL,
steps TEXT DEFAULT '[]',
status TEXT DEFAULT 'planned' CHECK (status IN ('planned', 'executing', 'completed', 'failed', 'paused')),
progress REAL DEFAULT 0,
context TEXT DEFAULT '{}',
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- 记忆事件表
CREATE TABLE IF NOT EXISTS memory_events (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
type TEXT NOT NULL,
content TEXT NOT NULL,
metadata TEXT DEFAULT '{}',
timestamp TEXT DEFAULT (datetime('now')),
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- 定时任务表
CREATE TABLE IF NOT EXISTS scheduled_tasks (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
channel TEXT DEFAULT '',
schedule_type TEXT NOT NULL CHECK (schedule_type IN ('once', 'daily', 'weekly', 'cron')),
schedule_time TEXT NOT NULL,
timezone TEXT DEFAULT 'Asia/Shanghai',
task_type TEXT NOT NULL,
task_prompt TEXT NOT NULL,
status TEXT DEFAULT 'active' CHECK (status IN ('active', 'paused', 'completed')),
last_run TEXT,
next_run TEXT,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- 对话历史表
CREATE TABLE IF NOT EXISTS conversations (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
agent_id TEXT DEFAULT 'zclaw',
role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'system')),
content TEXT NOT NULL,
metadata TEXT DEFAULT '{}',
timestamp TEXT DEFAULT (datetime('now')),
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- Agent 表
CREATE TABLE IF NOT EXISTS agents (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
type TEXT NOT NULL CHECK (type IN ('planner', 'browser', 'file', 'terminal', 'combiner', 'custom')),
config TEXT DEFAULT '{}',
status TEXT DEFAULT 'idle' CHECK (status IN ('idle', 'busy', 'error', 'terminated')),
created_at TEXT DEFAULT (datetime('now'))
);
-- 索引
CREATE INDEX IF NOT EXISTS idx_tasks_user ON tasks(user_id);
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
CREATE INDEX IF NOT EXISTS idx_memory_user ON memory_events(user_id);
CREATE INDEX IF NOT EXISTS idx_memory_timestamp ON memory_events(timestamp);
CREATE INDEX IF NOT EXISTS idx_conversations_user ON conversations(user_id);
CREATE INDEX IF NOT EXISTS idx_conversations_timestamp ON conversations(timestamp);
CREATE INDEX IF NOT EXISTS idx_scheduled_user ON scheduled_tasks(user_id);
CREATE INDEX IF NOT EXISTS idx_devices_user ON devices(user_id);
`;

13
src/gateway/index.ts Normal file
View File

@@ -0,0 +1,13 @@
export { GatewayManager } from './manager';
export type { GatewayManagerOptions, GatewayInfo, GatewayStatus } from './manager';
export { GatewayWsClient } from './ws-client';
export type {
ConnectParams,
GatewayRequest,
GatewayResponse,
GatewayEvent,
GatewayFrame,
AgentStreamEvent,
WsClientOptions,
ConnectionState,
} from './ws-client';

328
src/gateway/manager.ts Normal file
View File

@@ -0,0 +1,328 @@
/**
* ZCLAW Gateway Manager
*
* Manages the OpenClaw Gateway subprocess lifecycle:
* - Start/stop Gateway daemon
* - Health checking
* - Auto-restart on crash
* - Configuration management
*/
import { spawn, ChildProcess, execSync } from 'child_process';
import { EventEmitter } from 'events';
import * as path from 'path';
import * as fs from 'fs';
export type GatewayStatus = 'stopped' | 'starting' | 'running' | 'error' | 'stopping';
export interface GatewayManagerOptions {
/** OpenClaw home directory (default: ~/.openclaw) */
home?: string;
/** Gateway port (default: 18789) */
port?: number;
/** Gateway host (default: 127.0.0.1) */
host?: string;
/** Auth token for Gateway connections */
token?: string;
/** Auto-restart on crash */
autoRestart?: boolean;
/** Max restart attempts */
maxRestarts?: number;
/** Health check interval in ms (default: 15000) */
healthCheckInterval?: number;
}
export interface GatewayInfo {
status: GatewayStatus;
pid?: number;
port: number;
host: string;
wsUrl: string;
version?: string;
uptime?: number;
error?: string;
}
export class GatewayManager extends EventEmitter {
private process: ChildProcess | null = null;
private status: GatewayStatus = 'stopped';
private startTime: number = 0;
private restartCount: number = 0;
private healthTimer: ReturnType<typeof setInterval> | null = null;
private options: Required<GatewayManagerOptions>;
private lastError: string | null = null;
constructor(opts: GatewayManagerOptions = {}) {
super();
this.options = {
home: opts.home || path.join(process.env.HOME || process.env.USERPROFILE || '', '.openclaw'),
port: opts.port || 18789,
host: opts.host || '127.0.0.1',
token: opts.token || '',
autoRestart: opts.autoRestart ?? true,
maxRestarts: opts.maxRestarts || 5,
healthCheckInterval: opts.healthCheckInterval || 15000,
};
}
/** Get current gateway info */
getInfo(): GatewayInfo {
return {
status: this.status,
pid: this.process?.pid,
port: this.options.port,
host: this.options.host,
wsUrl: `ws://${this.options.host}:${this.options.port}`,
version: this.getVersion(),
uptime: this.status === 'running' ? Date.now() - this.startTime : undefined,
error: this.lastError || undefined,
};
}
/** Check if OpenClaw is installed */
isInstalled(): boolean {
try {
execSync('openclaw --version', { encoding: 'utf-8', stdio: 'pipe' });
return true;
} catch {
return false;
}
}
/** Get OpenClaw version */
getVersion(): string | undefined {
try {
return execSync('openclaw --version', { encoding: 'utf-8', stdio: 'pipe' }).trim();
} catch {
return undefined;
}
}
/** Start the OpenClaw Gateway */
async start(): Promise<void> {
if (this.status === 'running' || this.status === 'starting') {
return;
}
if (!this.isInstalled()) {
this.setStatus('error');
this.lastError = 'OpenClaw is not installed';
throw new Error('OpenClaw is not installed. Run: npm install -g openclaw@latest');
}
this.setStatus('starting');
this.lastError = null;
try {
// Check if Gateway is already running (external instance)
const alreadyRunning = await this.checkHealth();
if (alreadyRunning) {
this.setStatus('running');
this.startTime = Date.now();
this.startHealthCheck();
this.emit('connected', { external: true });
return;
}
// Start Gateway as subprocess
const env: Record<string, string> = {
...process.env as Record<string, string>,
OPENCLAW_HOME: this.options.home,
};
if (this.options.token) {
env.OPENCLAW_GATEWAY_TOKEN = this.options.token;
}
this.process = spawn('openclaw', ['gateway'], {
env,
stdio: ['ignore', 'pipe', 'pipe'],
detached: false,
});
// Capture stdout
this.process.stdout?.on('data', (data: Buffer) => {
const output = data.toString();
this.emit('log', { level: 'info', message: output.trim() });
// Detect when Gateway is ready
if (output.includes('Gateway listening') || output.includes('ready')) {
this.setStatus('running');
this.startTime = Date.now();
this.restartCount = 0;
this.startHealthCheck();
}
});
// Capture stderr
this.process.stderr?.on('data', (data: Buffer) => {
const output = data.toString();
this.emit('log', { level: 'error', message: output.trim() });
});
// Handle process exit
this.process.on('exit', (code, signal) => {
const wasRunning = this.status === 'running';
this.process = null;
this.stopHealthCheck();
if (this.status === 'stopping') {
this.setStatus('stopped');
return;
}
if (code !== 0 && wasRunning && this.options.autoRestart) {
this.restartCount++;
if (this.restartCount <= this.options.maxRestarts) {
this.lastError = `Gateway crashed (exit code: ${code}), restarting (${this.restartCount}/${this.options.maxRestarts})`;
this.emit('log', { level: 'warn', message: this.lastError });
setTimeout(() => this.start(), 2000);
return;
}
}
this.lastError = `Gateway exited with code ${code}, signal ${signal}`;
this.setStatus(code === 0 ? 'stopped' : 'error');
});
// Handle process error
this.process.on('error', (err) => {
this.lastError = err.message;
this.setStatus('error');
this.process = null;
});
// Wait for Gateway to be ready (timeout 30s)
await this.waitForReady(30000);
} catch (err: any) {
this.lastError = err.message;
this.setStatus('error');
throw err;
}
}
/** Stop the Gateway */
async stop(): Promise<void> {
if (this.status === 'stopped' || this.status === 'stopping') {
return;
}
this.setStatus('stopping');
this.stopHealthCheck();
if (this.process) {
// Send SIGTERM for graceful shutdown
this.process.kill('SIGTERM');
// Wait up to 10s for graceful exit
await new Promise<void>((resolve) => {
const timeout = setTimeout(() => {
if (this.process) {
this.process.kill('SIGKILL');
}
resolve();
}, 10000);
if (this.process) {
this.process.once('exit', () => {
clearTimeout(timeout);
resolve();
});
} else {
clearTimeout(timeout);
resolve();
}
});
}
this.process = null;
this.setStatus('stopped');
}
/** Restart the Gateway */
async restart(): Promise<void> {
await this.stop();
await this.start();
}
/** Check Gateway health via HTTP */
async checkHealth(): Promise<boolean> {
try {
const url = `http://${this.options.host}:${this.options.port}`;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 3000);
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timeout);
return response.ok || response.status === 200 || response.status === 101;
} catch {
return false;
}
}
/** Wait for Gateway to become ready */
private waitForReady(timeoutMs: number): Promise<void> {
return new Promise((resolve, reject) => {
const start = Date.now();
const check = async () => {
if (this.status === 'running') {
resolve();
return;
}
if (this.status === 'error') {
reject(new Error(this.lastError || 'Gateway failed to start'));
return;
}
if (Date.now() - start > timeoutMs) {
// Try health check as last resort
const healthy = await this.checkHealth();
if (healthy) {
this.setStatus('running');
this.startTime = Date.now();
this.startHealthCheck();
resolve();
return;
}
reject(new Error('Gateway startup timed out'));
return;
}
setTimeout(check, 1000);
};
check();
});
}
/** Start periodic health checks */
private startHealthCheck() {
this.stopHealthCheck();
this.healthTimer = setInterval(async () => {
const healthy = await this.checkHealth();
if (!healthy && this.status === 'running') {
this.emit('log', { level: 'warn', message: 'Health check failed' });
// Don't immediately mark as error — may be transient
}
}, this.options.healthCheckInterval);
}
/** Stop health checks */
private stopHealthCheck() {
if (this.healthTimer) {
clearInterval(this.healthTimer);
this.healthTimer = null;
}
}
/** Update status and emit event */
private setStatus(status: GatewayStatus) {
const prev = this.status;
this.status = status;
if (prev !== status) {
this.emit('status', { status, previous: prev });
}
}
}

443
src/gateway/ws-client.ts Normal file
View File

@@ -0,0 +1,443 @@
/**
* ZCLAW WebSocket Client
*
* Typed WebSocket client for OpenClaw Gateway protocol.
* Handles:
* - Connection + handshake (connect challenge/response)
* - Request/response pattern (with timeout)
* - Event subscription
* - Auto-reconnect
* - Agent streaming
*/
import WebSocket from 'ws';
import { EventEmitter } from 'events';
import * as crypto from 'crypto';
// === Protocol Types ===
export interface ConnectParams {
minProtocol?: number;
maxProtocol?: number;
client: {
id: string;
version: string;
platform: string;
mode: 'operator' | 'node';
};
role: 'operator' | 'node';
scopes: string[];
auth?: { token?: string };
locale?: string;
userAgent?: string;
device?: {
id: string;
publicKey?: string;
signature?: string;
signedAt?: number;
nonce?: string;
};
}
export interface GatewayRequest {
type: 'req';
id: string;
method: string;
params?: Record<string, any>;
}
export interface GatewayResponse {
type: 'res';
id: string;
ok: boolean;
payload?: any;
error?: any;
}
export interface GatewayEvent {
type: 'event';
event: string;
payload?: any;
seq?: number;
stateVersion?: number;
}
export type GatewayFrame = GatewayRequest | GatewayResponse | GatewayEvent;
// Agent stream events
export interface AgentStreamEvent {
stream: 'assistant' | 'tool' | 'lifecycle';
delta?: string;
content?: string;
tool?: string;
phase?: 'start' | 'end' | 'error';
runId?: string;
error?: string;
}
export interface WsClientOptions {
/** Gateway WebSocket URL (default: ws://127.0.0.1:18789) */
url?: string;
/** Auth token */
token?: string;
/** Client identifier */
clientId?: string;
/** Client version */
clientVersion?: string;
/** Auto-reconnect on disconnect */
autoReconnect?: boolean;
/** Reconnect interval ms */
reconnectInterval?: number;
/** Max reconnect attempts (0 = infinite) */
maxReconnectAttempts?: number;
/** Request timeout ms */
requestTimeout?: number;
}
export type ConnectionState = 'disconnected' | 'connecting' | 'handshaking' | 'connected' | 'reconnecting';
export class GatewayWsClient extends EventEmitter {
private ws: WebSocket | null = null;
private state: ConnectionState = 'disconnected';
private requestId: number = 0;
private pendingRequests = new Map<string, {
resolve: (value: any) => void;
reject: (reason: any) => void;
timer: ReturnType<typeof setTimeout>;
}>();
private reconnectAttempts: number = 0;
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
private deviceId: string;
private options: Required<WsClientOptions>;
constructor(opts: WsClientOptions = {}) {
super();
this.options = {
url: opts.url || 'ws://127.0.0.1:18789',
token: opts.token || '',
clientId: opts.clientId || 'zclaw-tauri',
clientVersion: opts.clientVersion || '0.1.0',
autoReconnect: opts.autoReconnect ?? true,
reconnectInterval: opts.reconnectInterval || 3000,
maxReconnectAttempts: opts.maxReconnectAttempts || 0,
requestTimeout: opts.requestTimeout || 30000,
};
this.deviceId = crypto.randomBytes(16).toString('hex');
}
/** Current connection state */
getState(): ConnectionState {
return this.state;
}
/** Connect to Gateway */
async connect(): Promise<void> {
if (this.state === 'connected' || this.state === 'connecting') {
return;
}
this.setState('connecting');
return new Promise((resolve, reject) => {
try {
this.ws = new WebSocket(this.options.url);
this.ws.on('open', () => {
this.setState('handshaking');
// Wait for connect.challenge event
});
this.ws.on('message', (data: Buffer) => {
try {
const frame: GatewayFrame = JSON.parse(data.toString());
this.handleFrame(frame, resolve);
} catch (err: any) {
this.emit('error', new Error(`Failed to parse frame: ${err.message}`));
}
});
this.ws.on('close', (code: number, reason: Buffer) => {
const wasConnected = this.state === 'connected';
this.cleanup();
if (wasConnected && this.options.autoReconnect) {
this.scheduleReconnect();
}
this.emit('close', { code, reason: reason.toString() });
});
this.ws.on('error', (err: Error) => {
if (this.state === 'connecting') {
this.cleanup();
reject(err);
}
this.emit('error', err);
});
} catch (err) {
this.cleanup();
reject(err);
}
});
}
/** Disconnect from Gateway */
disconnect() {
this.cancelReconnect();
this.options.autoReconnect = false;
if (this.ws) {
this.ws.close(1000, 'Client disconnect');
}
this.cleanup();
}
/** Send a request and wait for response */
async request(method: string, params?: Record<string, any>): Promise<any> {
if (this.state !== 'connected') {
throw new Error(`Cannot send request in state: ${this.state}`);
}
const id = `req_${++this.requestId}`;
const frame: GatewayRequest = { type: 'req', id, method, params: params || {} };
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
this.pendingRequests.delete(id);
reject(new Error(`Request ${method} timed out after ${this.options.requestTimeout}ms`));
}, this.options.requestTimeout);
this.pendingRequests.set(id, { resolve, reject, timer });
this.send(frame);
});
}
/** Send agent message (trigger agent loop) */
async sendAgentMessage(message: string, opts?: {
sessionKey?: string;
model?: string;
agentId?: string;
}): Promise<{ runId: string; acceptedAt: string }> {
return this.request('agent', {
message,
sessionKey: opts?.sessionKey,
model: opts?.model,
agentId: opts?.agentId,
});
}
/** Wait for agent run to complete */
async waitForAgent(runId: string): Promise<{ status: string; startedAt: string; endedAt: string; error?: string }> {
return this.request('agent.wait', { runId });
}
/** Send message through IM channel */
async sendChannelMessage(channel: string, chatId: string, text: string): Promise<any> {
return this.request('send', { channel, chatId, text });
}
/** Get Gateway health */
async getHealth(): Promise<any> {
return this.request('health');
}
/** Get Gateway status */
async getStatus(): Promise<any> {
return this.request('status');
}
// === ZCLAW Custom RPC Methods ===
async listClones(): Promise<any> {
return this.request('zclaw.clones.list');
}
async createClone(opts: { name: string; role?: string; nickname?: string; scenarios?: string[]; model?: string }): Promise<any> {
return this.request('zclaw.clones.create', opts);
}
async getUsageStats(): Promise<any> {
return this.request('zclaw.stats.usage');
}
async getSessionStats(): Promise<any> {
return this.request('zclaw.stats.sessions');
}
async getWorkspaceInfo(): Promise<any> {
return this.request('zclaw.workspace.info');
}
async getPluginStatus(): Promise<any> {
return this.request('zclaw.plugins.status');
}
async getQuickConfig(): Promise<any> {
return this.request('zclaw.config.quick', { get: true });
}
async saveQuickConfig(config: Record<string, any>): Promise<any> {
return this.request('zclaw.config.quick', config);
}
// === Internal ===
private handleFrame(frame: GatewayFrame, connectResolve?: (value: void) => void) {
switch (frame.type) {
case 'event':
this.handleEvent(frame as GatewayEvent, connectResolve);
break;
case 'res':
this.handleResponse(frame as GatewayResponse);
break;
default:
// Ignore unexpected frame types
break;
}
}
private handleEvent(event: GatewayEvent, connectResolve?: (value: void) => void) {
if (event.event === 'connect.challenge' && this.state === 'handshaking') {
// Respond to challenge with connect request
this.performHandshake(event.payload?.nonce, connectResolve);
return;
}
// Emit typed events
this.emit('event', event);
this.emit(`event:${event.event}`, event.payload);
// Specific agent stream events
if (event.event === 'agent') {
this.emit('agent:stream', event.payload as AgentStreamEvent);
}
}
private performHandshake(nonce: string, connectResolve?: (value: void) => void) {
const platform = process.platform === 'win32' ? 'windows' : process.platform === 'darwin' ? 'macos' : 'linux';
const connectReq: GatewayRequest = {
type: 'req',
id: `connect_${Date.now()}`,
method: 'connect',
params: {
minProtocol: 3,
maxProtocol: 3,
client: {
id: this.options.clientId,
version: this.options.clientVersion,
platform,
mode: 'operator',
},
role: 'operator',
scopes: ['operator.read', 'operator.write'],
auth: this.options.token ? { token: this.options.token } : {},
locale: 'zh-CN',
userAgent: `zclaw-tauri/${this.options.clientVersion}`,
device: {
id: this.deviceId,
},
},
};
// Listen for the connect response
const handler = (data: Buffer) => {
try {
const frame = JSON.parse(data.toString());
if (frame.type === 'res' && frame.id === connectReq.id) {
this.ws?.removeListener('message', handler);
if (frame.ok) {
this.setState('connected');
this.reconnectAttempts = 0;
this.emit('connected', frame.payload);
connectResolve?.();
} else {
const err = new Error(`Handshake failed: ${JSON.stringify(frame.error)}`);
this.emit('error', err);
this.cleanup();
}
}
} catch { /* ignore parse errors during handshake */ }
};
this.ws?.on('message', handler);
this.send(connectReq);
}
private handleResponse(res: GatewayResponse) {
const pending = this.pendingRequests.get(res.id);
if (pending) {
clearTimeout(pending.timer);
this.pendingRequests.delete(res.id);
if (res.ok) {
pending.resolve(res.payload);
} else {
pending.reject(new Error(JSON.stringify(res.error)));
}
}
}
private send(frame: GatewayFrame) {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(frame));
}
}
private setState(state: ConnectionState) {
const prev = this.state;
this.state = state;
if (prev !== state) {
this.emit('state', { state, previous: prev });
}
}
private cleanup() {
// Clear all pending requests
for (const [id, pending] of this.pendingRequests) {
clearTimeout(pending.timer);
pending.reject(new Error('Connection closed'));
}
this.pendingRequests.clear();
if (this.ws) {
this.ws.removeAllListeners();
if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
try { this.ws.close(); } catch { /* ignore */ }
}
this.ws = null;
}
this.setState('disconnected');
}
private scheduleReconnect() {
if (this.options.maxReconnectAttempts > 0 && this.reconnectAttempts >= this.options.maxReconnectAttempts) {
this.emit('reconnect:failed', { attempts: this.reconnectAttempts });
return;
}
this.reconnectAttempts++;
this.setState('reconnecting');
const delay = Math.min(
this.options.reconnectInterval * Math.pow(1.5, this.reconnectAttempts - 1),
30000
);
this.reconnectTimer = setTimeout(async () => {
try {
await this.connect();
this.emit('reconnect:success', { attempts: this.reconnectAttempts });
} catch {
// Will trigger another reconnect via the close handler
}
}, delay);
}
private cancelReconnect() {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
}
}

124
src/im/adapters/feishu.ts Normal file
View File

@@ -0,0 +1,124 @@
// 飞书 IM 适配器
import type { IMAdapter, IMSendOptions, IMMessageHandler, IMMessage } from '../types';
import { createLogger } from '../../utils/logger';
import { generateId } from '../../utils/id';
const log = createLogger('FeishuAdapter');
export class FeishuAdapter implements IMAdapter {
name = 'feishu';
private appId: string;
private appSecret: string;
private connected = false;
private accessToken = '';
private handlers: IMMessageHandler[] = [];
private pollingInterval?: ReturnType<typeof setInterval>;
constructor(appId: string, appSecret: string) {
this.appId = appId;
this.appSecret = appSecret;
}
async connect(): Promise<void> {
if (!this.appId || !this.appSecret) {
log.warn('Feishu credentials not configured, running in mock mode');
this.connected = true;
return;
}
try {
// 获取 tenant_access_token
const response = await fetch('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
app_id: this.appId,
app_secret: this.appSecret,
}),
});
const data: any = await response.json();
if (data.code === 0) {
this.accessToken = data.tenant_access_token;
this.connected = true;
log.info('Feishu connected successfully');
} else {
throw new Error(`Feishu auth failed: ${data.msg}`);
}
} catch (error: any) {
log.error(`Feishu connection failed: ${error.message}`);
// 降级到 mock 模式
this.connected = true;
log.warn('Running in mock mode');
}
}
async disconnect(): Promise<void> {
if (this.pollingInterval) {
clearInterval(this.pollingInterval);
}
this.connected = false;
this.accessToken = '';
log.info('Feishu disconnected');
}
async send(options: IMSendOptions): Promise<void> {
if (!this.accessToken) {
log.warn(`[Mock] Send to ${options.channelId}: ${options.content.slice(0, 100)}`);
return;
}
const msgType = options.contentType === 'card' ? 'interactive' : 'text';
const content = msgType === 'text'
? JSON.stringify({ text: options.content })
: options.content;
try {
const response = await fetch('https://open.feishu.cn/open-apis/im/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.accessToken}`,
},
body: JSON.stringify({
receive_id: options.channelId,
msg_type: msgType,
content,
}),
});
const data: any = await response.json();
if (data.code !== 0) {
log.error(`Feishu send failed: ${data.msg}`);
}
} catch (error: any) {
log.error(`Feishu send error: ${error.message}`);
}
}
onMessage(handler: IMMessageHandler): void {
this.handlers.push(handler);
}
isConnected(): boolean {
return this.connected;
}
// 模拟接收消息(用于本地测试)
async simulateMessage(content: string, userId: string = 'test_user'): Promise<void> {
const message: IMMessage = {
id: generateId('msg'),
channelType: 'feishu',
channelId: 'test_channel',
userId,
userName: '测试用户',
content,
contentType: 'text',
timestamp: new Date(),
};
for (const handler of this.handlers) {
await handler(message);
}
}
}

80
src/im/gateway.ts Normal file
View File

@@ -0,0 +1,80 @@
// IM 网关 - 统一消息路由
import type { IMAdapter, IMMessage, IMSendOptions, IMMessageHandler } from './types';
import { createLogger } from '../utils/logger';
const log = createLogger('IMGateway');
export class IMGateway {
private adapters: Map<string, IMAdapter> = new Map();
private messageHandlers: IMMessageHandler[] = [];
registerAdapter(adapter: IMAdapter): void {
this.adapters.set(adapter.name, adapter);
// 转发消息到全局处理器
adapter.onMessage(async (message) => {
log.info(`Message from ${message.channelType}/${message.userId}: ${message.content.slice(0, 50)}...`);
for (const handler of this.messageHandlers) {
try {
await handler(message);
} catch (error: any) {
log.error(`Message handler error: ${error.message}`);
}
}
});
log.info(`IM adapter registered: ${adapter.name}`);
}
onMessage(handler: IMMessageHandler): void {
this.messageHandlers.push(handler);
}
async send(options: IMSendOptions): Promise<void> {
const adapter = this.adapters.get(options.channelType);
if (!adapter) {
log.error(`No adapter for channel type: ${options.channelType}`);
throw new Error(`No adapter for channel type: ${options.channelType}`);
}
if (!adapter.isConnected()) {
log.warn(`Adapter ${options.channelType} not connected, attempting reconnect...`);
await adapter.connect();
}
await adapter.send(options);
log.debug(`Message sent via ${options.channelType}`);
}
async connectAll(): Promise<void> {
for (const [name, adapter] of this.adapters) {
try {
await adapter.connect();
log.info(`Connected: ${name}`);
} catch (error: any) {
log.error(`Failed to connect ${name}: ${error.message}`);
}
}
}
async disconnectAll(): Promise<void> {
for (const [name, adapter] of this.adapters) {
try {
await adapter.disconnect();
log.info(`Disconnected: ${name}`);
} catch (error: any) {
log.error(`Failed to disconnect ${name}: ${error.message}`);
}
}
}
getAdapter(name: string): IMAdapter | undefined {
return this.adapters.get(name);
}
getConnectedAdapters(): string[] {
return Array.from(this.adapters.entries())
.filter(([, adapter]) => adapter.isConnected())
.map(([name]) => name);
}
}

3
src/im/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export type { IMAdapter, IMMessage, IMSendOptions, IMMessageHandler } from './types';
export { IMGateway } from './gateway';
export { FeishuAdapter } from './adapters/feishu';

33
src/im/types.ts Normal file
View File

@@ -0,0 +1,33 @@
// IM 网关 - 类型定义
export interface IMMessage {
id: string;
channelType: string; // 'feishu' | 'telegram' | 'qq' | 'wecom'
channelId: string; // 频道/群组 ID
userId: string;
userName?: string;
content: string;
contentType: 'text' | 'image' | 'file' | 'card';
metadata?: Record<string, any>;
timestamp: Date;
}
export interface IMSendOptions {
channelType: string;
channelId: string;
userId?: string;
content: string;
contentType?: 'text' | 'card' | 'markdown';
metadata?: Record<string, any>;
}
export type IMMessageHandler = (message: IMMessage) => void | Promise<void>;
export interface IMAdapter {
name: string;
connect(): Promise<void>;
disconnect(): Promise<void>;
send(options: IMSendOptions): Promise<void>;
onMessage(handler: IMMessageHandler): void;
isConnected(): boolean;
}

View File

@@ -1,5 +1,34 @@
// ZCLAW 入口文件
import { ZClawApp } from './app';
// 导出所有模块
export * from './core/remote-execution';
export * from './core/task-orchestration';
export * from './core/memory';
export * from './core/proactive';
export * from './core/multi-agent';
export * from './core/ai';
export * from './im';
export * from './db';
export * from './config';
export * from './utils';
export { ZClawApp } from './app';
// 主启动流程
const app = new ZClawApp();
app.start().catch((error) => {
console.error('ZCLAW failed to start:', error);
process.exit(1);
});
// 优雅退出
process.on('SIGINT', async () => {
await app.shutdown();
process.exit(0);
});
process.on('SIGTERM', async () => {
await app.shutdown();
process.exit(0);
});

12
src/utils/id.ts Normal file
View File

@@ -0,0 +1,12 @@
// ZCLAW ID 生成工具
import { randomBytes } from 'crypto';
export function generateId(prefix: string = ''): string {
const timestamp = Date.now().toString(36);
const random = randomBytes(4).toString('hex');
return prefix ? `${prefix}_${timestamp}_${random}` : `${timestamp}_${random}`;
}
export function generateShortId(): string {
return randomBytes(6).toString('base64url');
}

3
src/utils/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export { generateId, generateShortId } from './id';
export { createLogger, setLogLevel } from './logger';
export type { Logger } from './logger';

57
src/utils/logger.ts Normal file
View File

@@ -0,0 +1,57 @@
// ZCLAW 日志系统
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
const LEVEL_PRIORITY: Record<LogLevel, number> = {
debug: 0,
info: 1,
warn: 2,
error: 3,
};
const LEVEL_COLORS: Record<LogLevel, string> = {
debug: '\x1b[36m', // cyan
info: '\x1b[32m', // green
warn: '\x1b[33m', // yellow
error: '\x1b[31m', // red
};
const RESET = '\x1b[0m';
let currentLevel: LogLevel = 'info';
export function setLogLevel(level: LogLevel): void {
currentLevel = level;
}
function shouldLog(level: LogLevel): boolean {
return LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[currentLevel];
}
function formatTimestamp(): string {
return new Date().toISOString().slice(11, 23);
}
function log(level: LogLevel, module: string, message: string, data?: any): void {
if (!shouldLog(level)) return;
const color = LEVEL_COLORS[level];
const timestamp = formatTimestamp();
const prefix = `${color}[${timestamp}] [${level.toUpperCase()}] [${module}]${RESET}`;
if (data !== undefined) {
console.log(`${prefix} ${message}`, typeof data === 'object' ? JSON.stringify(data, null, 2) : data);
} else {
console.log(`${prefix} ${message}`);
}
}
export function createLogger(module: string) {
return {
debug: (message: string, data?: any) => log('debug', module, message, data),
info: (message: string, data?: any) => log('info', module, message, data),
warn: (message: string, data?: any) => log('warn', module, message, data),
error: (message: string, data?: any) => log('error', module, message, data),
};
}
export type Logger = ReturnType<typeof createLogger>;