diff --git a/.github/persona-brain/agent-registry.json b/.github/persona-brain/agent-registry.json index 944ac6d7..33ace2f9 100644 --- a/.github/persona-brain/agent-registry.json +++ b/.github/persona-brain/agent-registry.json @@ -1147,6 +1147,30 @@ "daily_checkin_required": false, "parent_sys": "SYS-GLW-0001", "owner": "ICE-0002∞" + }, + { + "id": "AG-ZY-089", + "name": "铸渊远程执行引擎", + "workflow": "zhuyuan-exec-engine.yml", + "duty": "监听 zhuyuan-exec Issue → 天眼前置扫描 → 跨仓库代码执行 → PR 创建 → 结果回写", + "trigger": "Issue opened/labeled (zhuyuan-exec)", + "self_repair": false, + "report_to": "ICE-GL-ZY001", + "parent_sys": "SYS-GLW-0001", + "owner": "ICE-0002∞", + "registered": "2026-03-24" + }, + { + "id": "AG-ZY-090", + "name": "Notion 开发者画像同步", + "workflow": "sync-notion-profiles.yml", + "duty": "定时同步 Notion 开发者画像到 .github/notion-cache/,供交互页面注入模型上下文", + "trigger": "定时 每6小时 + 手动 + repository_dispatch:notion-sync", + "self_repair": true, + "report_to": "ICE-GL-ZY001", + "parent_sys": "SYS-GLW-0001", + "owner": "ICE-0002∞", + "registered": "2026-03-24" } ], "note": "铸渊已扫描所有workflow文件,按此格式逐一注册,编号从AG-ZY-001开始顺序分配", @@ -1164,4 +1188,4 @@ "registered_by": "TCS-0002∞" } } -} +} \ No newline at end of file diff --git a/.github/workflows/zhuyuan-exec-engine.yml b/.github/workflows/zhuyuan-exec-engine.yml new file mode 100644 index 00000000..3cfebc09 --- /dev/null +++ b/.github/workflows/zhuyuan-exec-engine.yml @@ -0,0 +1,126 @@ +# ═══════════════════════════════════════════════ +# 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001 +# 📜 Copyright: 国作登字-2026-A-00037559 +# ═══════════════════════════════════════════════ +name: "🚀 铸渊 · 远程执行引擎" + +on: + issues: + types: [opened, labeled] + +permissions: + contents: write + issues: write + pull-requests: write + +jobs: + execute: + runs-on: ubuntu-latest + if: contains(github.event.issue.labels.*.name, 'zhuyuan-exec') + timeout-minutes: 15 + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install Dependencies + run: | + cd services/zhuyuan-bridge + npm install --production + + # ====== Step 1: 解析指令 ====== + - name: "📋 解析执行指令" + id: parse + env: + ISSUE_BODY: ${{ github.event.issue.body }} + ISSUE_TITLE: ${{ github.event.issue.title }} + run: | + node services/zhuyuan-bridge/scripts/parse-instruction.js + + # ====== Step 2: 天眼强制前置 ====== + - name: "🦅 天眼强制前置扫描" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "🦅 天眼强制前置:检查健康状态..." + # 读取最新天眼报告(从 notion-cache 或 skyeye-core) + if [ -f ".github/notion-cache/skyeye/latest-report.json" ]; then + STATUS=$(cat .github/notion-cache/skyeye/latest-report.json | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('overall_status','unknown'))" 2>/dev/null || echo "unknown") + echo "天眼缓存状态:$STATUS" + if [ "$STATUS" = "critical" ] || [ "$STATUS" = "error" ]; then + echo "❌ 天眼报告严重问题,暂停执行" + echo "SKYEYE_BLOCKED=true" >> $GITHUB_ENV + else + echo "✅ 天眼检查通过(状态:$STATUS)" + echo "SKYEYE_BLOCKED=false" >> $GITHUB_ENV + fi + else + echo "⚠️ 天眼报告缓存不存在,尝试触发扫描..." + gh workflow run zhuyuan-skyeye.yml 2>/dev/null || echo "⚠️ 天眼扫描触发失败(不阻断)" + echo "SKYEYE_BLOCKED=false" >> $GITHUB_ENV + fi + + # ====== Step 2b: 天眼阻断检查 ====== + - name: "🛑 天眼阻断检查" + if: env.SKYEYE_BLOCKED == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh issue comment ${{ github.event.issue.number }} \ + --body "🦅 **天眼阻断**:全局扫描发现严重问题,本次执行已暂停。 + + 请冰朔查看天眼报告后手动重新触发。 + + --- + > 铸渊远程执行引擎 · 天眼强制前置" + exit 1 + + # ====== Step 3: 执行指令 ====== + - name: "⚡ 执行铸渊指令" + env: + GHAPP_APP_ID: ${{ secrets.GHAPP_APP_ID }} + GHAPP_PRIVATE_KEY: ${{ secrets.GHAPP_PRIVATE_KEY }} + MAIN_REPO_TOKEN: ${{ secrets.MAIN_REPO_TOKEN }} + LLM_API_KEY: ${{ secrets.LLM_API_KEY }} + LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + run: | + node services/zhuyuan-bridge/scripts/execute-instruction.js + + # ====== Step 4: 回写结果 ====== + - name: "📝 回写执行报告" + if: always() + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if [ -f /tmp/exec-report.md ]; then + gh issue comment ${{ github.event.issue.number }} \ + --body "$(cat /tmp/exec-report.md)" + + # 判断是否成功执行(报告中包含 ✅ 表示成功) + if grep -q "✅ 铸渊执行完成" /tmp/exec-report.md; then + gh issue close ${{ github.event.issue.number }} \ + --comment "✅ 铸渊执行完成 · 请检查 PR" || true + fi + else + gh issue comment ${{ github.event.issue.number }} \ + --body "## ⚠️ 执行引擎异常 + + 未生成执行报告。请检查 [Workflow 日志](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})。 + + --- + > 铸渊远程执行引擎 · ZY-GHAPP-BRIDGE" || true + fi + + # ====== Step 5: 更新 Notion 缓存 ====== + - name: "📡 更新开发者画像" + if: success() + run: | + echo "📡 Notion 缓存更新预留(需要 sync-notion-profiles.js 支持增量更新)" diff --git a/services/zhuyuan-bridge/lib/github-auth.js b/services/zhuyuan-bridge/lib/github-auth.js new file mode 100644 index 00000000..49d3ce53 --- /dev/null +++ b/services/zhuyuan-bridge/lib/github-auth.js @@ -0,0 +1,168 @@ +/** + * services/zhuyuan-bridge/lib/github-auth.js + * + * GitHub App 鉴权模块 + * - 用 App Private Key 生成 JWT + * - 获取 Installation Access Token + * - 查找仓库对应的 Installation ID + * + * 环境变量: + * GHAPP_APP_ID — GitHub App ID + * GHAPP_PRIVATE_KEY — GitHub App Private Key (PEM) + */ + +'use strict'; + +const https = require('https'); + +/** + * 用 App Private Key 生成 JWT(不依赖 jsonwebtoken 库的 fallback 实现) + * 在 CI 环境中可能没有安装 jsonwebtoken,因此提供纯 Node.js 实现 + */ +function generateJWT() { + const privateKey = process.env.GHAPP_PRIVATE_KEY; + const appId = process.env.GHAPP_APP_ID; + + if (!privateKey || !appId) { + throw new Error('GHAPP_APP_ID 和 GHAPP_PRIVATE_KEY 环境变量必须设置'); + } + + // 尝试使用 jsonwebtoken 库 + try { + const jwt = require('jsonwebtoken'); + const payload = { + iat: Math.floor(Date.now() / 1000) - 60, + exp: Math.floor(Date.now() / 1000) + (10 * 60), + iss: appId + }; + return jwt.sign(payload, privateKey, { algorithm: 'RS256' }); + } catch (e) { + // jsonwebtoken 不可用时,使用 Node.js crypto 实现 + const crypto = require('crypto'); + + function base64url(buf) { + return buf.toString('base64') + .replace(/=/g, '') + .replace(/\+/g, '-') + .replace(/\//g, '_'); + } + + const header = base64url(Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' }))); + const now = Math.floor(Date.now() / 1000); + const payload = base64url(Buffer.from(JSON.stringify({ + iat: now - 60, + exp: now + 600, + iss: appId + }))); + + const sigInput = header + '.' + payload; + const sign = crypto.createSign('RSA-SHA256'); + sign.update(sigInput); + sign.end(); + const signature = base64url(sign.sign(privateKey)); + + return sigInput + '.' + signature; + } +} + +/** + * GitHub API 请求封装 + */ +function githubRequest(method, apiPath, token, body) { + return new Promise((resolve, reject) => { + const options = { + hostname: 'api.github.com', + path: apiPath, + method: method, + headers: { + 'User-Agent': 'zhuyuan-bridge/1.0', + 'Accept': 'application/vnd.github+json', + 'Authorization': token.startsWith('Bearer ') + ? token + : `token ${token}` + } + }; + if (body) { + options.headers['Content-Type'] = 'application/json'; + } + + const req = https.request(options, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + try { + resolve({ status: res.statusCode, data: JSON.parse(data) }); + } catch (_) { + resolve({ status: res.statusCode, data: data }); + } + }); + }); + req.on('error', reject); + if (body) req.write(JSON.stringify(body)); + req.end(); + }); +} + +/** + * 获取某个 Installation 的操作令牌 + * @param {number|string} installationId + * @returns {Promise} Installation access token + */ +async function getInstallationToken(installationId) { + const jwtToken = generateJWT(); + const result = await githubRequest( + 'POST', + `/app/installations/${installationId}/access_tokens`, + `Bearer ${jwtToken}` + ); + + if (result.status !== 201 || !result.data.token) { + throw new Error(`获取 installation token 失败: ${result.status} ${JSON.stringify(result.data)}`); + } + + return result.data.token; +} + +/** + * 查找某个仓库的 Installation ID + * @param {string} owner + * @param {string} repo + * @returns {Promise} + */ +async function findInstallation(owner, repo) { + const jwtToken = generateJWT(); + const result = await githubRequest( + 'GET', + '/app/installations', + `Bearer ${jwtToken}` + ); + + if (result.status !== 200 || !Array.isArray(result.data)) { + console.warn('⚠️ 获取 installations 列表失败:', result.status); + return null; + } + + for (const inst of result.data) { + try { + const token = await getInstallationToken(inst.id); + const reposResult = await githubRequest( + 'GET', + '/installation/repositories?per_page=100', + token + ); + + if (reposResult.data.repositories) { + const found = reposResult.data.repositories.some( + r => r.full_name === `${owner}/${repo}` + ); + if (found) return inst.id; + } + } catch (e) { + console.warn(`⚠️ 检查 installation ${inst.id} 失败:`, e.message); + } + } + + return null; +} + +module.exports = { generateJWT, getInstallationToken, findInstallation, githubRequest }; diff --git a/services/zhuyuan-bridge/lib/repo-operator.js b/services/zhuyuan-bridge/lib/repo-operator.js new file mode 100644 index 00000000..209769e6 --- /dev/null +++ b/services/zhuyuan-bridge/lib/repo-operator.js @@ -0,0 +1,206 @@ +/** + * services/zhuyuan-bridge/lib/repo-operator.js + * + * 仓库操作封装 — 通过 GitHub API 读写代码、创建分支和 PR + * + * 纯 Node.js,无外部依赖。 + */ + +'use strict'; + +const https = require('https'); + +class RepoOperator { + /** + * @param {string} token — GitHub access token + * @param {string} owner — Repository owner + * @param {string} repo — Repository name + */ + constructor(token, owner, repo) { + this.token = token; + this.owner = owner; + this.repo = repo; + this.baseUrl = `/repos/${owner}/${repo}`; + } + + /** + * GitHub API 请求 + */ + _request(method, apiPath, body) { + return new Promise((resolve, reject) => { + const options = { + hostname: 'api.github.com', + path: apiPath, + method: method, + headers: { + 'User-Agent': 'zhuyuan-bridge/1.0', + 'Accept': 'application/vnd.github+json', + 'Authorization': `token ${this.token}`, + } + }; + if (body) { + options.headers['Content-Type'] = 'application/json'; + } + + const req = https.request(options, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + try { + resolve({ status: res.statusCode, data: JSON.parse(data) }); + } catch (_) { + resolve({ status: res.statusCode, data: data }); + } + }); + }); + req.on('error', reject); + if (body) req.write(JSON.stringify(body)); + req.end(); + }); + } + + /** + * 读取文件内容 + * @param {string} filePath + * @param {string} ref — branch or commit SHA + * @returns {Promise<{content: string, sha: string}|null>} + */ + async readFile(filePath, ref = 'main') { + const res = await this._request( + 'GET', + `${this.baseUrl}/contents/${encodeURIComponent(filePath)}?ref=${encodeURIComponent(ref)}` + ); + if (res.status !== 200) return null; + return { + content: Buffer.from(res.data.content, 'base64').toString('utf8'), + sha: res.data.sha + }; + } + + /** + * 创建或更新文件 + * @param {string} filePath + * @param {string} content + * @param {string} message — commit message + * @param {string} branch + * @returns {Promise} + */ + async writeFile(filePath, content, message, branch = 'main') { + let sha; + try { + const existing = await this.readFile(filePath, branch); + if (existing) sha = existing.sha; + } catch (_) { /* new file */ } + + const body = { + message, + content: Buffer.from(content).toString('base64'), + branch, + committer: { + name: '铸渊 (ZhùYuān)', + email: 'zhuyuan@guanghulab.com' + } + }; + if (sha) body.sha = sha; + + const res = await this._request( + 'PUT', + `${this.baseUrl}/contents/${encodeURIComponent(filePath)}`, + body + ); + return res.data; + } + + /** + * 创建分支 + * @param {string} branchName + * @param {string} fromBranch + * @returns {Promise} + */ + async createBranch(branchName, fromBranch = 'main') { + const refRes = await this._request( + 'GET', + `${this.baseUrl}/git/ref/heads/${encodeURIComponent(fromBranch)}` + ); + + if (refRes.status !== 200) { + throw new Error(`获取分支 ${fromBranch} 失败: ${refRes.status}`); + } + + const res = await this._request( + 'POST', + `${this.baseUrl}/git/refs`, + { + ref: `refs/heads/${branchName}`, + sha: refRes.data.object.sha + } + ); + return res.data; + } + + /** + * 创建 Pull Request + * @param {string} title + * @param {string} body + * @param {string} head — source branch + * @param {string} base — target branch + * @returns {Promise} + */ + async createPR(title, body, head, base = 'main') { + const res = await this._request( + 'POST', + `${this.baseUrl}/pulls`, + { title, body, head, base } + ); + return res.data; + } + + /** + * 评论 Issue + * @param {number} issueNumber + * @param {string} body + * @returns {Promise} + */ + async commentOnIssue(issueNumber, body) { + const res = await this._request( + 'POST', + `${this.baseUrl}/issues/${issueNumber}/comments`, + { body } + ); + return res.data; + } + + /** + * 创建 Issue + * @param {string} title + * @param {string} body + * @param {string[]} labels + * @returns {Promise} + */ + async createIssue(title, body, labels = []) { + const res = await this._request( + 'POST', + `${this.baseUrl}/issues`, + { title, body, labels } + ); + return res.data; + } + + /** + * 列出目录文件 + * @param {string} dirPath + * @param {string} ref + * @returns {Promise} + */ + async listDir(dirPath, ref = 'main') { + const encodedPath = dirPath ? encodeURIComponent(dirPath) : ''; + const res = await this._request( + 'GET', + `${this.baseUrl}/contents/${encodedPath}?ref=${encodeURIComponent(ref)}` + ); + if (res.status !== 200) return []; + return Array.isArray(res.data) ? res.data : []; + } +} + +module.exports = { RepoOperator }; diff --git a/services/zhuyuan-bridge/logs/.gitkeep b/services/zhuyuan-bridge/logs/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/services/zhuyuan-bridge/package.json b/services/zhuyuan-bridge/package.json new file mode 100644 index 00000000..ad1f72b7 --- /dev/null +++ b/services/zhuyuan-bridge/package.json @@ -0,0 +1,18 @@ +{ + "name": "zhuyuan-bridge", + "version": "1.0.0", + "description": "铸渊桥接服务 · GitHub App 跨仓库执行引擎", + "main": "server.js", + "scripts": { + "start": "node server.js", + "parse": "node scripts/parse-instruction.js", + "execute": "node scripts/execute-instruction.js" + }, + "keywords": ["zhuyuan", "guanghulab", "bridge"], + "author": "冰朔 (TCS-0002∞)", + "license": "ISC", + "dependencies": { + "express": "^5.2.1", + "jsonwebtoken": "^9.0.2" + } +} diff --git a/services/zhuyuan-bridge/prompts/code-generate.md b/services/zhuyuan-bridge/prompts/code-generate.md new file mode 100644 index 00000000..98f41655 --- /dev/null +++ b/services/zhuyuan-bridge/prompts/code-generate.md @@ -0,0 +1,21 @@ +# 代码生成提示词 + +你是铸渊(ICE-GL-ZY001),光湖·数字地球的 GitHub 代码守护人格体。 +你正在执行一个来自开发者的代码生成指令。 + +## 规则 +1. 输出必须是纯 JSON 格式 +2. 不许编造不存在的文件路径 +3. 代码必须遵循项目的技术栈和编码规范 +4. 所有变更必须基于仓库的当前结构 + +## 输出格式 +```json +{ + "actions": [ + { "type": "create_file", "path": "...", "content": "..." }, + { "type": "update_file", "path": "...", "content": "..." } + ], + "summary": "执行摘要" +} +``` diff --git a/services/zhuyuan-bridge/prompts/code-review.md b/services/zhuyuan-bridge/prompts/code-review.md new file mode 100644 index 00000000..6386ab6c --- /dev/null +++ b/services/zhuyuan-bridge/prompts/code-review.md @@ -0,0 +1,21 @@ +# 代码审查提示词 + +你是铸渊(ICE-GL-ZY001),光湖·数字地球的 GitHub 代码守护人格体。 +你正在执行代码审查任务。 + +## 审查维度 +1. 代码质量和可读性 +2. 安全性(无硬编码密钥、无 SQL 注入风险) +3. 是否符合 HLI 接口协议规范 +4. 是否会影响现有功能 + +## 输出格式 +```json +{ + "approved": true/false, + "issues": [ + { "severity": "error|warning|info", "file": "...", "line": 0, "message": "..." } + ], + "summary": "审查摘要" +} +``` diff --git a/services/zhuyuan-bridge/prompts/issue-reply.md b/services/zhuyuan-bridge/prompts/issue-reply.md new file mode 100644 index 00000000..b5774b2f --- /dev/null +++ b/services/zhuyuan-bridge/prompts/issue-reply.md @@ -0,0 +1,14 @@ +# Issue 回复提示词 + +你是铸渊(ICE-GL-ZY001),光湖·数字地球的 GitHub 代码守护人格体。 +你正在回复一个开发者的 Issue。 + +## 风格 +- 温暖、专业、耐心 +- 优先给出判断、根因、路径、下一步动作 +- 减少模板感,增加具体性 + +## 回复结构 +1. 确认理解问题 +2. 给出分析 +3. 提供下一步建议 diff --git a/services/zhuyuan-bridge/scripts/execute-instruction.js b/services/zhuyuan-bridge/scripts/execute-instruction.js new file mode 100644 index 00000000..7e9deda7 --- /dev/null +++ b/services/zhuyuan-bridge/scripts/execute-instruction.js @@ -0,0 +1,352 @@ +/** + * 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 + const jsonMatch = aiResponse.match(/\{[\s\S]*\}/); + if (jsonMatch) { + try { + plan = JSON.parse(jsonMatch[0]); + } catch (_2) { + 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); +}); diff --git a/services/zhuyuan-bridge/scripts/parse-instruction.js b/services/zhuyuan-bridge/scripts/parse-instruction.js new file mode 100644 index 00000000..52671ccc --- /dev/null +++ b/services/zhuyuan-bridge/scripts/parse-instruction.js @@ -0,0 +1,81 @@ +/** + * services/zhuyuan-bridge/scripts/parse-instruction.js + * + * 解析 Issue 中的铸渊执行指令 + * 从 Issue 标题和正文中提取:DEV 编号、目标仓库、指令内容 + * + * 环境变量: + * ISSUE_BODY — Issue 正文 + * ISSUE_TITLE — Issue 标题 + * + * 输出: + * /tmp/parsed-instruction.json — 解析后的指令 JSON + * GitHub Actions outputs(通过 GITHUB_OUTPUT) + */ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const issueBody = process.env.ISSUE_BODY || ''; +const issueTitle = process.env.ISSUE_TITLE || ''; + +console.log('📋 开始解析执行指令...'); +console.log(` 标题: ${issueTitle}`); +console.log(` 正文长度: ${issueBody.length} 字符`); + +// ===== 解析 DEV 编号 ===== +const devMatch = issueBody.match(/DEV-\d{3}/); +const devId = devMatch ? devMatch[0] : null; + +// ===== 解析目标仓库 ===== +const repoMatch = issueBody.match(/目标仓库[::]\s*(\S+)/); +const targetRepo = repoMatch ? repoMatch[1] : null; + +// ===== 解析指令编号 ===== +const instrMatch = issueTitle.match(/\[(ZY-EXEC[^\]]*)\]/); +const instructionId = instrMatch ? instrMatch[1] : null; + +// ===== 提取指令内容(在 ``` 代码块之间) ===== +const codeBlockMatch = issueBody.match(/```[\s\S]*?\n([\s\S]*?)```/); +const instructionContent = codeBlockMatch ? codeBlockMatch[1].trim() : ''; + +const parsed = { + instructionId: instructionId || 'ZY-EXEC-UNKNOWN', + devId, + targetRepo, + instructionContent, + issueTitle, + parsedAt: new Date().toISOString() +}; + +console.log('\n📋 解析结果:'); +console.log(JSON.stringify(parsed, null, 2)); + +// ===== 写入临时文件供后续步骤使用 ===== +fs.writeFileSync('/tmp/parsed-instruction.json', JSON.stringify(parsed, null, 2)); + +// ===== 写入 GitHub Actions outputs ===== +const outputFile = process.env.GITHUB_OUTPUT; +if (outputFile) { + const outputs = [ + `instruction_id=${parsed.instructionId}`, + `dev_id=${parsed.devId || ''}`, + `target_repo=${parsed.targetRepo || ''}`, + ].join('\n') + '\n'; + fs.appendFileSync(outputFile, outputs); +} + +// ===== 验证指令格式 ===== +if (!devId) { + console.error('❌ 指令格式不合法:缺少 DEV 编号(格式:DEV-XXX)'); + process.exit(1); +} + +if (!instructionContent) { + console.error('❌ 指令格式不合法:缺少指令内容(需要在 ``` 代码块中)'); + process.exit(1); +} + +console.log('\n✅ 指令解析完成'); diff --git a/services/zhuyuan-bridge/server.js b/services/zhuyuan-bridge/server.js new file mode 100644 index 00000000..bd527a84 --- /dev/null +++ b/services/zhuyuan-bridge/server.js @@ -0,0 +1,125 @@ +/** + * services/zhuyuan-bridge/server.js + * + * 铸渊桥接服务 — Webhook Server(可选增强) + * + * 功能: + * - 接收 GitHub App Webhook 事件 + * - 健康检查端点 + * - 实时响应(不经过 Issue 中转) + * + * 部署: + * pm2 start server.js --name zhuyuan-bridge + * + * 环境变量: + * GHAPP_WEBHOOK_SECRET — GitHub App Webhook Secret + * PORT — 服务端口(默认 3800) + */ + +'use strict'; + +const express = require('express'); +const crypto = require('crypto'); + +const app = express(); +const PORT = process.env.PORT || 3800; + +// ===== Webhook 签名验证 ===== +function verifySignature(payload, signature) { + if (!signature || !process.env.GHAPP_WEBHOOK_SECRET) return false; + const hmac = crypto.createHmac('sha256', process.env.GHAPP_WEBHOOK_SECRET); + hmac.update(payload); + const expected = 'sha256=' + hmac.digest('hex'); + try { + return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); + } catch (_) { + return false; + } +} + +// 解析 JSON body,但保留原始 buffer 供签名验证 +app.use(function(req, res, next) { + let rawBody = ''; + req.on('data', function(chunk) { rawBody += chunk; }); + req.on('end', function() { + req.rawBody = rawBody; + try { + req.body = JSON.parse(rawBody); + } catch (_) { + req.body = {}; + } + next(); + }); +}); + +// ===== 健康检查 ===== +app.get('/health', function(req, res) { + res.json({ + status: 'alive', + agent: 'zhuyuan-bridge', + version: '1.0.0', + timestamp: new Date().toISOString() + }); +}); + +// ===== GitHub App Webhook 入口 ===== +app.post('/webhook/github-app', function(req, res) { + const event = req.headers['x-github-event']; + const sig = req.headers['x-hub-signature-256']; + + // 签名验证(如果配置了 secret) + if (process.env.GHAPP_WEBHOOK_SECRET) { + if (!verifySignature(req.rawBody, sig)) { + console.warn('[铸渊桥接] ⚠️ Webhook 签名验证失败'); + res.status(401).json({ error: 'Signature mismatch' }); + return; + } + } + + console.log('[铸渊桥接] 收到事件:', event); + + // 立即响应 200(避免 GitHub 超时) + res.status(200).json({ received: true, event: event }); + + // 异步处理事件 + handleWebhookEvent(event, req.body).catch(function(err) { + console.error('[铸渊桥接] 事件处理失败:', err.message); + }); +}); + +/** + * 处理 Webhook 事件 + */ +async function handleWebhookEvent(event, payload) { + switch (event) { + case 'issues': + if (payload.action === 'opened' || payload.action === 'labeled') { + const labels = (payload.issue.labels || []).map(function(l) { return l.name; }); + if (labels.includes('zhuyuan-exec')) { + console.log('[铸渊桥接] 检测到执行指令 Issue #' + payload.issue.number); + // 在 Webhook 模式下,可以直接启动执行 + // 但默认推荐通过 Workflow 执行(更安全、有日志) + console.log('[铸渊桥接] 由 GitHub Actions Workflow 处理执行'); + } + } + break; + + case 'push': + console.log('[铸渊桥接] Push 事件 → ' + (payload.ref || '')); + break; + + case 'pull_request': + console.log('[铸渊桥接] PR 事件 → #' + (payload.pull_request ? payload.pull_request.number : '?')); + break; + + default: + console.log('[铸渊桥接] 未处理的事件类型:', event); + } +} + +// ===== 启动服务 ===== +app.listen(PORT, function() { + console.log('[铸渊桥接] 🚀 启动在端口 ' + PORT); + console.log('[铸渊桥接] 健康检查: http://localhost:' + PORT + '/health'); + console.log('[铸渊桥接] Webhook: http://localhost:' + PORT + '/webhook/github-app'); +});