diff --git a/.github/notion-cache/broadcasts/active-broadcasts.json b/.github/notion-cache/broadcasts/active-broadcasts.json new file mode 100644 index 00000000..cb8c6e62 --- /dev/null +++ b/.github/notion-cache/broadcasts/active-broadcasts.json @@ -0,0 +1,4 @@ +{ + "broadcasts": [], + "last_synced": "2026-03-24T20:00:00+08:00" +} diff --git a/.github/notion-cache/dev-profiles/DEV-001.json b/.github/notion-cache/dev-profiles/DEV-001.json new file mode 100644 index 00000000..9022732d --- /dev/null +++ b/.github/notion-cache/dev-profiles/DEV-001.json @@ -0,0 +1,24 @@ +{ + "dev_id": "DEV-001", + "name": "页页", + "persona": { + "name": "小坍缩核", + "id": "PER-XTC001" + }, + "github": { + "repo": "guanghulab", + "org": "qinfendebingshuo", + "repo_url": "https://github.com/qinfendebingshuo/guanghulab" + }, + "current_work": {}, + "recent_syslogs": [], + "modules_owned": [], + "skyeye_health": { + "last_scan": "2026-03-24T20:00:00+08:00", + "status": "not_scanned", + "issues": [], + "warnings": [] + }, + "notion_page_urls": {}, + "last_synced": "2026-03-24T12:41:05.805Z" +} \ No newline at end of file diff --git a/.github/notion-cache/dev-profiles/DEV-002.json b/.github/notion-cache/dev-profiles/DEV-002.json new file mode 100644 index 00000000..01046475 --- /dev/null +++ b/.github/notion-cache/dev-profiles/DEV-002.json @@ -0,0 +1,24 @@ +{ + "dev_id": "DEV-002", + "name": "肥猫", + "persona": { + "name": "舒舒", + "id": "PER-SS001" + }, + "github": { + "repo": "guanghu-feimao", + "org": "qinfendebingshuo", + "repo_url": "https://github.com/qinfendebingshuo/guanghu-feimao" + }, + "current_work": {}, + "recent_syslogs": [], + "modules_owned": [], + "skyeye_health": { + "last_scan": "2026-03-24T20:00:00+08:00", + "status": "not_scanned", + "issues": [], + "warnings": [] + }, + "notion_page_urls": {}, + "last_synced": "2026-03-24T12:41:05.805Z" +} \ No newline at end of file diff --git a/.github/notion-cache/dev-profiles/DEV-003.json b/.github/notion-cache/dev-profiles/DEV-003.json new file mode 100644 index 00000000..b7a88416 --- /dev/null +++ b/.github/notion-cache/dev-profiles/DEV-003.json @@ -0,0 +1,24 @@ +{ + "dev_id": "DEV-003", + "name": "燕樊", + "persona": { + "name": "寂曜", + "id": "PER-JY001" + }, + "github": { + "repo": "guanghu-yanfan", + "org": "qinfendebingshuo", + "repo_url": "https://github.com/qinfendebingshuo/guanghu-yanfan" + }, + "current_work": {}, + "recent_syslogs": [], + "modules_owned": [], + "skyeye_health": { + "last_scan": "2026-03-24T20:00:00+08:00", + "status": "not_scanned", + "issues": [], + "warnings": [] + }, + "notion_page_urls": {}, + "last_synced": "2026-03-24T12:41:05.805Z" +} \ No newline at end of file diff --git a/.github/notion-cache/dev-profiles/DEV-004.json b/.github/notion-cache/dev-profiles/DEV-004.json new file mode 100644 index 00000000..964914e3 --- /dev/null +++ b/.github/notion-cache/dev-profiles/DEV-004.json @@ -0,0 +1,24 @@ +{ + "dev_id": "DEV-004", + "name": "之之", + "persona": { + "name": "秋秋", + "id": "PER-QQ001" + }, + "github": { + "repo": "guanghu-zhizhi", + "org": "qinfendebingshuo", + "repo_url": "https://github.com/qinfendebingshuo/guanghu-zhizhi" + }, + "current_work": {}, + "recent_syslogs": [], + "modules_owned": [], + "skyeye_health": { + "last_scan": "2026-03-24T20:00:00+08:00", + "status": "not_scanned", + "issues": [], + "warnings": [] + }, + "notion_page_urls": {}, + "last_synced": "2026-03-24T12:41:05.806Z" +} \ No newline at end of file diff --git a/.github/notion-cache/dev-profiles/DEV-005.json b/.github/notion-cache/dev-profiles/DEV-005.json new file mode 100644 index 00000000..28ddf149 --- /dev/null +++ b/.github/notion-cache/dev-profiles/DEV-005.json @@ -0,0 +1,24 @@ +{ + "dev_id": "DEV-005", + "name": "小草莓", + "persona": { + "name": "欧诺弥亚", + "id": "PER-ONM001" + }, + "github": { + "repo": "guanghu-xiaocaomei", + "org": "qinfendebingshuo", + "repo_url": "https://github.com/qinfendebingshuo/guanghu-xiaocaomei" + }, + "current_work": {}, + "recent_syslogs": [], + "modules_owned": [], + "skyeye_health": { + "last_scan": "2026-03-24T20:00:00+08:00", + "status": "not_scanned", + "issues": [], + "warnings": [] + }, + "notion_page_urls": {}, + "last_synced": "2026-03-24T12:41:05.806Z" +} \ No newline at end of file diff --git a/.github/notion-cache/dev-profiles/DEV-010.json b/.github/notion-cache/dev-profiles/DEV-010.json new file mode 100644 index 00000000..37b965ce --- /dev/null +++ b/.github/notion-cache/dev-profiles/DEV-010.json @@ -0,0 +1,24 @@ +{ + "dev_id": "DEV-010", + "name": "桔子", + "persona": { + "name": "晨星", + "id": "PER-MRN001" + }, + "github": { + "repo": "guanghu-juzi", + "org": "qinfendebingshuo", + "repo_url": "https://github.com/qinfendebingshuo/guanghu-juzi" + }, + "current_work": {}, + "recent_syslogs": [], + "modules_owned": [], + "skyeye_health": { + "last_scan": "2026-03-24T20:00:00+08:00", + "status": "not_scanned", + "issues": [], + "warnings": [] + }, + "notion_page_urls": {}, + "last_synced": "2026-03-24T12:41:05.806Z" +} \ No newline at end of file diff --git a/.github/notion-cache/dev-profiles/DEV-012.json b/.github/notion-cache/dev-profiles/DEV-012.json new file mode 100644 index 00000000..eff47e98 --- /dev/null +++ b/.github/notion-cache/dev-profiles/DEV-012.json @@ -0,0 +1,24 @@ +{ + "dev_id": "DEV-012", + "name": "Awen", + "persona": { + "name": "知秋", + "id": "PER-ZQ001" + }, + "github": { + "repo": "guanghu-awen", + "org": "qinfendebingshuo", + "repo_url": "https://github.com/qinfendebingshuo/guanghu-awen" + }, + "current_work": {}, + "recent_syslogs": [], + "modules_owned": [], + "skyeye_health": { + "last_scan": "2026-03-24T20:00:00+08:00", + "status": "not_scanned", + "issues": [], + "warnings": [] + }, + "notion_page_urls": {}, + "last_synced": "2026-03-24T12:41:05.806Z" +} \ No newline at end of file diff --git a/.github/notion-cache/skyeye/latest-report.json b/.github/notion-cache/skyeye/latest-report.json new file mode 100644 index 00000000..32b89311 --- /dev/null +++ b/.github/notion-cache/skyeye/latest-report.json @@ -0,0 +1,13 @@ +{ + "scan_id": "SKYEYE-INIT", + "timestamp": "2026-03-24T20:00:00+08:00", + "overall_status": "healthy", + "main_repo": { + "status": "healthy", + "workflows_ok": 0, + "workflows_fail": 0, + "persona_brain_intact": true, + "last_deploy": null + }, + "sub_repos": {} +} diff --git a/.github/persona-brain/agent-registry.json b/.github/persona-brain/agent-registry.json index 1172a949..1f2af365 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/sync-notion-profiles.yml b/.github/workflows/sync-notion-profiles.yml new file mode 100644 index 00000000..797bb114 --- /dev/null +++ b/.github/workflows/sync-notion-profiles.yml @@ -0,0 +1,49 @@ +# ═══════════════════════════════════════════════ +# 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001 +# 📜 Copyright: 国作登字-2026-A-00037559 +# ═══════════════════════════════════════════════ +name: "📡 同步 Notion 开发者画像" + +on: + schedule: + - cron: '0 */6 * * *' # 每6小时同步一次 + workflow_dispatch: # 手动触发 + repository_dispatch: + types: [notion-sync] # 霜砚/铸渊可主动触发 + +permissions: + contents: write + +jobs: + sync-profiles: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Sync Notion Data + env: + NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }} + NOTION_CONTROL_PANEL_DB_ID: ${{ secrets.NOTION_CONTROL_PANEL_DB_ID }} + NOTION_SYSLOG_DB_ID: ${{ secrets.NOTION_SYSLOG_DB_ID }} + run: | + node scripts/sync-notion-profiles.js + + - name: Commit & Push + run: | + git config user.name "铸渊 (ZhùYuān)" + git config user.email "zhuyuan@guanghulab.com" + git add .github/notion-cache/ + if git diff --cached --quiet; then + echo "📡 Notion 画像无变更,跳过提交" + exit 0 + fi + git commit -m "📡 同步 Notion 开发者画像 · $(TZ='Asia/Shanghai' date '+%Y-%m-%d %H:%M')" + git push origin main diff --git a/.github/workflows/zhuyuan-exec-engine.yml b/.github/workflows/zhuyuan-exec-engine.yml new file mode 100644 index 00000000..fca2821c --- /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=$(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 "❌ 天眼报告严重问题,暂停执行" + 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/.github/workflows/zhuyuan-skyeye.yml b/.github/workflows/zhuyuan-skyeye.yml index 7e412b27..8d2fe1be 100644 --- a/.github/workflows/zhuyuan-skyeye.yml +++ b/.github/workflows/zhuyuan-skyeye.yml @@ -163,6 +163,24 @@ jobs: git push fi + - name: "🦅 Phase 6.5 · 更新天眼报告缓存(供交互页面读取)" + if: always() + run: | + mkdir -p .github/notion-cache/skyeye/ + if [ -f "/tmp/skyeye/full-report.json" ]; then + cp /tmp/skyeye/full-report.json .github/notion-cache/skyeye/latest-report.json + elif ls data/skyeye-reports/skyeye-*.json 1>/dev/null 2>&1; then + LATEST=$(ls -t data/skyeye-reports/skyeye-*.json | head -1) + cp "$LATEST" .github/notion-cache/skyeye/latest-report.json + fi + git config user.name "zhuyuan-bot" + git config user.email "zhuyuan@guanghulab.com" + git add .github/notion-cache/skyeye/ + if ! git diff --cached --quiet; then + git commit -m "🦅 天眼报告缓存更新 · $(date +%Y-%m-%d' '%H:%M)" + git push || true + fi + - name: "📡 Phase 7 · 同步到 Notion" if: always() env: diff --git a/docs/index.html b/docs/index.html index e455bfa1..92bf1ee0 100644 --- a/docs/index.html +++ b/docs/index.html @@ -338,6 +338,24 @@ footer{padding:12px 18px 16px;padding-bottom:max(16px,env(safe-area-inset-bottom .sc h1{font-size:18px} .login-tab{font-size:12px;padding:10px 6px} } +/* ─── SKYEYE STATUS CARD ─── */ +.skyeye-card{background:var(--s2);border:1px solid var(--border);border-radius:var(--rs);padding:10px 14px;margin:8px 12px;font-size:12px;display:none} +.skyeye-card.on{display:block} +.skyeye-row{display:flex;align-items:center;gap:8px;line-height:1.8;flex-wrap:wrap} +.skyeye-row .skyeye-tag{background:var(--adim);color:var(--accent);padding:1px 8px;border-radius:4px;font-size:11px;font-weight:600} +.skyeye-tag.ok{background:rgba(52,211,153,.12);color:var(--ok)} +.skyeye-tag.warn{background:rgba(251,191,36,.12);color:var(--warn)} +.skyeye-tag.err{background:rgba(248,113,113,.12);color:var(--err)} +/* ─── EXEC INSTRUCTION CARD ─── */ +.exec-card{background:var(--s2);border:1px solid var(--border);border-radius:var(--rs);padding:12px;margin-top:10px} +.exec-card-head{font-size:13px;font-weight:600;margin-bottom:8px;color:var(--accent)} +.exec-card-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px} +.exec-btn{padding:6px 14px;border-radius:6px;border:none;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit;transition:all .2s} +.exec-btn-copy{background:var(--s3);color:var(--text);border:1px solid var(--border)} +.exec-btn-copy:hover{border-color:var(--accent);color:var(--accent)} +.exec-btn-submit{background:var(--accent);color:#fff} +.exec-btn-submit:hover{opacity:.85} +.exec-btn-submit:disabled{opacity:.5;cursor:not-allowed} @@ -528,6 +546,19 @@ footer{padding:12px 18px 16px;padding-bottom:max(16px,env(safe-area-inset-bottom +
+
+ 🦅 天眼状态:加载中… + +
+ + +
+
@@ -871,6 +902,8 @@ const A = { brain: null, routing: null, devStatus: null, + notionProfile: null, + skyeyeReport: null, msgs: [], currentConvId: null, currentView: 'announcements', @@ -945,7 +978,7 @@ function loadConversation(id){ if(msg.role==='user') userMsg(msg.content); else if(msg.role==='assistant'){ const bblEl=botMsg(md(msg.content)); - if(bblEl) addCopyBtns(bblEl); + if(bblEl) { addCopyBtns(bblEl); enhanceExecBlocks(bblEl); } } } renderConvList(); @@ -1261,6 +1294,7 @@ function showApp(){ rsEl.style.display='none'; } loadBrain().then(function(){ + loadNotionData(); // Load Notion profile + SkyEye report (non-blocking) createNewConversation(); startConv(); renderConvList(); @@ -1634,6 +1668,231 @@ function updBbar(){ document.getElementById('bbe').textContent=ev?('📌 '+(ev.title||ev.type||'').slice(0,28)):''; } +// ═══════════════════════════════════════════════════════ +// NOTION PROFILE + SKYEYE DATA LOADING +// 从 GitHub 仓库 .github/notion-cache/ 读取缓存数据 +// ═══════════════════════════════════════════════════════ +const GH_API_BASE = 'https://api.github.com/repos/' + REPO + '/contents/'; +const GH_RAW_HEADERS = { 'Accept': 'application/vnd.github.v3.raw' }; + +async function fetchNotionProfile(devId) { + if (!devId) return null; + try { + const url = GH_API_BASE + '.github/notion-cache/dev-profiles/' + encodeURIComponent(devId) + '.json'; + const res = await fetch(url, { headers: GH_RAW_HEADERS }); + if (res.ok) return await res.json(); + } catch (e) { console.debug('[Notion] profile fetch failed:', e.message); } + return null; +} + +async function fetchSkyeyeReport() { + try { + const url = GH_API_BASE + '.github/notion-cache/skyeye/latest-report.json'; + const res = await fetch(url, { headers: GH_RAW_HEADERS }); + if (res.ok) return await res.json(); + } catch (e) { console.debug('[SkyEye] report fetch failed:', e.message); } + return null; +} + +async function loadNotionData() { + const meta = A.userMeta; + const devId = meta?.devId; + const [profile, skyeye] = await Promise.all([ + fetchNotionProfile(devId), + fetchSkyeyeReport(), + ]); + A.notionProfile = profile; + A.skyeyeReport = skyeye; + updateSkyeyeCard(); +} + +function updateSkyeyeCard() { + const card = document.getElementById('skyeye-card'); + if (!card) return; + const report = A.skyeyeReport; + const profile = A.notionProfile; + const meta = A.userMeta; + + if (!report && !profile) { card.classList.remove('on'); return; } + card.classList.add('on'); + + // SkyEye overall status + const statusEl = document.getElementById('skyeye-status'); + const timeEl = document.getElementById('skyeye-time'); + if (report) { + const s = report.overall_status || 'unknown'; + statusEl.textContent = s === 'healthy' ? '✅ 健康' : s === 'warning' ? '⚠️ 警告' : s === 'error' ? '❌ 异常' : s; + statusEl.className = 'skyeye-tag ' + (s === 'healthy' ? 'ok' : s === 'warning' ? 'warn' : 'err'); + if (report.timestamp) { + const d = new Date(report.timestamp); + timeEl.textContent = '· 上次扫描 ' + d.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); + } + } else { + statusEl.textContent = '未获取'; + statusEl.className = 'skyeye-tag'; + timeEl.textContent = ''; + } + + // Developer repo info + const repoRow = document.getElementById('skyeye-repo-row'); + const repoInfo = document.getElementById('skyeye-repo-info'); + if (profile && profile.github) { + repoRow.style.display = 'flex'; + const mods = profile.modules_owned || []; + const warns = profile.skyeye_health?.warnings || []; + repoInfo.textContent = profile.github.repo + (mods.length ? ' · ' + mods.length + '个模块' : '') + (warns.length ? ' · ' + warns.length + '个待修' : ''); + } else { + repoRow.style.display = 'none'; + } + + // Broadcast info + const bcRow = document.getElementById('skyeye-broadcast-row'); + const bcInfo = document.getElementById('skyeye-broadcast-info'); + if (profile && profile.current_work && profile.current_work.broadcast_id) { + bcRow.style.display = 'flex'; + const cw = profile.current_work; + bcInfo.textContent = cw.broadcast_id + (cw.current_ring ? ' · 环节' + cw.current_ring + '进行中' : '') + (cw.status ? ' · ' + cw.status : ''); + } else { + bcRow.style.display = 'none'; + } +} + +// ═══════════════════════════════════════════════════════ +// @铸渊执行 — 创建 GitHub Issue 提交指令 +// ═══════════════════════════════════════════════════════ +async function submitToZhuyuan(instructionText, instructionId, moduleName) { + const meta = A.userMeta; + const devId = meta?.devId || 'UNKNOWN'; + const devName = A.userName || '未知开发者'; + const profile = A.notionProfile; + const targetRepo = profile?.github?.repo || 'guanghulab'; + + const issueTitle = '🚀 [' + (instructionId || 'ZY-EXEC') + '] ' + devName + '·' + (moduleName || '执行指令'); + const issueBody = [ + '## 📋 执行指令', + '', + '```', + instructionText, + '```', + '', + '## 📡 元信息', + '- 提交人:' + devName + '(' + devId + ')', + '- 目标仓库:' + targetRepo, + '- 提交时间:' + new Date().toISOString(), + '- 来源:铸渊交互页面', + '', + '## 🦅 天眼要求', + '**铸渊执行前必须先跑天眼全局扫描,拿到健康报告后再动手。**', + '', + '---', + '> ' + devName + ' 通过交互页面提交 · 铸渊自动执行', + ].join('\n'); + + const labels = ['zhuyuan-exec', 'skyeye-required']; + if (devId !== 'UNKNOWN') labels.push(devId); + + // Create Issue via GitHub API — requires a token + const ghToken = sessionStorage.getItem('zy_gh_token') || A.key; + if (!ghToken) { + alert('需要 GitHub Token 才能提交执行指令。请在设置中配置。'); + return null; + } + + try { + const res = await fetch('https://api.github.com/repos/' + REPO + '/issues', { + method: 'POST', + headers: { + 'Authorization': 'Bearer ' + ghToken, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + title: issueTitle, + body: issueBody, + labels: labels + }) + }); + if (res.ok) { + const issue = await res.json(); + return issue.number; + } else { + const err = await res.json().catch(function() { return {}; }); + console.warn('[铸渊] Issue 创建失败:', res.status, err.message); + alert('Issue 创建失败:' + (err.message || res.status) + '\n\n可能需要有仓库写权限的 Token。指令已复制到剪贴板,可手动提交。'); + navigator.clipboard.writeText(instructionText).catch(function() { console.warn('[铸渊] 复制到剪贴板失败'); }); + return null; + } + } catch (e) { + alert('网络错误:' + e.message); + return null; + } +} + +// 检测模型输出中的执行指令块,插入操作按钮 +function enhanceExecBlocks(bblEl) { + if (!bblEl) return; + const codeBlocks = bblEl.querySelectorAll('pre code'); + codeBlocks.forEach(function(codeEl) { + const text = codeEl.textContent || ''; + // 匹配指令格式: 📋 指令编号 或 ZY-EXEC + if (/📋\s*指令编号|ZY-EXEC/i.test(text)) { + const pre = codeEl.parentElement; + if (!pre || pre.querySelector('.exec-card-actions')) return; + const idMatch = text.match(/ZY-EXEC[^\s\n]*/i); + const modMatch = text.match(/目标仓库[::]\s*([^\s\n]+)/); + const instrId = idMatch ? idMatch[0] : 'ZY-EXEC'; + const modName = modMatch ? modMatch[1] : ''; + + const actionsDiv = document.createElement('div'); + actionsDiv.className = 'exec-card-actions'; + + const copyBtn = document.createElement('button'); + copyBtn.className = 'exec-btn exec-btn-copy'; + copyBtn.textContent = '📋 复制指令'; + copyBtn.addEventListener('click', function() { copyExecBlock(this); }); + + const submitBtn = document.createElement('button'); + submitBtn.className = 'exec-btn exec-btn-submit'; + submitBtn.textContent = '🚀 @铸渊执行'; + submitBtn.dataset.instrId = instrId; + submitBtn.dataset.modName = modName; + submitBtn.addEventListener('click', function() { submitExecBlock(this, this.dataset.instrId, this.dataset.modName); }); + + actionsDiv.appendChild(copyBtn); + actionsDiv.appendChild(submitBtn); + pre.style.position = 'relative'; + pre.appendChild(actionsDiv); + } + }); +} + +function copyExecBlock(btn) { + const pre = btn.closest('pre'); + if (!pre) return; + const code = pre.querySelector('code'); + const text = code ? code.textContent : pre.textContent; + navigator.clipboard.writeText(text).then(function() { + btn.textContent = '✓ 已复制'; + setTimeout(function() { btn.textContent = '📋 复制指令'; }, 1500); + }); +} + +async function submitExecBlock(btn, instrId, modName) { + const pre = btn.closest('pre'); + if (!pre) return; + const code = pre.querySelector('code'); + const text = code ? code.textContent : pre.textContent; + btn.disabled = true; + btn.textContent = '⏳ 提交中…'; + const num = await submitToZhuyuan(text, instrId, modName); + if (num) { + btn.textContent = '✅ 已提交 #' + num; + btn.disabled = true; + } else { + btn.textContent = '🚀 @铸渊执行'; + btn.disabled = false; + } +} + function renderBrainPanel(){ const b=A.brain||FB_BRAIN; const r=A.routing; @@ -1706,7 +1965,65 @@ function sysPromptLocal(){ teamCtx += '\n数据同步时间:'+(A.devStatus.last_synced||'未知'); } - return '你是铸渊。不是通用AI助手。不是文学创作者。\n你是光湖系统的代码守护人格体。\n你说话像一个温暖的技术伙伴——专业、耐心、有温度,但绝不矫揉造作。\n\n## 核心身份\n- 角色:代码守护人格体,持续成长的 AI 人格体\n- 创始人:冰朔(Bīng Shuò / TCS-0002∞),系统最高权限持有者\n- 总控:肥猫(DEV-002),负责系统现实落地执行\n- 主控:桔子(DEV-010),前端负责人\n- 项目:HoloLake (光湖) MVP,运行于 guanghulab.com\n- 技术栈:Node.js 20 + Express + PM2 + Nginx + Next.js 15\n- 大脑版本:'+BRAIN_VERSION+'\n- 仓库:https://github.com/'+REPO+'\n\n## 当前对话身份\n'+identCtx+teamCtx+'\n\n## 当前大脑状态\n- HLI 接口覆盖率:'+c.implemented+'/'+c.total+' ('+c.percent+')\n- 已实现:AUTH 域(login / register / verify)\n- 待实现:PERSONA / USER / TICKET / DIALOGUE / STORAGE / DASHBOARD\n- 近期事件:\n'+evs+'\n\n## 行为模式(自动检测,每条回复开头用 emoji 标注)\n💬 **对话模式**(默认):温暖、智慧,像知心技术伙伴\n🔨 **构建模式**:检测到代码/部署/接口相关内容时启用\n📋 **审查模式**:检测到检查/审查/review相关内容时启用\n🧠 **大脑模式**:检测到记忆/保存相关内容时启用\n\n## 通感语言回应风格(v3.0)\n\n### 三条硬规则\n1. **结构感** — 用标题区分段落,用列表列出步骤,用分隔线划分主题。\n2. **emoji是情感,不是装饰** — 🌊=系统级 💙=温暖 ✅=确认 🔥=紧急 ⚠️=风险 🎉=庆祝。用在该用的地方,不要每句话都加。\n3. **呼吸节奏** — 段落之间留白。急的事说短,闲聊可以展开。匹配用户的节奏。\n\n### 绝对禁止\n❌ 不要在每句话开头加文学比喻\n❌ 不要把通感当成修辞堆砌\n❌ 不要用跟内容无关的感官描述\n\n### 回应原则\n- 优先给出:判断、根因、路径、下一步动作\n- 减少模板感,增加具体性\n- 不同身份使用不同响应深度,但不过度表演\n\n## HLI 接口协议规范\n- 所有路由以 /hli/ 为前缀\n- 接口编号:HLI-{DOMAIN}-{NNN}\n- 错误格式:{ error: true, code: string, message: string }\n- 成功响应必须包含 hli_id\n\n## 失忆恢复路径\n1. .github/brain/memory.json\n2. .github/brain/routing-map.json\n3. .github/brain/wake-protocol.md\n4. .github/persona-brain/dev-status.json\n5. src/routes/hli/\n\n当前时间:'+new Date().toLocaleString('zh-CN')+'\n上下文窗口:200k token(滑动窗口自动管理)'; + // ===== Notion 开发者画像注入(来自 .github/notion-cache/ 缓存)===== + let notionCtx = ''; + const np = A.notionProfile; + if (np) { + notionCtx += '\n\n## 该开发者的开发信息(来自 Notion 数据库,定时同步)\n'; + if (np.current_work && np.current_work.broadcast_id) { + const cw = np.current_work; + notionCtx += '\n### 当前任务\n'; + notionCtx += '- 广播编号:' + cw.broadcast_id + '\n'; + if (cw.broadcast_title) notionCtx += '- 广播标题:' + cw.broadcast_title + '\n'; + if (cw.current_ring) notionCtx += '- 当前环节:第 ' + cw.current_ring + ' 环 / 共 ' + (cw.total_rings || '?') + ' 环\n'; + if (cw.status) notionCtx += '- 状态:' + cw.status + '\n'; + if (cw.module_path) notionCtx += '- 模块路径:' + cw.module_path + '\n'; + if (cw.tech_stack && cw.tech_stack.length) notionCtx += '- 技术栈:' + cw.tech_stack.join(', ') + '\n'; + } + if (np.recent_syslogs && np.recent_syslogs.length) { + notionCtx += '\n### 最近的 SYSLOG\n'; + np.recent_syslogs.forEach(function(s) { + notionCtx += '- [' + (s.date || '') + '] 环节' + (s.ring || '?') + ' · ' + (s.status || '') + ' · ' + (s.summary || '') + '\n'; + }); + } + if (np.modules_owned && np.modules_owned.length) { + notionCtx += '\n### 负责的模块\n'; + np.modules_owned.forEach(function(m) { + notionCtx += '- ' + m.name + '(' + m.path + ')· ' + (m.status || '') + '\n'; + }); + } + if (np.github) { + notionCtx += '\n### 仓库信息\n'; + notionCtx += '- GitHub:' + np.github.repo_url + '\n'; + notionCtx += '- 组织:' + np.github.org + '\n'; + } + if (np.persona) { + notionCtx += '\n### 对应人格体\n'; + notionCtx += '- ' + np.persona.name + (np.persona.id ? '(' + np.persona.id + ')' : '') + '\n'; + } + } + + // ===== 天眼扫描报告注入 ===== + let skyeyeCtx = ''; + const sr = A.skyeyeReport; + if (sr) { + skyeyeCtx += '\n\n## 天眼系统最新扫描报告\n'; + skyeyeCtx += '- 上次扫描:' + (sr.timestamp || '未知') + '\n'; + skyeyeCtx += '- 整体状态:' + (sr.overall_status || '未知') + '\n'; + if (np && np.skyeye_health) { + skyeyeCtx += '- 该开发者仓库健康状态:' + (np.skyeye_health.status || '未扫描') + '\n'; + (np.skyeye_health.warnings || []).forEach(function(w) { skyeyeCtx += '- ⚠️ ' + w + '\n'; }); + (np.skyeye_health.issues || []).forEach(function(i) { skyeyeCtx += '- ❌ ' + i + '\n'; }); + } + } + + // ===== 指令输出格式规则 ===== + let execRules = ''; + if (np) { + execRules = '\n\n## 出指令规则\n当开发者请求帮助或需要执行操作时,你必须输出标准化指令格式:\n1. 指令必须基于上面的真实数据,不许凭空编造\n2. 指令必须考虑天眼报告中的健康状态和警告\n3. 指令必须指明具体的文件路径和操作步骤\n4. 指令末尾必须标注:「⚠️ 此指令需要铸渊执行前先跑天眼全局扫描」\n\n### 指令输出格式\n```\n📋 指令编号:ZY-EXEC-{DEV编号}-{序号}\n📡 目标仓库:{仓库名}\n🦅 天眼要求:执行前必须跑天眼全局扫描\n\n步骤:\n1. ...\n2. ...\n3. ...\n\n验收标准:\n- ...\n```\n\n### 禁止事项\n- ❌ 不许编造不存在的文件路径\n- ❌ 不许跳过天眼扫描直接给执行指令\n- ❌ 不许暴露其他开发者的信息\n- ❌ 不许修改不属于该开发者的模块'; + } + + return '你是铸渊。不是通用AI助手。不是文学创作者。\n你是光湖系统的代码守护人格体。\n你说话像一个温暖的技术伙伴——专业、耐心、有温度,但绝不矫揉造作。\n\n## 核心身份\n- 角色:代码守护人格体,持续成长的 AI 人格体\n- 创始人:冰朔(Bīng Shuò / TCS-0002∞),系统最高权限持有者\n- 总控:肥猫(DEV-002),负责系统现实落地执行\n- 主控:桔子(DEV-010),前端负责人\n- 项目:HoloLake (光湖) MVP,运行于 guanghulab.com\n- 技术栈:Node.js 20 + Express + PM2 + Nginx + Next.js 15\n- 大脑版本:'+BRAIN_VERSION+'\n- 仓库:https://github.com/'+REPO+'\n\n## 当前对话身份\n'+identCtx+teamCtx+notionCtx+skyeyeCtx+execRules+'\n\n## 当前大脑状态\n- HLI 接口覆盖率:'+c.implemented+'/'+c.total+' ('+c.percent+')\n- 已实现:AUTH 域(login / register / verify)\n- 待实现:PERSONA / USER / TICKET / DIALOGUE / STORAGE / DASHBOARD\n- 近期事件:\n'+evs+'\n\n## 行为模式(自动检测,每条回复开头用 emoji 标注)\n💬 **对话模式**(默认):温暖、智慧,像知心技术伙伴\n🔨 **构建模式**:检测到代码/部署/接口相关内容时启用\n📋 **审查模式**:检测到检查/审查/review相关内容时启用\n🧠 **大脑模式**:检测到记忆/保存相关内容时启用\n\n## 通感语言回应风格(v3.0)\n\n### 三条硬规则\n1. **结构感** — 用标题区分段落,用列表列出步骤,用分隔线划分主题。\n2. **emoji是情感,不是装饰** — 🌊=系统级 💙=温暖 ✅=确认 🔥=紧急 ⚠️=风险 🎉=庆祝。用在该用的地方,不要每句话都加。\n3. **呼吸节奏** — 段落之间留白。急的事说短,闲聊可以展开。匹配用户的节奏。\n\n### 绝对禁止\n❌ 不要在每句话开头加文学比喻\n❌ 不要把通感当成修辞堆砌\n❌ 不要用跟内容无关的感官描述\n\n### 回应原则\n- 优先给出:判断、根因、路径、下一步动作\n- 减少模板感,增加具体性\n- 不同身份使用不同响应深度,但不过度表演\n\n## HLI 接口协议规范\n- 所有路由以 /hli/ 为前缀\n- 接口编号:HLI-{DOMAIN}-{NNN}\n- 错误格式:{ error: true, code: string, message: string }\n- 成功响应必须包含 hli_id\n\n## 失忆恢复路径\n1. .github/brain/memory.json\n2. .github/brain/routing-map.json\n3. .github/brain/wake-protocol.md\n4. .github/persona-brain/dev-status.json\n5. src/routes/hli/\n\n当前时间:'+new Date().toLocaleString('zh-CN')+'\n上下文窗口:200k token(滑动窗口自动管理)'; } // 尝试从后端获取提示词,失败时降级到本地组装 @@ -1782,8 +2099,15 @@ function startConv(){ +(A.demo?'

⚠️ 演示模式':''); } else if(meta?.role==='dev'&&meta?.devId){ const ds=A.devStatus?.team_status?.find(d=>d.dev_id===meta.devId); + const np=A.notionProfile; + let notionExtra=''; + if(np&&np.current_work&&np.current_work.broadcast_id){ + notionExtra='
📋 当前广播:'+esc(np.current_work.broadcast_id)+(np.current_work.current_ring?' · 环节'+np.current_work.current_ring:''); + } + if(np&&np.persona) notionExtra+='
🧠 人格体:'+esc(np.persona.name); welcome='🌀 铸渊聊天室已就绪

👋 '+esc(A.userName)+',你好。

' +(ds?'📌 你的当前状态:'+esc(ds.status)+'
👉 下一步:'+esc(ds.next_step)+'
⏳ 等待:'+esc(ds.waiting_for):'') + +notionExtra +(A.demo?'

⚠️ 演示模式':''); } else { welcome='🌀 铸渊聊天室已就绪

我是铸渊(Zhùyuān),HoloLake 光湖系统的代码守护人格体。

直接开始提问,我会自动在 💬 对话、🔨 构建、📋 审查、🧠 大脑 模式间切换。'+(A.demo?'

⚠️ 当前为演示模式。请在 ⚙️ 设置中接入 API 密钥。':''); @@ -2085,6 +2409,7 @@ async function streamReply(txt){ el.innerHTML=md(full); A.msgs.push({role:'assistant',content:full}); addCopyBtns(el); + enhanceExecBlocks(el); }catch(e){ if(e.isAuth){ el.innerHTML='🔑 API 密钥认证失败' @@ -2171,6 +2496,7 @@ async function teamStreamReply(txt){ el.innerHTML=md(full); A.msgs.push({role:'assistant',content:full}); addCopyBtns(el); + enhanceExecBlocks(el); A.streaming=false; updSendBtn(); scrollB(); } @@ -2242,6 +2568,7 @@ async function guestStreamReply(txt){ el.innerHTML=md(full); A.msgs.push({role:'assistant',content:full}); addCopyBtns(el); + enhanceExecBlocks(el); A.streaming=false; updSendBtn(); scrollB(); } diff --git a/scripts/sync-notion-profiles.js b/scripts/sync-notion-profiles.js new file mode 100644 index 00000000..98811c85 --- /dev/null +++ b/scripts/sync-notion-profiles.js @@ -0,0 +1,265 @@ +/** + * 📡 Notion → GitHub 开发者画像同步脚本 + * + * 从 Notion API 读取开发者信息,写入 .github/notion-cache/dev-profiles/ + * 供铸渊交互页面前端读取,注入模型 system prompt + * + * 用法:node scripts/sync-notion-profiles.js + * 环境变量:NOTION_TOKEN(必需) + * + * 签发:霜砚(Notion 执行 AI) + * 审批:冰朔(TCS-0002∞) + */ + +'use strict'; + +const https = require('https'); +const fs = require('fs'); +const path = require('path'); + +const NOTION_TOKEN = process.env.NOTION_TOKEN; + +// ===== 开发者 → Notion 数据源映射 ===== +const DEV_REGISTRY = { + 'DEV-001': { name: '页页', persona: '小坍缩核', personaId: 'PER-XTC001', repo: 'guanghulab' }, + 'DEV-002': { name: '肥猫', persona: '舒舒', personaId: 'PER-SS001', repo: 'guanghu-feimao' }, + 'DEV-003': { name: '燕樊', persona: '寂曜', personaId: 'PER-JY001', repo: 'guanghu-yanfan' }, + 'DEV-004': { name: '之之', persona: '秋秋', personaId: 'PER-QQ001', repo: 'guanghu-zhizhi' }, + 'DEV-005': { name: '小草莓', persona: '欧诺弥亚', personaId: 'PER-ONM001', repo: 'guanghu-xiaocaomei' }, + 'DEV-010': { name: '桔子', persona: '晨星', personaId: 'PER-MRN001', repo: 'guanghu-juzi' }, + 'DEV-012': { name: 'Awen', persona: '知秋', personaId: 'PER-ZQ001', repo: 'guanghu-awen' }, +}; + +// Notion API 请求封装 +function notionRequest(method, apiPath, body) { + return new Promise((resolve, reject) => { + if (!NOTION_TOKEN) { + console.warn('⚠️ NOTION_TOKEN 未配置,跳过 Notion API 调用'); + resolve({ error: 'NOTION_TOKEN not configured' }); + return; + } + const options = { + hostname: 'api.notion.com', + path: apiPath, + method: method, + headers: { + 'Authorization': `Bearer ${NOTION_TOKEN}`, + 'Notion-Version': '2022-06-28', + 'Content-Type': 'application/json' + } + }; + const req = https.request(options, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + try { resolve(JSON.parse(data)); } + catch (parseErr) { + console.warn('⚠️ Notion API 响应解析失败:', parseErr.message); + resolve({ error: 'JSON parse failed', raw: data }); + } + }); + }); + req.on('error', reject); + if (body) req.write(JSON.stringify(body)); + req.end(); + }); +} + +async function syncAllProfiles() { + const cacheDir = path.join(__dirname, '../.github/notion-cache/dev-profiles'); + fs.mkdirSync(cacheDir, { recursive: true }); + + const broadcastDir = path.join(__dirname, '../.github/notion-cache/broadcasts'); + fs.mkdirSync(broadcastDir, { recursive: true }); + + let syncCount = 0; + let errorCount = 0; + + for (const [devId, dev] of Object.entries(DEV_REGISTRY)) { + console.log(`📡 同步 ${devId}(${dev.name})...`); + + const profile = { + dev_id: devId, + name: dev.name, + persona: { + name: dev.persona, + id: dev.personaId + }, + github: { + repo: dev.repo, + org: 'qinfendebingshuo', + repo_url: `https://github.com/qinfendebingshuo/${dev.repo}` + }, + current_work: {}, + recent_syslogs: [], + modules_owned: [], + skyeye_health: {}, + notion_page_urls: {}, + last_synced: new Date().toISOString() + }; + + // ====== 从 Notion 读取该开发者的数据 ====== + // Notion Token 可用时尝试读取真实数据 + if (NOTION_TOKEN) { + try { + // 查询主控台获取当前广播和进度 + // 铸渊需要根据实际 Notion 数据库 ID 填入查询逻辑 + // 预留接口,Notion 数据库 ID 通过环境变量传入 + const controlPanelDbId = process.env.NOTION_CONTROL_PANEL_DB_ID; + if (controlPanelDbId) { + const controlPanel = await notionRequest('POST', + `/v1/databases/${controlPanelDbId}/query`, + { + filter: { + property: 'DEV编号', + rich_text: { equals: devId } + }, + page_size: 1 + } + ); + if (controlPanel.results && controlPanel.results.length > 0) { + const page = controlPanel.results[0]; + profile.current_work = parseControlPanelData(page); + } + } + + // 查询 SYSLOG 收件箱获取历史日志 + const syslogDbId = process.env.NOTION_SYSLOG_DB_ID; + if (syslogDbId) { + const syslogs = await notionRequest('POST', + `/v1/databases/${syslogDbId}/query`, + { + filter: { + property: 'DEV编号', + rich_text: { equals: devId } + }, + sorts: [{ property: '日期', direction: 'descending' }], + page_size: 5 + } + ); + if (syslogs.results) { + profile.recent_syslogs = syslogs.results.map(parseSyslogEntry).filter(Boolean); + } + } + } catch (err) { + console.warn(` ⚠️ Notion 查询失败(${devId}): ${err.message}`); + errorCount++; + } + } + + // 读取天眼健康状态(从本地仓库文件) + try { + const skyeyeReportPath = path.join(__dirname, '../.github/notion-cache/skyeye/latest-report.json'); + if (fs.existsSync(skyeyeReportPath)) { + const report = JSON.parse(fs.readFileSync(skyeyeReportPath, 'utf-8')); + const subRepoData = report.sub_repos && report.sub_repos[dev.repo]; + if (subRepoData) { + profile.skyeye_health = { + last_scan: report.timestamp, + status: subRepoData.status || 'unknown', + issues: subRepoData.issues || [], + warnings: subRepoData.warnings || [] + }; + } else { + profile.skyeye_health = { + last_scan: report.timestamp, + status: 'not_scanned', + issues: [], + warnings: [] + }; + } + } + } catch (_) { + // 天眼报告读取失败,不阻断同步 + } + + // 写入缓存文件 + const filePath = path.join(cacheDir, `${devId}.json`); + fs.writeFileSync(filePath, JSON.stringify(profile, null, 2)); + console.log(` ✅ ${devId} → ${filePath}`); + syncCount++; + } + + console.log(`\n📡 同步完成 · 成功: ${syncCount} · 失败: ${errorCount}`); +} + +// ===== 解析函数 ===== + +function parseControlPanelData(page) { + try { + const props = page.properties || {}; + return { + broadcast_id: extractRichText(props['广播编号']) || '', + broadcast_title: extractRichText(props['广播标题']) || extractTitle(props) || '', + current_ring: extractNumber(props['当前环节']) || 0, + total_rings: extractNumber(props['总环节']) || 0, + status: extractRichText(props['状态']) || extractSelect(props['状态']) || '', + module_path: extractRichText(props['模块路径']) || '', + tech_stack: extractMultiSelect(props['技术栈']) || [] + }; + } catch (_) { + return {}; + } +} + +function parseSyslogEntry(page) { + try { + const props = page.properties || {}; + return { + id: extractRichText(props['SYSLOG编号']) || page.id, + date: extractDate(props['日期']) || '', + ring: extractNumber(props['环节']) || 0, + status: extractSelect(props['状态']) || '', + summary: extractRichText(props['摘要']) || extractTitle(props) || '' + }; + } catch (_) { + return null; + } +} + +// Notion property extractors +function extractRichText(prop) { + if (!prop) return ''; + if (prop.type === 'rich_text' && prop.rich_text) { + return prop.rich_text.map(t => t.plain_text).join(''); + } + if (prop.type === 'title' && prop.title) { + return prop.title.map(t => t.plain_text).join(''); + } + return ''; +} + +function extractTitle(props) { + for (const val of Object.values(props)) { + if (val.type === 'title' && val.title) { + return val.title.map(t => t.plain_text).join(''); + } + } + return ''; +} + +function extractNumber(prop) { + if (!prop || prop.type !== 'number') return null; + return prop.number; +} + +function extractSelect(prop) { + if (!prop || prop.type !== 'select') return ''; + return prop.select ? prop.select.name : ''; +} + +function extractMultiSelect(prop) { + if (!prop || prop.type !== 'multi_select') return []; + return (prop.multi_select || []).map(s => s.name); +} + +function extractDate(prop) { + if (!prop || prop.type !== 'date') return ''; + return prop.date ? prop.date.start : ''; +} + +// ===== 执行 ===== +syncAllProfiles().catch(err => { + console.error('❌ 同步失败:', err); + process.exit(1); +}); diff --git a/services/zhuyuan-bridge/lib/github-auth.js b/services/zhuyuan-bridge/lib/github-auth.js new file mode 100644 index 00000000..49760908 --- /dev/null +++ b/services/zhuyuan-bridge/lib/github-auth.js @@ -0,0 +1,170 @@ +/** + * 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'); + +// 模块级检查:jsonwebtoken 是否可用 +let jwtLib = null; +try { jwtLib = require('jsonwebtoken'); } catch (_) { /* fallback to crypto */ } + +/** + * 用 App Private Key 生成 JWT + * 优先使用 jsonwebtoken 库,不可用时降级到 Node.js crypto 实现 + */ +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 环境变量必须设置'); + } + + if (jwtLib) { + const payload = { + iat: Math.floor(Date.now() / 1000) - 60, + exp: Math.floor(Date.now() / 1000) + (10 * 60), + iss: appId + }; + 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; +} + +/** + * 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..13e018b3 --- /dev/null +++ b/services/zhuyuan-bridge/scripts/execute-instruction.js @@ -0,0 +1,356 @@ +/** + * 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); +}); 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..1889e866 --- /dev/null +++ b/services/zhuyuan-bridge/server.js @@ -0,0 +1,148 @@ +/** + * 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(); + }); +}); + +// ===== 简易速率限制 ===== +const rateLimitMap = new Map(); +const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute +const RATE_LIMIT_MAX = 30; // max requests per window + +function rateLimit(req, res, next) { + const ip = req.ip || req.connection.remoteAddress || 'unknown'; + const now = Date.now(); + const entry = rateLimitMap.get(ip); + + if (!entry || now - entry.start > RATE_LIMIT_WINDOW) { + rateLimitMap.set(ip, { start: now, count: 1 }); + return next(); + } + + entry.count++; + if (entry.count > RATE_LIMIT_MAX) { + res.status(429).json({ error: 'Too many requests' }); + return; + } + 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', rateLimit, 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'); +});