512 lines
15 KiB
JavaScript
512 lines
15 KiB
JavaScript
|
|
/**
|
|||
|
|
* GLADA · 步骤执行器 · step-executor.js
|
|||
|
|
*
|
|||
|
|
* 负责:
|
|||
|
|
* 1. 接收单个步骤的执行指令
|
|||
|
|
* 2. 调用 LLM 生成代码/修改方案
|
|||
|
|
* 3. 解析 LLM 输出,提取文件变更
|
|||
|
|
* 4. 应用文件变更到本地文件系统
|
|||
|
|
* 5. 运行测试验证变更
|
|||
|
|
* 6. 记录执行日志(包含"为什么")
|
|||
|
|
*
|
|||
|
|
* 版权:国作登字-2026-A-00037559
|
|||
|
|
* 签发:铸渊 · ICE-GL-ZY001
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
'use strict';
|
|||
|
|
|
|||
|
|
const fs = require('fs');
|
|||
|
|
const path = require('path');
|
|||
|
|
const { execSync } = require('child_process');
|
|||
|
|
const https = require('https');
|
|||
|
|
const http = require('http');
|
|||
|
|
|
|||
|
|
const modelRouter = require('./model-router');
|
|||
|
|
|
|||
|
|
const ROOT = path.resolve(__dirname, '..');
|
|||
|
|
|
|||
|
|
// ── LLM 调用配置 ───────────────────────────────
|
|||
|
|
function getLLMConfig() {
|
|||
|
|
const apiKey = process.env.ZY_LLM_API_KEY || process.env.LLM_API_KEY || '';
|
|||
|
|
const baseUrl = (process.env.ZY_LLM_BASE_URL || process.env.LLM_BASE_URL || '').replace(/\/+$/, '');
|
|||
|
|
return { apiKey, baseUrl };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* HTTP 请求工具
|
|||
|
|
*/
|
|||
|
|
function httpRequest(url, options, body) {
|
|||
|
|
return new Promise((resolve, reject) => {
|
|||
|
|
const parsed = new URL(url);
|
|||
|
|
const isHttps = parsed.protocol === 'https:';
|
|||
|
|
const mod = isHttps ? https : http;
|
|||
|
|
|
|||
|
|
const opts = {
|
|||
|
|
hostname: parsed.hostname,
|
|||
|
|
port: parsed.port || (isHttps ? 443 : 80),
|
|||
|
|
path: parsed.pathname + parsed.search,
|
|||
|
|
method: options.method || 'POST',
|
|||
|
|
headers: options.headers || {},
|
|||
|
|
timeout: options.timeout || 120000,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const req = mod.request(opts, (res) => {
|
|||
|
|
let data = '';
|
|||
|
|
res.on('data', (chunk) => { data += chunk; });
|
|||
|
|
res.on('end', () => {
|
|||
|
|
resolve({ status: res.statusCode, body: data });
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
req.on('error', reject);
|
|||
|
|
req.on('timeout', () => {
|
|||
|
|
req.destroy();
|
|||
|
|
reject(new Error('Request timeout'));
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (body) {
|
|||
|
|
req.write(body);
|
|||
|
|
}
|
|||
|
|
req.end();
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 调用 LLM API(带重试和指数退避)
|
|||
|
|
* @param {string} systemPrompt - 系统提示词
|
|||
|
|
* @param {string} userMessage - 用户消息
|
|||
|
|
* @param {Object} [options] - 选项
|
|||
|
|
* @returns {Promise<string>} LLM 响应文本
|
|||
|
|
*/
|
|||
|
|
async function callLLM(systemPrompt, userMessage, options = {}) {
|
|||
|
|
const config = getLLMConfig();
|
|||
|
|
const model = options.model || 'deepseek-chat';
|
|||
|
|
const maxTokens = options.maxTokens || 8192;
|
|||
|
|
const maxRetries = options.maxRetries || 3;
|
|||
|
|
const backoffMs = options.backoffMs || 5000;
|
|||
|
|
|
|||
|
|
if (!config.apiKey || !config.baseUrl) {
|
|||
|
|
throw new Error('LLM API 未配置:请设置 ZY_LLM_API_KEY 和 ZY_LLM_BASE_URL');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const url = `${config.baseUrl}/chat/completions`;
|
|||
|
|
const body = JSON.stringify({
|
|||
|
|
model,
|
|||
|
|
max_tokens: maxTokens,
|
|||
|
|
messages: [
|
|||
|
|
{ role: 'system', content: systemPrompt },
|
|||
|
|
{ role: 'user', content: userMessage }
|
|||
|
|
]
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
let lastError = null;
|
|||
|
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|||
|
|
try {
|
|||
|
|
const response = await httpRequest(url, {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': 'application/json',
|
|||
|
|
'Authorization': `Bearer ${config.apiKey}`
|
|||
|
|
},
|
|||
|
|
timeout: 180000
|
|||
|
|
}, body);
|
|||
|
|
|
|||
|
|
if (response.status !== 200) {
|
|||
|
|
const errBody = String(response.body || '').substring(0, 500);
|
|||
|
|
// 429 (Rate limit) or 5xx are retryable
|
|||
|
|
if ((response.status === 429 || response.status >= 500) && attempt < maxRetries) {
|
|||
|
|
const wait = backoffMs * Math.pow(2, attempt - 1);
|
|||
|
|
console.warn(`[GLADA-LLM] ⚠️ API返回 ${response.status},${wait / 1000}s 后重试 (${attempt}/${maxRetries})`);
|
|||
|
|
await new Promise(r => setTimeout(r, wait));
|
|||
|
|
lastError = new Error(`LLM API 错误: ${response.status} - ${errBody}`);
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
throw new Error(`LLM API 错误: ${response.status} - ${errBody}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const result = JSON.parse(response.body);
|
|||
|
|
return result.choices?.[0]?.message?.content || '';
|
|||
|
|
|
|||
|
|
} catch (err) {
|
|||
|
|
lastError = err;
|
|||
|
|
// Network errors are retryable
|
|||
|
|
if (attempt < maxRetries && (err.code === 'ECONNRESET' || err.code === 'ETIMEDOUT' || err.code === 'ECONNREFUSED' || err.message === 'Request timeout')) {
|
|||
|
|
const wait = backoffMs * Math.pow(2, attempt - 1);
|
|||
|
|
console.warn(`[GLADA-LLM] ⚠️ 网络错误: ${err.message},${wait / 1000}s 后重试 (${attempt}/${maxRetries})`);
|
|||
|
|
await new Promise(r => setTimeout(r, wait));
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
throw err;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
throw lastError || new Error('LLM API 调用失败:已达最大重试次数');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 解析 LLM 输出中的文件变更
|
|||
|
|
*
|
|||
|
|
* 期望的 LLM 输出格式:
|
|||
|
|
* ```json
|
|||
|
|
* {
|
|||
|
|
* "reasoning": "为什么这样修改的原因",
|
|||
|
|
* "files": [
|
|||
|
|
* {
|
|||
|
|
* "path": "相对路径",
|
|||
|
|
* "action": "create|modify|delete",
|
|||
|
|
* "content": "完整文件内容(create/modify时)"
|
|||
|
|
* }
|
|||
|
|
* ],
|
|||
|
|
* "summary": "变更摘要"
|
|||
|
|
* }
|
|||
|
|
* ```
|
|||
|
|
*
|
|||
|
|
* @param {string} llmOutput - LLM 的原始输出
|
|||
|
|
* @returns {Object|null} 解析后的变更对象
|
|||
|
|
*/
|
|||
|
|
function parseFileChanges(llmOutput) {
|
|||
|
|
// 尝试从输出中提取 JSON
|
|||
|
|
const jsonMatch = llmOutput.match(/```json\s*\n([\s\S]*?)\n```/);
|
|||
|
|
let parsed = null;
|
|||
|
|
|
|||
|
|
if (jsonMatch) {
|
|||
|
|
try {
|
|||
|
|
parsed = JSON.parse(jsonMatch[1]);
|
|||
|
|
} catch {
|
|||
|
|
// 继续尝试其他方式
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!parsed) {
|
|||
|
|
// 尝试直接解析整个输出
|
|||
|
|
try {
|
|||
|
|
parsed = JSON.parse(llmOutput);
|
|||
|
|
} catch {
|
|||
|
|
// 尝试提取第一个 { 到最后一个 } 之间的内容
|
|||
|
|
const braceStart = llmOutput.indexOf('{');
|
|||
|
|
const braceEnd = llmOutput.lastIndexOf('}');
|
|||
|
|
if (braceStart !== -1 && braceEnd > braceStart) {
|
|||
|
|
try {
|
|||
|
|
parsed = JSON.parse(llmOutput.substring(braceStart, braceEnd + 1));
|
|||
|
|
} catch {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!parsed || !parsed.files || !Array.isArray(parsed.files)) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证 files 格式
|
|||
|
|
for (const file of parsed.files) {
|
|||
|
|
if (!file.path || !file.action) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
if (!['create', 'modify', 'delete'].includes(file.action)) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
if ((file.action === 'create' || file.action === 'modify') && typeof file.content !== 'string') {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return parsed;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 拍摄文件快照(用于回滚)
|
|||
|
|
* @param {string[]} filePaths - 要快照的文件路径
|
|||
|
|
* @returns {Map<string, string|null>} 文件内容快照
|
|||
|
|
*/
|
|||
|
|
function snapshotFiles(filePaths) {
|
|||
|
|
const snapshot = new Map();
|
|||
|
|
|
|||
|
|
for (const relPath of filePaths) {
|
|||
|
|
const absPath = path.resolve(ROOT, relPath);
|
|||
|
|
if (fs.existsSync(absPath)) {
|
|||
|
|
snapshot.set(relPath, fs.readFileSync(absPath, 'utf-8'));
|
|||
|
|
} else {
|
|||
|
|
snapshot.set(relPath, null); // 文件不存在
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return snapshot;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 从快照恢复文件
|
|||
|
|
* @param {Map<string, string|null>} snapshot - 文件快照
|
|||
|
|
*/
|
|||
|
|
function restoreFromSnapshot(snapshot) {
|
|||
|
|
for (const [relPath, content] of snapshot) {
|
|||
|
|
const absPath = path.resolve(ROOT, relPath);
|
|||
|
|
if (content === null) {
|
|||
|
|
// 原来不存在的文件,删除
|
|||
|
|
if (fs.existsSync(absPath)) {
|
|||
|
|
fs.unlinkSync(absPath);
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// 恢复原内容
|
|||
|
|
fs.mkdirSync(path.dirname(absPath), { recursive: true });
|
|||
|
|
fs.writeFileSync(absPath, content, 'utf-8');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 应用文件变更
|
|||
|
|
* @param {Object[]} files - 变更文件列表
|
|||
|
|
* @param {Object} constraints - 约束条件
|
|||
|
|
* @returns {{ applied: string[], errors: string[] }}
|
|||
|
|
*/
|
|||
|
|
function applyFileChanges(files, constraints = {}) {
|
|||
|
|
const applied = [];
|
|||
|
|
const errors = [];
|
|||
|
|
const noTouch = constraints.no_touch_files || [];
|
|||
|
|
|
|||
|
|
for (const file of files) {
|
|||
|
|
// 检查约束
|
|||
|
|
const isNoTouch = noTouch.some(nt => file.path.startsWith(nt));
|
|||
|
|
if (isNoTouch) {
|
|||
|
|
errors.push(`跳过受保护文件: ${file.path}`);
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const absPath = path.resolve(ROOT, file.path);
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
if (file.action === 'delete') {
|
|||
|
|
if (fs.existsSync(absPath)) {
|
|||
|
|
fs.unlinkSync(absPath);
|
|||
|
|
applied.push(file.path);
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// create 或 modify
|
|||
|
|
fs.mkdirSync(path.dirname(absPath), { recursive: true });
|
|||
|
|
fs.writeFileSync(absPath, file.content, 'utf-8');
|
|||
|
|
applied.push(file.path);
|
|||
|
|
}
|
|||
|
|
} catch (err) {
|
|||
|
|
errors.push(`文件操作失败 [${file.action}] ${file.path}: ${err.message}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return { applied, errors };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 运行测试
|
|||
|
|
* @param {string} testCommand - 测试命令(仅允许白名单中的命令)
|
|||
|
|
* @returns {{ success: boolean, output: string }}
|
|||
|
|
*/
|
|||
|
|
const ALLOWED_TEST_COMMANDS = [
|
|||
|
|
'npm run test:smoke',
|
|||
|
|
'npm run test:contract',
|
|||
|
|
'npm run test',
|
|||
|
|
'npm test',
|
|||
|
|
'node glada/tests/glada-smoke.test.js'
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
function runTests(testCommand = 'npm run test:smoke') {
|
|||
|
|
// 安全白名单校验,防止任意命令注入
|
|||
|
|
if (!ALLOWED_TEST_COMMANDS.includes(testCommand)) {
|
|||
|
|
return {
|
|||
|
|
success: false,
|
|||
|
|
output: `测试命令不在白名单中: ${testCommand}\n允许的命令: ${ALLOWED_TEST_COMMANDS.join(', ')}`
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const output = execSync(testCommand, {
|
|||
|
|
cwd: ROOT,
|
|||
|
|
encoding: 'utf-8',
|
|||
|
|
timeout: 60000,
|
|||
|
|
stdio: ['pipe', 'pipe', 'pipe']
|
|||
|
|
});
|
|||
|
|
return { success: true, output };
|
|||
|
|
} catch (err) {
|
|||
|
|
return {
|
|||
|
|
success: false,
|
|||
|
|
output: (err.stdout || '') + '\n' + (err.stderr || err.message || '')
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 执行单个步骤
|
|||
|
|
* @param {Object} step - 步骤信息 { step_id, description }
|
|||
|
|
* @param {string} systemPrompt - 系统提示词(来自 context-builder)
|
|||
|
|
* @param {Object} gladaTask - 完整的 GLADA 任务
|
|||
|
|
* @param {Object} [options] - 执行选项
|
|||
|
|
* @returns {Promise<Object>} 执行结果
|
|||
|
|
*/
|
|||
|
|
async function executeStep(step, systemPrompt, gladaTask, options = {}) {
|
|||
|
|
const startTime = Date.now();
|
|||
|
|
|
|||
|
|
// 从 execution_plan 读取配置(如果存在),优先级:options > execution_plan > defaults
|
|||
|
|
const execPlan = gladaTask.execution_plan || {};
|
|||
|
|
const retryPolicy = execPlan.retry_policy || {};
|
|||
|
|
|
|||
|
|
const result = {
|
|||
|
|
step_id: step.step_id,
|
|||
|
|
status: 'pending',
|
|||
|
|
started_at: new Date().toISOString(),
|
|||
|
|
completed_at: null,
|
|||
|
|
reasoning: null,
|
|||
|
|
files_changed: [],
|
|||
|
|
test_result: null,
|
|||
|
|
error: null,
|
|||
|
|
duration_ms: 0
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
console.log(`[GLADA-Executor] 🔧 执行步骤 ${step.step_id}: ${step.description}`);
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 1. 构建步骤执行指令
|
|||
|
|
const userMessage = buildStepPrompt(step, gladaTask);
|
|||
|
|
|
|||
|
|
// 2. 调用 LLM(使用模型路由器自动选择最佳模型)
|
|||
|
|
// 模型选择优先级: options.model > execPlan.model_preference > model-router 自动选择
|
|||
|
|
const modelSelection = await modelRouter.selectModel(step.description, {
|
|||
|
|
model: options.model || execPlan.model_preference || null,
|
|||
|
|
taskType: options.taskType
|
|||
|
|
});
|
|||
|
|
console.log(`[GLADA-Executor] 🤖 调用 LLM · 模型: ${modelSelection.model} (${modelSelection.source})`);
|
|||
|
|
const llmResponse = await callLLM(systemPrompt, userMessage, {
|
|||
|
|
model: modelSelection.model,
|
|||
|
|
maxTokens: options.maxTokens || 8192,
|
|||
|
|
maxRetries: retryPolicy.max_retries || 3,
|
|||
|
|
backoffMs: retryPolicy.backoff_ms || 5000
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 3. 解析文件变更
|
|||
|
|
const changes = parseFileChanges(llmResponse);
|
|||
|
|
if (!changes) {
|
|||
|
|
result.status = 'failed';
|
|||
|
|
result.error = 'LLM 输出格式无法解析为文件变更';
|
|||
|
|
result.raw_output = llmResponse.substring(0, 2000);
|
|||
|
|
result.duration_ms = Date.now() - startTime;
|
|||
|
|
return result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
result.reasoning = changes.reasoning || '无推理记录';
|
|||
|
|
|
|||
|
|
// 4. 拍摄快照
|
|||
|
|
const filePaths = changes.files.map(f => f.path);
|
|||
|
|
const snapshot = snapshotFiles(filePaths);
|
|||
|
|
|
|||
|
|
// 5. 应用变更
|
|||
|
|
console.log(`[GLADA-Executor] 📝 应用 ${changes.files.length} 个文件变更...`);
|
|||
|
|
const { applied, errors } = applyFileChanges(changes.files, gladaTask.constraints);
|
|||
|
|
|
|||
|
|
if (errors.length > 0) {
|
|||
|
|
console.warn(`[GLADA-Executor] ⚠️ 部分变更有错误:`, errors);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
result.files_changed = applied;
|
|||
|
|
|
|||
|
|
// 6. 运行测试(如果启用)
|
|||
|
|
if (gladaTask.constraints?.required_tests !== false && options.skipTests !== true) {
|
|||
|
|
console.log(`[GLADA-Executor] 🧪 运行测试...`);
|
|||
|
|
const testResult = runTests();
|
|||
|
|
result.test_result = {
|
|||
|
|
success: testResult.success,
|
|||
|
|
output: testResult.output.substring(0, 1000)
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 测试失败则回滚
|
|||
|
|
if (!testResult.success) {
|
|||
|
|
console.warn(`[GLADA-Executor] ❌ 测试失败,回滚变更`);
|
|||
|
|
restoreFromSnapshot(snapshot);
|
|||
|
|
result.status = 'rolled_back';
|
|||
|
|
result.error = '测试失败,已回滚: ' + testResult.output.substring(0, 500);
|
|||
|
|
result.files_changed = [];
|
|||
|
|
result.duration_ms = Date.now() - startTime;
|
|||
|
|
return result;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
result.status = 'completed';
|
|||
|
|
result.summary = changes.summary || `完成步骤 ${step.step_id}`;
|
|||
|
|
result.completed_at = new Date().toISOString();
|
|||
|
|
result.duration_ms = Date.now() - startTime;
|
|||
|
|
|
|||
|
|
console.log(`[GLADA-Executor] ✅ 步骤 ${step.step_id} 完成 (${result.duration_ms}ms)`);
|
|||
|
|
return result;
|
|||
|
|
|
|||
|
|
} catch (err) {
|
|||
|
|
result.status = 'failed';
|
|||
|
|
result.error = err.message;
|
|||
|
|
result.duration_ms = Date.now() - startTime;
|
|||
|
|
console.error(`[GLADA-Executor] ❌ 步骤 ${step.step_id} 失败: ${err.message}`);
|
|||
|
|
return result;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 构建步骤执行提示词
|
|||
|
|
* @param {Object} step - 步骤信息
|
|||
|
|
* @param {Object} gladaTask - 完整任务
|
|||
|
|
* @returns {string} 用户消息
|
|||
|
|
*/
|
|||
|
|
function buildStepPrompt(step, gladaTask) {
|
|||
|
|
const parts = [
|
|||
|
|
`## 当前步骤`,
|
|||
|
|
`步骤 ${step.step_id}: ${step.description}`,
|
|||
|
|
'',
|
|||
|
|
`## 任务上下文`,
|
|||
|
|
`任务: ${gladaTask.plan.title}`,
|
|||
|
|
`总步骤数: ${gladaTask.plan.steps.length}`,
|
|||
|
|
''
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
// 约束
|
|||
|
|
if (gladaTask.constraints) {
|
|||
|
|
parts.push('## 约束');
|
|||
|
|
if (gladaTask.constraints.no_touch_files?.length > 0) {
|
|||
|
|
parts.push(`禁止修改的文件: ${gladaTask.constraints.no_touch_files.join(', ')}`);
|
|||
|
|
}
|
|||
|
|
if (gladaTask.constraints.max_files_changed) {
|
|||
|
|
parts.push(`最大修改文件数: ${gladaTask.constraints.max_files_changed}`);
|
|||
|
|
}
|
|||
|
|
parts.push('');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
parts.push('## 输出要求');
|
|||
|
|
parts.push('请以严格的 JSON 格式输出你的修改方案:');
|
|||
|
|
parts.push('```json');
|
|||
|
|
parts.push('{');
|
|||
|
|
parts.push(' "reasoning": "为什么这样修改(必须详细解释因果链)",');
|
|||
|
|
parts.push(' "files": [');
|
|||
|
|
parts.push(' {');
|
|||
|
|
parts.push(' "path": "相对于仓库根目录的文件路径",');
|
|||
|
|
parts.push(' "action": "create|modify|delete",');
|
|||
|
|
parts.push(' "content": "完整的文件内容(create/modify时必须提供完整内容)"');
|
|||
|
|
parts.push(' }');
|
|||
|
|
parts.push(' ],');
|
|||
|
|
parts.push(' "summary": "变更摘要"');
|
|||
|
|
parts.push('}');
|
|||
|
|
parts.push('```');
|
|||
|
|
parts.push('');
|
|||
|
|
parts.push('重要:');
|
|||
|
|
parts.push('1. content 必须是完整的文件内容,不要使用省略号或 "..." ');
|
|||
|
|
parts.push('2. reasoning 必须解释"为什么"这样做,而不仅仅描述"做了什么"');
|
|||
|
|
parts.push('3. 确保修改不会破坏现有功能');
|
|||
|
|
parts.push('4. 只修改与当前步骤相关的文件');
|
|||
|
|
|
|||
|
|
return parts.join('\n');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
module.exports = {
|
|||
|
|
callLLM,
|
|||
|
|
parseFileChanges,
|
|||
|
|
snapshotFiles,
|
|||
|
|
restoreFromSnapshot,
|
|||
|
|
applyFileChanges,
|
|||
|
|
runTests,
|
|||
|
|
executeStep,
|
|||
|
|
buildStepPrompt
|
|||
|
|
};
|