agent-Specialization/easyagent/src/batch/index.js

489 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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);
});