diff --git a/.github/workflows/zhuyuan-exec-engine.yml b/.github/workflows/zhuyuan-exec-engine.yml index 3cfebc09..fca2821c 100644 --- a/.github/workflows/zhuyuan-exec-engine.yml +++ b/.github/workflows/zhuyuan-exec-engine.yml @@ -52,7 +52,7 @@ jobs: 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") + STATUS=$(node -e "try{const d=require('./.github/notion-cache/skyeye/latest-report.json');console.log(d.overall_status||'unknown')}catch(_){console.log('unknown')}" 2>/dev/null || echo "unknown") echo "天眼缓存状态:$STATUS" if [ "$STATUS" = "critical" ] || [ "$STATUS" = "error" ]; then echo "❌ 天眼报告严重问题,暂停执行" diff --git a/services/zhuyuan-bridge/lib/github-auth.js b/services/zhuyuan-bridge/lib/github-auth.js index 49d3ce53..49760908 100644 --- a/services/zhuyuan-bridge/lib/github-auth.js +++ b/services/zhuyuan-bridge/lib/github-auth.js @@ -15,9 +15,13 @@ const https = require('https'); +// 模块级检查:jsonwebtoken 是否可用 +let jwtLib = null; +try { jwtLib = require('jsonwebtoken'); } catch (_) { /* fallback to crypto */ } + /** - * 用 App Private Key 生成 JWT(不依赖 jsonwebtoken 库的 fallback 实现) - * 在 CI 环境中可能没有安装 jsonwebtoken,因此提供纯 Node.js 实现 + * 用 App Private Key 生成 JWT + * 优先使用 jsonwebtoken 库,不可用时降级到 Node.js crypto 实现 */ function generateJWT() { const privateKey = process.env.GHAPP_PRIVATE_KEY; @@ -27,42 +31,40 @@ function generateJWT() { throw new Error('GHAPP_APP_ID 和 GHAPP_PRIVATE_KEY 环境变量必须设置'); } - // 尝试使用 jsonwebtoken 库 - try { - const jwt = require('jsonwebtoken'); + if (jwtLib) { 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; + return jwtLib.sign(payload, privateKey, { algorithm: 'RS256' }); } + + // 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; } /** diff --git a/services/zhuyuan-bridge/scripts/execute-instruction.js b/services/zhuyuan-bridge/scripts/execute-instruction.js index 7e9deda7..13e018b3 100644 --- a/services/zhuyuan-bridge/scripts/execute-instruction.js +++ b/services/zhuyuan-bridge/scripts/execute-instruction.js @@ -215,13 +215,17 @@ async function main() { try { plan = JSON.parse(aiResponse); } catch (_) { - // 尝试从回复中提取 JSON - const jsonMatch = aiResponse.match(/\{[\s\S]*\}/); + // 尝试从回复中提取 JSON(非贪婪匹配,找第一个完整 JSON 对象) + const jsonMatch = aiResponse.match(/\{[\s\S]*?"actions"[\s\S]*?\}(?=\s*$|\s*\n)/); if (jsonMatch) { try { plan = JSON.parse(jsonMatch[0]); } catch (_2) { - plan = null; + // 降级:找所有 { } 对中最长的合法 JSON + const allMatches = aiResponse.match(/\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/g) || []; + for (const m of allMatches) { + try { plan = JSON.parse(m); if (plan.actions) break; } catch (_3) { plan = null; } + } } } }