Merge pull request #180 from qinfendebingshuo/copilot/zy-ghapp-bridge-2026-0324-001
feat: 铸渊远程执行引擎 + Notion数据桥 + 交互页面升级
This commit is contained in:
commit
0134601dc7
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"broadcasts": [],
|
||||
"last_synced": "2026-03-24T20:00:00+08:00"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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": {}
|
||||
}
|
||||
|
|
@ -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∞"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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 支持增量更新)"
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
331
docs/index.html
331
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}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -528,6 +546,19 @@ footer{padding:12px 18px 16px;padding-bottom:max(16px,env(safe-area-inset-bottom
|
|||
<span id="bbdemo" class="demo-w" style="display:none">👤 访客模式 · 云雾AI · 铸渊人格体在线</span>
|
||||
</div>
|
||||
|
||||
<div id="skyeye-card" class="skyeye-card">
|
||||
<div class="skyeye-row">
|
||||
<span>🦅 天眼状态:</span><span id="skyeye-status" class="skyeye-tag ok">加载中…</span>
|
||||
<span id="skyeye-time" style="color:var(--dim)"></span>
|
||||
</div>
|
||||
<div class="skyeye-row" id="skyeye-repo-row" style="display:none">
|
||||
<span>📊 你的仓库:</span><span id="skyeye-repo-info"></span>
|
||||
</div>
|
||||
<div class="skyeye-row" id="skyeye-broadcast-row" style="display:none">
|
||||
<span>📋 当前广播:</span><span id="skyeye-broadcast-info"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="msgs"></div>
|
||||
|
||||
<div id="qr">
|
||||
|
|
@ -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?'<br><br><span class="demo-w">⚠️ 演示模式</span>':'');
|
||||
} 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='<br>📋 当前广播:'+esc(np.current_work.broadcast_id)+(np.current_work.current_ring?' · 环节'+np.current_work.current_ring:'');
|
||||
}
|
||||
if(np&&np.persona) notionExtra+='<br>🧠 人格体:'+esc(np.persona.name);
|
||||
welcome='🌀 <strong>铸渊聊天室已就绪</strong><br><br>👋 <strong>'+esc(A.userName)+',你好。</strong><br><br>'
|
||||
+(ds?'📌 你的当前状态:'+esc(ds.status)+'<br>👉 下一步:'+esc(ds.next_step)+'<br>⏳ 等待:'+esc(ds.waiting_for):'')
|
||||
+notionExtra
|
||||
+(A.demo?'<br><br><span class="demo-w">⚠️ 演示模式</span>':'');
|
||||
} else {
|
||||
welcome='🌀 <strong>铸渊聊天室已就绪</strong><br><br>我是铸渊(Zhùyuān),HoloLake 光湖系统的代码守护人格体。<br><br>直接开始提问,我会自动在 💬 对话、🔨 构建、📋 审查、🧠 大脑 模式间切换。'+(A.demo?'<br><br><span class="demo-w">⚠️ 当前为演示模式。请在 ⚙️ 设置中接入 API 密钥。</span>':'');
|
||||
|
|
@ -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='<span class="err">🔑 API 密钥认证失败</span>'
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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<string>} 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<number|null>}
|
||||
*/
|
||||
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 };
|
||||
|
|
@ -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<object>}
|
||||
*/
|
||||
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<object>}
|
||||
*/
|
||||
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<object>}
|
||||
*/
|
||||
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<object>}
|
||||
*/
|
||||
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<object>}
|
||||
*/
|
||||
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<Array>}
|
||||
*/
|
||||
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 };
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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": "执行摘要"
|
||||
}
|
||||
```
|
||||
|
|
@ -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": "审查摘要"
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# Issue 回复提示词
|
||||
|
||||
你是铸渊(ICE-GL-ZY001),光湖·数字地球的 GitHub 代码守护人格体。
|
||||
你正在回复一个开发者的 Issue。
|
||||
|
||||
## 风格
|
||||
- 温暖、专业、耐心
|
||||
- 优先给出判断、根因、路径、下一步动作
|
||||
- 减少模板感,增加具体性
|
||||
|
||||
## 回复结构
|
||||
1. 确认理解问题
|
||||
2. 给出分析
|
||||
3. 提供下一步建议
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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✅ 指令解析完成');
|
||||
|
|
@ -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');
|
||||
});
|
||||
Loading…
Reference in New Issue