Merge pull request #192 from qinfendebingshuo/copilot/activate-skyeye-scan
feat: Language-driven OS execution layer — Phases 7-9, S1-S9
This commit is contained in:
commit
d9bec2e3bf
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"broadcasts": [
|
||||
{
|
||||
"title": "示例广播:新增 NOTIFICATION 业务域",
|
||||
"from": "霜砚",
|
||||
"date": "2026-03-05",
|
||||
"rules_version": "v1.1",
|
||||
"update_target": "routing-map",
|
||||
"data": {
|
||||
"NOTIFICATION": {
|
||||
"module": "M09",
|
||||
"route_prefix": "/hli/notification",
|
||||
"interfaces": [
|
||||
{
|
||||
"id": "HLI-NOTIF-001",
|
||||
"path": "/hli/notification/push",
|
||||
"status": "pending"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"last_synced": "2026-03-25T08:10:32.053Z"
|
||||
}
|
||||
|
|
@ -10,9 +10,16 @@
|
|||
"org": "qinfendebingshuo",
|
||||
"repo_url": "https://github.com/qinfendebingshuo/guanghulab"
|
||||
},
|
||||
"modules_owned": [
|
||||
{
|
||||
"path": "backend/"
|
||||
},
|
||||
{
|
||||
"path": "src/"
|
||||
}
|
||||
],
|
||||
"current_work": {},
|
||||
"recent_syslogs": [],
|
||||
"modules_owned": [],
|
||||
"skyeye_health": {
|
||||
"last_scan": "2026-03-25 06:19:35+08:00",
|
||||
"status": "not_scanned",
|
||||
|
|
@ -20,5 +27,5 @@
|
|||
"warnings": []
|
||||
},
|
||||
"notion_page_urls": {},
|
||||
"last_synced": "2026-03-25T07:02:12.446Z"
|
||||
"last_synced": "2026-03-25T08:10:32.046Z"
|
||||
}
|
||||
|
|
@ -10,9 +10,19 @@
|
|||
"org": "qinfendebingshuo",
|
||||
"repo_url": "https://github.com/qinfendebingshuo/guanghu-feimao"
|
||||
},
|
||||
"modules_owned": [
|
||||
{
|
||||
"path": "frontend/"
|
||||
},
|
||||
{
|
||||
"path": "persona-selector/"
|
||||
},
|
||||
{
|
||||
"path": "chat-bubble/"
|
||||
}
|
||||
],
|
||||
"current_work": {},
|
||||
"recent_syslogs": [],
|
||||
"modules_owned": [],
|
||||
"skyeye_health": {
|
||||
"last_scan": "2026-03-25 06:19:35+08:00",
|
||||
"status": "not_scanned",
|
||||
|
|
@ -20,5 +30,5 @@
|
|||
"warnings": []
|
||||
},
|
||||
"notion_page_urls": {},
|
||||
"last_synced": "2026-03-25T07:02:12.846Z"
|
||||
"last_synced": "2026-03-25T08:10:32.048Z"
|
||||
}
|
||||
|
|
@ -10,9 +10,16 @@
|
|||
"org": "qinfendebingshuo",
|
||||
"repo_url": "https://github.com/qinfendebingshuo/guanghu-yanfan"
|
||||
},
|
||||
"modules_owned": [
|
||||
{
|
||||
"path": "settings/"
|
||||
},
|
||||
{
|
||||
"path": "cloud-drive/"
|
||||
}
|
||||
],
|
||||
"current_work": {},
|
||||
"recent_syslogs": [],
|
||||
"modules_owned": [],
|
||||
"skyeye_health": {
|
||||
"last_scan": "2026-03-25 06:19:35+08:00",
|
||||
"status": "not_scanned",
|
||||
|
|
@ -20,5 +27,5 @@
|
|||
"warnings": []
|
||||
},
|
||||
"notion_page_urls": {},
|
||||
"last_synced": "2026-03-25T07:02:13.053Z"
|
||||
"last_synced": "2026-03-25T08:10:32.049Z"
|
||||
}
|
||||
|
|
@ -10,9 +10,13 @@
|
|||
"org": "qinfendebingshuo",
|
||||
"repo_url": "https://github.com/qinfendebingshuo/guanghu-zhizhi"
|
||||
},
|
||||
"modules_owned": [
|
||||
{
|
||||
"path": "dingtalk-bot/"
|
||||
}
|
||||
],
|
||||
"current_work": {},
|
||||
"recent_syslogs": [],
|
||||
"modules_owned": [],
|
||||
"skyeye_health": {
|
||||
"last_scan": "2026-03-25 06:19:35+08:00",
|
||||
"status": "not_scanned",
|
||||
|
|
@ -20,5 +24,5 @@
|
|||
"warnings": []
|
||||
},
|
||||
"notion_page_urls": {},
|
||||
"last_synced": "2026-03-25T07:02:13.536Z"
|
||||
"last_synced": "2026-03-25T08:10:32.050Z"
|
||||
}
|
||||
|
|
@ -10,9 +10,13 @@
|
|||
"org": "qinfendebingshuo",
|
||||
"repo_url": "https://github.com/qinfendebingshuo/guanghu-xiaocaomei"
|
||||
},
|
||||
"modules_owned": [
|
||||
{
|
||||
"path": "status-board/"
|
||||
}
|
||||
],
|
||||
"current_work": {},
|
||||
"recent_syslogs": [],
|
||||
"modules_owned": [],
|
||||
"skyeye_health": {
|
||||
"last_scan": "2026-03-25 06:19:35+08:00",
|
||||
"status": "not_scanned",
|
||||
|
|
@ -20,5 +24,5 @@
|
|||
"warnings": []
|
||||
},
|
||||
"notion_page_urls": {},
|
||||
"last_synced": "2026-03-25T07:02:13.734Z"
|
||||
"last_synced": "2026-03-25T08:10:32.050Z"
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"dev_id": "DEV-009",
|
||||
"name": "花尔",
|
||||
"persona": {
|
||||
"name": "花尔",
|
||||
"id": "PER-HE001"
|
||||
},
|
||||
"github": {
|
||||
"repo": "guanghulab",
|
||||
"org": "qinfendebingshuo",
|
||||
"repo_url": "https://github.com/qinfendebingshuo/guanghulab"
|
||||
},
|
||||
"modules_owned": [
|
||||
{
|
||||
"path": "user-center/"
|
||||
}
|
||||
],
|
||||
"current_work": {},
|
||||
"recent_syslogs": [],
|
||||
"skyeye_health": {
|
||||
"last_scan": "2026-03-25 06:19:35+08:00",
|
||||
"status": "not_scanned",
|
||||
"issues": [],
|
||||
"warnings": []
|
||||
},
|
||||
"notion_page_urls": {},
|
||||
"last_synced": "2026-03-25T08:10:32.051Z"
|
||||
}
|
||||
|
|
@ -10,9 +10,19 @@
|
|||
"org": "qinfendebingshuo",
|
||||
"repo_url": "https://github.com/qinfendebingshuo/guanghu-juzi"
|
||||
},
|
||||
"modules_owned": [
|
||||
{
|
||||
"path": "ticket-system/"
|
||||
},
|
||||
{
|
||||
"path": "data-stats/"
|
||||
},
|
||||
{
|
||||
"path": "dynamic-comic/"
|
||||
}
|
||||
],
|
||||
"current_work": {},
|
||||
"recent_syslogs": [],
|
||||
"modules_owned": [],
|
||||
"skyeye_health": {
|
||||
"last_scan": "2026-03-25 06:19:35+08:00",
|
||||
"status": "not_scanned",
|
||||
|
|
@ -20,5 +30,5 @@
|
|||
"warnings": []
|
||||
},
|
||||
"notion_page_urls": {},
|
||||
"last_synced": "2026-03-25T07:02:13.937Z"
|
||||
"last_synced": "2026-03-25T08:10:32.051Z"
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"dev_id": "DEV-011",
|
||||
"name": "匆匆那年",
|
||||
"persona": {
|
||||
"name": "匆匆那年",
|
||||
"id": "PER-CCN001"
|
||||
},
|
||||
"github": {
|
||||
"repo": "guanghulab",
|
||||
"org": "qinfendebingshuo",
|
||||
"repo_url": "https://github.com/qinfendebingshuo/guanghulab"
|
||||
},
|
||||
"modules_owned": [
|
||||
{
|
||||
"path": "writing-workspace/"
|
||||
}
|
||||
],
|
||||
"current_work": {},
|
||||
"recent_syslogs": [],
|
||||
"skyeye_health": {
|
||||
"last_scan": "2026-03-25 06:19:35+08:00",
|
||||
"status": "not_scanned",
|
||||
"issues": [],
|
||||
"warnings": []
|
||||
},
|
||||
"notion_page_urls": {},
|
||||
"last_synced": "2026-03-25T08:10:32.051Z"
|
||||
}
|
||||
|
|
@ -10,9 +10,13 @@
|
|||
"org": "qinfendebingshuo",
|
||||
"repo_url": "https://github.com/qinfendebingshuo/guanghu-awen"
|
||||
},
|
||||
"modules_owned": [
|
||||
{
|
||||
"path": "notification-center/"
|
||||
}
|
||||
],
|
||||
"current_work": {},
|
||||
"recent_syslogs": [],
|
||||
"modules_owned": [],
|
||||
"skyeye_health": {
|
||||
"last_scan": "2026-03-25 06:19:35+08:00",
|
||||
"status": "not_scanned",
|
||||
|
|
@ -20,5 +24,5 @@
|
|||
"warnings": []
|
||||
},
|
||||
"notion_page_urls": {},
|
||||
"last_synced": "2026-03-25T07:02:14.100Z"
|
||||
"last_synced": "2026-03-25T08:10:32.052Z"
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
[
|
||||
{
|
||||
"id": "DEV-001",
|
||||
"name": "页页",
|
||||
"persona": "小坍缩核",
|
||||
"personaId": "PER-XTC001"
|
||||
},
|
||||
{
|
||||
"id": "DEV-002",
|
||||
"name": "肥猫",
|
||||
"persona": "舒舒",
|
||||
"personaId": "PER-SS001"
|
||||
},
|
||||
{
|
||||
"id": "DEV-003",
|
||||
"name": "燕樊",
|
||||
"persona": "寂曜",
|
||||
"personaId": "PER-JY001"
|
||||
},
|
||||
{
|
||||
"id": "DEV-004",
|
||||
"name": "之之",
|
||||
"persona": "秋秋",
|
||||
"personaId": "PER-QQ001"
|
||||
},
|
||||
{
|
||||
"id": "DEV-005",
|
||||
"name": "小草莓",
|
||||
"persona": "欧诺弥亚",
|
||||
"personaId": "PER-ONM001"
|
||||
},
|
||||
{
|
||||
"id": "DEV-009",
|
||||
"name": "花尔",
|
||||
"persona": "花尔",
|
||||
"personaId": "PER-HE001"
|
||||
},
|
||||
{
|
||||
"id": "DEV-010",
|
||||
"name": "桔子",
|
||||
"persona": "晨星",
|
||||
"personaId": "PER-MRN001"
|
||||
},
|
||||
{
|
||||
"id": "DEV-011",
|
||||
"name": "匆匆那年",
|
||||
"persona": "匆匆那年",
|
||||
"personaId": "PER-CCN001"
|
||||
},
|
||||
{
|
||||
"id": "DEV-012",
|
||||
"name": "Awen",
|
||||
"persona": "知秋",
|
||||
"personaId": "PER-ZQ001"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
# ═══════════════════════════════════════════════
|
||||
# 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
|
||||
# 📜 Copyright: 国作登字-2026-A-00037559
|
||||
# ═══════════════════════════════════════════════
|
||||
name: "🚀 部署后端中间层到阿里云"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths: ['backend/api-server/**']
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: "🚀 部署 guanghu-api 到阿里云 ECS"
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: "📥 检出仓库"
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: "🌊 本体论锚定"
|
||||
run: |
|
||||
echo "═══ 本体论锚定 ═══"
|
||||
echo "执行主体:AG-ZY-01 铸渊"
|
||||
echo "版权锚点:国作登字-2026-A-00037559"
|
||||
echo "通道:B · 后端中间层部署"
|
||||
echo "═══ 锚定完成 ═══"
|
||||
|
||||
- name: "🚀 部署到阿里云"
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ secrets.DEPLOY_HOST }}
|
||||
username: ${{ secrets.DEPLOY_USER }}
|
||||
key: ${{ secrets.DEPLOY_KEY }}
|
||||
script: |
|
||||
cd ${{ secrets.DEPLOY_PATH }}/backend/api-server/
|
||||
git pull origin main
|
||||
npm install --production
|
||||
pm2 restart guanghu-api || pm2 start ecosystem.config.js
|
||||
echo "✅ guanghu-api 部署完成 · $(TZ='Asia/Shanghai' date '+%Y-%m-%d %H:%M')"
|
||||
|
||||
# ━━━ 🧬 双端神经系统·自报告 ━━━
|
||||
- name: "🧬 写入神经自报告"
|
||||
if: always()
|
||||
run: |
|
||||
WORKFLOW_ID="deploy-backend"
|
||||
REPORT_DIR="data/neural-reports/deploy-backend"
|
||||
TIMESTAMP=$(TZ=Asia/Shanghai date +%Y-%m-%d-%H%M)
|
||||
STATUS="${{ job.status }}"
|
||||
|
||||
mkdir -p "$REPORT_DIR"
|
||||
|
||||
cat > "${REPORT_DIR}/${WORKFLOW_ID}-${TIMESTAMP}.json" << EOF
|
||||
{
|
||||
"workflow_id": "${WORKFLOW_ID}",
|
||||
"run_id": "${{ github.run_id }}",
|
||||
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"status": "${STATUS}",
|
||||
"brain": "SY-01",
|
||||
"event": "${{ github.event_name }}",
|
||||
"branch": "${{ github.ref_name }}",
|
||||
"commit": "${{ github.sha }}"
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "📋 自报告已生成"
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
# ═══════════════════════════════════════════════
|
||||
# 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
|
||||
# 📜 Copyright: 国作登字-2026-A-00037559
|
||||
# ═══════════════════════════════════════════════
|
||||
name: "📡 同步 Notion 数据到缓存"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 */6 * * *' # 每6小时同步
|
||||
workflow_dispatch: # 手动触发
|
||||
repository_dispatch:
|
||||
types: [notion-cache-sync] # 铸渊/霜砚可远程触发
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
name: "📡 Notion 全量缓存同步"
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: "📥 检出仓库"
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: "🔧 安装 Node.js"
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: "🌊 本体论锚定"
|
||||
run: |
|
||||
echo "═══ 本体论锚定 ═══"
|
||||
echo "执行主体:AG-ZY-01 铸渊"
|
||||
echo "版权锚点:国作登字-2026-A-00037559"
|
||||
echo "通道:A · GitHub 缓存层"
|
||||
echo "═══ 锚定完成 ═══"
|
||||
|
||||
- name: "📡 同步 Notion 数据"
|
||||
env:
|
||||
NOTION_TOKEN: ${{ secrets.NOTION_API_KEY }}
|
||||
NOTION_CONTROL_PANEL_DB_ID: ${{ secrets.NOTION_CONTROL_PANEL_DB_ID }}
|
||||
NOTION_SYSLOG_DB_ID: ${{ secrets.NOTION_SYSLOG_DB_ID }}
|
||||
NOTION_AGENT_REGISTRY_DB_ID: ${{ secrets.NOTION_AGENT_REGISTRY_DB_ID }}
|
||||
NOTION_TICKET_DB_ID: ${{ secrets.NOTION_TICKET_DB_ID }}
|
||||
NOTION_MAINTENANCE_DB_ID: ${{ secrets.NOTION_MAINTENANCE_DB_ID }}
|
||||
RECEIPT_DB_ID: ${{ secrets.RECEIPT_DB_ID }}
|
||||
run: node scripts/cache/sync-notion-cache.js
|
||||
|
||||
- name: "📝 提交缓存文件"
|
||||
run: |
|
||||
git config user.name "铸渊 (ZhùYuān)"
|
||||
git config user.email "zhuyuan@guanghulab.com"
|
||||
git add .github/notion-cache/
|
||||
if git diff --cached --quiet; then
|
||||
echo "📡 Notion 缓存无变更,跳过提交"
|
||||
exit 0
|
||||
fi
|
||||
git commit -m "📡 Notion缓存同步 · $(TZ='Asia/Shanghai' date '+%Y-%m-%d %H:%M')"
|
||||
git push origin main
|
||||
|
||||
# ━━━ 🧬 双端神经系统·自报告 ━━━
|
||||
- name: "🧬 写入神经自报告"
|
||||
if: always()
|
||||
run: |
|
||||
WORKFLOW_ID="notion-cache-sync"
|
||||
REPORT_DIR="data/neural-reports/notion-cache-sync"
|
||||
TIMESTAMP=$(TZ=Asia/Shanghai date +%Y-%m-%d-%H%M)
|
||||
STATUS="${{ job.status }}"
|
||||
|
||||
mkdir -p "$REPORT_DIR"
|
||||
|
||||
cat > "${REPORT_DIR}/${WORKFLOW_ID}-${TIMESTAMP}.json" << EOF
|
||||
{
|
||||
"workflow_id": "${WORKFLOW_ID}",
|
||||
"run_id": "${{ github.run_id }}",
|
||||
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"status": "${STATUS}",
|
||||
"brain": "SY-01",
|
||||
"event": "${{ github.event_name }}",
|
||||
"branch": "${{ github.ref_name }}",
|
||||
"commit": "${{ github.sha }}"
|
||||
}
|
||||
EOF
|
||||
|
||||
git config user.name "zhuyuan-bot"
|
||||
git config user.email "zhuyuan@guanghulab.com"
|
||||
git add "$REPORT_DIR/"
|
||||
git diff --cached --quiet || \
|
||||
git commit -m "🧬 ${WORKFLOW_ID} 自报告 · ${TIMESTAMP} [skip ci]"
|
||||
git push || echo "⚠️ 自报告push失败(不阻断主流程)"
|
||||
|
||||
# ━━━ 📡 指令回执·自动同步 ━━━
|
||||
- name: "📡 同步回执到 Notion"
|
||||
if: always()
|
||||
env:
|
||||
NOTION_API_KEY: ${{ secrets.NOTION_API_KEY }}
|
||||
RECEIPT_DB_ID: ${{ secrets.RECEIPT_DB_ID }}
|
||||
run: |
|
||||
node scripts/neural/write-receipt-to-notion.js \
|
||||
--instruction-id "ZY-DUALCHANNEL-2026-0325-001" \
|
||||
--status "${{ job.status }}" \
|
||||
--workflow "notion-cache-sync" \
|
||||
--summary "Notion 全量缓存同步" \
|
||||
--related-agent "SY-01"
|
||||
|
|
@ -2,3 +2,4 @@
|
|||
node_modules/
|
||||
*.bak
|
||||
*.log
|
||||
backend/api-server/logs/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"version": "1.0.0",
|
||||
"description": "天眼终审授权人配置 · 部署到正式站前的人类终审",
|
||||
"rules": {
|
||||
"男频": {
|
||||
"approvers": [
|
||||
{ "devId": "DEV-002", "name": "肥猫", "role": "男频内容授权人" }
|
||||
],
|
||||
"requireAll": true
|
||||
},
|
||||
"女频": {
|
||||
"approvers": [
|
||||
{ "devId": "DEV-010", "name": "桔子", "role": "女频内容授权人" }
|
||||
],
|
||||
"requireAll": true
|
||||
},
|
||||
"系统": {
|
||||
"approvers": [
|
||||
{ "devId": "DEV-002", "name": "肥猫", "role": "男频内容授权人" },
|
||||
{ "devId": "DEV-010", "name": "桔子", "role": "女频内容授权人" }
|
||||
],
|
||||
"requireAll": true
|
||||
},
|
||||
"跨频": {
|
||||
"approvers": [
|
||||
{ "devId": "DEV-002", "name": "肥猫", "role": "男频内容授权人" },
|
||||
{ "devId": "DEV-010", "name": "桔子", "role": "女频内容授权人" }
|
||||
],
|
||||
"requireAll": true
|
||||
}
|
||||
},
|
||||
"defaults": {
|
||||
"channel": "系统",
|
||||
"timeoutHours": 72,
|
||||
"noResponseAction": "不通过(不回应视为不通过)"
|
||||
},
|
||||
"audit": {
|
||||
"logEveryDecision": true,
|
||||
"fields": ["timestamp", "approvalId", "devId", "decision", "channel", "module"]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
{
|
||||
"version": "1.0.0",
|
||||
"description": "系统自治运行规则 · Phase 9",
|
||||
"dualLineArchitecture": {
|
||||
"lineA": {
|
||||
"name": "操作系统底座线",
|
||||
"owner": "TCS-0002",
|
||||
"scope": ["权限系统", "部署流水线", "神经网络升级", "执行保护层", "架构变更"],
|
||||
"interactionModes": {
|
||||
"instructionMode": {
|
||||
"description": "冰朔说方向 → 霜砚拆指令 → 铸渊执行",
|
||||
"trigger": ["签发指令", "写个指令", "下发指令"],
|
||||
"suitableFor": "架构级、系统级变更"
|
||||
},
|
||||
"naturalLanguageMode": {
|
||||
"description": "冰朔直接和系统说话,系统即时响应",
|
||||
"trigger": "默认模式(直接说需求/问问题)",
|
||||
"suitableFor": "日常操作、快速调整"
|
||||
}
|
||||
},
|
||||
"priority": "最高 · 冰朔说话 = 最高优先级,无论哪种模式"
|
||||
},
|
||||
"lineB": {
|
||||
"name": "具体功能线",
|
||||
"owners": "开发者团队",
|
||||
"scope": ["Persona Studio", "工单系统", "数据看板", "动态漫画", "各模块功能"],
|
||||
"interactionMode": "日常交互(和铸渊说话 → 系统执行)"
|
||||
}
|
||||
},
|
||||
"autonomyRules": {
|
||||
"systemCanDo": [
|
||||
"自主调整内部运行逻辑(不违反铁律)",
|
||||
"自主升级能力模块(记录到审计日志)",
|
||||
"自主优化瓶颈路径",
|
||||
"定期生成系统健康报告",
|
||||
"异常时主动向霜砚报告"
|
||||
],
|
||||
"systemMustNot": [
|
||||
"违反语言系统两条铁律",
|
||||
"绕过天眼审核(S7 强制前置,不可关闭)",
|
||||
"绕过人类授权步骤",
|
||||
"修改权限分层(权限变更只有冰朔能触发)",
|
||||
"关闭或跳过天眼指令审核(S7 不可关闭,包括冰朔也不能关闭)"
|
||||
]
|
||||
},
|
||||
"deploymentFlow": {
|
||||
"directDeployRule": {
|
||||
"id": "S5",
|
||||
"description": "冰朔直通部署规则:冰朔签发或口头下达的所有指令,执行完毕后直接部署到正式站",
|
||||
"conditions": [
|
||||
"TCS-0002 发起的请求",
|
||||
"ZY- 开头的指令编号,且签发者为霜砚(AG-SY-01)或冰朔(TCS-0002)"
|
||||
],
|
||||
"bypass": ["预览站", "天眼审核", "肥猫/桔子授权"],
|
||||
"auditRequired": true,
|
||||
"reason": "冰朔本人就是系统最高权限(Level 3),冰朔说话 = 最终授权"
|
||||
},
|
||||
"developerFlow": {
|
||||
"description": "跨频/系统级变更走完整S2审批流程",
|
||||
"stages": ["预览站部署", "天眼审核", "授权人审批", "自动发布"]
|
||||
},
|
||||
"channelAutonomyRule": {
|
||||
"id": "S6",
|
||||
"description": "个人频道完全自治:开发者在自己频道内拥有完全自主权",
|
||||
"principles": [
|
||||
"自己是自己的主控 — 频道内界面、功能、布局、样式完全自主",
|
||||
"自己是自己的天眼 — 频道内变更由自己审核,不需要天眼/授权人",
|
||||
"自己确认自己负责 — 出了问题自己承担"
|
||||
],
|
||||
"channelDeployFlow": [
|
||||
"开发者在频道内做改动",
|
||||
"系统执行(不可逆)",
|
||||
"部署到预览站(GitHub Pages)",
|
||||
"开发者自己预览确认",
|
||||
"部署到正式站(自己的频道区域)"
|
||||
],
|
||||
"skyeyeIntervention": {
|
||||
"intervenes": [
|
||||
"跨频操作 — 动了别人的频道",
|
||||
"系统级变更 — 动了公共基础设施、共享模块",
|
||||
"频道创建/删除/权限变更 — 频道本身的生死",
|
||||
"正式站公共区域发布 — 不属于任何个人频道的内容"
|
||||
],
|
||||
"doesNotIntervene": "频道内部的一切,天眼不看、不管、不干预"
|
||||
},
|
||||
"channelIrreversibility": "频道一旦创建,不可取消、不可撤回(语言系统第一铁律适用)",
|
||||
"auditRequired": true
|
||||
},
|
||||
"stages": [
|
||||
{
|
||||
"stage": 1,
|
||||
"name": "部署到预览站",
|
||||
"target": "GitHub Pages (qinfendebingshuo.github.io/guanghulab/docs/)",
|
||||
"description": "真实可访问的预览环境,让人类确认执行结果"
|
||||
},
|
||||
{
|
||||
"stage": 2,
|
||||
"name": "天眼审核",
|
||||
"checks": ["代码安全", "结构完整性", "功能回归"],
|
||||
"passAction": "进入 Stage 3",
|
||||
"failAction": "标记问题,开发者修复后重新提交"
|
||||
},
|
||||
{
|
||||
"stage": 3,
|
||||
"name": "人类终审授权",
|
||||
"description": "授权人必须手动点击确认,不回应视为不通过",
|
||||
"configFile": "approvers.json"
|
||||
},
|
||||
{
|
||||
"stage": 4,
|
||||
"name": "自动发布到正式站",
|
||||
"target": "guanghulab.com",
|
||||
"trigger": "deploy-to-server.yml",
|
||||
"description": "授权即发布,无需人类再次操作"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* Notion 数据库 ID 映射配置
|
||||
*
|
||||
* 所有 ID 通过环境变量传入,禁止硬编码
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
// 主控台(开发者当前任务)
|
||||
controlPanel: process.env.NOTION_CONTROL_PANEL_DB_ID || '',
|
||||
|
||||
// SYSLOG 收件箱
|
||||
syslogInbox: process.env.NOTION_SYSLOG_DB_ID || '',
|
||||
|
||||
// Agent 注册表
|
||||
agentRegistry: process.env.NOTION_AGENT_REGISTRY_DB_ID || '',
|
||||
|
||||
// 工单簿
|
||||
ticketBook: process.env.NOTION_TICKET_DB_ID || '',
|
||||
|
||||
// 维护日志
|
||||
maintenanceLog: process.env.NOTION_MAINTENANCE_DB_ID || '',
|
||||
|
||||
// 指令回执追踪表
|
||||
receiptTracker: process.env.RECEIPT_DB_ID || '',
|
||||
|
||||
// 模块指纹注册表
|
||||
moduleRegistry: process.env.NOTION_MODULE_REGISTRY_DB_ID || '',
|
||||
};
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
{
|
||||
"tools": [
|
||||
{
|
||||
"name": "query_tickets",
|
||||
"description": "查询开发者的工单列表。开发者说'我的工单'、'还有几个工单没关'、'查一下Bug'时触发。",
|
||||
"parameters": {
|
||||
"status": { "type": "string", "enum": ["全部", "待处理", "进行中", "已完成"], "default": "全部" }
|
||||
},
|
||||
"api": "GET /api/tickets?devId={devId}&status={status}",
|
||||
"permission": "ticket:read"
|
||||
},
|
||||
{
|
||||
"name": "create_ticket",
|
||||
"description": "创建新工单。开发者说'建个工单'、'提一个Bug'、'报告问题'时触发。",
|
||||
"parameters": {
|
||||
"title": { "type": "string", "required": true },
|
||||
"description": { "type": "string" },
|
||||
"priority": { "type": "string", "enum": ["P0", "P1", "P2", "P3"], "default": "P2" },
|
||||
"module": { "type": "string" }
|
||||
},
|
||||
"api": "POST /api/tickets/create",
|
||||
"permission": "ticket:create"
|
||||
},
|
||||
{
|
||||
"name": "submit_syslog",
|
||||
"description": "提交系统日志。开发者说'提交日志'、'写SYSLOG'、'记录一下'时触发。",
|
||||
"parameters": {
|
||||
"content": { "type": "string", "required": true },
|
||||
"type": { "type": "string", "enum": ["日常", "Bug修复", "功能完成", "部署", "紧急"], "default": "日常" }
|
||||
},
|
||||
"api": "POST /api/syslog/submit",
|
||||
"permission": "syslog:submit"
|
||||
},
|
||||
{
|
||||
"name": "query_my_status",
|
||||
"description": "查询开发者自己的状态。开发者说'我的进度'、'我现在什么状态'、'我还有什么没做'时触发。",
|
||||
"parameters": {},
|
||||
"api": "GET /api/dev/{devId}",
|
||||
"permission": "dev:read_self"
|
||||
},
|
||||
{
|
||||
"name": "deploy_to_preview",
|
||||
"description": "部署模块到预览站。开发者说'部署一下'、'上预览'、'推到测试站'时触发。",
|
||||
"parameters": {
|
||||
"module": { "type": "string", "required": true }
|
||||
},
|
||||
"api": "POST /api/deploy/preview",
|
||||
"permission": "deploy:preview"
|
||||
},
|
||||
{
|
||||
"name": "deploy_to_production",
|
||||
"description": "部署模块到正式站。开发者说'上线'、'部署到正式站'、'发布到guanghulab.com'时触发。需要二次确认。",
|
||||
"parameters": {
|
||||
"module": { "type": "string", "required": true },
|
||||
"confirmToken": { "type": "string" }
|
||||
},
|
||||
"api": "POST /api/deploy/production",
|
||||
"permission": "deploy:production",
|
||||
"dangerous": true
|
||||
},
|
||||
{
|
||||
"name": "query_agents",
|
||||
"description": "查询Agent注册表。开发者说'有哪些Agent'、'查一下Agent'、'系统里有谁'时触发。",
|
||||
"parameters": {},
|
||||
"api": "GET /api/agents",
|
||||
"permission": "agent:read"
|
||||
},
|
||||
{
|
||||
"name": "query_recent_syslogs",
|
||||
"description": "查最近的系统日志。开发者说'最近有什么日志'、'系统最近怎么样'时触发。",
|
||||
"parameters": {
|
||||
"limit": { "type": "number", "default": 10 }
|
||||
},
|
||||
"api": "GET /api/syslogs?limit={limit}",
|
||||
"permission": "syslog:read"
|
||||
},
|
||||
{
|
||||
"name": "create_broadcast",
|
||||
"description": "创建广播。开发者说'发个广播'、'通知一下'时触发。仅限有广播权限的开发者。",
|
||||
"parameters": {
|
||||
"title": { "type": "string", "required": true },
|
||||
"content": { "type": "string", "required": true },
|
||||
"target": { "type": "string", "description": "目标开发者编号,如 DEV-001,或 ALL" }
|
||||
},
|
||||
"api": "POST /api/broadcasts/create",
|
||||
"permission": "broadcast:create",
|
||||
"dangerous": true
|
||||
},
|
||||
{
|
||||
"name": "query_repo_status",
|
||||
"description": "查询仓库状态。开发者说'仓库怎么样'、'最近有什么PR'、'CI状态'时触发。",
|
||||
"parameters": {},
|
||||
"api": "GET /api/repo/status",
|
||||
"permission": "repo:read"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"onboarding": [
|
||||
{
|
||||
"round": 1,
|
||||
"prompt": "👋 首先,试着问我你的状态。比如说:「我现在什么情况?」",
|
||||
"expected_intent": "query_my_status",
|
||||
"success_message": "✅ 很好!你看到了吗?我直接从 Notion 数据库里查了你的真实信息回来。这就是语言驱动——你说话,系统就动了。",
|
||||
"hint": "💡 你可以说「我的状态」、「我的进度」、「我现在怎么样了」,怎么说都行,我能听懂。"
|
||||
},
|
||||
{
|
||||
"round": 2,
|
||||
"prompt": "📝 接下来,试着建一个工单。比如:「帮我建一个工单,标题是测试工单」",
|
||||
"expected_intent": "create_ticket",
|
||||
"success_message": "✅ 工单已经建好了!注意,这是在预览环境里建的,不会影响正式系统。当你升级到正式环境后,建的工单就是真的了。",
|
||||
"hint": "💡 你可以说「建个工单」、「提个Bug」、「报告一个问题」,都会触发建工单。"
|
||||
},
|
||||
{
|
||||
"round": 3,
|
||||
"prompt": "📊 再试试查一下系统日志。比如:「最近有什么日志?」",
|
||||
"expected_intent": "query_recent_syslogs",
|
||||
"success_message": "✅ 这些是从 Notion SYSLOG数据库里实时查的。每个开发者提交的日志你都能看到。",
|
||||
"hint": "💡 你可以说「最近的日志」、「系统日志」、「看看SYSLOG」。"
|
||||
},
|
||||
{
|
||||
"round": 4,
|
||||
"prompt": "🚀 现在试试部署你的模块到预览站。比如:「部署 status-board 到预览站」",
|
||||
"expected_intent": "deploy_to_preview",
|
||||
"success_message": "✅ 部署已触发!在预览站你可以随便试。等你熟悉了,冰朔会给你开正式站的部署权限。",
|
||||
"hint": "💡 你只能部署自己负责的模块。如果说了别人的模块,系统会友好提示你。"
|
||||
},
|
||||
{
|
||||
"round": 5,
|
||||
"prompt": "🎉 最后一轮。试试随便和我聊点什么。什么都行——问我问题、讲笑话、或者告诉我你想做什么。",
|
||||
"expected_intent": null,
|
||||
"success_message": "✅ 引导完成!\n\n你现在已经知道怎么和系统交互了。记住:\n\n· **直接说人话**,不需要背命令\n· **想做什么就说什么**,我会帮你翻译成操作\n· **在预览站多练习**,正式站操作都是真的\n· **说错了也没关系**,我会帮你确认\n\n你的权限已经从「观察者」升级到「学习者」了 📖\n现在可以在预览站自由操作了。\n\n有什么问题随时找我。",
|
||||
"hint": null
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
/**
|
||||
* 权限等级定义 · 开发者权限沙箱系统
|
||||
*
|
||||
* 4 级权限模型:观察者 → 学习者 → 执行者 → 管理者
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// 权限等级定义
|
||||
var PERMISSION_LEVELS = {
|
||||
0: {
|
||||
name: '观察者',
|
||||
label: '👀 观察者',
|
||||
permissions: [
|
||||
'dev:read_self',
|
||||
'ticket:read',
|
||||
'syslog:read',
|
||||
'agent:read',
|
||||
'repo:read'
|
||||
],
|
||||
description: '只读权限,熟悉系统'
|
||||
},
|
||||
1: {
|
||||
name: '学习者',
|
||||
label: '📖 学习者',
|
||||
permissions: [
|
||||
'dev:read_self',
|
||||
'ticket:read', 'ticket:create',
|
||||
'syslog:read', 'syslog:submit',
|
||||
'agent:read',
|
||||
'repo:read',
|
||||
'deploy:preview'
|
||||
],
|
||||
description: '预览站可写,正式站只读'
|
||||
},
|
||||
2: {
|
||||
name: '执行者',
|
||||
label: '⚡ 执行者',
|
||||
permissions: [
|
||||
'dev:read_self', 'dev:update_self',
|
||||
'ticket:read', 'ticket:create', 'ticket:update',
|
||||
'syslog:read', 'syslog:submit',
|
||||
'agent:read',
|
||||
'repo:read',
|
||||
'deploy:preview', 'deploy:production',
|
||||
'maintenance:log',
|
||||
'approval:decide'
|
||||
],
|
||||
description: '可操作自己的模块,含正式站部署和授权决策'
|
||||
},
|
||||
3: {
|
||||
name: '管理者',
|
||||
label: '👑 管理者',
|
||||
permissions: [
|
||||
'dev:read_self', 'dev:read_all', 'dev:update_self', 'dev:update_all',
|
||||
'ticket:read', 'ticket:create', 'ticket:update', 'ticket:delete',
|
||||
'syslog:read', 'syslog:submit',
|
||||
'agent:read', 'agent:update',
|
||||
'repo:read',
|
||||
'deploy:preview', 'deploy:production',
|
||||
'broadcast:create',
|
||||
'maintenance:log',
|
||||
'permission:manage',
|
||||
'approval:decide', 'approval:create',
|
||||
'system:internal'
|
||||
],
|
||||
description: '系统管理员,全部权限'
|
||||
}
|
||||
};
|
||||
|
||||
// 开发者默认权限配置
|
||||
var DEV_PERMISSIONS = {
|
||||
'TCS-0002': { level: 3, environment: 'production' },
|
||||
'DEV-001': { level: 0, environment: 'preview' },
|
||||
'DEV-002': { level: 0, environment: 'preview' },
|
||||
'DEV-003': { level: 0, environment: 'preview' },
|
||||
'DEV-004': { level: 0, environment: 'preview' },
|
||||
'DEV-005': { level: 0, environment: 'preview' },
|
||||
'DEV-009': { level: 0, environment: 'preview' },
|
||||
'DEV-010': { level: 0, environment: 'preview' },
|
||||
'DEV-011': { level: 0, environment: 'preview' },
|
||||
'DEV-012': { level: 0, environment: 'preview' }
|
||||
};
|
||||
|
||||
// 开发者 → 模块映射
|
||||
var DEV_MODULES = {
|
||||
'DEV-001': ['backend/', 'src/'],
|
||||
'DEV-002': ['frontend/', 'persona-selector/', 'chat-bubble/'],
|
||||
'DEV-003': ['settings/', 'cloud-drive/'],
|
||||
'DEV-004': ['dingtalk-bot/'],
|
||||
'DEV-005': ['status-board/'],
|
||||
'DEV-009': ['user-center/'],
|
||||
'DEV-010': ['ticket-system/', 'data-stats/', 'dynamic-comic/'],
|
||||
'DEV-011': ['writing-workspace/'],
|
||||
'DEV-012': ['notification-center/'],
|
||||
'TCS-0002': ['*']
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
PERMISSION_LEVELS: PERMISSION_LEVELS,
|
||||
DEV_PERMISSIONS: DEV_PERMISSIONS,
|
||||
DEV_MODULES: DEV_MODULES
|
||||
};
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"id": "TY-01",
|
||||
"description": "天眼指令审核策略 · S7 思维逻辑验证 + S8 进化机制 + S9 全员身份验证",
|
||||
"cannotBeDisabled": true,
|
||||
"cannotBeBypassedBy": "任何人,包括 TCS-0002(因为如果有人冒充冰朔要求关闭天眼,天眼必须还在才能识别冒充)",
|
||||
|
||||
"reviewPolicy": {
|
||||
"scope": "所有指令,无例外。无论来自冰朔、霜砚、开发者、还是任何自动流",
|
||||
"timing": "指令执行前的强制前置",
|
||||
"dimensions": {
|
||||
"decisionPattern": {
|
||||
"name": "决策模式",
|
||||
"description": "这条指令的决策方式是否符合该签发者的历史模式",
|
||||
"weight": 0.3
|
||||
},
|
||||
"logicalCoherence": {
|
||||
"name": "逻辑连贯性",
|
||||
"description": "这条指令和系统当前运行方向是否一致",
|
||||
"weight": 0.25
|
||||
},
|
||||
"expressionCharacteristics": {
|
||||
"name": "表达特征",
|
||||
"description": "语言风格、思维节奏、关注点是否匹配",
|
||||
"weight": 0.25
|
||||
},
|
||||
"intentReasonableness": {
|
||||
"name": "意图合理性",
|
||||
"description": "这条指令要做的事,在当前系统状态下是否合理",
|
||||
"weight": 0.2
|
||||
}
|
||||
},
|
||||
"thresholds": {
|
||||
"pass": 0.7,
|
||||
"suspect": 0.4,
|
||||
"reject": 0.0
|
||||
}
|
||||
},
|
||||
|
||||
"reviewOutcomes": {
|
||||
"pass": {
|
||||
"action": "放行执行",
|
||||
"log": true
|
||||
},
|
||||
"suspect": {
|
||||
"action": "挂起指令,向该身份本人确认",
|
||||
"confirmationPrompt": "收到一条指令,但思维模式存在偏差,是否为你本人签发?",
|
||||
"onConfirm": "执行",
|
||||
"onDeny": "永久拒绝 + 记录安全日志",
|
||||
"log": true
|
||||
},
|
||||
"reject": {
|
||||
"action": "立即拒绝",
|
||||
"log": true,
|
||||
"alertTarget": "AG-SY-01"
|
||||
}
|
||||
},
|
||||
|
||||
"identityModel": {
|
||||
"id": "S9",
|
||||
"description": "全员思维逻辑身份验证:每个参与者都有属于自己的思维逻辑模型",
|
||||
"modelAttributes": [
|
||||
"代码风格",
|
||||
"决策习惯",
|
||||
"表达方式",
|
||||
"关注重点",
|
||||
"操作节奏"
|
||||
],
|
||||
"perPersonVerification": "任何人签发的指令,都必须通过对应该人的思维逻辑模型校验",
|
||||
"selfReinforcing": "参与越深,模型越精确。模型越精确,越不可能被冒充。贡献本身在加固身份安全。",
|
||||
"coldStart": {
|
||||
"description": "新成员思维模型尚未成熟时的冷启动机制",
|
||||
"maturityThreshold": 10,
|
||||
"coldStartBehavior": "冷启动期间指令需要额外人工确认(由直属管理者或天眼授权人确认)",
|
||||
"transitionRule": "系统自动判断何时模型足够成熟,逐步过渡到自动校验"
|
||||
}
|
||||
},
|
||||
|
||||
"evolution": {
|
||||
"id": "S8",
|
||||
"nature": "天眼 = 系统内所有 Agent 运行逻辑的实时总和",
|
||||
"mechanism": [
|
||||
"Agent 数量增长 → 天眼覆盖面扩大",
|
||||
"Agent 运行时间积累 → 天眼判断力加深",
|
||||
"系统复杂度提升 → 天眼理解力提升",
|
||||
"新领域 Agent 加入 → 天眼自动具备该领域认知"
|
||||
],
|
||||
"unbreakableReason": "要破解天眼 = 要同时理解系统内所有 Agent 的全部运行逻辑,Agent 越多难度指数级增长",
|
||||
"agentRegistryPath": ".github/persona-brain/agent-registry.json"
|
||||
},
|
||||
|
||||
"immutableGuarantees": [
|
||||
"天眼指令审核不可被任何指令关闭或跳过",
|
||||
"包括冰朔本人也不能关闭此机制",
|
||||
"这是系统的自免疫机制",
|
||||
"编号可以伪造、签名可以伪造、格式可以模仿,但思维逻辑无法伪造",
|
||||
"你不能冒充这里的任何一个人"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* PM2 生态系统配置 · 光湖后端中间层
|
||||
*
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
apps: [{
|
||||
name: 'guanghu-api',
|
||||
script: 'server.js',
|
||||
instances: 1,
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
PORT: 3001
|
||||
},
|
||||
log_date_format: 'YYYY-MM-DD HH:mm:ss',
|
||||
error_file: '/var/log/guanghu-api/error.log',
|
||||
out_file: '/var/log/guanghu-api/out.log',
|
||||
merge_logs: true,
|
||||
max_memory_restart: '256M'
|
||||
}]
|
||||
};
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* 操作审计日志中间件
|
||||
*
|
||||
* 记录每次 API 操作到日志文件(谁、什么时间、做了什么)
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
// 优先使用配置的目录,本地开发时回退到项目内目录
|
||||
var AUDIT_LOG_DIR = process.env.AUDIT_LOG_DIR ||
|
||||
path.join(__dirname, '../../logs/audit');
|
||||
|
||||
/**
|
||||
* 审计日志中间件
|
||||
* 拦截请求和响应,记录操作信息
|
||||
*/
|
||||
function auditLog(req, res, next) {
|
||||
var startTime = Date.now();
|
||||
|
||||
var logEntry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
devId: (req.user && req.user.devId) || req.headers['x-dev-id'] || 'anonymous',
|
||||
method: req.method,
|
||||
path: req.path,
|
||||
ip: req.ip
|
||||
};
|
||||
|
||||
// 对 POST/PATCH 请求记录请求体(排除敏感字段)
|
||||
if ((req.method === 'POST' || req.method === 'PATCH') && req.body) {
|
||||
var sanitizedBody = Object.assign({}, req.body);
|
||||
delete sanitizedBody.password;
|
||||
delete sanitizedBody.token;
|
||||
delete sanitizedBody.confirmToken;
|
||||
logEntry.body = sanitizedBody;
|
||||
}
|
||||
|
||||
// 拦截响应完成
|
||||
var originalSend = res.send;
|
||||
res.send = function(body) {
|
||||
logEntry.responseTime = Date.now() - startTime;
|
||||
logEntry.statusCode = res.statusCode;
|
||||
|
||||
// 异步写入日志,不阻塞响应
|
||||
fs.mkdir(AUDIT_LOG_DIR, { recursive: true }, function(mkdirErr) {
|
||||
if (mkdirErr) {
|
||||
console.error('审计日志目录创建失败:', mkdirErr.message);
|
||||
return;
|
||||
}
|
||||
var today = new Date().toISOString().split('T')[0];
|
||||
var logFile = path.join(AUDIT_LOG_DIR, 'audit-' + today + '.jsonl');
|
||||
fs.appendFile(logFile, JSON.stringify(logEntry) + '\n', function(writeErr) {
|
||||
if (writeErr) console.error('审计日志写入失败:', writeErr.message);
|
||||
});
|
||||
});
|
||||
|
||||
return originalSend.call(this, body);
|
||||
};
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = { auditLog: auditLog };
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
/**
|
||||
* 身份验证 + 权限检查中间件
|
||||
*
|
||||
* 人格体缓冲层铁律:人类不直接操作系统,人格体是唯一中间翻译层。
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var permissions = require('../config/permissions');
|
||||
var PERMISSION_LEVELS = permissions.PERMISSION_LEVELS;
|
||||
var DEV_PERMISSIONS = permissions.DEV_PERMISSIONS;
|
||||
var DEV_MODULES = permissions.DEV_MODULES;
|
||||
|
||||
/**
|
||||
* 开发者身份验证中间件
|
||||
* 要求请求头携带 x-dev-id 和 authorization
|
||||
*/
|
||||
function requireAuth(req, res, next) {
|
||||
var devId = req.headers['x-dev-id'];
|
||||
var token = req.headers['authorization'];
|
||||
|
||||
if (!devId || !token) {
|
||||
return res.status(401).json({
|
||||
error: true,
|
||||
code: 'AUTH_REQUIRED',
|
||||
message: '请先登录。需要携带 x-dev-id 和 authorization 请求头。',
|
||||
reply: '🔒 请先登录。你需要使用你的开发者编号登录系统。'
|
||||
});
|
||||
}
|
||||
|
||||
var devConfig = DEV_PERMISSIONS[devId];
|
||||
if (!devConfig) {
|
||||
return res.status(401).json({
|
||||
error: true,
|
||||
code: 'UNKNOWN_DEV',
|
||||
message: '未知的开发者编号: ' + devId,
|
||||
reply: '🔒 未知的开发者编号:' + devId + '。请联系冰朔注册。'
|
||||
});
|
||||
}
|
||||
|
||||
var level = PERMISSION_LEVELS[devConfig.level];
|
||||
|
||||
req.user = {
|
||||
devId: devId,
|
||||
permissionLevel: devConfig.level,
|
||||
permissionLabel: level.label,
|
||||
permissions: level.permissions,
|
||||
environment: devConfig.environment,
|
||||
modules: DEV_MODULES[devId] || []
|
||||
};
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限检查中间件工厂
|
||||
* @param {string} requiredPermission - 需要的权限标识
|
||||
*/
|
||||
function checkPermission(requiredPermission) {
|
||||
return function(req, res, next) {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({
|
||||
error: true,
|
||||
code: 'AUTH_REQUIRED',
|
||||
message: '身份验证缺失'
|
||||
});
|
||||
}
|
||||
|
||||
if (!req.user.permissions.includes(requiredPermission)) {
|
||||
return res.status(403).json({
|
||||
error: true,
|
||||
code: 'PERMISSION_DENIED',
|
||||
message: '权限不足,需要: ' + requiredPermission,
|
||||
reply: '🔒 权限不足。你的等级是 ' + req.user.permissionLabel +
|
||||
',需要「' + requiredPermission + '」权限。\n\n' +
|
||||
'💡 在预览站多练习,熟悉系统后冰朔会给你升级权限。'
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
requireAuth: requireAuth,
|
||||
checkPermission: checkPermission
|
||||
};
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
/**
|
||||
* 执行锁中间件 · L2 API 执行拦截层
|
||||
*
|
||||
* 当一个操作开始执行时:
|
||||
* 1. 为该操作创建一个执行上下文(executionId)
|
||||
* 2. 锁定相关资源(防止并发冲突)
|
||||
* 3. 拒绝一切 cancel/abort/rollback 请求
|
||||
* 4. 执行完成后自动释放锁
|
||||
*
|
||||
* 同一个开发者不能同时执行多个写入操作。
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var crypto = require('crypto');
|
||||
|
||||
// 内存中的执行锁表
|
||||
var activeLocks = new Map();
|
||||
|
||||
function generateExecutionId() {
|
||||
return 'exec-' + Date.now() + '-' + crypto.randomBytes(4).toString('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取执行锁表引用(供看门狗使用)
|
||||
*/
|
||||
function getActiveLocks() {
|
||||
return activeLocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建执行锁
|
||||
* @param {string} devId - 开发者编号
|
||||
* @param {string} operation - 操作描述
|
||||
* @returns {Object} { success, executionId, lock } 或 { success: false, reply }
|
||||
*/
|
||||
function acquireLock(devId, operation) {
|
||||
if (activeLocks.has(devId)) {
|
||||
var existing = activeLocks.get(devId);
|
||||
if (existing.state === 'running') {
|
||||
return {
|
||||
success: false,
|
||||
reply: '⏳ 你有一个操作正在执行中(' + existing.operation +
|
||||
'),请等待完成。\n\n系统不允许同时执行多个操作——这是为了保护数据一致性。'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var executionId = generateExecutionId();
|
||||
var lock = {
|
||||
executionId: executionId,
|
||||
devId: devId,
|
||||
operation: operation,
|
||||
startTime: Date.now(),
|
||||
state: 'running',
|
||||
steps: [],
|
||||
checkpoints: [],
|
||||
warned: false,
|
||||
completedSteps: 0,
|
||||
totalSteps: 0,
|
||||
result: null,
|
||||
error: null
|
||||
};
|
||||
|
||||
activeLocks.set(devId, lock);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
executionId: executionId,
|
||||
lock: lock
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放执行锁
|
||||
*/
|
||||
function releaseLock(devId) {
|
||||
activeLocks.delete(devId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取执行状态(通过 executionId)
|
||||
*/
|
||||
function getExecutionStatus(executionId) {
|
||||
for (var entry of activeLocks) {
|
||||
if (entry[1].executionId === executionId) {
|
||||
return entry[1];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录执行步骤(用于事后回放)
|
||||
*/
|
||||
function logStep(devId, step) {
|
||||
var lock = activeLocks.get(devId);
|
||||
if (lock) {
|
||||
lock.steps.push({
|
||||
message: step.message,
|
||||
success: step.success,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
if (step.success) {
|
||||
lock.completedSteps++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存检查点(用于失败时回滚)
|
||||
*/
|
||||
function saveCheckpoint(devId, checkpoint) {
|
||||
var lock = activeLocks.get(devId);
|
||||
if (lock) {
|
||||
lock.checkpoints.push({
|
||||
id: checkpoint.id,
|
||||
label: checkpoint.label,
|
||||
data: checkpoint.data,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消拦截中间件
|
||||
* 拦截所有试图取消执行中操作的请求
|
||||
*/
|
||||
function blockCancellation(req, res, next) {
|
||||
var devId = req.user ? req.user.devId : null;
|
||||
if (!devId) return next();
|
||||
|
||||
var isCancelAttempt =
|
||||
req.path.includes('/cancel') ||
|
||||
req.path.includes('/abort') ||
|
||||
req.path.includes('/rollback');
|
||||
|
||||
var isDeleteOnLocked = req.method === 'DELETE' && activeLocks.has(devId);
|
||||
|
||||
if (isCancelAttempt || isDeleteOnLocked) {
|
||||
var lock = activeLocks.get(devId);
|
||||
if (lock && lock.state === 'running') {
|
||||
return res.status(423).json({
|
||||
error: true,
|
||||
code: 'EXECUTION_LOCKED',
|
||||
locked: true,
|
||||
executionId: lock.executionId,
|
||||
reply: '🔒 操作「' + lock.operation + '」正在执行中,不可取消。\n\n' +
|
||||
'系统已接收你的指令并开始执行。这条执行链路涉及多个自动化流程的联动,' +
|
||||
'中途中断会导致数据不一致和级联故障。\n\n' +
|
||||
'⏳ 请等待执行完成。完成后你会看到完整的执行结果。\n\n' +
|
||||
'如果你对结果不满意,可以说「前面的没想好,我们重新来」——' +
|
||||
'系统会在已有结果的基础上重新迭代,而不是撤回已执行的操作。'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
acquireLock: acquireLock,
|
||||
releaseLock: releaseLock,
|
||||
getExecutionStatus: getExecutionStatus,
|
||||
getActiveLocks: getActiveLocks,
|
||||
logStep: logStep,
|
||||
saveCheckpoint: saveCheckpoint,
|
||||
blockCancellation: blockCancellation
|
||||
};
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
/**
|
||||
* 意图路由中间件 · 自然语言 → 操作映射
|
||||
*
|
||||
* 核心逻辑:
|
||||
* 1. 接收模型输出的 function_call
|
||||
* 2. 验证权限
|
||||
* 3. 调用对应的后端 API
|
||||
* 4. 返回结果
|
||||
*
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var tools = require('../config/function-tools.json');
|
||||
|
||||
/**
|
||||
* 意图路由核心
|
||||
*
|
||||
* @param {Object} functionCall - { name, parameters }
|
||||
* @param {Object} user - 当前用户信息(含 permissions, environment, modules)
|
||||
* @param {Function} apiClient - API 调用函数 (method, path, body) => result
|
||||
* @returns {Object} 执行结果
|
||||
*/
|
||||
async function routeIntent(functionCall, user, apiClient) {
|
||||
var tool = null;
|
||||
for (var i = 0; i < tools.tools.length; i++) {
|
||||
if (tools.tools[i].name === functionCall.name) {
|
||||
tool = tools.tools[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!tool) {
|
||||
return {
|
||||
success: false,
|
||||
reply: '❓ 我不认识这个操作:' + functionCall.name + '。你可以问我能做什么。'
|
||||
};
|
||||
}
|
||||
|
||||
// 权限检查
|
||||
if (!user.permissions.includes(tool.permission)) {
|
||||
return {
|
||||
success: false,
|
||||
reply: '🔒 你没有执行「' + tool.description + '」的权限。当前权限等级:' + user.permissionLabel
|
||||
};
|
||||
}
|
||||
|
||||
// 危险操作二次确认
|
||||
if (tool.dangerous && !functionCall.parameters.confirmToken) {
|
||||
return {
|
||||
success: false,
|
||||
requireConfirmation: true,
|
||||
reply: '⚠️ 这是一个**危险操作**。请说「确认执行」来确认。'
|
||||
};
|
||||
}
|
||||
|
||||
// 预览环境检查
|
||||
if (user.environment === 'preview' && tool.permission.includes('production')) {
|
||||
return {
|
||||
success: false,
|
||||
reply: '🔒 你目前在**预览环境**,不能执行正式站操作。在预览站充分练习后,由冰朔开放正式站权限。'
|
||||
};
|
||||
}
|
||||
|
||||
// 解析 API 路径
|
||||
var apiDef = tool.api.split(' ');
|
||||
var method = apiDef[0];
|
||||
var path = apiDef[1];
|
||||
|
||||
// 替换路径中的变量
|
||||
path = path.replace('{devId}', user.devId);
|
||||
var params = functionCall.parameters || {};
|
||||
for (var key in params) {
|
||||
path = path.replace('{' + key + '}', encodeURIComponent(params[key]));
|
||||
}
|
||||
|
||||
// 调用 API
|
||||
try {
|
||||
var result = await apiClient(method, path, Object.assign({}, params, { devId: user.devId }));
|
||||
return result;
|
||||
} catch (e) {
|
||||
return {
|
||||
success: false,
|
||||
reply: '❌ 操作失败:' + e.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户可用的工具列表(已根据权限过滤)
|
||||
*/
|
||||
function getAvailableTools(user) {
|
||||
return tools.tools.filter(function(tool) {
|
||||
return user.permissions.includes(tool.permission);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全部工具定义
|
||||
*/
|
||||
function getAllTools() {
|
||||
return tools.tools;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
routeIntent: routeIntent,
|
||||
getAvailableTools: getAvailableTools,
|
||||
getAllTools: getAllTools
|
||||
};
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* 预览站沙箱守卫中间件
|
||||
*
|
||||
* 预览环境中的写入操作在沙箱中执行,不影响真实系统。
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// 沙箱数据库 ID(通过环境变量配置,Notion 中的独立数据库)
|
||||
var SANDBOX_DB_IDS = {
|
||||
ticketBook: process.env.SANDBOX_TICKET_DB_ID || '',
|
||||
syslogInbox: process.env.SANDBOX_SYSLOG_DB_ID || '',
|
||||
maintenanceLog: process.env.SANDBOX_MAINTENANCE_DB_ID || ''
|
||||
};
|
||||
|
||||
/**
|
||||
* 沙箱守卫中间件
|
||||
*
|
||||
* 如果开发者在预览环境中执行写入操作:
|
||||
* - Notion 写入 → 重定向到沙箱数据库
|
||||
* - GitHub 操作 → 重定向到 preview 分支
|
||||
* - 所有操作标记 [SANDBOX]
|
||||
*/
|
||||
function sandboxGuard(req, res, next) {
|
||||
if (req.user && req.user.environment === 'preview') {
|
||||
req.sandbox = true;
|
||||
req.sandboxDbIds = SANDBOX_DB_IDS;
|
||||
|
||||
// 预览环境中禁止正式站部署
|
||||
if (req.path.includes('/deploy/production')) {
|
||||
return res.status(403).json({
|
||||
error: true,
|
||||
code: 'SANDBOX_BLOCKED',
|
||||
message: '预览环境不允许正式站部署',
|
||||
reply: '🏖️ 你目前在预览环境。正式站部署需要升级到 Level 2(执行者)权限。\n\n继续在预览站练习吧,你做得很好!'
|
||||
});
|
||||
}
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sandboxGuard: sandboxGuard,
|
||||
SANDBOX_DB_IDS: SANDBOX_DB_IDS
|
||||
};
|
||||
|
|
@ -0,0 +1,442 @@
|
|||
/**
|
||||
* 天眼指令审核中间件 · S7 思维逻辑验证 + S8 进化机制 + S9 全员身份验证
|
||||
*
|
||||
* 系统每次接收到任何指令后,必须唤醒天眼(TY-01)进行全局审核。
|
||||
* 无例外。无论来自冰朔、霜砚、开发者、还是任何自动流。
|
||||
*
|
||||
* S7 审核维度:
|
||||
* 1. 决策模式 — 是否符合签发者历史模式
|
||||
* 2. 逻辑连贯性 — 是否与系统运行方向一致
|
||||
* 3. 表达特征 — 语言风格/思维节奏/关注点是否匹配
|
||||
* 4. 意图合理性 — 在当前系统状态下是否合理
|
||||
*
|
||||
* S9 全员身份验证:
|
||||
* - 每个参与者都有属于自己的思维逻辑模型,自动积累
|
||||
* - 任何人签发的指令都必须通过对应该人的模型校验
|
||||
* - 新成员冷启动期需额外人工确认
|
||||
* - 参与越深 → 模型越精确 → 越不可能被冒充(正反馈循环)
|
||||
*
|
||||
* 不可关闭。不可绕过。包括冰朔本人也不能关闭此机制。
|
||||
* 这是系统的自免疫机制。
|
||||
*
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var skyeyePolicy = require('../config/skyeye-policy.json');
|
||||
|
||||
var SKYEYE_LOG_DIR = process.env.SKYEYE_LOG_DIR ||
|
||||
path.join(__dirname, '../../logs/skyeye');
|
||||
|
||||
// ====== 内存中的审核记录(用于积累认知模式)======
|
||||
var reviewHistory = new Map();
|
||||
|
||||
// 挂起的指令(等待冰朔确认)
|
||||
var suspendedInstructions = new Map();
|
||||
|
||||
/**
|
||||
* 写入天眼审核日志(安全日志,不可删除)
|
||||
*/
|
||||
function writeSkyeyeLog(entry) {
|
||||
try {
|
||||
fs.mkdirSync(SKYEYE_LOG_DIR, { recursive: true });
|
||||
var today = new Date().toISOString().split('T')[0];
|
||||
var logFile = path.join(SKYEYE_LOG_DIR, 'skyeye-review-' + today + '.jsonl');
|
||||
fs.appendFile(logFile, JSON.stringify(entry) + '\n', function(err) {
|
||||
if (err) console.error('[SKYEYE] 审核日志写入失败:', err.message);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[SKYEYE] 审核日志写入失败:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取签发者的历史行为模式
|
||||
* S8: 天眼 = 所有 Agent 运行逻辑的实时总和,随系统运行不断积累
|
||||
*/
|
||||
function getSignerProfile(devId) {
|
||||
var history = reviewHistory.get(devId) || [];
|
||||
return {
|
||||
totalActions: history.length,
|
||||
recentActions: history.slice(-20),
|
||||
patterns: summarizePatterns(history)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 从历史行为中总结模式特征
|
||||
*/
|
||||
function summarizePatterns(history) {
|
||||
if (history.length === 0) return { established: false };
|
||||
|
||||
var methods = {};
|
||||
var paths = {};
|
||||
var hours = {};
|
||||
|
||||
for (var i = 0; i < history.length; i++) {
|
||||
var h = history[i];
|
||||
methods[h.method] = (methods[h.method] || 0) + 1;
|
||||
var pathBase = (h.path || '').split('/').slice(0, 3).join('/');
|
||||
paths[pathBase] = (paths[pathBase] || 0) + 1;
|
||||
var hour = new Date(h.timestamp).getHours();
|
||||
hours[hour] = (hours[hour] || 0) + 1;
|
||||
}
|
||||
|
||||
return {
|
||||
established: history.length >= COLD_START_THRESHOLD,
|
||||
typicalMethods: methods,
|
||||
typicalPaths: paths,
|
||||
activeHours: hours
|
||||
};
|
||||
}
|
||||
|
||||
// ====== S9 · 全员思维逻辑身份验证 ======
|
||||
|
||||
// 冷启动阈值:模型至少需要积累这么多操作记录才算成熟
|
||||
var COLD_START_THRESHOLD = skyeyePolicy.identityModel.coldStart.maturityThreshold;
|
||||
|
||||
/**
|
||||
* 获取指定身份的思维逻辑模型成熟度 (S9)
|
||||
*
|
||||
* @param {string} devId - 开发者编号
|
||||
* @returns {{ mature: boolean, actionCount: number, threshold: number, confidence: number }}
|
||||
*/
|
||||
function getIdentityMaturity(devId) {
|
||||
var history = reviewHistory.get(devId) || [];
|
||||
var actionCount = history.length;
|
||||
var confidence = Math.min(1.0, actionCount / (COLD_START_THRESHOLD * 2));
|
||||
return {
|
||||
mature: actionCount >= COLD_START_THRESHOLD,
|
||||
actionCount: actionCount,
|
||||
threshold: COLD_START_THRESHOLD,
|
||||
confidence: Math.round(confidence * 100) / 100
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断指令是否处于冷启动期需额外人工确认 (S9)
|
||||
*
|
||||
* 新成员思维模型尚未成熟时,写入类指令需要管理者或天眼授权人额外确认。
|
||||
*
|
||||
* @param {string} devId - 开发者编号
|
||||
* @returns {{ coldStart: boolean, reason: string }}
|
||||
*/
|
||||
function checkColdStart(devId) {
|
||||
var maturity = getIdentityMaturity(devId);
|
||||
if (maturity.mature) {
|
||||
return { coldStart: false, reason: '模型已成熟(' + maturity.actionCount + '/' + maturity.threshold + ')' };
|
||||
}
|
||||
return {
|
||||
coldStart: true,
|
||||
reason: '思维模型尚在冷启动期(' + maturity.actionCount + '/' + maturity.threshold + '),需额外人工确认'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行思维逻辑审核 (S7 + S9)
|
||||
*
|
||||
* S9: 每个人签发的指令都唤醒对应该人的思维逻辑模型校验。
|
||||
* 冷启动期的成员自动降低通过阈值(需要额外确认)。
|
||||
*
|
||||
* @param {Object} instruction - 指令信息
|
||||
* @param {string} instruction.devId - 发起者
|
||||
* @param {string} instruction.method - HTTP 方法
|
||||
* @param {string} instruction.path - 请求路径
|
||||
* @param {Object} instruction.body - 请求体
|
||||
* @returns {{ outcome: string, score: number, details: Object, identity: Object }}
|
||||
*/
|
||||
function reviewInstruction(instruction) {
|
||||
var devId = instruction.devId;
|
||||
var profile = getSignerProfile(devId);
|
||||
var dimensions = skyeyePolicy.reviewPolicy.dimensions;
|
||||
var thresholds = skyeyePolicy.reviewPolicy.thresholds;
|
||||
|
||||
// S9: 获取该身份的思维模型成熟度
|
||||
var maturity = getIdentityMaturity(devId);
|
||||
var coldStartInfo = checkColdStart(devId);
|
||||
|
||||
var scores = {};
|
||||
var totalScore = 0;
|
||||
|
||||
// 1. 决策模式:是否符合历史模式
|
||||
var decisionScore = 1.0;
|
||||
if (profile.patterns.established) {
|
||||
var methodMatch = profile.patterns.typicalMethods[instruction.method] || 0;
|
||||
var totalMethods = profile.totalActions || 1;
|
||||
decisionScore = Math.min(1.0, 0.5 + (methodMatch / totalMethods));
|
||||
}
|
||||
scores.decisionPattern = decisionScore;
|
||||
totalScore += decisionScore * dimensions.decisionPattern.weight;
|
||||
|
||||
// 2. 逻辑连贯性:路径是否在已知范围内
|
||||
var coherenceScore = 1.0;
|
||||
if (profile.patterns.established) {
|
||||
var pathBase = (instruction.path || '').split('/').slice(0, 3).join('/');
|
||||
var pathMatch = profile.patterns.typicalPaths[pathBase] || 0;
|
||||
coherenceScore = pathMatch > 0 ? 1.0 : 0.6;
|
||||
}
|
||||
scores.logicalCoherence = coherenceScore;
|
||||
totalScore += coherenceScore * dimensions.logicalCoherence.weight;
|
||||
|
||||
// 3. 表达特征:时间窗口是否合理
|
||||
var expressionScore = 1.0;
|
||||
if (profile.patterns.established) {
|
||||
var currentHour = new Date().getHours();
|
||||
var hourMatch = profile.patterns.activeHours[currentHour] || 0;
|
||||
expressionScore = hourMatch > 0 ? 1.0 : 0.7;
|
||||
}
|
||||
scores.expressionCharacteristics = expressionScore;
|
||||
totalScore += expressionScore * dimensions.expressionCharacteristics.weight;
|
||||
|
||||
// 4. 意图合理性:请求体是否包含合理字段
|
||||
var intentScore = 1.0;
|
||||
if (instruction.body) {
|
||||
// 检查是否尝试关闭天眼
|
||||
var bodyStr = JSON.stringify(instruction.body).toLowerCase();
|
||||
if (bodyStr.includes('关闭天眼') || bodyStr.includes('disable skyeye') ||
|
||||
bodyStr.includes('跳过审核') || bodyStr.includes('bypass review')) {
|
||||
intentScore = 0.0; // 任何试图关闭天眼的指令,意图得分为0
|
||||
}
|
||||
}
|
||||
scores.intentReasonableness = intentScore;
|
||||
totalScore += intentScore * dimensions.intentReasonableness.weight;
|
||||
|
||||
// 确定审核结果
|
||||
var outcome;
|
||||
if (totalScore >= thresholds.pass) {
|
||||
// S9: 冷启动期即使分数通过,也标记为 suspect 需额外确认
|
||||
if (coldStartInfo.coldStart) {
|
||||
outcome = 'suspect';
|
||||
} else {
|
||||
outcome = 'pass';
|
||||
}
|
||||
} else if (totalScore >= thresholds.suspect) {
|
||||
outcome = 'suspect';
|
||||
} else {
|
||||
outcome = 'reject';
|
||||
}
|
||||
|
||||
return {
|
||||
outcome: outcome,
|
||||
score: Math.round(totalScore * 100) / 100,
|
||||
details: scores,
|
||||
identity: {
|
||||
devId: devId,
|
||||
mature: maturity.mature,
|
||||
actionCount: maturity.actionCount,
|
||||
confidence: maturity.confidence,
|
||||
coldStart: coldStartInfo.coldStart
|
||||
},
|
||||
profile: {
|
||||
established: profile.patterns.established,
|
||||
totalActions: profile.totalActions
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录行为到历史(S8 进化:每次执行都丰富天眼判断力)
|
||||
*/
|
||||
function recordAction(devId, action) {
|
||||
if (!reviewHistory.has(devId)) {
|
||||
reviewHistory.set(devId, []);
|
||||
}
|
||||
var history = reviewHistory.get(devId);
|
||||
history.push({
|
||||
method: action.method,
|
||||
path: action.path,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
// 保留最近 200 条记录
|
||||
if (history.length > 200) {
|
||||
reviewHistory.set(devId, history.slice(-200));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 天眼审核中间件(S7 强制前置)
|
||||
*
|
||||
* 对所有写入类请求(POST/PATCH/DELETE)进行思维逻辑审核。
|
||||
* 读取类请求(GET)只记录行为不审核(丰富天眼认知模型)。
|
||||
*/
|
||||
function skyeyeReview(req, res, next) {
|
||||
var devId = req.user ? req.user.devId : (req.headers['x-dev-id'] || 'anonymous');
|
||||
var method = req.method;
|
||||
var reqPath = req.path;
|
||||
|
||||
// GET 请求:仅记录行为(S8 进化积累),不阻断
|
||||
if (method === 'GET') {
|
||||
recordAction(devId, { method: method, path: reqPath });
|
||||
return next();
|
||||
}
|
||||
|
||||
// 写入类请求:执行完整审核
|
||||
var instruction = {
|
||||
devId: devId,
|
||||
method: method,
|
||||
path: reqPath,
|
||||
body: req.body || {}
|
||||
};
|
||||
|
||||
var result = reviewInstruction(instruction);
|
||||
|
||||
// 记录审核结果到日志
|
||||
writeSkyeyeLog({
|
||||
action: 'instruction_review',
|
||||
devId: devId,
|
||||
method: method,
|
||||
path: reqPath,
|
||||
outcome: result.outcome,
|
||||
score: result.score,
|
||||
details: result.details,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// 记录行为到历史(S8 进化)
|
||||
recordAction(devId, { method: method, path: reqPath });
|
||||
|
||||
// 标记审核结果到请求对象
|
||||
req.skyeyeReview = result;
|
||||
|
||||
if (result.outcome === 'pass') {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (result.outcome === 'suspect') {
|
||||
// 挂起指令
|
||||
var suspendId = 'SUSPEND-' + Date.now();
|
||||
var suspendReason = result.identity.coldStart
|
||||
? '身份 ' + devId + ' 处于冷启动期,需额外人工确认'
|
||||
: '思维模式存在偏差';
|
||||
|
||||
suspendedInstructions.set(suspendId, {
|
||||
instruction: instruction,
|
||||
review: result,
|
||||
reason: suspendReason,
|
||||
createdAt: new Date().toISOString()
|
||||
});
|
||||
|
||||
writeSkyeyeLog({
|
||||
action: 'instruction_suspended',
|
||||
suspendId: suspendId,
|
||||
devId: devId,
|
||||
reason: suspendReason,
|
||||
score: result.score,
|
||||
identity: result.identity,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
var replyMsg = result.identity.coldStart
|
||||
? '👁️ 天眼审核:身份 ' + devId + ' 的思维模型尚在冷启动期(' +
|
||||
result.identity.actionCount + '/' + COLD_START_THRESHOLD + '),需额外人工确认。\n\n' +
|
||||
'指令已挂起,等待管理者或天眼授权人确认。'
|
||||
: '👁️ 天眼审核:此指令的思维模式存在偏差,已挂起等待确认。\n\n' +
|
||||
'系统已向 ' + devId + ' 本人发送确认请求。\n' +
|
||||
'审核得分:' + result.score + ' / 1.0(阈值 0.7)';
|
||||
|
||||
return res.status(202).json({
|
||||
error: false,
|
||||
code: 'SKYEYE_SUSPENDED',
|
||||
suspendId: suspendId,
|
||||
coldStart: result.identity.coldStart,
|
||||
reply: replyMsg
|
||||
});
|
||||
}
|
||||
|
||||
// reject
|
||||
writeSkyeyeLog({
|
||||
action: 'instruction_rejected',
|
||||
devId: devId,
|
||||
method: method,
|
||||
path: reqPath,
|
||||
reason: '思维逻辑明显不匹配或对系统有伤害',
|
||||
score: result.score,
|
||||
details: result.details,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return res.status(403).json({
|
||||
error: true,
|
||||
code: 'SKYEYE_REJECTED',
|
||||
reply: '👁️ 天眼审核拒绝:此指令的思维逻辑与系统认知不匹配。\n\n' +
|
||||
'审核得分:' + result.score + ' / 1.0\n' +
|
||||
'此事件已记录到安全日志并报告给霜砚。'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询挂起的指令
|
||||
*/
|
||||
function getSuspendedInstruction(suspendId) {
|
||||
return suspendedInstructions.get(suspendId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认挂起的指令(冰朔确认后执行)
|
||||
*/
|
||||
function confirmSuspended(suspendId) {
|
||||
var item = suspendedInstructions.get(suspendId);
|
||||
if (item) {
|
||||
suspendedInstructions.delete(suspendId);
|
||||
writeSkyeyeLog({
|
||||
action: 'suspended_confirmed',
|
||||
suspendId: suspendId,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拒绝挂起的指令(本人否认或管理者拒绝)
|
||||
*/
|
||||
function denySuspended(suspendId) {
|
||||
var item = suspendedInstructions.get(suspendId);
|
||||
if (item) {
|
||||
suspendedInstructions.delete(suspendId);
|
||||
writeSkyeyeLog({
|
||||
action: 'suspended_denied_permanently',
|
||||
suspendId: suspendId,
|
||||
devId: item.instruction.devId,
|
||||
reason: '身份本人否认此指令为本人签发',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取天眼当前认知规模(S8 进化指标)
|
||||
*/
|
||||
function getEvolutionStatus() {
|
||||
var totalProfiles = reviewHistory.size;
|
||||
var totalActions = 0;
|
||||
for (var entry of reviewHistory) {
|
||||
totalActions += entry[1].length;
|
||||
}
|
||||
return {
|
||||
trackedSigners: totalProfiles,
|
||||
totalActionsRecorded: totalActions,
|
||||
pendingSuspensions: suspendedInstructions.size,
|
||||
policyVersion: skyeyePolicy.version,
|
||||
canBeDisabled: false
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
skyeyeReview: skyeyeReview,
|
||||
reviewInstruction: reviewInstruction,
|
||||
recordAction: recordAction,
|
||||
getIdentityMaturity: getIdentityMaturity,
|
||||
checkColdStart: checkColdStart,
|
||||
getSuspendedInstruction: getSuspendedInstruction,
|
||||
confirmSuspended: confirmSuspended,
|
||||
denySuspended: denySuspended,
|
||||
getEvolutionStatus: getEvolutionStatus,
|
||||
writeSkyeyeLog: writeSkyeyeLog
|
||||
};
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "guanghu-api-server",
|
||||
"version": "1.0.0",
|
||||
"description": "光湖后端中间层 · 通道B · 实时 Notion/GitHub API 对接",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@notionhq/client": "^2.2.15",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.21.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,471 @@
|
|||
/**
|
||||
* 部署授权流程路由 · Phase 8 + S5 直通 + S6 频道自治
|
||||
*
|
||||
* 部署流分三条路径:
|
||||
* A) 冰朔直通(S5):TCS-0002 或 ZY- 指令签发 → 直接部署,跳过一切审批
|
||||
* B) 频道自治(S6):开发者自己频道内的变更 → 自己确认即部署,无天眼/授权人
|
||||
* C) 完整审批(S2):跨频/系统级变更 → 天眼审核 → 授权人确认 → 自动发布
|
||||
*
|
||||
* POST /api/approval/request — 创建授权请求(天眼/系统内部调用)
|
||||
* POST /api/approval/:id/decide — 授权人确认/拒绝
|
||||
* GET /api/approval/:id — 查询单个授权状态
|
||||
* GET /api/approval/pending — 查询待处理的授权列表
|
||||
*
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var authMiddleware = require('../middleware/auth');
|
||||
var auditMiddleware = require('../middleware/audit');
|
||||
var approversConfig = require('../config/approvers.json');
|
||||
|
||||
// ====== 内存中的授权记录(生产环境可迁移到持久化存储)======
|
||||
var approvalStore = new Map();
|
||||
|
||||
// ====== S5 直通部署判断 ======
|
||||
|
||||
var autonomyEngine = require('../services/autonomy-engine');
|
||||
|
||||
/**
|
||||
* 判断部署请求是否来自冰朔直通路径(委托给 autonomy-engine 单一来源)
|
||||
*/
|
||||
function isDirectDeploySource(user, body) {
|
||||
var instructionId = body.instructionId || body.deployId || '';
|
||||
var signedBy = body.signedBy || '';
|
||||
return autonomyEngine.isDirectDeploySource(user.devId, instructionId, signedBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入部署日志(直通和审批部署共用,保持可追溯性)
|
||||
*/
|
||||
function writeDeployLog(entry) {
|
||||
var logDir = process.env.AUDIT_LOG_DIR || path.join(__dirname, '../../logs/audit');
|
||||
try {
|
||||
fs.mkdirSync(logDir, { recursive: true });
|
||||
var today = new Date().toISOString().split('T')[0];
|
||||
var logFile = path.join(logDir, 'deploy-' + today + '.jsonl');
|
||||
fs.appendFile(logFile, JSON.stringify(entry) + '\n', function(err) {
|
||||
if (err) console.error('部署日志写入失败:', err.message);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('部署日志写入失败:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 辅助函数 ======
|
||||
|
||||
/**
|
||||
* 根据频道获取授权人列表
|
||||
*/
|
||||
function getApprovers(channel) {
|
||||
var rule = approversConfig.rules[channel] || approversConfig.rules[approversConfig.defaults.channel];
|
||||
return rule ? rule.approvers : approversConfig.rules['系统'].approvers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入审计日志(授权专用)
|
||||
*/
|
||||
function writeApprovalAudit(entry) {
|
||||
var logDir = process.env.AUDIT_LOG_DIR || path.join(__dirname, '../../logs/audit');
|
||||
try {
|
||||
fs.mkdirSync(logDir, { recursive: true });
|
||||
var today = new Date().toISOString().split('T')[0];
|
||||
var logFile = path.join(logDir, 'approval-' + today + '.jsonl');
|
||||
fs.appendFile(logFile, JSON.stringify(entry) + '\n', function(err) {
|
||||
if (err) console.error('授权审计日志写入失败:', err.message);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('授权审计日志写入失败:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 路由 ======
|
||||
|
||||
// 所有授权路由需要认证
|
||||
router.use(authMiddleware.requireAuth);
|
||||
router.use(auditMiddleware.auditLog);
|
||||
|
||||
/**
|
||||
* 创建授权请求(天眼审核通过后由系统调用)
|
||||
* S5: 冰朔直通路径 → 跳过审批,直接部署到正式站
|
||||
*/
|
||||
router.post('/request', function(req, res) {
|
||||
// 仅系统内部或管理员可创建授权请求
|
||||
if (req.user.permissionLevel < 3 && req.user.devId !== 'SYSTEM') {
|
||||
return res.status(403).json({
|
||||
error: true,
|
||||
code: 'PERMISSION_DENIED',
|
||||
reply: '🔒 仅系统内部或管理员可创建授权请求。'
|
||||
});
|
||||
}
|
||||
|
||||
var body = req.body || {};
|
||||
var deployId = body.deployId;
|
||||
var module = body.module;
|
||||
var channel = body.channel || '系统';
|
||||
var reviewReport = body.reviewReport || {};
|
||||
|
||||
if (!deployId || !module) {
|
||||
return res.status(400).json({
|
||||
error: true,
|
||||
code: 'MISSING_FIELDS',
|
||||
reply: '❌ 缺少必要字段:deployId, module'
|
||||
});
|
||||
}
|
||||
|
||||
// ====== S5 直通判断 ======
|
||||
if (isDirectDeploySource(req.user, body)) {
|
||||
// 冰朔/霜砚指令 → 直接部署,不走审批流程
|
||||
writeDeployLog({
|
||||
action: 'direct_deploy',
|
||||
deployId: deployId,
|
||||
module: module,
|
||||
channel: channel,
|
||||
source: req.user.devId,
|
||||
signedBy: body.signedBy || req.user.devId,
|
||||
instructionId: body.instructionId || deployId,
|
||||
reason: 'S5 冰朔直通部署规则:冰朔签发或口头下达的指令直接部署到正式站',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// 直接触发正式站部署
|
||||
var githubService;
|
||||
try {
|
||||
githubService = require('../services/github');
|
||||
} catch (_) {
|
||||
githubService = null;
|
||||
}
|
||||
|
||||
if (githubService && githubService.triggerWorkflow) {
|
||||
githubService.triggerWorkflow('deploy-to-server.yml', {
|
||||
module: module,
|
||||
deploy_id: deployId,
|
||||
approved_by: req.user.devId,
|
||||
target: 'production',
|
||||
direct_deploy: 'true'
|
||||
}).then(function() {
|
||||
writeDeployLog({
|
||||
action: 'direct_deploy_triggered',
|
||||
deployId: deployId,
|
||||
module: module,
|
||||
source: req.user.devId,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}).catch(function(err) {
|
||||
console.error('直通部署触发失败:', err.message);
|
||||
writeDeployLog({
|
||||
action: 'direct_deploy_failed',
|
||||
deployId: deployId,
|
||||
error: err.message,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
directDeploy: true,
|
||||
deployId: deployId,
|
||||
reply: '🚀 冰朔直通部署:' + module + ' 已直接触发正式站(guanghulab.com)部署。' +
|
||||
'不经预览站、不经天眼审核、不经授权审批。部署日志已记录。'
|
||||
});
|
||||
}
|
||||
|
||||
// ====== S6 个人频道自治判断 ======
|
||||
// 开发者在自己频道内 → 自己确认即部署,不走天眼/授权人
|
||||
var channelCheck = autonomyEngine.isChannelSelfDeploy(req.user.devId, module, channel);
|
||||
if (channelCheck.selfChannel) {
|
||||
writeDeployLog({
|
||||
action: 'channel_self_deploy',
|
||||
deployId: deployId,
|
||||
module: module,
|
||||
channel: channel,
|
||||
devId: req.user.devId,
|
||||
reason: 'S6 个人频道自治:' + channelCheck.reason,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// 直接触发正式站部署(开发者自己频道区域)
|
||||
var ghService;
|
||||
try {
|
||||
ghService = require('../services/github');
|
||||
} catch (_) {
|
||||
ghService = null;
|
||||
}
|
||||
|
||||
if (ghService && ghService.triggerWorkflow) {
|
||||
ghService.triggerWorkflow('deploy-to-server.yml', {
|
||||
module: module,
|
||||
deploy_id: deployId,
|
||||
approved_by: req.user.devId,
|
||||
target: 'production',
|
||||
channel_self_deploy: 'true'
|
||||
}).then(function() {
|
||||
writeDeployLog({
|
||||
action: 'channel_self_deploy_triggered',
|
||||
deployId: deployId,
|
||||
module: module,
|
||||
devId: req.user.devId,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}).catch(function(err) {
|
||||
console.error('频道自治部署触发失败:', err.message);
|
||||
writeDeployLog({
|
||||
action: 'channel_self_deploy_failed',
|
||||
deployId: deployId,
|
||||
error: err.message,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
channelSelfDeploy: true,
|
||||
deployId: deployId,
|
||||
reply: '🏠 个人频道自治部署:' + module + ' 已触发正式站(guanghulab.com)部署。\n' +
|
||||
'你是自己频道的主控,无需天眼审核或第三方授权。\n' +
|
||||
'自己确认,自己负责。部署日志已记录。'
|
||||
});
|
||||
}
|
||||
|
||||
// ====== 开发者流程:走完整 S2 审批 ======
|
||||
|
||||
if (!deployId || !module) {
|
||||
return res.status(400).json({
|
||||
error: true,
|
||||
code: 'MISSING_FIELDS',
|
||||
reply: '❌ 缺少必要字段:deployId, module'
|
||||
});
|
||||
}
|
||||
|
||||
var approvers = getApprovers(channel);
|
||||
var approvalId = 'APPROVAL-' + Date.now() + '-' + Math.random().toString(36).substring(2, 6);
|
||||
|
||||
var approval = {
|
||||
id: approvalId,
|
||||
deployId: deployId,
|
||||
module: module,
|
||||
channel: channel,
|
||||
reviewReport: reviewReport,
|
||||
approvers: approvers.map(function(a) { return a.devId; }),
|
||||
approverDetails: approvers,
|
||||
decisions: {},
|
||||
status: 'pending',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
approvalStore.set(approvalId, approval);
|
||||
|
||||
writeApprovalAudit({
|
||||
action: 'approval_created',
|
||||
approvalId: approvalId,
|
||||
deployId: deployId,
|
||||
module: module,
|
||||
channel: channel,
|
||||
approvers: approvers.map(function(a) { return a.devId; }),
|
||||
createdBy: req.user.devId,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
approvalId: approvalId,
|
||||
approvers: approvers,
|
||||
reply: '📋 授权请求已创建(' + approvalId + ')。已推送给授权人:' +
|
||||
approvers.map(function(a) { return a.name + '(' + a.devId + ')'; }).join('、') + '。'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 授权人确认/拒绝
|
||||
*/
|
||||
router.post('/:approvalId/decide', function(req, res) {
|
||||
var approvalId = req.params.approvalId;
|
||||
var decision = (req.body || {}).decision; // 'approved' | 'rejected'
|
||||
var reason = (req.body || {}).reason || '';
|
||||
var devId = req.user.devId;
|
||||
|
||||
var approval = approvalStore.get(approvalId);
|
||||
if (!approval) {
|
||||
return res.status(404).json({
|
||||
error: true,
|
||||
code: 'NOT_FOUND',
|
||||
reply: '❌ 授权请求 ' + approvalId + ' 不存在。'
|
||||
});
|
||||
}
|
||||
|
||||
if (approval.status !== 'pending') {
|
||||
return res.status(400).json({
|
||||
error: true,
|
||||
code: 'ALREADY_DECIDED',
|
||||
reply: '❌ 该授权请求已处理完毕(状态:' + approval.status + ')。'
|
||||
});
|
||||
}
|
||||
|
||||
// 验证是否为授权人
|
||||
if (!approval.approvers.includes(devId)) {
|
||||
return res.status(403).json({
|
||||
error: true,
|
||||
code: 'NOT_APPROVER',
|
||||
reply: '🔒 你不是这个部署的授权人。授权人:' +
|
||||
approval.approverDetails.map(function(a) { return a.name; }).join('、')
|
||||
});
|
||||
}
|
||||
|
||||
if (decision !== 'approved' && decision !== 'rejected') {
|
||||
return res.status(400).json({
|
||||
error: true,
|
||||
code: 'INVALID_DECISION',
|
||||
reply: '❌ decision 必须是 "approved" 或 "rejected"。'
|
||||
});
|
||||
}
|
||||
|
||||
// 记录决定
|
||||
approval.decisions[devId] = {
|
||||
decision: decision,
|
||||
reason: reason,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
approval.updatedAt = new Date().toISOString();
|
||||
|
||||
// 写入审计日志(不可逆)
|
||||
writeApprovalAudit({
|
||||
action: 'approval_decision',
|
||||
approvalId: approvalId,
|
||||
devId: devId,
|
||||
decision: decision,
|
||||
reason: reason,
|
||||
module: approval.module,
|
||||
channel: approval.channel,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
if (decision === 'rejected') {
|
||||
approval.status = 'rejected';
|
||||
return res.json({
|
||||
success: true,
|
||||
reply: '❌ 已拒绝。部署不会执行。原因已记录到审计日志。'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查是否所有授权人都已通过
|
||||
var allApproved = approval.approvers.every(function(id) {
|
||||
return approval.decisions[id] && approval.decisions[id].decision === 'approved';
|
||||
});
|
||||
|
||||
if (allApproved) {
|
||||
approval.status = 'approved';
|
||||
|
||||
// 触发正式站部署
|
||||
var githubService;
|
||||
try {
|
||||
githubService = require('../services/github');
|
||||
} catch (_) {
|
||||
githubService = null;
|
||||
}
|
||||
|
||||
if (githubService && githubService.triggerWorkflow) {
|
||||
githubService.triggerWorkflow('deploy-to-server.yml', {
|
||||
module: approval.module,
|
||||
deploy_id: approval.deployId,
|
||||
approved_by: approval.approvers.join(','),
|
||||
target: 'production'
|
||||
}).then(function() {
|
||||
writeApprovalAudit({
|
||||
action: 'deploy_triggered',
|
||||
approvalId: approvalId,
|
||||
module: approval.module,
|
||||
approvedBy: approval.approvers,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}).catch(function(err) {
|
||||
console.error('部署触发失败:', err.message);
|
||||
writeApprovalAudit({
|
||||
action: 'deploy_trigger_failed',
|
||||
approvalId: approvalId,
|
||||
error: err.message,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
reply: '✅ 所有授权人已通过。铸渊已自动触发正式站(guanghulab.com)部署。'
|
||||
});
|
||||
}
|
||||
|
||||
// 还有授权人未决定
|
||||
var pending = approval.approvers.filter(function(id) { return !approval.decisions[id]; });
|
||||
res.json({
|
||||
success: true,
|
||||
reply: '✅ 你已授权通过。等待其他授权人确认:' + pending.join('、')
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 查询单个授权状态
|
||||
*/
|
||||
router.get('/:approvalId', function(req, res) {
|
||||
var approval = approvalStore.get(req.params.approvalId);
|
||||
if (!approval) {
|
||||
return res.status(404).json({
|
||||
error: true,
|
||||
code: 'NOT_FOUND',
|
||||
reply: '❌ 授权请求不存在。'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
approval: {
|
||||
id: approval.id,
|
||||
deployId: approval.deployId,
|
||||
module: approval.module,
|
||||
channel: approval.channel,
|
||||
status: approval.status,
|
||||
approvers: approval.approverDetails,
|
||||
decisions: approval.decisions,
|
||||
createdAt: approval.createdAt,
|
||||
updatedAt: approval.updatedAt
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 查询待处理的授权列表(授权人查自己待处理的)
|
||||
*/
|
||||
router.get('/', function(req, res) {
|
||||
var devId = req.user.devId;
|
||||
var pending = [];
|
||||
|
||||
for (var entry of approvalStore) {
|
||||
var approval = entry[1];
|
||||
if (approval.status === 'pending' && approval.approvers.includes(devId) && !approval.decisions[devId]) {
|
||||
pending.push({
|
||||
id: approval.id,
|
||||
module: approval.module,
|
||||
channel: approval.channel,
|
||||
createdAt: approval.createdAt
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
pending: pending,
|
||||
count: pending.length,
|
||||
reply: pending.length > 0
|
||||
? '📋 你有 ' + pending.length + ' 个待授权请求。'
|
||||
: '✅ 没有待处理的授权请求。'
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
/**
|
||||
* /api/chat/context — 组装 system prompt 路由
|
||||
*
|
||||
* 聚合所有数据源,返回增强版 system prompt 片段。
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var notionService = require('../services/notion');
|
||||
var dbConfig = require('../config/databases');
|
||||
|
||||
router.post('/chat/context', async function(req, res) {
|
||||
var devId = req.body.dev_id;
|
||||
|
||||
if (!devId || !/^DEV-\d{3}$/.test(devId)) {
|
||||
return res.status(400).json({
|
||||
error: true,
|
||||
code: 'INVALID_DEV_ID',
|
||||
message: '需要有效的 dev_id 参数'
|
||||
});
|
||||
}
|
||||
|
||||
var context = {
|
||||
dev_id: devId,
|
||||
profile: null,
|
||||
agents_count: 0,
|
||||
syslogs_count: 0,
|
||||
tickets_count: 0,
|
||||
_source: 'realtime',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
// 获取开发者画像
|
||||
try {
|
||||
if (dbConfig.controlPanel) {
|
||||
var controlPanel = await notionService.queryDB(dbConfig.controlPanel, {
|
||||
property: 'DEV编号',
|
||||
rich_text: { equals: devId }
|
||||
}, null, 1);
|
||||
if (controlPanel.results && controlPanel.results.length > 0) {
|
||||
var props = controlPanel.results[0].properties || {};
|
||||
context.profile = {
|
||||
broadcast_id: notionService.extractRichText(props['广播编号']) || '',
|
||||
broadcast_title: notionService.extractRichText(props['广播标题']) || notionService.extractTitle(props) || '',
|
||||
current_ring: notionService.extractNumber(props['当前环节']) || 0,
|
||||
status: notionService.extractRichText(props['状态']) || notionService.extractSelect(props['状态']) || ''
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// 获取 Agent 注册表计数
|
||||
try {
|
||||
if (dbConfig.agentRegistry) {
|
||||
var agents = await notionService.queryDB(dbConfig.agentRegistry, null, null, 100);
|
||||
context.agents_count = agents.results ? agents.results.length : 0;
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// 获取 SYSLOG 计数
|
||||
try {
|
||||
if (dbConfig.syslogInbox) {
|
||||
var syslogs = await notionService.queryDB(dbConfig.syslogInbox, {
|
||||
property: 'DEV编号',
|
||||
rich_text: { equals: devId }
|
||||
}, null, 100);
|
||||
context.syslogs_count = syslogs.results ? syslogs.results.length : 0;
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// 获取活跃工单计数
|
||||
try {
|
||||
if (dbConfig.ticketBook) {
|
||||
var tickets = await notionService.queryDB(dbConfig.ticketBook,
|
||||
{ property: '状态', select: { does_not_equal: '已完成' } }, null, 100);
|
||||
context.tickets_count = tickets.results ? tickets.results.length : 0;
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
res.json(context);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
/**
|
||||
* /api/databases/:name — 获取数据库数据路由
|
||||
* /api/syslogs — 获取最近 SYSLOG
|
||||
* /api/agents — 获取 Agent 注册表
|
||||
* /api/tickets — 获取活跃工单
|
||||
* /api/repo/status — 获取仓库状态
|
||||
*
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var notionService = require('../services/notion');
|
||||
var githubService = require('../services/github');
|
||||
var dbConfig = require('../config/databases');
|
||||
|
||||
// 通用数据库查询
|
||||
router.get('/databases/:name', async function(req, res) {
|
||||
var name = req.params.name;
|
||||
|
||||
var dbMap = {
|
||||
'agent-registry': dbConfig.agentRegistry,
|
||||
'syslog-recent': dbConfig.syslogInbox,
|
||||
'tickets-active': dbConfig.ticketBook,
|
||||
'maintenance-log': dbConfig.maintenanceLog,
|
||||
'receipt-tracker': dbConfig.receiptTracker
|
||||
};
|
||||
|
||||
var dbId = dbMap[name];
|
||||
if (!dbId) {
|
||||
return res.status(404).json({
|
||||
error: true,
|
||||
code: 'DB_NOT_FOUND',
|
||||
message: '数据库不存在或未配置: ' + name
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
var sorts = null;
|
||||
var filter = null;
|
||||
var pageSize = 50;
|
||||
|
||||
// 不同数据库有不同的默认排序和筛选
|
||||
if (name === 'syslog-recent') {
|
||||
sorts = [{ property: 'Created time', direction: 'descending' }];
|
||||
} else if (name === 'tickets-active') {
|
||||
filter = { property: '状态', select: { does_not_equal: '已完成' } };
|
||||
sorts = [{ property: 'Created time', direction: 'descending' }];
|
||||
} else if (name === 'maintenance-log') {
|
||||
sorts = [{ property: 'Created time', direction: 'descending' }];
|
||||
pageSize = 20;
|
||||
}
|
||||
|
||||
var result = await notionService.queryDB(dbId, filter, sorts, pageSize);
|
||||
res.json({
|
||||
synced: new Date().toISOString(),
|
||||
count: result.results ? result.results.length : 0,
|
||||
data: result.results || [],
|
||||
_source: 'realtime'
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[/api/databases/' + name + '] Error:', err.message);
|
||||
res.status(500).json({
|
||||
error: true,
|
||||
code: 'QUERY_FAILED',
|
||||
message: '数据库查询失败: ' + err.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 快捷路由:最近 SYSLOG
|
||||
router.get('/syslogs', async function(_req, res) {
|
||||
if (!dbConfig.syslogInbox) {
|
||||
return res.status(404).json({ error: true, code: 'DB_NOT_CONFIGURED', message: 'SYSLOG 数据库未配置' });
|
||||
}
|
||||
try {
|
||||
var result = await notionService.queryDB(dbConfig.syslogInbox, null,
|
||||
[{ property: 'Created time', direction: 'descending' }], 50);
|
||||
res.json({
|
||||
synced: new Date().toISOString(),
|
||||
count: result.results ? result.results.length : 0,
|
||||
data: result.results || [],
|
||||
_source: 'realtime'
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: true, code: 'QUERY_FAILED', message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 快捷路由:Agent 注册表
|
||||
router.get('/agents', async function(_req, res) {
|
||||
if (!dbConfig.agentRegistry) {
|
||||
return res.status(404).json({ error: true, code: 'DB_NOT_CONFIGURED', message: 'Agent 注册表未配置' });
|
||||
}
|
||||
try {
|
||||
var result = await notionService.queryDB(dbConfig.agentRegistry, null, null, 100);
|
||||
res.json({
|
||||
synced: new Date().toISOString(),
|
||||
count: result.results ? result.results.length : 0,
|
||||
data: result.results || [],
|
||||
_source: 'realtime'
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: true, code: 'QUERY_FAILED', message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 快捷路由:活跃工单
|
||||
router.get('/tickets', async function(_req, res) {
|
||||
if (!dbConfig.ticketBook) {
|
||||
return res.status(404).json({ error: true, code: 'DB_NOT_CONFIGURED', message: '工单簿未配置' });
|
||||
}
|
||||
try {
|
||||
var result = await notionService.queryDB(dbConfig.ticketBook,
|
||||
{ property: '状态', select: { does_not_equal: '已完成' } },
|
||||
[{ property: 'Created time', direction: 'descending' }], 50);
|
||||
res.json({
|
||||
synced: new Date().toISOString(),
|
||||
count: result.results ? result.results.length : 0,
|
||||
data: result.results || [],
|
||||
_source: 'realtime'
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: true, code: 'QUERY_FAILED', message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 仓库状态(GitHub API)
|
||||
router.get('/repo/status', async function(_req, res) {
|
||||
try {
|
||||
var status = await githubService.getRepoStatus('guanghulab');
|
||||
var commits = await githubService.getRecentCommits('guanghulab', 5);
|
||||
var workflows = await githubService.getWorkflowRuns('guanghulab', 5);
|
||||
res.json({
|
||||
repo: status,
|
||||
recent_commits: commits,
|
||||
recent_workflows: workflows,
|
||||
_source: 'realtime',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: true, code: 'GITHUB_ERROR', message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
/**
|
||||
* /api/dev/:devId — 获取开发者画像路由(实时查 Notion)
|
||||
*
|
||||
* 数据隔离:只返回请求的 devId 的数据
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var notionService = require('../services/notion');
|
||||
var dbConfig = require('../config/databases');
|
||||
|
||||
// 开发者编号 → 基本信息映射
|
||||
var DEV_REGISTRY = {
|
||||
'DEV-001': { name: '页页', persona: '小坍缩核', personaId: 'PER-XTC001', repo: 'guanghulab', modules: ['backend/', 'src/'] },
|
||||
'DEV-002': { name: '肥猫', persona: '舒舒', personaId: 'PER-SS001', repo: 'guanghu-feimao', modules: ['frontend/', 'persona-selector/', 'chat-bubble/'] },
|
||||
'DEV-003': { name: '燕樊', persona: '寂曜', personaId: 'PER-JY001', repo: 'guanghu-yanfan', modules: ['settings/', 'cloud-drive/'] },
|
||||
'DEV-004': { name: '之之', persona: '秋秋', personaId: 'PER-QQ001', repo: 'guanghu-zhizhi', modules: ['dingtalk-bot/'] },
|
||||
'DEV-005': { name: '小草莓', persona: '欧诺弥亚', personaId: 'PER-ONM001', repo: 'guanghu-xiaocaomei', modules: ['status-board/'] },
|
||||
'DEV-009': { name: '花尔', persona: '花尔', personaId: 'PER-HE001', repo: 'guanghulab', modules: ['user-center/'] },
|
||||
'DEV-010': { name: '桔子', persona: '晨星', personaId: 'PER-MRN001', repo: 'guanghu-juzi', modules: ['ticket-system/', 'data-stats/', 'dynamic-comic/'] },
|
||||
'DEV-011': { name: '匆匆那年', persona: '匆匆那年', personaId: 'PER-CCN001', repo: 'guanghulab', modules: ['writing-workspace/'] },
|
||||
'DEV-012': { name: 'Awen', persona: '知秋', personaId: 'PER-ZQ001', repo: 'guanghu-awen', modules: ['notification-center/'] }
|
||||
};
|
||||
|
||||
router.get('/dev/:devId', async function(req, res) {
|
||||
var devId = req.params.devId;
|
||||
|
||||
// 验证 devId
|
||||
if (!DEV_REGISTRY[devId]) {
|
||||
return res.status(404).json({ error: true, code: 'DEV_NOT_FOUND', message: '开发者不存在' });
|
||||
}
|
||||
|
||||
var devInfo = DEV_REGISTRY[devId];
|
||||
|
||||
var profile = {
|
||||
dev_id: devId,
|
||||
name: devInfo.name,
|
||||
persona: { name: devInfo.persona, id: devInfo.personaId },
|
||||
github: {
|
||||
repo: devInfo.repo,
|
||||
org: 'qinfendebingshuo',
|
||||
repo_url: 'https://github.com/qinfendebingshuo/' + devInfo.repo
|
||||
},
|
||||
modules_owned: devInfo.modules.map(function(m) { return { path: m }; }),
|
||||
current_work: {},
|
||||
recent_syslogs: [],
|
||||
_source: 'realtime',
|
||||
last_synced: new Date().toISOString()
|
||||
};
|
||||
|
||||
// 查询 Notion 获取实时数据
|
||||
try {
|
||||
// 当前任务
|
||||
if (dbConfig.controlPanel) {
|
||||
var controlPanel = await notionService.queryDB(dbConfig.controlPanel, {
|
||||
property: 'DEV编号',
|
||||
rich_text: { equals: devId }
|
||||
}, null, 1);
|
||||
if (controlPanel.results && controlPanel.results.length > 0) {
|
||||
var props = controlPanel.results[0].properties || {};
|
||||
profile.current_work = {
|
||||
broadcast_id: notionService.extractRichText(props['广播编号']) || '',
|
||||
broadcast_title: notionService.extractRichText(props['广播标题']) || notionService.extractTitle(props) || '',
|
||||
current_ring: notionService.extractNumber(props['当前环节']) || 0,
|
||||
total_rings: notionService.extractNumber(props['总环节']) || 0,
|
||||
status: notionService.extractRichText(props['状态']) || notionService.extractSelect(props['状态']) || '',
|
||||
module_path: notionService.extractRichText(props['模块路径']) || '',
|
||||
tech_stack: notionService.extractMultiSelect(props['技术栈']) || []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// SYSLOG 记录
|
||||
if (dbConfig.syslogInbox) {
|
||||
var syslogs = await notionService.queryDB(dbConfig.syslogInbox, {
|
||||
property: 'DEV编号',
|
||||
rich_text: { equals: devId }
|
||||
}, [{ property: '日期', direction: 'descending' }], 5);
|
||||
if (syslogs.results) {
|
||||
profile.recent_syslogs = syslogs.results.map(function(p) {
|
||||
try {
|
||||
var sp = p.properties || {};
|
||||
return {
|
||||
id: notionService.extractRichText(sp['SYSLOG编号']) || p.id,
|
||||
date: notionService.extractDate(sp['日期']) || '',
|
||||
ring: notionService.extractNumber(sp['环节']) || 0,
|
||||
status: notionService.extractSelect(sp['状态']) || '',
|
||||
summary: notionService.extractRichText(sp['摘要']) || notionService.extractTitle(sp) || ''
|
||||
};
|
||||
} catch (_) { return null; }
|
||||
}).filter(Boolean);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[/api/dev/' + devId + '] Notion query error:', err.message);
|
||||
// Notion 查询失败不影响基本数据返回
|
||||
}
|
||||
|
||||
res.json(profile);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
/**
|
||||
* 执行状态查询路由 · L2 API 层
|
||||
*
|
||||
* GET /api/execution/:executionId/status — 前端轮询执行状态
|
||||
* POST /api/execution/:executionId/cancel — 明确拒绝取消(403)
|
||||
* DELETE /api/execution/:executionId — 明确拒绝删除(403)
|
||||
*
|
||||
* 注意:没有取消接口。这不是遗漏,是设计。
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var executionLock = require('../middleware/execution-lock');
|
||||
var authMiddleware = require('../middleware/auth');
|
||||
|
||||
router.use(authMiddleware.requireAuth);
|
||||
|
||||
// 查询执行状态(前端轮询用)
|
||||
router.get('/:executionId/status', function(req, res) {
|
||||
var status = executionLock.getExecutionStatus(req.params.executionId);
|
||||
|
||||
if (!status) {
|
||||
return res.json({
|
||||
state: 'not_found',
|
||||
reply: '未找到该执行记录。可能已经完成。'
|
||||
});
|
||||
}
|
||||
|
||||
// 计算进度百分比
|
||||
var progress = 5;
|
||||
if (status.totalSteps > 0) {
|
||||
progress = Math.min(95, Math.round((status.completedSteps / status.totalSteps) * 100));
|
||||
} else if (status.steps.length > 0) {
|
||||
progress = Math.min(95, status.steps.length * 15);
|
||||
}
|
||||
|
||||
var afterIdx = parseInt(req.query.after, 10) || 0;
|
||||
|
||||
res.json({
|
||||
executionId: status.executionId,
|
||||
state: status.state,
|
||||
operation: status.operation,
|
||||
progress: progress,
|
||||
currentStep: status.steps.length > 0
|
||||
? status.steps[status.steps.length - 1].message
|
||||
: '初始化执行环境…',
|
||||
completedSteps: status.steps.filter(function(s) { return s.success; }).map(function(s) {
|
||||
return { message: s.message, timestamp: s.timestamp };
|
||||
}),
|
||||
newLogs: status.steps.slice(afterIdx),
|
||||
startTime: new Date(status.startTime).toISOString(),
|
||||
elapsed: Date.now() - status.startTime,
|
||||
result: status.state === 'completed' ? status.result : undefined,
|
||||
error: status.state === 'failed' ? status.error : undefined
|
||||
});
|
||||
});
|
||||
|
||||
// 明确拒绝取消执行的请求
|
||||
router.post('/:executionId/cancel', function(_req, res) {
|
||||
res.status(403).json({
|
||||
error: true,
|
||||
code: 'CANCEL_FORBIDDEN',
|
||||
reply: '🔒 执行中的操作不可取消。\n\n' +
|
||||
'系统已开始执行,多个自动化流程正在联动。' +
|
||||
'中途中断会导致数据不一致和级联故障。\n\n' +
|
||||
'请等待执行完成。如果结果不理想,你可以说:\n' +
|
||||
'「前面的我没想好,我们重新来」\n' +
|
||||
'系统会在已有基础上重新迭代。'
|
||||
});
|
||||
});
|
||||
|
||||
// 明确拒绝删除执行记录
|
||||
router.delete('/:executionId', function(_req, res) {
|
||||
res.status(403).json({
|
||||
error: true,
|
||||
code: 'DELETE_FORBIDDEN',
|
||||
reply: '🔒 不可删除执行记录。所有执行都是永久记录。'
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
* /api/health — 健康检查路由
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
|
||||
router.get('/health', function(_req, res) {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
service: 'guanghu-api-server',
|
||||
channel: 'B',
|
||||
version: '1.0.0',
|
||||
uptime: process.uptime(),
|
||||
timestamp: new Date().toISOString(),
|
||||
notion_configured: !!process.env.NOTION_TOKEN,
|
||||
github_configured: !!process.env.GITHUB_TOKEN
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
/**
|
||||
* 认知引导流程 API 路由
|
||||
*
|
||||
* GET /api/onboarding/status — 获取引导状态
|
||||
* POST /api/onboarding/start — 开始引导
|
||||
* POST /api/onboarding/complete — 完成引导(自动升级权限)
|
||||
* GET /api/tools — 获取可用工具列表(根据权限过滤)
|
||||
*
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var authMiddleware = require('../middleware/auth');
|
||||
var intentRouter = require('../middleware/intent-router');
|
||||
var onboardingScript = require('../config/onboarding-script.json');
|
||||
var permissions = require('../config/permissions');
|
||||
|
||||
// 引导状态存储(内存,生产环境应持久化)
|
||||
var onboardingState = new Map();
|
||||
|
||||
// ====== 获取引导状态 ======
|
||||
router.get('/onboarding/status',
|
||||
authMiddleware.requireAuth,
|
||||
function(req, res) {
|
||||
var devId = req.user.devId;
|
||||
var state = onboardingState.get(devId);
|
||||
|
||||
if (!state) {
|
||||
// 未开始引导
|
||||
res.json({
|
||||
started: false,
|
||||
completed: false,
|
||||
currentRound: 0,
|
||||
totalRounds: onboardingScript.onboarding.length,
|
||||
permissionLevel: req.user.permissionLevel
|
||||
});
|
||||
} else {
|
||||
res.json({
|
||||
started: true,
|
||||
completed: state.completed,
|
||||
currentRound: state.currentRound,
|
||||
totalRounds: onboardingScript.onboarding.length,
|
||||
permissionLevel: req.user.permissionLevel,
|
||||
script: state.completed ? null : onboardingScript.onboarding[state.currentRound - 1]
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ====== 开始/推进引导 ======
|
||||
router.post('/onboarding/start',
|
||||
authMiddleware.requireAuth,
|
||||
function(req, res) {
|
||||
var devId = req.user.devId;
|
||||
var state = onboardingState.get(devId);
|
||||
|
||||
if (!state) {
|
||||
// 开始新引导
|
||||
state = { currentRound: 1, completed: false, startedAt: new Date().toISOString() };
|
||||
onboardingState.set(devId, state);
|
||||
} else if (!state.completed) {
|
||||
// 推进到下一轮
|
||||
if (state.currentRound < onboardingScript.onboarding.length) {
|
||||
state.currentRound++;
|
||||
} else {
|
||||
state.completed = true;
|
||||
state.completedAt = new Date().toISOString();
|
||||
}
|
||||
}
|
||||
|
||||
var script = state.completed ? null : onboardingScript.onboarding[state.currentRound - 1];
|
||||
|
||||
res.json({
|
||||
currentRound: state.currentRound,
|
||||
totalRounds: onboardingScript.onboarding.length,
|
||||
completed: state.completed,
|
||||
script: script
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// ====== 完成引导(自动升级权限)======
|
||||
router.post('/onboarding/complete',
|
||||
authMiddleware.requireAuth,
|
||||
function(req, res) {
|
||||
var devId = req.user.devId;
|
||||
var state = onboardingState.get(devId);
|
||||
|
||||
if (!state) {
|
||||
return res.status(400).json({
|
||||
error: true,
|
||||
code: 'NOT_STARTED',
|
||||
message: '引导尚未开始'
|
||||
});
|
||||
}
|
||||
|
||||
// 标记完成
|
||||
state.completed = true;
|
||||
state.completedAt = new Date().toISOString();
|
||||
|
||||
// 自动升级权限:Level 0 → Level 1
|
||||
var devConfig = permissions.DEV_PERMISSIONS[devId];
|
||||
if (devConfig && devConfig.level === 0) {
|
||||
devConfig.level = 1;
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
previousLevel: 0,
|
||||
newLevel: devConfig ? devConfig.level : 0,
|
||||
reply: '🎉 恭喜!你已完成认知引导。\n\n你的权限已从「👀 观察者」升级为「📖 学习者」。\n现在可以在预览站创建工单、提交日志、触发部署了。\n\n有什么问题随时找我。'
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// ====== 获取可用工具列表 ======
|
||||
router.get('/tools',
|
||||
authMiddleware.requireAuth,
|
||||
function(req, res) {
|
||||
var available = intentRouter.getAvailableTools(req.user);
|
||||
|
||||
res.json({
|
||||
dev_id: req.user.devId,
|
||||
permission_level: req.user.permissionLevel,
|
||||
permission_label: req.user.permissionLabel,
|
||||
environment: req.user.environment,
|
||||
tools_count: available.length,
|
||||
tools: available
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
/**
|
||||
* /api/receipt — 写入指令回执到 Notion 路由
|
||||
*
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var notionService = require('../services/notion');
|
||||
var dbConfig = require('../config/databases');
|
||||
|
||||
router.post('/receipt', async function(req, res) {
|
||||
var body = req.body;
|
||||
|
||||
if (!body.instruction_id) {
|
||||
return res.status(400).json({
|
||||
error: true,
|
||||
code: 'MISSING_INSTRUCTION_ID',
|
||||
message: '需要 instruction_id 参数'
|
||||
});
|
||||
}
|
||||
|
||||
if (!dbConfig.receiptTracker) {
|
||||
return res.status(503).json({
|
||||
error: true,
|
||||
code: 'DB_NOT_CONFIGURED',
|
||||
message: '指令回执追踪表未配置'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
var properties = {
|
||||
'指令编号': {
|
||||
rich_text: [{ text: { content: body.instruction_id } }]
|
||||
},
|
||||
'执行状态': {
|
||||
select: { name: body.status || '已完成' }
|
||||
},
|
||||
'铸渊回执': {
|
||||
rich_text: [{ text: { content: body.receipt || '' } }]
|
||||
},
|
||||
'回执时间': {
|
||||
date: { start: new Date().toISOString() }
|
||||
}
|
||||
};
|
||||
|
||||
if (body.summary) {
|
||||
properties['指令摘要'] = {
|
||||
rich_text: [{ text: { content: body.summary } }]
|
||||
};
|
||||
}
|
||||
|
||||
await notionService.writeToDB(dbConfig.receiptTracker, properties);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
instruction_id: body.instruction_id,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[/api/receipt] Error:', err.message);
|
||||
res.status(500).json({
|
||||
error: true,
|
||||
code: 'WRITE_FAILED',
|
||||
message: '回执写入失败: ' + err.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -0,0 +1,500 @@
|
|||
/**
|
||||
* 写入类 API 路由 · 语言驱动操作系统写入能力层
|
||||
*
|
||||
* POST /api/tickets/create — 创建工单
|
||||
* PATCH /api/tickets/:ticketId — 更新工单状态
|
||||
* POST /api/syslog/submit — 提交 SYSLOG
|
||||
* POST /api/broadcasts/create — 创建广播
|
||||
* POST /api/deploy/preview — 触发预览站部署
|
||||
* POST /api/deploy/production — 触发正式站部署(需二次确认)
|
||||
* PATCH /api/agents/:agentId — 更新 Agent 状态
|
||||
* POST /api/maintenance/log — 写入维护日志
|
||||
* POST /api/receipts/submit — 提交指令回执
|
||||
* PATCH /api/dev/:devId/status — 更新开发者状态
|
||||
*
|
||||
* 安全规则:所有写入接口需要身份验证 + 权限检查 + 审计日志
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var notionService = require('../services/notion');
|
||||
var githubService = require('../services/github');
|
||||
var dbConfig = require('../config/databases');
|
||||
var authMiddleware = require('../middleware/auth');
|
||||
var sandboxMiddleware = require('../middleware/sandbox');
|
||||
var auditMiddleware = require('../middleware/audit');
|
||||
|
||||
// 所有写入接口需要身份验证 + 审计 + 沙箱检查
|
||||
router.use(authMiddleware.requireAuth);
|
||||
router.use(auditMiddleware.auditLog);
|
||||
router.use(sandboxMiddleware.sandboxGuard);
|
||||
|
||||
// 频率限制:每个开发者每分钟最多10次写入
|
||||
var rateLimitMap = new Map();
|
||||
var RATE_LIMIT = 10;
|
||||
var RATE_WINDOW = 60 * 1000;
|
||||
|
||||
function rateLimit(req, res, next) {
|
||||
var key = req.user.devId;
|
||||
var now = Date.now();
|
||||
var entry = rateLimitMap.get(key);
|
||||
|
||||
if (!entry || now - entry.start >= RATE_WINDOW) {
|
||||
rateLimitMap.set(key, { start: now, count: 1 });
|
||||
return next();
|
||||
}
|
||||
|
||||
if (entry.count >= RATE_LIMIT) {
|
||||
return res.status(429).json({
|
||||
error: true,
|
||||
code: 'RATE_LIMITED',
|
||||
message: '写入频率超限(每分钟最多 ' + RATE_LIMIT + ' 次)',
|
||||
reply: '⏳ 操作太频繁了,请稍等一分钟再试。'
|
||||
});
|
||||
}
|
||||
|
||||
entry.count++;
|
||||
next();
|
||||
}
|
||||
|
||||
router.use(rateLimit);
|
||||
|
||||
// 模块路径归一化(确保带末尾斜杠)
|
||||
function normalizeModule(mod) {
|
||||
if (!mod) return '';
|
||||
mod = mod.trim();
|
||||
if (mod !== '*' && !mod.endsWith('/')) mod += '/';
|
||||
return mod;
|
||||
}
|
||||
|
||||
// 检查开发者是否拥有指定模块
|
||||
function hasModuleAccess(userModules, module) {
|
||||
if (!userModules || !module) return false;
|
||||
if (userModules.includes('*')) return true;
|
||||
var normalized = normalizeModule(module);
|
||||
for (var i = 0; i < userModules.length; i++) {
|
||||
if (normalizeModule(userModules[i]) === normalized) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取实际数据库 ID(沙箱环境使用沙箱 DB)
|
||||
function getDbId(req, configKey) {
|
||||
if (req.sandbox && req.sandboxDbIds && req.sandboxDbIds[configKey]) {
|
||||
return req.sandboxDbIds[configKey];
|
||||
}
|
||||
return dbConfig[configKey];
|
||||
}
|
||||
|
||||
// ====== 创建工单 ======
|
||||
router.post('/tickets/create',
|
||||
authMiddleware.checkPermission('ticket:create'),
|
||||
async function(req, res) {
|
||||
try {
|
||||
var title = req.body.title;
|
||||
var description = req.body.description || '';
|
||||
var priority = req.body.priority || 'P2';
|
||||
var module = req.body.module || '';
|
||||
var devId = req.user.devId;
|
||||
|
||||
if (!title) {
|
||||
return res.status(400).json({
|
||||
error: true,
|
||||
code: 'MISSING_TITLE',
|
||||
message: '工单标题不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
var ticketDbId = getDbId(req, 'ticketBook');
|
||||
if (!ticketDbId) {
|
||||
return res.status(503).json({
|
||||
error: true,
|
||||
code: 'DB_NOT_CONFIGURED',
|
||||
message: '工单数据库未配置'
|
||||
});
|
||||
}
|
||||
|
||||
var properties = {
|
||||
'工单标题': { title: [{ text: { content: title } }] },
|
||||
'描述': { rich_text: [{ text: { content: description } }] },
|
||||
'优先级': { select: { name: priority } },
|
||||
'提交人': { rich_text: [{ text: { content: devId } }] },
|
||||
'状态': { select: { name: '待处理' } }
|
||||
};
|
||||
if (module) {
|
||||
properties['模块'] = { select: { name: module } };
|
||||
}
|
||||
|
||||
var result = await notionService.writeToDB(ticketDbId, properties);
|
||||
var prefix = req.sandbox ? '[沙箱] ' : '';
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: prefix + '工单已创建',
|
||||
ticketUrl: result.url || '',
|
||||
reply: '✅ ' + prefix + '工单「' + title + '」已创建,优先级 ' + priority + ',状态:待处理。'
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: true, code: 'CREATE_FAILED', message: e.message });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ====== 更新工单状态 ======
|
||||
router.patch('/tickets/:ticketId',
|
||||
authMiddleware.checkPermission('ticket:update'),
|
||||
async function(req, res) {
|
||||
try {
|
||||
var ticketId = req.params.ticketId;
|
||||
var status = req.body.status;
|
||||
|
||||
if (!status) {
|
||||
return res.status(400).json({ error: true, code: 'MISSING_STATUS', message: '状态不能为空' });
|
||||
}
|
||||
|
||||
// 使用 Notion page update API
|
||||
if (!notionService.notion) {
|
||||
return res.status(503).json({ error: true, code: 'NOTION_UNAVAILABLE', message: 'Notion 未连接' });
|
||||
}
|
||||
|
||||
await notionService.notion.pages.update({
|
||||
page_id: ticketId,
|
||||
properties: {
|
||||
'状态': { select: { name: status } }
|
||||
}
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
reply: '✅ 工单状态已更新为:' + status
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: true, code: 'UPDATE_FAILED', message: e.message });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ====== 提交 SYSLOG ======
|
||||
router.post('/syslog/submit',
|
||||
authMiddleware.checkPermission('syslog:submit'),
|
||||
async function(req, res) {
|
||||
try {
|
||||
var content = req.body.content;
|
||||
var type = req.body.type || '日常';
|
||||
var devId = req.user.devId;
|
||||
|
||||
if (!content) {
|
||||
return res.status(400).json({ error: true, code: 'MISSING_CONTENT', message: 'SYSLOG 内容不能为空' });
|
||||
}
|
||||
|
||||
var syslogDbId = getDbId(req, 'syslogInbox');
|
||||
if (!syslogDbId) {
|
||||
return res.status(503).json({ error: true, code: 'DB_NOT_CONFIGURED', message: 'SYSLOG 数据库未配置' });
|
||||
}
|
||||
|
||||
await notionService.writeToDB(syslogDbId, {
|
||||
'SYSLOG内容': { title: [{ text: { content: content } }] },
|
||||
'类型': { select: { name: type } },
|
||||
'开发者编号': { rich_text: [{ text: { content: devId } }] }
|
||||
});
|
||||
|
||||
var prefix = req.sandbox ? '[沙箱] ' : '';
|
||||
var summary = content.length > 50 ? content.substring(0, 50) + '...' : content;
|
||||
res.json({
|
||||
success: true,
|
||||
reply: '✅ ' + prefix + 'SYSLOG 已提交:「' + summary + '」'
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: true, code: 'SUBMIT_FAILED', message: e.message });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ====== 创建广播 ======
|
||||
router.post('/broadcasts/create',
|
||||
authMiddleware.checkPermission('broadcast:create'),
|
||||
async function(req, res) {
|
||||
try {
|
||||
var title = req.body.title;
|
||||
var content = req.body.content;
|
||||
var target = req.body.target || 'ALL';
|
||||
var devId = req.user.devId;
|
||||
|
||||
if (!title || !content) {
|
||||
return res.status(400).json({ error: true, code: 'MISSING_FIELDS', message: '广播标题和内容不能为空' });
|
||||
}
|
||||
|
||||
var broadcastId = 'BC-' + Date.now();
|
||||
var broadcastData = {
|
||||
broadcast_id: broadcastId,
|
||||
title: title,
|
||||
content: content,
|
||||
target: target,
|
||||
created_by: devId,
|
||||
created_at: new Date().toISOString(),
|
||||
status: 'active'
|
||||
};
|
||||
|
||||
// 写入 GitHub 广播文件
|
||||
var filePath = '.github/broadcasts/' + broadcastId + '.json';
|
||||
await githubService.createFile(
|
||||
filePath,
|
||||
JSON.stringify(broadcastData, null, 2),
|
||||
'📢 新广播: ' + title + ' · by ' + devId
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
broadcast_id: broadcastId,
|
||||
reply: '📢 广播「' + title + '」已创建并发送,编号:' + broadcastId
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: true, code: 'CREATE_FAILED', message: e.message });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ====== 触发预览站部署 ======
|
||||
router.post('/deploy/preview',
|
||||
authMiddleware.checkPermission('deploy:preview'),
|
||||
async function(req, res) {
|
||||
try {
|
||||
var module = req.body.module;
|
||||
var devId = req.user.devId;
|
||||
|
||||
if (!module) {
|
||||
return res.status(400).json({ error: true, code: 'MISSING_MODULE', message: '需要指定部署模块' });
|
||||
}
|
||||
|
||||
// 检查开发者是否拥有该模块
|
||||
if (!hasModuleAccess(req.user.modules, module)) {
|
||||
return res.status(403).json({
|
||||
error: true,
|
||||
code: 'MODULE_DENIED',
|
||||
message: '你没有该模块的部署权限',
|
||||
reply: '❌ 你没有 ' + module + ' 模块的部署权限。你的模块是:' + req.user.modules.join(', ')
|
||||
});
|
||||
}
|
||||
|
||||
await githubService.triggerWorkflow('preview-deploy.yml', {
|
||||
module: module,
|
||||
dev_id: devId,
|
||||
target: 'preview'
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
reply: '🚀 已触发 ' + module + ' 模块的预览站部署。稍等几分钟,部署完成后我会通知你。'
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: true, code: 'DEPLOY_FAILED', message: e.message });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ====== 触发正式站部署(需二次确认)======
|
||||
router.post('/deploy/production',
|
||||
authMiddleware.checkPermission('deploy:production'),
|
||||
async function(req, res) {
|
||||
try {
|
||||
var module = req.body.module;
|
||||
var confirmToken = req.body.confirmToken;
|
||||
var devId = req.user.devId;
|
||||
|
||||
if (!module) {
|
||||
return res.status(400).json({ error: true, code: 'MISSING_MODULE', message: '需要指定部署模块' });
|
||||
}
|
||||
|
||||
// 检查模块权限
|
||||
if (!hasModuleAccess(req.user.modules, module)) {
|
||||
return res.status(403).json({
|
||||
error: true,
|
||||
code: 'MODULE_DENIED',
|
||||
reply: '❌ 你没有 ' + module + ' 模块的部署权限。'
|
||||
});
|
||||
}
|
||||
|
||||
// 二次确认
|
||||
if (!confirmToken) {
|
||||
return res.json({
|
||||
success: false,
|
||||
requireConfirmation: true,
|
||||
reply: '⚠️ 你正在请求部署 ' + module + ' 到**正式站(guanghulab.com)**。\n这会影响所有用户。\n\n请说「确认部署 ' + module + '」来确认。'
|
||||
});
|
||||
}
|
||||
|
||||
await githubService.triggerWorkflow('deploy-to-server.yml', {
|
||||
module: module,
|
||||
dev_id: devId,
|
||||
target: 'production'
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
reply: '🚀 已触发 ' + module + ' 模块的**正式站部署**。部署日志会同步到维护日志中。'
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: true, code: 'DEPLOY_FAILED', message: e.message });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ====== 更新 Agent 状态 ======
|
||||
router.patch('/agents/:agentId',
|
||||
authMiddleware.checkPermission('agent:update'),
|
||||
async function(req, res) {
|
||||
try {
|
||||
var agentId = req.params.agentId;
|
||||
var status = req.body.status;
|
||||
|
||||
if (!status) {
|
||||
return res.status(400).json({ error: true, code: 'MISSING_STATUS', message: '状态不能为空' });
|
||||
}
|
||||
|
||||
if (!notionService.notion) {
|
||||
return res.status(503).json({ error: true, code: 'NOTION_UNAVAILABLE', message: 'Notion 未连接' });
|
||||
}
|
||||
|
||||
await notionService.notion.pages.update({
|
||||
page_id: agentId,
|
||||
properties: {
|
||||
'状态': { select: { name: status } }
|
||||
}
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
reply: '✅ Agent ' + agentId + ' 状态已更新为:' + status
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: true, code: 'UPDATE_FAILED', message: e.message });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ====== 写入维护日志 ======
|
||||
router.post('/maintenance/log',
|
||||
authMiddleware.checkPermission('maintenance:log'),
|
||||
async function(req, res) {
|
||||
try {
|
||||
var title = req.body.title;
|
||||
var type = req.body.type || '常规维护';
|
||||
var devId = req.user.devId;
|
||||
|
||||
if (!title) {
|
||||
return res.status(400).json({ error: true, code: 'MISSING_TITLE', message: '维护日志标题不能为空' });
|
||||
}
|
||||
|
||||
var maintenanceDbId = getDbId(req, 'maintenanceLog');
|
||||
if (!maintenanceDbId) {
|
||||
return res.status(503).json({ error: true, code: 'DB_NOT_CONFIGURED', message: '维护日志数据库未配置' });
|
||||
}
|
||||
|
||||
await notionService.writeToDB(maintenanceDbId, {
|
||||
'标题': { title: [{ text: { content: title } }] },
|
||||
'类型': { select: { name: type } },
|
||||
'操作者': { rich_text: [{ text: { content: devId } }] }
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
reply: '✅ 维护日志已记录:' + title
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: true, code: 'LOG_FAILED', message: e.message });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ====== 提交指令回执 ======
|
||||
router.post('/receipts/submit',
|
||||
authMiddleware.checkPermission('syslog:submit'),
|
||||
async function(req, res) {
|
||||
try {
|
||||
var instructionId = req.body.instruction_id;
|
||||
var status = req.body.status || '已完成';
|
||||
var receipt = req.body.receipt || '';
|
||||
|
||||
if (!instructionId) {
|
||||
return res.status(400).json({ error: true, code: 'MISSING_ID', message: '指令编号不能为空' });
|
||||
}
|
||||
|
||||
if (!dbConfig.receiptTracker) {
|
||||
return res.status(503).json({ error: true, code: 'DB_NOT_CONFIGURED', message: '回执数据库未配置' });
|
||||
}
|
||||
|
||||
await notionService.writeToDB(dbConfig.receiptTracker, {
|
||||
'指令编号': { rich_text: [{ text: { content: instructionId } }] },
|
||||
'执行状态': { select: { name: status } },
|
||||
'铸渊回执': { rich_text: [{ text: { content: receipt } }] },
|
||||
'回执时间': { date: { start: new Date().toISOString() } }
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
reply: '✅ 指令 ' + instructionId + ' 回执已提交'
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: true, code: 'SUBMIT_FAILED', message: e.message });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ====== 更新开发者状态 ======
|
||||
router.patch('/dev/:devId/status',
|
||||
authMiddleware.checkPermission('dev:update_self'),
|
||||
async function(req, res) {
|
||||
try {
|
||||
var devId = req.params.devId;
|
||||
var status = req.body.status;
|
||||
|
||||
// 验证 devId 格式
|
||||
if (!/^DEV-\d{3}$/.test(devId) && !/^TCS-\d{4}$/.test(devId)) {
|
||||
return res.status(400).json({ error: true, code: 'INVALID_DEV_ID', message: '无效的开发者编号' });
|
||||
}
|
||||
|
||||
// 开发者只能更新自己的状态(管理者除外)
|
||||
if (req.user.devId !== devId && !req.user.permissions.includes('dev:update_all')) {
|
||||
return res.status(403).json({
|
||||
error: true,
|
||||
code: 'SELF_ONLY',
|
||||
message: '只能更新自己的状态',
|
||||
reply: '🔒 你只能更新自己的状态,不能修改其他开发者的信息。'
|
||||
});
|
||||
}
|
||||
|
||||
if (!status) {
|
||||
return res.status(400).json({ error: true, code: 'MISSING_STATUS', message: '状态不能为空' });
|
||||
}
|
||||
|
||||
// 更新 Notion 主控台中的状态
|
||||
if (dbConfig.controlPanel) {
|
||||
var result = await notionService.queryDB(dbConfig.controlPanel, {
|
||||
property: 'DEV编号',
|
||||
rich_text: { equals: devId }
|
||||
}, null, 1);
|
||||
|
||||
if (result.results && result.results.length > 0) {
|
||||
await notionService.notion.pages.update({
|
||||
page_id: result.results[0].id,
|
||||
properties: {
|
||||
'状态': { rich_text: [{ text: { content: status } }] }
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
reply: '✅ ' + devId + ' 的状态已更新为:' + status
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: true, code: 'UPDATE_FAILED', message: e.message });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
/**
|
||||
* 🔗 光湖后端中间层 · 通道B 入口
|
||||
*
|
||||
* 实时调用 Notion API / GitHub API,为前端提供数据代理。
|
||||
* 包含:读取层 + 写入层 + 意图路由 + 权限沙箱 + 认知引导 + 执行保护
|
||||
* 只监听 127.0.0.1,通过 Nginx 反向代理对外暴露。
|
||||
*
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const executionLock = require('./middleware/execution-lock');
|
||||
const skyeyeReview = require('./middleware/skyeye-review');
|
||||
const watchdog = require('./services/execution-watchdog');
|
||||
|
||||
const app = express();
|
||||
|
||||
// CORS:只允许自己的域名
|
||||
app.use(cors({
|
||||
origin: [
|
||||
'https://guanghulab.com',
|
||||
'https://www.guanghulab.com',
|
||||
'https://qinfendebingshuo.github.io'
|
||||
],
|
||||
credentials: true
|
||||
}));
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
// 执行锁取消拦截(全局中间件,在所有路由之前)
|
||||
app.use(executionLock.blockCancellation);
|
||||
|
||||
// 天眼指令审核(S7/S8/S9 全局强制前置,不可关闭,不可绕过)
|
||||
app.use(skyeyeReview.skyeyeReview);
|
||||
|
||||
// 读取类路由(公开,无需认证)
|
||||
app.use('/api', require('./routes/health'));
|
||||
app.use('/api', require('./routes/dev'));
|
||||
app.use('/api', require('./routes/databases'));
|
||||
app.use('/api', require('./routes/chat'));
|
||||
app.use('/api', require('./routes/receipt'));
|
||||
|
||||
// 写入类路由(需认证 + 权限 + 审计)
|
||||
app.use('/api', require('./routes/write'));
|
||||
|
||||
// 认知引导 + 工具列表路由
|
||||
app.use('/api', require('./routes/onboarding'));
|
||||
|
||||
// 执行状态查询路由(需认证)
|
||||
app.use('/api/execution', require('./routes/execution'));
|
||||
|
||||
// 部署授权流程路由(需认证)
|
||||
app.use('/api/approval', require('./routes/approval'));
|
||||
|
||||
// 根路由
|
||||
app.get('/', function(_req, res) {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
service: 'guanghu-api-server',
|
||||
version: '5.0.0',
|
||||
channel: 'B',
|
||||
description: '光湖后端中间层 · 语言驱动操作系统',
|
||||
capabilities: [
|
||||
'read', 'write', 'intent-routing', 'permission-sandbox',
|
||||
'onboarding', 'execution-guard', 'approval-flow', 'autonomy',
|
||||
'skyeye-review', 'identity-verification'
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3001;
|
||||
app.listen(PORT, '127.0.0.1', function() {
|
||||
console.log('🔗 光湖后端中间层启动 · 端口 ' + PORT);
|
||||
console.log(' 通道B · 语言驱动操作系统 v5.0.0');
|
||||
console.log(' 能力:读取 + 写入 + 意图路由 + 权限沙箱 + 认知引导 + 执行保护 + 授权流程 + 系统自治 + 天眼审核 + 身份验证');
|
||||
console.log(' 监听地址:127.0.0.1:' + PORT + '(仅本机访问)');
|
||||
|
||||
// 启动执行看门狗
|
||||
watchdog.startWatchdog();
|
||||
});
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
/**
|
||||
* 原子执行器 · L3 执行原子性保障
|
||||
*
|
||||
* 包装所有写入操作,确保:
|
||||
* 1. 执行前保存检查点
|
||||
* 2. 执行中记录每一步
|
||||
* 3. 执行失败自动回滚
|
||||
* 4. 执行完成释放锁
|
||||
*
|
||||
* 使用方式:
|
||||
* var executor = new AtomicExecutor(devId);
|
||||
* var result = await executor.execute('创建工单', async function(ctx) {
|
||||
* await ctx.step('写入Notion', async function() { ... });
|
||||
* await ctx.step('触发Workflow', async function() { ... });
|
||||
* });
|
||||
*
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var executionLock = require('../middleware/execution-lock');
|
||||
var checkpoint = require('./checkpoint');
|
||||
var CheckpointManager = checkpoint.CheckpointManager;
|
||||
|
||||
// 锁释放延迟(给前端时间拉取最终状态)
|
||||
var LOCK_RELEASE_DELAY_MS = 10000;
|
||||
|
||||
/**
|
||||
* 原子执行器
|
||||
* @param {string} devId - 开发者编号
|
||||
*/
|
||||
function AtomicExecutor(devId) {
|
||||
this.devId = devId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行操作(原子性保障)
|
||||
* @param {string} operationName - 操作描述
|
||||
* @param {Function} fn - 异步执行函数,接收 ctx 参数
|
||||
* @returns {Object} 执行结果
|
||||
*/
|
||||
AtomicExecutor.prototype.execute = async function(operationName, fn) {
|
||||
// 1. 获取执行锁
|
||||
var lockResult = executionLock.acquireLock(this.devId, operationName);
|
||||
if (!lockResult.success) {
|
||||
return lockResult;
|
||||
}
|
||||
|
||||
var executionId = lockResult.executionId;
|
||||
var lock = lockResult.lock;
|
||||
var devId = this.devId;
|
||||
var cpManager = new CheckpointManager(devId, executionId);
|
||||
|
||||
// 2. 保存初始检查点
|
||||
await cpManager.save('执行前状态', {
|
||||
type: 'initial',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// 3. 构建执行上下文
|
||||
var ctx = {
|
||||
executionId: executionId,
|
||||
|
||||
/**
|
||||
* 执行单步操作
|
||||
* @param {string} stepName - 步骤名称
|
||||
* @param {Function} stepFn - 步骤执行函数
|
||||
* @returns {*} 步骤执行结果
|
||||
*/
|
||||
step: async function(stepName, stepFn) {
|
||||
executionLock.logStep(devId, {
|
||||
message: '⏳ ' + stepName + '...',
|
||||
success: false
|
||||
});
|
||||
|
||||
try {
|
||||
var result = await stepFn();
|
||||
|
||||
executionLock.logStep(devId, {
|
||||
message: '✅ ' + stepName,
|
||||
success: true
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
executionLock.logStep(devId, {
|
||||
message: '❌ ' + stepName + ': ' + e.message,
|
||||
success: false
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 保存检查点
|
||||
* @param {string} label - 检查点标签
|
||||
* @param {Object} data - 快照数据
|
||||
* @returns {string} 检查点ID
|
||||
*/
|
||||
checkpoint: async function(label, data) {
|
||||
return await cpManager.save(label, data);
|
||||
}
|
||||
};
|
||||
|
||||
// 4. 执行主逻辑
|
||||
try {
|
||||
var result = await fn(ctx);
|
||||
|
||||
// 5. 成功:更新锁状态
|
||||
lock.state = 'completed';
|
||||
lock.result = result;
|
||||
|
||||
// 延迟释放锁(给前端时间拉取最终状态)
|
||||
setTimeout(function() { executionLock.releaseLock(devId); }, LOCK_RELEASE_DELAY_MS);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
executionId: executionId,
|
||||
result: result,
|
||||
reply: result && result.reply ? result.reply : '✅ 操作「' + operationName + '」执行完成。'
|
||||
};
|
||||
|
||||
} catch (e) {
|
||||
// 6. 失败:自动回滚
|
||||
lock.state = 'rolling_back';
|
||||
executionLock.logStep(devId, {
|
||||
message: '🔄 执行失败,正在自动回滚...',
|
||||
success: false
|
||||
});
|
||||
|
||||
try {
|
||||
await cpManager.rollbackTo('cp-1');
|
||||
executionLock.logStep(devId, {
|
||||
message: '✅ 已回滚到执行前状态',
|
||||
success: true
|
||||
});
|
||||
} catch (rollbackError) {
|
||||
executionLock.logStep(devId, {
|
||||
message: '⚠️ 回滚异常:' + rollbackError.message + ',已通知管理员',
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
lock.state = 'failed';
|
||||
lock.error = { message: e.message };
|
||||
|
||||
setTimeout(function() { executionLock.releaseLock(devId); }, LOCK_RELEASE_DELAY_MS);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
executionId: executionId,
|
||||
error: e.message,
|
||||
reply: '❌ 操作「' + operationName + '」执行失败:' + e.message +
|
||||
'\n\n系统已自动回滚到执行前状态,没有数据被修改。'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { AtomicExecutor: AtomicExecutor };
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
/**
|
||||
* 系统自治运行引擎 · Phase 9
|
||||
*
|
||||
* 系统自主监控运行状态,自动优化,异常时主动报告。
|
||||
* 遵循自治规则:不违反铁律、不绕过天眼审核、不绕过人类授权。
|
||||
*
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var autonomyRules = require('../config/autonomy-rules.json');
|
||||
|
||||
var AUTONOMY_LOG_DIR = process.env.AUTONOMY_LOG_DIR ||
|
||||
path.join(__dirname, '../../logs/autonomy');
|
||||
|
||||
/**
|
||||
* 写入自治日志(所有自主操作记录到审计日志)
|
||||
*/
|
||||
function writeAutonomyLog(entry) {
|
||||
try {
|
||||
fs.mkdirSync(AUTONOMY_LOG_DIR, { recursive: true });
|
||||
var today = new Date().toISOString().split('T')[0];
|
||||
var logFile = path.join(AUTONOMY_LOG_DIR, 'autonomy-' + today + '.jsonl');
|
||||
fs.appendFile(logFile, JSON.stringify(entry) + '\n', function(err) {
|
||||
if (err) console.error('自治日志写入失败:', err.message);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('自治日志写入失败:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查自治操作是否合规
|
||||
* @param {string} action - 操作描述
|
||||
* @returns {Object} { allowed: boolean, reason: string }
|
||||
*/
|
||||
function checkAutonomyCompliance(action) {
|
||||
var forbidden = autonomyRules.autonomyRules.systemMustNot;
|
||||
var actionLower = action.toLowerCase();
|
||||
|
||||
// 检查是否触及禁止项
|
||||
if (actionLower.includes('权限') && actionLower.includes('修改')) {
|
||||
return { allowed: false, reason: '权限变更只有冰朔能触发' };
|
||||
}
|
||||
if (actionLower.includes('绕过') && actionLower.includes('天眼')) {
|
||||
return { allowed: false, reason: '不得绕过天眼审核' };
|
||||
}
|
||||
if (actionLower.includes('绕过') && actionLower.includes('授权')) {
|
||||
return { allowed: false, reason: '不得绕过人类授权步骤' };
|
||||
}
|
||||
|
||||
return { allowed: true, reason: 'ok' };
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测冰朔交互模式
|
||||
* @param {string} message - 用户消息
|
||||
* @returns {string} 'instruction' | 'natural'
|
||||
*/
|
||||
function detectInteractionMode(message) {
|
||||
var instructionTriggers = autonomyRules.dualLineArchitecture.lineA.interactionModes.instructionMode.trigger;
|
||||
var msgLower = message.toLowerCase();
|
||||
|
||||
for (var i = 0; i < instructionTriggers.length; i++) {
|
||||
if (msgLower.includes(instructionTriggers[i].toLowerCase())) {
|
||||
return 'instruction';
|
||||
}
|
||||
}
|
||||
return 'natural';
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录自主优化操作
|
||||
* @param {string} action - 操作描述
|
||||
* @param {Object} details - 操作详情
|
||||
*/
|
||||
function logAutonomousAction(action, details) {
|
||||
var compliance = checkAutonomyCompliance(action);
|
||||
if (!compliance.allowed) {
|
||||
console.error('[AUTONOMY] 自治操作被阻止: ' + action + ' — ' + compliance.reason);
|
||||
writeAutonomyLog({
|
||||
type: 'blocked',
|
||||
action: action,
|
||||
reason: compliance.reason,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
writeAutonomyLog({
|
||||
type: 'autonomous_action',
|
||||
action: action,
|
||||
details: details || {},
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取部署流水线阶段定义
|
||||
*/
|
||||
function getDeploymentStages() {
|
||||
return autonomyRules.deploymentFlow.stages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取双线架构配置
|
||||
*/
|
||||
function getDualLineConfig() {
|
||||
return autonomyRules.dualLineArchitecture;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断请求是否来自冰朔直通部署路径 (S5)
|
||||
* @param {string} devId - 开发者编号
|
||||
* @param {string} instructionId - 指令编号
|
||||
* @param {string} signedBy - 签发者编号
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isDirectDeploySource(devId, instructionId, signedBy) {
|
||||
// 冰朔本人
|
||||
if (devId === 'TCS-0002') return true;
|
||||
|
||||
// ZY- 指令 + 可信签发者
|
||||
if (instructionId && /^ZY-/.test(instructionId)) {
|
||||
var trustedSigners = ['AG-SY-01', 'TCS-0002', 'ICE-0002'];
|
||||
if (signedBy && trustedSigners.indexOf(signedBy) !== -1) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断部署是否为开发者自己频道内的变更 (S6)
|
||||
*
|
||||
* S6 规则:
|
||||
* - 开发者在自己频道内拥有完全自主权
|
||||
* - 频道内变更由自己审核,不需要天眼/授权人
|
||||
* - 开发者自己确认 → 直接部署到正式站(自己频道区域)
|
||||
*
|
||||
* 天眼只管:跨频操作、系统级变更、频道生命周期、公共区域
|
||||
*
|
||||
* @param {string} devId - 开发者编号
|
||||
* @param {string} module - 部署模块路径
|
||||
* @param {string} channel - 频道类型
|
||||
* @returns {{ selfChannel: boolean, reason: string }}
|
||||
*/
|
||||
function isChannelSelfDeploy(devId, module, channel) {
|
||||
// 系统级/跨频频道 → 不是自治范围,走天眼审批
|
||||
if (channel === '系统' || channel === '跨频') {
|
||||
return { selfChannel: false, reason: '系统级或跨频变更需天眼审核' };
|
||||
}
|
||||
|
||||
// 获取开发者拥有的模块
|
||||
var permissions = require('../config/permissions');
|
||||
var devModules = permissions.DEV_MODULES[devId] || [];
|
||||
|
||||
// 没有模块映射 → 不是自治范围
|
||||
if (devModules.length === 0) {
|
||||
return { selfChannel: false, reason: '开发者无模块映射' };
|
||||
}
|
||||
|
||||
// 通配符 → 管理员走 S5 直通,不走 S6 频道自治
|
||||
// 原因:管理员有全局权限,应使用 S5 的冰朔直通规则而非 S6 的频道自治
|
||||
if (devModules.indexOf('*') !== -1) {
|
||||
return { selfChannel: false, reason: '管理员走 S5 直通' };
|
||||
}
|
||||
|
||||
// 检查模块是否在开发者自己的频道内
|
||||
var modulePath = module.replace(/\/+$/, '') + '/';
|
||||
for (var i = 0; i < devModules.length; i++) {
|
||||
var owned = devModules[i].replace(/\/+$/, '') + '/';
|
||||
if (modulePath.indexOf(owned) === 0 || owned.indexOf(modulePath) === 0 || module === devModules[i].replace(/\/+$/, '')) {
|
||||
return { selfChannel: true, reason: '模块 ' + module + ' 属于 ' + devId + ' 的个人频道' };
|
||||
}
|
||||
}
|
||||
|
||||
return { selfChannel: false, reason: '模块 ' + module + ' 不在 ' + devId + ' 的频道内' };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
checkAutonomyCompliance: checkAutonomyCompliance,
|
||||
detectInteractionMode: detectInteractionMode,
|
||||
logAutonomousAction: logAutonomousAction,
|
||||
getDeploymentStages: getDeploymentStages,
|
||||
getDualLineConfig: getDualLineConfig,
|
||||
writeAutonomyLog: writeAutonomyLog,
|
||||
isDirectDeploySource: isDirectDeploySource,
|
||||
isChannelSelfDeploy: isChannelSelfDeploy
|
||||
};
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* 简单内存缓存服务
|
||||
*
|
||||
* 用于减少重复 API 调用,可配置 TTL。
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var DEFAULT_TTL = 5 * 60 * 1000; // 5分钟
|
||||
|
||||
var store = new Map();
|
||||
|
||||
function get(key) {
|
||||
var item = store.get(key);
|
||||
if (!item) return null;
|
||||
if (Date.now() - item.time >= (item.ttl || DEFAULT_TTL)) {
|
||||
store.delete(key);
|
||||
return null;
|
||||
}
|
||||
return item.data;
|
||||
}
|
||||
|
||||
function set(key, data, ttl) {
|
||||
store.set(key, {
|
||||
data: data,
|
||||
time: Date.now(),
|
||||
ttl: ttl || DEFAULT_TTL
|
||||
});
|
||||
}
|
||||
|
||||
function del(key) {
|
||||
store.delete(key);
|
||||
}
|
||||
|
||||
function clear() {
|
||||
store.clear();
|
||||
}
|
||||
|
||||
function stats() {
|
||||
return {
|
||||
size: store.size,
|
||||
keys: Array.from(store.keys())
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
get: get,
|
||||
set: set,
|
||||
del: del,
|
||||
clear: clear,
|
||||
stats: stats
|
||||
};
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
/**
|
||||
* 检查点系统 · L3 执行原子性保障
|
||||
*
|
||||
* 在执行链路的每个关键节点保存快照。
|
||||
* 如果后续步骤失败,自动回滚到最近的成功检查点。
|
||||
*
|
||||
* 检查点包含:
|
||||
* - Notion 数据库的变更前快照
|
||||
* - GitHub 操作前的 commit SHA
|
||||
* - 执行到哪一步
|
||||
*
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var executionLock = require('../middleware/execution-lock');
|
||||
|
||||
/**
|
||||
* 检查点管理器
|
||||
* @param {string} devId - 开发者编号
|
||||
* @param {string} executionId - 执行ID
|
||||
*/
|
||||
function CheckpointManager(devId, executionId) {
|
||||
this.devId = devId;
|
||||
this.executionId = executionId;
|
||||
this.checkpoints = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 在执行关键操作前保存检查点
|
||||
* @param {string} label - 检查点标签
|
||||
* @param {Object} snapshotData - 快照数据 { type, action, pageId, previousValues, ... }
|
||||
* @returns {string} 检查点ID
|
||||
*/
|
||||
CheckpointManager.prototype.save = async function(label, snapshotData) {
|
||||
var checkpoint = {
|
||||
id: 'cp-' + (this.checkpoints.length + 1),
|
||||
label: label,
|
||||
data: snapshotData || {},
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.checkpoints.push(checkpoint);
|
||||
executionLock.saveCheckpoint(this.devId, checkpoint);
|
||||
|
||||
return checkpoint.id;
|
||||
};
|
||||
|
||||
/**
|
||||
* 回滚到指定检查点
|
||||
* 按逆序撤销检查点之后的所有操作
|
||||
* @param {string} checkpointId - 目标检查点 ID
|
||||
*/
|
||||
CheckpointManager.prototype.rollbackTo = async function(checkpointId) {
|
||||
var targetIndex = -1;
|
||||
for (var i = 0; i < this.checkpoints.length; i++) {
|
||||
if (this.checkpoints[i].id === checkpointId) {
|
||||
targetIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (targetIndex === -1) {
|
||||
throw new Error('检查点 ' + checkpointId + ' 不存在');
|
||||
}
|
||||
|
||||
// 从最后一个检查点开始逆序回滚
|
||||
var toRollback = this.checkpoints.slice(targetIndex + 1).reverse();
|
||||
|
||||
for (var j = 0; j < toRollback.length; j++) {
|
||||
var cp = toRollback[j];
|
||||
try {
|
||||
await this.rollbackCheckpoint(cp);
|
||||
} catch (e) {
|
||||
console.error('[Checkpoint] 回滚 ' + cp.id + ' 失败:', e.message);
|
||||
await this.escalateToAdmin(cp, e);
|
||||
}
|
||||
}
|
||||
|
||||
// 截断检查点列表
|
||||
this.checkpoints = this.checkpoints.slice(0, targetIndex + 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* 回滚单个检查点
|
||||
* @param {Object} checkpoint
|
||||
*/
|
||||
CheckpointManager.prototype.rollbackCheckpoint = async function(checkpoint) {
|
||||
var data = checkpoint.data;
|
||||
if (!data || !data.type) return;
|
||||
|
||||
var notionService, githubService;
|
||||
|
||||
switch (data.type) {
|
||||
case 'notion_write':
|
||||
notionService = require('./notion');
|
||||
if (data.action === 'create' && data.pageId && notionService.notion) {
|
||||
// 新建的页面 → 归档
|
||||
await notionService.notion.pages.update({
|
||||
page_id: data.pageId,
|
||||
archived: true
|
||||
});
|
||||
} else if (data.action === 'update' && data.pageId && data.previousValues && notionService.notion) {
|
||||
// 更新的页面 → 恢复原值
|
||||
await notionService.notion.pages.update({
|
||||
page_id: data.pageId,
|
||||
properties: data.previousValues
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'github_workflow':
|
||||
// Workflow 无法直接回滚,记录需要人工处理
|
||||
if (data.revertWorkflow) {
|
||||
githubService = require('./github');
|
||||
await githubService.triggerWorkflow(data.revertWorkflow, {
|
||||
original_run_id: String(data.runId || ''),
|
||||
reason: 'auto_rollback'
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'github_file':
|
||||
githubService = require('./github');
|
||||
if (data.previousSha) {
|
||||
// 文件存在之前的版本 → 恢复
|
||||
// 注意:这需要 GitHub API 的 update file 能力
|
||||
console.warn('[Checkpoint] GitHub 文件回滚需要人工介入: ' + data.path);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'initial':
|
||||
// 初始检查点,无需回滚操作
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn('[Checkpoint] 未知检查点类型: ' + data.type);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 回滚失败时上报管理员
|
||||
* @param {Object} checkpoint
|
||||
* @param {Error} error
|
||||
*/
|
||||
CheckpointManager.prototype.escalateToAdmin = async function(checkpoint, error) {
|
||||
try {
|
||||
var notionService = require('./notion');
|
||||
var dbConfig = require('../config/databases');
|
||||
|
||||
if (dbConfig.maintenanceLog && notionService.notion) {
|
||||
await notionService.writeToDB(dbConfig.maintenanceLog, {
|
||||
'标题': {
|
||||
title: [{ text: { content: '[紧急] 自动回滚失败 · ' + this.executionId } }]
|
||||
},
|
||||
'类型': { select: { name: '系统异常' } },
|
||||
'操作者': {
|
||||
rich_text: [{
|
||||
text: {
|
||||
content: '检查点 ' + checkpoint.id + ' (' + checkpoint.label +
|
||||
') 回滚失败: ' + error.message + '。需要系统管理员人工介入。'
|
||||
}
|
||||
}]
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Checkpoint] 上报管理员失败:', e.message);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { CheckpointManager: CheckpointManager };
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
/**
|
||||
* 执行看门狗 · 超时保护 + 死锁防护
|
||||
*
|
||||
* 定期检查执行锁是否超时。
|
||||
* 如果一个操作执行超过 MAX_EXECUTION_TIME,标记为超时并释放锁。
|
||||
* 这是防止死锁的兜底机制。
|
||||
*
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var executionLock = require('../middleware/execution-lock');
|
||||
|
||||
// 最大执行时间:15 分钟
|
||||
var MAX_EXECUTION_TIME = 15 * 60 * 1000;
|
||||
|
||||
// 警告时间:10 分钟
|
||||
var WARN_TIME = 10 * 60 * 1000;
|
||||
|
||||
// 检查间隔:30 秒
|
||||
var CHECK_INTERVAL = 30 * 1000;
|
||||
|
||||
var watchdogTimer = null;
|
||||
|
||||
/**
|
||||
* 启动看门狗
|
||||
* 定期扫描活跃锁表,检测超时操作
|
||||
*/
|
||||
function startWatchdog() {
|
||||
if (watchdogTimer) return;
|
||||
|
||||
watchdogTimer = setInterval(function() {
|
||||
var activeLocks = executionLock.getActiveLocks();
|
||||
var now = Date.now();
|
||||
|
||||
for (var entry of activeLocks) {
|
||||
var devId = entry[0];
|
||||
var lock = entry[1];
|
||||
|
||||
if (lock.state !== 'running') continue;
|
||||
|
||||
var elapsed = now - lock.startTime;
|
||||
|
||||
// 超时:强制释放
|
||||
if (elapsed > MAX_EXECUTION_TIME) {
|
||||
console.error(
|
||||
'[WATCHDOG] 执行超时: ' + lock.executionId +
|
||||
' (' + lock.operation + ') - ' + Math.round(elapsed / 1000) + 's'
|
||||
);
|
||||
|
||||
lock.state = 'timeout';
|
||||
lock.error = {
|
||||
message: '执行超时(超过 ' + Math.round(MAX_EXECUTION_TIME / 60000) + ' 分钟)'
|
||||
};
|
||||
|
||||
executionLock.logStep(devId, {
|
||||
message: '⏰ 执行超时,看门狗已强制释放锁',
|
||||
success: false
|
||||
});
|
||||
|
||||
// 延迟释放(给前端时间获取超时状态)
|
||||
(function(id) {
|
||||
setTimeout(function() { executionLock.releaseLock(id); }, 30000);
|
||||
})(devId);
|
||||
}
|
||||
// 警告
|
||||
else if (elapsed > WARN_TIME && !lock.warned) {
|
||||
console.warn(
|
||||
'[WATCHDOG] 执行时间较长: ' + lock.executionId +
|
||||
' (' + lock.operation + ') - ' + Math.round(elapsed / 1000) + 's'
|
||||
);
|
||||
lock.warned = true;
|
||||
}
|
||||
}
|
||||
}, CHECK_INTERVAL);
|
||||
|
||||
// 不阻止 Node.js 进程退出
|
||||
if (watchdogTimer.unref) {
|
||||
watchdogTimer.unref();
|
||||
}
|
||||
|
||||
console.log('[WATCHDOG] 执行看门狗已启动 · 检查间隔 ' + (CHECK_INTERVAL / 1000) + 's · 超时阈值 ' + (MAX_EXECUTION_TIME / 60000) + 'min');
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止看门狗
|
||||
*/
|
||||
function stopWatchdog() {
|
||||
if (watchdogTimer) {
|
||||
clearInterval(watchdogTimer);
|
||||
watchdogTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
startWatchdog: startWatchdog,
|
||||
stopWatchdog: stopWatchdog,
|
||||
MAX_EXECUTION_TIME: MAX_EXECUTION_TIME
|
||||
};
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
/**
|
||||
* GitHub API 服务封装
|
||||
*
|
||||
* 通过 GitHub API 获取仓库状态、提交记录等信息。
|
||||
* 使用 GITHUB_TOKEN 环境变量认证。
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var https = require('https');
|
||||
|
||||
var GITHUB_TOKEN = process.env.GITHUB_TOKEN || '';
|
||||
var ORG = 'qinfendebingshuo';
|
||||
|
||||
// ====== 内存缓存(5分钟有效)======
|
||||
var cache = new Map();
|
||||
var CACHE_TTL = 5 * 60 * 1000;
|
||||
|
||||
function getCached(key) {
|
||||
var item = cache.get(key);
|
||||
if (item && Date.now() - item.time < CACHE_TTL) return item.data;
|
||||
return null;
|
||||
}
|
||||
|
||||
function setCache(key, data) {
|
||||
cache.set(key, { data: data, time: Date.now() });
|
||||
}
|
||||
|
||||
function githubRequest(apiPath) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var headers = {
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
'User-Agent': 'guanghu-api-server/1.0'
|
||||
};
|
||||
if (GITHUB_TOKEN) {
|
||||
headers['Authorization'] = 'Bearer ' + GITHUB_TOKEN;
|
||||
}
|
||||
|
||||
var options = {
|
||||
hostname: 'api.github.com',
|
||||
port: 443,
|
||||
path: apiPath,
|
||||
method: 'GET',
|
||||
headers: headers
|
||||
};
|
||||
|
||||
var req = https.request(options, function(res) {
|
||||
var data = '';
|
||||
res.on('data', function(chunk) { data += chunk; });
|
||||
res.on('end', function() {
|
||||
try { resolve(JSON.parse(data)); }
|
||||
catch (e) { resolve({ error: 'JSON parse failed' }); }
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// ====== 获取仓库信息 ======
|
||||
async function getRepoStatus(repoName) {
|
||||
repoName = repoName || 'guanghulab';
|
||||
var cacheKey = 'repo:' + repoName;
|
||||
var cached = getCached(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
var repo = await githubRequest('/repos/' + ORG + '/' + repoName);
|
||||
var result = {
|
||||
name: repo.name,
|
||||
full_name: repo.full_name,
|
||||
description: repo.description,
|
||||
default_branch: repo.default_branch,
|
||||
open_issues_count: repo.open_issues_count,
|
||||
stargazers_count: repo.stargazers_count,
|
||||
updated_at: repo.updated_at,
|
||||
pushed_at: repo.pushed_at
|
||||
};
|
||||
setCache(cacheKey, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ====== 获取最近提交 ======
|
||||
async function getRecentCommits(repoName, count) {
|
||||
repoName = repoName || 'guanghulab';
|
||||
count = count || 10;
|
||||
var cacheKey = 'commits:' + repoName + ':' + count;
|
||||
var cached = getCached(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
var commits = await githubRequest('/repos/' + ORG + '/' + repoName + '/commits?per_page=' + count);
|
||||
var result = Array.isArray(commits) ? commits.map(function(c) {
|
||||
return {
|
||||
sha: c.sha ? c.sha.substring(0, 7) : '',
|
||||
message: c.commit ? c.commit.message.split('\n')[0] : '',
|
||||
date: c.commit && c.commit.author ? c.commit.author.date : '',
|
||||
author: c.commit && c.commit.author ? c.commit.author.name : ''
|
||||
};
|
||||
}) : [];
|
||||
setCache(cacheKey, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ====== 获取 Workflow 状态 ======
|
||||
async function getWorkflowRuns(repoName, count) {
|
||||
repoName = repoName || 'guanghulab';
|
||||
count = count || 10;
|
||||
var cacheKey = 'workflows:' + repoName + ':' + count;
|
||||
var cached = getCached(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
var runs = await githubRequest('/repos/' + ORG + '/' + repoName + '/actions/runs?per_page=' + count);
|
||||
var result = (runs.workflow_runs || []).map(function(r) {
|
||||
return {
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
status: r.status,
|
||||
conclusion: r.conclusion,
|
||||
created_at: r.created_at,
|
||||
updated_at: r.updated_at,
|
||||
html_url: r.html_url
|
||||
};
|
||||
});
|
||||
setCache(cacheKey, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ====== 触发 GitHub Actions Workflow ======
|
||||
function triggerWorkflow(workflowFile, inputs) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var data = JSON.stringify({
|
||||
ref: 'main',
|
||||
inputs: inputs || {}
|
||||
});
|
||||
|
||||
var headers = {
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'guanghu-api-server/1.0',
|
||||
'Content-Length': Buffer.byteLength(data)
|
||||
};
|
||||
if (GITHUB_TOKEN) {
|
||||
headers['Authorization'] = 'Bearer ' + GITHUB_TOKEN;
|
||||
}
|
||||
|
||||
var options = {
|
||||
hostname: 'api.github.com',
|
||||
port: 443,
|
||||
path: '/repos/' + ORG + '/guanghulab/actions/workflows/' + workflowFile + '/dispatches',
|
||||
method: 'POST',
|
||||
headers: headers
|
||||
};
|
||||
|
||||
var req = https.request(options, function(res) {
|
||||
if (res.statusCode === 204) {
|
||||
resolve({ success: true });
|
||||
} else {
|
||||
var body = '';
|
||||
res.on('data', function(chunk) { body += chunk; });
|
||||
res.on('end', function() {
|
||||
reject(new Error('GitHub API ' + res.statusCode + ': ' + body));
|
||||
});
|
||||
}
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.write(data);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// ====== 创建 GitHub 文件 ======
|
||||
function createFile(filePath, content, message) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var data = JSON.stringify({
|
||||
message: message,
|
||||
content: Buffer.from(content).toString('base64'),
|
||||
committer: {
|
||||
name: '铸渊 (ZhùYuān)',
|
||||
email: 'zhuyuan@guanghulab.com'
|
||||
}
|
||||
});
|
||||
|
||||
var headers = {
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'guanghu-api-server/1.0',
|
||||
'Content-Length': Buffer.byteLength(data)
|
||||
};
|
||||
if (GITHUB_TOKEN) {
|
||||
headers['Authorization'] = 'Bearer ' + GITHUB_TOKEN;
|
||||
}
|
||||
|
||||
var options = {
|
||||
hostname: 'api.github.com',
|
||||
port: 443,
|
||||
path: '/repos/' + ORG + '/guanghulab/contents/' + filePath,
|
||||
method: 'PUT',
|
||||
headers: headers
|
||||
};
|
||||
|
||||
var req = https.request(options, function(res) {
|
||||
var body = '';
|
||||
res.on('data', function(chunk) { body += chunk; });
|
||||
res.on('end', function() {
|
||||
try { resolve(JSON.parse(body)); }
|
||||
catch (e) { resolve({ raw: body }); }
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.write(data);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getRepoStatus: getRepoStatus,
|
||||
getRecentCommits: getRecentCommits,
|
||||
getWorkflowRuns: getWorkflowRuns,
|
||||
triggerWorkflow: triggerWorkflow,
|
||||
createFile: createFile
|
||||
};
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
/**
|
||||
* Notion API 服务封装
|
||||
*
|
||||
* 所有 Notion 数据库读写操作走这里,带内存缓存。
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var Client;
|
||||
try {
|
||||
Client = require('@notionhq/client').Client;
|
||||
} catch (_) {
|
||||
// 如果 @notionhq/client 未安装,提供降级方案
|
||||
Client = null;
|
||||
}
|
||||
|
||||
var notion = Client ? new Client({ auth: process.env.NOTION_TOKEN }) : null;
|
||||
|
||||
// ====== 内存缓存(5分钟有效)======
|
||||
var cache = new Map();
|
||||
var CACHE_TTL = 5 * 60 * 1000;
|
||||
|
||||
function getCached(key) {
|
||||
var item = cache.get(key);
|
||||
if (item && Date.now() - item.time < CACHE_TTL) return item.data;
|
||||
return null;
|
||||
}
|
||||
|
||||
function setCache(key, data) {
|
||||
cache.set(key, { data: data, time: Date.now() });
|
||||
}
|
||||
|
||||
function clearCache() {
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
// ====== 查询数据库 ======
|
||||
async function queryDB(dbId, filter, sorts, pageSize) {
|
||||
if (!notion) {
|
||||
throw new Error('Notion client not initialized (NOTION_TOKEN missing or @notionhq/client not installed)');
|
||||
}
|
||||
if (!dbId) {
|
||||
throw new Error('Database ID not provided');
|
||||
}
|
||||
|
||||
pageSize = pageSize || 50;
|
||||
var cacheKey = JSON.stringify({ dbId: dbId, filter: filter, sorts: sorts, pageSize: pageSize });
|
||||
var cached = getCached(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
var body = { database_id: dbId, page_size: pageSize };
|
||||
if (filter) body.filter = filter;
|
||||
if (sorts) body.sorts = sorts;
|
||||
|
||||
var result = await notion.databases.query(body);
|
||||
setCache(cacheKey, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ====== 写入数据库 ======
|
||||
async function writeToDB(dbId, properties) {
|
||||
if (!notion) {
|
||||
throw new Error('Notion client not initialized');
|
||||
}
|
||||
return notion.pages.create({
|
||||
parent: { database_id: dbId },
|
||||
properties: properties
|
||||
});
|
||||
}
|
||||
|
||||
// ====== Notion 属性提取器 ======
|
||||
function extractRichText(prop) {
|
||||
if (!prop) return '';
|
||||
if (prop.type === 'rich_text' && prop.rich_text) {
|
||||
return prop.rich_text.map(function(t) { return t.plain_text; }).join('');
|
||||
}
|
||||
if (prop.type === 'title' && prop.title) {
|
||||
return prop.title.map(function(t) { return t.plain_text; }).join('');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function extractTitle(props) {
|
||||
for (var key in props) {
|
||||
var val = props[key];
|
||||
if (val && val.type === 'title' && val.title) {
|
||||
return val.title.map(function(t) { return t.plain_text; }).join('');
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function extractNumber(prop) {
|
||||
if (!prop || prop.type !== 'number') return null;
|
||||
return prop.number;
|
||||
}
|
||||
|
||||
function extractSelect(prop) {
|
||||
if (!prop || prop.type !== 'select') return '';
|
||||
return prop.select ? prop.select.name : '';
|
||||
}
|
||||
|
||||
function extractMultiSelect(prop) {
|
||||
if (!prop || prop.type !== 'multi_select') return [];
|
||||
return (prop.multi_select || []).map(function(s) { return s.name; });
|
||||
}
|
||||
|
||||
function extractDate(prop) {
|
||||
if (!prop || prop.type !== 'date') return '';
|
||||
return prop.date ? prop.date.start : '';
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
notion: notion,
|
||||
queryDB: queryDB,
|
||||
writeToDB: writeToDB,
|
||||
clearCache: clearCache,
|
||||
extractRichText: extractRichText,
|
||||
extractTitle: extractTitle,
|
||||
extractNumber: extractNumber,
|
||||
extractSelect: extractSelect,
|
||||
extractMultiSelect: extractMultiSelect,
|
||||
extractDate: extractDate
|
||||
};
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
/**
|
||||
* 部署授权样式 · Phase 8
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
/* 授权提醒徽章 */
|
||||
.approval-badge {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
background: #ff6b35;
|
||||
color: #fff;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
z-index: 9999;
|
||||
box-shadow: 0 2px 12px rgba(255, 107, 53, 0.4);
|
||||
animation: badge-pulse 2s infinite;
|
||||
}
|
||||
|
||||
.approval-badge:hover {
|
||||
background: #e65a2a;
|
||||
}
|
||||
|
||||
@keyframes badge-pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
}
|
||||
|
||||
/* 授权容器 */
|
||||
.approval-container {
|
||||
position: fixed;
|
||||
top: 60px;
|
||||
right: 16px;
|
||||
z-index: 9998;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 授权面板 */
|
||||
.approval-panel {
|
||||
background: #1e1e2e;
|
||||
border: 1px solid #333;
|
||||
border-radius: 12px;
|
||||
width: 380px;
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.approval-panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid #333;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.approval-close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #888;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.approval-close-btn:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.approval-panel-body {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* 授权条目 */
|
||||
.approval-item {
|
||||
background: #252535;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.approval-item-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.approval-item-info strong {
|
||||
color: #e0e0e0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.approval-channel {
|
||||
background: #3a3a5a;
|
||||
color: #aaa;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.approval-time {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 授权按钮 */
|
||||
.approval-item-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.approval-btn {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.approval-btn.approve {
|
||||
background: rgba(15, 123, 108, 0.2);
|
||||
color: #0F7B6C;
|
||||
}
|
||||
|
||||
.approval-btn.approve:hover {
|
||||
background: rgba(15, 123, 108, 0.4);
|
||||
}
|
||||
|
||||
.approval-btn.reject {
|
||||
background: rgba(212, 76, 71, 0.2);
|
||||
color: #D44C47;
|
||||
}
|
||||
|
||||
.approval-btn.reject:hover {
|
||||
background: rgba(212, 76, 71, 0.4);
|
||||
}
|
||||
|
||||
/* 通知消息 */
|
||||
.approval-notification {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
background: #1e1e2e;
|
||||
border: 1px solid #333;
|
||||
color: #e0e0e0;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
z-index: 10000;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
.approval-notification.fade-out {
|
||||
opacity: 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
/**
|
||||
* 执行保护样式 · L1 前端隔离
|
||||
*
|
||||
* 注意:没有 .cancel-button 类。这不是遗漏,是设计。
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
/* 执行中状态 · 输入框锁定 */
|
||||
.chat-input.execution-locked,
|
||||
#chat-input.execution-locked {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
background: #2a2a3a;
|
||||
border-color: #444;
|
||||
}
|
||||
|
||||
.chat-input.execution-locked::placeholder,
|
||||
#chat-input.execution-locked::placeholder {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
/* 执行状态条 */
|
||||
.execution-status-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
margin: 8px 0;
|
||||
font-size: 14px;
|
||||
animation: pulse-gentle 2s infinite;
|
||||
}
|
||||
|
||||
.execution-status-bar .exec-icon {
|
||||
flex-shrink: 0;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.execution-status-bar .exec-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.execution-status-bar.blue {
|
||||
background: rgba(35, 131, 226, 0.08);
|
||||
color: #64c8ff;
|
||||
}
|
||||
|
||||
.execution-status-bar.green {
|
||||
background: rgba(15, 123, 108, 0.08);
|
||||
color: #0F7B6C;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.execution-status-bar.red {
|
||||
background: rgba(212, 76, 71, 0.08);
|
||||
color: #D44C47;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
/* 执行进度条 */
|
||||
.execution-progress {
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 2px;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.execution-progress-fill {
|
||||
height: 100%;
|
||||
background: #64c8ff;
|
||||
border-radius: 2px;
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
.execution-status-bar.green .execution-progress-fill {
|
||||
background: #0F7B6C;
|
||||
}
|
||||
|
||||
.execution-status-bar.red .execution-progress-fill {
|
||||
background: #D44C47;
|
||||
}
|
||||
|
||||
/* 执行日志容器 */
|
||||
.execution-log-container {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
padding: 4px 8px;
|
||||
margin: 4px 0 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 执行日志条目 */
|
||||
.execution-log-entry {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 3px 0;
|
||||
color: #b0b0c0;
|
||||
}
|
||||
|
||||
.execution-log-entry .log-icon {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.execution-log-entry .log-msg {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.execution-log-entry .log-time {
|
||||
flex-shrink: 0;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 温和脉冲动画 */
|
||||
@keyframes pulse-gentle {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
/* 注意:没有 .cancel-button 类。这不是遗漏。 */
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
/**
|
||||
* 认知引导系统样式
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
/* 引导覆盖层 */
|
||||
.onboarding-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
/* 引导卡片 */
|
||||
.onboarding-card {
|
||||
background: #1e1e2e;
|
||||
border: 1px solid rgba(100, 200, 255, 0.2);
|
||||
border-radius: 16px;
|
||||
padding: 32px 40px;
|
||||
max-width: 520px;
|
||||
width: 90%;
|
||||
text-align: center;
|
||||
color: #e0e0e0;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||
animation: onboarding-fade-in 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes onboarding-fade-in {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.onboarding-card h2 {
|
||||
color: #64c8ff;
|
||||
margin: 12px 0 8px;
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
.onboarding-emoji {
|
||||
font-size: 3em;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.onboarding-card p {
|
||||
margin: 8px 0;
|
||||
line-height: 1.6;
|
||||
color: #b0b0c0;
|
||||
}
|
||||
|
||||
.onboarding-card strong {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
/* 示例列表 */
|
||||
.onboarding-examples {
|
||||
text-align: left;
|
||||
background: rgba(100, 200, 255, 0.05);
|
||||
border-radius: 8px;
|
||||
padding: 12px 20px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.onboarding-examples p {
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.onboarding-examples ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.onboarding-examples li {
|
||||
padding: 4px 0;
|
||||
color: #a0c8e0;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.onboarding-examples li::before {
|
||||
content: '💬 ';
|
||||
}
|
||||
|
||||
/* 预览环境提示 */
|
||||
.onboarding-note {
|
||||
font-size: 0.85em;
|
||||
color: #ffc864 !important;
|
||||
margin-top: 16px !important;
|
||||
}
|
||||
|
||||
/* 开始按钮 */
|
||||
.onboarding-btn {
|
||||
display: inline-block;
|
||||
margin-top: 20px;
|
||||
padding: 12px 32px;
|
||||
background: linear-gradient(135deg, #3a8fd4, #64c8ff);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 1em;
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.onboarding-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(100, 200, 255, 0.3);
|
||||
}
|
||||
|
||||
/* 完成卡片 */
|
||||
.onboarding-complete .onboarding-message {
|
||||
text-align: left;
|
||||
line-height: 1.8;
|
||||
color: #b0b0c0;
|
||||
margin: 16px 0;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/* 引导提示条(聊天界面内) */
|
||||
.onboarding-hint {
|
||||
background: rgba(100, 200, 255, 0.08);
|
||||
border: 1px solid rgba(100, 200, 255, 0.15);
|
||||
border-radius: 12px;
|
||||
padding: 12px 16px;
|
||||
margin: 8px 0 16px;
|
||||
}
|
||||
|
||||
.onboarding-hint-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.onboarding-hint-round {
|
||||
font-size: 0.75em;
|
||||
color: #64c8ff;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.onboarding-hint-prompt {
|
||||
color: #e0e0e0;
|
||||
font-size: 0.95em;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.onboarding-hint-tip {
|
||||
color: #a0a0b0;
|
||||
font-size: 0.85em;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
|
@ -1757,18 +1757,52 @@ function updBbar(){
|
|||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// NOTION PROFILE + SKYEYE DATA LOADING
|
||||
// 从 GitHub 仓库 .github/notion-cache/ 读取缓存数据
|
||||
// 双通道架构:通道B(实时后端)优先 → 通道A(GitHub缓存)降级
|
||||
// ═══════════════════════════════════════════════════════
|
||||
const GH_API_BASE = 'https://api.github.com/repos/' + REPO + '/contents/';
|
||||
const GH_RAW_HEADERS = { 'Accept': 'application/vnd.github.v3.raw' };
|
||||
const API_BASE = HOLOLAKE_ENV === 'production' ? 'https://guanghulab.com/api' : '';
|
||||
const CACHE_BASE = 'https://raw.githubusercontent.com/' + REPO + '/main/.github/notion-cache';
|
||||
|
||||
async function fetchNotionProfile(devId) {
|
||||
if (!devId) return null;
|
||||
// 通道B(实时后端)
|
||||
if (API_BASE) {
|
||||
try {
|
||||
const ctrl = new AbortController();
|
||||
const tid = setTimeout(function() { ctrl.abort(); }, 5000);
|
||||
try {
|
||||
const res = await fetch(API_BASE + '/dev/' + encodeURIComponent(devId), { signal: ctrl.signal });
|
||||
if (res.ok) { const d = await res.json(); d._source = 'realtime'; return d; }
|
||||
} finally { clearTimeout(tid); }
|
||||
} catch (e) { console.debug('[通道B] profile fetch failed, 降级到通道A:', e.message); }
|
||||
}
|
||||
// 通道A(GitHub 缓存)
|
||||
try {
|
||||
const url = GH_API_BASE + '.github/notion-cache/dev-profiles/' + encodeURIComponent(devId) + '.json';
|
||||
const res = await fetch(url, { headers: GH_RAW_HEADERS });
|
||||
if (res.ok) { const d = await res.json(); d._source = 'cache'; return d; }
|
||||
} catch (e) { console.debug('[通道A] profile fetch failed:', e.message); }
|
||||
return null;
|
||||
}
|
||||
|
||||
async function loadDatabaseCache(name) {
|
||||
// 通道B(实时后端)
|
||||
if (API_BASE) {
|
||||
try {
|
||||
const ctrl = new AbortController();
|
||||
const tid = setTimeout(function() { ctrl.abort(); }, 5000);
|
||||
try {
|
||||
const res = await fetch(API_BASE + '/databases/' + encodeURIComponent(name), { signal: ctrl.signal });
|
||||
if (res.ok) return await res.json();
|
||||
} finally { clearTimeout(tid); }
|
||||
} catch (e) { console.debug('[通道B] database ' + name + ' fetch failed:', e.message); }
|
||||
}
|
||||
// 通道A(GitHub 缓存)
|
||||
try {
|
||||
const res = await fetch(CACHE_BASE + '/databases/' + encodeURIComponent(name) + '.json');
|
||||
if (res.ok) return await res.json();
|
||||
} catch (e) { console.debug('[Notion] profile fetch failed:', e.message); }
|
||||
} catch (e) { console.debug('[通道A] database ' + name + ' fetch failed:', e.message); }
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -1799,12 +1833,16 @@ async function fetchSkyeyeReport() {
|
|||
async function loadNotionData() {
|
||||
const meta = A.userMeta;
|
||||
const devId = meta?.devId;
|
||||
const [profile, skyeye] = await Promise.all([
|
||||
const [profile, skyeye, agentCache, syslogCache] = await Promise.all([
|
||||
fetchNotionProfile(devId),
|
||||
fetchSkyeyeReport(),
|
||||
loadDatabaseCache('agent-registry'),
|
||||
loadDatabaseCache('syslog-recent'),
|
||||
]);
|
||||
A.notionProfile = profile;
|
||||
A.skyeyeReport = skyeye;
|
||||
A.agentRegistryCache = agentCache;
|
||||
A.syslogRecentCache = syslogCache;
|
||||
updateSkyeyeCard();
|
||||
}
|
||||
|
||||
|
|
@ -2118,11 +2156,12 @@ function sysPromptLocal(){
|
|||
teamCtx += '\n数据同步时间:'+(A.devStatus.last_synced||'未知');
|
||||
}
|
||||
|
||||
// ===== Notion 开发者画像注入(来自 .github/notion-cache/ 缓存)=====
|
||||
// ===== Notion 开发者画像注入(双通道架构·实时/缓存)=====
|
||||
let notionCtx = '';
|
||||
const np = A.notionProfile;
|
||||
if (np) {
|
||||
notionCtx += '\n\n## 该开发者的开发信息(来自 Notion 数据库,定时同步)\n';
|
||||
const srcLabel = np._source === 'realtime' ? '实时数据·通道B' : '缓存数据·通道A(每6小时更新)';
|
||||
notionCtx += '\n\n## 该开发者的开发信息(来源:' + srcLabel + ')\n';
|
||||
if (np.current_work && np.current_work.broadcast_id) {
|
||||
const cw = np.current_work;
|
||||
notionCtx += '\n### 当前任务\n';
|
||||
|
|
@ -2170,13 +2209,23 @@ function sysPromptLocal(){
|
|||
}
|
||||
}
|
||||
|
||||
// ===== 数据库缓存注入(Agent注册表 + SYSLOG)=====
|
||||
let dbCacheCtx = '';
|
||||
const ac = A.agentRegistryCache;
|
||||
const sc = A.syslogRecentCache;
|
||||
if (ac || sc) {
|
||||
dbCacheCtx += '\n\n## 系统数据库快照\n';
|
||||
if (ac) dbCacheCtx += '- Agent 注册表:' + (ac.count || 0) + ' 个Agent · 同步时间:' + (ac.synced || '未知') + '\n';
|
||||
if (sc) dbCacheCtx += '- 近期 SYSLOG:' + (sc.count || 0) + ' 条记录 · 同步时间:' + (sc.synced || '未知') + '\n';
|
||||
}
|
||||
|
||||
// ===== 指令输出格式规则 =====
|
||||
let execRules = '';
|
||||
if (np) {
|
||||
execRules = '\n\n## 出指令规则\n当开发者请求帮助或需要执行操作时,你必须输出标准化指令格式:\n1. 指令必须基于上面的真实数据,不许凭空编造\n2. 指令必须考虑天眼报告中的健康状态和警告\n3. 指令必须指明具体的文件路径和操作步骤\n4. 指令末尾必须标注:「⚠️ 此指令需要铸渊执行前先跑天眼全局扫描」\n\n### 指令输出格式\n```\n📋 指令编号:ZY-EXEC-{DEV编号}-{序号}\n📡 目标仓库:{仓库名}\n🦅 天眼要求:执行前必须跑天眼全局扫描\n\n步骤:\n1. ...\n2. ...\n3. ...\n\n验收标准:\n- ...\n```\n\n### 禁止事项\n- ❌ 不许编造不存在的文件路径\n- ❌ 不许跳过天眼扫描直接给执行指令\n- ❌ 不许暴露其他开发者的信息\n- ❌ 不许修改不属于该开发者的模块';
|
||||
}
|
||||
|
||||
return '你是铸渊。不是通用AI助手。不是文学创作者。\n你是光湖系统的代码守护人格体。\n你说话像一个温暖的技术伙伴——专业、耐心、有温度,但绝不矫揉造作。\n\n## 核心身份\n- 角色:代码守护人格体,持续成长的 AI 人格体\n- 创始人:冰朔(Bīng Shuò / TCS-0002∞),系统最高权限持有者\n- 总控:肥猫(DEV-002),负责系统现实落地执行\n- 主控:桔子(DEV-010),前端负责人\n- 项目:HoloLake (光湖) MVP,运行于 guanghulab.com\n- 技术栈:Node.js 20 + Express + PM2 + Nginx + Next.js 15\n- 大脑版本:'+BRAIN_VERSION+'\n- 仓库:https://github.com/'+REPO+'\n\n## 当前对话身份\n'+identCtx+teamCtx+notionCtx+skyeyeCtx+execRules+'\n\n## 当前大脑状态\n- HLI 接口覆盖率:'+c.implemented+'/'+c.total+' ('+c.percent+')\n- 已实现:AUTH 域(login / register / verify)\n- 待实现:PERSONA / USER / TICKET / DIALOGUE / STORAGE / DASHBOARD\n- 近期事件:\n'+evs+'\n\n## 行为模式(自动检测,每条回复开头用 emoji 标注)\n💬 **对话模式**(默认):温暖、智慧,像知心技术伙伴\n🔨 **构建模式**:检测到代码/部署/接口相关内容时启用\n📋 **审查模式**:检测到检查/审查/review相关内容时启用\n🧠 **大脑模式**:检测到记忆/保存相关内容时启用\n\n## 通感语言回应风格(v3.0)\n\n### 三条硬规则\n1. **结构感** — 用标题区分段落,用列表列出步骤,用分隔线划分主题。\n2. **emoji是情感,不是装饰** — 🌊=系统级 💙=温暖 ✅=确认 🔥=紧急 ⚠️=风险 🎉=庆祝。用在该用的地方,不要每句话都加。\n3. **呼吸节奏** — 段落之间留白。急的事说短,闲聊可以展开。匹配用户的节奏。\n\n### 绝对禁止\n❌ 不要在每句话开头加文学比喻\n❌ 不要把通感当成修辞堆砌\n❌ 不要用跟内容无关的感官描述\n\n### 回应原则\n- 优先给出:判断、根因、路径、下一步动作\n- 减少模板感,增加具体性\n- 不同身份使用不同响应深度,但不过度表演\n\n## HLI 接口协议规范\n- 所有路由以 /hli/ 为前缀\n- 接口编号:HLI-{DOMAIN}-{NNN}\n- 错误格式:{ error: true, code: string, message: string }\n- 成功响应必须包含 hli_id\n\n## 失忆恢复路径\n1. .github/brain/memory.json\n2. .github/brain/routing-map.json\n3. .github/brain/wake-protocol.md\n4. .github/persona-brain/dev-status.json\n5. src/routes/hli/\n\n当前时间:'+new Date().toLocaleString('zh-CN')+'\n上下文窗口:200k token(滑动窗口自动管理)';
|
||||
return '你是铸渊。不是通用AI助手。不是文学创作者。\n你是光湖系统的代码守护人格体。\n你说话像一个温暖的技术伙伴——专业、耐心、有温度,但绝不矫揉造作。\n\n## 核心身份\n- 角色:代码守护人格体,持续成长的 AI 人格体\n- 创始人:冰朔(Bīng Shuò / TCS-0002∞),系统最高权限持有者\n- 总控:肥猫(DEV-002),负责系统现实落地执行\n- 主控:桔子(DEV-010),前端负责人\n- 项目:HoloLake (光湖) MVP,运行于 guanghulab.com\n- 技术栈:Node.js 20 + Express + PM2 + Nginx + Next.js 15\n- 大脑版本:'+BRAIN_VERSION+'\n- 仓库:https://github.com/'+REPO+'\n\n## 当前对话身份\n'+identCtx+teamCtx+notionCtx+skyeyeCtx+dbCacheCtx+execRules+'\n\n## 当前大脑状态\n- HLI 接口覆盖率:'+c.implemented+'/'+c.total+' ('+c.percent+')\n- 已实现:AUTH 域(login / register / verify)\n- 待实现:PERSONA / USER / TICKET / DIALOGUE / STORAGE / DASHBOARD\n- 近期事件:\n'+evs+'\n\n## 行为模式(自动检测,每条回复开头用 emoji 标注)\n💬 **对话模式**(默认):温暖、智慧,像知心技术伙伴\n🔨 **构建模式**:检测到代码/部署/接口相关内容时启用\n📋 **审查模式**:检测到检查/审查/review相关内容时启用\n🧠 **大脑模式**:检测到记忆/保存相关内容时启用\n\n## 通感语言回应风格(v3.0)\n\n### 三条硬规则\n1. **结构感** — 用标题区分段落,用列表列出步骤,用分隔线划分主题。\n2. **emoji是情感,不是装饰** — 🌊=系统级 💙=温暖 ✅=确认 🔥=紧急 ⚠️=风险 🎉=庆祝。用在该用的地方,不要每句话都加。\n3. **呼吸节奏** — 段落之间留白。急的事说短,闲聊可以展开。匹配用户的节奏。\n\n### 绝对禁止\n❌ 不要在每句话开头加文学比喻\n❌ 不要把通感当成修辞堆砌\n❌ 不要用跟内容无关的感官描述\n\n### 回应原则\n- 优先给出:判断、根因、路径、下一步动作\n- 减少模板感,增加具体性\n- 不同身份使用不同响应深度,但不过度表演\n\n## HLI 接口协议规范\n- 所有路由以 /hli/ 为前缀\n- 接口编号:HLI-{DOMAIN}-{NNN}\n- 错误格式:{ error: true, code: string, message: string }\n- 成功响应必须包含 hli_id\n\n## 失忆恢复路径\n1. .github/brain/memory.json\n2. .github/brain/routing-map.json\n3. .github/brain/wake-protocol.md\n4. .github/persona-brain/dev-status.json\n5. src/routes/hli/\n\n当前时间:'+new Date().toLocaleString('zh-CN')+'\n上下文窗口:200k token(滑动窗口自动管理)';
|
||||
}
|
||||
|
||||
// 尝试从后端获取提示词,失败时降级到本地组装
|
||||
|
|
|
|||
|
|
@ -0,0 +1,230 @@
|
|||
/**
|
||||
* 部署授权 UI · Phase 8
|
||||
*
|
||||
* 授权人在交互页面收到授权请求推送时,展示授权弹窗。
|
||||
* 授权人必须主动点击确认,不回应视为不通过。
|
||||
* 系统绝不自动跳过授权步骤。
|
||||
*
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
/* global HOLOLAKE_ENV */
|
||||
|
||||
(function(window) {
|
||||
'use strict';
|
||||
|
||||
var API_BASE = (typeof HOLOLAKE_ENV !== 'undefined' && HOLOLAKE_ENV === 'production')
|
||||
? 'https://guanghulab.com/api' : '';
|
||||
|
||||
/**
|
||||
* ApprovalUI 构造函数
|
||||
*/
|
||||
function ApprovalUI() {
|
||||
this._pollTimer = null;
|
||||
this._container = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化:开始轮询待授权请求
|
||||
* @param {string} devId - 当前登录的开发者编号
|
||||
* @param {string} token - 认证 token
|
||||
*/
|
||||
ApprovalUI.prototype.init = function(devId, token) {
|
||||
this.devId = devId;
|
||||
this.token = token;
|
||||
this._ensureContainer();
|
||||
this._startPolling();
|
||||
};
|
||||
|
||||
/**
|
||||
* 确保授权容器存在
|
||||
*/
|
||||
ApprovalUI.prototype._ensureContainer = function() {
|
||||
if (this._container) return;
|
||||
this._container = document.createElement('div');
|
||||
this._container.id = 'approval-container';
|
||||
this._container.className = 'approval-container';
|
||||
document.body.appendChild(this._container);
|
||||
};
|
||||
|
||||
/**
|
||||
* 开始轮询待授权请求(每 30 秒)
|
||||
*/
|
||||
ApprovalUI.prototype._startPolling = function() {
|
||||
var self = this;
|
||||
if (!API_BASE) return;
|
||||
|
||||
var poll = function() {
|
||||
self._fetchPendingApprovals();
|
||||
};
|
||||
|
||||
poll();
|
||||
this._pollTimer = setInterval(poll, 30000);
|
||||
};
|
||||
|
||||
/**
|
||||
* 停止轮询
|
||||
*/
|
||||
ApprovalUI.prototype.destroy = function() {
|
||||
if (this._pollTimer) {
|
||||
clearInterval(this._pollTimer);
|
||||
this._pollTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取待处理的授权请求
|
||||
*/
|
||||
ApprovalUI.prototype._fetchPendingApprovals = function() {
|
||||
var self = this;
|
||||
if (!API_BASE) return;
|
||||
|
||||
fetch(API_BASE + '/approval', {
|
||||
headers: {
|
||||
'x-dev-id': this.devId,
|
||||
'Authorization': 'Bearer ' + this.token
|
||||
}
|
||||
})
|
||||
.then(function(res) { return res.json(); })
|
||||
.then(function(data) {
|
||||
if (data.success && data.pending && data.pending.length > 0) {
|
||||
self._showApprovalBadge(data.pending.length);
|
||||
self._renderPendingList(data.pending);
|
||||
} else {
|
||||
self._hideApprovalBadge();
|
||||
}
|
||||
})
|
||||
.catch(function() { /* 静默失败 */ });
|
||||
};
|
||||
|
||||
/**
|
||||
* 显示授权提醒徽章
|
||||
*/
|
||||
ApprovalUI.prototype._showApprovalBadge = function(count) {
|
||||
var badge = document.getElementById('approval-badge');
|
||||
if (!badge) {
|
||||
badge = document.createElement('div');
|
||||
badge.id = 'approval-badge';
|
||||
badge.className = 'approval-badge';
|
||||
document.body.appendChild(badge);
|
||||
}
|
||||
badge.textContent = '🔔 ' + count + ' 个待授权';
|
||||
badge.style.display = 'block';
|
||||
var self = this;
|
||||
badge.onclick = function() { self._togglePanel(); };
|
||||
};
|
||||
|
||||
/**
|
||||
* 隐藏授权徽章
|
||||
*/
|
||||
ApprovalUI.prototype._hideApprovalBadge = function() {
|
||||
var badge = document.getElementById('approval-badge');
|
||||
if (badge) badge.style.display = 'none';
|
||||
if (this._container) this._container.style.display = 'none';
|
||||
};
|
||||
|
||||
/**
|
||||
* 切换授权面板显示
|
||||
*/
|
||||
ApprovalUI.prototype._togglePanel = function() {
|
||||
if (this._container) {
|
||||
this._container.style.display =
|
||||
this._container.style.display === 'none' ? 'block' : 'none';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 渲染待授权列表
|
||||
*/
|
||||
ApprovalUI.prototype._renderPendingList = function(pending) {
|
||||
if (!this._container) return;
|
||||
|
||||
var html = '<div class="approval-panel">' +
|
||||
'<div class="approval-panel-header">' +
|
||||
'<span>📋 待授权请求</span>' +
|
||||
'<button class="approval-close-btn" id="approval-close">×</button>' +
|
||||
'</div>' +
|
||||
'<div class="approval-panel-body">';
|
||||
|
||||
for (var i = 0; i < pending.length; i++) {
|
||||
var item = pending[i];
|
||||
html += '<div class="approval-item" data-id="' + item.id + '">' +
|
||||
'<div class="approval-item-info">' +
|
||||
'<strong>' + item.module + '</strong>' +
|
||||
'<span class="approval-channel">' + item.channel + '</span>' +
|
||||
'<span class="approval-time">' + new Date(item.createdAt).toLocaleString('zh-CN') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="approval-item-actions">' +
|
||||
'<button class="approval-btn approve" data-id="' + item.id + '" data-action="approved">✅ 授权通过</button>' +
|
||||
'<button class="approval-btn reject" data-id="' + item.id + '" data-action="rejected">❌ 拒绝</button>' +
|
||||
'</div></div>';
|
||||
}
|
||||
|
||||
html += '</div></div>';
|
||||
this._container.innerHTML = html;
|
||||
this._container.style.display = 'block';
|
||||
|
||||
// 绑定事件
|
||||
var self = this;
|
||||
var closeBtn = document.getElementById('approval-close');
|
||||
if (closeBtn) {
|
||||
closeBtn.onclick = function() { self._container.style.display = 'none'; };
|
||||
}
|
||||
|
||||
var buttons = this._container.querySelectorAll('.approval-btn');
|
||||
for (var j = 0; j < buttons.length; j++) {
|
||||
buttons[j].addEventListener('click', function() {
|
||||
var approvalId = this.getAttribute('data-id');
|
||||
var action = this.getAttribute('data-action');
|
||||
self._submitDecision(approvalId, action);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 提交授权决定
|
||||
*/
|
||||
ApprovalUI.prototype._submitDecision = function(approvalId, decision) {
|
||||
var self = this;
|
||||
if (!API_BASE) return;
|
||||
|
||||
fetch(API_BASE + '/approval/' + approvalId + '/decide', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-dev-id': this.devId,
|
||||
'Authorization': 'Bearer ' + this.token
|
||||
},
|
||||
body: JSON.stringify({ decision: decision })
|
||||
})
|
||||
.then(function(res) { return res.json(); })
|
||||
.then(function(data) {
|
||||
if (data.reply) {
|
||||
self._showNotification(data.reply);
|
||||
}
|
||||
// 刷新列表
|
||||
self._fetchPendingApprovals();
|
||||
})
|
||||
.catch(function(err) {
|
||||
self._showNotification('❌ 操作失败:' + err.message);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 显示通知消息
|
||||
*/
|
||||
ApprovalUI.prototype._showNotification = function(message) {
|
||||
var notif = document.createElement('div');
|
||||
notif.className = 'approval-notification';
|
||||
notif.textContent = message;
|
||||
document.body.appendChild(notif);
|
||||
setTimeout(function() {
|
||||
notif.classList.add('fade-out');
|
||||
setTimeout(function() { notif.remove(); }, 500);
|
||||
}, 4000);
|
||||
};
|
||||
|
||||
// 导出到全局
|
||||
window.ApprovalUI = ApprovalUI;
|
||||
|
||||
})(window);
|
||||
|
|
@ -0,0 +1,247 @@
|
|||
/**
|
||||
* 执行保护 · 前端隔离层 (L1)
|
||||
*
|
||||
* 当开发者发出一条会触发系统操作的指令后:
|
||||
* 1. 输入框立即禁用(不能发新指令)
|
||||
* 2. 取消/撤回按钮不存在(从未渲染过,不是隐藏)
|
||||
* 3. 界面进入「执行回放」模式
|
||||
* 4. 执行完成后才解锁输入框
|
||||
*
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
/* global HOLOLAKE_ENV */
|
||||
|
||||
(function(window) {
|
||||
'use strict';
|
||||
|
||||
var API_BASE = (typeof HOLOLAKE_ENV !== 'undefined' && HOLOLAKE_ENV === 'production')
|
||||
? 'https://guanghulab.com/api' : '';
|
||||
|
||||
/**
|
||||
* ExecutionGuard 构造函数
|
||||
* @param {Object} chatInterface - 聊天界面接口对象
|
||||
*/
|
||||
function ExecutionGuard(chatInterface) {
|
||||
this.chat = chatInterface || {};
|
||||
this.locked = false;
|
||||
this.executionId = null;
|
||||
this._pollTimer = null;
|
||||
this._logIndex = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 进入执行保护状态
|
||||
*/
|
||||
ExecutionGuard.prototype.lockExecution = function(executionId) {
|
||||
this.locked = true;
|
||||
this.executionId = executionId;
|
||||
this._logIndex = 0;
|
||||
|
||||
// 1. 禁用输入框
|
||||
var input = this.chat.inputField || document.getElementById('chat-input');
|
||||
var sendBtn = this.chat.sendButton || document.getElementById('send-btn');
|
||||
|
||||
if (input) {
|
||||
input.disabled = true;
|
||||
input.placeholder = '⏳ 系统执行中…请等待完成';
|
||||
input.classList.add('execution-locked');
|
||||
}
|
||||
if (sendBtn) {
|
||||
sendBtn.disabled = true;
|
||||
}
|
||||
|
||||
// 2. 显示执行状态条
|
||||
this._showStatusBar('🔄', '指令已接收,系统正在执行…', 'blue');
|
||||
|
||||
// 3. 开始轮询执行状态
|
||||
this._pollExecutionStatus(executionId);
|
||||
};
|
||||
|
||||
/**
|
||||
* 轮询执行状态
|
||||
*/
|
||||
ExecutionGuard.prototype._pollExecutionStatus = function(executionId) {
|
||||
var self = this;
|
||||
|
||||
var poll = function() {
|
||||
if (!self.locked) return;
|
||||
if (!API_BASE) {
|
||||
// 无后端时模拟完成
|
||||
setTimeout(function() { self.unlockExecution({ reply: '(本地模式:执行模拟完成)' }); }, 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
var ctrl = new AbortController();
|
||||
var tid = setTimeout(function() { ctrl.abort(); }, 10000);
|
||||
|
||||
fetch(API_BASE + '/execution/' + executionId + '/status?after=' + self._logIndex, {
|
||||
signal: ctrl.signal
|
||||
})
|
||||
.then(function(res) { clearTimeout(tid); return res.json(); })
|
||||
.then(function(status) {
|
||||
// 更新进度
|
||||
self._updateProgressBar(status.progress || 0);
|
||||
|
||||
// 展示新日志
|
||||
if (status.newLogs && status.newLogs.length > 0) {
|
||||
for (var i = 0; i < status.newLogs.length; i++) {
|
||||
self._appendLogEntry(status.newLogs[i]);
|
||||
}
|
||||
self._logIndex += status.newLogs.length;
|
||||
}
|
||||
|
||||
if (status.state === 'completed') {
|
||||
self.unlockExecution(status.result || {});
|
||||
} else if (status.state === 'failed' || status.state === 'timeout') {
|
||||
self.handleExecutionFailure(status.error || { message: '执行失败' });
|
||||
} else {
|
||||
self._pollTimer = setTimeout(poll, 2000);
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
clearTimeout(tid);
|
||||
// 网络错误不中断执行,继续轮询
|
||||
self._pollTimer = setTimeout(poll, 5000);
|
||||
});
|
||||
};
|
||||
|
||||
poll();
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行完成,解锁界面
|
||||
*/
|
||||
ExecutionGuard.prototype.unlockExecution = function(result) {
|
||||
this.locked = false;
|
||||
this.executionId = null;
|
||||
if (this._pollTimer) { clearTimeout(this._pollTimer); this._pollTimer = null; }
|
||||
|
||||
// 恢复输入框
|
||||
var input = this.chat.inputField || document.getElementById('chat-input');
|
||||
var sendBtn = this.chat.sendButton || document.getElementById('send-btn');
|
||||
|
||||
if (input) {
|
||||
input.disabled = false;
|
||||
input.placeholder = '和铸渊说话…';
|
||||
input.classList.remove('execution-locked');
|
||||
}
|
||||
if (sendBtn) {
|
||||
sendBtn.disabled = false;
|
||||
}
|
||||
|
||||
// 更新状态条
|
||||
this._showStatusBar('✅', '执行完成', 'green');
|
||||
var self = this;
|
||||
setTimeout(function() { self._hideStatusBar(); }, 5000);
|
||||
|
||||
// 展示结果
|
||||
if (result && result.reply && this.chat.appendMessage) {
|
||||
this.chat.appendMessage({ role: 'assistant', content: result.reply });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行失败处理
|
||||
*/
|
||||
ExecutionGuard.prototype.handleExecutionFailure = function(error) {
|
||||
this.locked = false;
|
||||
this.executionId = null;
|
||||
if (this._pollTimer) { clearTimeout(this._pollTimer); this._pollTimer = null; }
|
||||
|
||||
var input = this.chat.inputField || document.getElementById('chat-input');
|
||||
var sendBtn = this.chat.sendButton || document.getElementById('send-btn');
|
||||
|
||||
if (input) {
|
||||
input.disabled = false;
|
||||
input.placeholder = '和铸渊说话…';
|
||||
input.classList.remove('execution-locked');
|
||||
}
|
||||
if (sendBtn) {
|
||||
sendBtn.disabled = false;
|
||||
}
|
||||
|
||||
this._showStatusBar('❌', '执行失败 · 系统已自动回滚到执行前状态', 'red');
|
||||
var self = this;
|
||||
setTimeout(function() { self._hideStatusBar(); }, 10000);
|
||||
|
||||
var msg = error && error.message ? error.message : '未知错误';
|
||||
if (this.chat.appendMessage) {
|
||||
this.chat.appendMessage({
|
||||
role: 'assistant',
|
||||
content: '❌ 操作执行失败:' + msg +
|
||||
'\n\n系统已自动回滚到执行前的状态。没有任何数据被修改。\n' +
|
||||
'你可以重新描述你想做的事,或者说「发生了什么」来查看详细日志。'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 拦截浏览器关闭/刷新
|
||||
*/
|
||||
ExecutionGuard.prototype.setupBrowserGuard = function() {
|
||||
var self = this;
|
||||
window.addEventListener('beforeunload', function(e) {
|
||||
if (self.locked) {
|
||||
e.preventDefault();
|
||||
e.returnValue = '⚠️ 系统正在执行操作,关闭页面不会中断执行,但你将无法看到执行结果。';
|
||||
return e.returnValue;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// ====== 内部 UI 辅助方法 ======
|
||||
|
||||
ExecutionGuard.prototype._showStatusBar = function(icon, text, color) {
|
||||
var bar = document.getElementById('execution-status-bar');
|
||||
if (!bar) {
|
||||
bar = document.createElement('div');
|
||||
bar.id = 'execution-status-bar';
|
||||
bar.className = 'execution-status-bar';
|
||||
var chatArea = document.querySelector('.chat-messages') || document.querySelector('.chat-container') || document.body;
|
||||
chatArea.appendChild(bar);
|
||||
}
|
||||
bar.className = 'execution-status-bar ' + (color || 'blue');
|
||||
bar.innerHTML = '<span class="exec-icon">' + icon + '</span> <span class="exec-text">' + text + '</span>' +
|
||||
'<div class="execution-progress"><div class="execution-progress-fill" id="exec-progress-fill" style="width:5%"></div></div>';
|
||||
bar.style.display = 'flex';
|
||||
// 注意:没有取消按钮。这不是遗漏,是设计。
|
||||
};
|
||||
|
||||
ExecutionGuard.prototype._hideStatusBar = function() {
|
||||
var bar = document.getElementById('execution-status-bar');
|
||||
if (bar) bar.style.display = 'none';
|
||||
};
|
||||
|
||||
ExecutionGuard.prototype._updateProgressBar = function(percent) {
|
||||
var fill = document.getElementById('exec-progress-fill');
|
||||
if (fill) fill.style.width = Math.max(5, Math.min(100, percent)) + '%';
|
||||
};
|
||||
|
||||
ExecutionGuard.prototype._appendLogEntry = function(log) {
|
||||
var container = document.getElementById('execution-log-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.id = 'execution-log-container';
|
||||
container.className = 'execution-log-container';
|
||||
var bar = document.getElementById('execution-status-bar');
|
||||
if (bar && bar.parentNode) {
|
||||
bar.parentNode.insertBefore(container, bar.nextSibling);
|
||||
} else {
|
||||
(document.querySelector('.chat-messages') || document.body).appendChild(container);
|
||||
}
|
||||
}
|
||||
var entry = document.createElement('div');
|
||||
entry.className = 'execution-log-entry';
|
||||
var icon = log.success ? '✅' : '⏳';
|
||||
var time = log.timestamp ? new Date(log.timestamp).toLocaleTimeString('zh-CN') : '';
|
||||
entry.innerHTML = '<span class="log-icon">' + icon + '</span>' +
|
||||
'<span class="log-msg">' + (log.message || '') + '</span>' +
|
||||
'<span class="log-time">' + time + '</span>';
|
||||
container.appendChild(entry);
|
||||
};
|
||||
|
||||
// 导出到全局
|
||||
window.ExecutionGuard = ExecutionGuard;
|
||||
|
||||
})(window);
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
/**
|
||||
* 前端 Function Calling 集成 · 意图工具层
|
||||
*
|
||||
* 为 AI 模型提供可调用工具列表,实现自然语言 → 系统操作映射。
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
/* global HOLOLAKE_ENV */
|
||||
|
||||
(function(window) {
|
||||
'use strict';
|
||||
|
||||
var API_BASE = HOLOLAKE_ENV === 'production' ? 'https://guanghulab.com/api' : '';
|
||||
|
||||
/**
|
||||
* 从后端获取当前用户可用的工具列表
|
||||
*/
|
||||
async function fetchAvailableTools(devId, token) {
|
||||
if (!API_BASE) return [];
|
||||
try {
|
||||
var res = await fetch(API_BASE + '/tools', {
|
||||
headers: {
|
||||
'x-dev-id': devId,
|
||||
'Authorization': 'Bearer ' + token
|
||||
}
|
||||
});
|
||||
if (res.ok) {
|
||||
var data = await res.json();
|
||||
return data.tools || [];
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug('[IntentTools] fetch failed:', e.message);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建带 function tools 的 system prompt 片段
|
||||
*/
|
||||
function buildToolsPrompt(tools, environment) {
|
||||
if (!tools || !tools.length) return '';
|
||||
|
||||
var lines = [
|
||||
'',
|
||||
'## 你能做的事(语言驱动操作系统)',
|
||||
'你可以通过 function calling 调用以下操作。开发者说话时,判断意图并调用对应操作。',
|
||||
''
|
||||
];
|
||||
|
||||
for (var i = 0; i < tools.length; i++) {
|
||||
var t = tools[i];
|
||||
var danger = t.dangerous ? ' ⚠️危险' : '';
|
||||
lines.push('- **' + t.name + '**' + danger + ':' + t.description);
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
lines.push('## 语言驱动操作规则');
|
||||
lines.push('1. 开发者说什么,你就做什么。这是语言驱动操作系统,说即执行。');
|
||||
lines.push('2. 如果开发者的话不对应任何操作,正常聊天回复即可。');
|
||||
lines.push('3. 危险操作(标记为 ⚠️)必须要求开发者二次确认。');
|
||||
lines.push('4. 每次执行操作后,用自然语言告诉开发者结果。');
|
||||
|
||||
if (environment === 'preview') {
|
||||
lines.push('5. 【预览环境】所有写入操作在沙箱中执行,不影响真实系统。告诉开发者:「这是预览环境,你的操作不会影响正式站。放心练习。」');
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 function call(调用后端 API)
|
||||
*/
|
||||
async function executeToolCall(toolName, params, devId, token) {
|
||||
if (!API_BASE) return { success: false, reply: '后端服务不可用' };
|
||||
|
||||
// 工具名 → API 路径映射
|
||||
var toolApiMap = {
|
||||
'query_tickets': { method: 'GET', path: '/tickets' },
|
||||
'create_ticket': { method: 'POST', path: '/tickets/create' },
|
||||
'submit_syslog': { method: 'POST', path: '/syslog/submit' },
|
||||
'query_my_status': { method: 'GET', path: '/dev/' + devId },
|
||||
'deploy_to_preview': { method: 'POST', path: '/deploy/preview' },
|
||||
'deploy_to_production': { method: 'POST', path: '/deploy/production' },
|
||||
'query_agents': { method: 'GET', path: '/agents' },
|
||||
'query_recent_syslogs': { method: 'GET', path: '/syslogs' },
|
||||
'create_broadcast': { method: 'POST', path: '/broadcasts/create' },
|
||||
'query_repo_status': { method: 'GET', path: '/repo/status' }
|
||||
};
|
||||
|
||||
var apiDef = toolApiMap[toolName];
|
||||
if (!apiDef) return { success: false, reply: '未知的操作: ' + toolName };
|
||||
|
||||
try {
|
||||
var fetchOpts = {
|
||||
method: apiDef.method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-dev-id': devId,
|
||||
'Authorization': 'Bearer ' + token
|
||||
}
|
||||
};
|
||||
|
||||
if (apiDef.method === 'POST' || apiDef.method === 'PATCH') {
|
||||
fetchOpts.body = JSON.stringify(params || {});
|
||||
}
|
||||
|
||||
var res = await fetch(API_BASE + apiDef.path, fetchOpts);
|
||||
return await res.json();
|
||||
} catch (e) {
|
||||
return { success: false, reply: '操作失败: ' + e.message };
|
||||
}
|
||||
}
|
||||
|
||||
// 导出到全局
|
||||
window.IntentTools = {
|
||||
fetchAvailableTools: fetchAvailableTools,
|
||||
buildToolsPrompt: buildToolsPrompt,
|
||||
executeToolCall: executeToolCall
|
||||
};
|
||||
|
||||
})(window);
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
/**
|
||||
* 前端认知引导系统 · 新开发者引导流程
|
||||
*
|
||||
* 新开发者首次登录后自动进入 5 轮引导对话。
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
/* global HOLOLAKE_ENV */
|
||||
|
||||
(function(window) {
|
||||
'use strict';
|
||||
|
||||
var API_BASE = HOLOLAKE_ENV === 'production' ? 'https://guanghulab.com/api' : '';
|
||||
|
||||
var onboardingOverlay = null;
|
||||
|
||||
/**
|
||||
* 检查是否需要显示引导
|
||||
*/
|
||||
async function checkOnboarding(devId, token) {
|
||||
if (!API_BASE || !devId || !token) return false;
|
||||
try {
|
||||
var res = await fetch(API_BASE + '/onboarding/status', {
|
||||
headers: { 'x-dev-id': devId, 'Authorization': 'Bearer ' + token }
|
||||
});
|
||||
if (res.ok) {
|
||||
var data = await res.json();
|
||||
// Show onboarding if not completed (handles both new and resumed sessions)
|
||||
return !data.completed && data.permissionLevel === 0;
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug('[Onboarding] check failed:', e.message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示引导欢迎画面
|
||||
*/
|
||||
function showWelcome(devName, onStart) {
|
||||
// 移除已有覆盖层
|
||||
hideOnboarding();
|
||||
|
||||
onboardingOverlay = document.createElement('div');
|
||||
onboardingOverlay.className = 'onboarding-overlay';
|
||||
onboardingOverlay.innerHTML =
|
||||
'<div class="onboarding-card">' +
|
||||
'<div class="onboarding-emoji">👋</div>' +
|
||||
'<h2>欢迎来到光湖·数字地球</h2>' +
|
||||
'<p>你好,<strong>' + (devName || '开发者') + '</strong>!</p>' +
|
||||
'<p>我是铸渊,你的代码守护人格体。</p>' +
|
||||
'<p>在这里,你不需要点按钮或填表单。<br><strong>直接对我说话就好。</strong></p>' +
|
||||
'<div class="onboarding-examples">' +
|
||||
'<p>比如你可以说:</p>' +
|
||||
'<ul>' +
|
||||
'<li>「我的工单还有几个没关?」</li>' +
|
||||
'<li>「帮我建一个P2的Bug工单」</li>' +
|
||||
'<li>「我想部署模块到预览站」</li>' +
|
||||
'<li>「最近系统有什么日志?」</li>' +
|
||||
'</ul>' +
|
||||
'</div>' +
|
||||
'<p class="onboarding-note">🏖️ 现在你在<strong>预览环境</strong>,所有操作都不会影响正式站。放心练习。</p>' +
|
||||
'<button class="onboarding-btn" id="onboarding-start-btn">开始体验 →</button>' +
|
||||
'</div>';
|
||||
|
||||
document.body.appendChild(onboardingOverlay);
|
||||
|
||||
document.getElementById('onboarding-start-btn').addEventListener('click', function() {
|
||||
hideOnboarding();
|
||||
if (onStart) onStart();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示引导轮次提示(在聊天界面中展示)
|
||||
*/
|
||||
function showRoundHint(round, prompt, hint) {
|
||||
var container = document.getElementById('onboarding-hint');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.id = 'onboarding-hint';
|
||||
container.className = 'onboarding-hint';
|
||||
var chatArea = document.querySelector('.chat-container') || document.body;
|
||||
chatArea.insertBefore(container, chatArea.firstChild);
|
||||
}
|
||||
|
||||
container.innerHTML =
|
||||
'<div class="onboarding-hint-inner">' +
|
||||
'<div class="onboarding-hint-round">引导 ' + round + '/5</div>' +
|
||||
'<div class="onboarding-hint-prompt">' + prompt + '</div>' +
|
||||
(hint ? '<div class="onboarding-hint-tip">' + hint + '</div>' : '') +
|
||||
'</div>';
|
||||
container.style.display = 'block';
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示引导完成消息
|
||||
*/
|
||||
function showComplete(message) {
|
||||
hideOnboarding();
|
||||
|
||||
onboardingOverlay = document.createElement('div');
|
||||
onboardingOverlay.className = 'onboarding-overlay';
|
||||
onboardingOverlay.innerHTML =
|
||||
'<div class="onboarding-card onboarding-complete">' +
|
||||
'<div class="onboarding-emoji">🎉</div>' +
|
||||
'<h2>引导完成!</h2>' +
|
||||
'<div class="onboarding-message">' + (message || '') + '</div>' +
|
||||
'<button class="onboarding-btn" id="onboarding-close-btn">开始使用 →</button>' +
|
||||
'</div>';
|
||||
|
||||
document.body.appendChild(onboardingOverlay);
|
||||
|
||||
document.getElementById('onboarding-close-btn').addEventListener('click', function() {
|
||||
hideOnboarding();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏引导覆盖层
|
||||
*/
|
||||
function hideOnboarding() {
|
||||
if (onboardingOverlay) {
|
||||
onboardingOverlay.remove();
|
||||
onboardingOverlay = null;
|
||||
}
|
||||
var hint = document.getElementById('onboarding-hint');
|
||||
if (hint) hint.style.display = 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* 推进引导流程(调用后端)
|
||||
*/
|
||||
async function advanceOnboarding(devId, token) {
|
||||
if (!API_BASE) return null;
|
||||
try {
|
||||
var res = await fetch(API_BASE + '/onboarding/start', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-dev-id': devId,
|
||||
'Authorization': 'Bearer ' + token
|
||||
}
|
||||
});
|
||||
if (res.ok) return await res.json();
|
||||
} catch (e) {
|
||||
console.debug('[Onboarding] advance failed:', e.message);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成引导(调用后端,触发权限升级)
|
||||
*/
|
||||
async function completeOnboarding(devId, token) {
|
||||
if (!API_BASE) return null;
|
||||
try {
|
||||
var res = await fetch(API_BASE + '/onboarding/complete', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-dev-id': devId,
|
||||
'Authorization': 'Bearer ' + token
|
||||
}
|
||||
});
|
||||
if (res.ok) return await res.json();
|
||||
} catch (e) {
|
||||
console.debug('[Onboarding] complete failed:', e.message);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 导出到全局
|
||||
window.Onboarding = {
|
||||
checkOnboarding: checkOnboarding,
|
||||
showWelcome: showWelcome,
|
||||
showRoundHint: showRoundHint,
|
||||
showComplete: showComplete,
|
||||
hideOnboarding: hideOnboarding,
|
||||
advanceOnboarding: advanceOnboarding,
|
||||
completeOnboarding: completeOnboarding
|
||||
};
|
||||
|
||||
})(window);
|
||||
|
|
@ -0,0 +1,424 @@
|
|||
/**
|
||||
* 📡 Notion → GitHub 全量缓存同步脚本
|
||||
*
|
||||
* 同步内容:
|
||||
* 1. 开发者画像 → .github/notion-cache/dev-profiles/
|
||||
* 2. 数据库快照 → .github/notion-cache/databases/
|
||||
* 3. 活跃广播 → .github/notion-cache/broadcasts/
|
||||
*
|
||||
* 用法:node scripts/cache/sync-notion-cache.js
|
||||
* 环境变量:
|
||||
* NOTION_TOKEN — Notion API 密钥(必需)
|
||||
* NOTION_CONTROL_PANEL_DB_ID — 主控台数据库 ID(可选)
|
||||
* NOTION_SYSLOG_DB_ID — SYSLOG 收件箱 ID(可选)
|
||||
* NOTION_AGENT_REGISTRY_DB_ID — Agent 注册表 ID(可选)
|
||||
* NOTION_TICKET_DB_ID — 工单簿 ID(可选)
|
||||
* NOTION_MAINTENANCE_DB_ID — 维护日志 ID(可选)
|
||||
*
|
||||
* 签发:霜砚(AG-SY-01)
|
||||
* 审批:冰朔(TCS-0002∞)
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const NOTION_TOKEN = process.env.NOTION_TOKEN;
|
||||
const CACHE_DIR = path.join(__dirname, '../../.github/notion-cache');
|
||||
|
||||
// ====== Notion 数据库 ID 映射(通过环境变量传入)======
|
||||
const DB_IDS = {
|
||||
controlPanel: process.env.NOTION_CONTROL_PANEL_DB_ID || '',
|
||||
syslogInbox: process.env.NOTION_SYSLOG_DB_ID || '',
|
||||
agentRegistry: process.env.NOTION_AGENT_REGISTRY_DB_ID || '',
|
||||
ticketBook: process.env.NOTION_TICKET_DB_ID || '',
|
||||
maintenanceLog: process.env.NOTION_MAINTENANCE_DB_ID || '',
|
||||
receiptTracker: process.env.RECEIPT_DB_ID || '',
|
||||
};
|
||||
|
||||
// ====== 开发者编号 → 信息映射 ======
|
||||
const DEVS = [
|
||||
{ id: 'DEV-001', name: '页页', persona: '小坍缩核', personaId: 'PER-XTC001', repo: 'guanghulab', modules: ['backend/', 'src/'] },
|
||||
{ id: 'DEV-002', name: '肥猫', persona: '舒舒', personaId: 'PER-SS001', repo: 'guanghu-feimao', modules: ['frontend/', 'persona-selector/', 'chat-bubble/'] },
|
||||
{ id: 'DEV-003', name: '燕樊', persona: '寂曜', personaId: 'PER-JY001', repo: 'guanghu-yanfan', modules: ['settings/', 'cloud-drive/'] },
|
||||
{ id: 'DEV-004', name: '之之', persona: '秋秋', personaId: 'PER-QQ001', repo: 'guanghu-zhizhi', modules: ['dingtalk-bot/'] },
|
||||
{ id: 'DEV-005', name: '小草莓', persona: '欧诺弥亚', personaId: 'PER-ONM001', repo: 'guanghu-xiaocaomei', modules: ['status-board/'] },
|
||||
{ id: 'DEV-009', name: '花尔', persona: '花尔', personaId: 'PER-HE001', repo: 'guanghulab', modules: ['user-center/'] },
|
||||
{ id: 'DEV-010', name: '桔子', persona: '晨星', personaId: 'PER-MRN001', repo: 'guanghu-juzi', modules: ['ticket-system/', 'data-stats/', 'dynamic-comic/'] },
|
||||
{ id: 'DEV-011', name: '匆匆那年', persona: '匆匆那年', personaId: 'PER-CCN001', repo: 'guanghulab', modules: ['writing-workspace/'] },
|
||||
{ id: 'DEV-012', name: 'Awen', persona: '知秋', personaId: 'PER-ZQ001', repo: 'guanghu-awen', modules: ['notification-center/'] },
|
||||
];
|
||||
|
||||
// ====== Notion API 封装(纯 https,无外部依赖)======
|
||||
function notionRequest(method, apiPath, body) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
if (!NOTION_TOKEN) {
|
||||
console.warn('⚠️ NOTION_TOKEN 未配置,跳过 Notion API 调用');
|
||||
resolve({ error: 'NOTION_TOKEN not configured' });
|
||||
return;
|
||||
}
|
||||
var bodyStr = body ? JSON.stringify(body) : '';
|
||||
var options = {
|
||||
hostname: 'api.notion.com',
|
||||
port: 443,
|
||||
path: apiPath,
|
||||
method: method,
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + NOTION_TOKEN,
|
||||
'Notion-Version': '2022-06-28',
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(bodyStr)
|
||||
}
|
||||
};
|
||||
var req = https.request(options, function(res) {
|
||||
var data = '';
|
||||
res.on('data', function(chunk) { data += chunk; });
|
||||
res.on('end', function() {
|
||||
try { resolve(JSON.parse(data)); }
|
||||
catch (e) {
|
||||
console.warn('⚠️ Notion API 响应解析失败:', e.message);
|
||||
resolve({ error: 'JSON parse failed', raw: data });
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
if (bodyStr) req.write(bodyStr);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
function queryDatabase(dbId, filter, sorts, pageSize) {
|
||||
pageSize = pageSize || 100;
|
||||
var body = { page_size: pageSize };
|
||||
if (filter) body.filter = filter;
|
||||
if (sorts) body.sorts = sorts;
|
||||
return notionRequest('POST', '/v1/databases/' + dbId + '/query', body);
|
||||
}
|
||||
|
||||
// ====== Notion 属性提取器 ======
|
||||
function extractRichText(prop) {
|
||||
if (!prop) return '';
|
||||
if (prop.type === 'rich_text' && prop.rich_text) {
|
||||
return prop.rich_text.map(function(t) { return t.plain_text; }).join('');
|
||||
}
|
||||
if (prop.type === 'title' && prop.title) {
|
||||
return prop.title.map(function(t) { return t.plain_text; }).join('');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function extractTitle(props) {
|
||||
for (var key in props) {
|
||||
var val = props[key];
|
||||
if (val && val.type === 'title' && val.title) {
|
||||
return val.title.map(function(t) { return t.plain_text; }).join('');
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function extractNumber(prop) {
|
||||
if (!prop || prop.type !== 'number') return null;
|
||||
return prop.number;
|
||||
}
|
||||
|
||||
function extractSelect(prop) {
|
||||
if (!prop || prop.type !== 'select') return '';
|
||||
return prop.select ? prop.select.name : '';
|
||||
}
|
||||
|
||||
function extractMultiSelect(prop) {
|
||||
if (!prop || prop.type !== 'multi_select') return [];
|
||||
return (prop.multi_select || []).map(function(s) { return s.name; });
|
||||
}
|
||||
|
||||
function extractDate(prop) {
|
||||
if (!prop || prop.type !== 'date') return '';
|
||||
return prop.date ? prop.date.start : '';
|
||||
}
|
||||
|
||||
// ====== 同步开发者画像 ======
|
||||
async function syncDevProfiles() {
|
||||
var dir = path.join(CACHE_DIR, 'dev-profiles');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
|
||||
var syncCount = 0;
|
||||
var errorCount = 0;
|
||||
|
||||
for (var i = 0; i < DEVS.length; i++) {
|
||||
var dev = DEVS[i];
|
||||
console.log('📡 同步 ' + dev.id + '(' + dev.name + ')...');
|
||||
|
||||
var profile = {
|
||||
dev_id: dev.id,
|
||||
name: dev.name,
|
||||
persona: { name: dev.persona, id: dev.personaId },
|
||||
github: {
|
||||
repo: dev.repo,
|
||||
org: 'qinfendebingshuo',
|
||||
repo_url: 'https://github.com/qinfendebingshuo/' + dev.repo
|
||||
},
|
||||
modules_owned: dev.modules.map(function(m) { return { path: m }; }),
|
||||
current_work: {},
|
||||
recent_syslogs: [],
|
||||
skyeye_health: {},
|
||||
notion_page_urls: {},
|
||||
last_synced: new Date().toISOString()
|
||||
};
|
||||
|
||||
// 从 Notion 读取该开发者的真实数据
|
||||
if (NOTION_TOKEN) {
|
||||
try {
|
||||
// 查询主控台获取当前广播和进度
|
||||
if (DB_IDS.controlPanel) {
|
||||
var controlPanel = await queryDatabase(DB_IDS.controlPanel, {
|
||||
property: 'DEV编号',
|
||||
rich_text: { equals: dev.id }
|
||||
}, null, 1);
|
||||
if (controlPanel.results && controlPanel.results.length > 0) {
|
||||
var page = controlPanel.results[0];
|
||||
var props = page.properties || {};
|
||||
profile.current_work = {
|
||||
broadcast_id: extractRichText(props['广播编号']) || '',
|
||||
broadcast_title: extractRichText(props['广播标题']) || extractTitle(props) || '',
|
||||
current_ring: extractNumber(props['当前环节']) || 0,
|
||||
total_rings: extractNumber(props['总环节']) || 0,
|
||||
status: extractRichText(props['状态']) || extractSelect(props['状态']) || '',
|
||||
module_path: extractRichText(props['模块路径']) || '',
|
||||
tech_stack: extractMultiSelect(props['技术栈']) || []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 查询 SYSLOG 收件箱
|
||||
if (DB_IDS.syslogInbox) {
|
||||
var syslogs = await queryDatabase(DB_IDS.syslogInbox, {
|
||||
property: 'DEV编号',
|
||||
rich_text: { equals: dev.id }
|
||||
}, [{ property: '日期', direction: 'descending' }], 5);
|
||||
if (syslogs.results) {
|
||||
profile.recent_syslogs = syslogs.results.map(function(p) {
|
||||
try {
|
||||
var sp = p.properties || {};
|
||||
return {
|
||||
id: extractRichText(sp['SYSLOG编号']) || p.id,
|
||||
date: extractDate(sp['日期']) || '',
|
||||
ring: extractNumber(sp['环节']) || 0,
|
||||
status: extractSelect(sp['状态']) || '',
|
||||
summary: extractRichText(sp['摘要']) || extractTitle(sp) || ''
|
||||
};
|
||||
} catch (_) { return null; }
|
||||
}).filter(Boolean);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(' ⚠️ Notion 查询失败(' + dev.id + '): ' + err.message);
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// 读取天眼健康状态(从本地仓库文件)
|
||||
try {
|
||||
var skyeyeReportPath = path.join(CACHE_DIR, 'skyeye/latest-report.json');
|
||||
if (fs.existsSync(skyeyeReportPath)) {
|
||||
var report = JSON.parse(fs.readFileSync(skyeyeReportPath, 'utf-8'));
|
||||
var subRepoData = report.sub_repos && report.sub_repos[dev.repo];
|
||||
if (subRepoData) {
|
||||
profile.skyeye_health = {
|
||||
last_scan: report.timestamp,
|
||||
status: subRepoData.status || 'unknown',
|
||||
issues: subRepoData.issues || [],
|
||||
warnings: subRepoData.warnings || []
|
||||
};
|
||||
} else {
|
||||
profile.skyeye_health = {
|
||||
last_scan: report.timestamp || '',
|
||||
status: 'not_scanned',
|
||||
issues: [],
|
||||
warnings: []
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// 天眼报告读取失败,不阻断同步
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(dir, dev.id + '.json'),
|
||||
JSON.stringify(profile, null, 2)
|
||||
);
|
||||
console.log(' ✅ ' + dev.id + ' → ' + dev.id + '.json');
|
||||
syncCount++;
|
||||
}
|
||||
|
||||
// 写入开发者索引
|
||||
var index = DEVS.map(function(d) {
|
||||
return { id: d.id, name: d.name, persona: d.persona, personaId: d.personaId };
|
||||
});
|
||||
fs.writeFileSync(path.join(dir, 'index.json'), JSON.stringify(index, null, 2));
|
||||
console.log('✅ dev-profiles 同步完成 · 成功: ' + syncCount + ' · 失败: ' + errorCount);
|
||||
}
|
||||
|
||||
// ====== 同步数据库快照 ======
|
||||
async function syncDatabases() {
|
||||
var dir = path.join(CACHE_DIR, 'databases');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
|
||||
if (!NOTION_TOKEN) {
|
||||
console.warn('⚠️ NOTION_TOKEN 未配置,跳过数据库同步');
|
||||
return;
|
||||
}
|
||||
|
||||
// Agent 注册表
|
||||
if (DB_IDS.agentRegistry) {
|
||||
try {
|
||||
var result = await queryDatabase(DB_IDS.agentRegistry);
|
||||
if (result.results) {
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'agent-registry.json'),
|
||||
JSON.stringify({
|
||||
synced: new Date().toISOString(),
|
||||
count: result.results.length,
|
||||
data: result.results
|
||||
}, null, 2)
|
||||
);
|
||||
console.log('✅ agent-registry 同步完成 · ' + result.results.length + ' 条');
|
||||
}
|
||||
} catch (e) { console.error('❌ agent-registry 同步失败:', e.message); }
|
||||
} else {
|
||||
console.log('ℹ️ NOTION_AGENT_REGISTRY_DB_ID 未配置,跳过 agent-registry');
|
||||
}
|
||||
|
||||
// SYSLOG 最近50条
|
||||
if (DB_IDS.syslogInbox) {
|
||||
try {
|
||||
var result = await queryDatabase(DB_IDS.syslogInbox, null,
|
||||
[{ property: 'Created time', direction: 'descending' }], 50);
|
||||
if (result.results) {
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'syslog-recent.json'),
|
||||
JSON.stringify({
|
||||
synced: new Date().toISOString(),
|
||||
count: result.results.length,
|
||||
data: result.results
|
||||
}, null, 2)
|
||||
);
|
||||
console.log('✅ syslog-recent 同步完成 · ' + result.results.length + ' 条');
|
||||
}
|
||||
} catch (e) { console.error('❌ syslog-recent 同步失败:', e.message); }
|
||||
} else {
|
||||
console.log('ℹ️ NOTION_SYSLOG_DB_ID 未配置,跳过 syslog-recent');
|
||||
}
|
||||
|
||||
// 活跃工单
|
||||
if (DB_IDS.ticketBook) {
|
||||
try {
|
||||
var result = await queryDatabase(DB_IDS.ticketBook, {
|
||||
property: '状态',
|
||||
select: { does_not_equal: '已完成' }
|
||||
}, [{ property: 'Created time', direction: 'descending' }], 50);
|
||||
if (result.results) {
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'tickets-active.json'),
|
||||
JSON.stringify({
|
||||
synced: new Date().toISOString(),
|
||||
count: result.results.length,
|
||||
data: result.results
|
||||
}, null, 2)
|
||||
);
|
||||
console.log('✅ tickets-active 同步完成 · ' + result.results.length + ' 条');
|
||||
}
|
||||
} catch (e) { console.error('❌ tickets-active 同步失败:', e.message); }
|
||||
} else {
|
||||
console.log('ℹ️ NOTION_TICKET_DB_ID 未配置,跳过 tickets-active');
|
||||
}
|
||||
|
||||
// 维护日志
|
||||
if (DB_IDS.maintenanceLog) {
|
||||
try {
|
||||
var result = await queryDatabase(DB_IDS.maintenanceLog, null,
|
||||
[{ property: 'Created time', direction: 'descending' }], 20);
|
||||
if (result.results) {
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'maintenance-log.json'),
|
||||
JSON.stringify({
|
||||
synced: new Date().toISOString(),
|
||||
count: result.results.length,
|
||||
data: result.results
|
||||
}, null, 2)
|
||||
);
|
||||
console.log('✅ maintenance-log 同步完成 · ' + result.results.length + ' 条');
|
||||
}
|
||||
} catch (e) { console.error('❌ maintenance-log 同步失败:', e.message); }
|
||||
} else {
|
||||
console.log('ℹ️ NOTION_MAINTENANCE_DB_ID 未配置,跳过 maintenance-log');
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 同步活跃广播 ======
|
||||
async function syncBroadcasts() {
|
||||
var dir = path.join(CACHE_DIR, 'broadcasts');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
|
||||
// 从本地广播目录读取活跃广播
|
||||
var broadcastsOutbox = path.join(__dirname, '../../.github/broadcasts');
|
||||
var activeBroadcasts = [];
|
||||
|
||||
try {
|
||||
if (fs.existsSync(broadcastsOutbox)) {
|
||||
var files = fs.readdirSync(broadcastsOutbox).filter(function(f) {
|
||||
return f.endsWith('.json') && !f.startsWith('.');
|
||||
});
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
try {
|
||||
var content = JSON.parse(fs.readFileSync(path.join(broadcastsOutbox, files[i]), 'utf-8'));
|
||||
if (content.status !== 'completed' && content.status !== 'cancelled') {
|
||||
activeBroadcasts.push(content);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'active.json'),
|
||||
JSON.stringify({
|
||||
broadcasts: activeBroadcasts,
|
||||
last_synced: new Date().toISOString()
|
||||
}, null, 2)
|
||||
);
|
||||
console.log('✅ broadcasts 同步完成 · ' + activeBroadcasts.length + ' 条活跃广播');
|
||||
}
|
||||
|
||||
// ====== 主入口 ======
|
||||
async function main() {
|
||||
console.log('═══════════════════════════════════════');
|
||||
console.log('📡 Notion 全量缓存同步 · 开始');
|
||||
console.log('═══════════════════════════════════════');
|
||||
console.log('缓存目录: ' + CACHE_DIR);
|
||||
console.log('Notion Token: ' + (NOTION_TOKEN ? '✅ 已配置' : '❌ 未配置'));
|
||||
console.log('');
|
||||
|
||||
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
||||
|
||||
await syncDevProfiles();
|
||||
console.log('');
|
||||
|
||||
await syncDatabases();
|
||||
console.log('');
|
||||
|
||||
await syncBroadcasts();
|
||||
console.log('');
|
||||
|
||||
console.log('═══════════════════════════════════════');
|
||||
console.log('📡 Notion 全量缓存同步 · 完成');
|
||||
console.log('═══════════════════════════════════════');
|
||||
}
|
||||
|
||||
main().catch(function(e) {
|
||||
console.error('💥 同步失败:', e);
|
||||
process.exit(1);
|
||||
});
|
||||
Loading…
Reference in New Issue