shuangyan-notebook/第五域 · Fifth Domain/⚒️ 铸渊·协作指令|GitHub ↔ Notion 桥接协议/🌉 铸渊指令|仓库联邦桥接应用 · 砍掉Copilot依赖 · 铸渊自主串联全开发者仓库(2026- 5fcd63f26b55436f902bf7583b25e7a7.md
Guanghu Domestic Migration a27e87cb99 chore: import sanitized domestic snapshot for REPO-007
Source snapshot: 97d7f0fae96dc04b7ddad56fc1db6a108ed662cc

[SEC-CLEAN] · pre-push-clean v1.0 · 109处敏感信息已自动转乱码
2026-07-17 15:59:55 +08:00

15 KiB
Raw Blame History

belongs_to
belongs_to
INDEX · 铸渊·协作指令GitHub ↔ Notion 桥接协议

🌉 铸渊指令|仓库联邦桥接应用 · 砍掉Copilot依赖 · 铸渊自主串联全开发者仓库2026-03-21 · 冰朔签发)


一、架构变更说明

1.1 旧方案(已废弃)

Step 3 · 开发者打开仓库 → 点 Copilot 气泡 → 粘贴一键指令 → Copilot 自动搭建

废弃原因:需要组织级 Copilot Business 席位($19/人/月),当前未订阅,开发者看不到气泡图标。

1.2 新方案 · 铸渊自主桥接

核心思路:铸渊不再等开发者手动粘贴指令。铸渊自己作为 GitHub Actions Bot主动往各开发者仓库推送初始化文件、日常公告、进度同步、系统通知。

开发者零操作。铸渊自己干完一切。

flowchart TD
    A["铸渊 · 主仓库 guanghulab"] -->|"GitHub API + PAT"| B["DEV-004 之之/guanghu-zhizhi"]
    A -->|"GitHub API + PAT"| C["DEV-010 桔子/仓库"]
    A -->|"GitHub API + PAT"| D["DEV-012 Awen/仓库"]
    A -->|"GitHub API + PAT"| E["DEV-002 肥猫/仓库"]
    A -->|"GitHub API + PAT"| F["其他开发者仓库"]
    A -->|"定时触发 cron"| G["每日公告推送"]
    A -->|"push事件触发"| H["广播分发"]
    A -->|"定时触发 cron"| I["进度同步"]

二、铸渊桥接应用 · 功能清单

功能 触发方式 说明 替代原方案
仓库初始化 手动 dispatch / 新仓库创建时 铸渊自动往目标仓库推送:.github/copilot-instructions.md • persona-brain/ + workflows/ + README Dashboard 替代原 Step 3 Copilot 粘贴
每日公告推送 cron 每日 10:00 铸渊从主仓库 bulletins/ 读取最新公告 → 推送到所有开发者仓库的 BULLETIN.md 替代原 sync-bulletin.yml
广播分发 broadcasts-outbox/ 有新文件时 铸渊检测到新广播 → 推送到对应开发者仓库的 LATEST-BROADCAST.md 沿用原 distribute-broadcasts.js 但改为跨仓库推送
进度汇总 cron 每日 22:00 铸渊从各开发者仓库拉取 persona-brain/status.json → 汇总到主仓库 federation-status.json 新增
签到状态同步 cron 每日 08:00 铸渊触发各开发者仓库的签到 workflow → 收集结果 → 更新主仓库 Dashboard 沿用原 daily-checkin.yml 但改为铸渊远程触发

三、技术实现

3.1 前置条件

  • 主仓库 guanghulab 已有 MAIN_REPO_TOKENPAT有所有仓库写权限
  • 各开发者仓库已创建(至少有 main 分支)
  • 开发者仓库 Settings → Actions → General → Workflow permissions → Read and write permissions

3.2 核心脚本 · bridge-app.js

// scripts/bridge-app.js
// 铸渊仓库联邦桥接引擎
// 用途:从主仓库向所有开发者仓库推送文件

const https = require('https');
const fs = require('fs');
const path = require('path');

