Merge pull request #123 from qinfendebingshuo/copilot/na
🦅 天眼系统升级 + 人格体编号体系 + ASOP-GH 自优化协议
This commit is contained in:
commit
2caf5f30fc
|
|
@ -29,19 +29,19 @@
|
|||
"DEV-001": {
|
||||
"name": "页页",
|
||||
"persona_id": "PER-001",
|
||||
"github_usernames": [],
|
||||
"github_usernames": ["夜夜光湖"],
|
||||
"allowed_paths": ["dev/DEV-001/", "backend/", "src/"]
|
||||
},
|
||||
"DEV-002": {
|
||||
"name": "肥猫",
|
||||
"persona_id": "PER-002",
|
||||
"github_usernames": [],
|
||||
"github_usernames": ["建培"],
|
||||
"allowed_paths": ["dev/DEV-002/", "frontend/", "persona-selector/", "chat-bubble/"]
|
||||
},
|
||||
"DEV-003": {
|
||||
"name": "燕樊",
|
||||
"persona_id": "PER-003",
|
||||
"github_usernames": [],
|
||||
"github_usernames": ["六寻寻7-max"],
|
||||
"allowed_paths": ["dev/DEV-003/", "settings/", "cloud-drive/"]
|
||||
},
|
||||
"DEV-004": {
|
||||
|
|
@ -59,7 +59,7 @@
|
|||
"DEV-009": {
|
||||
"name": "花尔",
|
||||
"persona_id": "PER-009",
|
||||
"github_usernames": [],
|
||||
"github_usernames": ["华尔华"],
|
||||
"allowed_paths": ["dev/DEV-009/", "user-center/"]
|
||||
},
|
||||
"DEV-010": {
|
||||
|
|
@ -77,7 +77,7 @@
|
|||
"DEV-012": {
|
||||
"name": "Awen",
|
||||
"persona_id": "PER-012",
|
||||
"github_usernames": [],
|
||||
"github_usernames": ["文卓熙"],
|
||||
"allowed_paths": ["dev/DEV-012/", "notification-center/", "notification/"]
|
||||
},
|
||||
"DEV-013": {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
name: 铸渊 · Notion 页面阅读器
|
||||
|
||||
# 从 Notion 页面链接读取内容,输出 Markdown 格式文本
|
||||
# 支持保存到文件并提交到仓库
|
||||
#
|
||||
# 依赖 Secrets:
|
||||
# NOTION_API_TOKEN Notion API token
|
||||
#
|
||||
# dispatch payload / inputs:
|
||||
# notion_page_url Notion 页面链接或页面 ID
|
||||
# save_to_repo 是否保存到仓库(true/false,默认 false)
|
||||
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [read-notion-page]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
notion_page_url:
|
||||
description: 'Notion 页面链接或页面 ID'
|
||||
required: true
|
||||
save_to_repo:
|
||||
description: '保存到仓库 data/notion-pages/ 目录(true/false)'
|
||||
required: false
|
||||
default: 'false'
|
||||
|
||||
jobs:
|
||||
read-page:
|
||||
name: 📖 读取 Notion 页面
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: 📖 读取 Notion 页面内容
|
||||
env:
|
||||
NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }}
|
||||
NOTION_PAGE_URL: ${{ github.event.client_payload.notion_page_url || github.event.inputs.notion_page_url }}
|
||||
NOTION_OUTPUT_DIR: ${{ (github.event.client_payload.save_to_repo == 'true' || github.event.inputs.save_to_repo == 'true') && 'data/notion-pages' || '' }}
|
||||
run: node scripts/notion-page-reader.js
|
||||
|
||||
- name: 💾 提交保存的页面内容
|
||||
if: github.event.client_payload.save_to_repo == 'true' || github.event.inputs.save_to_repo == 'true'
|
||||
run: |
|
||||
git config user.name "zhuyuan-bot"
|
||||
git config user.email "zhuyuan-bot@guanghulab.com"
|
||||
git add data/notion-pages/
|
||||
if git diff --cached --quiet; then
|
||||
echo "没有新文件需要提交"
|
||||
else
|
||||
git commit -m "📖 铸渊 · 保存 Notion 页面内容 [skip ci]"
|
||||
git push
|
||||
fi
|
||||
|
|
@ -72,6 +72,11 @@ jobs:
|
|||
echo "auto_fixable=$AUTO_FIXABLE" >> $GITHUB_OUTPUT
|
||||
echo "needs_human=$NEEDS_HUMAN" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: "🔄 Phase 3.5 · ASOP 自优化审批"
|
||||
run: |
|
||||
echo "🔄 ASOP 审批引擎启动..."
|
||||
node scripts/skyeye/asop-reviewer.js > /tmp/skyeye/asop-review.json || echo "{}" > /tmp/skyeye/asop-review.json
|
||||
|
||||
- name: "🔧 Phase 4 · 修复Agent"
|
||||
if: steps.diagnose.outputs.auto_fixable != '0'
|
||||
env:
|
||||
|
|
@ -107,6 +112,16 @@ jobs:
|
|||
echo "$FAILED_WORKFLOWS"
|
||||
# 记录到报告,具体重触发逻辑由各 workflow 自带的 workflow_dispatch 支持
|
||||
|
||||
- name: "🔧 Phase 5.5 · ASOP 执行已批准优化"
|
||||
run: |
|
||||
echo "🔧 ASOP 执行器启动..."
|
||||
node scripts/skyeye/asop-executor.js > /tmp/skyeye/asop-execute.json || echo "{}" > /tmp/skyeye/asop-execute.json
|
||||
|
||||
- name: "🔍 Phase 5.7 · ASOP 验证优化效果"
|
||||
run: |
|
||||
echo "🔍 ASOP 验证器启动..."
|
||||
node scripts/skyeye/asop-verifier.js > /tmp/skyeye/asop-verify.json || echo "{}" > /tmp/skyeye/asop-verify.json
|
||||
|
||||
- name: "📋 Phase 6 · 全局健康报告"
|
||||
run: |
|
||||
node scripts/skyeye/report-generator.js
|
||||
|
|
@ -115,7 +130,7 @@ jobs:
|
|||
cp /tmp/skyeye/full-report.json "data/skyeye-reports/skyeye-${DATE}.json"
|
||||
git config user.name "zhuyuan-bot"
|
||||
git config user.email "zhuyuan@guanghulab.com"
|
||||
git add data/skyeye-reports/ .github/persona-brain/memory.json
|
||||
git add data/skyeye-reports/ .github/persona-brain/memory.json data/asop-requests/
|
||||
if ! git diff --cached --quiet; then
|
||||
git commit -m "🦅 天眼报告 · $(date +%Y-%m-%d) · 问题:${{ steps.diagnose.outputs.issues_count }} 自动修复:${{ steps.diagnose.outputs.auto_fixable }}"
|
||||
git push
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ const GITHUB_OUTPUT = process.env.GITHUB_OUTPUT || '/dev/null';
|
|||
// ━━━ 仓库主人 ━━━
|
||||
const REPO_OWNER = 'qinfendebingshuo';
|
||||
|
||||
// ━━━ 人格体签名正则 ━━━
|
||||
const PERSONA_SIGNATURE_REGEX = /\[PER-(\d{3})\]/;
|
||||
// ━━━ 人格体签名正则(v2 升级:支持 PER-XXX / TCS-XXX / PER-PENDING-XXX) ━━━
|
||||
const PERSONA_SIGNATURE_REGEX = /^\[([A-Z]+-[A-Z0-9\-∞]+)\]/;
|
||||
|
||||
// ━━━ 显示长度限制 ━━━
|
||||
const MAX_COMMIT_DISPLAY = 80;
|
||||
|
|
@ -100,7 +100,7 @@ function extractPersonaSignature(commitMessage) {
|
|||
if (!commitMessage) return null;
|
||||
const match = commitMessage.match(PERSONA_SIGNATURE_REGEX);
|
||||
if (match) {
|
||||
return `PER-${match[1]}`;
|
||||
return match[1]; // e.g. "PER-SS001", "TCS-0002∞", "PER-PENDING-005"
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,326 @@
|
|||
// scripts/notion-page-reader.js
|
||||
// 铸渊 · Notion 页面阅读器
|
||||
//
|
||||
// 从 Notion 页面链接或 ID 读取内容,输出 Markdown 格式纯文本。
|
||||
//
|
||||
// 用法:
|
||||
// node scripts/notion-page-reader.js <NOTION_URL_OR_PAGE_ID>
|
||||
//
|
||||
// 环境变量:
|
||||
// NOTION_TOKEN Notion API token(必须)
|
||||
//
|
||||
// 支持的 URL 格式:
|
||||
// https://www.notion.so/workspace/Page-Title-abc123def456...
|
||||
// https://www.notion.so/abc123def456...
|
||||
// https://notion.so/abc123def456...
|
||||
// https://workspace.notion.site/Page-Title-abc123def456...
|
||||
// 直接传入 32 位十六进制 ID 或带连字符的 UUID
|
||||
|
||||
'use strict';
|
||||
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// 常量
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
||||
const NOTION_VERSION = '2022-06-28';
|
||||
const NOTION_API_HOSTNAME = 'api.notion.com';
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// URL 解析
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* 从 Notion URL 或原始 ID 中提取页面 ID
|
||||
* @param {string} input - Notion URL 或页面 ID
|
||||
* @returns {string|null} 标准化的页面 ID(带连字符的 UUID)或 null
|
||||
*/
|
||||
function extractPageId(input) {
|
||||
if (!input || typeof input !== 'string') return null;
|
||||
|
||||
var cleaned = input.trim();
|
||||
|
||||
// 去掉尾部查询参数和锚点
|
||||
cleaned = cleaned.split('?')[0].split('#')[0];
|
||||
|
||||
var hex32;
|
||||
|
||||
// 已经是带连字符的 UUID 格式
|
||||
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(cleaned)) {
|
||||
return cleaned.toLowerCase();
|
||||
}
|
||||
|
||||
// 已经是 32 位纯十六进制
|
||||
if (/^[0-9a-f]{32}$/i.test(cleaned)) {
|
||||
hex32 = cleaned.toLowerCase();
|
||||
return hex32.slice(0, 8) + '-' + hex32.slice(8, 12) + '-' + hex32.slice(12, 16) + '-' + hex32.slice(16, 20) + '-' + hex32.slice(20);
|
||||
}
|
||||
|
||||
// URL 格式:提取路径末尾的 32 位十六进制
|
||||
var match = cleaned.match(/([0-9a-f]{32})$/i);
|
||||
if (match) {
|
||||
hex32 = match[1].toLowerCase();
|
||||
return hex32.slice(0, 8) + '-' + hex32.slice(8, 12) + '-' + hex32.slice(12, 16) + '-' + hex32.slice(16, 20) + '-' + hex32.slice(20);
|
||||
}
|
||||
|
||||
// URL 中嵌入的带连字符 UUID
|
||||
var uuidMatch = cleaned.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i);
|
||||
if (uuidMatch) {
|
||||
return uuidMatch[1].toLowerCase();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// HTTP 请求工具
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
||||
function httpsRequest(options) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var req = https.request(options, function (res) {
|
||||
var data = '';
|
||||
res.on('data', function (chunk) { data += chunk; });
|
||||
res.on('end', function () {
|
||||
try {
|
||||
var parsed = JSON.parse(data);
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(parsed);
|
||||
} else {
|
||||
reject(new Error('Notion API ' + res.statusCode + ': ' + (parsed.message || data)));
|
||||
}
|
||||
} catch (e) {
|
||||
reject(new Error('Notion API 响应解析失败: ' + data.slice(0, 200)));
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.setTimeout(30000, function () {
|
||||
req.destroy(new Error('请求超时 (30s)'));
|
||||
});
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// Notion API
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
||||
function notionGet(endpoint, token) {
|
||||
return httpsRequest({
|
||||
hostname: NOTION_API_HOSTNAME,
|
||||
port: 443,
|
||||
path: endpoint,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Notion-Version': NOTION_VERSION,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Notion 页面元数据(标题、属性等)
|
||||
*/
|
||||
async function getNotionPage(pageId, token) {
|
||||
return notionGet('/v1/pages/' + pageId, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取 Notion 页面的所有子块(递归分页)
|
||||
*/
|
||||
async function getNotionPageBlocks(pageId, token) {
|
||||
var blocks = [];
|
||||
var cursor = undefined;
|
||||
do {
|
||||
var qs = cursor ? '?start_cursor=' + cursor : '';
|
||||
var result = await notionGet('/v1/blocks/' + pageId + '/children' + qs, token);
|
||||
blocks.push.apply(blocks, result.results || []);
|
||||
cursor = result.has_more ? result.next_cursor : undefined;
|
||||
} while (cursor);
|
||||
return blocks;
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// 内容提取
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* 从 Notion 页面属性中提取标题
|
||||
*/
|
||||
function extractPageTitle(page) {
|
||||
var props = page.properties || {};
|
||||
var keys = Object.keys(props);
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var prop = props[keys[i]];
|
||||
if (prop.type === 'title' && prop.title) {
|
||||
return prop.title.map(function (t) { return t.plain_text || ''; }).join('');
|
||||
}
|
||||
}
|
||||
return '(无标题)';
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Notion 块中提取纯文本
|
||||
*/
|
||||
function extractBlockText(block) {
|
||||
var type = block.type;
|
||||
if (!block[type]) return '';
|
||||
|
||||
var richTexts = block[type].rich_text || block[type].text || [];
|
||||
return richTexts.map(function (rt) { return rt.plain_text || ''; }).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Notion 块列表转为 Markdown 格式纯文本
|
||||
*/
|
||||
function blocksToMarkdown(blocks) {
|
||||
return blocks.map(function (block) {
|
||||
var type = block.type;
|
||||
var text = extractBlockText(block);
|
||||
|
||||
if (type === 'heading_1') return '\n# ' + text;
|
||||
if (type === 'heading_2') return '\n## ' + text;
|
||||
if (type === 'heading_3') return '\n### ' + text;
|
||||
if (type === 'bulleted_list_item') return '- ' + text;
|
||||
if (type === 'numbered_list_item') return '1. ' + text;
|
||||
if (type === 'to_do') {
|
||||
var checked = block.to_do && block.to_do.checked ? '☑' : '☐';
|
||||
return checked + ' ' + text;
|
||||
}
|
||||
if (type === 'code') {
|
||||
var lang = (block.code && block.code.language) || '';
|
||||
return '```' + lang + '\n' + text + '\n```';
|
||||
}
|
||||
if (type === 'divider') return '---';
|
||||
if (type === 'callout') return '> ' + text;
|
||||
if (type === 'quote') return '> ' + text;
|
||||
if (type === 'toggle') return '▸ ' + text;
|
||||
if (type === 'table_row') {
|
||||
var cells = (block.table_row && block.table_row.cells) || [];
|
||||
return '| ' + cells.map(function (cell) {
|
||||
return cell.map(function (rt) { return rt.plain_text || ''; }).join('');
|
||||
}).join(' | ') + ' |';
|
||||
}
|
||||
if (type === 'image') {
|
||||
var src = '';
|
||||
if (block.image) {
|
||||
if (block.image.type === 'external') src = block.image.external && block.image.external.url;
|
||||
if (block.image.type === 'file') src = block.image.file && block.image.file.url;
|
||||
}
|
||||
var caption = (block.image && block.image.caption) || [];
|
||||
var captionText = caption.map(function (rt) { return rt.plain_text || ''; }).join('');
|
||||
return ' + ')';
|
||||
}
|
||||
if (type === 'bookmark') {
|
||||
var url = (block.bookmark && block.bookmark.url) || '';
|
||||
return '🔗 ' + url;
|
||||
}
|
||||
if (type === 'child_page') {
|
||||
var childTitle = (block.child_page && block.child_page.title) || '';
|
||||
return '📄 ' + childTitle;
|
||||
}
|
||||
if (type === 'child_database') {
|
||||
var dbTitle = (block.child_database && block.child_database.title) || '';
|
||||
return '🗃️ ' + dbTitle;
|
||||
}
|
||||
return text;
|
||||
}).filter(Boolean).join('\n');
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// 主流程
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
||||
async function main() {
|
||||
var input = process.env.NOTION_PAGE_URL || process.argv[2];
|
||||
var token = process.env.NOTION_TOKEN;
|
||||
var outputDir = process.env.NOTION_OUTPUT_DIR || '';
|
||||
|
||||
if (!token) {
|
||||
console.error('❌ 缺少 NOTION_TOKEN 环境变量');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!input) {
|
||||
console.error('❌ 用法: node scripts/notion-page-reader.js <NOTION_URL_OR_PAGE_ID>');
|
||||
console.error(' 或设置环境变量 NOTION_PAGE_URL');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var pageId = extractPageId(input);
|
||||
if (!pageId) {
|
||||
console.error('❌ 无法从输入中提取页面 ID: ' + input);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('📖 铸渊 · Notion 页面阅读器');
|
||||
console.log(' → 页面 ID: ' + pageId);
|
||||
console.log('');
|
||||
|
||||
// 1. 获取页面元数据
|
||||
console.log('⏳ 正在获取页面信息...');
|
||||
var page = await getNotionPage(pageId, token);
|
||||
var title = extractPageTitle(page);
|
||||
console.log(' → 标题: ' + title);
|
||||
|
||||
// 2. 获取页面内容块
|
||||
console.log('⏳ 正在读取页面内容...');
|
||||
var blocks = await getNotionPageBlocks(pageId, token);
|
||||
console.log(' → 获取到 ' + blocks.length + ' 个内容块');
|
||||
|
||||
// 3. 转换为可读文本
|
||||
var markdown = blocksToMarkdown(blocks);
|
||||
|
||||
// 4. 输出
|
||||
console.log('');
|
||||
console.log('════════════════════════════════════════');
|
||||
console.log('📄 ' + title);
|
||||
console.log('════════════════════════════════════════');
|
||||
console.log(markdown);
|
||||
console.log('');
|
||||
console.log('════════════════════════════════════════');
|
||||
console.log('✅ 读取完成 · 共 ' + blocks.length + ' 个内容块');
|
||||
|
||||
// 5. 可选:保存到文件
|
||||
if (outputDir) {
|
||||
var safeTitle = title.replace(/[^a-zA-Z0-9\u4e00-\u9fa5_-]/g, '_').slice(0, 80);
|
||||
var filename = safeTitle + '-' + pageId.replace(/-/g, '') + '.md';
|
||||
var outputPath = path.join(outputDir, filename);
|
||||
|
||||
var fileContent = '# ' + title + '\n\n';
|
||||
fileContent += '> 📖 Notion 页面 ID: `' + pageId + '`\n';
|
||||
fileContent += '> 🕐 读取时间: ' + new Date().toISOString() + '\n\n';
|
||||
fileContent += '---\n\n';
|
||||
fileContent += markdown + '\n';
|
||||
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(outputPath, fileContent, 'utf-8');
|
||||
console.log('');
|
||||
console.log('💾 已保存到: ' + outputPath);
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// 模块导出(供其他脚本引用)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
||||
module.exports = {
|
||||
extractPageId: extractPageId,
|
||||
extractBlockText: extractBlockText,
|
||||
blocksToMarkdown: blocksToMarkdown,
|
||||
extractPageTitle: extractPageTitle,
|
||||
};
|
||||
|
||||
// 直接运行时执行主流程
|
||||
if (require.main === module) {
|
||||
main().catch(function (err) {
|
||||
console.error('❌ 读取失败: ' + err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
// scripts/skyeye/asop-executor.js
|
||||
// 天眼·ASOP 已批准优化执行器
|
||||
//
|
||||
// 读取 data/asop-requests/approved/ 下的已批准申请
|
||||
// 按优先级逐个执行
|
||||
// 执行完成后移动到 executed/
|
||||
//
|
||||
// 执行原则:
|
||||
// ① 执行前保存快照到 snapshots/
|
||||
// ② 只执行明确定义的操作
|
||||
// ③ 执行后记录结果
|
||||
// ④ 绝不做破坏性操作
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '../..');
|
||||
const ASOP_DIR = path.join(ROOT, 'data/asop-requests');
|
||||
const APPROVED_DIR = path.join(ASOP_DIR, 'approved');
|
||||
const EXECUTED_DIR = path.join(ASOP_DIR, 'executed');
|
||||
const SNAPSHOTS_DIR = path.join(ASOP_DIR, 'snapshots');
|
||||
|
||||
const BEIJING_OFFSET_MS = 8 * 3600 * 1000;
|
||||
|
||||
// ━━━ 安全读取 JSON ━━━
|
||||
function readJSON(filePath) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ━━━ 保存快照(执行前备份) ━━━
|
||||
function saveSnapshot(requestId, affectedFiles) {
|
||||
const snapshotDir = path.join(SNAPSHOTS_DIR, requestId);
|
||||
fs.mkdirSync(snapshotDir, { recursive: true });
|
||||
|
||||
for (const file of affectedFiles) {
|
||||
const srcPath = path.join(ROOT, file);
|
||||
if (fs.existsSync(srcPath)) {
|
||||
const destPath = path.join(snapshotDir, file.replace(/\//g, '__'));
|
||||
fs.copyFileSync(srcPath, destPath);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`📸 快照已保存: ${requestId} (${affectedFiles.length} 文件)`);
|
||||
}
|
||||
|
||||
// ━━━ 执行单个优化 ━━━
|
||||
function executeOptimization(request) {
|
||||
const requestId = request.request_id;
|
||||
const affectedFiles = (request.impact_assessment && request.impact_assessment.affected_files) || [];
|
||||
|
||||
console.log(`\n🔧 执行: ${requestId}`);
|
||||
console.log(` 方案: ${request.proposed_optimization}`);
|
||||
console.log(` 影响: ${affectedFiles.join(', ') || '无文件变更'}`);
|
||||
|
||||
// 保存快照
|
||||
if (affectedFiles.length > 0) {
|
||||
saveSnapshot(requestId, affectedFiles);
|
||||
}
|
||||
|
||||
// ASOP 执行器只记录执行意图,实际执行由天眼修复 Agent 统一处理
|
||||
// 这样可以避免 ASOP 执行器和修复 Agent 做重复/冲突的操作
|
||||
const result = {
|
||||
executed: true,
|
||||
executed_at: new Date(Date.now() + BEIJING_OFFSET_MS).toISOString().replace('T', ' ').slice(0, 19) + '+08:00',
|
||||
executor: '🦅 天眼·ASOP执行器',
|
||||
snapshot_saved: affectedFiles.length > 0,
|
||||
note: '已记录执行意图,待下次天眼验证效果'
|
||||
};
|
||||
|
||||
console.log(` ✅ 执行完成: ${requestId}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ━━━ 主执行流程 ━━━
|
||||
function executeAll() {
|
||||
console.log('🔧 天眼·ASOP 执行器启动');
|
||||
console.log('═══════════════════════════════════════════\n');
|
||||
|
||||
if (!fs.existsSync(APPROVED_DIR)) {
|
||||
console.log('ℹ️ approved/ 目录不存在,无待执行优化');
|
||||
const result = { total: 0, executed: 0, failed: 0 };
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return result;
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(APPROVED_DIR)
|
||||
.filter(f => f.endsWith('.json') && f !== '.gitkeep');
|
||||
|
||||
if (files.length === 0) {
|
||||
console.log('🔧 ASOP:无已批准待执行的优化');
|
||||
const result = { total: 0, executed: 0, failed: 0 };
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return result;
|
||||
}
|
||||
|
||||
console.log(`🔧 ASOP:发现 ${files.length} 条已批准待执行的优化\n`);
|
||||
|
||||
const summary = { total: files.length, executed: 0, failed: 0, results: [] };
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(APPROVED_DIR, file);
|
||||
const req = readJSON(filePath);
|
||||
if (!req) {
|
||||
console.log(`⚠️ 无法解析 ${file},跳过`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = executeOptimization(req);
|
||||
req.execution_result = result;
|
||||
|
||||
// 移动到 executed/
|
||||
fs.mkdirSync(EXECUTED_DIR, { recursive: true });
|
||||
fs.writeFileSync(path.join(EXECUTED_DIR, file), JSON.stringify(req, null, 2) + '\n');
|
||||
fs.unlinkSync(filePath);
|
||||
|
||||
summary.executed++;
|
||||
summary.results.push({ request_id: req.request_id, status: 'executed' });
|
||||
} catch (e) {
|
||||
console.error(`❌ ${req.request_id} 执行失败: ${e.message}`);
|
||||
summary.failed++;
|
||||
summary.results.push({ request_id: req.request_id, status: 'failed', error: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n📊 执行结果:成功 ${summary.executed} / 失败 ${summary.failed}`);
|
||||
console.log(JSON.stringify(summary, null, 2));
|
||||
return summary;
|
||||
}
|
||||
|
||||
// ━━━ 导出 ━━━
|
||||
module.exports = { executeAll, saveSnapshot };
|
||||
|
||||
// ━━━ 直接运行 ━━━
|
||||
if (require.main === module) {
|
||||
executeAll();
|
||||
}
|
||||
|
|
@ -0,0 +1,233 @@
|
|||
// scripts/skyeye/asop-reviewer.js
|
||||
// 天眼·ASOP 自优化审批引擎
|
||||
//
|
||||
// 天眼每日 06:00 运行时扫描 data/asop-requests/pending/
|
||||
// 按 ASOP 三级边界审批:GL1 自主 / GL2 天眼审批 / GL3 升级冰朔
|
||||
//
|
||||
// 审批标准:
|
||||
// 1. 申请是否在该 Workflow 职责范围内?
|
||||
// 2. 优化后是否不影响其他 Workflow 和系统整体架构?
|
||||
// 3. 优化理由是否有数据支撑?
|
||||
// 4. 是否违反核心不可变区?
|
||||
// 5. GL3 级别 → 自动升级到冰朔
|
||||
//
|
||||
// 输出:审批结果 JSON → stdout
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '../..');
|
||||
const ASOP_DIR = path.join(ROOT, 'data/asop-requests');
|
||||
const PENDING_DIR = path.join(ASOP_DIR, 'pending');
|
||||
const APPROVED_DIR = path.join(ASOP_DIR, 'approved');
|
||||
const REJECTED_DIR = path.join(ASOP_DIR, 'rejected');
|
||||
|
||||
const BEIJING_OFFSET_MS = 8 * 3600 * 1000;
|
||||
|
||||
// ━━━ 核心不可变区(仓库侧) ━━━
|
||||
const IMMUTABLE_FILES = [
|
||||
'.github/workflows/zhuyuan-skyeye.yml',
|
||||
'scripts/skyeye/diagnose.js',
|
||||
'.github/persona-brain/routing-map.json'
|
||||
];
|
||||
|
||||
const IMMUTABLE_CONCEPTS = [
|
||||
'whitelist',
|
||||
'persona_id_format',
|
||||
'skyeye_report_schema',
|
||||
'secrets_key_names'
|
||||
];
|
||||
|
||||
// ━━━ 30天变更累计上限 ━━━
|
||||
const MAX_GL2_PER_WORKFLOW_30D = 3;
|
||||
|
||||
// ━━━ 审批阈值 ━━━
|
||||
const MIN_EVIDENCE_LENGTH = 10;
|
||||
const MAX_AFFECTED_WORKFLOWS_GL2 = 2;
|
||||
const MIN_ROLLBACK_PLAN_LENGTH = 5;
|
||||
|
||||
// ━━━ 安全读取 JSON ━━━
|
||||
function readJSON(filePath) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ━━━ 移动申请文件到目标目录 ━━━
|
||||
function moveRequest(requestId, fromDir, toDir, decision) {
|
||||
const srcFile = path.join(fromDir, `${requestId}.json`);
|
||||
if (!fs.existsSync(srcFile)) return;
|
||||
|
||||
const request = readJSON(srcFile);
|
||||
if (!request) return;
|
||||
|
||||
request.decision = {
|
||||
result: decision.result,
|
||||
reason: decision.reason,
|
||||
reviewed_at: new Date(Date.now() + BEIJING_OFFSET_MS).toISOString().replace('T', ' ').slice(0, 19) + '+08:00',
|
||||
reviewer: '🦅 天眼·ASOP审批引擎'
|
||||
};
|
||||
|
||||
if (decision.execution_steps) {
|
||||
request.decision.execution_steps = decision.execution_steps;
|
||||
}
|
||||
|
||||
fs.mkdirSync(toDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(toDir, `${requestId}.json`), JSON.stringify(request, null, 2) + '\n');
|
||||
fs.unlinkSync(srcFile);
|
||||
}
|
||||
|
||||
// ━━━ 统计 30 天内某 Workflow 已批准的 GL2 数量 ━━━
|
||||
function countRecentApprovals(workflow) {
|
||||
const dirs = [APPROVED_DIR, path.join(ASOP_DIR, 'executed'), path.join(ASOP_DIR, 'verified')];
|
||||
const thirtyDaysAgo = Date.now() - 30 * 24 * 3600 * 1000;
|
||||
let count = 0;
|
||||
|
||||
for (const dir of dirs) {
|
||||
if (!fs.existsSync(dir)) continue;
|
||||
const files = fs.readdirSync(dir).filter(f => f.endsWith('.json') && f !== '.gitkeep');
|
||||
for (const file of files) {
|
||||
const req = readJSON(path.join(dir, file));
|
||||
if (!req) continue;
|
||||
if (req.requester && req.requester.workflow === workflow && req.level === 'GL2') {
|
||||
const ts = req.timestamp ? new Date(req.timestamp.replace('+08:00', '+0800')).getTime() : 0;
|
||||
if (ts > thirtyDaysAgo) count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
// ━━━ 审批单个申请 ━━━
|
||||
function evaluateRequest(req) {
|
||||
// GL3 → 自动升级到冰朔
|
||||
if (req.level === 'GL3') {
|
||||
return { result: 'escalate', reason: 'GL3 级别需冰朔审批' };
|
||||
}
|
||||
|
||||
// 核心不可变区检查
|
||||
const affectedFiles = (req.impact_assessment && req.impact_assessment.affected_files) || [];
|
||||
const touchesImmutable = affectedFiles.some(f => IMMUTABLE_FILES.includes(f));
|
||||
if (touchesImmutable) {
|
||||
return { result: 'escalate', reason: '涉及核心不可变区文件,自动升级到冰朔审批' };
|
||||
}
|
||||
|
||||
// 数据支撑检查
|
||||
if (!req.data_evidence || req.data_evidence.length < MIN_EVIDENCE_LENGTH) {
|
||||
return { result: 'rejected', reason: `缺少数据支撑(evidence 不足 ${MIN_EVIDENCE_LENGTH} 字符),请提供具体证据` };
|
||||
}
|
||||
|
||||
// 影响范围检查:超过阈值个 Workflow → 升级
|
||||
const affectedWorkflows = (req.impact_assessment && req.impact_assessment.affected_workflows) || [];
|
||||
if (affectedWorkflows.length > MAX_AFFECTED_WORKFLOWS_GL2) {
|
||||
return { result: 'escalate', reason: `影响超过 ${MAX_AFFECTED_WORKFLOWS_GL2} 个 Workflow(${affectedWorkflows.length} 个),需冰朔评估` };
|
||||
}
|
||||
|
||||
// 30 天变更累计检查
|
||||
const workflow = req.requester && req.requester.workflow;
|
||||
if (workflow) {
|
||||
const recentCount = countRecentApprovals(workflow);
|
||||
if (recentCount >= MAX_GL2_PER_WORKFLOW_30D) {
|
||||
return {
|
||||
result: 'escalate',
|
||||
reason: `${workflow} 在 30 天内已有 ${recentCount} 次 GL2 变更(上限 ${MAX_GL2_PER_WORKFLOW_30D}),自动冻结,升级冰朔审查`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 回滚计划检查
|
||||
if (!req.rollback_plan || req.rollback_plan.length < MIN_ROLLBACK_PLAN_LENGTH) {
|
||||
return { result: 'rejected', reason: `缺少回滚计划(rollback_plan 不足 ${MIN_ROLLBACK_PLAN_LENGTH} 字符),请补充回退方案` };
|
||||
}
|
||||
|
||||
// 通过所有检查 → 批准
|
||||
return {
|
||||
result: 'approved',
|
||||
reason: '申请合理 · 影响可控 · 数据充分 · 回滚计划完整'
|
||||
};
|
||||
}
|
||||
|
||||
// ━━━ 主审批流程 ━━━
|
||||
function reviewAll() {
|
||||
console.log('🔄 天眼·ASOP 审批引擎启动');
|
||||
console.log('═══════════════════════════════════════════\n');
|
||||
|
||||
if (!fs.existsSync(PENDING_DIR)) {
|
||||
console.log('ℹ️ pending/ 目录不存在,无待审批申请');
|
||||
const result = { total: 0, approved: 0, rejected: 0, escalated: 0 };
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return result;
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(PENDING_DIR)
|
||||
.filter(f => f.endsWith('.json') && f !== '.gitkeep');
|
||||
|
||||
if (files.length === 0) {
|
||||
console.log('🔄 ASOP:无待审批申请');
|
||||
const result = { total: 0, approved: 0, rejected: 0, escalated: 0 };
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return result;
|
||||
}
|
||||
|
||||
console.log(`🔄 ASOP:发现 ${files.length} 条待审批申请\n`);
|
||||
|
||||
const summary = { total: files.length, approved: 0, rejected: 0, escalated: 0, decisions: [] };
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(PENDING_DIR, file);
|
||||
const req = readJSON(filePath);
|
||||
if (!req) {
|
||||
console.log(`⚠️ 无法解析 ${file},跳过`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const decision = evaluateRequest(req);
|
||||
const requestId = req.request_id || file.replace('.json', '');
|
||||
|
||||
switch (decision.result) {
|
||||
case 'approved':
|
||||
moveRequest(requestId, PENDING_DIR, APPROVED_DIR, decision);
|
||||
summary.approved++;
|
||||
console.log(`✅ ASOP 批准:${requestId} — ${decision.reason}`);
|
||||
break;
|
||||
case 'rejected':
|
||||
moveRequest(requestId, PENDING_DIR, REJECTED_DIR, decision);
|
||||
summary.rejected++;
|
||||
console.log(`❌ ASOP 拒绝:${requestId} — ${decision.reason}`);
|
||||
break;
|
||||
case 'escalate':
|
||||
// 升级的申请保留在 pending/ 等冰朔处理,标记已升级
|
||||
req.escalated = true;
|
||||
req.escalate_reason = decision.reason;
|
||||
fs.writeFileSync(filePath, JSON.stringify(req, null, 2) + '\n');
|
||||
summary.escalated++;
|
||||
console.log(`⬆️ ASOP 升级:${requestId} — ${decision.reason}`);
|
||||
break;
|
||||
}
|
||||
|
||||
summary.decisions.push({
|
||||
request_id: requestId,
|
||||
level: req.level,
|
||||
workflow: req.requester ? req.requester.workflow : '',
|
||||
result: decision.result,
|
||||
reason: decision.reason
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`\n📊 审批结果:批准 ${summary.approved} / 拒绝 ${summary.rejected} / 升级 ${summary.escalated}`);
|
||||
console.log(JSON.stringify(summary, null, 2));
|
||||
return summary;
|
||||
}
|
||||
|
||||
// ━━━ 导出 ━━━
|
||||
module.exports = { reviewAll, evaluateRequest };
|
||||
|
||||
// ━━━ 直接运行 ━━━
|
||||
if (require.main === module) {
|
||||
reviewAll();
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
// scripts/skyeye/asop-submit.js
|
||||
// 天眼·ASOP 自优化申请提交工具
|
||||
//
|
||||
// 供各 Workflow 使用,提交自优化申请到 data/asop-requests/pending/
|
||||
//
|
||||
// 用法:
|
||||
// node scripts/skyeye/asop-submit.js --workflow "xxx.yml" --level GL2 \
|
||||
// --problem "描述" --proposal "方案" --evidence "证据" \
|
||||
// --affected-workflows "a.yml,b.yml" --affected-files "f1,f2" --risk "低"
|
||||
//
|
||||
// 或在其他脚本中引入:
|
||||
// const { submitASOPRequest } = require('./asop-submit');
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '../..');
|
||||
const PENDING_DIR = path.join(ROOT, 'data/asop-requests/pending');
|
||||
|
||||
const BEIJING_OFFSET_MS = 8 * 3600 * 1000;
|
||||
|
||||
// ━━━ 生成申请 ID ━━━
|
||||
function generateRequestId() {
|
||||
const now = new Date();
|
||||
const bjDate = new Date(now.getTime() + BEIJING_OFFSET_MS);
|
||||
const dateStr = bjDate.toISOString().split('T')[0].replace(/-/g, '');
|
||||
const seq = String(Math.floor(Math.random() * 900) + 100);
|
||||
return `ASOP-GH-${dateStr}-${seq}`;
|
||||
}
|
||||
|
||||
// ━━━ 提交 ASOP 申请 ━━━
|
||||
function submitASOPRequest(params) {
|
||||
const {
|
||||
workflow,
|
||||
workflowName = '',
|
||||
runId = '',
|
||||
level = 'GL2',
|
||||
problem = '',
|
||||
proposal = '',
|
||||
evidence = '',
|
||||
affectedWorkflows = [],
|
||||
affectedFiles = [],
|
||||
risk = '低',
|
||||
rollbackPlan = ''
|
||||
} = params;
|
||||
|
||||
if (!workflow) {
|
||||
console.error('❌ ASOP: workflow 参数必填');
|
||||
return null;
|
||||
}
|
||||
if (!['GL1', 'GL2', 'GL3'].includes(level)) {
|
||||
console.error('❌ ASOP: level 必须是 GL1/GL2/GL3');
|
||||
return null;
|
||||
}
|
||||
if (!problem || !proposal) {
|
||||
console.error('❌ ASOP: problem 和 proposal 参数必填');
|
||||
return null;
|
||||
}
|
||||
|
||||
const requestId = generateRequestId();
|
||||
const now = new Date();
|
||||
const bjTime = new Date(now.getTime() + BEIJING_OFFSET_MS);
|
||||
|
||||
const request = {
|
||||
request_id: requestId,
|
||||
timestamp: bjTime.toISOString().replace('T', ' ').slice(0, 19) + '+08:00',
|
||||
requester: {
|
||||
workflow: workflow,
|
||||
name: workflowName || workflow.replace('.yml', ''),
|
||||
run_id: runId || process.env.GITHUB_RUN_ID || ''
|
||||
},
|
||||
level: level,
|
||||
current_problem: problem,
|
||||
proposed_optimization: proposal,
|
||||
impact_assessment: {
|
||||
affected_workflows: affectedWorkflows,
|
||||
affected_files: affectedFiles,
|
||||
risk: risk
|
||||
},
|
||||
data_evidence: evidence,
|
||||
rollback_plan: rollbackPlan
|
||||
};
|
||||
|
||||
// GL1 直接执行,不写入 pending
|
||||
if (level === 'GL1') {
|
||||
console.log(`🔄 ASOP GL1 · ${requestId} · 自主执行,不需审批`);
|
||||
console.log(` 问题: ${problem}`);
|
||||
console.log(` 方案: ${proposal}`);
|
||||
return request;
|
||||
}
|
||||
|
||||
// GL2/GL3 写入 pending/
|
||||
fs.mkdirSync(PENDING_DIR, { recursive: true });
|
||||
const filePath = path.join(PENDING_DIR, `${requestId}.json`);
|
||||
fs.writeFileSync(filePath, JSON.stringify(request, null, 2) + '\n');
|
||||
console.log(`🔄 ASOP ${level} · ${requestId} · 已提交到 pending/`);
|
||||
console.log(` 问题: ${problem}`);
|
||||
console.log(` 方案: ${proposal}`);
|
||||
if (level === 'GL3') {
|
||||
console.log(` ⬆️ GL3 级别,需冰朔审批`);
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
// ━━━ 导出 ━━━
|
||||
module.exports = { submitASOPRequest };
|
||||
|
||||
// ━━━ CLI 入口 ━━━
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
function getArg(name) {
|
||||
const idx = args.indexOf('--' + name);
|
||||
return idx >= 0 && idx + 1 < args.length ? args[idx + 1] : '';
|
||||
}
|
||||
|
||||
const result = submitASOPRequest({
|
||||
workflow: getArg('workflow'),
|
||||
workflowName: getArg('name'),
|
||||
runId: getArg('run-id'),
|
||||
level: getArg('level') || 'GL2',
|
||||
problem: getArg('problem'),
|
||||
proposal: getArg('proposal'),
|
||||
evidence: getArg('evidence'),
|
||||
affectedWorkflows: getArg('affected-workflows') ? getArg('affected-workflows').split(',') : [],
|
||||
affectedFiles: getArg('affected-files') ? getArg('affected-files').split(',') : [],
|
||||
risk: getArg('risk') || '低',
|
||||
rollbackPlan: getArg('rollback-plan')
|
||||
});
|
||||
|
||||
if (result) {
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
// scripts/skyeye/asop-verifier.js
|
||||
// 天眼·ASOP 优化效果验证器
|
||||
//
|
||||
// 天眼运行时检查 data/asop-requests/executed/ 下已执行的优化
|
||||
// 验证效果:
|
||||
// ① 优化后相关 Workflow 是否正常运行?
|
||||
// ② 是否引入了新的问题?
|
||||
// ③ 连续 2 次失败 → 自动回滚
|
||||
//
|
||||
// 验证通过 → 移到 verified/ 归档
|
||||
// 验证失败 → 回滚 + 记录
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '../..');
|
||||
const ASOP_DIR = path.join(ROOT, 'data/asop-requests');
|
||||
const EXECUTED_DIR = path.join(ASOP_DIR, 'executed');
|
||||
const VERIFIED_DIR = path.join(ASOP_DIR, 'verified');
|
||||
const SNAPSHOTS_DIR = path.join(ASOP_DIR, 'snapshots');
|
||||
const SKYEYE_DIR = '/tmp/skyeye';
|
||||
|
||||
const BEIJING_OFFSET_MS = 8 * 3600 * 1000;
|
||||
const CONSECUTIVE_FAILURE_THRESHOLD = 2;
|
||||
|
||||
// ━━━ 安全读取 JSON ━━━
|
||||
function readJSON(filePath) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ━━━ 检查受影响 Workflow 的最近运行状态 ━━━
|
||||
function checkWorkflowHealth(affectedWorkflows) {
|
||||
// 从天眼扫描结果中获取 Workflow 运行状态
|
||||
const recentRunsPath = path.join(SKYEYE_DIR, 'recent-runs.json');
|
||||
if (!fs.existsSync(recentRunsPath)) {
|
||||
return { healthy: true, reason: '无最近运行数据可验证,默认通过' };
|
||||
}
|
||||
|
||||
const recentRuns = readJSON(recentRunsPath) || [];
|
||||
const failures = [];
|
||||
|
||||
for (const wf of affectedWorkflows) {
|
||||
const wfRuns = recentRuns.filter(r => {
|
||||
const name = r.name || '';
|
||||
return name.includes(wf.replace('.yml', '').replace('.yaml', ''));
|
||||
});
|
||||
|
||||
const recentFailures = wfRuns.filter(r => r.conclusion === 'failure');
|
||||
if (recentFailures.length >= CONSECUTIVE_FAILURE_THRESHOLD) {
|
||||
failures.push({
|
||||
workflow: wf,
|
||||
consecutive_failures: recentFailures.length
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
return {
|
||||
healthy: false,
|
||||
reason: `连续失败检测:${failures.map(f => `${f.workflow}(${f.consecutive_failures}次)`).join(', ')}`,
|
||||
failures
|
||||
};
|
||||
}
|
||||
|
||||
return { healthy: true, reason: '所有受影响 Workflow 运行正常' };
|
||||
}
|
||||
|
||||
// ━━━ 回滚优化 ━━━
|
||||
function rollbackOptimization(request) {
|
||||
const requestId = request.request_id;
|
||||
const snapshotDir = path.join(SNAPSHOTS_DIR, requestId);
|
||||
|
||||
if (!fs.existsSync(snapshotDir)) {
|
||||
console.log(`⚠️ ${requestId} 快照不存在,无法回滚`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const snapshotFiles = fs.readdirSync(snapshotDir).filter(f => f !== '.gitkeep');
|
||||
let restored = 0;
|
||||
|
||||
for (const file of snapshotFiles) {
|
||||
const originalPath = file.replace(/__/g, '/');
|
||||
const srcPath = path.join(snapshotDir, file);
|
||||
const destPath = path.join(ROOT, originalPath);
|
||||
|
||||
try {
|
||||
const destDir = path.dirname(destPath);
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
fs.copyFileSync(srcPath, destPath);
|
||||
restored++;
|
||||
} catch (e) {
|
||||
console.error(`⚠️ 回滚 ${originalPath} 失败: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🔄 ${requestId} 已回滚 ${restored}/${snapshotFiles.length} 个文件`);
|
||||
return restored > 0;
|
||||
}
|
||||
|
||||
// ━━━ 验证单个优化 ━━━
|
||||
function verifyOptimization(request) {
|
||||
const requestId = request.request_id;
|
||||
const affectedWorkflows = (request.impact_assessment && request.impact_assessment.affected_workflows) || [];
|
||||
|
||||
console.log(`\n🔍 验证: ${requestId}`);
|
||||
console.log(` 方案: ${request.proposed_optimization}`);
|
||||
|
||||
// 检查受影响 Workflow 的健康度
|
||||
const health = checkWorkflowHealth(affectedWorkflows);
|
||||
|
||||
if (!health.healthy) {
|
||||
console.log(` ❌ 验证失败: ${health.reason}`);
|
||||
|
||||
// 异常熔断:回滚
|
||||
const rolled = rollbackOptimization(request);
|
||||
return {
|
||||
verified: false,
|
||||
reason: health.reason,
|
||||
rolled_back: rolled,
|
||||
verified_at: new Date(Date.now() + BEIJING_OFFSET_MS).toISOString().replace('T', ' ').slice(0, 19) + '+08:00'
|
||||
};
|
||||
}
|
||||
|
||||
console.log(` ✅ 验证通过: ${health.reason}`);
|
||||
return {
|
||||
verified: true,
|
||||
reason: health.reason,
|
||||
rolled_back: false,
|
||||
verified_at: new Date(Date.now() + BEIJING_OFFSET_MS).toISOString().replace('T', ' ').slice(0, 19) + '+08:00'
|
||||
};
|
||||
}
|
||||
|
||||
// ━━━ 主验证流程 ━━━
|
||||
function verifyAll() {
|
||||
console.log('🔍 天眼·ASOP 验证器启动');
|
||||
console.log('═══════════════════════════════════════════\n');
|
||||
|
||||
if (!fs.existsSync(EXECUTED_DIR)) {
|
||||
console.log('ℹ️ executed/ 目录不存在,无待验证优化');
|
||||
const result = { total: 0, verified: 0, rolled_back: 0 };
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return result;
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(EXECUTED_DIR)
|
||||
.filter(f => f.endsWith('.json') && f !== '.gitkeep');
|
||||
|
||||
if (files.length === 0) {
|
||||
console.log('🔍 ASOP:无待验证的已执行优化');
|
||||
const result = { total: 0, verified: 0, rolled_back: 0 };
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return result;
|
||||
}
|
||||
|
||||
console.log(`🔍 ASOP:发现 ${files.length} 条待验证的已执行优化\n`);
|
||||
|
||||
const summary = { total: files.length, verified: 0, rolled_back: 0, results: [] };
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(EXECUTED_DIR, file);
|
||||
const req = readJSON(filePath);
|
||||
if (!req) {
|
||||
console.log(`⚠️ 无法解析 ${file},跳过`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = verifyOptimization(req);
|
||||
req.verification_result = result;
|
||||
|
||||
if (result.verified) {
|
||||
// 验证通过 → 移到 verified/ 归档
|
||||
fs.mkdirSync(VERIFIED_DIR, { recursive: true });
|
||||
fs.writeFileSync(path.join(VERIFIED_DIR, file), JSON.stringify(req, null, 2) + '\n');
|
||||
fs.unlinkSync(filePath);
|
||||
summary.verified++;
|
||||
} else {
|
||||
// 验证失败 + 已回滚 → 标记在 executed/(保留记录)
|
||||
fs.writeFileSync(filePath, JSON.stringify(req, null, 2) + '\n');
|
||||
summary.rolled_back++;
|
||||
}
|
||||
|
||||
summary.results.push({
|
||||
request_id: req.request_id,
|
||||
verified: result.verified,
|
||||
rolled_back: result.rolled_back,
|
||||
reason: result.reason
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`\n📊 验证结果:通过 ${summary.verified} / 回滚 ${summary.rolled_back}`);
|
||||
console.log(JSON.stringify(summary, null, 2));
|
||||
return summary;
|
||||
}
|
||||
|
||||
// ━━━ 导出 ━━━
|
||||
module.exports = { verifyAll, verifyOptimization, rollbackOptimization };
|
||||
|
||||
// ━━━ 直接运行 ━━━
|
||||
if (require.main === module) {
|
||||
verifyAll();
|
||||
}
|
||||
|
|
@ -113,18 +113,22 @@ function parsePersonaPage(page) {
|
|||
if (prop.title) return prop.title.map(t => t.plain_text).join('');
|
||||
if (prop.rich_text) return prop.rich_text.map(t => t.plain_text).join('');
|
||||
if (prop.select) return prop.select ? prop.select.name : '';
|
||||
if (prop.status) return prop.status ? prop.status.name : '';
|
||||
return '';
|
||||
}
|
||||
|
||||
return {
|
||||
page_id: page.id,
|
||||
persona_id: getText(props['人格体编号']),
|
||||
name: getText(props['名称']) || getText(props['Name']),
|
||||
name: getText(props['人格体名称']) || getText(props['名称']) || getText(props['Name']),
|
||||
type: getText(props['编号类型']),
|
||||
bound_human: getText(props['绑定人类']),
|
||||
dev_id: getText(props['开发者ID']),
|
||||
github_username: getText(props['GitHub用户名']),
|
||||
status: getText(props['状态']),
|
||||
module: getText(props['负责模块']),
|
||||
commit_signature: getText(props['签名标识']),
|
||||
repo_paths: getText(props['仓库路径权限']),
|
||||
commit_signature: getText(props['签名格式']) || getText(props['签名标识']),
|
||||
source: 'notion'
|
||||
};
|
||||
}
|
||||
|
|
@ -158,7 +162,7 @@ function lookupLocal(personaId) {
|
|||
return null;
|
||||
}
|
||||
|
||||
// ━━━ writeBack: 写回 Notion ━━━
|
||||
// ━━━ writeBack: 写回 Notion(铸渊最后拉取 + 铸渊同步备注) ━━━
|
||||
async function writeBack(personaId, data) {
|
||||
if (!PERSONA_DB_ID || !NOTION_TOKEN) {
|
||||
console.log('⚠️ Notion 凭证不完整,跳过写回');
|
||||
|
|
@ -173,20 +177,21 @@ async function writeBack(personaId, data) {
|
|||
return false;
|
||||
}
|
||||
|
||||
// 构建更新属性
|
||||
// 构建更新属性(只写铸渊管辖的两个字段)
|
||||
const properties = {};
|
||||
if (data.status) {
|
||||
properties['状态'] = {
|
||||
select: { name: data.status }
|
||||
|
||||
// 铸渊最后拉取
|
||||
properties['铸渊最后拉取'] = {
|
||||
date: { start: new Date().toISOString() }
|
||||
};
|
||||
|
||||
// 铸渊同步备注:[时间戳] [模块] [动作] · [变更摘要] · [分流路径]
|
||||
if (data.sync_note) {
|
||||
properties['铸渊同步备注'] = {
|
||||
rich_text: [{ type: 'text', text: { content: data.sync_note.substring(0, MAX_NOTION_RICH_TEXT) } }]
|
||||
};
|
||||
}
|
||||
if (data.last_activity) {
|
||||
properties['最后活动'] = {
|
||||
date: { start: data.last_activity }
|
||||
};
|
||||
}
|
||||
if (data.gate_result) {
|
||||
properties['门禁记录'] = {
|
||||
} else if (data.gate_result) {
|
||||
properties['铸渊同步备注'] = {
|
||||
rich_text: [{ type: 'text', text: { content: data.gate_result.substring(0, MAX_NOTION_RICH_TEXT) } }]
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,10 +72,22 @@ function main() {
|
|||
console.log('\n🔬 Phase 3 · 诊断');
|
||||
results.diagnosis = runModule('diagnose.js', 'diagnosis.json');
|
||||
|
||||
// Phase 3.5: ASOP 审批(新增)
|
||||
console.log('\n🔄 Phase 3.5 · ASOP 自优化审批');
|
||||
results.asop_review = runModule('asop-reviewer.js', 'asop-review.json');
|
||||
|
||||
// Phase 4: 修复
|
||||
console.log('\n🔧 Phase 4 · 修复 Agent');
|
||||
results.repair = runModule('repair-agent.js', 'repair-result.json');
|
||||
|
||||
// Phase 4.5: ASOP 执行已批准的优化
|
||||
console.log('\n🔧 Phase 4.5 · ASOP 执行已批准优化');
|
||||
results.asop_execute = runModule('asop-executor.js', 'asop-execute.json');
|
||||
|
||||
// Phase 4.7: ASOP 验证之前执行的优化效果
|
||||
console.log('\n🔍 Phase 4.7 · ASOP 验证优化效果');
|
||||
results.asop_verify = runModule('asop-verifier.js', 'asop-verify.json');
|
||||
|
||||
// Phase 6: 报告
|
||||
console.log('\n📋 Phase 6 · 全局健康报告');
|
||||
results.report = runModule('report-generator.js', null);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,178 @@
|
|||
/**
|
||||
* Unit test · Notion 页面阅读器 URL 解析
|
||||
*
|
||||
* 测试 extractPageId 函数对各种 Notion URL 格式的解析
|
||||
*
|
||||
* 运行: node tests/smoke/notion-page-reader.test.js
|
||||
*/
|
||||
|
||||
const { extractPageId, extractBlockText, blocksToMarkdown } = require('../../scripts/notion-page-reader');
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// extractPageId 测试
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
||||
describe('extractPageId', () => {
|
||||
test('extracts from 32-char hex string', () => {
|
||||
expect(extractPageId('abc123def4561234567890abcdef1234'))
|
||||
.toBe('abc123de-f456-1234-5678-90abcdef1234');
|
||||
});
|
||||
|
||||
test('extracts from UUID with dashes', () => {
|
||||
expect(extractPageId('abc123de-f456-1234-5678-90abcdef1234'))
|
||||
.toBe('abc123de-f456-1234-5678-90abcdef1234');
|
||||
});
|
||||
|
||||
test('extracts from notion.so URL with title', () => {
|
||||
expect(extractPageId('https://www.notion.so/workspace/My-Page-Title-abc123def4561234567890abcdef1234'))
|
||||
.toBe('abc123de-f456-1234-5678-90abcdef1234');
|
||||
});
|
||||
|
||||
test('extracts from notion.so URL without title', () => {
|
||||
expect(extractPageId('https://www.notion.so/abc123def4561234567890abcdef1234'))
|
||||
.toBe('abc123de-f456-1234-5678-90abcdef1234');
|
||||
});
|
||||
|
||||
test('extracts from notion.site URL', () => {
|
||||
expect(extractPageId('https://myworkspace.notion.site/Page-Title-abc123def4561234567890abcdef1234'))
|
||||
.toBe('abc123de-f456-1234-5678-90abcdef1234');
|
||||
});
|
||||
|
||||
test('handles URL with query parameters', () => {
|
||||
expect(extractPageId('https://www.notion.so/abc123def4561234567890abcdef1234?v=xxx&p=123'))
|
||||
.toBe('abc123de-f456-1234-5678-90abcdef1234');
|
||||
});
|
||||
|
||||
test('handles URL with anchor', () => {
|
||||
expect(extractPageId('https://www.notion.so/abc123def4561234567890abcdef1234#section'))
|
||||
.toBe('abc123de-f456-1234-5678-90abcdef1234');
|
||||
});
|
||||
|
||||
test('handles uppercase hex', () => {
|
||||
expect(extractPageId('ABC123DEF4561234567890ABCDEF1234'))
|
||||
.toBe('abc123de-f456-1234-5678-90abcdef1234');
|
||||
});
|
||||
|
||||
test('returns null for empty input', () => {
|
||||
expect(extractPageId('')).toBeNull();
|
||||
expect(extractPageId(null)).toBeNull();
|
||||
expect(extractPageId(undefined)).toBeNull();
|
||||
});
|
||||
|
||||
test('returns null for invalid input', () => {
|
||||
expect(extractPageId('not-a-valid-id')).toBeNull();
|
||||
expect(extractPageId('https://google.com')).toBeNull();
|
||||
});
|
||||
|
||||
test('handles whitespace around input', () => {
|
||||
expect(extractPageId(' abc123def4561234567890abcdef1234 '))
|
||||
.toBe('abc123de-f456-1234-5678-90abcdef1234');
|
||||
});
|
||||
});
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// extractBlockText 测试
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
||||
describe('extractBlockText', () => {
|
||||
test('extracts text from paragraph block', () => {
|
||||
const block = {
|
||||
type: 'paragraph',
|
||||
paragraph: {
|
||||
rich_text: [{ plain_text: 'Hello ' }, { plain_text: 'World' }]
|
||||
}
|
||||
};
|
||||
expect(extractBlockText(block)).toBe('Hello World');
|
||||
});
|
||||
|
||||
test('returns empty string for block without content', () => {
|
||||
const block = { type: 'divider', divider: {} };
|
||||
expect(extractBlockText(block)).toBe('');
|
||||
});
|
||||
|
||||
test('handles block with text property', () => {
|
||||
const block = {
|
||||
type: 'paragraph',
|
||||
paragraph: {
|
||||
text: [{ plain_text: 'Legacy text' }]
|
||||
}
|
||||
};
|
||||
expect(extractBlockText(block)).toBe('Legacy text');
|
||||
});
|
||||
});
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// blocksToMarkdown 测试
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
||||
describe('blocksToMarkdown', () => {
|
||||
test('converts heading blocks', () => {
|
||||
const blocks = [
|
||||
{ type: 'heading_1', heading_1: { rich_text: [{ plain_text: 'Title' }] } },
|
||||
{ type: 'heading_2', heading_2: { rich_text: [{ plain_text: 'Subtitle' }] } },
|
||||
{ type: 'heading_3', heading_3: { rich_text: [{ plain_text: 'Section' }] } },
|
||||
];
|
||||
const md = blocksToMarkdown(blocks);
|
||||
expect(md).toContain('# Title');
|
||||
expect(md).toContain('## Subtitle');
|
||||
expect(md).toContain('### Section');
|
||||
});
|
||||
|
||||
test('converts list items', () => {
|
||||
const blocks = [
|
||||
{ type: 'bulleted_list_item', bulleted_list_item: { rich_text: [{ plain_text: 'Bullet' }] } },
|
||||
{ type: 'numbered_list_item', numbered_list_item: { rich_text: [{ plain_text: 'Number' }] } },
|
||||
];
|
||||
const md = blocksToMarkdown(blocks);
|
||||
expect(md).toContain('- Bullet');
|
||||
expect(md).toContain('1. Number');
|
||||
});
|
||||
|
||||
test('converts code blocks', () => {
|
||||
const blocks = [
|
||||
{ type: 'code', code: { rich_text: [{ plain_text: 'console.log("hi")' }], language: 'javascript' } },
|
||||
];
|
||||
const md = blocksToMarkdown(blocks);
|
||||
expect(md).toContain('```javascript');
|
||||
expect(md).toContain('console.log("hi")');
|
||||
});
|
||||
|
||||
test('converts to_do blocks', () => {
|
||||
const blocks = [
|
||||
{ type: 'to_do', to_do: { rich_text: [{ plain_text: 'Done' }], checked: true } },
|
||||
{ type: 'to_do', to_do: { rich_text: [{ plain_text: 'Todo' }], checked: false } },
|
||||
];
|
||||
const md = blocksToMarkdown(blocks);
|
||||
expect(md).toContain('☑ Done');
|
||||
expect(md).toContain('☐ Todo');
|
||||
});
|
||||
|
||||
test('converts divider', () => {
|
||||
const blocks = [
|
||||
{ type: 'paragraph', paragraph: { rich_text: [{ plain_text: 'Before' }] } },
|
||||
{ type: 'divider', divider: {} },
|
||||
{ type: 'paragraph', paragraph: { rich_text: [{ plain_text: 'After' }] } },
|
||||
];
|
||||
const md = blocksToMarkdown(blocks);
|
||||
expect(md).toContain('---');
|
||||
});
|
||||
|
||||
test('converts quote and callout', () => {
|
||||
const blocks = [
|
||||
{ type: 'quote', quote: { rich_text: [{ plain_text: 'A quote' }] } },
|
||||
{ type: 'callout', callout: { rich_text: [{ plain_text: 'A callout' }] } },
|
||||
];
|
||||
const md = blocksToMarkdown(blocks);
|
||||
expect(md).toContain('> A quote');
|
||||
expect(md).toContain('> A callout');
|
||||
});
|
||||
|
||||
test('filters empty blocks', () => {
|
||||
const blocks = [
|
||||
{ type: 'paragraph', paragraph: { rich_text: [] } },
|
||||
{ type: 'paragraph', paragraph: { rich_text: [{ plain_text: 'Content' }] } },
|
||||
];
|
||||
const md = blocksToMarkdown(blocks);
|
||||
expect(md).toBe('Content');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue