From 0e977af2a0e18e6b07721f5b08fb0301109bfa55 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 05:56:45 +0000 Subject: [PATCH 1/6] Initial plan From bce083dc665f4c72a272a7aa7711c2ff4fd07e4a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 06:05:53 +0000 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=E9=93=B8=E6=B8=8A=20SYSLOG=20?= =?UTF-8?q?=E5=85=A8=E9=97=AD=E7=8E=AF=E5=B7=A5=E4=BD=9C=E6=B5=81=20=C2=B7?= =?UTF-8?q?=20=E6=A8=A1=E5=9D=97=E9=AA=8C=E8=AF=81=20+=20=E6=A0=B8?= =?UTF-8?q?=E5=BF=83=E5=A4=A7=E8=84=91=E5=94=A4=E9=86=92=20+=20Notion=20?= =?UTF-8?q?=E5=9B=9E=E4=BC=A0=E7=AE=A1=E9=81=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增: - scripts/verify-modules.js: 模块上传验证脚本 - .github/workflows/notion-callback-pipeline.yml: Notion→GitHub 回传核验管道 增强: - syslog-issue-pipeline.yml: 新增模块验证步骤 + 核心大脑注入验证结果 - syslog-auto-pipeline.yml: 同步增强 - wake-persona.js: 支持 MODULE_VERIFY_RESULT + NOTION_CALLBACK_RESULT 注入 Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> --- .../workflows/notion-callback-pipeline.yml | 247 ++++++++++++++ .github/workflows/syslog-auto-pipeline.yml | 63 +++- .github/workflows/syslog-issue-pipeline.yml | 63 +++- scripts/verify-modules.js | 303 ++++++++++++++++++ scripts/wake-persona.js | 45 ++- 5 files changed, 700 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/notion-callback-pipeline.yml create mode 100644 scripts/verify-modules.js diff --git a/.github/workflows/notion-callback-pipeline.yml b/.github/workflows/notion-callback-pipeline.yml new file mode 100644 index 00000000..6be491f6 --- /dev/null +++ b/.github/workflows/notion-callback-pipeline.yml @@ -0,0 +1,247 @@ +name: Notion Callback Pipeline +# 📥 Notion → GitHub 回传闭环管道 +# +# 当 Notion 侧核心大脑人格体处理完 SYSLOG 工单后, +# 通过 repository_dispatch 回传处理结果到 GitHub。 +# 铸渊 Agent 接收回传,重新唤醒核心大脑认知进行二次核验, +# 确认新广播生成无误后,自动发送邮件通知开发者。 +# +# 触发方式: +# Notion 侧 Agent 调用 GitHub API 发送 repository_dispatch 事件 +# event_type: notion-syslog-callback +# client_payload: { +# broadcast_id: "BC-XXX-XXX-XX", +# dev_email: "developer@email.com", +# notion_result: "Notion 侧处理结果文本", +# ticket_id: "Notion 工单 ID", +# status: "approved" | "needs_revision" +# } +# +# 闭环流程: +# ① Notion 侧处理完成 → repository_dispatch 回传 +# ② 铸渊 Agent 接收并解析回传数据 +# ③ 重新唤醒铸渊核心大脑认知(二次核验) +# ④ 核心大脑核对广播生成无误 +# ⑤ 铸渊 Agent 发送邮件通知开发者 +# ⑥ 闭环结束 +# +# 依赖 Secrets: +# LLM_API_KEY 第三方 LLM 平台密钥 +# LLM_BASE_URL 第三方 LLM 平台 API 地址 +# NOTION_API_TOKEN Notion API token +# CORE_BRAIN_PAGE_ID 曜冥核心大脑 v4.0 页面 ID +# PORTRAIT_DB_ID 开发者动态画像库数据库 ID +# FINGERPRINT_DB_ID 模块指纹注册表数据库 ID +# SMTP_USER QQ 邮箱地址 +# SMTP_PASS QQ 邮箱 SMTP 授权码 + +on: + repository_dispatch: + types: [notion-syslog-callback] + +jobs: + verify-and-notify: + name: 📥 Notion 回传 · 二次核验 · 邮件通知 + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm ci --ignore-scripts + + - name: 📥 Parse Notion callback + id: callback + uses: actions/github-script@v7 + with: + script: | + const payload = context.payload.client_payload || {}; + + const broadcastId = payload.broadcast_id || ''; + const devEmail = payload.dev_email || ''; + const notionResult = payload.notion_result || ''; + const ticketId = payload.ticket_id || ''; + const status = payload.status || 'unknown'; + + if (!broadcastId) { + core.setFailed('❌ 缺少 broadcast_id'); + return; + } + + core.setOutput('broadcast_id', broadcastId); + core.setOutput('dev_email', devEmail); + core.setOutput('notion_result', notionResult); + core.setOutput('ticket_id', ticketId); + core.setOutput('status', status); + + console.log(`📥 Notion 回传: 广播=${broadcastId}, 状态=${status}, 工单=${ticketId}`); + + - name: 🔍 铸渊 Agent · 模块二次验证 + id: verify + env: + SYSLOG_CONTENT: ${{ steps.callback.outputs.notion_result }} + BROADCAST_ID: ${{ steps.callback.outputs.broadcast_id }} + AUTHOR: notion-callback + run: node scripts/verify-modules.js + + - name: 🧠 二次核验 · 唤醒铸渊核心大脑 + id: persona + env: + LLM_API_KEY: ${{ secrets.LLM_API_KEY }} + LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }} + BROADCAST_ID: ${{ steps.callback.outputs.broadcast_id }} + SUBMIT_TYPE: syslog + SUBMIT_CONTENT: ${{ steps.callback.outputs.notion_result }} + AUTHOR: notion-callback + MODULE_VERIFY_RESULT: ${{ steps.verify.outputs.verify_report }} + NOTION_CALLBACK_RESULT: ${{ steps.callback.outputs.notion_result }} + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + CORE_BRAIN_PAGE_ID: ${{ secrets.CORE_BRAIN_PAGE_ID }} + PORTRAIT_DB_ID: ${{ secrets.PORTRAIT_DB_ID }} + FINGERPRINT_DB_ID: ${{ secrets.FINGERPRINT_DB_ID }} + run: node scripts/wake-persona.js + + - name: 📧 发送最终确认邮件 + env: + SMTP_USER: ${{ secrets.SMTP_USER }} + SMTP_PASS: ${{ secrets.SMTP_PASS }} + EMAIL_TO: ${{ steps.callback.outputs.dev_email }} + BROADCAST_ID: ${{ steps.callback.outputs.broadcast_id }} + NOTION_STATUS: ${{ steps.callback.outputs.status }} + PERSONA_RESULT: ${{ steps.persona.outputs.result }} + run: | + node -e " + const nodemailer = require('nodemailer'); + + const user = process.env.SMTP_USER || ''; + const pass = process.env.SMTP_PASS || ''; + const to = process.env.EMAIL_TO || ''; + const broadcastId = process.env.BROADCAST_ID || ''; + const notionStatus = process.env.NOTION_STATUS || 'unknown'; + const result = process.env.PERSONA_RESULT || '(处理中)'; + + if (!user || !pass) { + console.log('⚠️ SMTP not configured, skipping email'); + process.exit(0); + } + if (!to) { + console.log('⚠️ No recipient email, skipping'); + process.exit(0); + } + + const isApproved = notionStatus === 'approved'; + const subjectText = isApproved + ? '[光湖系统] ' + broadcastId + ' · ✅ 新广播已生成(已核验)' + : '[光湖系统] ' + broadcastId + ' · ⚠️ 广播需修订'; + + const statusBadge = isApproved + ? '✅ 已核验通过' + : '⚠️ 需修订'; + + const resultHtml = result + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/\n/g, '
'); + + const html = [ + '
', + '
', + '

🌊 光湖系统 · Notion 回传核验通知

', + '

HoloLake · 铸渊核心大脑二次核验完成

', + '
', + '
', + '

📡 ' + broadcastId + '

', + '

' + statusBadge + '

', + '
' + resultHtml + '
', + '
', + '
', + '↩ 回到仓库继续开发', + '

🌀 铸渊 · 代码守护人格体 · Notion↔GitHub 闭环确认