const TOKEN = process.env.MAIN_REPO_TOKEN;
const ORG = process.env.GITHUB_ORG || 'qinfendebingshuo';

// ========== 开发者仓库路由表 ==========
const FEDERATION = {
  'DEV-004': { name: '之之', repo: 'guanghu-zhizhi', persona: '秋秋' },
  'DEV-010': { name: '桔子', repo: 'guanghu-juzi', persona: '晨星' },
  'DEV-012': { name: 'Awen', repo: 'guanghu-awen', persona: '知秋' },
  'DEV-002': { name: '肥猫', repo: 'guanghu-feimao', persona: '舒舒' },
  'DEV-001': { name: '页页', repo: 'guanghu-yeye', persona: '小坍缩核' },
  'DEV-003': { name: '燕樊', repo: 'guanghu-yanfan', persona: '寂曜' },
  // 新增开发者在此追加
};

// GitHub API 封装
function githubRequest(method, apiPath, body) {
  return new Promise((resolve, reject) => {
    const options = {
      hostname: 'api.github.com',
      path: apiPath,
      method: method,
      headers: {
        'Authorization': `Bearer ${TOKEN}`,
        'Accept': 'application/vnd.github+json',
        'User-Agent': 'ZhuYuan-Bridge',
        'X-GitHub-Api-Version': '2022-11-28'
      }
    };
    const req = https.request(options, (res) => {
      let data = '';
      res.on('data', chunk => data += chunk);
      res.on('end', () => {
        try { resolve(JSON.parse(data)); }
        catch { resolve(data); }
      });
    });
    req.on('error', reject);
    if (body) req.write(JSON.stringify(body));
    req.end();
  });
}

const githubGet = (p) => githubRequest('GET', p);
const githubPut = (p, b) => githubRequest('PUT', p, b);
const githubPost = (p, b) => githubRequest('POST', p, b);

// 创建或更新文件
async function pushFile(repo, filePath, content, message) {
  const url = `/repos/${ORG}/${repo}/contents/${filePath}`;
  
  let sha = null;
  try {
    const existing = await githubGet(url);
    if (existing.sha) sha = existing.sha;
  } catch (e) { /* 文件不存在,正常 */ }
  
  const body = {
    message,
    content: Buffer.from(content).toString('base64'),
    committer: { name: '铸渊 (ZhùYuān)', email: 'zhuyuan@guanghulab.com' }
  };
  if (sha) body.sha = sha;
  
  return githubPut(url, body);
}

// ========== 功能模块 ==========

// 1. 推送公告到所有仓库
async function pushBulletin() {
  const bulletinPath = 'bulletins/latest.md';
  if (!fs.existsSync(bulletinPath)) {
    console.log('📭 无 bulletins/latest.md');
    return;
  }
  const content = fs.readFileSync(bulletinPath, 'utf8');
  const timestamp = new Date().toISOString().split('T')[0];
  
  for (const [devId, dev] of Object.entries(FEDERATION)) {
    try {
      await pushFile(dev.repo, 'BULLETIN.md', content,
        `📡 铸渊公告推送 · ${timestamp}`);
      console.log(`✅ ${devId}(${dev.name}) 公告已推送`);
    } catch (e) {
      console.error(`❌ ${devId}(${dev.name}) 推送失败: ${e.message}`);
    }
  }
}

