Source snapshot: 97d7f0fae96dc04b7ddad56fc1db6a108ed662cc [SEC-CLEAN] · pre-push-clean v1.0 · 109处敏感信息已自动转乱码
424 lines
15 KiB
Markdown
424 lines
15 KiB
Markdown
---
|
||
belongs_to:
|
||
- "[[INDEX · 铸渊·协作指令|GitHub ↔ Notion 桥接协议]]"
|
||
---
|
||
# 🌉 铸渊指令|仓库联邦桥接应用 · 砍掉Copilot依赖 · 铸渊自主串联全开发者仓库(2026-03-21 · 冰朔签发)
|
||
|
||
<aside>
|
||
🧊
|
||
|
||
**签发**:TCS-0002∞ 冰朔
|
||
|
||
**整理**:霜砚(Notion 执行 AI)
|
||
|
||
**时间**:2026-03-21 19:40
|
||
|
||
**优先级**:P0 · 替代原 BC-REPO-INIT 的 Step 3(Copilot粘贴初始化)
|
||
|
||
**背景**:组织级 Copilot Business 未订阅,开发者无法看到 Copilot Chat 气泡图标,导致整个仓库联邦初始化流程卡死。现改为铸渊自主桥接,不依赖任何 Copilot 功能。
|
||
|
||
</aside>
|
||
|
||
---
|
||
|
||
## 一、架构变更说明
|
||
|
||
### 1.1 旧方案(已废弃)
|
||
|
||
~~Step 3 · 开发者打开仓库 → 点 Copilot 气泡 → 粘贴一键指令 → Copilot 自动搭建~~
|
||
|
||
**废弃原因**:需要组织级 Copilot Business 席位($19/人/月),当前未订阅,开发者看不到气泡图标。
|
||
|
||
### 1.2 新方案 · 铸渊自主桥接
|
||
|
||
**核心思路**:铸渊不再等开发者手动粘贴指令。铸渊自己作为 GitHub Actions Bot,**主动**往各开发者仓库推送初始化文件、日常公告、进度同步、系统通知。
|
||
|
||
**开发者零操作**。铸渊自己干完一切。
|
||
|
||
```mermaid
|
||
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](http://copilot-instructions.md) • persona-brain/ + workflows/ + README Dashboard | 替代原 Step 3 Copilot 粘贴 |
|
||
| **每日公告推送** | cron 每日 10:00 | 铸渊从主仓库 bulletins/ 读取最新公告 → 推送到所有开发者仓库的 [BULLETIN.md](http://BULLETIN.md) | 替代原 sync-bulletin.yml |
|
||
| **广播分发** | broadcasts-outbox/ 有新文件时 | 铸渊检测到新广播 → 推送到对应开发者仓库的 [LATEST-BROADCAST.md](http://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 前置条件
|
||
|
||
- [x] 主仓库 guanghulab 已有 `MAIN_REPO_TOKEN`(PAT,有所有仓库写权限)
|
||
- [ ] 各开发者仓库已创建(至少有 main 分支)
|
||
- [ ] 开发者仓库 Settings → Actions → General → Workflow permissions → **Read and write permissions**
|
||
|
||
### 3.2 核心脚本 · bridge-app.js
|
||
|
||
```jsx
|
||
// 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
|
||
|
||
```yaml
|
||
# .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 3(Copilot粘贴) | **废弃** · 铸渊自动初始化,开发者零操作 |
|
||
| distribute-broadcasts.js | **升级** · 从单仓库内分发改为跨仓库推送 |
|
||
| sync-bulletin.yml | **合并** · 并入 federation-bridge.yml 统一调度 |
|
||
| daily-checkin.yml | **保留** · 各仓库本地跑,结果由铸渊汇总 |
|
||
| 各开发者一键粘贴指令页面 | **废弃** · 不再需要手动粘贴,铸渊自动推送 |
|
||
|
||
---
|
||
|
||
## 六、安全边界
|
||
|
||
<aside>
|
||
🔒
|
||
|
||
**MAIN_REPO_TOKEN 权限范围**:仅限组织内仓库读写。铸渊不操作组织设置、不修改权限、不删除仓库。
|
||
|
||
**铸渊行为边界**:只推文件、只读状态、只触发 dispatch。不修改分支保护规则,不操作 PR 合并。
|
||
|
||
**开发者权限不变**:开发者仍然是自己仓库的 Owner/Admin,铸渊只是往里面放文件。
|
||
|
||
</aside>
|
||
|
||
---
|
||
|
||
> 🌉 本指令替代原 BC-REPO-INIT 的 Copilot 依赖路径。
|
||
>
|
||
|
||
> 铸渊从"等开发者粘贴"变成"自己主动推送"。
|
||
>
|
||
|
||
> 开发者唯一需要做的事:开启 Actions 权限(一次性)。
|
||
>
|
||
|
||
> 其他的,铸渊全部自己干。
|
||
> |