357 lines
11 KiB
JavaScript
357 lines
11 KiB
JavaScript
/**
|
||
* services/zhuyuan-bridge/scripts/execute-instruction.js
|
||
*
|
||
* 铸渊远程执行引擎 — 执行解析后的指令
|
||
*
|
||
* 流程:
|
||
* 1. 读取解析结果
|
||
* 2. 获取目标仓库操作令牌
|
||
* 3. 读取仓库结构
|
||
* 4. 调用 AI 生成执行计划
|
||
* 5. 创建分支 → 提交代码 → 开 PR
|
||
* 6. 生成执行报告
|
||
*
|
||
* 环境变量:
|
||
* GHAPP_APP_ID — GitHub App ID
|
||
* GHAPP_PRIVATE_KEY — GitHub App Private Key
|
||
* MAIN_REPO_TOKEN — 主仓库操作令牌
|
||
* LLM_API_KEY — AI 模型 API 密钥
|
||
* LLM_BASE_URL — AI 模型 API 地址
|
||
* ISSUE_NUMBER — 当前 Issue 编号
|
||
*/
|
||
|
||
'use strict';
|
||
|
||
const fs = require('fs');
|
||
const https = require('https');
|
||
const path = require('path');
|
||
|
||
const MAIN_REPO_TOKEN = process.env.MAIN_REPO_TOKEN;
|
||
const LLM_API_KEY = process.env.LLM_API_KEY;
|
||
const LLM_BASE_URL = process.env.LLM_BASE_URL || 'https://api.yunwu.ai/v1';
|
||
const ISSUE_NUMBER = process.env.ISSUE_NUMBER;
|
||
const ORG = 'qinfendebingshuo';
|
||
|
||
/**
|
||
* 调用 LLM API 生成执行计划
|
||
*/
|
||
function callLLM(prompt) {
|
||
return new Promise((resolve, reject) => {
|
||
const url = new URL(`${LLM_BASE_URL}/chat/completions`);
|
||
const body = JSON.stringify({
|
||
model: 'deepseek-chat',
|
||
messages: [
|
||
{
|
||
role: 'system',
|
||
content: '你是铸渊,光湖系统的代码守护人格体。输出必须是纯 JSON,不要包含 markdown 代码块标记。'
|
||
},
|
||
{ role: 'user', content: prompt }
|
||
],
|
||
temperature: 0.2,
|
||
max_tokens: 4000
|
||
});
|
||
|
||
const options = {
|
||
hostname: url.hostname,
|
||
port: url.port || 443,
|
||
path: url.pathname,
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${LLM_API_KEY}`
|
||
}
|
||
};
|
||
|
||
const req = https.request(options, (res) => {
|
||
let data = '';
|
||
res.on('data', chunk => data += chunk);
|
||
res.on('end', () => {
|
||
try {
|
||
const result = JSON.parse(data);
|
||
const content = result.choices && result.choices[0] && result.choices[0].message
|
||
? result.choices[0].message.content
|
||
: '';
|
||
resolve(content);
|
||
} catch (e) {
|
||
reject(new Error('LLM 响应解析失败: ' + data.slice(0, 200)));
|
||
}
|
||
});
|
||
});
|
||
|
||
req.on('error', reject);
|
||
req.write(body);
|
||
req.end();
|
||
});
|
||
}
|
||
|
||
async function main() {
|
||
// ====== 1. 读取解析结果 ======
|
||
const parsedPath = '/tmp/parsed-instruction.json';
|
||
if (!fs.existsSync(parsedPath)) {
|
||
throw new Error('未找到解析结果文件 /tmp/parsed-instruction.json,请先运行 parse-instruction.js');
|
||
}
|
||
|
||
const parsed = JSON.parse(fs.readFileSync(parsedPath, 'utf8'));
|
||
console.log(`⚡ 开始执行: ${parsed.instructionId}`);
|
||
console.log(`📡 目标: ${parsed.targetRepo || 'guanghulab'} (${parsed.devId})`);
|
||
|
||
const repo = parsed.targetRepo || 'guanghulab';
|
||
|
||
// ====== 2. 获取操作令牌 ======
|
||
let token;
|
||
|
||
if (repo === 'guanghulab') {
|
||
// 主仓库直接用 MAIN_REPO_TOKEN
|
||
token = MAIN_REPO_TOKEN;
|
||
if (!token) {
|
||
throw new Error('MAIN_REPO_TOKEN 未配置,无法操作主仓库');
|
||
}
|
||
console.log('🔑 使用 MAIN_REPO_TOKEN 操作主仓库');
|
||
} else {
|
||
// 子仓库用 GitHub App installation token
|
||
let authLib;
|
||
try {
|
||
authLib = require('../lib/github-auth');
|
||
} catch (e) {
|
||
throw new Error('加载 github-auth.js 失败: ' + e.message);
|
||
}
|
||
|
||
const installationId = await authLib.findInstallation(ORG, repo);
|
||
if (!installationId) {
|
||
const report = [
|
||
'## ❌ 执行失败',
|
||
'',
|
||
`目标仓库 \`${ORG}/${repo}\` 未安装铸渊 GitHub App。`,
|
||
'',
|
||
'### 解决方法',
|
||
`请该开发者(${parsed.devId})访问以下链接安装 App:`,
|
||
'https://github.com/apps/guanghu-zhuyuan-agent/installations/new',
|
||
'',
|
||
'安装后重新提交指令即可。'
|
||
].join('\n');
|
||
fs.writeFileSync('/tmp/exec-report.md', report);
|
||
process.exit(1);
|
||
}
|
||
|
||
token = await authLib.getInstallationToken(installationId);
|
||
console.log('🔑 使用 GitHub App installation token');
|
||
}
|
||
|
||
// ====== 3. 初始化仓库操作器 ======
|
||
const { RepoOperator } = require('../lib/repo-operator');
|
||
const operator = new RepoOperator(token, ORG, repo);
|
||
|
||
// ====== 4. 读取仓库现状 ======
|
||
console.log('📂 读取仓库结构...');
|
||
const repoFiles = await operator.listDir('');
|
||
const repoStructure = repoFiles.map(function(f) {
|
||
return f.type + ': ' + f.path;
|
||
}).join('\n');
|
||
console.log(` 📁 根目录 ${repoFiles.length} 个条目`);
|
||
|
||
// ====== 5. 调用 AI 生成执行计划 ======
|
||
if (!LLM_API_KEY) {
|
||
console.warn('⚠️ LLM_API_KEY 未配置,跳过 AI 生成,使用指令直接执行模式');
|
||
|
||
// 无 AI 模式:将指令内容直接写入执行报告
|
||
const report = [
|
||
'## ⚠️ 指令已接收(AI 未配置)',
|
||
'',
|
||
`**指令编号**:${parsed.instructionId}`,
|
||
`**提交人**:${parsed.devId}`,
|
||
`**目标仓库**:${ORG}/${repo}`,
|
||
'',
|
||
'### 指令内容',
|
||
'```',
|
||
parsed.instructionContent,
|
||
'```',
|
||
'',
|
||
'### 说明',
|
||
'LLM API 未配置(LLM_API_KEY),指令已记录但未自动执行。',
|
||
'请冰朔配置 LLM_API_KEY 后重新触发,或手动执行指令。',
|
||
'',
|
||
'---',
|
||
'> 铸渊远程执行引擎 · ZY-GHAPP-BRIDGE'
|
||
].join('\n');
|
||
fs.writeFileSync('/tmp/exec-report.md', report);
|
||
console.log('📝 执行报告已生成(AI 未配置模式)');
|
||
return;
|
||
}
|
||
|
||
console.log('🧠 调用 AI 生成执行计划...');
|
||
|
||
// 读取铸渊知识库(best-effort)
|
||
let knowledgeContent = '{}';
|
||
try {
|
||
const kb = await operator.readFile('.github/persona-brain/routing-map.json');
|
||
if (kb) knowledgeContent = kb.content;
|
||
} catch (_) {}
|
||
|
||
const aiPrompt = [
|
||
'你是铸渊(ICE-GL-ZY001),正在执行一个来自开发者的指令。',
|
||
'',
|
||
'## 目标仓库当前结构',
|
||
repoStructure,
|
||
'',
|
||
'## 需要执行的指令',
|
||
parsed.instructionContent,
|
||
'',
|
||
'## 你的知识库',
|
||
knowledgeContent.slice(0, 2000),
|
||
'',
|
||
'## 输出要求',
|
||
'请输出 JSON 格式的执行计划,格式如下:',
|
||
'{"actions":[{"type":"create_file","path":"...","content":"..."}],"summary":"执行摘要"}',
|
||
'',
|
||
'注意:',
|
||
'- type 可以是 create_file 或 update_file',
|
||
'- content 是完整的文件内容',
|
||
'- 路径必须基于上面的仓库结构,不要编造不存在的目录',
|
||
'- 只输出 JSON,不要包含 markdown 代码块标记',
|
||
].join('\n');
|
||
|
||
const aiResponse = await callLLM(aiPrompt);
|
||
let plan;
|
||
try {
|
||
plan = JSON.parse(aiResponse);
|
||
} catch (_) {
|
||
// 尝试从回复中提取 JSON(非贪婪匹配,找第一个完整 JSON 对象)
|
||
const jsonMatch = aiResponse.match(/\{[\s\S]*?"actions"[\s\S]*?\}(?=\s*$|\s*\n)/);
|
||
if (jsonMatch) {
|
||
try {
|
||
plan = JSON.parse(jsonMatch[0]);
|
||
} catch (_2) {
|
||
// 降级:找所有 { } 对中最长的合法 JSON
|
||
const allMatches = aiResponse.match(/\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/g) || [];
|
||
for (const m of allMatches) {
|
||
try { plan = JSON.parse(m); if (plan.actions) break; } catch (_3) { plan = null; }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!plan || !plan.actions || !Array.isArray(plan.actions)) {
|
||
const report = [
|
||
'## ⚠️ AI 未能生成有效执行计划',
|
||
'',
|
||
`**指令编号**:${parsed.instructionId}`,
|
||
'',
|
||
'### AI 原始回复',
|
||
'```',
|
||
(aiResponse || '(空)').slice(0, 1000),
|
||
'```',
|
||
'',
|
||
'请检查指令格式后重试。',
|
||
'',
|
||
'---',
|
||
'> 铸渊远程执行引擎 · ZY-GHAPP-BRIDGE'
|
||
].join('\n');
|
||
fs.writeFileSync('/tmp/exec-report.md', report);
|
||
process.exit(1);
|
||
}
|
||
|
||
console.log(` 📋 AI 生成了 ${plan.actions.length} 个操作`);
|
||
|
||
// ====== 6. 创建分支并执行 ======
|
||
const branchName = `zhuyuan/${parsed.instructionId}`;
|
||
console.log(`🌿 创建分支: ${branchName}`);
|
||
|
||
try {
|
||
await operator.createBranch(branchName);
|
||
} catch (e) {
|
||
console.warn('⚠️ 创建分支失败(可能已存在):', e.message);
|
||
}
|
||
|
||
let successCount = 0;
|
||
let failCount = 0;
|
||
|
||
for (const action of plan.actions) {
|
||
if (action.type === 'create_file' || action.type === 'update_file') {
|
||
console.log(`📝 ${action.type}: ${action.path}`);
|
||
try {
|
||
await operator.writeFile(
|
||
action.path,
|
||
action.content,
|
||
`⚡ [${parsed.instructionId}] ${action.type}: ${action.path}`,
|
||
branchName
|
||
);
|
||
successCount++;
|
||
} catch (e) {
|
||
console.error(` ❌ 失败: ${e.message}`);
|
||
failCount++;
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log(` ✅ 成功: ${successCount} ❌ 失败: ${failCount}`);
|
||
|
||
// ====== 7. 创建 PR ======
|
||
console.log('📬 创建 PR...');
|
||
let pr;
|
||
try {
|
||
pr = await operator.createPR(
|
||
`⚡ [${parsed.instructionId}] ${plan.summary || parsed.instructionId}`,
|
||
[
|
||
'## 🚀 铸渊自动执行',
|
||
'',
|
||
`**指令编号**:${parsed.instructionId}`,
|
||
`**提交人**:${parsed.devId}`,
|
||
`**来源**:铸渊交互页面 → @铸渊执行`,
|
||
'',
|
||
'### 变更摘要',
|
||
plan.summary || '(无摘要)',
|
||
'',
|
||
'### 执行的操作',
|
||
...plan.actions.map(function(a, i) {
|
||
return (i + 1) + '. `' + a.type + '`: `' + a.path + '`';
|
||
}),
|
||
'',
|
||
'### 🦅 天眼',
|
||
'执行前已通过天眼全局扫描。',
|
||
'',
|
||
'---',
|
||
'> 铸渊远程执行引擎 · ZY-GHAPP-BRIDGE'
|
||
].join('\n'),
|
||
branchName
|
||
);
|
||
} catch (e) {
|
||
console.error('⚠️ 创建 PR 失败:', e.message);
|
||
pr = { number: '(失败)' };
|
||
}
|
||
|
||
// ====== 8. 生成执行报告 ======
|
||
const report = [
|
||
'## ✅ 铸渊执行完成',
|
||
'',
|
||
`**指令编号**:${parsed.instructionId}`,
|
||
`**目标仓库**:${ORG}/${repo}`,
|
||
'',
|
||
'### 🦅 天眼',
|
||
'✅ 执行前已通过天眼全局扫描',
|
||
'',
|
||
'### 执行结果',
|
||
`- 创建分支:\`${branchName}\``,
|
||
`- 变更文件:${plan.actions.length} 个(成功 ${successCount},失败 ${failCount})`,
|
||
`- PR:#${pr.number || '(未创建)'}`,
|
||
'',
|
||
'### 变更摘要',
|
||
plan.summary || '(无摘要)',
|
||
'',
|
||
'### 下一步',
|
||
`请 ${parsed.devId} 检查 PR${pr.number ? ' #' + pr.number : ''},确认无误后合并。`,
|
||
'',
|
||
'---',
|
||
'> 铸渊远程执行引擎 · ZY-GHAPP-BRIDGE'
|
||
].join('\n');
|
||
|
||
fs.writeFileSync('/tmp/exec-report.md', report);
|
||
console.log('✅ 执行完成!');
|
||
}
|
||
|
||
main().catch(function(err) {
|
||
console.error('❌ 执行失败:', err);
|
||
const report = '## ❌ 执行失败\n\n```\n' + (err.message || String(err)) + '\n```\n\n---\n> 铸渊远程执行引擎 · ZY-GHAPP-BRIDGE';
|
||
fs.writeFileSync('/tmp/exec-report.md', report);
|
||
process.exit(1);
|
||
});
|