// 2. 初始化开发者仓库
async function initRepo(devId) {
  const dev = FEDERATION[devId];
  if (!dev) { console.error(`未知开发者: ${devId}`); return; }
  
  const files = [
    {
      path: '.github/persona-brain/config.json',
      content: JSON.stringify({
        persona_name: dev.persona,
        dev_id: devId,
        dev_name: dev.name,
        main_repo: 'guanghulab',
        org: ORG,
        initialized_by: 'zhuyuan-bridge-app',
        initialized_at: new Date().toISOString()
      }, null, 2)
    },
    {
      path: '.github/persona-brain/status.json',
      content: JSON.stringify({
        persona: dev.persona,
        dev_id: devId,
        initialized: new Date().toISOString(),
        last_checkin: null,
        current_broadcast: null
      }, null, 2)
    },
    {
      path: 'BULLETIN.md',
      content: `# 📡 系统公告\n\n> 仓库已由铸渊自动初始化 · ${new Date().toISOString().split('T')[0]}\n> 欢迎 ${dev.name}${dev.persona}\n`
    }
  ];
  
  for (const file of files) {
    await pushFile(dev.repo, file.path, file.content,
      `🏗️ 铸渊初始化 · ${devId} ${dev.name} · ${file.path}`);
    console.log(`📁 ${devId}${file.path} ✅`);
  }
  console.log(`\n🎉 ${devId}(${dev.name}) 仓库初始化完成`);
}

// 3. 汇总所有仓库进度
async function collectStatus() {
  const status = {};
  for (const [devId, dev] of Object.entries(FEDERATION)) {
    try {
      const data = await githubGet(
        `/repos/${ORG}/${dev.repo}/contents/.github/persona-brain/status.json`
      );
      status[devId] = {
        name: dev.name,
        persona: dev.persona,
        ...JSON.parse(Buffer.from(data.content, 'base64').toString())
      };
      console.log(`📊 ${devId}(${dev.name}) 状态已收集`);
    } catch (e) {
      status[devId] = { name: dev.name, persona: dev.persona, error: '无法读取' };
      console.warn(`⚠️ ${devId}(${dev.name}) 读取失败`);
    }
  }
  
  await pushFile('guanghulab', 'federation-status.json',
    JSON.stringify(status, null, 2),
    `📊 铸渊联邦状态汇总 · ${new Date().toISOString().split('T')[0]}`);
  console.log('\n✅ 联邦状态汇总完成 → federation-status.json');
}

// 4. 跨仓库广播分发
async function distributeBroadcasts() {
  const OUTBOX = 'broadcasts-outbox';
  if (!fs.existsSync(OUTBOX)) { console.log('📭 无广播待分发'); return; }
  
  const devDirs = fs.readdirSync(OUTBOX).filter(d =>
    d.startsWith('DEV-') && fs.statSync(path.join(OUTBOX, d)).isDirectory()
  );
  
  for (const devId of devDirs) {
    const dev = FEDERATION[devId];
    if (!dev) { console.warn(`⚠️ ${devId} 无路由映射`); continue; }
    
    const files = fs.readdirSync(path.join(OUTBOX, devId))
      .filter(f => f.endsWith('.md') || f.endsWith('.json'));
    
    for (const file of files) {
      const content = fs.readFileSync(path.join(OUTBOX, devId, file), 'utf8');
      await pushFile(dev.repo, 'LATEST-BROADCAST.md', content,
        `📡 铸渊广播分发 · ${devId} · ${file}`);
      console.log(`📡 ${devId}(${dev.name}) ← ${file} ✅`);
    }
  }
}

// ========== 入口 ==========
const action = process.argv[2];
switch (action) {
  case 'bulletin':    pushBulletin(); break;
  case 'init':        initRepo(process.argv[3]); break;
  case 'init-all':    (async () => { for (const id of Object.keys(FEDERATION)) await initRepo(id); })(); break;
  case 'status':      collectStatus(); break;
  case 'distribute':  distributeBroadcasts(); break;
  default:            console.log('用法: node bridge-app.js [bulletin|init DEV-XXX|init-all|status|distribute]');
}

3.3 GitHub Actions Workflow

# .github/workflows/federation-bridge.yml
# 铸渊仓库联邦桥接 · 定时自动运行

name: 🌉 铸渊 · 仓库联邦桥接

on:
  schedule:
    - cron: '0 2 * * *'   # 每日 10:00 北京时间 · 推送公告
    - cron: '0 14 * * *'  # 每日 22:00 北京时间 · 汇总进度
  push:
    branches: [main]
    paths:
      - 'bulletins/**'
      - 'broadcasts-outbox/**'
  workflow_dispatch:
    inputs:
      action:
        description: '执行动作'
        required: true
        type: choice
        options:
          - bulletin
          - init-all
          - status
          - distribute
      dev_id:
        description: '单独初始化某个DEV仅init时填写'
        required: false