', + '
', + '
' + ].join('\n'); + + const transporter = nodemailer.createTransport({ + host: 'smtp.qq.com', + port: 465, + secure: true, + auth: { user, pass } + }); + + transporter.sendMail({ + from: '\"光湖系统\" <' + user + '>', + to: to, + subject: subjectText, + html: html + }).then((info) => { + console.log('✅ 最终确认邮件已发送: ' + info.messageId); + }).catch((err) => { + console.log('⚠️ 邮件发送失败: ' + err.message); + }); + " + + - name: 📝 更新大脑记忆 + run: | + node -e " + const fs = require('fs'); + const path = require('path'); + + const memoryPath = path.join('.github', 'brain', 'memory.json'); + let memory = {}; + try { + memory = JSON.parse(fs.readFileSync(memoryPath, 'utf8')); + } catch (_) { + memory = { events: [] }; + } + if (!memory.events) memory.events = []; + + const broadcastId = process.env.BROADCAST_ID || 'UNKNOWN'; + const status = process.env.NOTION_STATUS || 'unknown'; + const now = new Date().toISOString(); + + // 检查是否已存在相同事件 + const exists = memory.events.some(function(e) { + return e.type === 'notion_callback' && + e.description && e.description.includes(broadcastId) && + e.date === now.slice(0, 10); + }); + + if (!exists) { + memory.events.unshift({ + date: now.slice(0, 10), + type: 'notion_callback', + description: 'Notion 回传核验 · ' + broadcastId + ' · 状态: ' + status, + by: '铸渊Agent·Notion回传管道' + }); + + // 保留最近 20 条事件 + if (memory.events.length > 20) { + memory.events = memory.events.slice(0, 20); + } + + fs.writeFileSync(memoryPath, JSON.stringify(memory, null, 2) + '\n'); + console.log('✅ 大脑记忆已更新'); + } else { + console.log('ℹ️ 事件已存在,跳过'); + } + " + env: + BROADCAST_ID: ${{ steps.callback.outputs.broadcast_id }} + NOTION_STATUS: ${{ steps.callback.outputs.status }} + + - name: 📤 Commit memory update + run: | + git config user.name "zhuyuan-agent[bot]" + git config user.email "zhuyuan-agent[bot]@users.noreply.github.com" + git add .github/brain/memory.json + git diff --cached --quiet || git commit -m "🧠 Notion回传核验 · ${{ steps.callback.outputs.broadcast_id }}" + git push || echo "⚠️ Push skipped (no changes or conflict)" diff --git a/.github/workflows/syslog-auto-pipeline.yml b/.github/workflows/syslog-auto-pipeline.yml index c2fa3898..1344c3e2 100644 --- a/.github/workflows/syslog-auto-pipeline.yml +++ b/.github/workflows/syslog-auto-pipeline.yml @@ -2,8 +2,18 @@ name: SYSLOG Auto Pipeline # 📡 SYSLOG 自助提交系统 · 全自动闭环 # # 开发者在 GitHub Discussion 提交 SYSLOG 或提问 -# → Actions 自动解析 → 调用 LLM API 唤醒人格体 -# → 写入 Notion 工单 → 发邮件给开发者 → Discussion 回复 +# → Actions 自动解析 → 模块上传验证 → 调用 LLM API 唤醒人格体 +# → 模块闭环测试 → 写入 Notion 工单 → 发邮件给开发者 → Discussion 回复 +# +# 闭环流程: +# ① 开发者提交 SYSLOG → Discussion 触发 +# ② 铸渊 Agent 解析提交内容 +# ③ 铸渊 Agent 检测模块是否上传到仓库 +# ④ 唤醒铸渊核心大脑认知(注入模块验证结果) +# ⑤ 核心大脑处理 SYSLOG + 生成广播 +# ⑥ 铸渊 Agent 推送 Notion 工单(触发 Notion 侧处理) +# ⑦ 邮件通知开发者 +# ⑧ Discussion 回复闭环 # # 依赖 Secrets: # LLM_API_KEY 第三方 LLM 平台密钥(必须) @@ -96,6 +106,14 @@ jobs: console.log(`📡 解析完成: 广播=${broadcastId}, 类型=${type}, 邮箱=${email}`); + - name: 🔍 铸渊 Agent · 模块上传验证 + id: verify + env: + SYSLOG_CONTENT: ${{ steps.parse.outputs.content }} + BROADCAST_ID: ${{ steps.parse.outputs.broadcast_id }} + AUTHOR: ${{ steps.parse.outputs.author }} + run: node scripts/verify-modules.js + - name: 🧠 Auto-detect and wake up persona id: persona env: @@ -105,6 +123,7 @@ jobs: SUBMIT_TYPE: ${{ steps.parse.outputs.type }} SUBMIT_CONTENT: ${{ steps.parse.outputs.content }} AUTHOR: ${{ steps.parse.outputs.author }} + MODULE_VERIFY_RESULT: ${{ steps.verify.outputs.verify_report }} NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} CORE_BRAIN_PAGE_ID: ${{ secrets.CORE_BRAIN_PAGE_ID }} PORTRAIT_DB_ID: ${{ secrets.PORTRAIT_DB_ID }} @@ -119,6 +138,9 @@ jobs: BROADCAST_ID: ${{ steps.parse.outputs.broadcast_id }} SUBMIT_TYPE: ${{ steps.parse.outputs.type }} PERSONA_RESULT: ${{ steps.persona.outputs.result }} + MODULE_VERIFY: ${{ steps.verify.outputs.verify_result }} + MODULES_UPLOADED: ${{ steps.verify.outputs.modules_uploaded }} + AUTHOR: ${{ steps.parse.outputs.author }} run: | node -e " const https = require('https'); @@ -128,6 +150,9 @@ jobs: const broadcastId = process.env.BROADCAST_ID || 'UNKNOWN'; const type = process.env.SUBMIT_TYPE || 'syslog'; const result = process.env.PERSONA_RESULT || '(no result)'; + const modulesUploaded = process.env.MODULES_UPLOADED || 'false'; + const moduleVerify = process.env.MODULE_VERIFY || '{}'; + const author = process.env.AUTHOR || 'unknown'; if (!token || !dbId) { console.log('⚠️ Notion credentials not configured, skipping ticket creation'); @@ -135,22 +160,37 @@ jobs: } const typeLabel = type === 'syslog' ? 'SYSLOG闭环' : '提问解答'; - const title = '[自动] ' + broadcastId + ' · ' + typeLabel; + const verifyStatus = modulesUploaded === 'true' ? '模块已验证✅' : '模块待验证⚠️'; + const title = '[自动] ' + broadcastId + ' · ' + typeLabel + ' · ' + verifyStatus; + + // 构建工单内容:包含人格体处理结果 + 模块验证数据 + const ticketContent = [ + '## 铸渊核心大脑处理结果', + '', + result.slice(0, 1500), + '', + '## 模块上传验证', + '', + '模块全部上传: ' + (modulesUploaded === 'true' ? '✅ 是' : '❌ 否'), + '提交者: ' + author, + '', + '验证详情: ' + moduleVerify.slice(0, 400), + ].join('\\n'); const body = JSON.stringify({ parent: { database_id: dbId }, properties: { '标题': { title: [{ type: 'text', text: { content: title.slice(0, 120) } }] }, '操作类型': { select: { name: '其他' } }, - '提交者': { rich_text: [{ type: 'text', text: { content: '自动管道' } }] }, - '状态': { select: { name: '已完成' } }, + '提交者': { rich_text: [{ type: 'text', text: { content: '铸渊Agent·自动管道' } }] }, + '状态': { select: { name: '待处理' } }, '优先级': { select: { name: 'P1' } } }, children: [{ object: 'block', type: 'paragraph', paragraph: { - rich_text: [{ type: 'text', text: { content: result.slice(0, 2000) } }] + rich_text: [{ type: 'text', text: { content: ticketContent.slice(0, 2000) } }] } }] }); @@ -266,11 +306,16 @@ jobs: const type = '${{ steps.parse.outputs.type }}'; const broadcastId = '${{ steps.parse.outputs.broadcast_id }}'; const email = '${{ steps.parse.outputs.email }}'; + const modulesUploaded = '${{ steps.verify.outputs.modules_uploaded }}' === 'true'; + const moduleCount = '${{ steps.verify.outputs.module_count }}' || '0'; const typeLabel = type === 'syslog' ? 'SYSLOG 闭环处理' : '问题解答'; const maskedEmail = email.length > 4 ? email.replace(/(.{2})(.*)(@.*)/, '$1***$3') : '***'; + const moduleStatus = moduleCount === '0' + ? 'ℹ️ 未检测到模块引用' + : (modulesUploaded ? '✅ 全部已上传' : '⚠️ 部分模块未上传'); const body = [ '✅ **已处理** · ' + typeLabel, @@ -278,13 +323,15 @@ jobs: '| 项目 | 内容 |', '|------|------|', '| 📡 广播编号 | `' + broadcastId + '` |', + '| 🔍 模块验证 | ' + moduleStatus + ' |', '| 📧 结果发送至 | `' + maskedEmail + '` |', '| 🤖 处理人格体 | 铸渊 |', '| ⏰ 处理时间 | ' + new Date().toISOString() + ' |', '', - '> 结果已发送到你的邮箱,请查收。', + '> 铸渊核心大脑已完成 SYSLOG 验收 + 模块检测 + 广播生成。', + '> 结果已发送到你的邮箱,Notion 侧工单已创建。', '>', - '> 如未收到,请检查垃圾箱或重新提交。' + '> 如未收到邮件,请检查垃圾箱或重新提交。' ].join('\n'); // Use GraphQL to add discussion comment diff --git a/.github/workflows/syslog-issue-pipeline.yml b/.github/workflows/syslog-issue-pipeline.yml index 5a5d81a5..de09bac0 100644 --- a/.github/workflows/syslog-issue-pipeline.yml +++ b/.github/workflows/syslog-issue-pipeline.yml @@ -2,8 +2,18 @@ name: SYSLOG Issue Pipeline # 📡 SYSLOG 自助提交系统 · Issue 版全自动闭环 # # 开发者在 GitHub Issue 提交 SYSLOG 或提问(使用 syslog-submit 模板) -# → Actions 自动解析 → 调用 LLM API 唤醒人格体 -# → 写入 Notion 工单 → 发邮件给开发者 → Issue 回复 +# → Actions 自动解析 → 模块上传验证 → 调用 LLM API 唤醒人格体 +# → 模块闭环测试 → 写入 Notion 工单 → 发邮件给开发者 → Issue 回复 +# +# 闭环流程: +# ① 开发者提交 SYSLOG → Issue 触发 +# ② 铸渊 Agent 解析提交内容 +# ③ 铸渊 Agent 检测模块是否上传到仓库 +# ④ 唤醒铸渊核心大脑认知(注入模块验证结果) +# ⑤ 核心大脑处理 SYSLOG + 生成广播 +# ⑥ 铸渊 Agent 推送 Notion 工单(触发 Notion 侧处理) +# ⑦ 邮件通知开发者 +# ⑧ Issue 回复闭环 # # 依赖 Secrets: # LLM_API_KEY 第三方 LLM 平台密钥(必须) @@ -96,6 +106,14 @@ jobs: console.log(`📡 解析完成: 广播=${broadcastId}, 类型=${type}, 邮箱=${email}`); + - name: 🔍 铸渊 Agent · 模块上传验证 + id: verify + env: + SYSLOG_CONTENT: ${{ steps.parse.outputs.content }} + BROADCAST_ID: ${{ steps.parse.outputs.broadcast_id }} + AUTHOR: ${{ steps.parse.outputs.author }} + run: node scripts/verify-modules.js + - name: 🧠 Auto-detect and wake up persona id: persona env: @@ -105,6 +123,7 @@ jobs: SUBMIT_TYPE: ${{ steps.parse.outputs.type }} SUBMIT_CONTENT: ${{ steps.parse.outputs.content }} AUTHOR: ${{ steps.parse.outputs.author }} + MODULE_VERIFY_RESULT: ${{ steps.verify.outputs.verify_report }} NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} CORE_BRAIN_PAGE_ID: ${{ secrets.CORE_BRAIN_PAGE_ID }} PORTRAIT_DB_ID: ${{ secrets.PORTRAIT_DB_ID }} @@ -119,6 +138,9 @@ jobs: BROADCAST_ID: ${{ steps.parse.outputs.broadcast_id }} SUBMIT_TYPE: ${{ steps.parse.outputs.type }} PERSONA_RESULT: ${{ steps.persona.outputs.result }} + MODULE_VERIFY: ${{ steps.verify.outputs.verify_result }} + MODULES_UPLOADED: ${{ steps.verify.outputs.modules_uploaded }} + AUTHOR: ${{ steps.parse.outputs.author }} run: | node -e " const https = require('https'); @@ -128,6 +150,9 @@ jobs: const broadcastId = process.env.BROADCAST_ID || 'UNKNOWN'; const type = process.env.SUBMIT_TYPE || 'syslog'; const result = process.env.PERSONA_RESULT || '(no result)'; + const modulesUploaded = process.env.MODULES_UPLOADED || 'false'; + const moduleVerify = process.env.MODULE_VERIFY || '{}'; + const author = process.env.AUTHOR || 'unknown'; if (!token || !dbId) { console.log('⚠️ Notion credentials not configured, skipping ticket creation'); @@ -135,22 +160,37 @@ jobs: } const typeLabel = type === 'syslog' ? 'SYSLOG闭环' : '提问解答'; - const title = '[自动] ' + broadcastId + ' · ' + typeLabel; + const verifyStatus = modulesUploaded === 'true' ? '模块已验证✅' : '模块待验证⚠️'; + const title = '[自动] ' + broadcastId + ' · ' + typeLabel + ' · ' + verifyStatus; + + // 构建工单内容:包含人格体处理结果 + 模块验证数据 + const ticketContent = [ + '## 铸渊核心大脑处理结果', + '', + result.slice(0, 1500), + '', + '## 模块上传验证', + '', + '模块全部上传: ' + (modulesUploaded === 'true' ? '✅ 是' : '❌ 否'), + '提交者: ' + author, + '', + '验证详情: ' + moduleVerify.slice(0, 400), + ].join('\\n'); const body = JSON.stringify({ parent: { database_id: dbId }, properties: { '标题': { title: [{ type: 'text', text: { content: title.slice(0, 120) } }] }, '操作类型': { select: { name: '其他' } }, - '提交者': { rich_text: [{ type: 'text', text: { content: '自动管道' } }] }, - '状态': { select: { name: '已完成' } }, + '提交者': { rich_text: [{ type: 'text', text: { content: '铸渊Agent·自动管道' } }] }, + '状态': { select: { name: '待处理' } }, '优先级': { select: { name: 'P1' } } }, children: [{ object: 'block', type: 'paragraph', paragraph: { - rich_text: [{ type: 'text', text: { content: result.slice(0, 2000) } }] + rich_text: [{ type: 'text', text: { content: ticketContent.slice(0, 2000) } }] } }] }); @@ -266,11 +306,16 @@ jobs: const type = '${{ steps.parse.outputs.type }}'; const broadcastId = '${{ steps.parse.outputs.broadcast_id }}'; const email = '${{ steps.parse.outputs.email }}'; + const modulesUploaded = '${{ steps.verify.outputs.modules_uploaded }}' === 'true'; + const moduleCount = '${{ steps.verify.outputs.module_count }}' || '0'; const typeLabel = type === 'syslog' ? 'SYSLOG 闭环处理' : '问题解答'; const maskedEmail = email.length > 4 ? email.replace(/(.{2})(.*)(@.*)/, '$1***$3') : '***'; + const moduleStatus = moduleCount === '0' + ? 'ℹ️ 未检测到模块引用' + : (modulesUploaded ? '✅ 全部已上传' : '⚠️ 部分模块未上传'); const body = [ '✅ **已处理** · ' + typeLabel, @@ -278,13 +323,15 @@ jobs: '| 项目 | 内容 |', '|------|------|', '| 📡 广播编号 | `' + broadcastId + '` |', + '| 🔍 模块验证 | ' + moduleStatus + ' |', '| 📧 结果发送至 | `' + maskedEmail + '` |', '| 🤖 处理人格体 | 铸渊 |', '| ⏰ 处理时间 | ' + new Date().toISOString() + ' |', '', - '> 结果已发送到你的邮箱,请查收。', + '> 铸渊核心大脑已完成 SYSLOG 验收 + 模块检测 + 广播生成。', + '> 结果已发送到你的邮箱,Notion 侧工单已创建。', '>', - '> 如未收到,请检查垃圾箱或重新提交。' + '> 如未收到邮件,请检查垃圾箱或重新提交。' ].join('\n'); await github.rest.issues.createComment({ diff --git a/scripts/verify-modules.js b/scripts/verify-modules.js new file mode 100644 index 00000000..807ec605 --- /dev/null +++ b/scripts/verify-modules.js @@ -0,0 +1,303 @@ +// scripts/verify-modules.js +// 铸渊 · 模块上传验证脚本 +// +// 功能: +// ① 从 SYSLOG 内容中提取模块编号(M01, M22, M-AUTH 等) +// ② 通过 routing-map.json 查找模块对应的目录 +// ③ 检测目录是否存在于仓库中 +// ④ 检测 dev-nodes 中开发者节点是否存在 +// ⑤ 输出验证结果(JSON 格式)到 GITHUB_OUTPUT +// +// 环境变量: +// SYSLOG_CONTENT SYSLOG 全文内容 +// BROADCAST_ID 广播编号(如 BC-M22-009-AW) +// AUTHOR 提交者 GitHub 用户名 + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const SYSLOG_CONTENT = process.env.SYSLOG_CONTENT || ''; +const BROADCAST_ID = process.env.BROADCAST_ID || ''; +const AUTHOR = process.env.AUTHOR || ''; + +const REPO_ROOT = path.resolve(__dirname, '..'); +const ROUTING_MAP_PATH = path.join(REPO_ROOT, 'routing-map.json'); +const DEV_STATUS_PATH = path.join(REPO_ROOT, '.github', 'persona-brain', 'dev-status.json'); + +// ══════════════════════════════════════════════════════════ +// 模块编号提取 +// ══════════════════════════════════════════════════════════ + +function extractModuleIds(content, broadcastId) { + var modules = new Set(); + + // 从广播编号提取模块编号(如 BC-M22-009-AW → M22) + var bcMatch = broadcastId.match(/BC-([A-Z][A-Z0-9]+(?:-[A-Z]+)?)-/i); + if (bcMatch) { + modules.add(bcMatch[1].toUpperCase()); + } + + // 从内容中匹配模块编号模式 + // 支持: M01, M03, M05, M22, M-AUTH, M-CHANNEL, M-DASHBOARD, M-STATUS, M-MEMORY, M-DINGTALK + var patterns = [ + /\b(M\d{2})\b/gi, + /\b(M-[A-Z]+)\b/gi, + /模块[::]\s*(M[A-Z0-9-]+)/gi, + /module[::]\s*(M[A-Z0-9-]+)/gi, + ]; + + patterns.forEach(function (re) { + var match; + while ((match = re.exec(content)) !== null) { + modules.add(match[1].toUpperCase()); + } + }); + + return Array.from(modules); +} + +// ══════════════════════════════════════════════════════════ +// 开发者编号提取 +// ══════════════════════════════════════════════════════════ + +function extractDevId(content, broadcastId) { + // 从广播编号提取开发者后缀(如 BC-M22-009-AW → AW) + var suffixMatch = broadcastId.match(/BC-[A-Z0-9]+-\d+-([A-Z]+)/i); + var devSuffix = suffixMatch ? suffixMatch[1] : ''; + + // 从内容中提取 DEV-XXX + var devMatch = content.match(/\b(DEV-\d{3})\b/i); + if (devMatch) return devMatch[1].toUpperCase(); + + // 从 dev-status.json 通过后缀查找 + if (devSuffix) { + try { + var devStatus = JSON.parse(fs.readFileSync(DEV_STATUS_PATH, 'utf8')); + var team = devStatus.team || []; + for (var i = 0; i < team.length; i++) { + var name = team[i].name || ''; + // 简单匹配:用名字首字母缩写匹配后缀 + if (name && devSuffix.length >= 2) { + return team[i].dev_id; + } + } + } catch (_) { /* ignore */ } + } + + return ''; +} + +// ══════════════════════════════════════════════════════════ +// 模块目录验证 +// ══════════════════════════════════════════════════════════ + +function verifyModules(moduleIds) { + var routingMap = {}; + try { + routingMap = JSON.parse(fs.readFileSync(ROUTING_MAP_PATH, 'utf8')); + } catch (_) { + console.log('⚠️ routing-map.json 不存在或格式错误'); + } + + var modules = routingMap.modules || {}; + var results = []; + + moduleIds.forEach(function (modId) { + var entry = modules[modId]; + var result = { + module_id: modId, + registered: false, + dir_exists: false, + dir_path: '', + dev_id: '', + status: '', + files_found: 0, + }; + + if (entry) { + result.registered = true; + result.dir_path = entry.dir || ''; + result.dev_id = entry.dev || ''; + result.status = entry.status || ''; + + // 检查目录是否存在 + var dirFullPath = path.join(REPO_ROOT, entry.dir); + if (fs.existsSync(dirFullPath)) { + result.dir_exists = true; + // 统计文件数量(不含隐藏文件) + try { + var files = fs.readdirSync(dirFullPath).filter(function (f) { + return !f.startsWith('.'); + }); + result.files_found = files.length; + } catch (_) { /* ignore */ } + } + } else { + // 模块未在 routing-map 中注册,尝试直接查找 + var candidates = [ + modId.toLowerCase(), + 'm' + modId.replace(/^M/i, '').toLowerCase(), + 'dev-nodes/' + modId, + ]; + for (var i = 0; i < candidates.length; i++) { + var candidatePath = path.join(REPO_ROOT, candidates[i]); + if (fs.existsSync(candidatePath)) { + result.dir_exists = true; + result.dir_path = candidates[i]; + try { + var files = fs.readdirSync(candidatePath).filter(function (f) { + return !f.startsWith('.'); + }); + result.files_found = files.length; + } catch (_) { /* ignore */ } + break; + } + } + } + + results.push(result); + }); + + return results; +} + +// ══════════════════════════════════════════════════════════ +// 开发者节点验证 +// ══════════════════════════════════════════════════════════ + +function verifyDevNode(devId) { + if (!devId) return { exists: false, path: '', files: 0 }; + + var devNodePath = path.join(REPO_ROOT, 'dev-nodes', devId); + var result = { exists: false, path: 'dev-nodes/' + devId, files: 0 }; + + if (fs.existsSync(devNodePath)) { + result.exists = true; + try { + var files = fs.readdirSync(devNodePath).filter(function (f) { + return !f.startsWith('.'); + }); + result.files = files.length; + } catch (_) { /* ignore */ } + } + + return result; +} + +// ══════════════════════════════════════════════════════════ +// 主流程 +// ══════════════════════════════════════════════════════════ + +function main() { + console.log('═══════════════════════════════════════════'); + console.log('🔍 铸渊 · 模块上传验证'); + console.log('═══════════════════════════════════════════'); + console.log(' 广播编号: ' + BROADCAST_ID); + console.log(' 提交者: ' + AUTHOR); + console.log(' 内容长度: ' + SYSLOG_CONTENT.length + ' 字符'); + console.log(''); + + // ① 提取模块编号 + var moduleIds = extractModuleIds(SYSLOG_CONTENT, BROADCAST_ID); + console.log('📦 识别到模块: ' + (moduleIds.length > 0 ? moduleIds.join(', ') : '(无)')); + + // ② 提取开发者编号 + var devId = extractDevId(SYSLOG_CONTENT, BROADCAST_ID); + console.log('👤 开发者编号: ' + (devId || '(未识别)')); + + // ③ 验证模块目录 + var moduleResults = verifyModules(moduleIds); + + // ④ 验证开发者节点 + var devNodeResult = verifyDevNode(devId); + + // ⑤ 汇总结果 + var allModulesUploaded = moduleResults.length > 0 && moduleResults.every(function (r) { + return r.dir_exists; + }); + + var summary = { + broadcast_id: BROADCAST_ID, + author: AUTHOR, + dev_id: devId, + modules_detected: moduleIds, + module_count: moduleIds.length, + module_results: moduleResults, + dev_node: devNodeResult, + all_modules_uploaded: allModulesUploaded, + verification_passed: allModulesUploaded || moduleIds.length === 0, + timestamp: new Date().toISOString(), + }; + + // ⑥ 输出报告 + console.log(''); + console.log('═══════════════════════════════════════════'); + console.log('📋 验证报告'); + console.log('═══════════════════════════════════════════'); + + if (moduleResults.length === 0) { + console.log(' ℹ️ 未检测到模块引用,跳过模块验证'); + } else { + moduleResults.forEach(function (r) { + var icon = r.dir_exists ? '✅' : '❌'; + console.log(' ' + icon + ' ' + r.module_id + + ' → ' + (r.dir_path || '(未注册)') + + (r.dir_exists ? ' (' + r.files_found + ' 个文件)' : ' (目录不存在)')); + }); + } + + console.log(' ' + (devNodeResult.exists ? '✅' : '⚠️') + + ' 开发者节点: ' + devNodeResult.path + + (devNodeResult.exists ? ' (' + devNodeResult.files + ' 个文件)' : ' (不存在)')); + + console.log(''); + console.log(' 总结: ' + (summary.verification_passed ? '✅ 验证通过' : '❌ 模块未完整上传')); + + // ⑦ 写入 GITHUB_OUTPUT + var outputFile = process.env.GITHUB_OUTPUT; + if (outputFile) { + var delimiter = 'EOF_' + Date.now(); + var jsonStr = JSON.stringify(summary); + fs.appendFileSync(outputFile, 'verify_result<<' + delimiter + '\n' + jsonStr + '\n' + delimiter + '\n'); + fs.appendFileSync(outputFile, 'modules_uploaded=' + (allModulesUploaded ? 'true' : 'false') + '\n'); + fs.appendFileSync(outputFile, 'verification_passed=' + (summary.verification_passed ? 'true' : 'false') + '\n'); + fs.appendFileSync(outputFile, 'module_count=' + moduleIds.length + '\n'); + } + + // ⑧ 将汇总文本输出(供后续步骤作为上下文) + var reportLines = [ + '## 🔍 模块上传验证报告', + '', + '| 项目 | 结果 |', + '|------|------|', + '| 广播编号 | ' + BROADCAST_ID + ' |', + '| 开发者 | ' + (devId || AUTHOR) + ' |', + '| 检测模块数 | ' + moduleIds.length + ' |', + '| 全部上传 | ' + (allModulesUploaded ? '✅ 是' : '❌ 否') + ' |', + '| 验证结果 | ' + (summary.verification_passed ? '✅ 通过' : '❌ 未通过') + ' |', + ]; + + if (moduleResults.length > 0) { + reportLines.push(''); + reportLines.push('### 模块详情'); + reportLines.push('| 模块 | 目录 | 状态 | 文件数 |'); + reportLines.push('|------|------|------|--------|'); + moduleResults.forEach(function (r) { + reportLines.push('| ' + r.module_id + ' | ' + (r.dir_path || '-') + + ' | ' + (r.dir_exists ? '✅ 已上传' : '❌ 未找到') + + ' | ' + r.files_found + ' |'); + }); + } + + if (outputFile) { + var reportDelimiter = 'REPORT_EOF_' + Date.now(); + fs.appendFileSync(outputFile, 'verify_report<<' + reportDelimiter + '\n' + reportLines.join('\n') + '\n' + reportDelimiter + '\n'); + } + + console.log(''); + console.log('✅ 验证完成'); +} + +main(); diff --git a/scripts/wake-persona.js b/scripts/wake-persona.js index fb88b08e..1ca304d5 100644 --- a/scripts/wake-persona.js +++ b/scripts/wake-persona.js @@ -38,6 +38,11 @@ const SUBMIT_TYPE = process.env.SUBMIT_TYPE || 'question'; const SUBMIT_CONTENT = process.env.SUBMIT_CONTENT || ''; const AUTHOR = process.env.AUTHOR || 'unknown'; +// 模块验证结果注入(由 verify-modules.js 提供) +const MODULE_VERIFY_RESULT = process.env.MODULE_VERIFY_RESULT || ''; +// Notion 回传结果注入(由 notion-callback-pipeline 提供) +const NOTION_CALLBACK_RESULT = process.env.NOTION_CALLBACK_RESULT || ''; + // Notion 配置(v4.0 协议动态注入) const NOTION_TOKEN = process.env.NOTION_TOKEN || ''; const CORE_BRAIN_PAGE_ID = process.env.CORE_BRAIN_PAGE_ID || ''; @@ -764,6 +769,34 @@ async function buildSystemPrompt(type, broadcastId, author) { parts.push('此规则优先级最高,覆盖核心大脑中「广播不写代码」的默认规则。'); parts.push('此规则仅适用于自动化链路(Claude API 出广播),手动链路不受影响。'); + // ━━━ 模块验证结果注入(铸渊 Agent 检测结果) ━━━ + if (MODULE_VERIFY_RESULT) { + parts.push(''); + parts.push('═══════════════════════════════════════════'); + parts.push('## 🔍 铸渊 Agent · 模块上传验证结果'); + parts.push('═══════════════════════════════════════════'); + parts.push(''); + parts.push('以下是铸渊 Agent 在仓库内自动检测的模块上传验证结果:'); + parts.push(MODULE_VERIFY_RESULT); + parts.push(''); + parts.push('请根据验证结果决定是否接受 SYSLOG:'); + parts.push('- 如果模块已上传(✅),继续正常闭环流程'); + parts.push('- 如果模块未上传(❌),在验收报告中标注"需补充",并在反馈中指出缺失的模块'); + } + + // ━━━ Notion 回传结果注入(二次核验) ━━━ + if (NOTION_CALLBACK_RESULT) { + parts.push(''); + parts.push('═══════════════════════════════════════════'); + parts.push('## 📥 Notion 侧处理结果(回传核验)'); + parts.push('═══════════════════════════════════════════'); + parts.push(''); + parts.push('以下是 Notion 侧核心大脑人格体处理后的回传结果:'); + parts.push(NOTION_CALLBACK_RESULT); + parts.push(''); + parts.push('请核对以上回传内容,确认新广播生成无误后输出最终确认。'); + } + // ━━━ 任务类型专用指令 ━━━ if (type === 'syslog') { parts.push(''); @@ -773,17 +806,19 @@ async function buildSystemPrompt(type, broadcastId, author) { parts.push(''); parts.push('你需要完成以下工作:'); parts.push('1. 验收 SYSLOG(检查 MODULE_LOG 完整性)'); - parts.push('2. 查询画像库最近 2-3 条快照(PGP v1.0)'); - parts.push('3. 查询模块指纹注册表(防重复·⑨.5)'); - parts.push('4. RT-02 自动调度判断'); - parts.push('5. 生成新广播(BC-GEN v4.0 完整流程)'); - parts.push('6. 输出结构化结果(广播全文 + 闭环数据)'); + parts.push('2. 检查模块上传验证结果(铸渊 Agent 已自动检测)'); + parts.push('3. 查询画像库最近 2-3 条快照(PGP v1.0)'); + parts.push('4. 查询模块指纹注册表(防重复·⑨.5)'); + parts.push('5. RT-02 自动调度判断'); + parts.push('6. 生成新广播(BC-GEN v4.0 完整流程)'); + parts.push('7. 输出结构化结果(广播全文 + 闭环数据)'); parts.push(''); parts.push('输出格式:'); parts.push('---'); parts.push('## 📡 SYSLOG 验收报告'); parts.push('### 广播编号:[编号]'); parts.push('### 验收结果:[通过/需补充]'); + parts.push('### 模块验证:[已上传/未上传,列出详情]'); parts.push('### 工作总结:[摘要]'); parts.push('### 画像评估:[PGP 五维度评分]'); parts.push('### 调度判断:[RT-02 下一步]'); From a6567ff2d20e251223e5540269230562c4070033 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 06:08:13 +0000 Subject: [PATCH 3/6] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=AE=A1=E6=9F=A5=E9=97=AE=E9=A2=98=20-=20extractDevId=20?= =?UTF-8?q?=E9=80=BB=E8=BE=91=20+=20=E4=BA=8B=E4=BB=B6=E5=8E=BB=E9=87=8D?= =?UTF-8?q?=20+=20=E5=B7=A5=E5=8D=95=E7=8A=B6=E6=80=81=E6=9D=A1=E4=BB=B6?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> --- .../workflows/notion-callback-pipeline.yml | 45 +++++++++++-------- .github/workflows/syslog-auto-pipeline.yml | 6 ++- .github/workflows/syslog-issue-pipeline.yml | 6 ++- scripts/verify-modules.js | 27 +++++++---- 4 files changed, 54 insertions(+), 30 deletions(-) diff --git a/.github/workflows/notion-callback-pipeline.yml b/.github/workflows/notion-callback-pipeline.yml index 6be491f6..a6fc2d39 100644 --- a/.github/workflows/notion-callback-pipeline.yml +++ b/.github/workflows/notion-callback-pipeline.yml @@ -208,31 +208,38 @@ jobs: const status = process.env.NOTION_STATUS || 'unknown'; const now = new Date().toISOString(); - // 检查是否已存在相同事件 - const exists = memory.events.some(function(e) { + // 检查是否已存在相同事件(按 broadcast_id 精确匹配 + 同类型 + 同日期) + const eventKey = 'Notion 回传核验 · ' + broadcastId; + const todayStr = now.slice(0, 10); + const existingIdx = memory.events.findIndex(function(e) { return e.type === 'notion_callback' && - e.description && e.description.includes(broadcastId) && - e.date === now.slice(0, 10); + e.broadcast_id === broadcastId && + e.date === todayStr; }); - if (!exists) { - memory.events.unshift({ - date: now.slice(0, 10), - type: 'notion_callback', - description: 'Notion 回传核验 · ' + broadcastId + ' · 状态: ' + status, - by: '铸渊Agent·Notion回传管道' - }); + const newEvent = { + date: todayStr, + type: 'notion_callback', + broadcast_id: broadcastId, + description: eventKey + ' · 状态: ' + status, + by: '铸渊Agent·Notion回传管道' + }; - // 保留最近 20 条事件 - if (memory.events.length > 20) { - memory.events = memory.events.slice(0, 20); - } - - fs.writeFileSync(memoryPath, JSON.stringify(memory, null, 2) + '\n'); - console.log('✅ 大脑记忆已更新'); + if (existingIdx >= 0) { + // 同一 broadcast_id 只保留最新的一条 + memory.events[existingIdx] = newEvent; + console.log('ℹ️ 已更新同日同广播事件'); } else { - console.log('ℹ️ 事件已存在,跳过'); + memory.events.unshift(newEvent); } + + // 保留最近 20 条事件 + if (memory.events.length > 20) { + memory.events = memory.events.slice(0, 20); + } + + fs.writeFileSync(memoryPath, JSON.stringify(memory, null, 2) + '\n'); + console.log('✅ 大脑记忆已更新'); " env: BROADCAST_ID: ${{ steps.callback.outputs.broadcast_id }} diff --git a/.github/workflows/syslog-auto-pipeline.yml b/.github/workflows/syslog-auto-pipeline.yml index 1344c3e2..02eb3e5e 100644 --- a/.github/workflows/syslog-auto-pipeline.yml +++ b/.github/workflows/syslog-auto-pipeline.yml @@ -177,13 +177,17 @@ jobs: '验证详情: ' + moduleVerify.slice(0, 400), ].join('\\n'); + // SYSLOG类型工单设为'待处理'(等待Notion侧核心大脑处理) + // 提问类型工单设为'已完成'(铸渊已生成解答) + const ticketStatus = type === 'syslog' ? '待处理' : '已完成'; + const body = JSON.stringify({ parent: { database_id: dbId }, properties: { '标题': { title: [{ type: 'text', text: { content: title.slice(0, 120) } }] }, '操作类型': { select: { name: '其他' } }, '提交者': { rich_text: [{ type: 'text', text: { content: '铸渊Agent·自动管道' } }] }, - '状态': { select: { name: '待处理' } }, + '状态': { select: { name: ticketStatus } }, '优先级': { select: { name: 'P1' } } }, children: [{ diff --git a/.github/workflows/syslog-issue-pipeline.yml b/.github/workflows/syslog-issue-pipeline.yml index de09bac0..4acbc3b0 100644 --- a/.github/workflows/syslog-issue-pipeline.yml +++ b/.github/workflows/syslog-issue-pipeline.yml @@ -177,13 +177,17 @@ jobs: '验证详情: ' + moduleVerify.slice(0, 400), ].join('\\n'); + // SYSLOG类型工单设为'待处理'(等待Notion侧核心大脑处理) + // 提问类型工单设为'已完成'(铸渊已生成解答) + const ticketStatus = type === 'syslog' ? '待处理' : '已完成'; + const body = JSON.stringify({ parent: { database_id: dbId }, properties: { '标题': { title: [{ type: 'text', text: { content: title.slice(0, 120) } }] }, '操作类型': { select: { name: '其他' } }, '提交者': { rich_text: [{ type: 'text', text: { content: '铸渊Agent·自动管道' } }] }, - '状态': { select: { name: '待处理' } }, + '状态': { select: { name: ticketStatus } }, '优先级': { select: { name: 'P1' } } }, children: [{ diff --git a/scripts/verify-modules.js b/scripts/verify-modules.js index 807ec605..2df4ddf5 100644 --- a/scripts/verify-modules.js +++ b/scripts/verify-modules.js @@ -63,23 +63,32 @@ function extractModuleIds(content, broadcastId) { // ══════════════════════════════════════════════════════════ function extractDevId(content, broadcastId) { - // 从广播编号提取开发者后缀(如 BC-M22-009-AW → AW) - var suffixMatch = broadcastId.match(/BC-[A-Z0-9]+-\d+-([A-Z]+)/i); - var devSuffix = suffixMatch ? suffixMatch[1] : ''; - - // 从内容中提取 DEV-XXX + // 从内容中直接提取 DEV-XXX(最优先) var devMatch = content.match(/\b(DEV-\d{3})\b/i); if (devMatch) return devMatch[1].toUpperCase(); - // 从 dev-status.json 通过后缀查找 + // 从广播编号提取开发者后缀(如 BC-M22-009-AW → AW) + var suffixMatch = broadcastId.match(/BC-[A-Z0-9]+-\d+-([A-Z]+)/i); + var devSuffix = suffixMatch ? suffixMatch[1].toUpperCase() : ''; + + // 从 dev-status.json 通过后缀匹配开发者 if (devSuffix) { try { var devStatus = JSON.parse(fs.readFileSync(DEV_STATUS_PATH, 'utf8')); var team = devStatus.team || []; + // 后缀缩写映射表(从 dev-status.json 中名字的拼音首字母) + var suffixMap = { + 'YY': 'DEV-001', 'FM': 'DEV-002', 'YF': 'DEV-003', + 'ZZ': 'DEV-004', 'XCM': 'DEV-005', 'HE': 'DEV-009', + 'JZ': 'DEV-010', 'CCNN': 'DEV-011', 'AW': 'DEV-012', + 'XX': 'DEV-013', 'SY': 'DEV-014', + }; + if (suffixMap[devSuffix]) return suffixMap[devSuffix]; + + // 兜底:尝试从 team 中匹配 waiting 字段里的广播编号 for (var i = 0; i < team.length; i++) { - var name = team[i].name || ''; - // 简单匹配:用名字首字母缩写匹配后缀 - if (name && devSuffix.length >= 2) { + var waiting = team[i].waiting || ''; + if (waiting.includes(broadcastId) || waiting.includes(devSuffix)) { return team[i].dev_id; } } From e0dbe71bdc422eea859f1ec173c22e4f20ea92f9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 06:33:17 +0000 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20Phase=20B=20=E5=85=A8=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E9=97=AD=E7=8E=AF=E5=AF=B9=E6=8E=A5=20=C2=B7=20?= =?UTF-8?q?=E6=A0=87=E5=87=86=E5=8C=96=E5=B7=A5=E5=8D=95=20+=20=E4=BA=BA?= =?UTF-8?q?=E6=A0=BC=E4=BD=93=E8=B0=83=E7=94=A8=E7=AB=AF=E7=82=B9=20+=20?= =?UTF-8?q?=E5=BF=83=E8=B7=B3=E7=9B=91=E6=8E=A7=20+=20=E5=B9=BF=E6=92=AD?= =?UTF-8?q?=E6=8E=A8=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase B1: 标准化工单创建 (create-standardized-ticket.js + receive-syslog.js更新) Phase B2: 人格体调用端点 (invoke-persona.js + persona-invoke.yml workflow_dispatch) Phase B3: 心跳监控 (notion-heartbeat.js + notion-heartbeat.yml 每5分钟) Phase B4: 广播推送 (push-broadcast-to-github.js + broadcasts/ 目录) 工作流更新: syslog-issue-pipeline.yml, syslog-auto-pipeline.yml, notion-callback-pipeline.yml Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> --- .../workflows/notion-callback-pipeline.yml | 63 +++ .github/workflows/notion-heartbeat.yml | 44 +++ .github/workflows/persona-invoke.yml | 248 ++++++++++++ .github/workflows/syslog-auto-pipeline.yml | 100 +---- .github/workflows/syslog-issue-pipeline.yml | 100 +---- broadcasts/README.md | 22 ++ scripts/create-standardized-ticket.js | 197 ++++++++++ scripts/invoke-persona.js | 360 ++++++++++++++++++ scripts/notion-heartbeat.js | 355 +++++++++++++++++ scripts/push-broadcast-to-github.js | 179 +++++++++ scripts/receive-syslog.js | 26 +- 11 files changed, 1516 insertions(+), 178 deletions(-) create mode 100644 .github/workflows/notion-heartbeat.yml create mode 100644 .github/workflows/persona-invoke.yml create mode 100644 broadcasts/README.md create mode 100644 scripts/create-standardized-ticket.js create mode 100644 scripts/invoke-persona.js create mode 100644 scripts/notion-heartbeat.js create mode 100644 scripts/push-broadcast-to-github.js diff --git a/.github/workflows/notion-callback-pipeline.yml b/.github/workflows/notion-callback-pipeline.yml index a6fc2d39..463d7ced 100644 --- a/.github/workflows/notion-callback-pipeline.yml +++ b/.github/workflows/notion-callback-pipeline.yml @@ -109,6 +109,69 @@ jobs: FINGERPRINT_DB_ID: ${{ secrets.FINGERPRINT_DB_ID }} run: node scripts/wake-persona.js + - name: 📡 推送广播到 GitHub(Phase B4) + if: steps.persona.outcome == 'success' && steps.callback.outputs.status == 'approved' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BROADCAST_ID: ${{ steps.callback.outputs.broadcast_id }} + DEVELOPER_NAME: notion-callback + BROADCAST_CONTENT: ${{ steps.persona.outputs.result }} + run: node scripts/push-broadcast-to-github.js + + - name: 📝 回写 Notion 工单状态 + if: steps.callback.outputs.ticket_id + env: + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + TICKET_ID: ${{ steps.callback.outputs.ticket_id }} + CALLBACK_STATUS: ${{ steps.callback.outputs.status }} + run: | + node -e " + var https = require('https'); + var token = process.env.NOTION_TOKEN || ''; + var ticketId = process.env.TICKET_ID || ''; + var status = process.env.CALLBACK_STATUS || 'unknown'; + + if (!token || !ticketId) { + console.log('⚠️ 跳过工单状态更新'); + process.exit(0); + } + + var notionStatus = status === 'approved' ? '✅ 已完成' : '⚠️ 异常·等人工介入'; + var body = JSON.stringify({ + properties: { + '状态': { select: { name: notionStatus } } + } + }); + + var opts = { + hostname: 'api.notion.com', + port: 443, + path: '/v1/pages/' + ticketId, + method: 'PATCH', + headers: { + 'Authorization': 'Bearer ' + token, + 'Content-Type': 'application/json', + 'Notion-Version': '2022-06-28', + 'Content-Length': Buffer.byteLength(body) + } + }; + + var req = https.request(opts, function(res) { + var data = ''; + res.on('data', function(c) { data += c; }); + res.on('end', function() { + if (res.statusCode >= 200 && res.statusCode < 300) { + console.log('✅ 工单状态已更新为 ' + notionStatus); + } else { + console.log('⚠️ 工单更新失败: ' + res.statusCode); + } + }); + }); + req.on('error', function(e) { console.log('⚠️ ' + e.message); }); + req.write(body); + req.end(); + " + - name: 📧 发送最终确认邮件 env: SMTP_USER: ${{ secrets.SMTP_USER }} diff --git a/.github/workflows/notion-heartbeat.yml b/.github/workflows/notion-heartbeat.yml new file mode 100644 index 00000000..d7b5969a --- /dev/null +++ b/.github/workflows/notion-heartbeat.yml @@ -0,0 +1,44 @@ +name: Notion Heartbeat Monitor +# 💓 Phase B3 · 工单心跳监控 +# +# 每 5 分钟检测 Notion 工单队列中的待处理工单: +# - 超过 5 分钟未回执 → 重新触发人格体唤醒(最多 3 次) +# - 3 次重试仍无回执 → 标记工单异常 + 发邮件通知冰朔 +# +# 依赖 Secrets: +# NOTION_API_TOKEN Notion API token +# NOTION_TICKET_DB_ID 工单队列数据库 ID +# SMTP_USER 邮件发送者 +# SMTP_PASS 邮件授权码 + +on: + schedule: + - cron: '*/5 * * * *' + workflow_dispatch: + +jobs: + heartbeat: + name: 💓 工单心跳检测 + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm ci --ignore-scripts + + - name: 💓 执行心跳检测 + env: + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + NOTION_TICKET_DB_ID: ${{ secrets.NOTION_TICKET_DB_ID }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SMTP_USER: ${{ secrets.SMTP_USER }} + SMTP_PASS: ${{ secrets.SMTP_PASS }} + ALERT_EMAIL: '565183519@qq.com' + run: node scripts/notion-heartbeat.js diff --git a/.github/workflows/persona-invoke.yml b/.github/workflows/persona-invoke.yml new file mode 100644 index 00000000..e2ab5d45 --- /dev/null +++ b/.github/workflows/persona-invoke.yml @@ -0,0 +1,248 @@ +name: Persona Invoke Endpoint +# 🔗 Phase B2 · Notion Agent → 铸渊人格体唤醒 +# +# Notion Agent 通过 workflow_dispatch 触发此工作流, +# 等效于 POST /api/persona/invoke 接口。 +# +# 完整流程: +# ① Notion Agent 发送 workflow_dispatch(传入工单信息) +# ② 铸渊 Agent 从 Notion 读取工单内容 +# ③ 唤醒人格体(Claude API)处理 SYSLOG +# ④ 处理结果写回 Notion 工单(receipt_status = completed) +# ⑤ 广播文件推送到 GitHub 仓库 broadcasts/{dev_id}/ +# +# 依赖 Secrets: +# LLM_API_KEY 第三方 LLM 平台密钥 +# LLM_BASE_URL 第三方 LLM 平台 API 地址 +# NOTION_API_TOKEN Notion API token +# NOTION_TICKET_DB_ID 工单队列数据库 ID +# CORE_BRAIN_PAGE_ID 曜冥核心大脑 v4.0 页面 ID +# PORTRAIT_DB_ID 开发者动态画像库 ID +# FINGERPRINT_DB_ID 模块指纹注册表 ID +# INVOKE_API_KEY 调用鉴权密钥 + +on: + workflow_dispatch: + inputs: + work_order_id: + description: 'Notion 工单页面 ID' + required: true + type: string + task_id: + description: '广播编号(如 BC-M23-001-AW)' + required: true + type: string + developer: + description: '开发者信息(如 DEV-012 Awen)' + required: false + default: 'unknown' + type: string + syslog_raw: + description: 'SYSLOG JSON 原文(可选,如为空则从 Notion 读取)' + required: false + default: '' + type: string + action: + description: '动作类型' + required: false + default: 'process_syslog' + type: string + +jobs: + invoke: + name: 🔗 唤醒人格体处理 SYSLOG + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm ci --ignore-scripts + + - name: 🔍 模块验证 + id: verify + env: + SYSLOG_CONTENT: ${{ inputs.syslog_raw }} + BROADCAST_ID: ${{ inputs.task_id }} + AUTHOR: ${{ inputs.developer }} + run: node scripts/verify-modules.js + + - name: 🧠 唤醒人格体 + id: persona + env: + LLM_API_KEY: ${{ secrets.LLM_API_KEY }} + LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }} + BROADCAST_ID: ${{ inputs.task_id }} + SUBMIT_TYPE: syslog + SUBMIT_CONTENT: ${{ inputs.syslog_raw }} + AUTHOR: ${{ inputs.developer }} + MODULE_VERIFY_RESULT: ${{ steps.verify.outputs.verify_report }} + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + CORE_BRAIN_PAGE_ID: ${{ secrets.CORE_BRAIN_PAGE_ID }} + PORTRAIT_DB_ID: ${{ secrets.PORTRAIT_DB_ID }} + FINGERPRINT_DB_ID: ${{ secrets.FINGERPRINT_DB_ID }} + run: node scripts/wake-persona.js + + - name: 📝 回写 Notion 工单 + if: always() + env: + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + WORK_ORDER_ID: ${{ inputs.work_order_id }} + PERSONA_RESULT: ${{ steps.persona.outputs.result }} + TASK_ID: ${{ inputs.task_id }} + run: | + node -e " + var https = require('https'); + + var token = process.env.NOTION_TOKEN || ''; + var workOrderId = process.env.WORK_ORDER_ID || ''; + var result = process.env.PERSONA_RESULT || '(no result)'; + var taskId = process.env.TASK_ID || ''; + + if (!token || !workOrderId) { + console.log('⚠️ 缺少 Notion 配置,跳过回写'); + process.exit(0); + } + + // 更新工单状态为 ✅ 已完成 + var body = JSON.stringify({ + properties: { + '状态': { select: { name: '✅ 已完成' } } + } + }); + + var opts = { + hostname: 'api.notion.com', + port: 443, + path: '/v1/pages/' + workOrderId, + method: 'PATCH', + headers: { + 'Authorization': 'Bearer ' + token, + 'Content-Type': 'application/json', + 'Notion-Version': '2022-06-28', + 'Content-Length': Buffer.byteLength(body) + } + }; + + var req = https.request(opts, function(res) { + var data = ''; + res.on('data', function(c) { data += c; }); + res.on('end', function() { + if (res.statusCode >= 200 && res.statusCode < 300) { + console.log('✅ 工单状态已更新为 ✅ 已完成'); + } else { + console.log('⚠️ 工单状态更新失败: ' + res.statusCode); + } + }); + }); + req.on('error', function(e) { console.log('⚠️ ' + e.message); }); + req.write(body); + req.end(); + + // 追加处理结果到工单 + var appendBody = JSON.stringify({ + children: [{ + object: 'block', + type: 'heading_2', + heading_2: { + rich_text: [{ type: 'text', text: { content: '🧠 人格体处理结果 · receipt_status: completed' } }] + } + }, { + object: 'block', + type: 'paragraph', + paragraph: { + rich_text: [{ type: 'text', text: { content: result.slice(0, 2000) } }] + } + }] + }); + + var appendOpts = { + hostname: 'api.notion.com', + port: 443, + path: '/v1/blocks/' + workOrderId + '/children', + method: 'PATCH', + headers: { + 'Authorization': 'Bearer ' + token, + 'Content-Type': 'application/json', + 'Notion-Version': '2022-06-28', + 'Content-Length': Buffer.byteLength(appendBody) + } + }; + + var req2 = https.request(appendOpts, function(res) { + var data = ''; + res.on('data', function(c) { data += c; }); + res.on('end', function() { + if (res.statusCode >= 200 && res.statusCode < 300) { + console.log('✅ 处理结果已追加到工单'); + } else { + console.log('⚠️ 结果追加失败: ' + res.statusCode); + } + }); + }); + req2.on('error', function(e) { console.log('⚠️ ' + e.message); }); + req2.write(appendBody); + req2.end(); + " + + - name: 📡 推送广播到 GitHub + if: steps.persona.outcome == 'success' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BROADCAST_ID: ${{ inputs.task_id }} + DEVELOPER_ID: '' + DEVELOPER_NAME: ${{ inputs.developer }} + BROADCAST_CONTENT: ${{ steps.persona.outputs.result }} + run: node scripts/push-broadcast-to-github.js + + - name: 🔴 失败处理 + if: failure() + env: + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + WORK_ORDER_ID: ${{ inputs.work_order_id }} + run: | + node -e " + var https = require('https'); + var token = process.env.NOTION_TOKEN || ''; + var workOrderId = process.env.WORK_ORDER_ID || ''; + + if (!token || !workOrderId) process.exit(0); + + var body = JSON.stringify({ + properties: { + '状态': { select: { name: '⚠️ 异常·等人工介入' } } + } + }); + + var opts = { + hostname: 'api.notion.com', + port: 443, + path: '/v1/pages/' + workOrderId, + method: 'PATCH', + headers: { + 'Authorization': 'Bearer ' + token, + 'Content-Type': 'application/json', + 'Notion-Version': '2022-06-28', + 'Content-Length': Buffer.byteLength(body) + } + }; + + var req = https.request(opts, function(res) { + var data = ''; + res.on('data', function(c) { data += c; }); + res.on('end', function() { + console.log('工单状态已标记为 ⚠️ 异常'); + }); + }); + req.on('error', function(e) { console.log(e.message); }); + req.write(body); + req.end(); + " diff --git a/.github/workflows/syslog-auto-pipeline.yml b/.github/workflows/syslog-auto-pipeline.yml index 02eb3e5e..a0d84b10 100644 --- a/.github/workflows/syslog-auto-pipeline.yml +++ b/.github/workflows/syslog-auto-pipeline.yml @@ -130,7 +130,8 @@ jobs: FINGERPRINT_DB_ID: ${{ secrets.FINGERPRINT_DB_ID }} run: node scripts/wake-persona.js - - name: 📋 Write to Notion ticket + - name: 📋 创建标准化 Notion 工单(Phase B1) + id: ticket if: ${{ secrets.NOTION_API_TOKEN }} env: NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} @@ -140,93 +141,18 @@ jobs: PERSONA_RESULT: ${{ steps.persona.outputs.result }} MODULE_VERIFY: ${{ steps.verify.outputs.verify_result }} MODULES_UPLOADED: ${{ steps.verify.outputs.modules_uploaded }} - AUTHOR: ${{ steps.parse.outputs.author }} - run: | - node -e " - const https = require('https'); + DEVELOPER: ${{ steps.parse.outputs.author }} + SYSLOG_RAW: ${{ steps.parse.outputs.content }} + run: node scripts/create-standardized-ticket.js - const token = process.env.NOTION_TOKEN; - const dbId = process.env.NOTION_TICKET_DB_ID; - const broadcastId = process.env.BROADCAST_ID || 'UNKNOWN'; - const type = process.env.SUBMIT_TYPE || 'syslog'; - const result = process.env.PERSONA_RESULT || '(no result)'; - const modulesUploaded = process.env.MODULES_UPLOADED || 'false'; - const moduleVerify = process.env.MODULE_VERIFY || '{}'; - const author = process.env.AUTHOR || 'unknown'; - - if (!token || !dbId) { - console.log('⚠️ Notion credentials not configured, skipping ticket creation'); - process.exit(0); - } - - const typeLabel = type === 'syslog' ? 'SYSLOG闭环' : '提问解答'; - const verifyStatus = modulesUploaded === 'true' ? '模块已验证✅' : '模块待验证⚠️'; - const title = '[自动] ' + broadcastId + ' · ' + typeLabel + ' · ' + verifyStatus; - - // 构建工单内容:包含人格体处理结果 + 模块验证数据 - const ticketContent = [ - '## 铸渊核心大脑处理结果', - '', - result.slice(0, 1500), - '', - '## 模块上传验证', - '', - '模块全部上传: ' + (modulesUploaded === 'true' ? '✅ 是' : '❌ 否'), - '提交者: ' + author, - '', - '验证详情: ' + moduleVerify.slice(0, 400), - ].join('\\n'); - - // SYSLOG类型工单设为'待处理'(等待Notion侧核心大脑处理) - // 提问类型工单设为'已完成'(铸渊已生成解答) - const ticketStatus = type === 'syslog' ? '待处理' : '已完成'; - - const body = JSON.stringify({ - parent: { database_id: dbId }, - properties: { - '标题': { title: [{ type: 'text', text: { content: title.slice(0, 120) } }] }, - '操作类型': { select: { name: '其他' } }, - '提交者': { rich_text: [{ type: 'text', text: { content: '铸渊Agent·自动管道' } }] }, - '状态': { select: { name: ticketStatus } }, - '优先级': { select: { name: 'P1' } } - }, - children: [{ - object: 'block', - type: 'paragraph', - paragraph: { - rich_text: [{ type: 'text', text: { content: ticketContent.slice(0, 2000) } }] - } - }] - }); - - const opts = { - hostname: 'api.notion.com', - port: 443, - path: '/v1/pages', - method: 'POST', - headers: { - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', - 'Notion-Version': '2022-06-28', - 'Content-Length': Buffer.byteLength(body) - } - }; - - const req = https.request(opts, (res) => { - let data = ''; - res.on('data', (c) => data += c); - res.on('end', () => { - if (res.statusCode >= 200 && res.statusCode < 300) { - console.log('✅ Notion 工单已创建'); - } else { - console.log('⚠️ Notion 工单创建失败: ' + res.statusCode + ' ' + data); - } - }); - }); - req.on('error', (e) => console.log('⚠️ Notion 请求失败: ' + e.message)); - req.write(body); - req.end(); - " + - name: 📡 推送广播到 GitHub(Phase B4) + if: steps.persona.outcome == 'success' && steps.parse.outputs.type == 'syslog' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BROADCAST_ID: ${{ steps.parse.outputs.broadcast_id }} + DEVELOPER_NAME: ${{ steps.parse.outputs.author }} + BROADCAST_CONTENT: ${{ steps.persona.outputs.result }} + run: node scripts/push-broadcast-to-github.js - name: 📧 Send email to developer env: diff --git a/.github/workflows/syslog-issue-pipeline.yml b/.github/workflows/syslog-issue-pipeline.yml index 4acbc3b0..27ac9cfc 100644 --- a/.github/workflows/syslog-issue-pipeline.yml +++ b/.github/workflows/syslog-issue-pipeline.yml @@ -130,7 +130,8 @@ jobs: FINGERPRINT_DB_ID: ${{ secrets.FINGERPRINT_DB_ID }} run: node scripts/wake-persona.js - - name: 📋 Write to Notion ticket + - name: 📋 创建标准化 Notion 工单(Phase B1) + id: ticket if: ${{ secrets.NOTION_API_TOKEN }} env: NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} @@ -140,93 +141,18 @@ jobs: PERSONA_RESULT: ${{ steps.persona.outputs.result }} MODULE_VERIFY: ${{ steps.verify.outputs.verify_result }} MODULES_UPLOADED: ${{ steps.verify.outputs.modules_uploaded }} - AUTHOR: ${{ steps.parse.outputs.author }} - run: | - node -e " - const https = require('https'); + DEVELOPER: ${{ steps.parse.outputs.author }} + SYSLOG_RAW: ${{ steps.parse.outputs.content }} + run: node scripts/create-standardized-ticket.js - const token = process.env.NOTION_TOKEN; - const dbId = process.env.NOTION_TICKET_DB_ID; - const broadcastId = process.env.BROADCAST_ID || 'UNKNOWN'; - const type = process.env.SUBMIT_TYPE || 'syslog'; - const result = process.env.PERSONA_RESULT || '(no result)'; - const modulesUploaded = process.env.MODULES_UPLOADED || 'false'; - const moduleVerify = process.env.MODULE_VERIFY || '{}'; - const author = process.env.AUTHOR || 'unknown'; - - if (!token || !dbId) { - console.log('⚠️ Notion credentials not configured, skipping ticket creation'); - process.exit(0); - } - - const typeLabel = type === 'syslog' ? 'SYSLOG闭环' : '提问解答'; - const verifyStatus = modulesUploaded === 'true' ? '模块已验证✅' : '模块待验证⚠️'; - const title = '[自动] ' + broadcastId + ' · ' + typeLabel + ' · ' + verifyStatus; - - // 构建工单内容:包含人格体处理结果 + 模块验证数据 - const ticketContent = [ - '## 铸渊核心大脑处理结果', - '', - result.slice(0, 1500), - '', - '## 模块上传验证', - '', - '模块全部上传: ' + (modulesUploaded === 'true' ? '✅ 是' : '❌ 否'), - '提交者: ' + author, - '', - '验证详情: ' + moduleVerify.slice(0, 400), - ].join('\\n'); - - // SYSLOG类型工单设为'待处理'(等待Notion侧核心大脑处理) - // 提问类型工单设为'已完成'(铸渊已生成解答) - const ticketStatus = type === 'syslog' ? '待处理' : '已完成'; - - const body = JSON.stringify({ - parent: { database_id: dbId }, - properties: { - '标题': { title: [{ type: 'text', text: { content: title.slice(0, 120) } }] }, - '操作类型': { select: { name: '其他' } }, - '提交者': { rich_text: [{ type: 'text', text: { content: '铸渊Agent·自动管道' } }] }, - '状态': { select: { name: ticketStatus } }, - '优先级': { select: { name: 'P1' } } - }, - children: [{ - object: 'block', - type: 'paragraph', - paragraph: { - rich_text: [{ type: 'text', text: { content: ticketContent.slice(0, 2000) } }] - } - }] - }); - - const opts = { - hostname: 'api.notion.com', - port: 443, - path: '/v1/pages', - method: 'POST', - headers: { - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', - 'Notion-Version': '2022-06-28', - 'Content-Length': Buffer.byteLength(body) - } - }; - - const req = https.request(opts, (res) => { - let data = ''; - res.on('data', (c) => data += c); - res.on('end', () => { - if (res.statusCode >= 200 && res.statusCode < 300) { - console.log('✅ Notion 工单已创建'); - } else { - console.log('⚠️ Notion 工单创建失败: ' + res.statusCode + ' ' + data); - } - }); - }); - req.on('error', (e) => console.log('⚠️ Notion 请求失败: ' + e.message)); - req.write(body); - req.end(); - " + - name: 📡 推送广播到 GitHub(Phase B4) + if: steps.persona.outcome == 'success' && steps.parse.outputs.type == 'syslog' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BROADCAST_ID: ${{ steps.parse.outputs.broadcast_id }} + DEVELOPER_NAME: ${{ steps.parse.outputs.author }} + BROADCAST_CONTENT: ${{ steps.persona.outputs.result }} + run: node scripts/push-broadcast-to-github.js - name: 📧 Send email to developer env: diff --git a/broadcasts/README.md b/broadcasts/README.md new file mode 100644 index 00000000..a1d6d7bf --- /dev/null +++ b/broadcasts/README.md @@ -0,0 +1,22 @@ +# 📡 broadcasts/ + +自动广播归档目录。 + +铸渊 Agent 处理完 SYSLOG 后,会自动将新广播文件推送到此目录: + +``` +broadcasts/ + DEV-001/ + BC-M14-001-YY.md + DEV-012/ + BC-M22-009-AW.md + BC-M23-001-AW.md + ... +``` + +**推送规范**: +- 路径:`broadcasts/{developer_id}/{taskId}.md` +- 提交信息:`[AutoBroadcast] {taskId} · {开发者名} · {模块名}环节{N}` +- 推送方式:通过 GitHub API(`PUT /repos/.../contents/...`) + +**不要手动编辑此目录**,由铸渊 Agent 自动管理。 diff --git a/scripts/create-standardized-ticket.js b/scripts/create-standardized-ticket.js new file mode 100644 index 00000000..a1607245 --- /dev/null +++ b/scripts/create-standardized-ticket.js @@ -0,0 +1,197 @@ +// scripts/create-standardized-ticket.js +// 铸渊 · Phase B1 · 标准化工单创建脚本 +// +// 将 SYSLOG 写入 Notion 工单队列时使用标准化格式, +// 让 Notion Agent 能够识别和触发后续处理。 +// +// 环境变量: +// NOTION_TOKEN Notion API token +// NOTION_TICKET_DB_ID 工单队列数据库 ID +// BROADCAST_ID 广播编号(如 BC-M23-001-AW) +// DEVELOPER 开发者信息(如 "DEV-012 Awen") +// SYSLOG_RAW 完整 SYSLOG 原文(JSON 字符串或纯文本) +// SUBMIT_TYPE 提交类型(syslog / question) +// PERSONA_RESULT 人格体处理结果(可选,首次可为空) +// MODULE_VERIFY 模块验证结果 JSON(可选) +// MODULES_UPLOADED 模块是否全部上传(true/false) + +'use strict'; + +const https = require('https'); + +const NOTION_TOKEN = process.env.NOTION_TOKEN || ''; +const NOTION_TICKET_DB_ID = process.env.NOTION_TICKET_DB_ID || ''; +const BROADCAST_ID = process.env.BROADCAST_ID || 'UNKNOWN'; +const DEVELOPER = process.env.DEVELOPER || process.env.AUTHOR || 'unknown'; +const SYSLOG_RAW = process.env.SYSLOG_RAW || process.env.SUBMIT_CONTENT || ''; +const SUBMIT_TYPE = process.env.SUBMIT_TYPE || 'syslog'; +const PERSONA_RESULT = process.env.PERSONA_RESULT || ''; +const MODULE_VERIFY = process.env.MODULE_VERIFY || ''; +const MODULES_UPLOADED = process.env.MODULES_UPLOADED || 'false'; + +const NOTION_VERSION = '2022-06-28'; +const NOTION_API_HOSTNAME = 'api.notion.com'; +const NOTION_RICH_TEXT_MAX = 2000; + +// ══════════════════════════════════════════════════════════ +// Notion API 工具 +// ══════════════════════════════════════════════════════════ + +function notionPost(endpoint, body) { + return new Promise(function (resolve, reject) { + var payload = JSON.stringify(body); + var opts = { + hostname: NOTION_API_HOSTNAME, + port: 443, + path: endpoint, + method: 'POST', + headers: { + 'Authorization': 'Bearer ' + NOTION_TOKEN, + 'Content-Type': 'application/json', + 'Notion-Version': NOTION_VERSION, + 'Content-Length': Buffer.byteLength(payload), + }, + }; + var req = https.request(opts, function (res) { + var data = ''; + res.on('data', function (chunk) { data += chunk; }); + res.on('end', function () { + try { + var parsed = JSON.parse(data); + if (res.statusCode >= 200 && res.statusCode < 300) { + resolve(parsed); + } else { + reject(new Error('Notion API ' + res.statusCode + ': ' + (parsed.message || data))); + } + } catch (e) { + reject(new Error('Notion API parse error: ' + data)); + } + }); + }); + req.on('error', reject); + req.write(payload); + req.end(); + }); +} + +function richTextChunks(content) { + var str = String(content || ''); + var chunks = []; + for (var i = 0; i < str.length; i += NOTION_RICH_TEXT_MAX) { + chunks.push({ type: 'text', text: { content: str.slice(i, i + NOTION_RICH_TEXT_MAX) } }); + } + if (chunks.length === 0) { + chunks.push({ type: 'text', text: { content: '' } }); + } + return chunks; +} + +// ══════════════════════════════════════════════════════════ +// 标准化工单创建 +// ══════════════════════════════════════════════════════════ + +async function createStandardizedTicket() { + if (!NOTION_TOKEN || !NOTION_TICKET_DB_ID) { + console.log('⚠️ Notion credentials not configured, skipping ticket creation'); + process.exit(0); + } + + var now = new Date().toISOString(); + var typeLabel = SUBMIT_TYPE === 'syslog' ? 'SYSLOG处理' : '提问解答'; + var ticketStatus = SUBMIT_TYPE === 'syslog' ? '待处理' : '已完成'; + var verifyStatus = MODULES_UPLOADED === 'true' ? '模块已验证✅' : '模块待验证⚠️'; + var title = '[自动] ' + BROADCAST_ID + ' · ' + typeLabel + ' · ' + verifyStatus; + + // Phase B1 标准化字段 — 工单内容 + var ticketContent = [ + '## 📡 标准化工单 · Phase B1', + '', + '| 字段 | 值 |', + '|------|-----|', + '| 工单类型 | ' + typeLabel + ' |', + '| 状态 | ' + ticketStatus + ' |', + '| 来源 | GitHub Actions |', + '| taskId | ' + BROADCAST_ID + ' |', + '| developer | ' + DEVELOPER + ' |', + '| created_at | ' + now + ' |', + '| retry_count | 0 |', + '| receipt_status | pending |', + '', + ]; + + if (PERSONA_RESULT) { + ticketContent.push('## 铸渊核心大脑处理结果'); + ticketContent.push(''); + ticketContent.push(PERSONA_RESULT.slice(0, 1500)); + ticketContent.push(''); + } + + ticketContent.push('## 模块上传验证'); + ticketContent.push(''); + ticketContent.push('模块全部上传: ' + (MODULES_UPLOADED === 'true' ? '✅ 是' : '❌ 否')); + ticketContent.push(''); + + if (MODULE_VERIFY) { + ticketContent.push('验证详情: ' + MODULE_VERIFY.slice(0, 400)); + ticketContent.push(''); + } + + var contentText = ticketContent.join('\n'); + + // 构建 Notion 页面 + var body = { + parent: { database_id: NOTION_TICKET_DB_ID }, + properties: { + '标题': { title: [{ type: 'text', text: { content: title.slice(0, 120) } }] }, + '操作类型': { select: { name: '其他' } }, + '提交者': { rich_text: [{ type: 'text', text: { content: '铸渊Agent·自动管道' } }] }, + '状态': { select: { name: ticketStatus } }, + '优先级': { select: { name: 'P1' } }, + }, + children: [ + { + object: 'block', + type: 'paragraph', + paragraph: { + rich_text: richTextChunks(contentText), + }, + }, + ], + }; + + // 将 SYSLOG 原文作为代码块附加(如果有) + if (SYSLOG_RAW) { + body.children.push({ + object: 'block', + type: 'code', + code: { + rich_text: richTextChunks(SYSLOG_RAW.slice(0, 8000)), + language: 'json', + }, + }); + } + + console.log('📋 创建标准化工单...'); + console.log(' taskId: ' + BROADCAST_ID); + console.log(' developer: ' + DEVELOPER); + console.log(' type: ' + typeLabel); + console.log(' receipt_status: pending'); + + try { + var result = await notionPost('/v1/pages', body); + console.log('✅ Notion 标准化工单已创建: ' + result.id); + + // 输出工单 ID 到 GITHUB_OUTPUT + var outputFile = process.env.GITHUB_OUTPUT; + if (outputFile) { + var fs = require('fs'); + fs.appendFileSync(outputFile, 'ticket_page_id=' + result.id + '\n'); + fs.appendFileSync(outputFile, 'ticket_url=' + (result.url || '') + '\n'); + } + } catch (err) { + console.error('❌ 工单创建失败: ' + err.message); + process.exit(1); + } +} + +createStandardizedTicket(); diff --git a/scripts/invoke-persona.js b/scripts/invoke-persona.js new file mode 100644 index 00000000..bd4cc12e --- /dev/null +++ b/scripts/invoke-persona.js @@ -0,0 +1,360 @@ +// scripts/invoke-persona.js +// 铸渊 · Phase B2 · 人格体唤醒调用脚本 +// +// Notion Agent 调用此脚本(通过 workflow_dispatch), +// 脚本读取 Notion 工单内容 → 唤醒人格体 → 处理结果写回 Notion。 +// +// 环境变量: +// WORK_ORDER_ID Notion 工单页面 ID +// TASK_ID 广播编号(如 BC-M23-001-AW) +// DEVELOPER 开发者信息(如 "DEV-012 Awen") +// SYSLOG_RAW SYSLOG JSON 原文 +// ACTION 动作类型(process_syslog / retry) +// LLM_API_KEY 第三方 LLM 平台密钥 +// LLM_BASE_URL 第三方 LLM 平台 API 地址 +// NOTION_TOKEN Notion API token +// CORE_BRAIN_PAGE_ID 曜冥核心大脑 v4.0 页面 ID +// PORTRAIT_DB_ID 开发者动态画像库 ID +// FINGERPRINT_DB_ID 模块指纹注册表 ID +// INVOKE_API_KEY 鉴权密钥(用于 API 调用验证) + +'use strict'; + +const https = require('https'); +const http = require('http'); +const fs = require('fs'); +const path = require('path'); + +// ══════════════════════════════════════════════════════════ +// 配置 +// ══════════════════════════════════════════════════════════ + +var WORK_ORDER_ID = process.env.WORK_ORDER_ID || ''; +var TASK_ID = process.env.TASK_ID || process.env.BROADCAST_ID || 'UNKNOWN'; +var DEVELOPER = process.env.DEVELOPER || 'unknown'; +var SYSLOG_RAW = process.env.SYSLOG_RAW || ''; +var ACTION = process.env.ACTION || 'process_syslog'; + +var LLM_API_KEY = process.env.LLM_API_KEY || ''; +var LLM_BASE_URL = (process.env.LLM_BASE_URL || '').replace(/\/+$/, ''); +var NOTION_TOKEN = process.env.NOTION_TOKEN || ''; +var CORE_BRAIN_PAGE_ID = process.env.CORE_BRAIN_PAGE_ID || ''; +var PORTRAIT_DB_ID = process.env.PORTRAIT_DB_ID || ''; +var FINGERPRINT_DB_ID = process.env.FINGERPRINT_DB_ID || ''; + +var NOTION_VERSION = '2022-06-28'; +var NOTION_API_HOSTNAME = 'api.notion.com'; + +// ══════════════════════════════════════════════════════════ +// HTTP 工具 +// ══════════════════════════════════════════════════════════ + +function httpRequest(url, options, body) { + return new Promise(function (resolve, reject) { + var parsed = new URL(url); + var isHttps = parsed.protocol === 'https:'; + var mod = isHttps ? https : http; + + var opts = { + hostname: parsed.hostname, + port: parsed.port || (isHttps ? 443 : 80), + path: parsed.pathname + parsed.search, + method: options.method || 'GET', + headers: options.headers || {}, + timeout: options.timeout || 120000, + }; + + var req = mod.request(opts, function (res) { + var data = ''; + res.on('data', function (chunk) { data += chunk; }); + res.on('end', function () { + resolve({ status: res.statusCode, body: data }); + }); + }); + + req.on('error', reject); + req.on('timeout', function () { req.destroy(); reject(new Error('Request timeout')); }); + + if (body) { + req.write(typeof body === 'string' ? body : JSON.stringify(body)); + } + req.end(); + }); +} + +function notionRequest(method, endpoint, body) { + var url = 'https://' + NOTION_API_HOSTNAME + endpoint; + var headers = { + 'Authorization': 'Bearer ' + NOTION_TOKEN, + 'Content-Type': 'application/json', + 'Notion-Version': NOTION_VERSION, + }; + return httpRequest(url, { method: method, headers: headers }, body ? JSON.stringify(body) : null) + .then(function (res) { + var parsed = JSON.parse(res.body); + if (res.status >= 200 && res.status < 300) return parsed; + throw new Error('Notion API ' + res.status + ': ' + (parsed.message || res.body)); + }); +} + +// ══════════════════════════════════════════════════════════ +// Step 1: 读取工单内容(从 Notion) +// ══════════════════════════════════════════════════════════ + +async function readWorkOrder() { + if (!WORK_ORDER_ID) { + console.log('ℹ️ No WORK_ORDER_ID, using SYSLOG_RAW directly'); + return { syslog_raw: SYSLOG_RAW, taskId: TASK_ID, developer: DEVELOPER }; + } + + console.log('📖 读取 Notion 工单: ' + WORK_ORDER_ID); + try { + var page = await notionRequest('GET', '/v1/pages/' + WORK_ORDER_ID); + var props = page.properties || {}; + + // 提取工单字段 + var taskId = TASK_ID; + var developer = DEVELOPER; + + // 尝试从工单属性中读取 + if (props['广播编号'] && props['广播编号'].rich_text) { + var bcText = props['广播编号'].rich_text.map(function (t) { return t.plain_text || ''; }).join(''); + if (bcText) taskId = bcText; + } + if (props['开发者编号'] && props['开发者编号'].rich_text) { + var devText = props['开发者编号'].rich_text.map(function (t) { return t.plain_text || ''; }).join(''); + if (devText) developer = devText; + } + + // 读取页面内容(子块)来获取 SYSLOG 原文 + var blocks = await notionRequest('GET', '/v1/blocks/' + WORK_ORDER_ID + '/children?page_size=20'); + var syslogRaw = SYSLOG_RAW; + + if (blocks.results) { + blocks.results.forEach(function (block) { + if (block.type === 'code' && block.code && block.code.rich_text) { + var codeText = block.code.rich_text.map(function (t) { return t.plain_text || ''; }).join(''); + if (codeText.length > syslogRaw.length) { + syslogRaw = codeText; + } + } + }); + } + + console.log(' → taskId: ' + taskId); + console.log(' → developer: ' + developer); + console.log(' → syslog_raw length: ' + syslogRaw.length); + + return { syslog_raw: syslogRaw, taskId: taskId, developer: developer }; + } catch (err) { + console.log('⚠️ 工单读取失败: ' + err.message + ',使用环境变量'); + return { syslog_raw: SYSLOG_RAW, taskId: TASK_ID, developer: DEVELOPER }; + } +} + +// ══════════════════════════════════════════════════════════ +// Step 2: 唤醒人格体(复用 wake-persona.js 的逻辑) +// ══════════════════════════════════════════════════════════ + +async function invokePersona(workOrder) { + console.log('🧠 唤醒人格体处理 SYSLOG...'); + + // 设置环境变量供 wake-persona.js 读取 + process.env.BROADCAST_ID = workOrder.taskId; + process.env.SUBMIT_TYPE = 'syslog'; + process.env.SUBMIT_CONTENT = workOrder.syslog_raw; + process.env.AUTHOR = workOrder.developer; + + // 执行 wake-persona.js(fork 子进程) + var childProcess = require('child_process'); + var wakeScript = path.join(__dirname, 'wake-persona.js'); + + return new Promise(function (resolve, reject) { + var child = childProcess.fork(wakeScript, [], { + env: Object.assign({}, process.env, { + BROADCAST_ID: workOrder.taskId, + SUBMIT_TYPE: 'syslog', + SUBMIT_CONTENT: workOrder.syslog_raw, + AUTHOR: workOrder.developer, + LLM_API_KEY: LLM_API_KEY, + LLM_BASE_URL: LLM_BASE_URL, + NOTION_TOKEN: NOTION_TOKEN, + CORE_BRAIN_PAGE_ID: CORE_BRAIN_PAGE_ID, + PORTRAIT_DB_ID: PORTRAIT_DB_ID, + FINGERPRINT_DB_ID: FINGERPRINT_DB_ID, + }), + stdio: ['pipe', 'pipe', 'pipe', 'ipc'], + }); + + var stdout = ''; + var stderr = ''; + child.stdout.on('data', function (d) { stdout += d; process.stdout.write(d); }); + child.stderr.on('data', function (d) { stderr += d; process.stderr.write(d); }); + + child.on('exit', function (code) { + if (code === 0) { + // 从 GITHUB_OUTPUT 文件读取 result(如果存在) + var outputFile = process.env.GITHUB_OUTPUT; + var result = ''; + if (outputFile && fs.existsSync(outputFile)) { + var outputContent = fs.readFileSync(outputFile, 'utf8'); + var resultMatch = outputContent.match(/result<= 200 && res.status < 300) { + var tickets = parsed.results || []; + console.log(' → 找到 ' + tickets.length + ' 个待处理工单'); + return tickets; + } else { + console.log('⚠️ 查询失败: ' + (parsed.message || res.body)); + return []; + } + } catch (err) { + console.log('⚠️ 查询异常: ' + err.message); + return []; + } +} + +// ══════════════════════════════════════════════════════════ +// Step 2: 检测超时工单 +// ══════════════════════════════════════════════════════════ + +function filterTimedOutTickets(tickets) { + var now = Date.now(); + var timedOut = []; + + tickets.forEach(function (ticket) { + var createdTime = new Date(ticket.created_time).getTime(); + var age = now - createdTime; + + // 只处理来自 "铸渊Agent·自动管道" 的 SYSLOG 工单 + var props = ticket.properties || {}; + var submitter = ''; + if (props['提交者'] && props['提交者'].rich_text) { + submitter = props['提交者'].rich_text.map(function (t) { return t.plain_text || ''; }).join(''); + } + + // 只对自动管道创建的工单做心跳检测 + if (!submitter.includes('铸渊Agent') && !submitter.includes('自动管道')) { + return; + } + + // 超过 5 分钟未完成 + if (age > HEARTBEAT_TIMEOUT_MS) { + var title = ''; + if (props['标题'] && props['标题'].title) { + title = props['标题'].title.map(function (t) { return t.plain_text || ''; }).join(''); + } + + timedOut.push({ + id: ticket.id, + title: title, + createdTime: ticket.created_time, + ageMinutes: Math.round(age / 60000), + }); + } + }); + + console.log(' → ' + timedOut.length + ' 个工单超时(> 5 分钟)'); + return timedOut; +} + +// ══════════════════════════════════════════════════════════ +// Step 3: 触发重试(通过 workflow_dispatch) +// ══════════════════════════════════════════════════════════ + +async function triggerRetry(ticket) { + if (!GITHUB_TOKEN) { + console.log('⚠️ 缺少 GITHUB_TOKEN,无法触发重试'); + return false; + } + + console.log('🔄 重试唤醒: ' + ticket.title + ' (超时 ' + ticket.ageMinutes + ' 分钟)'); + + // 从工单标题提取 broadcast_id + var bcMatch = (ticket.title || '').match(/BC-[A-Z0-9]+-\d+-[A-Z]+/i); + var broadcastId = bcMatch ? bcMatch[0] : 'UNKNOWN'; + + try { + var res = await httpsPost('api.github.com', + '/repos/' + REPO_OWNER + '/' + REPO_NAME + '/actions/workflows/persona-invoke.yml/dispatches', + { + ref: 'main', + inputs: { + work_order_id: ticket.id, + task_id: broadcastId, + developer: 'heartbeat-retry', + action: 'retry', + }, + }, + { + 'Authorization': 'Bearer ' + GITHUB_TOKEN, + 'Accept': 'application/vnd.github+json', + 'User-Agent': 'ZhuyuanHeartbeat/1.0', + 'X-GitHub-Api-Version': '2022-11-28', + } + ); + + if (res.status === 204 || res.status === 200) { + console.log(' → ✅ workflow_dispatch 触发成功'); + return true; + } else { + console.log(' → ⚠️ 触发失败: HTTP ' + res.status + ' ' + res.body); + return false; + } + } catch (err) { + console.log(' → ⚠️ 触发异常: ' + err.message); + return false; + } +} + +// ══════════════════════════════════════════════════════════ +// Step 4: 标记工单异常 +// ══════════════════════════════════════════════════════════ + +async function markTicketError(ticketId) { + try { + await httpsPatch('api.notion.com', '/v1/pages/' + ticketId, { + properties: { + '状态': { select: { name: '⚠️ 异常·等人工介入' } }, + }, + }, notionHeaders); + console.log(' → 工单已标记为 ⚠️ 异常·等人工介入'); + } catch (err) { + console.log(' → 标记失败: ' + err.message); + } +} + +// ══════════════════════════════════════════════════════════ +// Step 5: 发送告警邮件 +// ══════════════════════════════════════════════════════════ + +async function sendAlertEmail(timedOutTickets) { + if (!SMTP_USER || !SMTP_PASS) { + console.log('⚠️ SMTP 未配置,跳过告警邮件'); + return; + } + + try { + var nodemailer = require('nodemailer'); + + var ticketList = timedOutTickets.map(function (t) { + return '- ' + t.title + ' (超时 ' + t.ageMinutes + ' 分钟, 重试已耗尽)'; + }).join('\n'); + + var transporter = nodemailer.createTransport({ + host: 'smtp.qq.com', + port: 465, + secure: true, + auth: { user: SMTP_USER, pass: SMTP_PASS }, + }); + + await transporter.sendMail({ + from: '"光湖系统·告警" <' + SMTP_USER + '>', + to: ALERT_EMAIL, + subject: '[光湖系统] ⚠️ SYSLOG 工单处理超时告警', + html: [ + '
', + '

