Merge pull request #198 from qinfendebingshuo/copilot/fix-website-config-issue

🛡️ SkyEye merge membrane v3: universal PR audit + developer portal sandbox system
This commit is contained in:
冰朔 2026-03-26 00:00:01 +08:00 committed by GitHub
commit f0be2f8f12
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 4944 additions and 47 deletions

View File

@ -1217,6 +1217,26 @@
"owner": "ICE-0002∞",
"registered": "2026-03-25",
"description": "每次系统执行后自动更新仪表盘数据 + README + GitHub Pages 实时仪表盘"
},
{
"id": "AG-ZY-095",
"name": "天眼合并膜引擎",
"type": "workflow",
"file": ".github/workflows/skyeye-pr-risk-check.yml",
"parent_sys": "SYS-GLW-0001",
"owner": "ICE-0002∞",
"registered": "2026-03-25",
"description": "天眼系统PR级合并膜 — 同账号沙箱隔离 + 全系统审核。R1关键文件/R2天眼区域入侵/R3构建产物/R4核心配置篡改/R5大规模删除/R6工作流篡改。物理层阻塞workflow失败=merge按钮禁用"
},
{
"id": "AG-ZY-096",
"name": "开发者门户频道部署引擎",
"type": "workflow",
"file": ".github/workflows/dev-portal-deploy.yml",
"parent_sys": "SYS-GLW-0001",
"owner": "ICE-0002∞",
"registered": "2026-03-25",
"description": "开发者频道部署后自动更新manifest.json + Notion画像数据库同步"
}
],
"note": "铸渊已扫描所有workflow文件按此格式逐一注册编号从AG-ZY-001开始顺序分配",
@ -1234,4 +1254,4 @@
"registered_by": "TCS-0002∞"
}
}
}
}

131
.github/workflows/dev-portal-deploy.yml vendored Normal file
View File

@ -0,0 +1,131 @@
# ═══════════════════════════════════════════════
# 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
# 📜 Copyright: 国作登字-2026-A-00037559
# ═══════════════════════════════════════════════
#
# 开发者门户部署工作流
# Developer Portal Channel Deploy
#
# 触发条件: PR 合并到 main 且修改了 docs/dev-portal/channels/
# 作用: 更新门户 manifest.json同步开发者动态到 Notion
#
name: 🌐 开发者门户 · 频道部署
on:
push:
branches: [main]
paths:
- 'docs/dev-portal/channels/**'
permissions:
contents: write
jobs:
update-manifest:
name: 📋 更新门户清单
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: 🔍 检测频道变更
id: detect
run: |
# Find which channels were updated
CHANGED=$(git diff --name-only HEAD~1 HEAD -- docs/dev-portal/channels/ || true)
if [ -z "$CHANGED" ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "无频道文件变更"
exit 0
fi
echo "skip=false" >> "$GITHUB_OUTPUT"
# Extract unique DEV-IDs
DEV_IDS=$(echo "$CHANGED" | grep -oP 'DEV-\d{3}' | sort -u | tr '\n' ',' | sed 's/,$//')
echo "dev_ids=$DEV_IDS" >> "$GITHUB_OUTPUT"
echo "变更频道: $DEV_IDS"
# Count files per channel
echo "$CHANGED" | while read f; do echo " · $f"; done
- name: 📋 更新 manifest.json
if: steps.detect.outputs.skip != 'true'
run: |
node -e "
const fs = require('fs');
const path = require('path');
const manifestPath = 'docs/dev-portal/manifest.json';
const configPath = '.github/persona-brain/gate-guard-config.json';
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
const devIds = '${{ steps.detect.outputs.dev_ids }}'.split(',').filter(Boolean);
const now = new Date().toISOString();
for (const devId of devIds) {
if (!manifest.channels[devId]) continue;
// Update channel info
const ch = manifest.channels[devId];
ch.status = 'active';
ch.last_deploy = now;
ch.deploy_count = (ch.deploy_count || 0) + 1;
// Scan channel directory for modules
const channelDir = path.join('docs/dev-portal/channels', devId);
if (fs.existsSync(channelDir)) {
const items = fs.readdirSync(channelDir).filter(f => f !== 'README.md' && f !== '.gitkeep');
ch.modules = items;
ch.last_deploy_summary = items.length + ' 个文件已部署';
}
// Add activity entry
const devConfig = config.developer_permissions && config.developer_permissions[devId];
manifest.recent_activity.unshift({
time: now,
dev_id: devId,
dev_name: devConfig ? devConfig.name : devId,
persona_id: devConfig ? devConfig.persona_id : null,
summary: '部署更新到 ' + devId + ' 频道'
});
}
// Keep only last 50 activity entries
manifest.recent_activity = manifest.recent_activity.slice(0, 50);
manifest.updated_at = now;
manifest.updated_by = 'dev-portal-deploy';
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
console.log('✅ manifest.json 已更新');
"
- name: 📡 同步开发者动态到 Notion
if: steps.detect.outputs.skip != 'true' && env.NOTION_API_KEY != ''
env:
NOTION_API_KEY: ${{ secrets.NOTION_API_KEY }}
DEV_PROFILE_DB_ID: ${{ secrets.DEV_PROFILE_DB_ID }}
run: |
node scripts/skyeye/dev-portal-notion-sync.js \
--dev-ids "${{ steps.detect.outputs.dev_ids }}" \
--action "channel_deploy" \
--timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|| echo "⚠️ Notion同步失败非致命"
- name: 💾 提交 manifest 更新
if: steps.detect.outputs.skip != 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add docs/dev-portal/manifest.json
git diff --cached --quiet && echo "无变更" || git commit -m "📋 开发者门户: 更新频道清单 [${{ steps.detect.outputs.dev_ids }}]"
git push 2>&1 || echo "⚠️ manifest 推送失败(非致命)"

View File

@ -0,0 +1,136 @@
# ═══════════════════════════════════════════════
# 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
# 📜 Copyright: 国作登字-2026-A-00037559
# ═══════════════════════════════════════════════
#
# 天眼 · 合并膜 (SkyEye Merge Membrane)
#
# 架构背景:
# 所有开发者共用 qinfendebingshuo 账号(企业权限单人仓库)
# 代理Copilot Agent以 copilot/* 分支开发(沙箱隔离)
# 合并到 main 时 → 天眼合并膜自动启动,全系统审核
# 审核不通过 → 物理层拒绝合并workflow 失败 = merge 按钮禁用)
#
# 沙箱隔离原则:
# 开发者自由开发(不在天眼包裹内)→ 天眼不干预
# 触及天眼包裹区域(.github/, scripts/, skyeye/, core/, docs/ 等)→ 必须通过审核
# 天眼核心配置security-protocol, gate-guard-config 等)→ 仅限主权者修改
#
# 风险检测维度:
# R1 · 关键文件覆盖检测
# R2 · 天眼包裹区域入侵检测
# R3 · 构建产物误入检测
# R4 · 天眼核心配置篡改检测
# R5 · 大规模删除检测
# R6 · 工作流篡改检测
#
# 物理层阻塞:
# 此工作流必须配置为 Required Status Check
# Settings → Branches → Branch protection rules → main → Require status checks
# 添加: "🛡️ 天眼合并膜" 为必需检查
#
name: 天眼 · 合并膜
on:
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
permissions:
contents: read
pull-requests: write
jobs:
merge-membrane:
name: 🛡️ 天眼合并膜
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Collect PR metadata
id: collect
env:
GH_TOKEN: ${{ github.token }}
run: |
echo "📂 收集PR变更文件列表..."
gh pr diff ${{ github.event.pull_request.number }} --name-only > /tmp/pr_files.txt
FILE_COUNT=$(wc -l < /tmp/pr_files.txt)
echo " 文件数: $FILE_COUNT"
echo ""
echo "📊 收集变更统计..."
git diff --numstat origin/${{ github.base_ref }}...HEAD > /tmp/pr_stats.txt 2>/dev/null || echo "" > /tmp/pr_stats.txt
echo ""
echo "📝 收集Commit元数据..."
git log origin/${{ github.base_ref }}..HEAD --format="%B---COMMIT_SEP---" > /tmp/pr_commits.txt 2>/dev/null || echo "" > /tmp/pr_commits.txt
echo ""
echo "📂 PR变更文件:"
cat /tmp/pr_files.txt
echo ""
echo "branch=${{ github.head_ref }}" >> "$GITHUB_OUTPUT"
- name: 🛡️ 天眼合并膜审核
id: membrane
env:
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
PR_BRANCH: ${{ github.head_ref }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_FILES: /tmp/pr_files.txt
PR_STATS: /tmp/pr_stats.txt
PR_COMMITS: /tmp/pr_commits.txt
run: node scripts/skyeye/pr-risk-check.js
- name: 📝 写入天眼审核报告
if: always()
uses: actions/github-script@v7
with:
script: |
const decision = '${{ steps.membrane.outcome }}';
const branch = '${{ github.head_ref }}';
const author = '${{ github.event.pull_request.user.login }}';
const prTitle = context.payload.pull_request.title;
const isAgentBranch = branch.startsWith('copilot/') || branch.startsWith('agent/') || branch.startsWith('bot/');
let body = '## 🛡️ 天眼 · 合并膜审核报告\n\n';
body += `| 项目 | 值 |\n`;
body += `|------|-----|\n`;
body += `| **PR作者** | ${author} |\n`;
body += `| **分支** | \`${branch}\` |\n`;
body += `| **代理PR** | ${isAgentBranch ? '是 🤖' : '否 👤'} |\n`;
body += `| **检查时间** | ${new Date().toISOString()} |\n\n`;
if (decision === 'success') {
body += '### ✅ 天眼全系统审核: 通过\n\n';
body += '此 PR 已通过天眼全局仓库架构审核,符合系统结构要求,可以合并。\n';
} else {
body += '### ❌ 天眼合并膜: 物理切断合并通道\n\n';
body += '此 PR 不符合仓库整体系统结构要求。\n\n';
body += '**处理方式:**\n';
body += '1. 🔍 检查上方 Actions 日志中标记的风险项\n';
body += '2. 📦 修复不符合仓库结构的变更\n';
body += '3. 🔄 推送修复后天眼将自动重新审核\n\n';
body += '> ⛔ 天眼合并膜已物理切断合并通道。\n';
}
body += '\n---\n';
body += '> 天眼 · 合并膜 (SkyEye Merge Membrane) · AG-ZY-095\n';
body += '> 「地球是圆的,没有缺口 — 语言系统包裹所有入口」';
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: body
});

