#!/usr/bin/env node 'use strict'; /** * easyagent 批处理模式 * 用于子智能体执行,不需要交互式 CLI */ const fs = require('fs'); const path = require('path'); const { streamChat } = require('../model/client'); const { executeTool } = require('../tools/dispatcher'); const { getModelByKey } = require('../config'); const { applyUsage, normalizeUsagePayload } = require('../utils/token_usage'); // 解析命令行参数 function parseArgs() { const args = process.argv.slice(2); const config = { workspace: process.cwd(), taskFile: null, systemPromptFile: null, outputFile: null, statsFile: null, progressFile: null, agentId: null, modelKey: null, thinkingMode: null, timeout: 600, }; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === '--workspace' && i + 1 < args.length) { config.workspace = args[++i]; } else if (arg === '--task-file' && i + 1 < args.length) { config.taskFile = args[++i]; } else if (arg === '--system-prompt-file' && i + 1 < args.length) { config.systemPromptFile = args[++i]; } else if (arg === '--output-file' && i + 1 < args.length) { config.outputFile = args[++i]; } else if (arg === '--stats-file' && i + 1 < args.length) { config.statsFile = args[++i]; } else if (arg === '--progress-file' && i + 1 < args.length) { config.progressFile = args[++i]; } else if (arg === '--agent-id' && i + 1 < args.length) { config.agentId = args[++i]; } else if (arg === '--model-key' && i + 1 < args.length) { config.modelKey = args[++i]; } else if (arg === '--thinking-mode' && i + 1 < args.length) { config.thinkingMode = String(args[++i] || '').trim().toLowerCase(); } else if (arg === '--timeout' && i + 1 < args.length) { config.timeout = parseInt(args[++i], 10); } } return config; } // 读取文件内容 function readFile(filePath) { try { return fs.readFileSync(filePath, 'utf8'); } catch (err) { console.error(`读取文件失败: ${filePath}`, err); process.exit(1); } } // 写入输出文件 function writeOutput(filePath, data) { try { fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8'); } catch (err) { console.error(`写入输出文件失败: ${filePath}`, err); } } // 更新统计文件 function updateStats(statsFile, stats) { try { fs.writeFileSync(statsFile, JSON.stringify(stats, null, 2), 'utf8'); } catch (err) { console.error(`更新统计文件失败: ${statsFile}`, err); } } function appendProgress(progressFile, entry) { if (!progressFile) return; try { const dir = path.dirname(progressFile); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.appendFileSync(progressFile, `${JSON.stringify(entry)}\n`, 'utf8'); } catch (err) { console.error(`写入进度失败: ${progressFile}`, err); } } // 主函数 async function main() { const config = parseArgs(); if (!config.taskFile || !config.systemPromptFile || !config.outputFile) { console.error('缺少必需参数: --task-file, --system-prompt-file, --output-file'); process.exit(1); } // 读取任务和系统提示 const taskMessage = readFile(config.taskFile); const systemPrompt = readFile(config.systemPromptFile); // 加载模型配置 const modelConfig = require('../config'); const ensuredConfig = modelConfig.ensureConfig(); if (!ensuredConfig.valid_models || ensuredConfig.valid_models.length === 0) { writeOutput(config.outputFile, { success: false, summary: '未找到可用模型配置', error: 'no_model_config', }); process.exit(1); } // 使用指定模型或默认模型 const modelKey = config.modelKey || ensuredConfig.default_model_key; const model = getModelByKey(ensuredConfig, modelKey); if (!model) { writeOutput(config.outputFile, { success: false, summary: `未找到模型: ${modelKey}`, error: 'model_not_found', }); process.exit(1); } // 加载工具定义 const tools = JSON.parse(fs.readFileSync(path.join(__dirname, '../../doc/tools.json'), 'utf8')); // 添加 finish_task 工具 tools.push({ type: 'function', function: { name: 'finish_task', description: '完成当前任务并退出。调用此工具表示你已经完成了分配的任务,所有交付文件已准备好。', parameters: { type: 'object', properties: { success: { type: 'boolean', description: '任务是否成功完成。true=成功完成所有要求;false=部分完成或遇到问题无法继续。', }, summary: { type: 'string', description: '任务完成摘要(50-200字),说明完成了什么工作、生成了哪些文件、关键发现或结果。如果失败,说明原因和已完成的部分。', }, }, required: ['success', 'summary'], }, }, }); // 初始化对话 const messages = [ { role: 'system', content: systemPrompt }, { role: 'user', content: taskMessage }, ]; // 统计信息 const stats = { runtime_start: Date.now(), runtime_seconds: 0, files_read: 0, edit_files: 0, searches: 0, web_pages: 0, commands: 0, api_calls: 0, token_usage: { prompt: 0, completion: 0, total: 0 }, }; const startTime = Date.now(); const timeoutMs = config.timeout * 1000; let turnCount = 0; const maxTurns = 50; // 防止无限循环 try { while (true) { turnCount++; stats.api_calls += 1; stats.turn_count = turnCount; // 检查超时 const elapsed = Date.now() - startTime; if (elapsed > timeoutMs) { writeOutput(config.outputFile, { success: false, summary: '任务超时未完成', timeout: true, stats: { ...stats, runtime_seconds: Math.floor(elapsed / 1000), }, }); process.exit(1); } // 检查轮次限制 if (turnCount > maxTurns) { writeOutput(config.outputFile, { success: false, summary: `任务执行超过 ${maxTurns} 轮,可能陷入循环`, max_turns_exceeded: true, stats: { ...stats, runtime_seconds: Math.floor(elapsed / 1000), }, }); process.exit(1); } // 更新统计文件 if (config.statsFile) { updateStats(config.statsFile, { ...stats, runtime_seconds: Math.floor(elapsed / 1000), turn_count: turnCount, }); } // 调用 API let assistantMessage = { role: 'assistant', content: '', tool_calls: [] }; let reasoningBuffer = ''; let currentToolCall = null; let usage = null; try { for await (const chunk of streamChat({ config: ensuredConfig, modelKey, messages, tools, thinkingMode: config.thinkingMode === 'thinking', currentContextTokens: 0, abortSignal: null, })) { const choice = chunk.choices?.[0]; const delta = choice?.delta; if (!delta) continue; // 处理内容 if (delta.content) { assistantMessage.content += delta.content; } // 处理 reasoning_content(兼容 reasoning_details) if (delta.reasoning_content || delta.reasoning_details || choice?.reasoning_details) { let rc = ''; if (delta.reasoning_content) { rc = delta.reasoning_content; } else if (delta.reasoning_details) { if (Array.isArray(delta.reasoning_details)) { rc = delta.reasoning_details.map((d) => d.text || '').join(''); } else if (typeof delta.reasoning_details === 'string') { rc = delta.reasoning_details; } else if (delta.reasoning_details && typeof delta.reasoning_details.text === 'string') { rc = delta.reasoning_details.text; } } else if (choice?.reasoning_details) { if (Array.isArray(choice.reasoning_details)) { rc = choice.reasoning_details.map((d) => d.text || '').join(''); } else if (typeof choice.reasoning_details === 'string') { rc = choice.reasoning_details; } else if (choice.reasoning_details && typeof choice.reasoning_details.text === 'string') { rc = choice.reasoning_details.text; } } if (rc) reasoningBuffer += rc; } // 处理工具调用 if (delta.tool_calls) { for (const tc of delta.tool_calls) { if (tc.index !== undefined) { if (!assistantMessage.tool_calls[tc.index]) { assistantMessage.tool_calls[tc.index] = { id: tc.id || '', type: 'function', function: { name: '', arguments: '' }, }; currentToolCall = assistantMessage.tool_calls[tc.index]; } else { currentToolCall = assistantMessage.tool_calls[tc.index]; } if (tc.id) currentToolCall.id = tc.id; if (tc.function?.name) currentToolCall.function.name += tc.function.name; if (tc.function?.arguments) currentToolCall.function.arguments += tc.function.arguments; } } } // 处理 usage if (chunk.usage) { usage = normalizeUsagePayload(chunk.usage); } } } catch (err) { writeOutput(config.outputFile, { success: false, summary: `API 调用失败: ${err.message}`, error: 'api_error', stats: { ...stats, runtime_seconds: Math.floor((Date.now() - startTime) / 1000), }, }); process.exit(1); } // 更新 token 统计 if (usage) { applyUsage(stats.token_usage, usage); } // 添加助手消息到历史(reasoning_content 放在 content 前) const finalAssistantMessage = reasoningBuffer ? { role: 'assistant', reasoning_content: reasoningBuffer, content: assistantMessage.content, tool_calls: assistantMessage.tool_calls, } : assistantMessage; messages.push(finalAssistantMessage); // 如果没有工具调用,检查是否忘记调用 finish_task if (!assistantMessage.tool_calls || assistantMessage.tool_calls.length === 0) { // 兜底机制:提醒调用 finish_task messages.push({ role: 'user', content: '如果你已经完成了任务,请调用 finish_task 工具提交完成报告。如果还没有完成,请继续执行任务。', }); continue; } // 执行工具调用 for (const toolCall of assistantMessage.tool_calls) { const toolName = toolCall.function.name; stats.tool_calls = (stats.tool_calls || 0) + 1; // 检查是否是 finish_task if (toolName === 'finish_task') { let args = {}; try { args = JSON.parse(toolCall.function.arguments || '{}'); } catch (err) { writeOutput(config.outputFile, { success: false, summary: 'finish_task 参数解析失败', error: 'invalid_finish_args', stats: { ...stats, runtime_seconds: Math.floor((Date.now() - startTime) / 1000), }, }); process.exit(1); } // 保存对话记录到交付目录的 .subagent/ 子目录 try { // 从任务文件路径推断交付目录 const taskFileDir = path.dirname(config.taskFile); // 读取任务消息以获取交付目录路径 const taskContent = fs.readFileSync(config.taskFile, 'utf8'); const deliverablesDirMatch = taskContent.match(/\*\*交付目录\*\*:(.+)/); if (deliverablesDirMatch) { const deliverablesDirPath = deliverablesDirMatch[1].split('\n')[0].trim(); const subagentDir = path.join(deliverablesDirPath, '.subagent'); // 确保目录存在 if (!fs.existsSync(subagentDir)) { fs.mkdirSync(subagentDir, { recursive: true }); } // 保存对话记录 const conversationData = { agent_id: config.agentId, created_at: new Date(startTime).toISOString(), completed_at: new Date().toISOString(), success: args.success, summary: args.summary, messages: messages, stats: { ...stats, runtime_seconds: Math.floor((Date.now() - startTime) / 1000), }, }; const conversationFile = path.join(subagentDir, 'conversation.json'); fs.writeFileSync(conversationFile, JSON.stringify(conversationData, null, 2), 'utf8'); } } catch (err) { // 保存对话记录失败不影响任务完成 console.error('保存对话记录失败:', err); } // 写入输出并退出 writeOutput(config.outputFile, { success: args.success || false, summary: args.summary || '任务完成', stats: { ...stats, runtime_seconds: Math.floor((Date.now() - startTime) / 1000), }, }); process.exit(0); } let progressArgs = {}; try { progressArgs = JSON.parse(toolCall.function.arguments || '{}'); } catch (err) { progressArgs = { _raw: toolCall.function.arguments || '' }; } const progressId = toolCall.id || `tool_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`; appendProgress(config.progressFile, { id: progressId, tool: toolName, status: 'running', args: progressArgs, ts: Date.now(), }); // 执行其他工具 const result = await executeTool({ workspace: config.workspace, config: ensuredConfig, allowMode: 'full_access', toolCall, abortSignal: null, }); appendProgress(config.progressFile, { id: progressId, tool: toolName, status: result && result.success ? 'completed' : 'failed', args: progressArgs, ts: Date.now(), }); // 更新统计 if (toolName === 'read_file') stats.files_read++; else if (toolName === 'edit_file') stats.edit_files++; else if (toolName === 'search_workspace') stats.searches++; else if (toolName === 'web_search' || toolName === 'extract_webpage') stats.web_pages++; else if (toolName === 'run_command') stats.commands++; // 添加工具结果到历史 messages.push({ role: 'tool', tool_call_id: toolCall.id, content: result.formatted || '', }); } } } catch (err) { writeOutput(config.outputFile, { success: false, summary: `执行出错: ${err.message}`, error: 'execution_error', stats: { ...stats, runtime_seconds: Math.floor((Date.now() - startTime) / 1000), }, }); process.exit(1); } } // 运行 main().catch((err) => { console.error('批处理模式执行失败:', err); process.exit(1); });