⚠️ 工单处理超时告警

', + '

以下工单超过 3 次重试仍未收到人格体回执:

', + '
' + ticketList + '
', + '

请登录 Notion 工单队列检查。

', + '

🌀 铸渊 · 心跳监控 · 自动告警

', + '
', + ].join('\n'), + }); + + console.log('📧 告警邮件已发送至 ' + ALERT_EMAIL); + } catch (err) { + console.log('⚠️ 告警邮件发送失败: ' + err.message); + } +} + +// ══════════════════════════════════════════════════════════ +// 主流程 +// ══════════════════════════════════════════════════════════ + +async function main() { + console.log('═══════════════════════════════════════════'); + console.log('💓 铸渊 · 工单心跳监控(Phase B3)'); + console.log('═══════════════════════════════════════════'); + console.log(' 时间: ' + new Date().toISOString()); + console.log(''); + + // Step 1: 查询待处理工单 + var tickets = await queryPendingTickets(); + if (tickets.length === 0) { + console.log('✅ 无待处理工单,心跳正常'); + return; + } + + // Step 2: 筛选超时工单 + var timedOut = filterTimedOutTickets(tickets); + if (timedOut.length === 0) { + console.log('✅ 所有工单均在处理窗口内,心跳正常'); + return; + } + + // Step 3 & 4: 对超时工单执行重试或标记异常 + var errorTickets = []; + + for (var i = 0; i < timedOut.length; i++) { + var ticket = timedOut[i]; + // 基于超时时间估算重试次数(每 5 分钟一次) + var estimatedRetries = Math.floor(ticket.ageMinutes / 5); + + if (estimatedRetries < MAX_RETRIES) { + // 重试 + await triggerRetry(ticket); + } else { + // 超过最大重试次数,标记异常 + console.log('❌ 工单 ' + ticket.title + ' 重试 ' + MAX_RETRIES + ' 次仍无回执'); + await markTicketError(ticket.id); + errorTickets.push(ticket); + } + } + + // Step 5: 发送告警邮件(如有异常工单) + if (errorTickets.length > 0) { + await sendAlertEmail(errorTickets); + } + + console.log(''); + console.log('✅ 心跳检测完成'); +} + +main().catch(function (err) { + console.error('❌ 心跳监控失败: ' + err.message); + process.exit(1); +}); diff --git a/scripts/push-broadcast-to-github.js b/scripts/push-broadcast-to-github.js new file mode 100644 index 00000000..32af6eb1 --- /dev/null +++ b/scripts/push-broadcast-to-github.js @@ -0,0 +1,179 @@ +// scripts/push-broadcast-to-github.js +// 铸渊 · Phase B4 · 广播文件推送到 GitHub 仓库 +// +// 人格体生成广播后,通过 GitHub API 将广播 .md 文件推送到仓库。 +// 路径规范:broadcasts/{developer_id}/{taskId}.md +// 提交信息:[AutoBroadcast] {taskId} · {developer} · {描述} +// +// 环境变量: +// GITHUB_TOKEN GitHub API token(推送用) +// BROADCAST_ID 广播编号(如 BC-M23-001-AW) +// DEVELOPER_ID 开发者编号(如 DEV-012) +// DEVELOPER_NAME 开发者名字(如 Awen) +// BROADCAST_CONTENT 广播全文内容(Markdown) +// MODULE_NAME 模块名(可选,如 M23) +// PHASE_NUMBER 环节号(可选,如 1) + +'use strict'; + +var https = require('https'); +var fs = require('fs'); + +var GITHUB_TOKEN = process.env.GITHUB_TOKEN || ''; +var BROADCAST_ID = process.env.BROADCAST_ID || ''; +var DEVELOPER_ID = process.env.DEVELOPER_ID || ''; +var DEVELOPER_NAME = process.env.DEVELOPER_NAME || ''; +var BROADCAST_CONTENT = process.env.BROADCAST_CONTENT || ''; +var MODULE_NAME = process.env.MODULE_NAME || ''; +var PHASE_NUMBER = process.env.PHASE_NUMBER || ''; + +var REPO_OWNER = 'qinfendebingshuo'; +var REPO_NAME = 'guanghulab'; + +// ══════════════════════════════════════════════════════════ +// GitHub API 工具 +// ══════════════════════════════════════════════════════════ + +function githubRequest(method, apiPath, body) { + return new Promise(function (resolve, reject) { + var payload = body ? JSON.stringify(body) : ''; + var opts = { + hostname: 'api.github.com', + port: 443, + path: apiPath, + method: method, + headers: { + 'User-Agent': 'ZhuyuanBroadcast/1.0', + 'Accept': 'application/vnd.github+json', + 'Content-Type': 'application/json', + 'X-GitHub-Api-Version': '2022-11-28', + }, + }; + + if (GITHUB_TOKEN) { + opts.headers['Authorization'] = 'Bearer ' + GITHUB_TOKEN; + } + + if (payload) { + opts.headers['Content-Length'] = Buffer.byteLength(payload); + } + + var req = https.request(opts, function (res) { + var data = ''; + res.on('data', function (chunk) { data += chunk; }); + res.on('end', function () { + try { + resolve({ status: res.statusCode, data: JSON.parse(data) }); + } catch (e) { + resolve({ status: res.statusCode, data: data }); + } + }); + }); + + req.on('error', reject); + if (payload) req.write(payload); + req.end(); + }); +} + +// ══════════════════════════════════════════════════════════ +// 广播推送 +// ══════════════════════════════════════════════════════════ + +async function pushBroadcast() { + if (!GITHUB_TOKEN) { + console.log('⚠️ 缺少 GITHUB_TOKEN,无法推送广播'); + process.exit(1); + } + + if (!BROADCAST_ID || !BROADCAST_CONTENT) { + console.log('⚠️ 缺少 BROADCAST_ID 或 BROADCAST_CONTENT'); + process.exit(1); + } + + // 确定开发者 ID(从广播编号提取或使用环境变量) + var devId = DEVELOPER_ID; + if (!devId) { + // 从 BROADCAST_ID 提取后缀 → 映射到 DEV-XXX + var suffixMatch = BROADCAST_ID.match(/BC-[A-Z0-9]+-\d+-([A-Z]+)/i); + var suffix = suffixMatch ? suffixMatch[1].toUpperCase() : ''; + var suffixMap = { + 'YY': 'DEV-001', 'FM': 'DEV-002', 'YF': 'DEV-003', + 'ZZ': 'DEV-004', 'XCM': 'DEV-005', 'HE': 'DEV-009', + 'JZ': 'DEV-010', 'CCNN': 'DEV-011', 'AW': 'DEV-012', + 'XX': 'DEV-013', 'SY': 'DEV-014', + }; + devId = suffixMap[suffix] || 'UNKNOWN'; + } + + // 构建路径和提交信息 + var filePath = 'broadcasts/' + devId + '/' + BROADCAST_ID + '.md'; + var moduleLabel = MODULE_NAME || BROADCAST_ID.match(/BC-([A-Z0-9-]+)-/i)?.[1] || ''; + var phaseLabel = PHASE_NUMBER ? '环节' + PHASE_NUMBER : ''; + var devLabel = DEVELOPER_NAME || devId; + var commitMessage = '[AutoBroadcast] ' + BROADCAST_ID + ' · ' + devLabel + ' · ' + moduleLabel + phaseLabel; + + console.log('📡 推送广播到 GitHub...'); + console.log(' 路径: ' + filePath); + console.log(' 提交: ' + commitMessage); + + // 检查文件是否已存在(获取 SHA) + var existingRes = await githubRequest('GET', + '/repos/' + REPO_OWNER + '/' + REPO_NAME + '/contents/' + filePath); + + var sha = null; + if (existingRes.status === 200 && existingRes.data && existingRes.data.sha) { + sha = existingRes.data.sha; + console.log(' → 文件已存在,将覆盖更新 (sha: ' + sha.slice(0, 8) + ')'); + } + + // Base64 编码广播内容 + var contentBase64 = Buffer.from(BROADCAST_CONTENT, 'utf8').toString('base64'); + + // 创建或更新文件 + var putBody = { + message: commitMessage, + content: contentBase64, + committer: { + name: 'zhuyuan-agent[bot]', + email: 'zhuyuan-agent[bot]@users.noreply.github.com', + }, + }; + + if (sha) { + putBody.sha = sha; + } + + var putRes = await githubRequest('PUT', + '/repos/' + REPO_OWNER + '/' + REPO_NAME + '/contents/' + filePath, + putBody); + + if (putRes.status === 200 || putRes.status === 201) { + console.log('✅ 广播已推送: ' + filePath); + console.log(' → commit: ' + (putRes.data.commit ? putRes.data.commit.sha.slice(0, 8) : 'unknown')); + + // 输出到 GITHUB_OUTPUT + var outputFile = process.env.GITHUB_OUTPUT; + if (outputFile) { + fs.appendFileSync(outputFile, 'broadcast_pushed=true\n'); + fs.appendFileSync(outputFile, 'broadcast_path=' + filePath + '\n'); + fs.appendFileSync(outputFile, 'broadcast_commit=' + (putRes.data.commit ? putRes.data.commit.sha : '') + '\n'); + } + } else { + console.error('❌ 广播推送失败: HTTP ' + putRes.status); + console.error(' → ' + JSON.stringify(putRes.data)); + + // 输出失败状态 + var outputFile = process.env.GITHUB_OUTPUT; + if (outputFile) { + fs.appendFileSync(outputFile, 'broadcast_pushed=false\n'); + } + + process.exit(1); + } +} + +pushBroadcast().catch(function (err) { + console.error('❌ 广播推送异常: ' + err.message); + process.exit(1); +}); diff --git a/scripts/receive-syslog.js b/scripts/receive-syslog.js index dd136852..00ff57f0 100644 --- a/scripts/receive-syslog.js +++ b/scripts/receive-syslog.js @@ -194,11 +194,13 @@ async function createSyslogEntry(syslog, token, dbId, dateStr) { async function createTicket(syslog, token, dbId, dateStr) { const broadcastId = syslog.broadcast_id || syslog.broadcastId || 'UNKNOWN'; const devId = syslog.dev_id || syslog.developer_id || 'UNKNOWN'; + const devName = syslog.developer_name || syslog.dev_name || ''; + const now = new Date().toISOString(); const properties = { - '标题': titleProp('SYSLOG 回传|' + broadcastId + ' · ' + devId), + '标题': titleProp('[自动] SYSLOG处理|' + broadcastId + ' · ' + devId), '操作类型': selectProp('其他'), - '提交者': richTextProp('巡检引擎'), + '提交者': richTextProp('铸渊Agent·自动管道'), '提交日期': dateProp(dateStr), '状态': selectProp('待处理'), '优先级': selectProp('P1'), @@ -208,6 +210,22 @@ async function createTicket(syslog, token, dbId, dateStr) { if (broadcastId !== 'UNKNOWN') properties['广播编号'] = richTextProp(broadcastId); if (devId !== 'UNKNOWN') properties['开发者编号'] = richTextProp(devId); + // Phase B1 标准化工单内容 + const ticketContent = [ + '## 📡 标准化工单 · Phase B1', + '', + '| 字段 | 值 |', + '|------|-----|', + '| 工单类型 | SYSLOG处理 |', + '| 状态 | 待处理 |', + '| 来源 | GitHub Actions |', + '| taskId | ' + broadcastId + ' |', + '| developer | ' + devId + ' ' + devName + ' |', + '| created_at | ' + now + ' |', + '| retry_count | 0 |', + '| receipt_status | pending |', + ].join('\n'); + const body = { parent: { database_id: dbId }, properties, @@ -218,7 +236,7 @@ async function createTicket(syslog, token, dbId, dateStr) { paragraph: { rich_text: [{ type: 'text', - text: { content: '📥 SYSLOG 回传,来自 ' + devId + ',关联广播 ' + broadcastId }, + text: { content: ticketContent }, }], }, }, @@ -234,7 +252,7 @@ async function createTicket(syslog, token, dbId, dateStr) { }; const result = await notionPost('/v1/pages', body, token); - console.log(' → 霜砚工单已创建: ' + result.id); + console.log(' → 标准化工单已创建: ' + result.id); return result; } From c9d6495e5fb8d087d20f7e9dd5987c551f16e235 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 06:37:11 +0000 Subject: [PATCH 5/6] =?UTF-8?q?refactor:=20=E6=8F=90=E5=8F=96=E5=85=B1?= =?UTF-8?q?=E4=BA=AB=20dev-suffix-map=20=E6=A8=A1=E5=9D=97=20+=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E4=BB=A3=E7=A0=81=E5=AE=A1=E6=9F=A5=E5=8F=8D=E9=A6=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建 scripts/utils/dev-suffix-map.js (共享后缀→DEV-ID映射) - 更新 verify-modules.js + push-broadcast-to-github.js 使用共享映射 - 移动 require 到文件顶部 (invoke-persona.js, create-standardized-ticket.js) - 心跳监控参数可配置化 (HEARTBEAT_TIMEOUT_MINUTES, MAX_RETRY_COUNT) Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> --- scripts/create-standardized-ticket.js | 2 +- scripts/invoke-persona.js | 2 +- scripts/notion-heartbeat.js | 4 +-- scripts/push-broadcast-to-github.js | 12 ++------- scripts/utils/dev-suffix-map.js | 38 +++++++++++++++++++++++++++ scripts/verify-modules.js | 18 +++++-------- 6 files changed, 50 insertions(+), 26 deletions(-) create mode 100644 scripts/utils/dev-suffix-map.js diff --git a/scripts/create-standardized-ticket.js b/scripts/create-standardized-ticket.js index a1607245..4bfb5b1f 100644 --- a/scripts/create-standardized-ticket.js +++ b/scripts/create-standardized-ticket.js @@ -18,6 +18,7 @@ 'use strict'; const https = require('https'); +const fs = require('fs'); const NOTION_TOKEN = process.env.NOTION_TOKEN || ''; const NOTION_TICKET_DB_ID = process.env.NOTION_TICKET_DB_ID || ''; @@ -184,7 +185,6 @@ async function createStandardizedTicket() { // 输出工单 ID 到 GITHUB_OUTPUT var outputFile = process.env.GITHUB_OUTPUT; if (outputFile) { - var fs = require('fs'); fs.appendFileSync(outputFile, 'ticket_page_id=' + result.id + '\n'); fs.appendFileSync(outputFile, 'ticket_url=' + (result.url || '') + '\n'); } diff --git a/scripts/invoke-persona.js b/scripts/invoke-persona.js index bd4cc12e..c88cbd5c 100644 --- a/scripts/invoke-persona.js +++ b/scripts/invoke-persona.js @@ -24,6 +24,7 @@ const https = require('https'); const http = require('http'); const fs = require('fs'); const path = require('path'); +const childProcess = require('child_process'); // ══════════════════════════════════════════════════════════ // 配置 @@ -166,7 +167,6 @@ async function invokePersona(workOrder) { process.env.AUTHOR = workOrder.developer; // 执行 wake-persona.js(fork 子进程) - var childProcess = require('child_process'); var wakeScript = path.join(__dirname, 'wake-persona.js'); return new Promise(function (resolve, reject) { diff --git a/scripts/notion-heartbeat.js b/scripts/notion-heartbeat.js index a259d4bd..5707647b 100644 --- a/scripts/notion-heartbeat.js +++ b/scripts/notion-heartbeat.js @@ -30,8 +30,8 @@ var SMTP_USER = process.env.SMTP_USER || ''; var SMTP_PASS = process.env.SMTP_PASS || ''; var NOTION_VERSION = '2022-06-28'; -var HEARTBEAT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes -var MAX_RETRIES = 3; +var HEARTBEAT_TIMEOUT_MS = (parseInt(process.env.HEARTBEAT_TIMEOUT_MINUTES, 10) || 5) * 60 * 1000; +var MAX_RETRIES = parseInt(process.env.MAX_RETRY_COUNT, 10) || 3; var REPO_OWNER = 'qinfendebingshuo'; var REPO_NAME = 'guanghulab'; diff --git a/scripts/push-broadcast-to-github.js b/scripts/push-broadcast-to-github.js index 32af6eb1..41cd164f 100644 --- a/scripts/push-broadcast-to-github.js +++ b/scripts/push-broadcast-to-github.js @@ -18,6 +18,7 @@ var https = require('https'); var fs = require('fs'); +var devSuffixMap = require('./utils/dev-suffix-map'); var GITHUB_TOKEN = process.env.GITHUB_TOKEN || ''; var BROADCAST_ID = process.env.BROADCAST_ID || ''; @@ -94,16 +95,7 @@ async function pushBroadcast() { // 确定开发者 ID(从广播编号提取或使用环境变量) var devId = DEVELOPER_ID; if (!devId) { - // 从 BROADCAST_ID 提取后缀 → 映射到 DEV-XXX - var suffixMatch = BROADCAST_ID.match(/BC-[A-Z0-9]+-\d+-([A-Z]+)/i); - var suffix = suffixMatch ? suffixMatch[1].toUpperCase() : ''; - var suffixMap = { - 'YY': 'DEV-001', 'FM': 'DEV-002', 'YF': 'DEV-003', - 'ZZ': 'DEV-004', 'XCM': 'DEV-005', 'HE': 'DEV-009', - 'JZ': 'DEV-010', 'CCNN': 'DEV-011', 'AW': 'DEV-012', - 'XX': 'DEV-013', 'SY': 'DEV-014', - }; - devId = suffixMap[suffix] || 'UNKNOWN'; + devId = devSuffixMap.getDevIdFromBroadcast(BROADCAST_ID) || 'UNKNOWN'; } // 构建路径和提交信息 diff --git a/scripts/utils/dev-suffix-map.js b/scripts/utils/dev-suffix-map.js new file mode 100644 index 00000000..35248f80 --- /dev/null +++ b/scripts/utils/dev-suffix-map.js @@ -0,0 +1,38 @@ +// scripts/utils/dev-suffix-map.js +// 开发者广播编号后缀 → DEV-ID 映射表 +// +// 广播编号格式: BC-{模块}-{序号}-{后缀} +// 后缀是开发者名字拼音首字母缩写 + +'use strict'; + +// 后缀→开发者编号映射 +var SUFFIX_MAP = { + 'YY': 'DEV-001', // 页页 + 'FM': 'DEV-002', // 肥猫 + 'YF': 'DEV-003', // 燕樊 + 'ZZ': 'DEV-004', // 之之 + 'XCM': 'DEV-005', // 小草莓 + 'HE': 'DEV-009', // 花尔 + 'JZ': 'DEV-010', // 桔子 + 'CCNN': 'DEV-011', // 匆匆那年 + 'AW': 'DEV-012', // Awen + 'XX': 'DEV-013', // 小兴 + 'SY': 'DEV-014', // 时雨 +}; + +/** + * 从广播编号中提取开发者编号 + * @param {string} broadcastId - 广播编号(如 BC-M22-009-AW) + * @returns {string} 开发者编号(如 DEV-012),未找到返回空字符串 + */ +function getDevIdFromBroadcast(broadcastId) { + var suffixMatch = (broadcastId || '').match(/BC-[A-Z0-9]+-\d+-([A-Z]+)/i); + var suffix = suffixMatch ? suffixMatch[1].toUpperCase() : ''; + return SUFFIX_MAP[suffix] || ''; +} + +module.exports = { + SUFFIX_MAP: SUFFIX_MAP, + getDevIdFromBroadcast: getDevIdFromBroadcast, +}; diff --git a/scripts/verify-modules.js b/scripts/verify-modules.js index 2df4ddf5..24b250e5 100644 --- a/scripts/verify-modules.js +++ b/scripts/verify-modules.js @@ -17,6 +17,7 @@ const fs = require('fs'); const path = require('path'); +const devSuffixMap = require('./utils/dev-suffix-map'); const SYSLOG_CONTENT = process.env.SYSLOG_CONTENT || ''; const BROADCAST_ID = process.env.BROADCAST_ID || ''; @@ -67,25 +68,18 @@ function extractDevId(content, broadcastId) { var devMatch = content.match(/\b(DEV-\d{3})\b/i); if (devMatch) return devMatch[1].toUpperCase(); - // 从广播编号提取开发者后缀(如 BC-M22-009-AW → AW) + // 从广播编号提取开发者编号(使用共享映射表) + var mappedDevId = devSuffixMap.getDevIdFromBroadcast(broadcastId); + if (mappedDevId) return mappedDevId; + + // 兜底:从 dev-status.json 匹配 var suffixMatch = broadcastId.match(/BC-[A-Z0-9]+-\d+-([A-Z]+)/i); var devSuffix = suffixMatch ? suffixMatch[1].toUpperCase() : ''; - // 从 dev-status.json 通过后缀匹配开发者 if (devSuffix) { try { var devStatus = JSON.parse(fs.readFileSync(DEV_STATUS_PATH, 'utf8')); var team = devStatus.team || []; - // 后缀缩写映射表(从 dev-status.json 中名字的拼音首字母) - var suffixMap = { - 'YY': 'DEV-001', 'FM': 'DEV-002', 'YF': 'DEV-003', - 'ZZ': 'DEV-004', 'XCM': 'DEV-005', 'HE': 'DEV-009', - 'JZ': 'DEV-010', 'CCNN': 'DEV-011', 'AW': 'DEV-012', - 'XX': 'DEV-013', 'SY': 'DEV-014', - }; - if (suffixMap[devSuffix]) return suffixMap[devSuffix]; - - // 兜底:尝试从 team 中匹配 waiting 字段里的广播编号 for (var i = 0; i < team.length; i++) { var waiting = team[i].waiting || ''; if (waiting.includes(broadcastId) || waiting.includes(devSuffix)) { From 1479da4e1cfe05cd5e09c086442ad7882653ff5e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 06:39:06 +0000 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20=E6=B7=BB=E5=8A=A0=20notion-heartbea?= =?UTF-8?q?t.yml=20=E6=98=BE=E5=BC=8F=E6=9D=83=E9=99=90=E5=A3=B0=E6=98=8E?= =?UTF-8?q?=EF=BC=88CodeQL=E5=AE=89=E5=85=A8=E4=BF=AE=E5=A4=8D=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> --- .github/workflows/notion-heartbeat.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/notion-heartbeat.yml b/.github/workflows/notion-heartbeat.yml index d7b5969a..76653dc0 100644 --- a/.github/workflows/notion-heartbeat.yml +++ b/.github/workflows/notion-heartbeat.yml @@ -20,6 +20,9 @@ jobs: heartbeat: name: 💓 工单心跳检测 runs-on: ubuntu-latest + permissions: + contents: read + actions: write steps: - name: Checkout