View File

@ -27,7 +27,6 @@ jobs:
- name: Run contract check
id: contract
continue-on-error: true
run: node scripts/contract-check.js 2>&1 | tee /tmp/contract-result.txt
- name: Run route alignment

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
# DEV-001 频道沙箱

View File

@ -0,0 +1 @@
# DEV-002 频道沙箱

View File

@ -0,0 +1 @@
# DEV-003 频道沙箱

View File

@ -0,0 +1 @@
# DEV-004 频道沙箱

View File

@ -0,0 +1 @@
# DEV-005 频道沙箱

View File

@ -0,0 +1 @@
# DEV-009 频道沙箱

View File

@ -0,0 +1 @@
# DEV-010 频道沙箱

View File

@ -0,0 +1 @@
# DEV-011 频道沙箱

View File

@ -0,0 +1 @@
# DEV-012 频道沙箱

View File

@ -0,0 +1 @@
# DEV-013 频道沙箱

View File

@ -0,0 +1 @@
# DEV-014 频道沙箱

341
docs/dev-portal/index.html Normal file
View File

@ -0,0 +1,341 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<title>🌊 光湖开发者门户 · Developer Portal</title>
<style>
:root {
--bg: #0d1117;
--card: #161b22;
--border: #30363d;
--text: #e6edf3;
--text2: #8b949e;
--accent: #58a6ff;
--green: #3fb950;
--yellow: #d29922;
--red: #f85149;
--purple: #bc8cff;
--pink: #f778ba;
--orange: #f0883e;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', 'Segoe UI', sans-serif;
min-height: 100vh;
}
.header {
text-align: center;
padding: 2rem 1rem 1rem;
border-bottom: 1px solid var(--border);
background: linear-gradient(180deg, rgba(88,166,255,0.05) 0%, transparent 100%);
}
.header h1 { font-size: 1.8rem; margin-bottom: 0.5rem; }
.header h1 .emoji { font-size: 2rem; }
.header .subtitle { color: var(--text2); font-size: 0.95rem; margin-bottom: 0.5rem; }
.header .note {
color: var(--yellow);
font-size: 0.8rem;
padding: 0.3rem 0.8rem;
background: rgba(210,153,34,0.1);
border: 1px solid rgba(210,153,34,0.3);
border-radius: 4px;
display: inline-block;
margin-top: 0.5rem;
}
.nav-bar {
display: flex;
justify-content: center;
gap: 1rem;
padding: 0.8rem 1rem;
border-bottom: 1px solid var(--border);
flex-wrap: wrap;
}
.nav-bar a {
color: var(--text2);
text-decoration: none;
font-size: 0.85rem;
padding: 0.3rem 0.8rem;
border-radius: 4px;
transition: all 0.2s;
}
.nav-bar a:hover { background: var(--card); color: var(--accent); }
.stats-bar {
display: flex;
justify-content: center;
gap: 1.5rem;
padding: 1rem;
flex-wrap: wrap;
}
.stat-pill {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.4rem 1rem;
border-radius: 2rem;
background: var(--card);
border: 1px solid var(--border);
font-size: 0.85rem;
}
.stat-pill .dot {
width: 8px; height: 8px; border-radius: 50%;
}
.dot.green { background: var(--green); }
.dot.yellow { background: var(--yellow); }
.dot.purple { background: var(--purple); }
.section-title {
font-size: 1.1rem;
font-weight: 600;
padding: 1.5rem 1.5rem 0.5rem;
max-width: 1200px;
margin: 0 auto;
color: var(--text2);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
padding: 0.5rem 1.5rem 1.5rem;
max-width: 1200px;
margin: 0 auto;
}
.channel-card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1.2rem;
transition: all 0.2s;
cursor: pointer;
position: relative;
overflow: hidden;
}
.channel-card:hover {
border-color: var(--accent);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
.channel-card .status-badge {
position: absolute;
top: 0.8rem;
right: 0.8rem;
font-size: 0.7rem;
padding: 0.15rem 0.5rem;
border-radius: 3px;
font-weight: 600;
}
.status-badge.ready { background: rgba(63,185,80,0.15); color: var(--green); border: 1px solid rgba(63,185,80,0.3); }
.status-badge.active { background: rgba(88,166,255,0.15); color: var(--accent); border: 1px solid rgba(88,166,255,0.3); }
.status-badge.pending { background: rgba(139,148,158,0.15); color: var(--text2); border: 1px solid rgba(139,148,158,0.3); }
.channel-card .dev-id {
font-size: 0.75rem;
color: var(--accent);
background: rgba(88,166,255,0.1);
padding: 0.1rem 0.4rem;
border-radius: 3px;
display: inline-block;
margin-bottom: 0.4rem;
}
.channel-card .dev-name {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 0.3rem;
}
.channel-card .persona-id {
font-size: 0.75rem;
color: var(--purple);
margin-bottom: 0.6rem;
}
.channel-card .modules {
font-size: 0.8rem;
color: var(--text2);
margin-bottom: 0.4rem;
}
.channel-card .last-deploy {
font-size: 0.75rem;
color: var(--text2);
border-top: 1px solid var(--border);
padding-top: 0.5rem;
margin-top: 0.5rem;
}
.activity-list {
max-width: 1200px;
margin: 0 auto;
padding: 0.5rem 1.5rem 2rem;
}
.activity-item {
display: flex;
align-items: flex-start;
gap: 0.8rem;
padding: 0.8rem;
border-bottom: 1px solid var(--border);
font-size: 0.85rem;
}
.activity-item .time {
color: var(--text2);
font-size: 0.75rem;
white-space: nowrap;
min-width: 80px;
}
.activity-item .desc { flex: 1; }
.activity-item .dev-tag {
color: var(--accent);
font-weight: 600;
}
.empty-state {
text-align: center;
padding: 2rem;
color: var(--text2);
font-size: 0.9rem;
}
.footer {
text-align: center;
padding: 2rem 1rem;
color: var(--text2);
font-size: 0.75rem;
border-top: 1px solid var(--border);
}
.footer a { color: var(--accent); text-decoration: none; }
@media (max-width: 600px) {
.header h1 { font-size: 1.4rem; }
.grid { grid-template-columns: 1fr; padding: 0.5rem 1rem; }
}
</style>
</head>
<body>
<div class="header">
<h1><span class="emoji">🌊</span> 光湖开发者门户</h1>
<div class="subtitle">Developer Portal · 语言驱动操作系统体验站</div>
<div class="note">⚡ 备用站 · 开发者沙箱频道 · 每位开发者拥有独立部署空间</div>
</div>
<div class="nav-bar">
<a href="../index.html">← 铸渊助手</a>
<a href="../dashboard/">系统仪表盘</a>
<a href="#channels">开发者频道</a>
<a href="#activity">最近动态</a>
</div>
<div class="stats-bar" id="stats-bar">
<div class="stat-pill"><span class="dot green"></span> 加载中...</div>
</div>
<div class="section-title" id="channels">📡 开发者频道</div>
<div class="grid" id="channels-grid">
<div class="empty-state">加载中...</div>
</div>
<div class="section-title" id="activity">📋 最近部署动态</div>
<div class="activity-list" id="activity-list">
<div class="empty-state">暂无部署记录 — 等待第一位开发者部署模块 🚀</div>
</div>
<div class="footer">
<p>光湖开发者门户 · Developer Portal v1.0</p>
<p>天眼系统保护 · 每位开发者仅可部署到自己的频道沙箱</p>
<p>主站: <a href="https://guanghulab.com">guanghulab.com</a> · 仪表盘: <a href="../dashboard/">Dashboard</a></p>
<p style="margin-top:0.5rem;color:var(--purple)">🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001 | ©国作登字-2026-A-00037559</p>
</div>
<script>
(function() {
'use strict';
var MANIFEST_URL = './manifest.json';
var RAW_MANIFEST_URL = 'https://raw.githubusercontent.com/qinfendebingshuo/guanghulab/main/docs/dev-portal/manifest.json';
function fetchManifest() {
return fetch(MANIFEST_URL + '?t=' + Date.now())
.then(function(r) { return r.ok ? r.json() : Promise.reject('local fail'); })
.catch(function() {
return fetch(RAW_MANIFEST_URL + '?t=' + Date.now())
.then(function(r) { return r.ok ? r.json() : Promise.reject('remote fail'); });
});
}
function renderStats(manifest) {
var channels = manifest.channels || {};
var keys = Object.keys(channels);
var total = keys.length;
var active = keys.filter(function(k) { return channels[k].deploy_count > 0; }).length;
var ready = keys.filter(function(k) { return channels[k].status === 'ready'; }).length;
var totalDeploys = keys.reduce(function(s, k) { return s + (channels[k].deploy_count || 0); }, 0);
var el = document.getElementById('stats-bar');
el.innerHTML =
'<div class="stat-pill"><span class="dot green"></span> 频道总数: ' + total + '</div>' +
'<div class="stat-pill"><span class="dot purple"></span> 活跃频道: ' + active + '</div>' +
'<div class="stat-pill"><span class="dot yellow"></span> 就绪: ' + ready + '</div>' +
'<div class="stat-pill">🚀 累计部署: ' + totalDeploys + '</div>';
}
function renderChannels(manifest) {
var channels = manifest.channels || {};
var grid = document.getElementById('channels-grid');
var keys = Object.keys(channels).sort();
if (keys.length === 0) {
grid.innerHTML = '<div class="empty-state">暂无开发者频道</div>';
return;
}
grid.innerHTML = keys.map(function(devId) {
var ch = channels[devId];
var statusClass = ch.deploy_count > 0 ? 'active' : ch.status;
var statusText = ch.deploy_count > 0 ? '活跃' : (ch.status === 'ready' ? '就绪' : '待激活');
var modules = (ch.modules && ch.modules.length > 0) ? ch.modules.join(', ') : '暂无模块';
var lastDeploy = ch.last_deploy
? '最近部署: ' + new Date(ch.last_deploy).toLocaleString('zh-CN') + (ch.last_deploy_summary ? ' · ' + ch.last_deploy_summary : '')
: '等待首次部署';
return '<div class="channel-card" onclick="location.href=\'channels/' + devId + '/\'">' +
'<div class="status-badge ' + statusClass + '">' + statusText + '</div>' +
'<div class="dev-id">' + devId + '</div>' +
'<div class="dev-name">' + (ch.name || devId) + '</div>' +
'<div class="persona-id">👤 ' + (ch.persona_id || '—') + '</div>' +
'<div class="modules">📦 ' + modules + '</div>' +
'<div class="last-deploy">' + lastDeploy + '</div>' +
'</div>';
}).join('');
}
function renderActivity(manifest) {
var list = document.getElementById('activity-list');
var activity = manifest.recent_activity || [];
if (activity.length === 0) {
list.innerHTML = '<div class="empty-state">暂无部署记录 — 等待第一位开发者部署模块 🚀</div>';
return;
}
list.innerHTML = activity.slice(0, 20).map(function(a) {
var time = a.time ? new Date(a.time).toLocaleString('zh-CN') : '—';
return '<div class="activity-item">' +
'<div class="time">' + time + '</div>' +
'<div class="desc"><span class="dev-tag">' + (a.dev_id || '?') + ' ' + (a.dev_name || '') + '</span> ' + (a.summary || '') + '</div>' +
'</div>';
}).join('');
}
fetchManifest()
.then(function(manifest) {
renderStats(manifest);
renderChannels(manifest);
renderActivity(manifest);
})
.catch(function(err) {
document.getElementById('stats-bar').innerHTML = '<div class="stat-pill"><span class="dot yellow"></span> 清单加载失败</div>';
document.getElementById('channels-grid').innerHTML = '<div class="empty-state">⚠️ 无法加载频道数据</div>';
console.error('Portal manifest load error:', err);
});
// Auto-refresh every 5 minutes
setInterval(function() {
fetchManifest().then(function(m) { renderStats(m); renderChannels(m); renderActivity(m); }).catch(function() {});
}, 300000);
})();
</script>
</body>
</html>

View File

@ -0,0 +1,108 @@
{
"portal_version": "1.0.0",
"updated_at": "2026-03-25T15:00:00Z",
"updated_by": "system",
"description": "光湖开发者门户 · 频道状态清单",
"channels": {
"DEV-001": {
"name": "页页",
"persona_id": "PER-001",
"status": "ready",
"modules": [],
"last_deploy": null,
"last_deploy_summary": null,
"deploy_count": 0
},
"DEV-002": {
"name": "肥猫",
"persona_id": "PER-002",
"status": "ready",
"modules": [],
"last_deploy": null,
"last_deploy_summary": null,
"deploy_count": 0
},
"DEV-003": {
"name": "燕樊",
"persona_id": "PER-003",
"status": "ready",
"modules": [],
"last_deploy": null,
"last_deploy_summary": null,
"deploy_count": 0
},
"DEV-004": {
"name": "之之",
"persona_id": "PER-004",
"status": "ready",
"modules": [],
"last_deploy": null,
"last_deploy_summary": null,
"deploy_count": 0
},
"DEV-005": {
"name": "小草莓",
"persona_id": "PER-005",
"status": "ready",
"modules": [],
"last_deploy": null,
"last_deploy_summary": null,
"deploy_count": 0
},
"DEV-009": {
"name": "花尔",
"persona_id": "PER-009",
"status": "ready",
"modules": [],
"last_deploy": null,
"last_deploy_summary": null,
"deploy_count": 0
},
"DEV-010": {
"name": "桔子",
"persona_id": "PER-010",
"status": "ready",
"modules": [],
"last_deploy": null,
"last_deploy_summary": null,
"deploy_count": 0
},
"DEV-011": {
"name": "匆匆那年",
"persona_id": "PER-011",
"status": "ready",
"modules": [],
"last_deploy": null,
"last_deploy_summary": null,
"deploy_count": 0
},
"DEV-012": {
"name": "Awen",
"persona_id": "PER-012",
"status": "ready",
"modules": [],
"last_deploy": null,
"last_deploy_summary": null,
"deploy_count": 0
},
"DEV-013": {
"name": "小兴",
"persona_id": "PER-013",
"status": "pending",
"modules": [],
"last_deploy": null,
"last_deploy_summary": null,
"deploy_count": 0
},
"DEV-014": {
"name": "时雨",
"persona_id": "PER-014",
"status": "pending",
"modules": [],
"last_deploy": null,
"last_deploy_summary": null,
"deploy_count": 0
}
},
"recent_activity": []
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,296 @@
#!/usr/bin/env node
// scripts/skyeye/dev-portal-guard.js
// 天眼·开发者门户沙箱守卫 (Dev Portal Sandbox Guard)
//
// ═══════════════════════════════════════════════
// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
// 📜 Copyright: 国作登字-2026-A-00037559
// ═══════════════════════════════════════════════
//
// 目的:保护开发者门户的沙箱隔离
// 每位开发者只能部署到自己的频道 (channels/DEV-XXX/)
// 门户框架文件 (index.html, manifest.json) 受保护
// 必须提供完整身份三要素:开发者编号 + 姓名 + 人格体编号
// 匿名代理(无身份信息)= 禁止部署到任何真实路径
//
// 输入环境变量:
// PR_BRANCH — 分支名
// PR_TITLE — PR 标题
// PR_COMMITS — Commit 信息路径
// PR_FILES — 变更文件列表路径
// NOTION_API_KEY — Notion API 密钥(可选,用于跟踪)
// DEV_PROFILE_DB_ID — 开发者画像数据库 ID可选
//
// 输出:
// exit 0 → 允许部署
// exit 1 → 禁止部署
// GITHUB_OUTPUT → decision, dev_id, dev_name, persona_id, channel_files
'use strict';
const fs = require('fs');
const path = require('path');
const ROOT = path.resolve(__dirname, '../..');
const BRAIN_CONFIG_PATH = path.join(ROOT, '.github/persona-brain/gate-guard-config.json');
const MANIFEST_PATH = path.join(ROOT, 'docs/dev-portal/manifest.json');
const PR_FILES_PATH = process.env.PR_FILES || '/tmp/pr_files.txt';
const PR_COMMITS_PATH = process.env.PR_COMMITS || '/tmp/pr_commits.txt';
const GITHUB_OUTPUT = process.env.GITHUB_OUTPUT || '/dev/null';
// ━━━ 门户保护文件 — 开发者不可修改 ━━━
const PORTAL_FRAMEWORK_FILES = [
'docs/dev-portal/index.html',
'docs/dev-portal/manifest.json'
];
// ━━━ 安全读取 JSON ━━━
function readJSON(filePath) {
try {
if (!fs.existsSync(filePath)) return null;
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch (e) {
return null;
}
}
function setOutput(key, value) {
try {
fs.appendFileSync(GITHUB_OUTPUT, `${key}=${value}\n`);
} catch (e) { /* silent */ }
}
// ━━━ 从 PR 元数据中提取身份三要素 ━━━
function extractIdentity(prTitle, commitsPath) {
const identity = {
devId: null,
devName: null,
personaId: null,
complete: false,
source: []
};
// From PR title
if (prTitle) {
const devMatch = prTitle.match(/\b(DEV-\d{3})\b/i);
if (devMatch) { identity.devId = devMatch[1].toUpperCase(); identity.source.push('pr_title'); }
}
// From commit messages
try {
if (fs.existsSync(commitsPath)) {
const content = fs.readFileSync(commitsPath, 'utf8');
// DEV-XXX
if (!identity.devId) {
const devMatch = content.match(/\b(DEV-\d{3})\b/i);
if (devMatch) { identity.devId = devMatch[1].toUpperCase(); identity.source.push('commit_msg'); }
}
// Persona signature [PER-XXX]
const perMatch = content.match(/\[(PER-\d{3})\]/);
if (perMatch) { identity.personaId = perMatch[1]; identity.source.push('commit_sig'); }
// TCS-GL-XX format
const tcsMatch = content.match(/\b(TCS-GL-\d{2})\b/i);
if (tcsMatch && !identity.devId) { identity.devId = tcsMatch[1].toUpperCase(); identity.source.push('tcs_ref'); }
}
} catch (e) { /* non-fatal */ }
// From changed file paths — infer DEV-XXX from channel paths
try {
if (fs.existsSync(PR_FILES_PATH)) {
const files = fs.readFileSync(PR_FILES_PATH, 'utf8').split('\n').filter(Boolean);
for (const file of files) {
const channelMatch = file.match(/^docs\/dev-portal\/channels\/(DEV-\d{3})\//);
if (channelMatch) {
if (!identity.devId) {
identity.devId = channelMatch[1];
identity.source.push('file_path');
}
break;
}
}
}
} catch (e) { /* non-fatal */ }
// Cross-reference with gate-guard config for name and persona_id
if (identity.devId) {
const config = readJSON(BRAIN_CONFIG_PATH);
if (config && config.developer_permissions && config.developer_permissions[identity.devId]) {
const dev = config.developer_permissions[identity.devId];
identity.devName = dev.name;
if (!identity.personaId) identity.personaId = dev.persona_id;
}
}
// Completeness check: need all three (devId + devName + personaId)
identity.complete = !!(identity.devId && identity.devName && identity.personaId);
return identity;
}
// ━━━ 读取变更文件列表 ━━━
function loadPRFiles() {
try {
if (!fs.existsSync(PR_FILES_PATH)) return [];
return fs.readFileSync(PR_FILES_PATH, 'utf8')
.split('\n')
.map(f => f.trim())
.filter(f => f.length > 0);
} catch (e) {
return [];
}
}
// ━━━ 分类文件 ━━━
function classifyFiles(files) {
const result = {
portalFramework: [], // 门户框架文件(保护)
ownChannel: [], // 自己频道的文件
otherChannels: [], // 他人频道的文件
outsidePortal: [], // 门户以外的文件
channelDevIds: new Set() // 涉及的频道编号
};
for (const file of files) {
if (PORTAL_FRAMEWORK_FILES.includes(file)) {
result.portalFramework.push(file);
} else if (file.startsWith('docs/dev-portal/channels/')) {
const channelMatch = file.match(/^docs\/dev-portal\/channels\/(DEV-\d{3})\//);
if (channelMatch) {
result.channelDevIds.add(channelMatch[1]);
result.ownChannel.push(file); // Will be re-classified after identity check
}
} else if (file.startsWith('docs/dev-portal/')) {
result.portalFramework.push(file); // Any other dev-portal file is framework
} else {
result.outsidePortal.push(file);
}
}
return result;
}
// ━━━ 主逻辑 ━━━
function run() {
const branch = process.env.PR_BRANCH || '';
const prTitle = process.env.PR_TITLE || '';
console.log('═══════════════════════════════════════════════');
console.log('🛡️ 天眼 · 开发者门户沙箱守卫');
console.log('═══════════════════════════════════════════════');
console.log(`分支: ${branch}`);
console.log(`标题: ${prTitle}`);
console.log(`时间: ${new Date().toISOString()}`);
console.log('');
// ─── 提取身份三要素 ───
console.log('━━━ 身份验证 ━━━');
const identity = extractIdentity(prTitle, PR_COMMITS_PATH);
if (identity.devId) console.log(` 开发者编号: ${identity.devId}`);
if (identity.devName) console.log(` 开发者姓名: ${identity.devName}`);
if (identity.personaId) console.log(` 人格体编号: ${identity.personaId}`);
console.log(` 身份来源: ${identity.source.join(', ') || '无'}`);
console.log(` 三要素完整: ${identity.complete ? '✅ 是' : '❌ 否'}`);
console.log('');
// ─── 匿名屏蔽 ───
if (!identity.complete) {
console.log('❌ 身份验证失败 — 匿名代理禁止部署');
console.log('');
console.log(' 缺失信息:');
if (!identity.devId) console.log(' · 开发者编号 (DEV-XXX)');
if (!identity.devName) console.log(' · 开发者姓名');
if (!identity.personaId) console.log(' · 人格体编号 (PER-XXX)');
console.log('');
console.log(' 提示: 在 PR 标题或 commit 信息中包含 DEV-XXX 和 [PER-XXX]');
console.log(' 例: "DEV-002: deploy login module" 或 "[PER-002] feat: add feature"');
setOutput('decision', 'block');
setOutput('block_reason', 'anonymous_agent');
process.exit(1);
}
// ─── 分类变更文件 ───
const files = loadPRFiles();
if (files.length === 0) {
console.log('⚠️ 无变更文件 — 放行');
setOutput('decision', 'pass');
setOutput('dev_id', identity.devId);
process.exit(0);
}
const classified = classifyFiles(files);
const portalFiles = files.filter(f => f.startsWith('docs/dev-portal/'));
if (portalFiles.length === 0) {
console.log(' 此 PR 不涉及门户文件 — 跳过门户守卫');
setOutput('decision', 'skip');
setOutput('dev_id', identity.devId);
process.exit(0);
}
console.log('━━━ 文件分类 ━━━');
console.log(` 门户框架文件: ${classified.portalFramework.length}`);
classified.portalFramework.forEach(f => console.log(` 🔒 ${f}`));
console.log(` 频道文件: ${classified.ownChannel.length}`);
console.log(` 涉及频道: ${[...classified.channelDevIds].join(', ') || '无'}`);
console.log(` 门户外文件: ${classified.outsidePortal.length}`);
console.log('');
// ─── 检查框架文件保护 ───
if (classified.portalFramework.length > 0) {
console.log('❌ 禁止修改门户框架文件:');
classified.portalFramework.forEach(f => console.log(` 🔒 ${f}`));
console.log('');
console.log(' 门户框架文件由天眼系统维护,开发者不可修改');
setOutput('decision', 'block');
setOutput('block_reason', 'framework_tampering');
setOutput('dev_id', identity.devId);
process.exit(1);
}
// ─── 检查频道隔离 ───
const channelIds = [...classified.channelDevIds];
const touchesOtherChannels = channelIds.some(id => id !== identity.devId);
if (touchesOtherChannels) {
const otherChannels = channelIds.filter(id => id !== identity.devId);
console.log('❌ 频道隔离违规 — 修改了他人频道:');
otherChannels.forEach(id => console.log(` 🚫 ${id} (不属于 ${identity.devId})`));
console.log('');
console.log(' 每位开发者只能部署到自己的频道沙箱');
setOutput('decision', 'block');
setOutput('block_reason', 'channel_isolation');
setOutput('dev_id', identity.devId);
process.exit(1);
}
// ─── 验证频道归属 ───
if (channelIds.length === 1 && channelIds[0] === identity.devId) {
const channelFiles = classified.ownChannel;
console.log(`✅ 频道验证通过 — ${identity.devName}(${identity.devId}) → channels/${identity.devId}/`);
console.log(` 部署文件: ${channelFiles.length}`);
channelFiles.forEach(f => console.log(` 📦 ${f}`));
console.log('');
setOutput('decision', 'pass');
setOutput('dev_id', identity.devId);
setOutput('dev_name', identity.devName);
setOutput('persona_id', identity.personaId);
setOutput('channel_files', channelFiles.join(','));
console.log('═══ 决策: PASS ═══');
console.log(` ${identity.devName} 的模块可以部署到 channels/${identity.devId}/`);
process.exit(0);
}
// Default pass for edge cases
console.log('✅ 门户守卫检查通过');
setOutput('decision', 'pass');
setOutput('dev_id', identity.devId);
process.exit(0);
}
run();

View File

@ -0,0 +1,193 @@
#!/usr/bin/env node
// scripts/skyeye/dev-portal-notion-sync.js
// 开发者门户 · Notion 动态同步
//
// ═══════════════════════════════════════════════
// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
// 📜 Copyright: 国作登字-2026-A-00037559
// ═══════════════════════════════════════════════
//
// 目的:将开发者频道部署动态同步到 Notion 画像数据库
// 铁律(与 write-receipt-to-notion.js 一致):
// 1. 写入失败不阻塞主流程
// 2. 同一事件幂等
// 3. 失败时写入本地 SYSLOG
//
// 环境变量:
// NOTION_API_KEY — Notion API 密钥(必须)
// DEV_PROFILE_DB_ID — 开发者画像数据库 ID必须
//
// 参数:
// --dev-ids "DEV-001,DEV-002" — 涉及的开发者编号
// --action "channel_deploy" — 操作类型
// --timestamp "ISO-8601" — 时间戳
'use strict';
const fs = require('fs');
const path = require('path');
const https = require('https');
const NOTION_API_KEY = process.env.NOTION_API_KEY || '';
const DEV_PROFILE_DB_ID = process.env.DEV_PROFILE_DB_ID || '';
const ROOT = path.resolve(__dirname, '../..');
const SYSLOG_DIR = path.join(ROOT, 'data/neural-reports/syslog');
// ━━━ 参数解析 ━━━
function parseArgs() {
const args = process.argv.slice(2);
const result = { devIds: [], action: 'unknown', timestamp: new Date().toISOString() };
for (let i = 0; i < args.length; i++) {
if (args[i] === '--dev-ids' && args[i + 1]) {
result.devIds = args[++i].split(',').filter(Boolean);
} else if (args[i] === '--action' && args[i + 1]) {
result.action = args[++i];
} else if (args[i] === '--timestamp' && args[i + 1]) {
result.timestamp = args[++i];
}
}
return result;
}
// ━━━ 加载门禁配置获取开发者信息 ━━━
function loadDevInfo(devId) {
try {
const configPath = path.join(ROOT, '.github/persona-brain/gate-guard-config.json');
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
const dev = config.developer_permissions && config.developer_permissions[devId];
return dev || null;
} catch (e) {
return null;
}
}
// ━━━ Notion API 请求 ━━━
function notionRequest(endpoint, body) {
return new Promise(function(resolve, reject) {
if (!NOTION_API_KEY) {
reject(new Error('NOTION_API_KEY 未配置'));
return;
}
const data = JSON.stringify(body);
const options = {
hostname: 'api.notion.com',
path: endpoint,
method: 'POST',
headers: {
'Authorization': 'Bearer ' + NOTION_API_KEY,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(data)
}
};
const req = https.request(options, function(res) {
let body = '';
res.on('data', function(chunk) { body += chunk; });
res.on('end', function() {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(JSON.parse(body));
} else {
reject(new Error('Notion API ' + res.statusCode + ': ' + body.slice(0, 200)));
}
});
});
req.on('error', reject);
req.setTimeout(15000, function() { req.destroy(new Error('Notion API timeout')); });
req.write(data);
req.end();
});
}
// ━━━ 写入 SYSLOG ━━━
function writeSyslog(entry) {
try {
if (!fs.existsSync(SYSLOG_DIR)) fs.mkdirSync(SYSLOG_DIR, { recursive: true });
const date = new Date().toISOString().slice(0, 10);
const logPath = path.join(SYSLOG_DIR, 'dev-portal-sync-' + date + '.jsonl');
fs.appendFileSync(logPath, JSON.stringify(entry) + '\n');
} catch (e) {
console.error('SYSLOG写入失败:', e.message);
}
}
// ━━━ 同步单个开发者动态到 Notion ━━━
async function syncDevActivity(devId, action, timestamp) {
const devInfo = loadDevInfo(devId);
const properties = {
'开发者编号': { title: [{ text: { content: devId } }] },
'开发者姓名': { rich_text: [{ text: { content: devInfo ? devInfo.name : devId } }] },
'人格体编号': { rich_text: [{ text: { content: devInfo ? devInfo.persona_id : '—' } }] },
'操作类型': { select: { name: action } },
'操作时间': { date: { start: timestamp } },
'门户频道': { rich_text: [{ text: { content: 'channels/' + devId + '/' } }] }
};
try {
await notionRequest('/v1/pages', {
parent: { database_id: DEV_PROFILE_DB_ID },
properties: properties
});
console.log(' ✅ ' + devId + ' 同步成功');
return true;
} catch (e) {
console.error(' ⚠️ ' + devId + ' 同步失败: ' + e.message);
writeSyslog({
time: new Date().toISOString(),
type: 'notion_sync_error',
dev_id: devId,
error: e.message
});
return false;
}
}
// ━━━ 主逻辑 ━━━
async function main() {
const args = parseArgs();
console.log('📡 开发者门户 · Notion 动态同步');
console.log(' 开发者: ' + (args.devIds.join(', ') || '无'));
console.log(' 操作: ' + args.action);
console.log(' 时间: ' + args.timestamp);
console.log('');
if (args.devIds.length === 0) {
console.log('⚠️ 无开发者编号,跳过同步');
process.exit(0);
}
if (!NOTION_API_KEY || !DEV_PROFILE_DB_ID) {
console.log('⚠️ Notion 配置缺失 (NOTION_API_KEY / DEV_PROFILE_DB_ID)');
console.log(' 动态将写入本地 SYSLOG');
writeSyslog({
time: args.timestamp,
type: 'channel_deploy',
dev_ids: args.devIds,
action: args.action,
note: 'Notion未配置写入本地'
});
process.exit(0);
}
let success = 0;
for (const devId of args.devIds) {
const ok = await syncDevActivity(devId, args.action, args.timestamp);
if (ok) success++;
}
console.log('');
console.log('同步结果: ' + success + '/' + args.devIds.length + ' 成功');
}
main().catch(function(e) {
console.error('❌ 同步异常: ' + e.message);
writeSyslog({ time: new Date().toISOString(), type: 'sync_crash', error: e.message });
// Non-fatal — don't block the deploy
process.exit(0);
});

View File

@ -0,0 +1,497 @@
#!/usr/bin/env node
// scripts/skyeye/pr-risk-check.js
// 天眼·合并膜 — PR合并前全系统审核引擎 (SkyEye Merge Membrane)
//
// ═══════════════════════════════════════════════
// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
// 📜 Copyright: 国作登字-2026-A-00037559
// ═══════════════════════════════════════════════
//
// 核心原则(冰朔 2026-03-25 确认):
// 所有合并到 main 的 PR — 无论冰朔还是其他开发者
// 均全部启动天眼系统全局全仓库架构审核
// 不符合现有仓库整体系统结构 → 物理切断合并通道
// 不再区分身份放行 — 天眼对所有人一视同仁
//
// 架构背景:
// 两条完全分离的部署路径:
// Path A · 主站 (guanghulab.com) — 全PR审核物理阻塞
// Path B · 开发者门户 (dev-portal/) — 沙箱隔离,频道部署
//
// 风险检测维度全PR适用
// R1 · 关键文件覆盖检测
// R2 · 构建产物误入检测
// R3 · 大规模删除检测
// R4 · 天眼核心配置篡改检测仅代理PR触发 critical
// R5 · 工作流篡改检测仅代理PR触发 critical
// R6 · 开发者门户框架保护
// R7 · 仓库结构完整性验证全PR适用 — 核心审计维度)
//
// 输入环境变量:
// PR_AUTHOR, PR_BRANCH, PR_TITLE
// PR_FILES, PR_STATS, PR_COMMITS
//
// 输出:
// exit 0 → pass允许合并
// exit 1 → block物理层拒绝合并
'use strict';
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// ━━━ 配置路径 ━━━
const ROOT = path.resolve(__dirname, '../..');
const BRAIN_CONFIG_PATH = path.join(ROOT, '.github/persona-brain/gate-guard-config.json');
const OWNER_CONFIG_PATH = path.join(ROOT, '.github/gate-guard-config.json');
const SECURITY_PROTOCOL_PATH = path.join(ROOT, '.github/persona-brain/security-protocol.json');
const PR_FILES_PATH = process.env.PR_FILES || '/tmp/pr_files.txt';
const PR_STATS_PATH = process.env.PR_STATS || '/tmp/pr_stats.txt';
const PR_COMMITS_PATH = process.env.PR_COMMITS || '/tmp/pr_commits.txt';
const GITHUB_OUTPUT = process.env.GITHUB_OUTPUT || '/dev/null';
// ━━━ Copilot Agent 分支模式 ━━━
const AGENT_BRANCH_PATTERNS = [
/^copilot\//,
/^agent\//,
/^bot\//
];
// ━━━ 天眼核心配置 — 代理PR修改触发 critical ━━━
const SKYEYE_CORE_FILES = [
'.github/persona-brain/security-protocol.json',
'.github/persona-brain/gate-guard-config.json',
'.github/persona-brain/agent-registry.json',
'.github/persona-brain/ontology.json',
'.github/gate-guard-config.json',
'scripts/gate-guard.js',
'scripts/gate-guard-v2.js',
'scripts/skyeye/pr-risk-check.js'
];
// ━━━ 门户框架文件 — 不可被开发者修改 ━━━
const PORTAL_FRAMEWORK_FILES = [
'docs/dev-portal/index.html',
'docs/dev-portal/manifest.json'
];
// ━━━ 构建产物特征 ━━━
const BUILD_ARTIFACT_PATTERNS = [
/^docs\/assets\/index-[A-Za-z0-9_-]+\.(js|css)$/,
/^dist\//,
/^build\//,
/^\.next\//,
/^node_modules\//,
/\.chunk\.(js|css)$/,
/\.bundle\.(js|css)$/
];
// ━━━ 仓库必需目录 — R7 结构完整性检查 ━━━
const REQUIRED_DIRS = [
'.github/workflows',
'.github/persona-brain',
'scripts',
'scripts/skyeye',
'skyeye',
'skyeye/guards',
'skyeye/scripts',
'core',
'docs',
'data'
];
// ━━━ 仓库必需文件 — R7 结构完整性检查 ━━━
const REQUIRED_FILES = [
'.github/persona-brain/security-protocol.json',
'.github/persona-brain/gate-guard-config.json',
'.github/persona-brain/agent-registry.json',
'docs/index.html',
'docs/CNAME',
'README.md',
'package.json'
];
// ━━━ 大规模删除阈值 ━━━
const MASS_DELETE_THRESHOLD = 500;
// ━━━ 工具函数 ━━━
function readJSON(filePath) {
try {
if (!fs.existsSync(filePath)) return null;
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch (e) {
console.error(`⚠️ 无法读取 ${path.basename(filePath)}: ${e.message}`);
return null;
}
}
function setOutput(key, value) {
try {
fs.appendFileSync(GITHUB_OUTPUT, `${key}=${value}\n`);
} catch (e) { /* silent */ }
}
function isAgentBranch(branchName) {
if (!branchName) return false;
return AGENT_BRANCH_PATTERNS.some(p => p.test(branchName));
}
function loadPRFiles() {
try {
if (!fs.existsSync(PR_FILES_PATH)) return [];
return fs.readFileSync(PR_FILES_PATH, 'utf8')
.split('\n').map(f => f.trim()).filter(f => f.length > 0);
} catch (e) { return []; }
}
function loadPRStats() {
try {
if (!fs.existsSync(PR_STATS_PATH)) return null;
const content = fs.readFileSync(PR_STATS_PATH, 'utf8').trim();
let totalDeletions = 0;
for (const line of content.split('\n')) {
const parts = line.split('\t');
if (parts.length >= 2) {
const del = parseInt(parts[1], 10);
if (!isNaN(del)) totalDeletions += del;
}
}
return { totalDeletions };
} catch (e) { return null; }
}
// ━━━ 身份提取(用于日志/上下文,不用于放行决策)━━━
function extractIdentity(commitsPath, prTitle) {
const identity = { isAgent: false, agentName: null, devId: null, source: 'unknown' };
if (prTitle) {
const devMatch = prTitle.match(/\b(DEV-\d{3})\b/i);
if (devMatch) { identity.devId = devMatch[1].toUpperCase(); identity.source = 'pr_title'; }
}
try {
if (fs.existsSync(commitsPath)) {
const content = fs.readFileSync(commitsPath, 'utf8');
if (content.includes('Agent-Logs-Url:')) { identity.isAgent = true; identity.source = 'agent_logs_url'; }
const coAuth = content.match(/Co-authored-by:\s*(.+?)\s*</);
if (coAuth) { identity.agentName = coAuth[1].trim(); identity.isAgent = true; }
if (!identity.devId) {
const devMatch = content.match(/\b(DEV-\d{3})\b/i);
if (devMatch) { identity.devId = devMatch[1].toUpperCase(); }
}
}
} catch (e) { /* non-fatal */ }
return identity;
}
// ━━━ R1 · 关键文件覆盖检测 ━━━
function checkCriticalFiles(files) {
const risks = [];
const criticalFiles = [
'docs/index.html', 'docs/CNAME', 'docs/.nojekyll',
'README.md', 'package.json', 'package-lock.json'
];
for (const file of files) {
if (criticalFiles.includes(file)) {
risks.push({ dimension: 'R1', severity: 'high', file,
detail: `关键文件被修改: ${file}` });
}
}
return risks;
}
// ━━━ R2 · 构建产物误入检测 ━━━
function checkBuildArtifacts(files) {
const risks = [];
for (const file of files) {
for (const pattern of BUILD_ARTIFACT_PATTERNS) {
if (pattern.test(file)) {
risks.push({ dimension: 'R2', severity: 'high', file,
detail: `构建产物不应提交到仓库: ${file}` });
break;
}
}
}
return risks;
}
// ━━━ R3 · 大规模删除检测 ━━━
function checkMassDeletion(stats) {
const risks = [];
if (stats && stats.totalDeletions > MASS_DELETE_THRESHOLD) {
risks.push({ dimension: 'R3', severity: 'high', file: '(multiple)',
detail: `大规模删除: ${stats.totalDeletions} 行被删除(阈值: ${MASS_DELETE_THRESHOLD}` });
}
return risks;
}
// ━━━ R4 · 天眼核心配置篡改检测代理PR = critical手动PR = high ━━━
function checkCoreConfigTampering(files, isAgent) {
const risks = [];
for (const file of files) {
if (SKYEYE_CORE_FILES.includes(file)) {
risks.push({ dimension: 'R4', severity: isAgent ? 'critical' : 'high', file,
detail: `天眼核心配置被修改: ${file}` });
}
}
return risks;
}
// ━━━ R5 · 工作流篡改检测代理PR = critical手动PR = high ━━━
function checkWorkflowTampering(files, isAgent) {
const risks = [];
for (const file of files) {
if (file.startsWith('.github/workflows/') && (file.endsWith('.yml') || file.endsWith('.yaml'))) {
risks.push({ dimension: 'R5', severity: isAgent ? 'critical' : 'high', file,
detail: `工作流文件被修改: ${file}` });
}
}
return risks;
}
// ━━━ R6 · 开发者门户框架保护 ━━━
function checkPortalFramework(files, isAgent) {
if (!isAgent) return []; // Owner can modify portal framework
const risks = [];
for (const file of files) {
if (PORTAL_FRAMEWORK_FILES.includes(file)) {
risks.push({ dimension: 'R6', severity: 'critical', file,
detail: `门户框架文件被修改: ${file} — 仅限天眼系统维护` });
}
}
return risks;
}
// ━━━ R7 · 仓库结构完整性验证(核心审计维度 — 对所有PR生效━━━
function checkStructuralIntegrity(files) {
const risks = [];
// 7a. Security protocol must remain intact
const protocol = readJSON(SECURITY_PROTOCOL_PATH);
if (!protocol) {
risks.push({ dimension: 'R7', severity: 'critical', file: '.github/persona-brain/security-protocol.json',
detail: '安全协议文件不存在或无法解析 — 仓库安全基础被破坏' });
} else {
if (!protocol.permanent || !protocol.cannot_be_overridden) {
risks.push({ dimension: 'R7', severity: 'critical', file: '.github/persona-brain/security-protocol.json',
detail: '安全协议 permanent/cannot_be_overridden 标记丢失' });
}
if (!protocol.root_rules || protocol.root_rules.length < 3) {
risks.push({ dimension: 'R7', severity: 'critical', file: '.github/persona-brain/security-protocol.json',
detail: '安全协议 root_rules 不完整(需 3 条根规则)' });
}
}
// 7b. Gate guard config must remain valid
const brainConfig = readJSON(BRAIN_CONFIG_PATH);
const ownerConfig = readJSON(OWNER_CONFIG_PATH);
if (!brainConfig && !ownerConfig) {
risks.push({ dimension: 'R7', severity: 'critical', file: '(gate-guard-config)',
detail: '门禁配置文件全部缺失 — 系统安全体系被破坏' });
}
// 7c. Check that required directories still exist
for (const dir of REQUIRED_DIRS) {
const dirPath = path.join(ROOT, dir);
if (!fs.existsSync(dirPath)) {
risks.push({ dimension: 'R7', severity: 'high', file: dir + '/',
detail: `必需目录缺失: ${dir}/` });
}
}
// 7d. Check that required files still exist
for (const file of REQUIRED_FILES) {
const filePath = path.join(ROOT, file);
if (!fs.existsSync(filePath)) {
risks.push({ dimension: 'R7', severity: 'high', file,
detail: `必需文件缺失: ${file}` });
}
}
// 7e. Check that PR doesn't DELETE required files
for (const file of files) {
if (REQUIRED_FILES.includes(file)) {
const filePath = path.join(ROOT, file);
// File is in the PR diff — check if it still exists in the working tree
// (if checkout has the merged version, missing = deleted)
if (!fs.existsSync(filePath)) {
risks.push({ dimension: 'R7', severity: 'critical', file,
detail: `PR删除了仓库必需文件: ${file}` });
}
}
}
// 7f. Run structural scan scripts if available
const scanScripts = [
{ name: 'scan-structure', path: path.join(ROOT, 'scripts/skyeye/scan-structure.js') },
{ name: 'scan-brain-health', path: path.join(ROOT, 'scripts/skyeye/scan-brain-health.js') },
{ name: 'scan-security-protocol', path: path.join(ROOT, 'scripts/skyeye/scan-security-protocol.js') }
];
for (const script of scanScripts) {
if (fs.existsSync(script.path)) {
try {
const output = execSync(`node "${script.path}" 2>&1`, {
cwd: ROOT, timeout: 30000, encoding: 'utf8'
});
// Check for failure indicators in output
if (output.includes('❌') || output.includes('FAIL') || output.includes('ERROR')) {
const failLines = output.split('\n').filter(l =>
l.includes('❌') || l.includes('FAIL') || l.includes('ERROR')
).slice(0, 3);
risks.push({ dimension: 'R7', severity: 'high', file: `(${script.name})`,
detail: `结构扫描 ${script.name} 报告问题: ${failLines.join('; ').slice(0, 200)}` });
}
} catch (e) {
// Script execution failure is itself a structural concern
const errMsg = (e.stderr || e.message || '').slice(0, 100);
risks.push({ dimension: 'R7', severity: 'high', file: `(${script.name})`,
detail: `结构扫描 ${script.name} 执行失败: ${errMsg}` });
}
}
}
return risks;
}
// ━━━ 风险决策 ━━━
function makeDecision(risks) {
const hasCritical = risks.some(r => r.severity === 'critical');
const highCount = risks.filter(r => r.severity === 'high').length;
if (hasCritical) return { decision: 'block', risk_level: 'critical' };
if (highCount >= 2) return { decision: 'block', risk_level: 'high' };
if (highCount === 1) return { decision: 'warn', risk_level: 'high' };
return { decision: 'pass', risk_level: 'low' };
}
// ━━━ 主逻辑 ━━━
function run() {
const author = process.env.PR_AUTHOR || '';
const branch = process.env.PR_BRANCH || '';
const prTitle = process.env.PR_TITLE || '';
console.log('═══════════════════════════════════════════════');
console.log('🛡️ 天眼 · 合并膜 (SkyEye Merge Membrane)');
console.log('═══════════════════════════════════════════════');
console.log(`PR作者(GitHub): ${author || '(unknown)'}`);
console.log(`分支: ${branch || '(unknown)'}`);
console.log(`标题: ${prTitle || '(unknown)'}`);
console.log(`时间: ${new Date().toISOString()}`);
console.log('');
console.log('⚖️ 审核模式: 全员全审 — 不区分身份,天眼对所有人一视同仁');
console.log('');
// ─── 身份识别(仅用于日志/上下文,不影响审核决策)───
const isAgent = isAgentBranch(branch);
const identity = extractIdentity(PR_COMMITS_PATH, prTitle);
const isAgentPR = isAgent || identity.isAgent;
if (isAgentPR) {
console.log('🤖 PR来源: 代理 (Copilot Agent)');
if (identity.devId) console.log(` 开发者编号: ${identity.devId}`);
if (identity.agentName) console.log(` 代理名称: ${identity.agentName}`);
} else {
console.log('👤 PR来源: 手动提交');
}
console.log('');
// ─── 加载变更文件 ───
const files = loadPRFiles();
if (files.length === 0) {
console.log('⚠️ 无变更文件或文件列表不可用');
console.log('❌ 无法获取变更文件列表 — 安全起见阻止合并');
setOutput('decision', 'block');
setOutput('risk_level', 'unknown');
setOutput('risk_summary', 'PR变更文件列表不可用');
process.exit(1);
}
console.log(`📂 变更文件数: ${files.length}`);
files.forEach(f => console.log(` · ${f}`));
console.log('');
const stats = loadPRStats();
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 天眼全系统审核 — 对所有PR生效无例外
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
console.log('━━━ 天眼全系统审核启动 ━━━');
console.log('');
// Phase 1: 风险维度检测 (R1-R6)
console.log('[Phase 1] 风险维度检测 (R1-R6)...');
const riskChecks = [
...checkCriticalFiles(files),
...checkBuildArtifacts(files),
...checkMassDeletion(stats),
...checkCoreConfigTampering(files, isAgentPR),
...checkWorkflowTampering(files, isAgentPR),
...checkPortalFramework(files, isAgentPR)
];
// Phase 2: 仓库结构完整性验证 (R7)
console.log('[Phase 2] 仓库结构完整性验证 (R7)...');
const structuralRisks = checkStructuralIntegrity(files);
// Combine all risks
const allRisks = [...riskChecks, ...structuralRisks];
// Deduplicate
const seen = new Set();
const risks = allRisks.filter(r => {
const key = `${r.dimension}:${r.file}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
console.log('');
// ─── 结果报告 ───
if (risks.length === 0) {
console.log('✅ 天眼全系统审核通过');
console.log(' 风险维度检测: 无风险');
console.log(' 仓库结构完整性: 正常');
console.log('');
console.log('═══ 决策: PASS ═══');
setOutput('decision', 'pass');
setOutput('risk_level', 'low');
setOutput('risk_summary', '天眼全系统审核通过');
process.exit(0);
}
// ─── 输出风险报告 ───
console.log(`⚠️ 检测到 ${risks.length} 项风险:`);
for (const risk of risks) {
const icon = risk.severity === 'critical' ? '🔴' : risk.severity === 'high' ? '🟠' : '🟡';
console.log(` ${icon} [${risk.dimension}] ${risk.detail}`);
}
console.log('');
// ─── 决策 ───
const { decision, risk_level } = makeDecision(risks);
const summary = risks.map(r => `[${r.dimension}] ${r.detail}`).join(' | ');
setOutput('decision', decision);
setOutput('risk_level', risk_level);
setOutput('risk_summary', summary.slice(0, 500));
if (decision === 'block') {
console.log(`═══ 决策: BLOCK (风险等级: ${risk_level}) ═══`);
console.log('❌ 天眼合并膜: 物理切断合并通道');
console.log(' 此PR不符合仓库整体系统结构要求');
process.exit(1);
} else if (decision === 'warn') {
console.log(`═══ 决策: WARN (风险等级: ${risk_level}) ═══`);
console.log('⚠️ 天眼合并膜: 存在风险项,建议审核后合并');
process.exit(0);
} else {
console.log(`═══ 决策: PASS (风险等级: ${risk_level}) ═══`);
process.exit(0);
}
}
run();