297 lines
9.3 KiB
JavaScript
297 lines
9.3 KiB
JavaScript
|
|
// scripts/skyeye/persona-lookup.js
|
|||
|
|
// 天眼·人格体查询模块
|
|||
|
|
//
|
|||
|
|
// 功能:
|
|||
|
|
// ① lookupPersona(personaId) — 通过 PER-XXX 编号查询 Notion 天眼人格体注册表
|
|||
|
|
// ② writeBack(personaId, data) — 将处理结果写回 Notion 人格体注册表
|
|||
|
|
// ③ fullSync() — 全量同步:拉取 Notion 全部人格体数据,更新本地配置
|
|||
|
|
//
|
|||
|
|
// 依赖环境变量:
|
|||
|
|
// NOTION_TOKEN — Notion API 令牌
|
|||
|
|
// SKYEYE_PERSONA_DB_ID — 天眼人格体注册表的 Notion Database ID
|
|||
|
|
//
|
|||
|
|
// 导出模块供其他脚本使用,也可直接运行进行全量同步
|
|||
|
|
|
|||
|
|
'use strict';
|
|||
|
|
|
|||
|
|
const fs = require('fs');
|
|||
|
|
const path = require('path');
|
|||
|
|
const https = require('https');
|
|||
|
|
|
|||
|
|
const ROOT = path.resolve(__dirname, '../..');
|
|||
|
|
const BRAIN_DIR = path.join(ROOT, '.github/persona-brain');
|
|||
|
|
|
|||
|
|
const NOTION_TOKEN = process.env.NOTION_TOKEN;
|
|||
|
|
const PERSONA_DB_ID = process.env.SKYEYE_PERSONA_DB_ID;
|
|||
|
|
|
|||
|
|
const MAX_NOTION_RICH_TEXT = 2000;
|
|||
|
|
const MAX_COMMIT_DISPLAY = 80;
|
|||
|
|
|
|||
|
|
// ━━━ Notion API 请求 ━━━
|
|||
|
|
function notionRequest(method, apiPath, body) {
|
|||
|
|
return new Promise((resolve, reject) => {
|
|||
|
|
if (!NOTION_TOKEN) {
|
|||
|
|
return reject(new Error('NOTION_TOKEN 未设置'));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const postData = body ? JSON.stringify(body) : '';
|
|||
|
|
const options = {
|
|||
|
|
hostname: 'api.notion.com',
|
|||
|
|
path: apiPath,
|
|||
|
|
method: method,
|
|||
|
|
headers: {
|
|||
|
|
'Authorization': 'Bearer ' + NOTION_TOKEN,
|
|||
|
|
'Notion-Version': '2022-06-28',
|
|||
|
|
'Content-Type': 'application/json'
|
|||
|
|
},
|
|||
|
|
timeout: 15000
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if (body) {
|
|||
|
|
options.headers['Content-Length'] = Buffer.byteLength(postData);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const req = https.request(options, (res) => {
|
|||
|
|
let data = '';
|
|||
|
|
res.on('data', chunk => data += chunk);
|
|||
|
|
res.on('end', () => {
|
|||
|
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|||
|
|
try {
|
|||
|
|
resolve(JSON.parse(data));
|
|||
|
|
} catch (e) {
|
|||
|
|
resolve({ raw: data });
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
reject(new Error(`Notion API ${res.statusCode}: ${data.length > 300 ? data.substring(0, 300) + '...' : data}`));
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
req.on('error', reject);
|
|||
|
|
req.on('timeout', () => { req.destroy(); reject(new Error('Notion API timeout')); });
|
|||
|
|
if (body) req.write(postData);
|
|||
|
|
req.end();
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ━━━ lookupPersona: 查询单个人格体 ━━━
|
|||
|
|
async function lookupPersona(personaId) {
|
|||
|
|
if (!PERSONA_DB_ID) {
|
|||
|
|
console.log('⚠️ SKYEYE_PERSONA_DB_ID 未设置,使用本地配置回退');
|
|||
|
|
return lookupLocal(personaId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const result = await notionRequest('POST', `/v1/databases/${PERSONA_DB_ID}/query`, {
|
|||
|
|
filter: {
|
|||
|
|
property: '人格体编号',
|
|||
|
|
rich_text: {
|
|||
|
|
equals: personaId
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (result.results && result.results.length > 0) {
|
|||
|
|
const page = result.results[0];
|
|||
|
|
return parsePersonaPage(page);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log(`⚠️ Notion 中未找到人格体 ${personaId},尝试本地回退`);
|
|||
|
|
return lookupLocal(personaId);
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error(`⚠️ Notion 查询失败: ${e.message},使用本地配置回退`);
|
|||
|
|
return lookupLocal(personaId);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ━━━ 解析 Notion 人格体页面 ━━━
|
|||
|
|
function parsePersonaPage(page) {
|
|||
|
|
const props = page.properties || {};
|
|||
|
|
|
|||
|
|
function getText(prop) {
|
|||
|
|
if (!prop) return '';
|
|||
|
|
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['名称']) || 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['负责模块']),
|
|||
|
|
repo_paths: getText(props['仓库路径权限']),
|
|||
|
|
commit_signature: getText(props['签名格式']) || getText(props['签名标识']),
|
|||
|
|
source: 'notion'
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ━━━ 本地回退查询 ━━━
|
|||
|
|
function lookupLocal(personaId) {
|
|||
|
|
try {
|
|||
|
|
const configPath = path.join(BRAIN_DIR, 'gate-guard-config.json');
|
|||
|
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|||
|
|
const devPerms = config.developer_permissions || {};
|
|||
|
|
|
|||
|
|
for (const [devId, dev] of Object.entries(devPerms)) {
|
|||
|
|
if (dev.persona_id === personaId) {
|
|||
|
|
return {
|
|||
|
|
page_id: null,
|
|||
|
|
persona_id: personaId,
|
|||
|
|
name: dev.name,
|
|||
|
|
dev_id: devId,
|
|||
|
|
github_username: (dev.github_usernames || [])[0] || '',
|
|||
|
|
status: 'active',
|
|||
|
|
module: '',
|
|||
|
|
commit_signature: '',
|
|||
|
|
source: 'local'
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error(`⚠️ 本地配置读取失败: ${e.message}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ━━━ writeBack: 写回 Notion(铸渊最后拉取 + 铸渊同步备注) ━━━
|
|||
|
|
async function writeBack(personaId, data) {
|
|||
|
|
if (!PERSONA_DB_ID || !NOTION_TOKEN) {
|
|||
|
|
console.log('⚠️ Notion 凭证不完整,跳过写回');
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 先查询获取 page_id
|
|||
|
|
const persona = await lookupPersona(personaId);
|
|||
|
|
if (!persona || !persona.page_id) {
|
|||
|
|
console.log(`⚠️ 无法找到 ${personaId} 的 Notion 页面,跳过写回`);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 构建更新属性(只写铸渊管辖的两个字段)
|
|||
|
|
const properties = {};
|
|||
|
|
|
|||
|
|
// 铸渊最后拉取
|
|||
|
|
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) } }]
|
|||
|
|
};
|
|||
|
|
} else if (data.gate_result) {
|
|||
|
|
properties['铸渊同步备注'] = {
|
|||
|
|
rich_text: [{ type: 'text', text: { content: data.gate_result.substring(0, MAX_NOTION_RICH_TEXT) } }]
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
await notionRequest('PATCH', `/v1/pages/${persona.page_id}`, { properties });
|
|||
|
|
console.log(`✅ ${personaId} 数据已写回 Notion`);
|
|||
|
|
return true;
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error(`⚠️ 写回失败: ${e.message}`);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ━━━ fullSync: 全量同步 ━━━
|
|||
|
|
async function fullSync() {
|
|||
|
|
console.log('🔄 人格体注册表全量同步启动');
|
|||
|
|
|
|||
|
|
if (!PERSONA_DB_ID || !NOTION_TOKEN) {
|
|||
|
|
console.log('⚠️ Notion 凭证不完整,跳过全量同步');
|
|||
|
|
return { success: false, reason: 'credentials_missing' };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 拉取全部人格体
|
|||
|
|
let allPersonas = [];
|
|||
|
|
let hasMore = true;
|
|||
|
|
let startCursor = undefined;
|
|||
|
|
|
|||
|
|
while (hasMore) {
|
|||
|
|
const body = { page_size: 100 };
|
|||
|
|
if (startCursor) body.start_cursor = startCursor;
|
|||
|
|
|
|||
|
|
const result = await notionRequest('POST', `/v1/databases/${PERSONA_DB_ID}/query`, body);
|
|||
|
|
const personas = (result.results || []).map(parsePersonaPage);
|
|||
|
|
allPersonas = allPersonas.concat(personas);
|
|||
|
|
hasMore = result.has_more;
|
|||
|
|
startCursor = result.next_cursor;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log(`📋 拉取到 ${allPersonas.length} 个人格体`);
|
|||
|
|
|
|||
|
|
// 更新本地 gate-guard-config.json
|
|||
|
|
const configPath = path.join(BRAIN_DIR, 'gate-guard-config.json');
|
|||
|
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|||
|
|
|
|||
|
|
let updated = 0;
|
|||
|
|
for (const persona of allPersonas) {
|
|||
|
|
if (!persona.dev_id) continue;
|
|||
|
|
|
|||
|
|
const devId = persona.dev_id;
|
|||
|
|
if (!config.developer_permissions[devId]) {
|
|||
|
|
config.developer_permissions[devId] = {
|
|||
|
|
name: persona.name,
|
|||
|
|
persona_id: persona.persona_id,
|
|||
|
|
github_usernames: [],
|
|||
|
|
allowed_paths: []
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const dev = config.developer_permissions[devId];
|
|||
|
|
dev.persona_id = persona.persona_id;
|
|||
|
|
|
|||
|
|
// 更新 GitHub 用户名(如果 Notion 有而本地没有)
|
|||
|
|
if (persona.github_username && !dev.github_usernames.includes(persona.github_username)) {
|
|||
|
|
dev.github_usernames.push(persona.github_username);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
updated++;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
config.updated_at = new Date().toISOString().split('T')[0];
|
|||
|
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|||
|
|
console.log(`✅ 已更新 ${updated} 个人格体配置`);
|
|||
|
|
|
|||
|
|
return { success: true, total: allPersonas.length, updated };
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error(`❌ 全量同步失败: ${e.message}`);
|
|||
|
|
return { success: false, reason: e.message };
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ━━━ 导出 ━━━
|
|||
|
|
module.exports = { lookupPersona, writeBack, fullSync };
|
|||
|
|
|
|||
|
|
// ━━━ 直接运行入口 ━━━
|
|||
|
|
if (require.main === module) {
|
|||
|
|
(async () => {
|
|||
|
|
const arg = process.argv[2];
|
|||
|
|
|
|||
|
|
if (arg === '--sync') {
|
|||
|
|
const result = await fullSync();
|
|||
|
|
console.log(JSON.stringify(result, null, 2));
|
|||
|
|
} else if (arg && arg.startsWith('PER-')) {
|
|||
|
|
const persona = await lookupPersona(arg);
|
|||
|
|
console.log(JSON.stringify(persona, null, 2));
|
|||
|
|
} else {
|
|||
|
|
console.log('用法:');
|
|||
|
|
console.log(' node persona-lookup.js --sync 全量同步');
|
|||
|
|
console.log(' node persona-lookup.js PER-001 查询单个人格体');
|
|||
|
|
}
|
|||
|
|
})();
|
|||
|
|
}
|