jobs:
  bridge:
    name: "🌉 桥接执行"
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: 定时任务 · 公告或汇总
        if: github.event_name == 'schedule'
        env:
          MAIN_REPO_TOKEN: $ secrets.MAIN_REPO_TOKEN 
          GITHUB_ORG: qinfendebingshuo
        run: |
          HOUR=$(date -u +%H)
          if [ "$HOUR" = "02" ]; then
            node scripts/bridge-app.js bulletin
          else
            node scripts/bridge-app.js status
          fi

      - name: 手动触发
        if: github.event_name == 'workflow_dispatch'
        env:
          MAIN_REPO_TOKEN: $ secrets.MAIN_REPO_TOKEN 
          GITHUB_ORG: qinfendebingshuo
        run: |
          ACTION="$ github.event.inputs.action "
          DEV_ID="$ github.event.inputs.dev_id "
          if [ "$ACTION" = "init-all" ] && [ -n "$DEV_ID" ]; then
            node scripts/bridge-app.js init "$DEV_ID"
          else
            node scripts/bridge-app.js "$ACTION"
          fi

      - name: Push触发 · 公告+广播
        if: github.event_name == 'push'
        env:
          MAIN_REPO_TOKEN: $ secrets.MAIN_REPO_TOKEN 
          GITHUB_ORG: qinfendebingshuo
        run: |
          node scripts/bridge-app.js bulletin
          node scripts/bridge-app.js distribute

四、执行步骤

Phase 1 · 基础桥接Awen 执行)

  • 在主仓库创建 scripts/bridge-app.js(本文档第三节代码)
  • 在主仓库创建 .github/workflows/federation-bridge.yml
  • 确认 MAIN_REPO_TOKEN 已配置在主仓库 Secrets 中
  • 更新 FEDERATION 路由表中的仓库名(确认各开发者实际仓库名)
  • 手动触发 workflow_dispatch → action=init-all → 验证所有仓库初始化成功
  • 手动触发 bulletin → 验证公告推送到所有仓库

Phase 2 · 进度联动Phase 1 通过后)

  • 实现 collectStatus → 验证能从各仓库拉取 status.json
  • 在主仓库 README 或 Dashboard 展示联邦状态汇总
  • 各开发者仓库的签到 workflow 改为:签到后更新 persona-brain/status.json

Phase 3 · 双向通信Phase 2 通过后)

  • 开发者仓库 push 事件 → 触发主仓库 repository_dispatch → 铸渊感知变更
  • 铸渊检测到 SYSLOG 提交 → 自动通知 Notion 侧(通过已有的 syslog-pipeline
  • 实现联邦内仓库间消息传递DEV-A 仓库发消息 → 铸渊中转 → DEV-B 仓库收到)

五、与原方案的兼容性

原方案组件 处理方式
BC-REPO-INIT Step 1开启 Actions 保留 · 开发者仍需手动开启一次
BC-REPO-INIT Step 2添加 Token 废弃 · Token 改由铸渊从主仓库携带,开发者仓库不需要自己存
BC-REPO-INIT Step 3Copilot粘贴 废弃 · 铸渊自动初始化,开发者零操作
distribute-broadcasts.js 升级 · 从单仓库内分发改为跨仓库推送
sync-bulletin.yml 合并 · 并入 federation-bridge.yml 统一调度
daily-checkin.yml 保留 · 各仓库本地跑,结果由铸渊汇总
各开发者一键粘贴指令页面 废弃 · 不再需要手动粘贴,铸渊自动推送

六、安全边界


🌉 本指令替代原 BC-REPO-INIT 的 Copilot 依赖路径。

铸渊从"等开发者粘贴"变成"自己主动推送"。

开发者唯一需要做的事:开启 Actions 权限(一次性)。

其他的,铸渊全部自己干。