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:
commit
f0be2f8f12
|
|
@ -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∞"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 推送失败(非致命)"
|
||||
|
|
@ -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
|
||||
});
|
||||
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
# DEV-001 频道沙箱
|
||||
|
|
@ -0,0 +1 @@
|
|||
# DEV-002 频道沙箱
|
||||
|
|
@ -0,0 +1 @@
|
|||
# DEV-003 频道沙箱
|
||||
|
|
@ -0,0 +1 @@
|
|||
# DEV-004 频道沙箱
|
||||
|
|
@ -0,0 +1 @@
|
|||
# DEV-005 频道沙箱
|
||||
|
|
@ -0,0 +1 @@
|
|||
# DEV-009 频道沙箱
|
||||
|
|
@ -0,0 +1 @@
|
|||
# DEV-010 频道沙箱
|
||||
|
|
@ -0,0 +1 @@
|
|||
# DEV-011 频道沙箱
|
||||
|
|
@ -0,0 +1 @@
|
|||
# DEV-012 频道沙箱
|
||||
|
|
@ -0,0 +1 @@
|
|||
# DEV-013 频道沙箱
|
||||
|
|
@ -0,0 +1 @@
|
|||
# DEV-014 频道沙箱
|
||||
|
|
@ -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>
|
||||
|
|
@ -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": []
|
||||
}
|
||||
3224
docs/index.html
3224
docs/index.html
File diff suppressed because it is too large
Load Diff
|
|
@ -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();
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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();
|
||||
Loading…
Reference in New Issue