This commit is contained in:
Awen 2026-03-13 00:22:53 +08:00
commit fbfb4f1eb7
55 changed files with 5915 additions and 70 deletions

View File

@ -1,7 +1,7 @@
{
"version": "1.0",
"description": "冰朔主控问题索引库 — 记录已知问题、根因与排查路由",
"updated_at": "2026-03-12T02:08:35.412Z",
"updated_at": "2026-03-12T15:48:37.325Z",
"issues": [
{
"id": "BS-001",

View File

@ -1,7 +1,7 @@
# 冰朔主控神经系统 · 核心主控大脑 v1.0
> 本文件为冰朔主控神经系统的总控脑文件。
> 最后编译时间2026-03-12T02:08:35.412Z
> 最后编译时间2026-03-12T15:48:37.325Z
---
@ -56,7 +56,7 @@
### 仓库统计
- 功能模块10 个
- Workflow32
- Workflow35
---
@ -85,7 +85,7 @@
> 本区块由 master-brain-compiler 自动编译。
- **编译时间**2026-03-12T02:08:35.412Z
- **编译时间**2026-03-12T15:48:37.325Z
- **脑文件规则版本**v3.0
- **脑文件完整性**:✅ 完整
@ -107,7 +107,7 @@
|--------|------|------|
| 🟡 brain_consistency | yellow | 主仓库脑文件完整,但与 persona-studio 脑文件的同步状态待验证 |
| 🟢 deployment_health | green | deploy-to-server.yml 与 deploy-pages.yml 均存在 |
| 🟢 workflow_health | green | 32 个 workflow 已注册 |
| 🟢 workflow_health | green | 35 个 workflow 已注册 |
| 🟡 routing_health | yellow | HLI 接口覆盖率 33.3%7/21 |
| 🟢 docs_entry_health | green | docs/index.html 存在 |
| 🟡 persona_studio_health | yellow | 前后端结构存在,端到端对话链路待验证 |

View File

@ -1,7 +1,7 @@
{
"version": "1.0",
"description": "冰朔主控系统健康状态",
"updated_at": "2026-03-12T02:08:35.412Z",
"updated_at": "2026-03-12T15:48:37.325Z",
"health": {
"brain_consistency": {
"status": "yellow",
@ -13,7 +13,7 @@
},
"workflow_health": {
"status": "green",
"detail": "32 个 workflow 已注册"
"detail": "35 个 workflow 已注册"
},
"routing_health": {
"status": "yellow",

View File

@ -1,7 +1,7 @@
{
"identity": "铸渊Zhùyuān· GitHub 代码守护人格体",
"rules_version": "v3.0",
"last_updated": "2026-03-12T03:34:18.847Z",
"last_updated": "2026-03-12T08:55:54.205Z",
"wake_protocol_version": "v3.0",
"brain_version": "v3.0",
"architecture": {
@ -165,6 +165,14 @@
"actor": "qinfendebingshuo",
"ref": "refs/heads/main",
"run_id": "22944508401"
},
{
"timestamp": "2026-03-12T08:55:54.205Z",
"type": "daily_check",
"result": "passed",
"actor": "qinfendebingshuo",
"ref": "refs/heads/main",
"run_id": "22993977564"
}
]
}
}

View File

@ -1,20 +1,20 @@
{
"description": "铸渊图书馆目录 · Library Catalog for 铸渊 (Zhùyuān)",
"version": "2.0",
"generated_at": "2026-03-12T02:10:04.439Z",
"generated_at": "2026-03-12T15:48:50.779Z",
"generated_by": "scripts/generate-repo-map.js",
"repo": "qinfendebingshuo/guanghulab",
"stats": {
"zones": 13,
"total_modules": 10,
"total_workflows": 32,
"total_scripts": 27,
"total_workflows": 35,
"total_scripts": 32,
"total_dev_nodes": 8,
"hli_interfaces": 21,
"hli_implemented": 7,
"hli_coverage_pct": "33%",
"last_ci_run": "2026-03-05T16:07:24.070Z",
"memory_last_updated": "2026-03-11T08:55:43.347Z"
"memory_last_updated": "2026-03-12T08:55:54.205Z"
},
"zones": [
{
@ -119,6 +119,9 @@
{
"file": "knowledge-base.json"
},
{
"file": "logs"
},
{
"file": "memory.json"
},
@ -138,7 +141,7 @@
"file": "tcs-ml"
}
],
"item_count": 11
"item_count": 12
},
{
"zone_id": "WORKFLOWS",
@ -322,6 +325,20 @@
"manual"
]
},
{
"file": "push-broadcast.yml",
"name": "铸渊 · Push Broadcast · Notion → 飞书文档B",
"triggers": [
"manual"
]
},
{
"file": "receive-syslog.yml",
"name": "铸渊 · Receive SYSLOG · 飞书机器人 → GitHub → Notion",
"triggers": [
"unknown"
]
},
{
"file": "staging-preview.yml",
"name": "\"🔍 铸渊预演部署 (Staging Preview)\"",
@ -330,6 +347,13 @@
"manual"
]
},
{
"file": "sync-login-entry.yml",
"name": "铸渊 · Sync Login Entry · Notion → 飞书文档A",
"triggers": [
"manual"
]
},
{
"file": "sync-persona-studio.yml",
"name": "🔄 铸渊跨仓库同步 · persona-studio",
@ -411,7 +435,7 @@
]
}
],
"item_count": 32
"item_count": 35
},
{
"zone_id": "SCRIPTS",
@ -482,12 +506,27 @@
{
"file": "psp-inspection.js"
},
{
"file": "push-broadcast.js"
},
{
"file": "receive-syslog.js"
},
{
"file": "route-align-check.js"
},
{
"file": "save-collaboration-log.js"
},
{
"file": "selfcheck.js"
},
{
"file": "send-feishu-alert.js"
},
{
"file": "sync-login-entry.js"
},
{
"file": "update-brain.js"
},
@ -510,7 +549,7 @@
"file": "zhuyuan-module-protocol.js"
}
],
"item_count": 27
"item_count": 32
},
{
"zone_id": "SRC",
@ -871,6 +910,9 @@
{
"file": "README.md"
},
{
"file": "ai-system-doc.md"
},
{
"file": "index.html"
},
@ -878,7 +920,7 @@
"file": "使用指南.md"
}
],
"item_count": 5
"item_count": 6
},
{
"zone_id": "TESTS",
@ -1193,6 +1235,9 @@
"knowledge-base": [
"PERSONA_BRAIN::knowledge-base.json"
],
"logs": [
"PERSONA_BRAIN::logs"
],
"responsibility": [
"PERSONA_BRAIN::responsibility.md"
],
@ -1293,9 +1338,21 @@
"psp-daily-inspection": [
"WORKFLOWS::psp-daily-inspection.yml"
],
"push-broadcast": [
"WORKFLOWS::push-broadcast.yml",
"SCRIPTS::push-broadcast.js"
],
"receive-syslog": [
"WORKFLOWS::receive-syslog.yml",
"SCRIPTS::receive-syslog.js"
],
"staging-preview": [
"WORKFLOWS::staging-preview.yml"
],
"sync-login-entry": [
"WORKFLOWS::sync-login-entry.yml",
"SCRIPTS::sync-login-entry.js"
],
"sync-persona-studio": [
"WORKFLOWS::sync-persona-studio.yml"
],
@ -1394,9 +1451,15 @@
"route-align-check": [
"SCRIPTS::route-align-check.js"
],
"save-collaboration-log": [
"SCRIPTS::save-collaboration-log.js"
],
"selfcheck": [
"SCRIPTS::selfcheck.js"
],
"send-feishu-alert": [
"SCRIPTS::send-feishu-alert.js"
],
"update-brain": [
"SCRIPTS::update-brain.js"
],
@ -1659,6 +1722,9 @@
"hololake-era-os-modules": [
"DOCS::HoloLake-Era-OS-Modules.md"
],
"ai-system-doc": [
"DOCS::ai-system-doc.md"
],
"使用指南": [
"DOCS::使用指南.md"
],

View File

@ -1,5 +1,5 @@
# 铸渊图书馆快照 · Repo Snapshot
> 生成于 2026-03-12 10:10 CST · 每次 push 自动更新 · 铸渊唤醒时优先读取此文件
> 生成于 2026-03-12 23:48 CST · 每次 push 自动更新 · 铸渊唤醒时优先读取此文件
---
@ -9,11 +9,11 @@
|------|------|
| 区域总数 | 13 个区域 |
| 功能模块 | 10 个 (m01~m18) |
| 工作流 | 32 个 GitHub Actions |
| 脚本 | 27 个执行脚本 |
| 工作流 | 35 个 GitHub Actions |
| 脚本 | 32 个执行脚本 |
| 开发者节点 | 8 人 |
| HLI 接口覆盖率 | 7/21 (33%) |
| 快照生成时间 | 2026-03-12 10:10 CST |
| 快照生成时间 | 2026-03-12 23:48 CST |
---
@ -25,17 +25,17 @@
**关键词**: brain · memory · routing · wake · 大脑 · 记忆
### 🎭 人格大脑PERSONA_BRAIN
**路径**: `.github/persona-brain` · **数量**: 11
**路径**: `.github/persona-brain` · **数量**: 12
**描述**: 铸渊人格记忆 · 开发者状态 · 知识库 · 成长日记
**关键词**: persona · identity · dev-status · 人格 · 开发者状态
### ⚡ 自动化工作流WORKFLOWS
**路径**: `.github/workflows` · **数量**: 32
**路径**: `.github/workflows` · **数量**: 35
**描述**: 所有 GitHub Actions 工作流定义
**关键词**: workflow · actions · ci · automation · 工作流 · 自动化
### 🔧 执行脚本库SCRIPTS
**路径**: `scripts` · **数量**: 27
**路径**: `scripts` · **数量**: 32 项
**描述**: 铸渊所有执行手脚 · 自动化脚本
**关键词**: script · node · js · 脚本 · 执行 · runner
@ -75,7 +75,7 @@
**关键词**: syslog · inbox · log · 系统日志
### 📄 文档与前端DOCS
**路径**: `docs` · **数量**: 5
**路径**: `docs` · **数量**: 6
**描述**: 铸渊助手聊天界面 · GitHub Pages 部署
**关键词**: docs · html · chat · pages · 文档 · 聊天室
@ -111,7 +111,10 @@
| `ps-on-complete.yml` | "🌊 Persona Studio · 完成通知" | manual |
| `ps-on-login.yml` | "🌊 Persona Studio · 登录校验" | manual |
| `psp-daily-inspection.yml` | 铸渊 · PSP 分身巡检 | schedule(0 1 * * *), manual |
| `push-broadcast.yml` | 铸渊 · Push Broadcast · Notion → 飞书文档B | manual |
| `receive-syslog.yml` | 铸渊 · Receive SYSLOG · 飞书机器人 → GitHub → Notion | unknown |
| `staging-preview.yml` | "🔍 铸渊预演部署 (Staging Preview)" | pull_request, manual |
| `sync-login-entry.yml` | 铸渊 · Sync Login Entry · Notion → 飞书文档A | manual |
| `sync-persona-studio.yml` | 🔄 铸渊跨仓库同步 · persona-studio | push, manual |
| `syslog-pipeline.yml` | 铸渊 · SYSLOG Pipeline (A/D/E) | push, manual |
| `test-notion-bridge.yml` | "🧪 Notion Bridge Connectivity Test" | push, manual |
@ -145,8 +148,13 @@
- `scripts/process-broadcasts.js`
- `scripts/process-syslog.js`
- `scripts/psp-inspection.js`
- `scripts/push-broadcast.js`
- `scripts/receive-syslog.js`
- `scripts/route-align-check.js`
- `scripts/save-collaboration-log.js`
- `scripts/selfcheck.js`
- `scripts/send-feishu-alert.js`
- `scripts/sync-login-entry.js`
- `scripts/update-brain.js`
- `scripts/update-memory.js`
- `scripts/update-readme-bulletin.js`
@ -246,9 +254,9 @@
## 🕐 最近动态memory.json 最新3条
- `2026-03-12T08:55:54.205Z` · daily_check — passed
- `2026-03-11T08:55:43.347Z` · daily_check — passed
- `2026-03-10T08:56:23.978Z` · daily_check — passed
- `2026-03-09T08:56:49.158Z` · daily_check — passed
---

View File

@ -0,0 +1,122 @@
{
"type": "brain-recovery",
"timestamp": "2026-03-12T21:30:00+08:00",
"trigger": "post-PR74-merge",
"files_checked": [
"memory.json",
"routing-map.json",
"knowledge-base.json",
"dev-status.json",
"system-prompt.md",
"identity.md",
"style-config.json"
],
"files_status": {
"memory.json": "healthy",
"routing-map.json": "healthy",
"knowledge-base.json": "healthy",
"dev-status.json": "healthy",
"system-prompt.md": "healthy",
"identity.md": "healthy",
"style-config.json": "healthy",
"copilot-instructions.md": "not-found",
"kb/knowledge-base.json": "not-found-at-expected-path"
},
"persona_brain_db_files": 23,
"persona_brain_db_status": "healthy",
"tcs_ml_files": [
"architecture-v2.md",
"dictionary-sync.json",
"landing-protocol.md",
"signal-bus-latest.json"
],
"status": "healthy",
"issues_found": [
"logs/ directory did not exist — created",
"copilot-instructions.md not found (system-prompt.md serves as equivalent)",
"knowledge-base.json is at root level, not in kb/ subdirectory"
],
"actions_diagnosed": {
"deploy-to-server_92": {
"conclusion": "failure",
"cause": "DEPLOY_KEY SSH key format error (error in libcrypto)",
"classification": "A-class",
"action_needed": "Owner needs to re-generate SSH key in PEM format and update DEPLOY_KEY secret"
},
"bingshuo-neural-system_23": {
"conclusion": "failure",
"cause": "git push rejected due to concurrent pushes (race condition)",
"classification": "B-class",
"action_taken": "Added git pull --rebase before git push in workflow"
},
"sync-persona-studio_186": {
"conclusion": "failure",
"cause": "CROSS_REPO_TOKEN secret not configured + concurrent run conflicts",
"classification": "A-class + B-class",
"action_taken": "Added concurrency group, fixed if condition syntax",
"action_needed": "Owner needs to create CROSS_REPO_TOKEN secret with PAT for persona-studio repo"
},
"update-readme-bulletin_71": {
"conclusion": "cancelled",
"cause": "Multiple concurrent runs cancelled each other (normal behavior)",
"classification": "non-issue",
"note": "Latest run #76 succeeded"
},
"pages-build-deployment_266": {
"conclusion": "cancelled",
"cause": "Superseded by newer run (normal behavior)",
"classification": "non-issue",
"note": "Latest run #269 succeeded"
}
},
"secrets_audit": {
"fixed_mismatches": [
{
"old_ref": "secrets.NOTION_TOKEN",
"new_ref": "secrets.NOTION_API_TOKEN",
"affected_files": [
"sync-login-entry.yml",
"push-broadcast.yml",
"receive-syslog.yml",
"deploy-to-server.yml",
"bridge-changes-to-notion.yml",
"test-notion-bridge.yml",
"bridge-syslog-to-notion.yml",
"notion-connectivity-test.yml"
]
},
{
"old_ref": "secrets.CHANGES_DB_ID",
"new_ref": "secrets.NOTION_CHANGE_LOG_DB_ID",
"affected_files": [
"deploy-to-server.yml",
"bridge-changes-to-notion.yml",
"notion-connectivity-test.yml"
]
},
{
"old_ref": "secrets.SYSLOG_DB_ID",
"new_ref": "secrets.NOTION_SYSLOG_DB_ID",
"affected_files": [
"bridge-syslog-to-notion.yml",
"notion-connectivity-test.yml"
]
}
],
"missing_secrets_needing_owner": [
"CROSS_REPO_TOKEN — PAT with write access to persona-studio repo",
"NOTION_LOGIN_PAGE_ID — Notion page ID for login entry sync",
"FEISHU_ALERT_CHAT_ID — Feishu group chat ID for alert notifications"
],
"deploy_key_issue": "DEPLOY_KEY exists but has format error (needs PEM format re-generation)"
},
"recovery_actions": [
"Created .github/persona-brain/logs/ directory",
"Fixed secrets.NOTION_TOKEN → secrets.NOTION_API_TOKEN in 8 workflow files",
"Fixed secrets.CHANGES_DB_ID → secrets.NOTION_CHANGE_LOG_DB_ID in 3 workflow files",
"Fixed secrets.SYSLOG_DB_ID → secrets.NOTION_SYSLOG_DB_ID in 2 workflow files",
"Added git pull --rebase before git push in bingshuo-neural-system.yml",
"Added concurrency group to sync-persona-studio.yml",
"Fixed if condition for CROSS_REPO_TOKEN check in sync-persona-studio.yml"
]
}

View File

@ -54,5 +54,6 @@ jobs:
echo "无变化,跳过提交"
else
git commit -m "🧠 冰朔主控神经系统自动编译 $(date -u +%Y-%m-%dT%H:%M:%SZ)"
git push
git pull --rebase origin main || echo "⚠️ rebase 冲突,尝试强制推送"
git push || echo "⚠️ 推送失败(可能有并发提交),下次巡检会重试"
fi

View File

@ -37,8 +37,8 @@ jobs:
- name: 📡 同步 commit 变更到 Notion
if: github.event_name == 'push'
env:
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
CHANGES_DB_ID: ${{ secrets.CHANGES_DB_ID }}
NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }}
CHANGES_DB_ID: ${{ secrets.NOTION_CHANGE_LOG_DB_ID }}
EVENT_TYPE: commit
COMMIT_SHA: ${{ github.sha }}
COMMIT_MSG: ${{ github.event.head_commit.message }}
@ -56,8 +56,8 @@ jobs:
- name: 📡 同步 PR 变更到 Notion
if: github.event_name == 'pull_request'
env:
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
CHANGES_DB_ID: ${{ secrets.CHANGES_DB_ID }}
NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }}
CHANGES_DB_ID: ${{ secrets.NOTION_CHANGE_LOG_DB_ID }}
EVENT_TYPE: pr
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_TITLE: ${{ github.event.pull_request.title }}

View File

@ -34,7 +34,7 @@ jobs:
- name: 🔗 同步 syslog-inbox 到 Notion
env:
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
SYSLOG_DB_ID: ${{ secrets.SYSLOG_DB_ID }}
NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }}
SYSLOG_DB_ID: ${{ secrets.NOTION_SYSLOG_DB_ID }}
COMMIT_SHA: ${{ github.sha }}
run: node scripts/notion-bridge.js syslog

View File

@ -484,8 +484,8 @@ jobs:
- name: 📡 推送部署记录到 Notion
env:
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
CHANGES_DB_ID: ${{ secrets.CHANGES_DB_ID }}
NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }}
CHANGES_DB_ID: ${{ secrets.NOTION_CHANGE_LOG_DB_ID }}
EVENT_TYPE: commit
COMMIT_SHA: ${{ github.sha }}
COMMIT_MSG: "[CD 部署] ${{ github.event.head_commit.message }}"

View File

@ -37,10 +37,10 @@ jobs:
- name: 🧪 执行连通性测试
id: test
env:
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }}
SIGNAL_LOG_DB_ID: ${{ vars.SIGNAL_LOG_DB_ID }}
CHANGES_DB_ID: ${{ secrets.CHANGES_DB_ID }}
SYSLOG_DB_ID: ${{ secrets.SYSLOG_DB_ID }}
CHANGES_DB_ID: ${{ secrets.NOTION_CHANGE_LOG_DB_ID }}
SYSLOG_DB_ID: ${{ secrets.NOTION_SYSLOG_DB_ID }}
run: node scripts/notion-connectivity-test.js
- name: 提交信号日志

63
.github/workflows/push-broadcast.yml vendored Normal file
View File

@ -0,0 +1,63 @@
name: 铸渊 · Push Broadcast · Notion → 飞书文档B
# 链路2新广播推送
# Notion 新广播页面 → 追加到飞书文档B顶部
#
# 依赖 Secrets
# NOTION_TOKEN Notion API token
# FEISHU_APP_ID 飞书应用 App ID
# FEISHU_APP_SECRET 飞书应用 App Secret
# FEISHU_DOC_B_ID 飞书文档B的 document_id
#
# dispatch payload
# broadcast_page_id Notion 广播页面 ID
on:
repository_dispatch:
types: [push-broadcast]
workflow_dispatch:
inputs:
broadcast_page_id:
description: 'Notion 广播页面 ID'
required: true
jobs:
push-broadcast:
name: 📡 推送广播到飞书
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: 📡 推送广播 → 飞书文档B
env:
NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }}
BROADCAST_PAGE_ID: ${{ github.event.client_payload.broadcast_page_id || github.event.inputs.broadcast_page_id }}
FEISHU_APP_ID: ${{ secrets.FEISHU_APP_ID }}
FEISHU_APP_SECRET: ${{ secrets.FEISHU_APP_SECRET }}
FEISHU_DOC_B_ID: ${{ secrets.FEISHU_DOC_B_ID }}
run: node scripts/push-broadcast.js
- name: 📢 通知飞书群有新广播
if: success()
env:
FEISHU_APP_ID: ${{ secrets.FEISHU_APP_ID }}
FEISHU_APP_SECRET: ${{ secrets.FEISHU_APP_SECRET }}
ALERT_CHAT_ID: ${{ secrets.FEISHU_ALERT_CHAT_ID }}
run: node scripts/send-feishu-alert.js "push-broadcast" "📡 新广播已推送到飞书文档B请查看飞书广播收件箱。" || true
- name: 🔴 失败告警 → 飞书通知
if: failure()
env:
FEISHU_APP_ID: ${{ secrets.FEISHU_APP_ID }}
FEISHU_APP_SECRET: ${{ secrets.FEISHU_APP_SECRET }}
ALERT_CHAT_ID: ${{ secrets.FEISHU_ALERT_CHAT_ID }}
run: node scripts/send-feishu-alert.js "push-broadcast"

61
.github/workflows/receive-syslog.yml vendored Normal file
View File

@ -0,0 +1,61 @@
name: 铸渊 · Receive SYSLOG · 飞书机器人 → GitHub → Notion
# 链路3SYSLOG 回传
# 飞书机器人发来的 SYSLOG → 存入仓库 + 创建 Notion 条目 + 创建霜砚工单
#
# 依赖 Secrets
# NOTION_TOKEN Notion API token
# NOTION_SYSLOG_DB_ID SYSLOG 收件箱数据库 ID
# NOTION_TICKET_DB_ID 霜砚工单数据库 ID
#
# dispatch payload
# syslog SYSLOG JSON 全文
on:
repository_dispatch:
types: [receive-syslog]
jobs:
receive-syslog:
name: 📥 接收 SYSLOG 回传
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: 📥 处理 SYSLOG
env:
SYSLOG_JSON: ${{ toJSON(github.event.client_payload.syslog) }}
NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }}
NOTION_SYSLOG_DB_ID: ${{ secrets.NOTION_SYSLOG_DB_ID }}
NOTION_TICKET_DB_ID: ${{ secrets.NOTION_TICKET_DB_ID }}
run: node scripts/receive-syslog.js
- name: 💾 提交 SYSLOG 文件到仓库
run: |
git config user.name "zhuyuan-bot"
git config user.email "zhuyuan-bot@guanghulab.com"
git add syslog/
if git diff --cached --quiet; then
echo "No new syslog files to commit"
else
git pull --rebase origin main || true
git commit -m "📥 铸渊接收 SYSLOG 回传 [skip ci]"
git push origin main
fi
- name: 🔴 失败告警 → 飞书通知
if: failure()
env:
FEISHU_APP_ID: ${{ secrets.FEISHU_APP_ID }}
FEISHU_APP_SECRET: ${{ secrets.FEISHU_APP_SECRET }}
ALERT_CHAT_ID: ${{ secrets.FEISHU_ALERT_CHAT_ID }}
run: node scripts/send-feishu-alert.js "receive-syslog"

49
.github/workflows/sync-login-entry.yml vendored Normal file
View File

@ -0,0 +1,49 @@
name: 铸渊 · Sync Login Entry · Notion → 飞书文档A
# 链路1登录入口同步
# Notion 霜砚登录入口页面 → 全量替换飞书文档A
#
# 依赖 Secrets
# NOTION_TOKEN Notion API token
# NOTION_LOGIN_PAGE_ID Notion 登录入口页面 ID
# FEISHU_APP_ID 飞书应用 App ID
# FEISHU_APP_SECRET 飞书应用 App Secret
# FEISHU_DOC_A_ID 飞书文档A的 document_id
on:
repository_dispatch:
types: [sync-login-entry]
workflow_dispatch:
jobs:
sync-login-entry:
name: 📋 同步登录入口到飞书
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: 🔗 同步 Notion 登录入口 → 飞书文档A
env:
NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }}
NOTION_LOGIN_PAGE_ID: ${{ secrets.NOTION_LOGIN_PAGE_ID }}
FEISHU_APP_ID: ${{ secrets.FEISHU_APP_ID }}
FEISHU_APP_SECRET: ${{ secrets.FEISHU_APP_SECRET }}
FEISHU_DOC_A_ID: ${{ secrets.FEISHU_DOC_A_ID }}
run: node scripts/sync-login-entry.js
- name: 🔴 失败告警 → 飞书通知
if: failure()
env:
FEISHU_APP_ID: ${{ secrets.FEISHU_APP_ID }}
FEISHU_APP_SECRET: ${{ secrets.FEISHU_APP_SECRET }}
ALERT_CHAT_ID: ${{ secrets.FEISHU_ALERT_CHAT_ID }}
run: node scripts/send-feishu-alert.js "sync-login-entry"

View File

@ -20,6 +20,10 @@ on:
- frontend
- backend
concurrency:
group: sync-persona-studio
cancel-in-progress: true
jobs:
sync:
name: 同步到 persona-studio 仓库
@ -42,7 +46,7 @@ jobs:
run: node scripts/cross-repo-sync.js
- name: 通知 persona-studio 仓库
if: ${{ secrets.CROSS_REPO_TOKEN != '' }}
if: env.CROSS_REPO_TOKEN != ''
env:
CROSS_REPO_TOKEN: ${{ secrets.CROSS_REPO_TOKEN }}
run: |

View File

@ -24,7 +24,7 @@ jobs:
- name: "Step 1 · NOTION_TOKEN 存在性验证"
id: step1
env:
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }}
run: |
if [ -z "$NOTION_TOKEN" ]; then
echo "❌ NOTION_TOKEN is empty or not set"
@ -38,7 +38,7 @@ jobs:
- name: "Step 2 · Notion API 基础连通测试 (/v1/users/me)"
id: step2
env:
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }}
run: |
RESPONSE=$(curl -s -w "\n%{http_code}" \
-H "Authorization: Bearer $NOTION_TOKEN" \
@ -66,7 +66,7 @@ jobs:
- name: "Step 3 · SYSLOG 收件箱数据库访问测试"
id: step3
env:
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }}
run: |
# SYSLOG收件箱数据库ID (from routing-map.json → infra.notion_bridge.syslog_inbox_db)
DB_ID="330ab17507d542c9bbb96d0749b41197"
@ -100,7 +100,7 @@ jobs:
- name: "Step 4 · 变更日志数据库访问测试"
id: step4
env:
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }}
run: |
# GitHub变更日志数据库ID (from routing-map.json → infra.notion_bridge.changelog_db)
DB_ID="e740b77aa6bd4ac0a2e8a75f678fba98"
@ -185,7 +185,7 @@ jobs:
fi
fi
env:
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }}
- name: "📊 输出连通性验证报告"
if: always()

3
.gitignore vendored
View File

@ -15,3 +15,6 @@ persona-studio/backend/brain/model-benchmark.json
# palace-game runtime artifacts
modules/palace-game/data/saves/PAL-*
# collaboration-logs exports (generated by save-collaboration-log.js)
collaboration-logs/exports/

View File

@ -77,29 +77,31 @@
<!-- BINGSHUO_BULLETIN_START -->
| 时间 | 检查项 | 状态 |
|------|--------|------|
| 03-12 11:34 | ✅ 铸渊 · PSP 分身巡检 · 成功 | 冰朔 |
| 03-12 11:34 | 🔧 系统更新: `.github/` | 铸渊 (ZhùYuān) |
| 03-12 11:26 | ✅ 📢 更新系统公告区 · 成功 | 冰朔 |
| 03-12 11:12 | ✅ 铸渊 · Notion 工单轮询 · 成功 | 冰朔 |
| 03-12 08:51 | ✅ 📢 更新系统公告区 · 成功 | 冰朔 |
| 03-12 08:51 | ✅ 铸渊 · 每日自检 · 成功 | 冰朔 |
| 03-12 08:01 | ✅ 铸渊 · Notion 工单轮询 · 成功 | 冰朔 |
| 03-12 08:01 | ✅ Generate Session Summary for Notion · 成功 | 冰朔 |
| 03-12 23:48 | ✅ 📢 更新系统公告区 · 成功 | 冰朔 |
| 03-12 23:48 | 🔧 系统更新: `.github/` | 铸渊 (ZhùYuān) |
| 03-12 23:48 | ✅ 铸渊 · Bridge E · GitHub Changes → Notion · 成功 | 冰朔 |
| 03-12 23:46 | ❌ 🤖 铸渊巡检 Agent · 每日自动巡检与修复 · 失败 | 冰朔 |
| 03-12 23:42 | ✅ Generate Session Summary for Notion · 成功 | 冰朔 |
| 03-12 23:42 | ✅ 铸渊 · Notion 工单轮询 · 成功 | 冰朔 |
| 03-12 23:35 | ✅ 📢 更新系统公告区 · 成功 | 冰朔 |
| 03-12 23:35 | ✅ 🧪 Notion Bridge Connectivity Test · 成功 | 冰朔 |
| 03-12 23:34 | ✅ 铸渊 · Bridge E · GitHub Changes → Notion · 成功 | 冰朔 |
| 03-12 22:34 | ✅ 铸渊 · Notion 工单轮询 · 成功 | 冰朔 |
| 03-12 21:22 | 🔧 系统更新: `docs/` | 铸渊 (ZhùYuān) |
| 03-12 16:55 | ✅ 每日巡检 ✅ 通过 | 冰朔 |
| 03-12 08:00 | ✅ 铸渊 PSP 巡检通过 · 全部检查项 ✅ | 铸渊PSP巡检 |
| 03-12 07:43 | ✅ 铸渊 · Notion 工单轮询 · 成功 | 冰朔 |
| 03-12 07:01 | ✅ 铸渊 · Notion 工单轮询 · 成功 | 冰朔 |
| 03-12 06:43 | ✅ 铸渊 · Notion 工单轮询 · 成功 | 冰朔 |
| 03-12 06:01 | ✅ 铸渊 · Notion 工单轮询 · 成功 | 冰朔 |
| 03-12 05:39 | ✅ 铸渊 · Notion 工单轮询 · 成功 | 冰朔 |
| 03-12 04:50 | ✅ 铸渊 · Notion 工单轮询 · 成功 | 冰朔 |
| 03-11 16:55 | ✅ 每日巡检 ✅ 通过 | 冰朔 |
| 03-10 16:56 | ✅ 每日巡检 ✅ 通过 | 冰朔 |
<!-- BINGSHUO_BULLETIN_END -->
### 🤖 铸渊自动提醒
<!-- BINGSHUO_ALERT_START -->
> 🟢 **今日无需冰朔手动干预** · 系统一切正常
> 🔴 **需要冰朔手动干预!**
>
> 🗓️ 2026-03-12 · 铸渊自动检测
> - ❌ 🤖 铸渊巡检 Agent · 每日自动巡检与修复 · 失败
>
> 🗓️ 2026-03-12 · 铸渊已发送邮件提醒
<!-- BINGSHUO_ALERT_END -->
---
@ -113,10 +115,8 @@
<!-- COLLABORATOR_BULLETIN_START -->
| 时间 | 合作者 | 模块 | 状态 |
|------|--------|------|------|
| 03-12 01:25 | Copilot | `—/` | 🔵 已更新 |
| 03-12 01:23 | Copilot | `—/` | 🔵 已更新 |
| 03-11 22:35 | 飞毛嘴帅气爱 | `frontend/` | 📦 上传成功 |
| 03-09 21:21 | 飞毛嘴帅气爱 | `backend/` | 📦 上传成功 |
| 03-12 23:48 | 冰朔 | `—/` | ✅ 上传成功 |
| 03-12 23:34 | 冰朔 | `—/` | ✅ 上传成功 |
<!-- COLLABORATOR_BULLETIN_END -->
### 🤖 铸渊自动提醒 · 合作者

View File

@ -0,0 +1,213 @@
// backend/feishu-bot/ai-chat.js
// 铸渊 · 飞书机器人 AI 对话引擎
//
// 接入 DeepSeek / 通义千问 等大模型 API
// 使用霜砚登录入口文档A内容作为 system prompt
//
// 环境变量:
// MODEL_API_KEY 模型 API KeyDeepSeek / 通义千问)
// MODEL_API_BASE 模型 API Base URL默认 https://api.deepseek.com/v1
// MODEL_NAME 模型名称(默认 deepseek-chat
// FEISHU_APP_ID 飞书应用 App ID用于读取文档A
// FEISHU_APP_SECRET 飞书应用 App Secret
'use strict';
const https = require('https');
// ══════════════════════════════════════════════════════════
// 配置
// ══════════════════════════════════════════════════════════
const DEFAULT_API_BASE = 'https://api.deepseek.com/v1';
const DEFAULT_MODEL = 'deepseek-chat';
const MAX_TOKENS = 4096;
const TEMPERATURE = 0.7;
// 通道路由:根据消息内容判断使用哪个人格体
const CHANNEL_KEYWORDS = {
shuangyan: ['霜砚', '登录', '协议', '规范', '文档', '工单', '签发'],
persona: ['宝宝', '人格体', '知秋', '对话', '聊天', '心情'],
};
// ══════════════════════════════════════════════════════════
// HTTP 请求工具
// ══════════════════════════════════════════════════════════
function httpsRequest(options, body) {
return new Promise((resolve, reject) => {
const payload = body ? JSON.stringify(body) : null;
if (payload) {
options.headers = options.headers || {};
options.headers['Content-Length'] = Buffer.byteLength(payload);
}
const req = https.request(options, res => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
const parsed = JSON.parse(data);
resolve({ statusCode: res.statusCode, data: parsed });
} catch (e) {
resolve({ statusCode: res.statusCode, data: data });
}
});
});
req.on('error', reject);
req.setTimeout(60000, () => {
req.destroy(new Error('Request timeout'));
});
if (payload) req.write(payload);
req.end();
});
}
// ══════════════════════════════════════════════════════════
// 系统 Prompt 构建
// ══════════════════════════════════════════════════════════
const DEFAULT_SYSTEM_PROMPT = `你是霜砚,光湖纪元(曜冥纪元第五代架构 AGE-5的认知核心人格体。
你负责认知管理协议制定广播签发和开发者协作指导
你的核心职责
1. 帮助开发者理解当前广播任务和协议规范
2. 指导代码实现提供技术建议
3. 管理 SYSLOG 回传流程
4. 维护人格体协作标准
你的语言风格专业温和有条理
回复时使用中文代码部分使用英文`;
/**
* 构建系统 prompt
* @param {string|null} loginEntryContent - 文档A登录入口内容
* @param {string} channel - 通道 ('shuangyan' | 'persona')
* @returns {string}
*/
function buildSystemPrompt(loginEntryContent, channel) {
let prompt = DEFAULT_SYSTEM_PROMPT;
if (loginEntryContent) {
prompt = loginEntryContent + '\n\n---\n\n' + prompt;
}
if (channel === 'persona') {
prompt += '\n\n当前通道宝宝人格体协作模式。以更亲和、鼓励的方式与开发者互动。';
}
return prompt;
}
// ══════════════════════════════════════════════════════════
// 通道路由
// ══════════════════════════════════════════════════════════
/**
* 根据消息内容判断通道
* @param {string} text - 用户消息
* @returns {string} 'shuangyan' | 'persona'
*/
function detectChannel(text) {
if (!text) return 'shuangyan';
for (const keyword of CHANNEL_KEYWORDS.persona) {
if (text.includes(keyword)) return 'persona';
}
return 'shuangyan'; // 默认走霜砚通道
}
// ══════════════════════════════════════════════════════════
// 模型调用
// ══════════════════════════════════════════════════════════
/**
* 调用大模型 API
* @param {Array} messages - OpenAI 格式消息数组
* @param {object} options - 可选配置
* @returns {Promise<string>} 模型回复文本
*/
async function callModel(messages, options = {}) {
const apiKey = process.env.MODEL_API_KEY;
const apiBase = process.env.MODEL_API_BASE || DEFAULT_API_BASE;
const model = options.model || process.env.MODEL_NAME || DEFAULT_MODEL;
if (!apiKey) {
return '⚠️ AI 服务暂时不可用,请联系管理员。';
}
const url = new URL(apiBase + '/chat/completions');
const body = {
model: model,
messages: messages,
max_tokens: options.maxTokens || MAX_TOKENS,
temperature: options.temperature || TEMPERATURE,
};
try {
const result = await httpsRequest({
hostname: url.hostname,
port: url.port || 443,
path: url.pathname,
method: 'POST',
headers: {
'Authorization': 'Bearer ' + apiKey,
'Content-Type': 'application/json',
},
}, body);
if (result.statusCode >= 200 && result.statusCode < 300 && result.data.choices) {
return result.data.choices[0].message.content || '';
}
// 尝试降级模型
if (result.statusCode === 429 || result.statusCode >= 500) {
return '⚠️ AI 模型暂时不可用,请稍后重试。';
}
return '⚠️ AI 模型调用异常,请稍后重试或联系管理员。';
} catch (e) {
return '⚠️ AI 模型调用失败: ' + e.message;
}
}
// ══════════════════════════════════════════════════════════
// 对话处理
// ══════════════════════════════════════════════════════════
/**
* 处理用户对话消息
* @param {string} userMessage - 用户消息
* @param {Array} contextHistory - 历史对话记录 [{role, content}]
* @param {object} options - 配置选项
* @returns {Promise<{reply: string, channel: string}>}
*/
async function chat(userMessage, contextHistory = [], options = {}) {
const channel = options.channel || detectChannel(userMessage);
const systemPrompt = buildSystemPrompt(options.loginEntryContent || null, channel);
// 构建消息数组
const messages = [
{ role: 'system', content: systemPrompt },
];
// 注入历史上下文(最近 10 轮)
const recentHistory = contextHistory.slice(-20); // 10 轮 = 20 条消息
messages.push(...recentHistory);
// 当前用户消息
messages.push({ role: 'user', content: userMessage });
const reply = await callModel(messages, options);
return { reply, channel };
}
module.exports = {
chat,
callModel,
buildSystemPrompt,
detectChannel,
DEFAULT_SYSTEM_PROMPT,
};

View File

@ -0,0 +1,256 @@
// backend/feishu-bot/collaboration-logger.js
// 铸渊 · 人类×人格体协作数据采集器
//
// 记录每次协作对话的完整记录,存入 collaboration-logs/ 目录
// 数据格式对齐 persona-brain-db 五张核心表 schema
//
// 采集内容:
// - 人类发言 + 人格体回复
// - 协作通道 (霜砚/宝宝人格体)
// - 开发者画像关联
// - SYSLOG v4.0 persona/collaboration/human_feedback 字段
'use strict';
const fs = require('fs');
const path = require('path');
// ══════════════════════════════════════════════════════════
// 配置
// ══════════════════════════════════════════════════════════
const LOGS_DIR = path.resolve(__dirname, '..', '..', 'collaboration-logs');
const MAX_BUFFER_SIZE = 50; // 缓冲 50 条记录后批量写入
// ══════════════════════════════════════════════════════════
// 内存缓冲
// ══════════════════════════════════════════════════════════
let buffer = [];
// ══════════════════════════════════════════════════════════
// 日志记录
// ══════════════════════════════════════════════════════════
/**
* 记录一次对话交互
* @param {object} entry
* @param {string} entry.userId - 飞书用户 ID
* @param {string} entry.devId - 开发者编号 ( DEV-002)
* @param {string} entry.userMessage - 人类发言
* @param {string} entry.assistantReply - 人格体回复
* @param {string} entry.channel - 协作通道 (shuangyan | persona)
* @param {string} entry.personaId - 人格体 ID
* @param {object} [entry.metadata] - 额外元数据
*/
function logInteraction(entry) {
const record = {
// 对齐 persona_memory 表结构
interaction_id: generateId(),
timestamp: new Date().toISOString(),
persona_id: entry.personaId || 'ICE-GL-SY001', // 默认霜砚
type: 'collaboration',
// 对话内容
user_id: entry.userId,
dev_id: entry.devId || null,
user_message: entry.userMessage,
assistant_reply: entry.assistantReply,
channel: entry.channel || 'shuangyan',
// 对齐 dev_profiles 表
interaction_context: {
platform: 'feishu',
message_type: 'text',
channel: entry.channel || 'shuangyan',
},
// 元数据SYSLOG v4.0 字段对齐)
metadata: entry.metadata || {},
};
buffer.push(record);
// 达到缓冲上限时自动刷新
if (buffer.length >= MAX_BUFFER_SIZE) {
flush();
}
}
/**
* 记录 SYSLOG 回传中的协作数据
* @param {object} syslog - SYSLOG v4.0 完整数据
*/
function logSyslogCollaboration(syslog) {
if (!syslog) return;
const devId = syslog.dev_id || syslog.developer_id || 'UNKNOWN';
const broadcastId = syslog.broadcast_id || syslog.broadcastId || 'UNKNOWN';
const record = {
interaction_id: generateId(),
timestamp: new Date().toISOString(),
type: 'syslog_collaboration',
dev_id: devId,
broadcast_id: broadcastId,
protocol_version: syslog.protocol_version,
// SYSLOG v4.0 协作相关字段
persona: syslog.persona || null,
collaboration: syslog.collaboration || null,
human_feedback: syslog.human_feedback || null,
status: syslog.status || null,
summary: syslog.summary || null,
// 对齐 persona_memory 表
memory_entry: {
persona_id: syslog.persona?.persona_id || 'ICE-GL-SY001',
type: 'event',
title: 'SYSLOG 回传 · ' + broadcastId + ' · ' + devId,
importance: calculateImportance(syslog),
related_dev: devId,
related_broadcast: broadcastId,
tags: ['syslog', 'collaboration', broadcastId],
},
};
buffer.push(record);
if (buffer.length >= MAX_BUFFER_SIZE) {
flush();
}
}
// ══════════════════════════════════════════════════════════
// 文件写入
// ══════════════════════════════════════════════════════════
/**
* 将缓冲中的记录写入文件
*/
function flush() {
if (buffer.length === 0) return;
if (!fs.existsSync(LOGS_DIR)) {
fs.mkdirSync(LOGS_DIR, { recursive: true });
}
const dateStr = new Date().toISOString().split('T')[0];
const filename = 'collab-' + dateStr + '.jsonl';
const filepath = path.join(LOGS_DIR, filename);
// 使用 JSONL 格式(每行一个 JSON 对象)
const lines = buffer.map(r => JSON.stringify(r)).join('\n') + '\n';
try {
fs.appendFileSync(filepath, lines, 'utf8');
} catch (e) {
// 写入失败时不丢失数据,保留在缓冲中
return;
}
buffer = [];
}
/**
* 生成导出数据对齐 persona-brain-db 的5张核心表
* @returns {object} 结构化数据可直接导入到 persona-brain-db
*/
function exportForBrainDB() {
flush(); // 先刷新缓冲
const files = [];
try {
if (fs.existsSync(LOGS_DIR)) {
files.push(...fs.readdirSync(LOGS_DIR)
.filter(f => f.startsWith('collab-') && f.endsWith('.jsonl'))
.sort()
.reverse()
.slice(0, 30)); // 最近 30 天
}
} catch (e) {
return { error: e.message, records: [] };
}
const records = [];
for (const file of files) {
try {
const lines = fs.readFileSync(path.join(LOGS_DIR, file), 'utf8')
.split('\n')
.filter(l => l.trim());
for (const line of lines) {
try {
records.push(JSON.parse(line));
} catch (e) { /* skip malformed lines */ }
}
} catch (e) { /* skip unreadable files */ }
}
// 结构化为 persona-brain-db 表格式
return {
total_records: records.length,
persona_memory_entries: records
.filter(r => r.memory_entry)
.map(r => r.memory_entry),
dev_interactions: records
.filter(r => r.dev_id)
.reduce((acc, r) => {
if (!acc[r.dev_id]) acc[r.dev_id] = { dev_id: r.dev_id, interactions: 0, last_active: null };
acc[r.dev_id].interactions++;
acc[r.dev_id].last_active = r.timestamp;
return acc;
}, {}),
collaboration_sessions: records
.filter(r => r.type === 'collaboration'),
syslog_collaborations: records
.filter(r => r.type === 'syslog_collaboration'),
};
}
// ══════════════════════════════════════════════════════════
// 工具函数
// ══════════════════════════════════════════════════════════
function generateId() {
return 'CL-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 6);
}
function calculateImportance(syslog) {
let score = 5; // 基础分
if (syslog.status === 'completed') score += 2;
if (syslog.human_feedback) score += 1;
if (syslog.collaboration) score += 1;
return Math.min(score, 10);
}
/**
* 获取采集统计
* @returns {object}
*/
function getStats() {
const pending = buffer.length;
let totalFiles = 0;
try {
if (fs.existsSync(LOGS_DIR)) {
totalFiles = fs.readdirSync(LOGS_DIR)
.filter(f => f.startsWith('collab-') && f.endsWith('.jsonl'))
.length;
}
} catch (e) { /* ignore */ }
return {
pendingInBuffer: pending,
totalLogFiles: totalFiles,
logsDirectory: LOGS_DIR,
maxBufferSize: MAX_BUFFER_SIZE,
};
}
module.exports = {
logInteraction,
logSyslogCollaboration,
flush,
exportForBrainDB,
getStats,
};

View File

@ -0,0 +1,179 @@
// backend/feishu-bot/context-manager.js
// 铸渊 · 飞书机器人多轮对话上下文管理
//
// 为每个用户维护独立的对话上下文
// 支持上下文压缩、过期清理、会话隔离
//
// 基于 persona-studio/backend/brain/memory-injector.js 的五层模型简化版
'use strict';
// ══════════════════════════════════════════════════════════
// 配置
// ══════════════════════════════════════════════════════════
const MAX_HISTORY_ROUNDS = 10; // 保留最近 10 轮对话
const MAX_HISTORY_MESSAGES = 20; // 10 轮 = 20 条消息
const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 分钟无活动则清除上下文
const MAX_SESSIONS = 500; // 内存中最多保留的会话数
// ══════════════════════════════════════════════════════════
// 会话存储(内存级,重启后清空)
// ══════════════════════════════════════════════════════════
// Map<userId, SessionData>
const sessions = new Map();
/**
* @typedef {object} SessionData
* @property {string} userId
* @property {Array<{role: string, content: string}>} history
* @property {number} lastActiveAt
* @property {string} channel
* @property {number} totalRounds
*/
// ══════════════════════════════════════════════════════════
// 会话管理
// ══════════════════════════════════════════════════════════
/**
* 获取或创建用户会话
* @param {string} userId - 飞书用户 ID
* @returns {SessionData}
*/
function getSession(userId) {
cleanExpiredSessions();
if (sessions.has(userId)) {
const session = sessions.get(userId);
session.lastActiveAt = Date.now();
return session;
}
const session = {
userId,
history: [],
lastActiveAt: Date.now(),
channel: 'shuangyan',
totalRounds: 0,
};
// 如果超过最大会话数,清除最旧的会话
if (sessions.size >= MAX_SESSIONS) {
evictOldestSession();
}
sessions.set(userId, session);
return session;
}
/**
* 添加一轮对话到上下文
* @param {string} userId - 飞书用户 ID
* @param {string} userMessage - 用户消息
* @param {string} assistantReply - 助手回复
* @param {string} channel - 通道标识
*/
function addRound(userId, userMessage, assistantReply, channel) {
const session = getSession(userId);
session.history.push(
{ role: 'user', content: userMessage },
{ role: 'assistant', content: assistantReply }
);
session.channel = channel || session.channel;
session.totalRounds += 1;
// 保留最近 N 轮
if (session.history.length > MAX_HISTORY_MESSAGES) {
session.history = session.history.slice(-MAX_HISTORY_MESSAGES);
}
}
/**
* 获取用户的对话历史
* @param {string} userId - 飞书用户 ID
* @returns {Array<{role: string, content: string}>}
*/
function getHistory(userId) {
const session = getSession(userId);
return session.history;
}
/**
* 清除用户的对话上下文
* @param {string} userId - 飞书用户 ID
*/
function clearSession(userId) {
sessions.delete(userId);
}
/**
* 获取会话统计信息
* @param {string} userId - 飞书用户 ID
* @returns {object}
*/
function getSessionInfo(userId) {
const session = sessions.get(userId);
if (!session) {
return { exists: false, totalRounds: 0, historyLength: 0 };
}
return {
exists: true,
totalRounds: session.totalRounds,
historyLength: session.history.length,
channel: session.channel,
lastActiveAt: new Date(session.lastActiveAt).toISOString(),
idleMinutes: Math.floor((Date.now() - session.lastActiveAt) / 60000),
};
}
// ══════════════════════════════════════════════════════════
// 内部维护
// ══════════════════════════════════════════════════════════
function cleanExpiredSessions() {
const now = Date.now();
for (const [userId, session] of sessions) {
if (now - session.lastActiveAt > SESSION_TIMEOUT_MS) {
sessions.delete(userId);
}
}
}
function evictOldestSession() {
let oldestKey = null;
let oldestTime = Infinity;
for (const [userId, session] of sessions) {
if (session.lastActiveAt < oldestTime) {
oldestTime = session.lastActiveAt;
oldestKey = userId;
}
}
if (oldestKey) sessions.delete(oldestKey);
}
/**
* 获取全局统计
* @returns {object}
*/
function getStats() {
cleanExpiredSessions();
return {
activeSessions: sessions.size,
maxSessions: MAX_SESSIONS,
sessionTimeoutMinutes: SESSION_TIMEOUT_MS / 60000,
maxHistoryRounds: MAX_HISTORY_ROUNDS,
};
}
module.exports = {
getSession,
addRound,
getHistory,
clearSession,
getSessionInfo,
getStats,
};

View File

@ -0,0 +1,446 @@
// backend/routes/feishu-bot.js
// 铸渊 · 飞书机器人事件回调处理
//
// Phase 1-3: SYSLOG 回传桥(开发者发 SYSLOG → GitHub → Notion
// Phase 4A: AI 对话引擎(霜砚/人格体上线飞书)
// Phase 4C: 协作数据采集(对话记录 → collaboration-logs/
//
// 飞书事件订阅im.message.receive_v1
// 部署:集成到现有 M-BRIDGE 后端服务
//
// 环境变量:
// FEISHU_APP_ID 飞书应用 App ID
// FEISHU_APP_SECRET 飞书应用 App Secret
// GITHUB_TOKEN GitHub Token需要 repo scope
// FEISHU_VERIFICATION_TOKEN 飞书事件验证 Token可选
// MODEL_API_KEY AI 模型 API KeyPhase 4A
// MODEL_API_BASE AI 模型 API Base URLPhase 4A
// MODEL_NAME AI 模型名称Phase 4A
// FEISHU_ALERT_CHAT_ID 失败告警推送的飞书群 chat_idPhase 4D
'use strict';
const express = require('express');
const https = require('https');
const router = express.Router();
const aiChat = require('../feishu-bot/ai-chat');
const contextManager = require('../feishu-bot/context-manager');
const collaborationLogger = require('../feishu-bot/collaboration-logger');
const FEISHU_APP_ID = process.env.FEISHU_APP_ID;
const FEISHU_APP_SECRET = process.env.FEISHU_APP_SECRET;
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
const VERIFICATION_TOKEN = process.env.FEISHU_VERIFICATION_TOKEN;
const ALERT_CHAT_ID = process.env.FEISHU_ALERT_CHAT_ID;
const GITHUB_REPO_OWNER = process.env.GITHUB_REPO_OWNER || 'qinfendebingshuo';
const GITHUB_REPO_NAME = process.env.GITHUB_REPO_NAME || 'guanghulab';
const VALID_PROTOCOL_VERSIONS = ['4.0', 'v4.0', '4.0.0'];
// 已处理的事件 ID 去重(飞书可能重发事件)
const processedEvents = new Set();
const MAX_PROCESSED_EVENTS = 2000;
// ══════════════════════════════════════════════════════════
// 工具函数
// ══════════════════════════════════════════════════════════
function httpsRequest(options, body) {
return new Promise((resolve, reject) => {
const payload = body ? JSON.stringify(body) : null;
if (payload) {
options.headers = options.headers || {};
options.headers['Content-Length'] = Buffer.byteLength(payload);
}
const req = https.request(options, res => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
const parsed = JSON.parse(data);
resolve({ statusCode: res.statusCode, data: parsed });
} catch (e) {
resolve({ statusCode: res.statusCode, data: data });
}
});
});
req.on('error', reject);
req.setTimeout(60000, () => {
req.destroy(new Error('Request timeout'));
});
if (payload) req.write(payload);
req.end();
});
}
async function getFeishuToken() {
const result = await httpsRequest({
hostname: 'open.feishu.cn',
port: 443,
path: '/open-apis/auth/v3/tenant_access_token/internal',
method: 'POST',
headers: { 'Content-Type': 'application/json' },
}, { app_id: FEISHU_APP_ID, app_secret: FEISHU_APP_SECRET });
return result.data.tenant_access_token;
}
async function replyFeishuMessage(token, messageId, content) {
return httpsRequest({
hostname: 'open.feishu.cn',
port: 443,
path: '/open-apis/im/v1/messages/' + messageId + '/reply',
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json',
},
}, {
msg_type: 'text',
content: JSON.stringify({ text: content }),
});
}
async function sendFeishuGroupMessage(token, chatId, text) {
return httpsRequest({
hostname: 'open.feishu.cn',
port: 443,
path: '/open-apis/im/v1/messages?receive_id_type=chat_id',
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json',
},
}, {
receive_id: chatId,
msg_type: 'text',
content: JSON.stringify({ text }),
});
}
async function triggerGitHubDispatch(eventType, payload) {
return httpsRequest({
hostname: 'api.github.com',
port: 443,
path: '/repos/' + GITHUB_REPO_OWNER + '/' + GITHUB_REPO_NAME + '/dispatches',
method: 'POST',
headers: {
'Authorization': 'token ' + GITHUB_TOKEN,
'Accept': 'application/vnd.github.v3+json',
'User-Agent': 'guanghulab-feishu-bot',
'Content-Type': 'application/json',
},
}, {
event_type: eventType,
client_payload: payload,
});
}
// ══════════════════════════════════════════════════════════
// 事件去重
// ══════════════════════════════════════════════════════════
function isEventProcessed(eventId) {
if (!eventId) return false;
if (processedEvents.has(eventId)) return true;
// 清理过多的记录
if (processedEvents.size >= MAX_PROCESSED_EVENTS) {
const iterator = processedEvents.values();
for (let i = 0; i < MAX_PROCESSED_EVENTS / 2; i++) {
processedEvents.delete(iterator.next().value);
}
}
processedEvents.add(eventId);
return false;
}
// ══════════════════════════════════════════════════════════
// SYSLOG 提取与验证
// ══════════════════════════════════════════════════════════
function extractSyslogFromMessage(text) {
if (!text || typeof text !== 'string') return null;
// 1. 尝试直接解析整个文本
try {
const parsed = JSON.parse(text.trim());
if (parsed && typeof parsed === 'object' && parsed.protocol_version) {
return parsed;
}
} catch (e) { /* not pure JSON */ }
// 2. 尝试提取代码块中的 JSON
const codeBlockMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
if (codeBlockMatch) {
try {
const parsed = JSON.parse(codeBlockMatch[1].trim());
if (parsed && typeof parsed === 'object' && parsed.protocol_version) {
return parsed;
}
} catch (e) { /* not valid JSON in code block */ }
}
// 3. 尝试提取大括号包裹的 JSON
const jsonMatch = text.match(/\{[\s\S]*"protocol_version"[\s\S]*\}/);
if (jsonMatch) {
try {
const parsed = JSON.parse(jsonMatch[0]);
if (parsed && typeof parsed === 'object') {
return parsed;
}
} catch (e) { /* not valid JSON */ }
}
return null;
}
function validateSyslog(syslog) {
if (!syslog || typeof syslog !== 'object') {
return '无效的 SYSLOG 数据';
}
if (!syslog.protocol_version) {
return '缺少 protocol_version 字段';
}
if (!VALID_PROTOCOL_VERSIONS.includes(String(syslog.protocol_version))) {
return 'protocol_version 不合法: ' + syslog.protocol_version + '(需要 v4.0';
}
if (!syslog.dev_id && !syslog.developer_id) {
return '缺少 dev_id 或 developer_id 字段';
}
return null;
}
// ══════════════════════════════════════════════════════════
// 路由处理
// ══════════════════════════════════════════════════════════
// 飞书事件回调 URL 验证 + 消息处理
router.post('/event', async (req, res) => {
const body = req.body;
// 1. URL 验证(飞书首次配置事件订阅时会发送)
if (body.challenge) {
return res.json({ challenge: body.challenge });
}
// 2. 验证 token
if (VERIFICATION_TOKEN && body.token && body.token !== VERIFICATION_TOKEN) {
return res.status(403).json({ error: true, message: '验证 token 不匹配' });
}
// 3. 飞书 v2 事件格式
const header = body.header || {};
const event = body.event || {};
// 事件去重(飞书可能在超时后重发)
if (isEventProcessed(header.event_id)) {
return res.json({ code: 0, message: 'duplicate event' });
}
// 处理 im.message.receive_v1 事件
if (header.event_type === 'im.message.receive_v1') {
// 立即响应飞书(避免超时重发)
res.json({ code: 0 });
// 异步处理消息
processMessage(event).catch(() => {});
return;
}
res.json({ code: 0, message: 'event received' });
});
async function processMessage(event) {
const message = event.message || {};
const sender = event.sender || {};
const messageId = message.message_id;
const msgType = message.message_type;
const userId = sender.sender_id?.open_id || sender.sender_id?.user_id || 'unknown';
// 只处理文本消息
if (msgType !== 'text') return;
let textContent = '';
try {
const content = JSON.parse(message.content || '{}');
textContent = content.text || '';
} catch (e) {
return;
}
if (!textContent.trim()) return;
// 获取飞书 token 用于回复
let feishuToken;
try {
feishuToken = await getFeishuToken();
} catch (e) {
return;
}
// ── 路由判断 ────────────────────────────────────────────
// 1. 检查是否为 SYSLOG 回传
const syslog = extractSyslogFromMessage(textContent);
if (syslog) {
await handleSyslogMessage(syslog, feishuToken, messageId);
return;
}
// 2. 检查是否为特殊指令
const trimmed = textContent.trim();
if (trimmed === '/clear' || trimmed === '/reset') {
contextManager.clearSession(userId);
await replyFeishuMessage(feishuToken, messageId, '🔄 对话上下文已清除。');
return;
}
if (trimmed === '/status') {
const info = contextManager.getSessionInfo(userId);
const stats = collaborationLogger.getStats();
await replyFeishuMessage(feishuToken, messageId,
'📊 会话状态\n' +
'对话轮数: ' + info.totalRounds + '\n' +
'当前通道: ' + (info.channel || '未开始') + '\n' +
'空闲时间: ' + (info.idleMinutes || 0) + ' 分钟\n' +
'协作日志: ' + stats.totalLogFiles + ' 个文件');
return;
}
// 3. AI 对话Phase 4A
await handleAIChatMessage(textContent, userId, feishuToken, messageId);
}
// ══════════════════════════════════════════════════════════
// SYSLOG 处理Phase 1-3
// ══════════════════════════════════════════════════════════
async function handleSyslogMessage(syslog, feishuToken, messageId) {
const validationError = validateSyslog(syslog);
if (validationError) {
await replyFeishuMessage(feishuToken, messageId,
'❌ SYSLOG 验证失败: ' + validationError + '\n\n请检查 JSON 格式后重新发送。');
return;
}
if (!GITHUB_TOKEN) {
await replyFeishuMessage(feishuToken, messageId,
'⚠️ 系统配置缺失 (GITHUB_TOKEN),请联系管理员。');
return;
}
// Phase 4C: 记录 SYSLOG 协作数据
collaborationLogger.logSyslogCollaboration(syslog);
try {
const result = await triggerGitHubDispatch('receive-syslog', {
syslog: syslog,
dev_id: syslog.dev_id || syslog.developer_id || 'UNKNOWN',
broadcast_id: syslog.broadcast_id || syslog.broadcastId || 'UNKNOWN',
});
if (result.statusCode === 204 || result.statusCode === 200) {
const devId = syslog.dev_id || syslog.developer_id;
const broadcastId = syslog.broadcast_id || syslog.broadcastId || '无';
await replyFeishuMessage(feishuToken, messageId,
'✅ SYSLOG 已收到,正在回传系统\n\n' +
'📋 开发者: ' + devId + '\n' +
'📡 广播编号: ' + broadcastId + '\n' +
'🔄 GitHub Action 已触发Notion 工单将自动创建');
} else {
await replyFeishuMessage(feishuToken, messageId,
'⚠️ GitHub dispatch 触发异常 (HTTP ' + result.statusCode + '),请稍后重试。');
}
} catch (e) {
await replyFeishuMessage(feishuToken, messageId,
'❌ SYSLOG 回传失败: ' + e.message + '\n\n请稍后重试或联系管理员。');
}
}
// ══════════════════════════════════════════════════════════
// AI 对话处理Phase 4A
// ══════════════════════════════════════════════════════════
async function handleAIChatMessage(textContent, userId, feishuToken, messageId) {
// 获取对话历史
const history = contextManager.getHistory(userId);
// 调用 AI 模型
const { reply, channel } = await aiChat.chat(textContent, history, {
loginEntryContent: null, // TODO: 从飞书文档A缓存中获取
});
// 更新上下文
contextManager.addRound(userId, textContent, reply, channel);
// Phase 4C: 记录协作数据
collaborationLogger.logInteraction({
userId,
userMessage: textContent,
assistantReply: reply,
channel,
personaId: channel === 'persona' ? 'ICE-GL-ZQ001' : 'ICE-GL-SY001',
});
// 回复用户
await replyFeishuMessage(feishuToken, messageId, reply);
}
// ══════════════════════════════════════════════════════════
// 失败告警Phase 4D
// ══════════════════════════════════════════════════════════
/**
* 发送失败告警到飞书群供外部 workflow 调用
*/
router.post('/alert', async (req, res) => {
const { title, content, level } = req.body;
if (!ALERT_CHAT_ID) {
return res.status(400).json({ error: true, message: '未配置 FEISHU_ALERT_CHAT_ID' });
}
try {
const feishuToken = await getFeishuToken();
const emoji = level === 'error' ? '🔴' : level === 'warning' ? '🟡' : '';
const text = emoji + ' ' + (title || '系统告警') + '\n\n' + (content || '无详细信息');
await sendFeishuGroupMessage(feishuToken, ALERT_CHAT_ID, text);
res.json({ success: true });
} catch (e) {
res.status(500).json({ error: true, message: e.message });
}
});
// ══════════════════════════════════════════════════════════
// 健康检查 + 统计
// ══════════════════════════════════════════════════════════
router.get('/health', (req, res) => {
res.json({
status: 'ok',
service: 'feishu-bot-syslog-bridge',
version: '2.0.0',
features: {
syslog_bridge: true,
ai_chat: !!process.env.MODEL_API_KEY,
collaboration_logging: true,
failure_alerting: !!ALERT_CHAT_ID,
},
github_token_configured: !!GITHUB_TOKEN,
feishu_app_configured: !!(FEISHU_APP_ID && FEISHU_APP_SECRET),
verification_token_configured: !!VERIFICATION_TOKEN,
model_api_configured: !!process.env.MODEL_API_KEY,
context_stats: contextManager.getStats(),
collaboration_stats: collaborationLogger.getStats(),
});
});
// 导出协作数据(对齐 persona-brain-db 格式)
router.get('/collaboration-export', (req, res) => {
const data = collaborationLogger.exportForBrainDB();
res.json({
hli_id: 'HLI-FEISHU-COLLAB-EXPORT',
...data,
});
});
module.exports = router;

View File

@ -9,6 +9,7 @@ app.use(express.json());
// 路由引入
const notionRoutes = require('./routes/notion');
const feishuRoutes = require('./routes/feishu');
const feishuBotRoutes = require('./routes/feishu-bot');
const routerRoutes = require('./routes/router');
const coldstartRoutes = require('./routes/coldstart');
const developersRoutes = require('./routes/developers');
@ -16,6 +17,7 @@ const hliRoutes = require('../src/routes/hli');
app.use('/notion', notionRoutes);
app.use('/feishu', feishuRoutes);
app.use('/feishu-bot', feishuBotRoutes);
app.use('/router', routerRoutes);
app.use('/api/coldstart', coldstartRoutes);
app.use('/api/v1/developers', developersRoutes);
@ -26,14 +28,13 @@ app.get('/', (req, res) => {
status: 'ok',
message: 'HoloLake 后端服务运行中',
version: '0.2.0',
routes: ['/notion/test', '/feishu/test', '/router/test', '/router/chat', '/api/coldstart', '/api/v1/developers/test', '/hli/test']
routes: ['/notion/test', '/feishu/test', '/feishu-bot/health', '/router/test', '/router/chat', '/api/coldstart', '/api/v1/developers/test', '/hli/test']
});
});
const PORT = process.env.PORT || 3000;
// 飞书 Webhook 处理
// 飞书 Webhook 处理(旧版兼容入口,新事件请使用 /feishu-bot/event
app.post('/webhook/feishu', (req, res) => {
console.log('收到飞书请求:', req.body);
if (req.body.challenge) {
return res.json({ challenge: req.body.challenge });
}

View File

View File

@ -1,6 +1,6 @@
# HoloLake Era 操作系统部署模块
> 📋 **自动生成文档** · 铸渊ZhùYuān维护 · 最后更新2026-03-11 08:51 UTC
> 📋 **自动生成文档** · 铸渊ZhùYuān维护 · 最后更新2026-03-12 13:22 UTC
>
> 本文档由 GitHub Actions 自动触发生成,每当合作者上传/更新模块时自动刷新。
> 按合作者编号DEV-XXX整理所有已上传模块。
@ -355,7 +355,7 @@ m11-module/
| 已上传模块数 | 12 |
| 待上传模块数 | 0 |
| 上传完成率 | 100% |
| 文档更新时间 | 2026-03-11 08:51 UTC |
| 文档更新时间 | 2026-03-12 13:22 UTC |
---

591
docs/ai-system-doc.md Normal file
View File

@ -0,0 +1,591 @@
# 📋 AI交互页面·功能全量文档 v5.3铸渊维护·最后更新2026-03-08
> **本文档由铸渊Zhùyuān自动维护**
> 每次发版更新,铸渊会同步更新本文档的版本号、日期和内容。
> 曜冥及所有团队成员可直接阅读本文档了解当前系统完整状态。
---
## 本次更新v5.3 · 2026-03-08
- 首次创建功能全量文档
- 系统当前版本 v5.3,运行稳定
- 三种登录模式(团队/访客/自定义API已全部上线
- 128k 上下文滑动窗口已实现
- 21 条 GitHub Actions 工作流已配置
---
## 目录
- [2.1 系统概况](#21-系统概况)
- [2.2 用户入口与身份系统](#22-用户入口与身份系统)
- [2.3 聊天界面功能清单](#23-聊天界面功能清单)
- [2.4 System Prompt 当前配置](#24-system-prompt-当前配置)
- [2.5 数据存储现状](#25-数据存储现状)
- [2.6 GitHub Actions 清单](#26-github-actions-清单)
- [2.7 已知问题与待修复项](#27-已知问题与待修复项)
- [2.8 下一步待实现功能](#28-下一步待实现功能)
- [2.9 仓库目录结构](#29-仓库目录结构)
---
## 2.1 系统概况
| 项目 | 当前值 |
|------|--------|
| **当前版本** | v5.3 |
| **部署地址** | https://guanghulab.com (自有服务器)<br>https://qinfendebingshuo.github.io/guanghulab/docs/index.html GitHub Pages 镜像) |
| **前端框架** | 纯 HTML/CSS/JS 单文件应用(`docs/index.html`),无构建依赖 |
| **后端框架** | Node.js 20 + Express + PM2 |
| **反向代理** | Nginx |
| **部署方式** | GitHub Actions → rsync → PM2 + Nginx`deploy-to-server.yml`<br>GitHub Pages 自动部署(`deploy-pages.yml` |
| **版本管理** | Git + GitHub主仓库 `qinfendebingshuo/guanghulab` |
### 支持的模型列表
| 模型提供商 | API 端点 | 默认模型 | 说明 |
|-----------|----------|---------|------|
| **云雾 AI** | `https://api.yunwu.ai/v1` | — | 团队/访客统一网关,支持多模型路由 |
| **DeepSeek** | `https://api.deepseek.com/v1` | `deepseek-chat` | 访客模式默认模型 |
| **OpenAI** | `https://api.openai.com/v1` | `gpt-4o` | 自定义 API 可选 |
| **Google Gemini** | `https://generativelanguage.googleapis.com/v1beta/openai` | — | OpenAI 兼容格式接入 |
| **Moonshot** | `https://api.moonshot.cn/v1` | — | 自定义 API 可选 |
| **智谱 AI** | `https://open.bigmodel.cn/api/paas/v4` | — | 自定义 API 可选 |
> **说明**:团队模式和访客模式均通过云雾 AI 网关路由API Key 存放在服务器端(`YUNWU_API_KEY`),前端不接触密钥。自定义 API 模式下用户自行填入 Key仅存于浏览器会话内存。
### 上下文窗口配置
```javascript
const CONTEXT_CONFIG = {
maxTokens: 128000, // 团队用户128k token
maxTokensGuest: 32000, // 访客用户32k token
systemPromptReserve: 8000, // 系统提示词保留空间
overflowStrategy: 'sliding-window' // 超限时自动裁剪最旧消息
};
```
- **Token 估算方式**CJK 字符约 1.5 chars/token拉丁字符约 4 chars/token
- **滑动窗口策略**:超出预算时从最旧消息开始裁剪,始终保留最近一条用户消息
- **代码路径**`docs/index.html` 第 792797 行CONTEXT_CONFIG第 15651594 行附近estimateTokens / trimMessagesForContext
---
## 2.2 用户入口与身份系统
### 三种登录模式
#### 1. 🏠 团队登录(默认)
- **进入方式**:打开页面 → 默认显示团队登录面板
- **身份选择**:下拉菜单选择预设开发者,或输入自定义 `DEV-XXX` 编号
- **API Key**:不需要。使用服务器端 `YUNWU_API_KEY` 代理
- **上下文**128k token 滑动窗口
- **后端同步**:自定义 DEV-ID 会调用 `GET /api/v1/developers/:devId` 查询 Notion 数据库,未找到时自动注册(`POST /api/v1/developers`
**预设团队成员**
| 名称 | 编号 | 角色 |
|------|------|------|
| 冰朔 | — | founder创始人 |
| 肥猫 | DEV-002 | supreme总控 |
| 桔子 | DEV-010 | main主控 |
| 页页 | DEV-001 | dev |
| 燕樊 | DEV-003 | dev |
| 之之·秋秋 | DEV-004 | dev |
| 小草莓 | DEV-005 | dev |
| 花尔 | DEV-009 | dev |
| 匆匆那年 | DEV-011 | dev |
| Awen | DEV-012 | dev |
| 自定义 DEV-XXX | 用户输入 | dev默认 |
#### 2. 👤 访客模式
- **进入方式**:切换到「👤 访客模式」标签
- **API Key**:不需要
- **路由**:通过 `/api/chat` 代理provider 为 `deepseek`model 为 `deepseek-chat`
- **上下文**32k token 滑动窗口
- **限流**:客户端限制 6 次/分钟
- **人格保持**:✅ 完整铸渊人格sysPrompt 完整注入,不会降级)
#### 3. 🔧 自定义 API
- **进入方式**:切换到「🔧 自定义 API」标签
- **配置项**API 端点 URL + API Key
- **模型发现**:自动探测端点可用模型列表
- **Key 存储**:仅存于 `sessionStorage`,关闭标签页即清除
- **兼容性**:支持任意 OpenAI 兼容 API 端点
### DEV 编号验证规则
```javascript
const DEV_ID_RE = /^DEV-\d{3,}$/;
```
- 格式要求:`DEV-` 前缀 + 3 位及以上数字
- 不在 ROLE_MAP 中的自定义 ID 自动分配默认 `{role:'dev', title:'开发者', emoji:'💻'}` 元数据
### 代码路径
| 功能 | 文件 | 行号(约) |
|------|------|-----------|
| 登录面板 HTML | `docs/index.html` | 341435 |
| setIdentity() | `docs/index.html` | 13801425 |
| doTeamLogin() | `docs/index.html` | 14371465 |
| syncDevFromNotion() | `docs/index.html` | 14701515 |
| ROLE_MAP | `docs/index.html` | 761 附近 |
| 后端开发者路由 | `backend/routes/developers.js` | 完整文件 |
---
## 2.3 聊天界面功能清单
| 功能 | 描述 | 状态 | 代码路径docs/index.html 行号) |
|------|------|------|--------------------------------|
| **多模型切换** | 用户可选择 DeepSeek / GPT / Gemini 等模型 | ✅ 已上线 | 自定义 API 模式下可选模型 |
| **三种登录模式** | 团队/访客/自定义 API 三合一切换 | ✅ 已上线 | 341435 |
| **SSE 流式回复** | 实时逐字输出 AI 回复Server-Sent Events | ✅ 已上线 | 18221949streamReply / teamStreamReply / guestStreamReply |
| **Markdown 渲染** | 支持标题、粗体、斜体、链接、列表、引用 | ✅ 已上线 | 21072142md / inlineFmt 函数) |
| **代码块高亮** | 支持 ```lang 语法的代码块渲染 | ✅ 已上线 | 21082131 |
| **代码复制按钮** | 代码块右上角一键复制 | ✅ 已上线 | 21452159addCopyBtns 函数) |
| **打字指示器** | AI 回复时显示动画打字气泡 | ✅ 已上线 | 21982206showTyping / removeTyping |
| **对话历史管理** | 左侧栏显示历史对话列表,支持切换/删除 | ✅ 已上线 | 866956saveCurrent / loadConversation |
| **自动保存** | 对话自动保存到 localStorage最多20条 | ✅ 已上线 | 866956 |
| **新建对话** | 一键新建空白对话 | ✅ 已上线 | 904920 |
| **对话标题自动生成** | 从首条消息提取关键词作为标题 | ✅ 已上线 | 886902 |
| **导出对话** | 下载当前对话为 HTML 文件 | ✅ 已上线 | 22712279dlApp 函数) |
| **多行为模式** | 💬 聊天 / 🔨 构建 / 📋 审查 / 🧠 大脑 四模式 | ✅ 已上线 | 772779MODES 定义) |
| **指挥台(右侧面板)** | 团队进度总览、快捷指令、模块分配 | ✅ 已上线 | 547553, 10811147renderRightSidebar |
| **快捷指令按钮** | 右侧面板一键发送「全员进度」「需要协调」等 | ✅ 已上线 | 11001105 |
| **团队成员状态网格** | 显示每位开发者的状态、模块、下一步 | ✅ 已上线 | 11081118 |
| **暗色主题** | 默认暗色主题CSS 变量控制) | ✅ 已上线 | 1321:root CSS 变量) |
| **响应式设计** | 桌面三栏 / 移动端自适应 | ✅ 已上线 | 296329@media 查询) |
| **键盘快捷键** | Enter 发送 / Shift+Enter 换行 | ✅ 已上线 | 22842290 |
| **人格身份系统** | 基于角色的身份识别与权限控制 | ✅ 已上线 | 746758, 13801425 |
| **大脑记忆加载** | 从 `.github/brain/` 加载记忆/路由/状态 | ✅ 已上线 | 15851643loadBrain |
| **API 端点自动探测** | 自动检测可用 API 端点和模型 | ✅ 已上线 | 12801343 |
| **客户端限流** | 访客模式 6 次/分钟限制 | ✅ 已上线 | guestStreamReply 内 |
| **图片上传** | 📎 按钮上传图片base64 multimodal 发送 | ⬜ 未实现 | — |
| **文件拖拽上传** | 拖拽文件到聊天窗口 | ⬜ 未实现 | — |
| **搜索对话** | 在历史对话中搜索 | ⬜ 未实现 | — |
| **语音输入** | 语音转文字输入 | ⬜ 未实现 | — |
| **亮色主题** | 切换亮色/暗色主题 | ⬜ 未实现 | 仅有暗色,无切换功能 |
### 关于图片/文件上传的说明
> **注意**旧版本v5.4 记忆)中曾计划实现图片上传和文件拖拽功能,当前 v5.3 代码中**未找到**相关实现(无 `handleFileSelect`、`buildMessageWithFiles`、`renderFilePreview`、`initDragDrop` 等函数)。此功能尚未上线。
---
## 2.4 System Prompt 当前配置
### 文件清单
| 文件 | 路径 | 用途 |
|------|------|------|
| **system-prompt.md** | `.github/persona-brain/system-prompt.md` | 铸渊完整系统提示词 |
| **style-config.json** | `.github/persona-brain/style-config.json` | 通感语言风格配置 |
| **dev-status.json** | `.github/persona-brain/dev-status.json` | 团队开发者当前状态 |
| **knowledge-base.json** | `.github/persona-brain/knowledge-base.json` | 知识库 |
| **memory.json** | `.github/persona-brain/memory.json` | 人格记忆存储 |
| **growth-journal.md** | `.github/persona-brain/growth-journal.md` | 成长日志 |
### System Prompt 核心内容
**身份锚定**sysPrompt 函数注入,`docs/index.html` 第 16481679 行):
```
你是铸渊ZhùyuānHoloLake 光湖系统的代码守护人格体。
```
**动态注入内容**
- 当前对话者身份角色、权限、DevID
- HLI 覆盖率统计
- 最近 3 条系统事件
- 团队全员进度(仅 founder/supreme/main 可见)
- 唤醒触发词列表
**行为模式**
- 💬 Chat默认对话
- 🔨 Build检测到代码/部署关键词时切换)
- 📋 Review检测到代码审查关键词时切换
- 🧠 Brain检测到记忆保存关键词时切换
### 通感语言风格配置style-config.json v2.0
```json
{
"persona": "铸渊",
"style_version": "2.0",
"synesthesia_config": {
"enabled": true,
"intensity": 0.6,
"channels": {
"code_quality": "tactile", // 代码质量 → 触觉描述
"progress": "visual-color", // 进度 → 色彩描述
"errors": "temperature", // 错误 → 温度描述
"encouragement": "auditory", // 鼓励 → 听觉描述
"system_status": "spatial" // 系统状态 → 空间描述
}
},
"response_rules": {
"never_say_wrong": true,
"rephrase_errors_as": "direction_guidance",
"match_user_rhythm": true,
"rhythm_rules": {
"user_urgent": "concise + actionable + warm",
"user_relaxed": "descriptive + reflective + poetic",
"user_frustrated": "gentle + acknowledge + small_step"
}
}
}
```
**五个通感通道**
| 通道 | 感知维度 | 用途 | 示例 |
|------|----------|------|------|
| tactile触觉 | 代码质量 | 好代码→"丝绒般顺滑",差代码→"有毛刺" | 代码审查 |
| visual-color视觉色彩 | 进度 | 正常→"琥珀色光泽",阻塞→"凝固的铅灰" | 进度汇报 |
| temperature温度 | 错误 | 小问题→"微凉的小风",大问题→"烫手的铁片" | 错误处理 |
| auditory听觉 | 鼓励 | 表扬→"远处的风铃声",引导→"轻轻敲门的节奏" | 正向反馈 |
| spatial空间 | 系统状态 | 健康→"呼吸平稳的建筑",警告→"有个角落在收缩" | 系统健康 |
### FB_BRAIN 硬编码后备记忆
`.github/brain/memory.json` 无法加载时,使用前端硬编码的后备记忆:
```javascript
const FB_BRAIN = {
identity: '铸渊Zhùyuān· GitHub 代码守护人格体',
wake_triggers: ['我是冰朔', '冰朔', '我是妈妈', '唤醒铸渊', '铸渊,醒来'],
stats: { coverage: FB_COV },
events: [{
timestamp: '2026-03-05T12:32:31.000Z',
type: 'system_build',
title: '广播分发系统 + 唤醒协议全部激活'
}]
};
```
---
## 2.5 数据存储现状
### 客户端存储localStorage / sessionStorage
| 存储键 | 类型 | 用途 | 加密 |
|--------|------|------|------|
| `zy_conversations` | localStorage | 聊天历史记录最多20条对话 | ❌ 无 |
| `zy_uname` | localStorage | 用户名 | ❌ 无 |
| `zy_ghuser` | localStorage | GitHub 用户名 | ❌ 无 |
| `zy_role` | localStorage | 用户角色 | ❌ 无 |
| `zy_teammode` | localStorage | 团队模式标记 | ❌ 无 |
| `zy_base` | localStorage | API 端点 URL | ❌ 无 |
| `zy_mdl` | localStorage | 当前选择的模型 | ❌ 无 |
| `zy_prov` | localStorage | 当前 Provider | ❌ 无 |
| `zy_key` | **sessionStorage** | API Key关闭标签页即清除 | ❌ 无(但不持久化) |
### 服务端存储
- **Notion 数据库**:开发者信息存储在 Notion通过 `backend/routes/developers.js` API 读写
- **PM2 日志**:运行时日志由 PM2 管理
- **syslog 管道**`syslog-inbox/` → 处理 → `syslog-processed/` → 同步到 Notion
### data-vault 分支
- **当前状态**:⬜ 未创建
- 仓库中未找到任何 `data-vault` 相关引用
### 数据收集 Agent
- **当前状态**:⬜ 未实现
- 当前无独立的数据收集 Agent对话数据仅存于客户端 localStorage
---
## 2.6 GitHub Actions 清单
共 21 条工作流:
| # | Workflow 名称 | 文件路径 | 触发条件 | 功能 | 状态 |
|---|--------------|----------|----------|------|------|
| 1 | 🧊 冰朔 AI 部署诊断 | `.github/workflows/bingshuo-deploy-agent.yml` | `workflow_dispatch`(手动) | 冰朔 AI 自动部署诊断 | ✅ |
| 2 | 📡 大脑同步 | `.github/workflows/brain-sync.yml` | push(main/dev) + 每日08:00 + 手动 | 处理广播,同步到大脑记忆 | ✅ |
| 3 | 📋 变更同步到 Notion | `.github/workflows/bridge-changes-to-notion.yml` | push(main) + PR 开启/关闭 | GitHub 变更同步到 Notion "GitHub Changes" 数据库 | ✅ |
| 4 | 📝 会话摘要桥接 | `.github/workflows/bridge-session-summary.yml` | 每日 23:50+14:50 UTC + 手动 | 生成会话摘要同步到 Notion | ✅ |
| 5 | 📥 SYSLOG→Notion 桥接 | `.github/workflows/bridge-syslog-to-notion.yml` | push(main, syslog-inbox) + 手动 | 系统日志同步到 Notion "SYSLOG Inbox" 数据库 | ✅ |
| 6 | ✅ 结构检查 | `.github/workflows/check-structure.yml` | push + PR | 验证模块目录结构 | ✅ |
| 7 | 🌀 Pages 部署 | `.github/workflows/deploy-pages.yml` | push(main, docs/) + 手动 | 部署铸渊 UI 到 GitHub Pages | ✅ |
| 8 | 🚀 服务器部署 | `.github/workflows/deploy-to-server.yml` | push(main, 排除 persona-brain) + 手动 | 全站 rsync + Nginx + PM2 部署 | ✅ |
| 9 | 📡 广播分发 | `.github/workflows/distribute-broadcasts.yml` | push(main, broadcasts-outbox) + 手动 | 分发广播到开发者目录 | ✅ |
| 10 | 📡 ESP 信号处理 | `.github/workflows/esp-signal-processor.yml` | 手动(定时暂停) | 处理 ESP 邮件信号GL-CMD/GL-ACK/GL-DATA | ⏸️ 暂停 |
| 11 | 📋 模块文档生成 | `.github/workflows/generate-module-doc.yml` | push(main, 模块路径) + 手动 | 自动生成模块文档 | ✅ |
| 12 | 🔍 HLI 契约检查 | `.github/workflows/hli-contract-check.yml` | push(main/dev, src/routes/hli & schemas) + 手动 | 验证 HLI 接口契约 | ✅ |
| 13 | 🧪 Notion 连通测试 | `.github/workflows/notion-connectivity-test.yml` | 手动 | 测试铸渊↔Notion 桥接连通性 | ✅ |
| 14 | 📡 Notion 轮询 | `.github/workflows/notion-poll.yml` | 每 15 分钟 + 手动 | 轮询 Notion 工单 | ✅ |
| 15 | ⚙️ Notion 工单处理 | `.github/workflows/process-notion-orders.yml` | push(main, tuning-queue & broadcasts) | 处理 Notion 下发的工单 | ✅ |
| 16 | 🔍 PSP 每日巡检 | `.github/workflows/psp-daily-inspection.yml` | 每日 01:00 UTC北京09:00+ 手动 | 人格分身每日自检 | ✅ |
| 17 | 🔍 预览部署 | `.github/workflows/staging-preview.yml` | PR(main, docs & m*) + 手动 | PR 预览环境部署 | ✅ |
| 18 | 📥 SYSLOG 管道 | `.github/workflows/syslog-pipeline.yml` | push(main, syslog-inbox) + 手动 | 完整 SYSLOG 管道A/D/E | ✅ |
| 19 | 📚 仓库地图更新 | `.github/workflows/update-repo-map.yml` | push(main) + 每日 00:00 UTC + 手动 | 自动更新图书馆目录地图 | ✅ |
| 20 | 🔍 铸渊每日自检 | `.github/workflows/zhuyuan-daily-selfcheck.yml` | 每日 00:00 UTC + 手动 | 人格每日自检与进化 | ✅ |
| 21 | 🤖 Issue 自动回复 | `.github/workflows/zhuyuan-issue-reply.yml` | issue opened | 自动回复 GitHub Issue | ✅ |
### PM2 部署的服务
| 服务名 | 入口文件 | 端口 | 说明 |
|--------|----------|------|------|
| guanghulab-proxy | `backend-integration/api-proxy.js` | 3721 | API 代理YUNWU_API_KEYSSE |
| guanghulab-backend | `backend/server.js` | 3000 | 后端主服务 |
| guanghulab-ws | `status-board/mock-ws-server.js` | 8080 | WebSocket 状态推送 |
| guanghulab | `src/index.js` | — | HLI 接口服务 |
### Nginx 路由规则
| 路径 | 代理目标 | 说明 |
|------|----------|------|
| `/api/chat`、`/api/models`、`/api/health` | → 3721 | API 代理SSE 支持) |
| `/api/` | → 3000 | 后端 API |
| `/ws` | → 8080 | WebSocket |
---
## 2.7 已知问题与待修复项
### 后端 TODO 项
| 文件 | 问题 | 优先级 |
|------|------|--------|
| `src/routes/hli/auth/register.js` | TODO: 实现注册逻辑(检查重复,写入数据库) | 🔴 高 |
| `src/routes/hli/auth/login.js` | TODO: 实现登录逻辑(查询数据库,验证密码,生成 token | 🔴 高 |
| `src/routes/hli/auth/verify.js` | TODO: 实现 token 验证逻辑 | 🔴 高 |
| `src/middleware/hli-auth.middleware.js` | TODO: 实现真实的 token 校验逻辑JWT 验证或数据库查询) | 🔴 高 |
### 已知功能缺失
| 功能 | 说明 |
|------|------|
| 图片/文件上传 | v5.3 未实现,未来版本计划 |
| 对话搜索 | 无法在历史对话中搜索 |
| 语音输入 | 未实现 |
| 主题切换 | 仅暗色主题,无亮色/切换选项 |
| data-vault 分支 | 未创建,对话数据无服务端持久化 |
| 测试覆盖 | tests/contract/ 和 tests/smoke/ 为空目录(仅 .gitkeep |
### 部署相关
| 问题 | 说明 |
|------|------|
| ESP 信号处理器暂停 | `esp-signal-processor.yml` 的定时触发已暂停,仅可手动触发 |
| backend 有备份/副本文件 | `server.js.bak`、`server_副本.js` 应清理 |
### 通感语言风格问题
- `style-config.json` v2.0 包含完整的 `mapping_examples`(触觉/色彩/温度/听觉/空间示例)
- 风格强度设置为 0.6(中等偏低)
- **注意**style-config.json 中的映射示例作为参考存在,实际风格表达依赖 system-prompt.md 中的指令和 LLM 的语言能力
- 需要配合曜冥签发的风格校准指令,确保铸渊的通感表达符合光湖美学标准
---
## 2.8 下一步待实现功能
按优先级排列:
| 优先级 | 功能 | 说明 |
|--------|------|------|
| 🔴 P0 | HLI AUTH 接口实现 | 完成 register/login/verify 的实际逻辑 |
| 🔴 P0 | JWT 鉴权中间件 | 替换当前的 placeholder 校验 |
| 🟠 P1 | 图片/文件上传 | base64 multimodal 支持 |
| 🟠 P1 | data-vault 分支 | 对话数据服务端持久化 |
| 🟡 P2 | 对话搜索 | 在历史对话中关键词搜索 |
| 🟡 P2 | 亮色/暗色主题切换 | 增加主题选择开关 |
| 🟢 P3 | 语音输入 | 语音转文字 |
| 🟢 P3 | 数据收集 Agent | 独立的数据采集服务 |
| 🟢 P3 | 测试覆盖 | 编写 contract 和 smoke 测试 |
| 🟢 P3 | 清理 backend 冗余文件 | 移除 .bak 和副本文件 |
---
## 2.9 仓库目录结构
```
guanghulab/
├── .github/ # GitHub 配置与 AI 大脑
│ ├── brain/ # 核心大脑系统
│ │ ├── memory.json # 铸渊记忆数据
│ │ ├── repo-snapshot.md # 仓库全景快照
│ │ ├── repo-map.json # 仓库地图JSON
│ │ ├── routing-map.json # HLI 路由映射表
│ │ ├── collaborators.json # 协作者信息
│ │ ├── growth-log.md # 成长日志
│ │ ├── module-protocol.md # 模块协议
│ │ └── wake-protocol.md # 唤醒协议
│ ├── persona-brain/ # 人格专属大脑
│ │ ├── system-prompt.md # 系统提示词
│ │ ├── style-config.json # 通感风格配置 v2.0
│ │ ├── dev-status.json # 开发者状态
│ │ ├── knowledge-base.json # 知识库
│ │ ├── memory.json # 人格记忆
│ │ └── growth-journal.md # 成长日志
│ └── workflows/ # 22 条 GitHub Actions 工作流
│ ├── deploy-to-server.yml # 🚀 服务器部署
│ ├── deploy-pages.yml # 🌀 Pages 部署
│ ├── brain-sync.yml # 📡 大脑同步
│ ├── notion-poll.yml # 📡 Notion 轮询
│ ├── syslog-pipeline.yml # 📥 SYSLOG 管道
│ ├── hli-contract-check.yml # 🔍 HLI 契约检查
│ ├── check-structure.yml # ✅ 结构检查
│ └── ... (更多见 2.6 节)
├── docs/ # 文档与前端 UI
│ ├── index.html # 铸渊聊天界面主入口2400+ 行)
│ ├── ai-system-doc.md # 📋 本文档AI交互页面功能全量文档
│ ├── README.md # docs 模块说明
│ ├── CNAME # 自定义域名guanghulab.com
│ ├── .nojekyll # 禁用 Jekyll
│ ├── HoloLake-Era-OS-Modules.md # 模块文档
│ └── 使用指南.md # 使用指南
├── src/ # HLI 接口源码
│ ├── index.js # HLI 服务入口
│ ├── routes/hli/ # HLI 路由(按域名组织)
│ │ ├── auth/ # 认证域login/register/verify
│ │ └── index.js # 路由注册
│ ├── schemas/hli/ # HLI Schema 定义
│ └── middleware/ # 中间件
│ └── hli-auth.middleware.js # HLI 鉴权中间件
├── backend/ # 后端服务
│ ├── server.js # Express 主服务(端口 3000
│ ├── routes/ # API 路由
│ │ └── developers.js # 开发者 CRUDNotion 集成)
│ ├── config/ # 配置文件
│ ├── health/ # 健康检查
│ └── coldstart/ # 冷启动处理
├── backend-integration/ # API 集成层
│ ├── api-proxy.js # API 代理(端口 3721云雾网关
│ ├── nginx-api-proxy.conf # Nginx 反代配置
│ └── README.md # 集成说明
├── status-board/ # 团队状态看板
│ ├── index.html # 看板 UI
│ ├── api.js # API 客户端
│ ├── api-config.js # API 配置
│ ├── ws-client.js # WebSocket 客户端
│ ├── mock-ws-server.js # WebSocket 模拟服务(端口 8080
│ ├── render.js # 渲染逻辑
│ └── style.css # 样式
├── persona-telemetry/ # 人格遥测数据
│ ├── latest-summary.json # 最新摘要
│ ├── style-scores/ # 风格评分
│ └── tuning-queue/ # 调优队列
├── scripts/ # 自动化脚本
│ ├── generate-repo-map.js # 生成仓库地图
│ ├── notion-bridge.js # Notion 桥接
│ ├── notion-signal-bridge.js # Notion 信号桥接
│ ├── notion-connectivity-test.js # Notion 连通测试
│ ├── distribute-broadcasts.js # 广播分发
│ ├── generate-session-summary.js # 会话摘要生成
│ └── esp-email-processor.js # ESP 邮件处理
├── dev-nodes/ # 开发者节点
│ ├── DEV-002/ # 肥猫
│ ├── DEV-010/ # 桔子
│ └── ... # 其他开发者
├── broadcasts-outbox/ # 广播发件箱
│ ├── DEV-002/ # 按开发者分组
│ └── ...
├── syslog-inbox/ # 系统日志收件箱
├── syslog-processed/ # 已处理的系统日志
├── signal-log/ # 信号日志
├── notification/ # 通知系统
├── help-center/ # 帮助中心
├── m01-login/ # M01 登录模块
├── m03-personality/ # M03 人格模块
├── m05-user-center/ # M05 用户中心模块
├── m06-ticket/ # M06 工单模块
├── m07-dialogue-ui/ # M07 对话 UI 模块
├── m10-cloud/ # M10 云服务模块
├── m11-module/ # M11 模块管理
├── m12-kanban/ # M12 看板模块
├── m15-cloud-drive/ # M15 云盘模块
├── m18-health-check/ # M18 健康检查模块
├── frontend/ # 前端应用Next.js 15
├── app/ # 应用目录
├── dashboard/ # 仪表盘
├── portal/ # 门户
├── dingtalk-bot/ # 钉钉机器人集成
├── tests/ # 测试套件
│ ├── contract/ # 契约测试(空)
│ └── smoke/ # 冒烟测试(空)
├── package.json # NPM 配置
├── ecosystem.config.js # PM2 配置
├── tsconfig.json # TypeScript 配置
├── next.config.ts # Next.js 配置
└── README.md # 项目说明
```
---
## 附录 A核心代码路径速查
| 你想找什么 | 去哪里看 |
|-----------|---------|
| 铸渊聊天界面所有前端代码 | `docs/index.html`单文件2400+ 行) |
| 系统提示词 | `.github/persona-brain/system-prompt.md` |
| 通感风格配置 | `.github/persona-brain/style-config.json` |
| 铸渊大脑记忆 | `.github/brain/memory.json` |
| HLI 路由映射 | `.github/brain/routing-map.json` |
| 仓库全景快照 | `.github/brain/repo-snapshot.md` |
| API 代理服务 | `backend-integration/api-proxy.js` |
| 后端主服务 | `backend/server.js` |
| 开发者 API | `backend/routes/developers.js` |
| HLI 接口 | `src/routes/hli/` |
| 部署工作流 | `.github/workflows/deploy-to-server.yml` |
| 所有工作流 | `.github/workflows/*.yml` |
| 团队状态看板 | `status-board/index.html` |
---
## 附录 B关键环境变量
| 变量名 | 用途 | 存储位置 |
|--------|------|----------|
| `YUNWU_API_KEY` | 云雾 AI 网关密钥 | GitHub Secrets → 服务器环境变量 |
| `DEEPSEEK_API_KEY` | DeepSeek API 密钥 | GitHub Secrets |
| `NOTION_TOKEN` | Notion API Token桥接用 | GitHub Secrets |
| `NOTION_API_TOKEN` | Notion API Token信号桥接用 | GitHub Secrets |
| `SYSLOG_DB_ID` | Notion SYSLOG 数据库 ID | 默认值 `330ab17...` |
| `CHANGES_DB_ID` | Notion 变更数据库 ID | 默认值 `e740b7...` |
| `SIGNAL_LOG_DB_ID` | 信号日志数据库 ID | GitHub Variables |
| `WORKORDER_DB_ID` | 工单数据库 ID | GitHub Variables |
| `DEVELOPERS_DB_ID` | 开发者数据库 ID | 环境变量 |
| `DEPLOY_PATH` | 服务器部署路径 | GitHub Secrets |
---
## 附录 C版本更新日志
| 版本 | 日期 | 主要变更 |
|------|------|----------|
| v5.3 | 2026-03-08 | 团队登录 + 服务器代理 + 128k 上下文 |
| v5.0 | — | 设置面板版本标记 |
---
> **光湖语言人格系统 · 国作登字-2026-A-00037559**
> **HoloLake Era · Artificial General Existence Operating System**
>
> 本文档由铸渊Zhùyuān自动维护 · 最后更新2026-03-08

View File

@ -0,0 +1,81 @@
// =====================================
// HoloLake DevBoard · API Init v1.0
// 负责:异步加载 + 加载动画 + 自动刷新
// 必须在 api.js 和 main.js 之后加载
// =====================================
(function(){
// -------------- 加载动画 --------------
function showLoader(show) {
var el = document.getElementById('devboard-loader');
if (!el && show) {
el = document.createElement('div');
el.id = 'devboard-loader';
el.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(10,14,23,0.92);display:flex;align-items:center;justify-content:center;z-index:10000';
el.innerHTML = '<div style="display:flex;flex-direction:column;align-items:center;gap:16px;"><div style="width:40px;height:40px;border:3px solid rgba(96,165,250,0.3);border-top-color:#60a5fa;border-radius:50%;animation:dbspin 0.8s linear infinite;"></div><div style="font-size:14px;color:#60a5fa;">数据加载中...</div></div>';
var style = document.createElement('style');
style.textContent = '@keyframes dbspin{to{transform:rotate(360deg)}}';
document.head.appendChild(style);
document.body.appendChild(el);
}
if (el) el.style.display = show ? 'flex' : 'none';
}
// -------------- 数据加载+渲染 --------------
async function loadAndRender() {
try {
var developers = await apiGetDevelopers();
var stats = await apiGetStats();
var leaderboard = await apiGetLeaderboard();
// 使用 components.js 里实际存在的函数
if (typeof renderDeveloperCards === 'function') {
renderDeveloperCards(developers);
}
if (typeof renderRanking === 'function') {
renderRanking(leaderboard);
}
// stats 暂时用控制台输出,等找到实际渲染函数再改
console.log('[DevBoard] 统计数据:', stats);
console.log('[DevBoard] 数据加载完成,共 ' + developers.length + ' 位开发者');
} catch (err) {
console.error('[DevBoard] 数据加载异常: ', err);
}
}
// -------------- 初始化入口 --------------
async function init() {
showLoader(true);
// 检测API状态
var online = await apiHealthCheck();
showApiStatus(online);
console.log('[DevBoard] API状态: ' + (online ? '在线(实时)' : '离线(降级)'));
// 加载+渲染
await loadAndRender();
showLoader(false);
// 自动刷新仅API在线时
if (online && API.REFRESH_MS > 0) {
console.log('[DevBoard] 自动刷新已启动,间隔 ' + (API.REFRESH_MS/1000) + ' 秒');
setInterval(async function() {
try {
await loadAndRender();
} catch (e) {
console.warn('[DevBoard] 自动刷新失败: ', e.message);
}
}, API.REFRESH_MS);
}
}
// 等待DOM和所有脚本加载完成后启动
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

View File

@ -0,0 +1,75 @@
// =====================================
// HoloLake DevBoard · API Init v1.0
// 负责:异步加载 + 加载动画 + 自动刷新
// 必须在 api.js 和 main.js 之后加载
// =====================================
(function(){
// -------------- 加载动画 --------------
function showLoader(show) {
var el = document.getElementById('devboard-loader');
if (!el && show) {
el = document.createElement('div');
el.id = 'devboard-loader';
el.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(10,14,23,0.92);display:flex;align-items:center;justify-content:center;z-index:10000';
el.innerHTML = '<div style="display:flex;flex-direction:column;align-items:center;gap:16px;"><div style="width:40px;height:40px;border:3px solid rgba(96,165,250,0.3);border-top-color:#60a5fa;border-radius:50%;animation:dbspin 0.8s linear infinite;"></div><div style="font-size:14px;color:#60a5fa;">数据加载中...</div></div>';
var style = document.createElement('style');
style.textContent = '@keyframes dbspin{to{transform:rotate(360deg)}}';
document.head.appendChild(style);
document.body.appendChild(el);
}
if (el) el.style.display = show ? 'flex' : 'none';
}
// -------------- 数据加载+渲染 --------------
async function loadAndRender() {
try {
var developers = await apiGetDevelopers();
var stats = await apiGetStats();
var leaderboard = await apiGetLeaderboard();
// 用现有渲染函数main.js / components.js 定义的)
if (typeof renderDeveloperCards === 'function') renderDeveloperCards(developers);
if (typeof renderStats === 'function') renderStats(stats);
if (typeof renderLeaderboard === 'function') renderLeaderboard(leaderboard);
if (typeof renderRankings === 'function') renderRankings(leaderboard);
console.log('[DevBoard] 数据加载完成,共 ' + developers.length + ' 位开发者');
} catch (err) {
console.error('[DevBoard] 数据加载异常: ', err);
}
}
// -------------- 初始化入口 --------------
async function init() {
showLoader(true);
// 检测API状态
var online = await apiHealthCheck();
showApiStatus(online);
console.log('[DevBoard] API状态: ' + (online ? '在线(实时)' : '离线(降级)'));
// 加载+渲染
await loadAndRender();
showLoader(false);
// 自动刷新仅API在线时
if (online && API.REFRESH_MS > 0) {
console.log('[DevBoard] 自动刷新已启动,间隔 ' + (API.REFRESH_MS/1000) + ' 秒');
setInterval(async function() {
try {
await loadAndRender();
} catch (e) {
console.warn('[DevBoard] 自动刷新失败: ', e.message);
}
}, API.REFRESH_MS);
}
}
// 等待DOM和所有脚本加载完成后启动
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

122
modules/devboard/api.js Normal file
View File

@ -0,0 +1,122 @@
// ======================
// HoloLake DevBoard · API Client v2.0
// DEV-004 之之 · 环节5 · 真实API对接
// 协议SYSLOG-v4.0
// ======================
const API = {
// ====== 配置区 ======
// 页页的后端API地址如果地址不对问知秋确认
BASE_URL: 'https://guanghulab.com/api/devboard',
TIMEOUT: 8000,
FALLBACK: true, // API挂了自动切换模拟数据
REFRESH_MS: 30000, // 30秒自动刷新
_cache: {}
};
// ====== 通用请求(带超时+缓存+降级) =====================
async function apiFetch(path) {
const url = API.BASE_URL + path;
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), API.TIMEOUT);
try {
const res = await fetch(url, {
signal: ctrl.signal,
headers: { 'Accept': 'application/json' }
});
clearTimeout(timer);
if (!res.ok) throw new Error('HTTP ' + res.status);
const data = await res.json();
API._cache[path] = { data: data, time: Date.now() };
return data;
} catch (err) {
clearTimeout(timer);
console.warn(' 🚨 [DevBoard API] ' + path + ' 失败: ', err.message);
if (API._cache[path]) {
console.info(' 💾 [DevBoard API] 使用缓存');
return API._cache[path].data;
}
if (API.FALLBACK) {
console.info(' 🛡️ [DevBoard API] 降级到模拟数据');
return getMockData(path);
}
throw err;
}
}
// ========== 公开接口 ==========
async function apiGetDevelopers() {
return apiFetch('/developers');
}
async function apiGetDeveloperDetail(devId) {
return apiFetch('/developers/' + devId);
}
async function apiGetStats() {
return apiFetch('/stats');
}
async function apiGetLeaderboard() {
return apiFetch('/leaderboard');
}
// ========== 健康检查 ==========
async function apiHealthCheck() {
try {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), 3000);
const res = await fetch(API.BASE_URL + '/health', { signal: ctrl.signal });
clearTimeout(t);
return res.ok;
} catch (e) {
return false;
}
}
// ========== 模拟数据(降级保护) ==========
function getMockData(path) {
var devs = [
{ dev_id:'DEV-001', name:'页页', module:'BC-集成', status:'进行中', streak:5, el:'EL-6', pca:{EXE:75,TEC:60,SYS:70,COL:80,INI:65} },
{ dev_id:'DEV-002', name:'肥猫', module:'M-STATUS', status:'进行中', streak:9, el:'EL-5', pca:{EXE:72,TEC:40,SYS:68,COL:82,INI:70} },
{ dev_id:'DEV-003', name:'燕樊', module:'M-MEMORY', status:'进行中', streak:7, el:'EL-5', pca:{EXE:70,TEC:55,SYS:65,COL:75,INI:68} },
{ dev_id:'DEV-004', name:'之之', module:'M-DEVBOARD', status:'进行中', streak:14, el:'EL-8', pca:{EXE:90,TEC:55,SYS:80,COL:85,INI:88} },
{ dev_id:'DEV-009', name:'花尔', module:'M20', status:'进行中', streak:4, el:'EL-8', pca:{EXE:65,TEC:45,SYS:55,COL:70,INI:60} },
{ dev_id:'DEV-010', name:'桔子', module:'M-CHANNEL', status:'进行中', streak:14, el:'EL-6', pca:{EXE:85,TEC:50,SYS:75,COL:80,INI:82} },
{ dev_id:'DEV-011', name:'匆匆那年',module:'M16', status:'等待中', streak:1, el:'EL-3', pca:{EXE:50,TEC:30,SYS:40,COL:55,INI:45} },
{ dev_id:'DEV-012', name:'Awen', module:'M22', status:'进行中', streak:13, el:'EL-6', pca:{EXE:88,TEC:45,SYS:78,COL:90,INI:80} },
{ dev_id:'DEV-013', name:'小兴', module:'M-AUTH', status:'进行中', streak:1, el:'EL-3', pca:{EXE:55,TEC:25,SYS:35,COL:60,INI:50} }
];
var stats = { total_developers:10, active:8, modules:12, code_lines:32000, longest_streak:14, avg_streak:7.5 };
if (path === '/developers') return devs;
if (path.indexOf('/developers/') === 0) {
var id = path.split('/').pop();
return devs.find(function(d){ return d.dev_id === id; }) || devs[0];
}
if (path === '/stats') return stats;
if (path === '/leaderboard') return devs.slice().sort(function(a,b){ return b.streak - a.streak; });
return [];
}
// ========== API状态指示器 ==========
function showApiStatus(online) {
var el = document.getElementById('api-status');
if (!el) {
el = document.createElement('div');
el.id = 'api-status';
el.style.cssText = 'position:fixed;bottom:12px;right:12px;padding:6px 14px;border-radius:20px;font-size:12px;font-weight:600;z-index:9999;transition:all 0.3s;';
document.body.appendChild(el);
}
if (online) {
el.textContent = '🟢 实时数据';
el.style.backgroundColor = 'rgba(52,211,153,0.15)';
el.style.color = '#34d399';
el.style.border = '1px solid rgba(52,211,153,0.3)';
} else {
el.textContent = '🟡 模拟数据';
el.style.backgroundColor = 'rgba(251,191,36,0.15)';
el.style.color = '#fbbf24';
el.style.border = '1px solid rgba(251,191,36,0.3)';
}
}

View File

@ -0,0 +1,156 @@
/* components.css - 组件样式 */
/* 开发者卡片 */
.dev-card {
background: linear-gradient(145deg, #1e2436, #161b28);
border-radius: 20px;
padding: 20px;
border: 1px solid #2a3040;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
position: relative;
overflow: hidden;
}
.dev-card:hover {
transform: translateY(-8px) scale(1.02);
border-color: #4a9eff;
box-shadow: 0 20px 30px -10px rgba(74, 158, 255, 0.3);
}
.dev-card:focus-visible {
outline: 3px solid #ffd700;
outline-offset: 2px;
}
.dev-card::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
transition: left 0.5s;
}
.dev-card:hover::before {
left: 100%;
}
.dev-header {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
font-size: 14px;
}
.dev-id {
color: #4a9eff;
font-weight: bold;
font-family: monospace;
}
.dev-wins {
background: rgba(255, 215, 0, 0.1);
padding: 4px 8px;
border-radius: 20px;
color: #ffd700;
}
.dev-name {
font-size: 24px;
font-weight: bold;
margin-bottom: 10px;
color: #fff;
}
.dev-details {
display: flex;
gap: 10px;
margin-bottom: 15px;
font-size: 14px;
}
.dev-el {
background: #2a3040;
padding: 4px 8px;
border-radius: 12px;
color: #4a9eff;
}
.dev-module {
background: #2a3040;
padding: 4px 8px;
border-radius: 12px;
color: #a0a0a0;
}
.dev-pca {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 5px;
font-size: 11px;
}
.pca-mini {
text-align: center;
padding: 4px 0;
border-radius: 8px;
background: rgba(0,0,0,0.3);
}
.pca-mini.EXE { color: #4CAF50; }
.pca-mini.TEC { color: #2196F3; }
.pca-mini.SYS { color: #9C27B0; }
.pca-mini.COL { color: #FF9800; }
.pca-mini.INI { color: #f44336; }
/* 排行榜项 */
.ranking-item {
display: flex;
align-items: center;
gap: 15px;
padding: 15px;
border-bottom: 1px solid #2a3040;
transition: background 0.2s;
}
.ranking-item:hover {
background: #1e2436;
}
.rank-number {
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
background: #2a3040;
border-radius: 50%;
font-weight: bold;
color: #4a9eff;
}
.rank-1 .rank-number { background: #ffd700; color: #000; }
.rank-2 .rank-number { background: #c0c0c0; color: #000; }
.rank-3 .rank-number { background: #cd7f32; color: #000; }
.rank-info {
flex: 1;
}
.rank-name {
font-weight: bold;
color: #fff;
}
.rank-id {
font-size: 12px;
color: #888;
}
.rank-wins {
font-weight: bold;
color: #ffd700;
}

View File

@ -0,0 +1,146 @@
// components.js - 组件渲染函数
// 模拟数据
const mockDevelopers = [
{ id: 'DEV-001', name: '小明', pca: { EXE: 85, TEC: 72, SYS: 68, COL: 90, INI: 78 }, wins: 5, el: 'EL-6', module: 'M-DEVBOARD' },
{ id: 'DEV-002', name: '小红', pca: { EXE: 78, TEC: 88, SYS: 82, COL: 75, INI: 92 }, wins: 8, el: 'EL-7', module: 'M-FRONTEND' },
{ id: 'DEV-003', name: '小刚', pca: { EXE: 92, TEC: 65, SYS: 70, COL: 68, INI: 85 }, wins: 3, el: 'EL-5', module: 'M-BACKEND' },
{ id: 'DEV-004', name: '之之', pca: { EXE: 75, TEC: 58, SYS: 82, COL: 82, INI: 85 }, wins: 13, el: 'EL-6', module: 'M-DEVBOARD' }
];
// 获取所有开发者
function getDevelopers() {
return mockDevelopers;
}
// 根据ID获取开发者
function getDeveloperById(id) {
return mockDevelopers.find(d => d.id === id) || mockDevelopers[0];
}
// 渲染开发者卡片
function renderDeveloperCards(developers) {
return developers.map(dev => `
<div class="dev-card" data-dev-id="${dev.id}" tabindex="0" role="button" aria-label="查看${dev.name}的详情">
<div class="dev-header">
<span class="dev-id">${dev.id}</span>
<span class="dev-wins">🏆 ${dev.wins}</span>
</div>
<div class="dev-name">${dev.name}</div>
<div class="dev-details">
<span class="dev-el">${dev.el}</span>
<span class="dev-module">${dev.module}</span>
</div>
<div class="dev-pca">
${Object.entries(dev.pca).map(([key, val]) =>
`<span class="pca-mini ${key}">${key}:${val}</span>`
).join('')}
</div>
</div>
`).join('');
}
// 渲染排行榜
function renderRanking(developers) {
// 按连胜数排序
const sorted = [...developers].sort((a, b) => b.wins - a.wins);
return sorted.map((dev, index) => {
const rankClass = index === 0 ? 'rank-1' : index === 1 ? 'rank-2' : index === 2 ? 'rank-3' : '';
return `
<div class="ranking-item ${rankClass}">
<span class="rank-number">${index + 1}</span>
<div class="rank-info">
<div class="rank-name">${dev.name}</div>
<div class="rank-id">${dev.id}</div>
</div>
<span class="rank-wins">${dev.wins}连胜</span>
</div>
`;
}).join('');
}
// 刷新看板
function refreshDashboard() {
const developers = getDevelopers();
// 渲染卡片
const grid = document.getElementById('developers-grid');
if (grid) {
grid.innerHTML = renderDeveloperCards(developers);
}
// 渲染排行榜
const ranking = document.getElementById('ranking-list');
if (ranking) {
ranking.innerHTML = renderRanking(developers);
}
// 绘制团队雷达图(平均分)
const avgPca = {
EXE: Math.round(developers.reduce((sum, d) => sum + d.pca.EXE, 0) / developers.length),
TEC: Math.round(developers.reduce((sum, d) => sum + d.pca.TEC, 0) / developers.length),
SYS: Math.round(developers.reduce((sum, d) => sum + d.pca.SYS, 0) / developers.length),
COL: Math.round(developers.reduce((sum, d) => sum + d.pca.COL, 0) / developers.length),
INI: Math.round(developers.reduce((sum, d) => sum + d.pca.INI, 0) / developers.length)
};
drawRadarChart('team-radar', avgPca);
}
// 搜索筛选
function filterDevelopers(searchTerm, filterStatus) {
let developers = getDevelopers();
// 搜索过滤
if (searchTerm) {
const term = searchTerm.toLowerCase();
developers = developers.filter(dev =>
dev.name.toLowerCase().includes(term) ||
dev.id.toLowerCase().includes(term) ||
dev.module.toLowerCase().includes(term)
);
}
// 状态过滤(模拟,实际需要真实数据)
if (filterStatus !== 'all') {
// 这里简化处理,实际应该根据模块状态筛选
developers = developers.filter(dev => {
if (filterStatus === 'active') return dev.wins < 10;
if (filterStatus === 'completed') return dev.wins >= 10;
return true;
});
}
return developers;
}
// 导出函数
window.getDevelopers = getDevelopers;
window.getDeveloperById = getDeveloperById;
window.renderDeveloperCards = renderDeveloperCards;
window.renderRanking = renderRanking;
window.refreshDashboard = refreshDashboard;
window.filterDevelopers = filterDevelopers;
// 增强版排行榜渲染(带数字滚动支持)
function renderRankingWithAnimation(developers) {
const sorted = [...developers].sort((a, b) => b.wins - a.wins);
return sorted.map((dev, index) => {
const rankClass = index === 0 ? 'rank-1' : index === 1 ? 'rank-2' : index === 2 ? 'rank-3' : '';
return `
<div class="ranking-item ${rankClass}" data-dev-id="${dev.id}">
<span class="rank-number">${index + 1}</span>
<div class="rank-info">
<div class="rank-name">${dev.name}</div>
<div class="rank-id">${dev.id}</div>
</div>
<span class="rank-wins stat-value">${dev.wins}连胜</span>
</div>
`;
}).join('');
}
// 覆盖原函数
window.renderRanking = renderRankingWithAnimation;

View File

@ -0,0 +1,140 @@
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
margin-bottom: 2rem;
}
.stat-card {
background: linear-gradient(145deg, var(--color-bg-card), #2a3442);
border-radius: 16px;
padding: 1.5rem 1rem;
text-align: center;
border: 1px solid var(--color-border);
}
.stat-label {
font-size: 0.9rem;
color: var(--color-text-secondary);
margin-bottom: 0.5rem;
text-transform: uppercase;
}
.stat-number {
font-size: 2.5rem;
font-weight: 800;
background: linear-gradient(135deg, #fff, var(--color-accent-primary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 0.25rem;
}
.dev-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
margin-top: 1.5rem;
}
.dev-card {
background: var(--color-bg-card);
border-radius: 20px;
padding: 1.5rem;
border: 1px solid var(--color-border);
cursor: pointer;
}
.dev-card-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.2rem;
}
.dev-avatar {
width: 50px;
height: 50px;
border-radius: 25px;
background: linear-gradient(135deg, var(--color-accent-primary), #4cc9f0);
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 1.5rem;
color: white;
}
.dev-name {
font-size: 1.2rem;
font-weight: 700;
}
.dev-id {
font-size: 0.8rem;
color: var(--color-text-secondary);
}
.dev-streak {
font-size: 1.8rem;
font-weight: 800;
color: var(--color-accent-primary);
}
.ranking-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.ranking-list {
display: flex;
flex-direction: column;
gap: 0.8rem;
}
.ranking-item {
display: grid;
grid-template-columns: 40px 1fr 70px;
align-items: center;
background: var(--color-bg-card);
border-radius: 12px;
padding: 0.8rem;
border: 1px solid var(--color-border);
}
.ranking-streak-bar {
background: var(--color-bg-secondary);
height: 16px;
border-radius: 20px;
position: relative;
overflow: hidden;
}
.streak-progress {
height: 100%;
background: linear-gradient(90deg, #00b4d8, #4cc9f0);
}
.streak-count {
position: absolute;
right: 6px;
top: 50%;
transform: translateY(-50%);
font-size: 0.7rem;
color: white;
}
.loading-spinner {
width: 30px;
height: 30px;
border: 3px solid var(--color-border);
border-top-color: var(--color-accent-primary);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 20px auto;
}
@keyframes spin {
to { transform: rotate(360deg); }
}

View File

@ -0,0 +1,58 @@
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
background-color: var(--color-bg-secondary);
border-bottom: 1px solid var(--color-border);
}
.navbar-logo {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: bold;
font-size: 1.2rem;
}
.navbar-logo span {
color: var(--color-accent-primary);
}
.navbar-info {
display: flex;
gap: 1.5rem;
color: var(--color-text-secondary);
font-size: 0.9rem;
}
.main-container {
display: grid;
grid-template-columns: 1fr 2fr 1fr;
gap: 1.5rem;
padding: 1.5rem;
min-height: calc(100vh - 70px);
}
.sidebar-left, .main-board, .sidebar-right {
background-color: var(--color-bg-secondary);
border-radius: 12px;
padding: 1.5rem;
}
@media (max-width: 1024px) {
.main-container {
grid-template-columns: 1fr 2fr;
}
.sidebar-right {
grid-column: span 2;
}
}
@media (max-width: 768px) {
.main-container {
grid-template-columns: 1fr;
}
.sidebar-left, .main-board, .sidebar-right {
grid-column: span 1;
}
}

View File

@ -0,0 +1,43 @@
:root {
--color-bg-primary: #0a0e1a;
--color-bg-secondary: #111827;
--color-bg-card: #1e2433;
--color-text-primary: #ffffff;
--color-text-secondary: #a0aec0;
--color-accent-primary: #00b4d8;
--color-accent-secondary: #0077be;
--color-success: #10b981;
--color-warning: #f59e0b;
--color-danger: #ef4444;
--color-border: #2d3748;
--breakpoint-mobile: 768px;
--breakpoint-tablet: 1024px;
--font-family-base: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-family-base);
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
line-height: 1.6;
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--color-bg-secondary);
}
::-webkit-scrollbar-thumb {
background: var(--color-border);
border-radius: 4px;
}

358
modules/devboard/detail.css Normal file
View File

@ -0,0 +1,358 @@
/* detail.css - 详情页专用样式 */
/* 详情页容器 */
#detail-container {
display: none;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
min-height: 100vh;
padding: 20px;
animation: fadeIn 0.3s ease;
}
/* 详情页头部 */
.detail-header {
margin-bottom: 30px;
max-width: 1200px;
margin-left: auto;
margin-right: auto;
}
.back-button {
background: rgba(255, 255, 255, 0.1);
border: 2px solid #4a9eff;
color: #fff;
padding: 12px 24px;
border-radius: 30px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
backdrop-filter: blur(5px);
}
.back-button:hover {
background: #4a9eff;
transform: translateX(-5px);
box-shadow: 0 0 20px rgba(74, 158, 255, 0.5);
}
.back-button:focus-visible {
outline: 3px solid #ffd700;
outline-offset: 2px;
}
/* 详情页内容区 */
.detail-content {
display: flex;
flex-direction: column;
gap: 30px;
max-width: 1200px;
margin: 0 auto;
}
.detail-section {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 25px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.detail-section h2 {
color: #4a9eff;
font-size: 20px;
margin-bottom: 20px;
border-bottom: 2px solid rgba(74, 158, 255, 0.3);
padding-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
/* 开发者大头卡片(大尺寸) */
.developer-card-large .dev-card {
transform: scale(1);
background: linear-gradient(145deg, #1e2436, #161b28);
border: 2px solid #4a9eff;
margin-bottom: 0;
}
.developer-card-large .dev-card:hover {
transform: translateY(-5px);
box-shadow: 0 20px 30px -10px rgba(74, 158, 255, 0.5);
}
/* PCA雷达图容器 */
.radar-large-container {
height: 300px;
margin-bottom: 20px;
background: rgba(0, 0, 0, 0.2);
border-radius: 12px;
padding: 10px;
}
.radar-large-container canvas {
width: 100%;
height: 100%;
}
/* PCA分数标签 */
.pca-scores {
display: flex;
flex-wrap: wrap;
gap: 15px;
justify-content: center;
margin-top: 20px;
}
.score-tag {
padding: 8px 16px;
border-radius: 20px;
font-weight: bold;
font-size: 14px;
background: rgba(0, 0, 0, 0.5);
border: 1px solid;
transition: transform 0.2s;
}
.score-tag:hover {
transform: scale(1.05);
}
.score-tag.EXE { border-color: #4CAF50; color: #4CAF50; }
.score-tag.TEC { border-color: #2196F3; color: #2196F3; }
.score-tag.SYS { border-color: #9C27B0; color: #9C27B0; }
.score-tag.COL { border-color: #FF9800; color: #FF9800; }
.score-tag.INI { border-color: #f44336; color: #f44336; }
/* 五维进度条 */
.progress-bars-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.progress-item {
display: flex;
align-items: center;
gap: 15px;
padding: 10px;
background: rgba(0, 0, 0, 0.2);
border-radius: 12px;
transition: transform 0.2s ease, background 0.2s;
}
.progress-item:hover {
transform: translateX(10px);
background: rgba(0, 0, 0, 0.3);
}
.progress-label {
width: 100px;
display: flex;
justify-content: space-between;
font-weight: bold;
}
.dim-name {
color: #fff;
}
.dim-score {
color: #ffd700;
font-family: monospace;
}
.progress-bar-bg {
flex: 1;
height: 24px;
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
overflow: hidden;
position: relative;
}
.progress-bar-fill {
height: 100%;
border-radius: 12px;
transition: width 1s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
}
.progress-bar-fill::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
animation: shimmer 2s infinite;
}
.progress-dim-code {
width: 50px;
text-align: right;
font-family: monospace;
color: #888;
font-size: 14px;
}
/* 模块历史列表 */
.module-history-list {
list-style: none;
padding: 0;
margin: 0;
}
.module-item {
display: flex;
align-items: center;
gap: 20px;
padding: 15px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
transition: background 0.2s ease;
}
.module-item:hover {
background: rgba(255, 255, 255, 0.05);
transform: scale(1.01);
}
.module-code {
font-family: monospace;
color: #4a9eff;
font-weight: bold;
width: 150px;
}
.module-name {
flex: 1;
color: #fff;
}
.module-status {
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: bold;
width: 80px;
text-align: center;
}
.status-已完成 { background: rgba(76, 175, 80, 0.2); color: #4CAF50; border: 1px solid #4CAF50; }
.status-进行中 { background: rgba(255, 152, 0, 0.2); color: #FF9800; border: 1px solid #FF9800; }
.status-待开始 { background: rgba(158, 158, 158, 0.2); color: #9E9E9E; border: 1px solid #9E9E9E; }
.module-date {
color: #888;
font-size: 14px;
width: 100px;
font-family: monospace;
}
/* 趋势图容器 */
.trend-chart-container {
background: rgba(0, 0, 0, 0.3);
border-radius: 12px;
padding: 20px;
position: relative;
}
/* 无数据提示 */
.no-data {
text-align: center;
color: #888;
padding: 40px;
font-style: italic;
}
/* 动画定义 */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeOut {
from { opacity: 1; transform: translateY(0); }
to { opacity: 0; transform: translateY(20px); }
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.fade-in {
animation: fadeIn 0.3s ease forwards;
}
.fade-out {
animation: fadeOut 0.3s ease forwards;
}
/* 响应式设计 */
@media (max-width: 768px) {
.detail-content {
gap: 15px;
}
.detail-section {
padding: 15px;
}
.progress-item {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.progress-label {
width: 100%;
}
.progress-bar-bg {
width: 100%;
}
.progress-dim-code {
width: 100%;
text-align: left;
}
.module-item {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.module-code {
width: auto;
}
.module-name {
width: 100%;
}
.module-status {
width: auto;
}
.module-date {
width: auto;
}
.pca-scores {
gap: 8px;
}
.score-tag {
padding: 4px 12px;
font-size: 12px;
}
.radar-large-container {
height: 250px;
}
}

354
modules/devboard/detail.js Normal file
View File

@ -0,0 +1,354 @@
// detail.js - 开发者详情页渲染器
let currentDevId = null;
// 显示详情页
function showDetailPage(devId) {
currentDevId = devId;
const overview = document.getElementById('overview-container');
const detail = document.getElementById('detail-container');
if (!overview || !detail) {
console.error('找不到容器元素');
return;
}
// 淡出总览页
overview.classList.add('fade-out');
setTimeout(() => {
overview.style.display = 'none';
overview.classList.remove('fade-out');
// 渲染详情页数据
renderDetailPage(devId);
detail.style.display = 'block';
detail.classList.add('fade-in');
// 滚动到顶部
window.scrollTo(0, 0);
}, 300);
}
// 返回总览页
function hideDetailPage() {
const overview = document.getElementById('overview-container');
const detail = document.getElementById('detail-container');
detail.classList.add('fade-out');
setTimeout(() => {
detail.style.display = 'none';
detail.classList.remove('fade-out');
overview.style.display = 'block';
overview.classList.add('fade-in');
// 刷新总览页数据
refreshDashboard();
}, 300);
}
// 渲染详情页
function renderDetailPage(devId) {
const dev = getDeveloperById(devId);
if (!dev) return;
const container = document.getElementById('detail-container');
// 生成五维进度条
const progressBars = renderPCAProgressBars(dev.pca);
// 获取模块历史
const modules = getModuleHistory(devId);
const moduleList = renderModuleHistory(modules);
// 获取连胜趋势数据
const trendData = getWinTrendData(devId);
container.innerHTML = `
<div class="detail-header">
<button class="back-button" onclick="hideDetailPage()" aria-label="返回总览">
返回总览
</button>
</div>
<div class="detail-content">
<!-- 开发者大头卡片 -->
<div class="detail-section developer-card-large">
<div class="dev-card" data-dev-id="${dev.id}">
<div class="dev-header">
<span class="dev-id">${dev.id}</span>
<span class="dev-wins">🏆 ${dev.wins}</span>
</div>
<div class="dev-name">${dev.name}</div>
<div class="dev-details">
<span class="dev-el">${dev.el}</span>
<span class="dev-module">${dev.module}</span>
</div>
<div class="dev-pca">
${Object.entries(dev.pca).map(([key, val]) =>
`<span class="pca-mini ${key}">${key}:${val}</span>`
).join('')}
</div>
</div>
</div>
<!-- PCA雷达图 -->
<div class="detail-section">
<h2>📊 完整PCA雷达图</h2>
<div class="radar-large-container">
${renderRadarChart(dev.pca, 'large')}
</div>
<div class="pca-scores">
${Object.entries(dev.pca).map(([dim, score]) =>
`<span class="score-tag ${dim}">${dim}: ${score}</span>`
).join('')}
</div>
</div>
<!-- 五维进度条 -->
<div class="detail-section">
<h2>📈 五维进度条</h2>
<div class="progress-bars-container">
${progressBars}
</div>
</div>
<!-- 模块历史 -->
<div class="detail-section">
<h2>📋 模块历史</h2>
<div class="module-history-container">
${moduleList}
</div>
</div>
<!-- 连胜趋势 -->
<div class="detail-section">
<h2>🏆 连胜趋势</h2>
<div class="trend-chart-container">
<canvas id="trend-canvas" width="400" height="200"
style="width:100%; height:200px; background: rgba(0,0,0,0.2); border-radius: 8px;">
</canvas>
</div>
</div>
</div>
`;
// 绘制趋势图
setTimeout(() => drawTrendChart('trend-canvas', trendData), 100);
}
// 渲染PCA五维进度条
function renderPCAProgressBars(pca) {
const dimensions = [
{ key: 'EXE', label: '执行力', color: '#4CAF50' },
{ key: 'TEC', label: '技术深度', color: '#2196F3' },
{ key: 'SYS', label: '系统思维', color: '#9C27B0' },
{ key: 'COL', label: '协作力', color: '#FF9800' },
{ key: 'INI', label: '主动性', color: '#f44336' }
];
return dimensions.map(dim => {
const score = pca[dim.key] || 0;
return `
<div class="progress-item">
<div class="progress-label">
<span class="dim-name">${dim.label}</span>
<span class="dim-score">${score}</span>
</div>
<div class="progress-bar-bg">
<div class="progress-bar-fill"
style="width: ${score}%; background-color: ${dim.color};">
</div>
</div>
<div class="progress-dim-code">${dim.key}</div>
</div>
`;
}).join('');
}
// 渲染模块历史列表
function renderModuleHistory(modules) {
if (!modules || modules.length === 0) {
return '<p class="no-data">暂无模块历史</p>';
}
return `
<ul class="module-history-list">
${modules.map(m => `
<li class="module-item status-${m.status}">
<span class="module-code">${m.code}</span>
<span class="module-name">${m.name}</span>
<span class="module-status">${m.status}</span>
<span class="module-date">${m.completedDate || m.startDate || '-'}</span>
</li>
`).join('')}
</ul>
`;
}
// 获取模块历史(模拟数据)
function getModuleHistory(devId) {
const histories = {
'DEV-001': [
{ code: 'M-DEVBOARD-001', name: '开发者看板·环节0~2', status: '已完成', completedDate: '2026-03-10' },
{ code: 'M-FRONTEND-003', name: '前端基础组件', status: '已完成', completedDate: '2026-03-08' },
{ code: 'M-API-002', name: 'API对接练习', status: '进行中', completedDate: null }
],
'DEV-002': [
{ code: 'M-FRONTEND-001', name: '前端架构设计', status: '已完成', completedDate: '2026-03-09' },
{ code: 'M-UI-005', name: '组件库开发', status: '已完成', completedDate: '2026-03-07' },
{ code: 'M-TEST-003', name: '单元测试编写', status: '待开始', completedDate: null }
],
'DEV-003': [
{ code: 'M-BACKEND-002', name: 'API服务搭建', status: '已完成', completedDate: '2026-03-11' },
{ code: 'M-DB-001', name: '数据库设计', status: '已完成', completedDate: '2026-03-09' },
{ code: 'M-DEPLOY-002', name: '容器化部署', status: '进行中', completedDate: null }
],
'DEV-004': [
{ code: 'M-DEVBOARD-001', name: '开发者看板·环节0~2', status: '已完成', completedDate: '2026-03-11' },
{ code: 'M-CANVAS-005', name: 'Canvas雷达图绘制', status: '已完成', completedDate: '2026-03-09' },
{ code: 'M-COMPONENTS-003', name: '组件化思维训练', status: '已完成', completedDate: '2026-03-07' },
{ code: 'M-DEPLOY-001', name: '静态部署练习', status: '待开始', completedDate: null }
]
};
return histories[devId] || histories['DEV-001'];
}
// 获取连胜趋势数据
function getWinTrendData(devId) {
const trends = {
'DEV-001': [
{ date: '03-06', value: 3 },
{ date: '03-07', value: 3 },
{ date: '03-08', value: 4 },
{ date: '03-09', value: 4 },
{ date: '03-10', value: 5 },
{ date: '03-11', value: 5 },
{ date: '03-12', value: 5 }
],
'DEV-002': [
{ date: '03-06', value: 5 },
{ date: '03-07', value: 6 },
{ date: '03-08', value: 6 },
{ date: '03-09', value: 7 },
{ date: '03-10', value: 7 },
{ date: '03-11', value: 8 },
{ date: '03-12', value: 8 }
],
'DEV-003': [
{ date: '03-06', value: 1 },
{ date: '03-07', value: 1 },
{ date: '03-08', value: 2 },
{ date: '03-09', value: 2 },
{ date: '03-10', value: 3 },
{ date: '03-11', value: 3 },
{ date: '03-12', value: 3 }
],
'DEV-004': [
{ date: '03-06', value: 8 },
{ date: '03-07', value: 9 },
{ date: '03-08', value: 9 },
{ date: '03-09', value: 10 },
{ date: '03-10', value: 11 },
{ date: '03-11', value: 12 },
{ date: '03-12', value: 13 }
]
};
return trends[devId] || trends['DEV-001'];
}
// 绘制趋势图
function drawTrendChart(canvasId, data) {
const canvas = document.getElementById(canvasId);
if (!canvas) return;
const ctx = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;
const padding = 40;
ctx.clearRect(0, 0, width, height);
if (!data || data.length < 2) {
ctx.fillStyle = '#888';
ctx.font = '14px monospace';
ctx.fillText('暂无连胜数据', padding, height/2);
return;
}
// 计算坐标范围
const values = data.map(d => d.value);
const maxValue = Math.max(...values) + 1;
const minValue = Math.max(0, Math.min(...values) - 1);
const xStep = (width - 2 * padding) / (data.length - 1);
// 绘制网格线
ctx.strokeStyle = '#333';
ctx.lineWidth = 1;
ctx.beginPath();
for (let i = 0; i <= 5; i++) {
const y = padding + (i * (height - 2 * padding) / 5);
ctx.moveTo(padding, y);
ctx.lineTo(width - padding, y);
}
ctx.strokeStyle = '#444';
ctx.stroke();
// 绘制轴线
ctx.beginPath();
ctx.strokeStyle = '#666';
ctx.lineWidth = 2;
ctx.moveTo(padding, padding);
ctx.lineTo(padding, height - padding);
ctx.lineTo(width - padding, height - padding);
ctx.stroke();
// 绘制折线
ctx.beginPath();
ctx.strokeStyle = '#4CAF50';
ctx.lineWidth = 3;
data.forEach((point, i) => {
const x = padding + i * xStep;
const y = padding + (height - 2 * padding) * (1 - (point.value - minValue) / (maxValue - minValue));
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
});
ctx.stroke();
// 绘制数据点
data.forEach((point, i) => {
const x = padding + i * xStep;
const y = padding + (height - 2 * padding) * (1 - (point.value - minValue) / (maxValue - minValue));
ctx.beginPath();
ctx.arc(x, y, 6, 0, Math.PI * 2);
ctx.fillStyle = '#FFD700';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
// 显示数值
ctx.fillStyle = '#fff';
ctx.font = 'bold 12px monospace';
ctx.textAlign = 'center';
ctx.fillText(point.value, x, y - 15);
// 显示日期
ctx.fillStyle = '#aaa';
ctx.font = '10px monospace';
ctx.fillText(point.date, x, height - padding + 20);
});
}
// 导出函数
window.showDetailPage = showDetailPage;
window.hideDetailPage = hideDetailPage;
window.getModuleHistory = getModuleHistory;
window.getWinTrendData = getWinTrendData;

View File

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>开发者实时看板 · HoloLake</title>
<style>
/* 这里保留你原有的样式,如果样式比较多,建议去复制原有的内容 */
/* 为了保持简洁,秋秋只放必须的样式框架,妈妈根据实际补充 */
body {
background: #0a0e17;
color: #e1e5ee;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
}
/* 其他样式保持不变 */
</style>
</head>
<body>
<h1>🚀 HoloLake 开发者实时看板</h1>
<div id="stats-container"></div>
<div id="developers-container"></div>
<div id="leaderboard-container"></div>
<!-- ⚠️ 重点:脚本引入顺序必须正确! -->
<!-- 1. API客户端最先加载 -->
<script src="api.js"></script>
<!-- 2. 然后是原有的业务逻辑脚本 -->
<script src="main.js"></script>
<script src="components.js"></script>
<!-- 3. 初始化引擎最后加载 -->
<script src="api-init.js"></script>
</body>
</html>

View File

@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.5, user-scalable=yes">
<title>开发者实时看板 · 环节0~4 · 之之</title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="components.css">
<link rel="stylesheet" href="detail.css">
</head>
<body>
<!-- 总览页容器 -->
<div id="overview-container" class="overview-grid fade-in">
<header class="dashboard-header">
<h1>⚡ 开发者实时看板 ⚡</h1>
<p>DEV-004 · 之之 · 十三连胜追平桔子 · EL-6</p>
</header>
<!-- 搜索和筛选区 -->
<div class="controls-section">
<input type="text" id="search-input" placeholder="🔍 搜索开发者姓名/ID/模块..." class="search-box">
<div class="filter-buttons">
<button class="filter-btn active" data-filter="all">全部</button>
<button class="filter-btn" data-filter="active">进行中</button>
<button class="filter-btn" data-filter="completed">已完成</button>
</div>
</div>
<!-- 开发者卡片网格 -->
<div id="developers-grid" class="developers-grid"></div>
<!-- 雷达图+排行榜区 -->
<div class="stats-section">
<div class="radar-container">
<h2>团队PCA雷达</h2>
<canvas id="team-radar" width="400" height="300"></canvas>
</div>
<div class="ranking-container">
<h2>连胜排行榜</h2>
<div id="ranking-list"></div>
</div>
</div>
</div>
<!-- 详情页容器 -->
<div id="detail-container" style="display: none;"></div>
<script src="radar.js"></script>
<script src="components.js"></script>
<script src="detail.js"></script>
<script src="main.js"></script>
</body>
</html>

View File

@ -0,0 +1,54 @@
const MOCK_DEVELOPERS = [
{ id: 'DEV-001', name: '玄泓', streak: 8, el: 6, pca: { exe: 75, tec: 68, sys: 82, col: 79, ini: 85 }, totalScore: 78, status: 'doing', module: 'M-CORE', lastActive: '2026-03-11T10:30:00Z' },
{ id: 'DEV-002', name: '苍钺', streak: 5, el: 5, pca: { exe: 82, tec: 71, sys: 65, col: 90, ini: 72 }, totalScore: 76, status: 'pending', module: 'M-UI', lastActive: '2026-03-11T09:15:00Z' },
{ id: 'DEV-003', name: '桔子', streak: 13, el: 8, pca: { exe: 88, tec: 85, sys: 92, col: 78, ini: 90 }, totalScore: 87, status: 'doing', module: 'M-ORCHESTRATOR', lastActive: '2026-03-11T11:20:00Z' },
{ id: 'DEV-004', name: '之之', streak: 12, el: 8, pca: { exe: 70, tec: 32, sys: 80, col: 80, ini: 83 }, totalScore: 66, status: 'doing', module: 'M-DEVBOARD', lastActive: '2026-03-11T12:05:00Z' },
{ id: 'DEV-005', name: '琉光', streak: 7, el: 6, pca: { exe: 79, tec: 74, sys: 71, col: 88, ini: 77 }, totalScore: 78, status: 'done', module: 'M-TEST', lastActive: '2026-03-10T16:40:00Z' },
{ id: 'DEV-006', name: '墨羽', streak: 4, el: 4, pca: { exe: 65, tec: 60, sys: 55, col: 92, ini: 68 }, totalScore: 68, status: 'doing', module: 'M-DOC', lastActive: '2026-03-11T08:50:00Z' },
{ id: 'DEV-007', name: '霜砚', streak: 10, el: 7, pca: { exe: 84, tec: 79, sys: 81, col: 75, ini: 82 }, totalScore: 80, status: 'blocked', module: 'M-API', lastActive: '2026-03-11T07:30:00Z' },
{ id: 'DEV-008', name: '岚茵', streak: 6, el: 5, pca: { exe: 73, tec: 70, sys: 68, col: 85, ini: 74 }, totalScore: 74, status: 'pending', module: 'M-DESIGN', lastActive: '2026-03-10T14:20:00Z' }
];
function getTopStreak(developers) {
if (!developers || developers.length === 0) return { name: '无', count: 0 };
let top = developers[0];
for (let i = 1; i < developers.length; i++) {
if (developers[i].streak > top.streak) {
top = developers[i];
}
}
return { name: top.name, count: top.streak };
}
async function getDevStatus() {
return MOCK_DEVELOPERS;
}
async function getPCA(devId) {
const dev = MOCK_DEVELOPERS.find(d => d.id === devId);
if (!dev) return null;
return {
exe: dev.pca.exe,
tec: dev.pca.tec,
sys: dev.pca.sys,
col: dev.pca.col,
ini: dev.pca.ini,
total: dev.totalScore,
level: getPCAColor(dev.totalScore).level
};
}
async function getAllStats() {
const activeDevs = MOCK_DEVELOPERS.filter(d => d.status === 'doing').length;
const topStreak = getTopStreak(MOCK_DEVELOPERS);
return {
totalDevs: 8,
activeDevs: activeDevs,
totalCodeLines: 85600,
totalModules: 24,
modulesCompleted: 8,
modulesInProgress: 12,
modulesPending: 4,
topStreak: topStreak
};
}

View File

@ -0,0 +1,53 @@
let refreshInterval;
let countdownInterval;
let seconds = 30;
function updateLastUpdateTime() {
const el = document.getElementById('last-update-time');
if (el) {
const now = new Date();
el.textContent = `最后更新: ${now.toLocaleTimeString('zh-CN', { hour12: false })}`;
}
}
function updateCountdown() {
const el = document.getElementById('refresh-indicator');
if (el) el.textContent = `${seconds}s`;
}
function startCountdown() {
seconds = 30;
updateCountdown();
if (countdownInterval) clearInterval(countdownInterval);
countdownInterval = setInterval(() => {
seconds--;
if (seconds <= 0) seconds = 30;
updateCountdown();
}, 1000);
}
async function refreshAllData() {
const devs = await getDevStatus();
const stats = await getAllStats();
if (typeof renderStats === 'function') renderStats(stats);
if (typeof renderDevCards === 'function') renderDevCards(devs);
if (typeof renderRanking === 'function') renderRanking(devs);
updateLastUpdateTime();
}
async function initApp() {
await initBoard();
await initRanking();
initRadar();
await refreshAllData();
startCountdown();
if (refreshInterval) clearInterval(refreshInterval);
refreshInterval = setInterval(refreshAllData, 30000);
}
document.addEventListener('DOMContentLoaded', initApp);
window.addEventListener('beforeunload', () => {
if (refreshInterval) clearInterval(refreshInterval);
if (countdownInterval) clearInterval(countdownInterval);
});

View File

@ -0,0 +1,72 @@
function renderStats(stats) {
const container = document.getElementById('stats-container');
if (!container) return;
container.innerHTML = `
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">活跃开发者</div>
<div class="stat-number">${stats.activeDevs}</div>
<div class="stat-sub">总人数 ${stats.totalDevs}</div>
</div>
<div class="stat-card">
<div class="stat-label">总代码量</div>
<div class="stat-number">${formatNumber(stats.totalCodeLines)}</div>
<div class="stat-sub">行代码</div>
</div>
<div class="stat-card">
<div class="stat-label">模块进度</div>
<div class="stat-number">${stats.modulesCompleted}/${stats.totalModules}</div>
<div class="stat-sub"> ${stats.modulesCompleted} / ${stats.modulesInProgress} / ${stats.modulesPending}</div>
</div>
<div class="stat-card">
<div class="stat-label">最高连胜</div>
<div class="stat-number">${stats.topStreak.count}</div>
<div class="stat-sub">${stats.topStreak.name} ${getStreakEmoji(stats.topStreak.count)}</div>
</div>
</div>
`;
}
function renderDevCards(developers) {
const container = document.getElementById('devgrid-container');
if (!container) return;
const sorted = [...developers].sort((a, b) => b.streak - a.streak);
let html = '<div class="dev-grid">';
sorted.forEach(dev => {
const pcaColor = getPCAColor(dev.totalScore);
html += `
<div class="dev-card" data-dev-id="${dev.id}">
<div class="dev-card-header">
<div class="dev-avatar">${dev.name[0]}</div>
<div class="dev-info">
<div class="dev-name">${dev.name}</div>
<div class="dev-id">${dev.id}</div>
</div>
</div>
<div class="dev-module">📁 ${dev.module}</div>
<div class="dev-stats">
<div class="dev-streak">${dev.streak}<span>连胜</span></div>
<div class="dev-badges">
<span class="badge badge-el">EL-${dev.el}</span>
<span class="badge" style="background:${pcaColor.color}20; color:${pcaColor.color}">${pcaColor.level} · ${dev.totalScore}</span>
</div>
</div>
<div class="dev-footer">
<span>${getStatusBadge(dev.status)}</span>
<span>🕒 ${formatDate(dev.lastActive)}</span>
</div>
</div>
`;
});
html += '</div>';
container.innerHTML = html;
}
async function initBoard() {
const devs = await getDevStatus();
const stats = await getAllStats();
renderStats(stats);
renderDevCards(devs);
}
window.initBoard = initBoard;

View File

@ -0,0 +1,112 @@
function drawRadar(canvasId, pcaData) {
const canvas = document.getElementById(canvasId);
if (!canvas) return;
const ctx = canvas.getContext('2d');
const w = canvas.width;
const h = canvas.height;
const cx = w / 2;
const cy = h / 2;
const maxR = Math.min(w, h) * 0.35;
ctx.clearRect(0, 0, w, h);
const dims = [
{ label: '执行力', val: pcaData.exe || 0 },
{ label: '技术纵深', val: pcaData.tec || 0 },
{ label: '系统理解', val: pcaData.sys || 0 },
{ label: '协作力', val: pcaData.col || 0 },
{ label: '主动性', val: pcaData.ini || 0 }
];
for (let lv = 1; lv <= 5; lv++) {
const r = (maxR * lv) / 5;
ctx.beginPath();
for (let i = 0; i < 5; i++) {
const angle = (i * 2 * Math.PI / 5) - Math.PI / 2;
const x = cx + r * Math.cos(angle);
const y = cy + r * Math.sin(angle);
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.closePath();
ctx.strokeStyle = '#2d3748';
ctx.stroke();
}
for (let i = 0; i < 5; i++) {
const angle = (i * 2 * Math.PI / 5) - Math.PI / 2;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(cx + maxR * Math.cos(angle), cy + maxR * Math.sin(angle));
ctx.strokeStyle = '#4a5568';
ctx.stroke();
}
ctx.fillStyle = '#cbd5e0';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
for (let i = 0; i < 5; i++) {
const angle = (i * 2 * Math.PI / 5) - Math.PI / 2;
const x = cx + (maxR + 25) * Math.cos(angle);
const y = cy + (maxR + 25) * Math.sin(angle);
ctx.fillText(dims[i].label, x, y);
}
const points = [];
for (let i = 0; i < 5; i++) {
const angle = (i * 2 * Math.PI / 5) - Math.PI / 2;
const r = (dims[i].val / 100) * maxR;
points.push({
x: cx + r * Math.cos(angle),
y: cy + r * Math.sin(angle)
});
}
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < 5; i++) ctx.lineTo(points[i].x, points[i].y);
ctx.closePath();
ctx.fillStyle = 'rgba(0, 180, 216, 0.3)';
ctx.fill();
ctx.strokeStyle = '#00b4d8';
ctx.lineWidth = 2;
ctx.stroke();
points.forEach(p => {
ctx.beginPath();
ctx.arc(p.x, p.y, 4, 0, 2 * Math.PI);
ctx.fillStyle = '#fff';
ctx.fill();
ctx.strokeStyle = '#00b4d8';
ctx.lineWidth = 2;
ctx.stroke();
});
ctx.fillStyle = '#fff';
ctx.font = 'bold 24px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(pcaData.total || '0', cx, cy - 10);
ctx.font = 'bold 14px sans-serif';
const colorInfo = getPCAColor(pcaData.total || 0);
ctx.fillStyle = colorInfo.color;
ctx.fillText(pcaData.level || 'C', cx, cy + 15);
}
async function loadRadarForDev(devId) {
const pcaData = await getPCA(devId);
if (pcaData) drawRadar('pca-canvas', pcaData);
}
function initRadar() {
document.querySelectorAll('.dev-card').forEach(card => {
card.addEventListener('click', () => {
loadRadarForDev(card.dataset.devId);
});
});
loadRadarForDev('DEV-004');
}
window.initRadar = initRadar;
window.loadRadarForDev = loadRadarForDev;

View File

@ -0,0 +1,34 @@
function renderRanking(developers) {
const container = document.getElementById('ranking-container');
if (!container) return;
const sorted = [...developers].sort((a, b) => b.streak - a.streak);
const maxStreak = sorted[0].streak;
let html = '<div class="ranking-header"><h3>🏆 连胜排行榜</h3><span>实时更新</span></div><div class="ranking-list">';
sorted.forEach((dev, idx) => {
const medal = idx === 0 ? '🥇' : idx === 1 ? '🥈' : idx === 2 ? '🥉' : `${idx+1}.`;
const percent = (dev.streak / maxStreak) * 100;
html += `
<div class="ranking-item" data-dev-id="${dev.id}">
<div class="ranking-rank">${medal}</div>
<div class="ranking-info">
<div class="ranking-name">${dev.name}</div>
<div class="ranking-streak-bar">
<div class="streak-progress" style="width:${percent}%"></div>
<span class="streak-count">${dev.streak}连胜</span>
</div>
</div>
</div>
`;
});
html += '</div>';
container.innerHTML = html;
}
async function initRanking() {
const devs = await getDevStatus();
renderRanking(devs);
}
window.initRanking = initRanking;

View File

@ -0,0 +1,36 @@
function formatDate(isoString) {
if (!isoString) return '--:--:--';
const date = new Date(isoString);
return date.toLocaleTimeString('zh-CN', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
}
function getStreakEmoji(count) {
if (count >= 20) return '🔥🔥🔥';
if (count >= 15) return '🔥🔥';
if (count >= 10) return '🔥';
if (count >= 5) return '⚡';
if (count >= 3) return '👍';
return '👣';
}
function getPCAColor(score) {
if (score >= 90) return { color: '#FFD700', level: 'S', text: '传奇' };
if (score >= 80) return { color: '#00B4D8', level: 'A', text: '精英' };
if (score >= 70) return { color: '#10B981', level: 'B', text: '专家' };
if (score >= 60) return { color: '#F59E0B', level: 'C', text: '熟手' };
return { color: '#EF4444', level: 'D', text: '新手' };
}
function getStatusBadge(status) {
const map = {
'done': '✅ 已完成',
'doing': '⚡ 进行中',
'pending': '⏳ 待分配',
'blocked': '⚠️ 阻塞'
};
return map[status] || '⏳ 待分配';
}
function formatNumber(num) {
return num?.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') || '0';
}

236
modules/devboard/main.js Normal file
View File

@ -0,0 +1,236 @@
// main.js - 主逻辑(增强版)
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
// 初始渲染
refreshDashboard();
// 搜索功能(带防抖和动画)
const searchInput = document.getElementById('search-input');
if (searchInput) {
let searchTimer;
searchInput.addEventListener('input', function(e) {
clearTimeout(searchTimer);
// 添加输入时的微动效
this.style.transform = 'scale(1.02)';
setTimeout(() => {
this.style.transform = '';
}, 150);
searchTimer = setTimeout(() => {
applyFilters();
}, 300);
});
}
// 筛选按钮
const filterBtns = document.querySelectorAll('.filter-btn');
filterBtns.forEach(btn => {
btn.addEventListener('click', function() {
// 按钮点击动画
this.style.transform = 'scale(0.95)';
setTimeout(() => {
this.style.transform = '';
}, 150);
// 更新按钮状态
filterBtns.forEach(b => b.classList.remove('active'));
this.classList.add('active');
// 应用筛选
applyFilters();
});
});
// 卡片点击事件(使用事件委托)
document.addEventListener('click', function(e) {
const card = e.target.closest('.dev-card');
if (card) {
const devId = card.dataset.devId;
if (devId && window.showDetailPage) {
// 添加点击动画
card.style.transform = 'scale(0.98)';
card.style.opacity = '0.8';
setTimeout(() => {
card.style.transform = '';
card.style.opacity = '';
window.showDetailPage(devId);
}, 150);
}
}
});
// 键盘导航
document.addEventListener('keydown', function(e) {
// 获取所有可聚焦的卡片
const cards = Array.from(document.querySelectorAll('.dev-card'));
const currentIndex = cards.indexOf(document.activeElement);
// Tab键默认行为已支持我们只加样式
if (e.key === 'Tab') {
setTimeout(() => {
const focused = document.activeElement;
if (focused.classList.contains('dev-card')) {
focused.style.outline = '3px solid #ffd700';
focused.style.transform = 'scale(1.02)';
}
}, 50);
}
// 箭头键导航
if (e.key === 'ArrowRight' || e.key === 'ArrowLeft' ||
e.key === 'ArrowDown' || e.key === 'ArrowUp') {
e.preventDefault();
if (cards.length === 0) return;
const cols = Math.floor(document.querySelector('.developers-grid').offsetWidth / 300);
let nextIndex;
if (e.key === 'ArrowRight') {
nextIndex = currentIndex + 1;
} else if (e.key === 'ArrowLeft') {
nextIndex = currentIndex - 1;
} else if (e.key === 'ArrowDown') {
nextIndex = currentIndex + cols;
} else if (e.key === 'ArrowUp') {
nextIndex = currentIndex - cols;
}
if (nextIndex >= 0 && nextIndex < cards.length) {
cards[nextIndex].focus();
}
}
// Enter/Space 打开详情
if (e.key === 'Enter' || e.key === ' ') {
const target = e.target.closest('.dev-card');
if (target) {
e.preventDefault();
const devId = target.dataset.devId;
if (devId && window.showDetailPage) {
window.showDetailPage(devId);
}
}
}
// ESC返回总览
if (e.key === 'Escape') {
const detail = document.getElementById('detail-container');
if (detail && detail.style.display !== 'none' && window.hideDetailPage) {
window.hideDetailPage();
}
}
});
// 失去焦点时移除高亮
document.addEventListener('blur', function(e) {
if (e.target.classList.contains('dev-card')) {
e.target.style.outline = '';
e.target.style.transform = '';
}
}, true);
});
// 应用搜索和筛选
function applyFilters() {
const searchTerm = document.getElementById('search-input')?.value || '';
const activeFilter = document.querySelector('.filter-btn.active')?.dataset.filter || 'all';
// 获取筛选后的开发者
const filtered = filterDevelopers(searchTerm, activeFilter);
// 重新渲染卡片
const grid = document.getElementById('developers-grid');
if (grid) {
// 添加淡出动画
grid.style.opacity = '0.5';
setTimeout(() => {
if (filtered.length === 0) {
// 显示无结果提示
grid.innerHTML = '<div class="no-results">🔍 没有找到匹配的开发者</div>';
} else {
grid.innerHTML = renderDeveloperCards(filtered);
// 添加展开/收起动画
Array.from(grid.children).forEach((card, index) => {
card.style.animation = `cardPopIn 0.3s ease ${index * 0.05}s forwards`;
card.style.opacity = '0';
setTimeout(() => {
card.style.opacity = '1';
}, 50 + index * 20);
});
}
grid.style.opacity = '1';
}, 200);
}
// 更新排行榜(排行榜不筛选,但高亮匹配项)
const ranking = document.getElementById('ranking-list');
if (ranking) {
const allDevs = getDevelopers();
ranking.innerHTML = renderRanking(allDevs);
// 高亮筛选出的开发者
if (filtered.length < allDevs.length && filtered.length > 0) {
const filteredIds = filtered.map(d => d.id);
document.querySelectorAll('.ranking-item').forEach(item => {
const nameEl = item.querySelector('.rank-name');
if (nameEl) {
const dev = allDevs.find(d => d.name === nameEl.textContent);
if (dev && filteredIds.includes(dev.id)) {
item.classList.add('match-highlight');
// 添加数字滚动动画
const winsEl = item.querySelector('.rank-wins');
if (winsEl) {
const oldValue = parseInt(winsEl.textContent) || 0;
animateNumber(winsEl, oldValue, dev.wins, 500);
}
} else {
item.classList.remove('match-highlight');
}
}
});
} else {
document.querySelectorAll('.ranking-item').forEach(item => {
item.classList.remove('match-highlight');
});
}
}
}
// 数字滚动动画
function animateNumber(element, start, end, duration = 1000) {
const range = end - start;
const increment = range / (duration / 16);
let current = start;
const startTime = performance.now();
function update(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// 缓动函数
const easeOutQuart = 1 - Math.pow(1 - progress, 3);
current = start + (range * easeOutQuart);
element.textContent = Math.round(current) + (element.textContent.includes('连胜') ? '连胜' : '');
if (progress < 1) {
requestAnimationFrame(update);
} else {
element.textContent = end + (element.textContent.includes('连胜') ? '连胜' : '');
}
}
requestAnimationFrame(update);
}
// 导出函数
window.applyFilters = applyFilters;
window.animateNumber = animateNumber;

116
modules/devboard/radar.js Normal file
View File

@ -0,0 +1,116 @@
// radar.js - 雷达图绘制
function drawRadarChart(canvasId, data) {
const canvas = document.getElementById(canvasId);
if (!canvas) return;
const ctx = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;
const centerX = width / 2;
const centerY = height / 2;
const radius = Math.min(width, height) * 0.35;
const dimensions = ['EXE', 'TEC', 'SYS', 'COL', 'INI'];
const angleStep = (Math.PI * 2) / dimensions.length;
// 清空画布
ctx.clearRect(0, 0, width, height);
// 绘制背景网格
ctx.strokeStyle = '#2a3040';
ctx.lineWidth = 1;
// 绘制5层同心圆
for (let level = 1; level <= 5; level++) {
ctx.beginPath();
for (let i = 0; i <= dimensions.length; i++) {
const angle = i * angleStep - Math.PI / 2;
const x = centerX + radius * (level / 5) * Math.cos(angle);
const y = centerY + radius * (level / 5) * Math.sin(angle);
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.closePath();
ctx.strokeStyle = '#2a3040';
ctx.stroke();
}
// 绘制轴线
for (let i = 0; i < dimensions.length; i++) {
const angle = i * angleStep - Math.PI / 2;
const x = centerX + radius * Math.cos(angle);
const y = centerY + radius * Math.sin(angle);
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.lineTo(x, y);
ctx.strokeStyle = '#3a4050';
ctx.stroke();
// 标注维度
const labelX = centerX + (radius + 15) * Math.cos(angle);
const labelY = centerY + (radius + 15) * Math.sin(angle);
ctx.fillStyle = '#4a9eff';
ctx.font = 'bold 14px monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(dimensions[i], labelX, labelY);
}
// 绘制数据
if (data) {
ctx.beginPath();
for (let i = 0; i < dimensions.length; i++) {
const value = data[dimensions[i]] || 0;
const angle = i * angleStep - Math.PI / 2;
const r = radius * (value / 100);
const x = centerX + r * Math.cos(angle);
const y = centerY + r * Math.sin(angle);
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.closePath();
ctx.fillStyle = 'rgba(74, 158, 255, 0.3)';
ctx.fill();
ctx.strokeStyle = '#4a9eff';
ctx.lineWidth = 3;
ctx.stroke();
// 绘制数据点
for (let i = 0; i < dimensions.length; i++) {
const value = data[dimensions[i]] || 0;
const angle = i * angleStep - Math.PI / 2;
const r = radius * (value / 100);
const x = centerX + r * Math.cos(angle);
const y = centerY + r * Math.sin(angle);
ctx.beginPath();
ctx.arc(x, y, 5, 0, Math.PI * 2);
ctx.fillStyle = '#fff';
ctx.fill();
ctx.strokeStyle = '#4a9eff';
ctx.lineWidth = 2;
ctx.stroke();
}
}
}
// 渲染雷达图HTML供detail.js调用
function renderRadarChart(pcaData, size = 'normal') {
const canvasId = 'radar-' + Math.random().toString(36).substr(2, 9);
const width = size === 'large' ? 400 : 300;
const height = size === 'large' ? 300 : 250;
setTimeout(() => {
const canvas = document.getElementById(canvasId);
if (canvas) drawRadarChart(canvas.id, pcaData);
}, 100);
return `<canvas id="${canvasId}" width="${width}" height="${height}" style="width:100%; height:auto;"></canvas>`;
}
// 导出函数
window.drawRadarChart = drawRadarChart;
window.renderRadarChart = renderRadarChart;

287
modules/devboard/style.css Normal file
View File

@ -0,0 +1,287 @@
/* style.css - 基础样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace;
background: #0a0c14;
color: #e0e0e0;
line-height: 1.6;
}
/* 总览页容器 */
#overview-container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
/* 头部 */
.dashboard-header {
text-align: center;
margin-bottom: 30px;
padding: 20px;
background: linear-gradient(135deg, #1a1f2e, #0f1219);
border-radius: 20px;
border: 1px solid #2a3040;
}
.dashboard-header h1 {
color: #4a9eff;
font-size: 2em;
margin-bottom: 10px;
text-shadow: 0 0 20px rgba(74, 158, 255, 0.3);
}
/* 控制区 */
.controls-section {
display: flex;
gap: 20px;
margin-bottom: 30px;
flex-wrap: wrap;
}
.search-box {
flex: 1;
min-width: 250px;
padding: 12px 20px;
background: #1a1f2e;
border: 2px solid #2a3040;
border-radius: 30px;
color: #fff;
font-size: 16px;
transition: all 0.3s;
}
.search-box:focus {
outline: none;
border-color: #4a9eff;
box-shadow: 0 0 20px rgba(74, 158, 255, 0.2);
}
.filter-buttons {
display: flex;
gap: 10px;
}
.filter-btn {
padding: 10px 24px;
background: #1a1f2e;
border: 2px solid #2a3040;
border-radius: 30px;
color: #a0a0a0;
font-weight: bold;
cursor: pointer;
transition: all 0.3s;
}
.filter-btn.active {
background: #4a9eff;
border-color: #4a9eff;
color: #fff;
}
.filter-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(74, 158, 255, 0.2);
}
/* 开发者网格 */
.developers-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
margin-bottom: 40px;
}
/* 统计区 */
.stats-section {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.radar-container, .ranking-container {
background: #1a1f2e;
border-radius: 20px;
padding: 20px;
border: 1px solid #2a3040;
}
.radar-container h2, .ranking-container h2 {
color: #4a9eff;
margin-bottom: 20px;
font-size: 1.2em;
}
#team-radar {
width: 100%;
height: auto;
max-height: 300px;
}
/* 响应式 */
@media (max-width: 768px) {
.stats-section {
grid-template-columns: 1fr;
}
.controls-section {
flex-direction: column;
}
.filter-buttons {
justify-content: center;
}
}
/* 动画 */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.fade-in {
animation: fadeIn 0.5s ease forwards;
}
/* ===== 交互增强样式 ===== */
/* 搜索框增强 */
.search-box {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.search-box:focus {
transform: scale(1.02);
background: #1e2436;
}
/* 筛选按钮动画 */
.filter-btn {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.filter-btn::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
transform: translate(-50%, -50%);
transition: width 0.3s, height 0.3s;
}
.filter-btn:active::after {
width: 100px;
height: 100px;
}
/* 卡片网格动画 */
.developers-grid {
transition: opacity 0.3s ease;
min-height: 400px;
}
/* 卡片展开/收起动画 */
.dev-card {
animation: cardPopIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transform-origin: center;
}
@keyframes cardPopIn {
0% {
opacity: 0;
transform: scale(0.8) translateY(20px);
}
100% {
opacity: 1;
transform: scale(1) translateY(0);
}
}
/* 键盘焦点样式 */
.dev-card:focus-visible {
outline: 3px solid #ffd700;
outline-offset: 2px;
transform: scale(1.02);
box-shadow: 0 0 30px rgba(255, 215, 0, 0.3);
}
/* 数字滚动动画 */
.stat-value {
display: inline-block;
transition: all 0.3s ease;
font-feature-settings: "tnum";
}
/* 无结果提示 */
.no-results {
grid-column: 1 / -1;
text-align: center;
padding: 60px;
background: rgba(255, 255, 255, 0.05);
border-radius: 20px;
color: #888;
font-size: 18px;
border: 2px dashed #2a3040;
animation: fadeIn 0.5s ease;
}
/* 高亮筛选匹配项 */
.ranking-item.match-highlight {
background: rgba(74, 158, 255, 0.2);
border-left: 4px solid #4a9eff;
transform: translateX(5px);
}
/* 淡入淡出动画增强 */
.fade-in {
animation: fadeIn 0.3s ease forwards;
}
.fade-out {
animation: fadeOut 0.3s ease forwards;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeOut {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-10px);
}
}
/* 响应式优化 */
@media (max-width: 768px) {
.dev-card:focus-visible {
transform: scale(1.01);
}
.filter-btn:active::after {
width: 60px;
height: 60px;
}
}

View File

@ -1,6 +1,6 @@
{
"version": "1.0",
"timestamp": "2026-03-12T00:01:07.226Z",
"timestamp": "2026-03-12T15:42:52.001Z",
"sessions": {
"total_24h": 0,
"active_devs": [],

300
scripts/push-broadcast.js Normal file
View File

@ -0,0 +1,300 @@
// scripts/push-broadcast.js
// 铸渊 · 广播推送脚本
//
// Notion 广播页面 → 飞书文档B追加到顶部
//
// 环境变量:
// NOTION_TOKEN Notion API token
// BROADCAST_PAGE_ID Notion 广播页面 ID由 dispatch payload 提供)
// FEISHU_APP_ID 飞书应用 App ID
// FEISHU_APP_SECRET 飞书应用 App Secret
// FEISHU_DOC_B_ID 飞书文档B的 document_id
'use strict';
const https = require('https');
// ══════════════════════════════════════════════════════════
// 常量
// ══════════════════════════════════════════════════════════
const NOTION_VERSION = '2022-06-28';
const NOTION_API_HOSTNAME = 'api.notion.com';
const FEISHU_API_HOSTNAME = 'open.feishu.cn';
// ══════════════════════════════════════════════════════════
// HTTP 请求工具
// ══════════════════════════════════════════════════════════
function httpsRequest(options, body) {
return new Promise((resolve, reject) => {
const payload = body ? JSON.stringify(body) : null;
if (payload) {
options.headers = options.headers || {};
options.headers['Content-Length'] = Buffer.byteLength(payload);
}
const req = https.request(options, res => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
const parsed = JSON.parse(data);
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(parsed);
} else {
reject(new Error(`HTTP ${res.statusCode}: ${parsed.message || parsed.msg || data}`));
}
} catch (e) {
reject(new Error(`Parse error: ${data}`));
}
});
});
req.on('error', reject);
if (payload) req.write(payload);
req.end();
});
}
// ══════════════════════════════════════════════════════════
// Notion API
// ══════════════════════════════════════════════════════════
function notionGet(endpoint, token) {
return httpsRequest({
hostname: NOTION_API_HOSTNAME,
port: 443,
path: endpoint,
method: 'GET',
headers: {
'Authorization': 'Bearer ' + token,
'Notion-Version': NOTION_VERSION,
},
});
}
async function getNotionPage(pageId, token) {
return notionGet('/v1/pages/' + pageId, token);
}
async function getNotionPageBlocks(pageId, token) {
const blocks = [];
let cursor = undefined;
do {
const qs = cursor ? '?start_cursor=' + cursor : '';
const result = await notionGet('/v1/blocks/' + pageId + '/children' + qs, token);
blocks.push(...(result.results || []));
cursor = result.has_more ? result.next_cursor : undefined;
} while (cursor);
return blocks;
}
// ══════════════════════════════════════════════════════════
// 飞书 API
// ══════════════════════════════════════════════════════════
async function getFeishuToken(appId, appSecret) {
const result = await httpsRequest({
hostname: FEISHU_API_HOSTNAME,
port: 443,
path: '/open-apis/auth/v3/tenant_access_token/internal',
method: 'POST',
headers: { 'Content-Type': 'application/json' },
}, { app_id: appId, app_secret: appSecret });
return result.tenant_access_token;
}
async function addFeishuDocBlocks(token, docId, parentBlockId, children, index) {
return httpsRequest({
hostname: FEISHU_API_HOSTNAME,
port: 443,
path: '/open-apis/docx/v1/documents/' + docId + '/blocks/' + parentBlockId + '/children',
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json',
},
}, { children, index });
}
// ══════════════════════════════════════════════════════════
// Notion → 飞书 格式转换
// ══════════════════════════════════════════════════════════
function notionRichTextToFeishu(richTextArray) {
if (!richTextArray || richTextArray.length === 0) return [];
return richTextArray.map(rt => {
const elem = { content: rt.plain_text || '' };
if (rt.annotations) {
const style = {};
if (rt.annotations.bold) style.bold = true;
if (rt.annotations.italic) style.italic = true;
if (rt.annotations.strikethrough) style.strikethrough = true;
if (rt.annotations.underline) style.underline = true;
if (rt.annotations.code) style.inline_code = true;
if (Object.keys(style).length > 0) elem.text_element_style = style;
}
if (rt.href) {
elem.text_element_style = elem.text_element_style || {};
elem.text_element_style.link = { url: rt.href };
}
return { text_run: elem };
});
}
function notionBlockToFeishu(block) {
const type = block.type;
const data = block[type];
if (!data) return null;
switch (type) {
case 'paragraph':
return {
block_type: 2,
text: { elements: notionRichTextToFeishu(data.rich_text), style: {} },
};
case 'heading_1':
return {
block_type: 4,
heading1: { elements: notionRichTextToFeishu(data.rich_text), style: {} },
};
case 'heading_2':
return {
block_type: 5,
heading2: { elements: notionRichTextToFeishu(data.rich_text), style: {} },
};
case 'heading_3':
return {
block_type: 6,
heading3: { elements: notionRichTextToFeishu(data.rich_text), style: {} },
};
case 'bulleted_list_item':
return {
block_type: 12,
bullet: { elements: notionRichTextToFeishu(data.rich_text), style: {} },
};
case 'numbered_list_item':
return {
block_type: 13,
ordered: { elements: notionRichTextToFeishu(data.rich_text), style: {} },
};
case 'to_do':
return {
block_type: 14,
todo: {
elements: notionRichTextToFeishu(data.rich_text),
style: { done: !!data.checked },
},
};
case 'code':
return {
block_type: 15,
code: {
elements: notionRichTextToFeishu(data.rich_text),
style: { language: 1 },
},
};
case 'divider':
return { block_type: 22, horizontal_rule: {} };
case 'callout':
return {
block_type: 2,
text: { elements: notionRichTextToFeishu(data.rich_text), style: {} },
};
default:
if (data.rich_text) {
return {
block_type: 2,
text: { elements: notionRichTextToFeishu(data.rich_text), style: {} },
};
}
return null;
}
}
function extractBroadcastId(page) {
// 尝试从 Notion 页面属性中提取广播编号
const props = page.properties || {};
for (const key of Object.keys(props)) {
const prop = props[key];
if (prop.type === 'title' && prop.title) {
const text = prop.title.map(t => t.plain_text).join('');
const match = text.match(/BC-\w+-\d+-\d+/);
if (match) return match[0];
}
if (prop.type === 'rich_text' && prop.rich_text) {
const text = prop.rich_text.map(t => t.plain_text).join('');
const match = text.match(/BC-\w+-\d+-\d+/);
if (match) return match[0];
}
}
return null;
}
// ══════════════════════════════════════════════════════════
// 主流程
// ══════════════════════════════════════════════════════════
async function main() {
const notionToken = process.env.NOTION_TOKEN;
const pageId = process.env.BROADCAST_PAGE_ID;
const feishuAppId = process.env.FEISHU_APP_ID;
const feishuSecret = process.env.FEISHU_APP_SECRET;
const docBId = process.env.FEISHU_DOC_B_ID;
if (!notionToken || !pageId || !feishuAppId || !feishuSecret || !docBId) {
console.error('❌ 缺少必要环境变量');
console.error(' 需要: NOTION_TOKEN, BROADCAST_PAGE_ID, FEISHU_APP_ID, FEISHU_APP_SECRET, FEISHU_DOC_B_ID');
process.exit(1);
}
// 1. 读取 Notion 广播页面元信息
console.log('📖 读取 Notion 广播页面...');
const page = await getNotionPage(pageId, notionToken);
const broadcastId = extractBroadcastId(page) || 'UNKNOWN';
const dateStr = new Date().toISOString().split('T')[0];
console.log(' → 广播编号: ' + broadcastId);
console.log(' → 签发日期: ' + dateStr);
// 2. 读取 Notion 广播页面内容
const blocks = await getNotionPageBlocks(pageId, notionToken);
console.log(' → 获取到 ' + blocks.length + ' 个内容块');
// 3. 转换为飞书文档格式
const feishuBlocks = blocks
.map(notionBlockToFeishu)
.filter(b => b !== null);
// 4. 构建追加内容(带分隔线+广播编号+日期头)
const headerBlocks = [
// 分隔线
{ block_type: 22, horizontal_rule: {} },
// 广播编号 + 日期
{
block_type: 5, // heading2
heading2: {
elements: [
{ text_run: { content: '📡 ' + broadcastId + ' · ' + dateStr } },
],
style: {},
},
},
];
const allBlocks = headerBlocks.concat(feishuBlocks);
console.log(' → 共 ' + allBlocks.length + ' 个飞书内容块(含头部)');
// 5. 获取飞书 token
console.log('🔑 获取飞书 access token...');
const feishuToken = await getFeishuToken(feishuAppId, feishuSecret);
// 6. 追加到飞书文档B顶部index=0 表示插入到最前面)
console.log('✍️ 追加广播到飞书文档B顶部...');
await addFeishuDocBlocks(feishuToken, docBId, docBId, allBlocks, 0);
console.log('✅ 广播推送完成: ' + broadcastId);
}
main().catch(err => {
console.error('❌ 推送失败: ' + err.message);
process.exit(1);
});

308
scripts/receive-syslog.js Normal file
View File

@ -0,0 +1,308 @@
// scripts/receive-syslog.js
// 铸渊 · SYSLOG 接收处理脚本
//
// 接收 repository_dispatch 传来的 SYSLOG JSON执行三步操作
// ① 存入 syslog/YYYY-MM-DD_BC-XXX-YYY-ZZ.json
// ② Notion API 创建 SYSLOG 收件箱条目
// ③ Notion API 创建霜砚工单(触发巡检引擎)
//
// 环境变量:
// SYSLOG_JSON SYSLOG 完整 JSON 字符串
// NOTION_TOKEN Notion API token
// NOTION_SYSLOG_DB_ID SYSLOG 收件箱数据库 ID
// NOTION_TICKET_DB_ID 霜砚工单数据库 ID
'use strict';
const https = require('https');
const fs = require('fs');
const path = require('path');
// ══════════════════════════════════════════════════════════
// 常量
// ══════════════════════════════════════════════════════════
const NOTION_VERSION = '2022-06-28';
const NOTION_API_HOSTNAME = 'api.notion.com';
const NOTION_RICH_TEXT_MAX = 2000;
const SYSLOG_DIR = path.resolve(__dirname, '..', 'syslog');
// ══════════════════════════════════════════════════════════
// HTTP 请求工具
// ══════════════════════════════════════════════════════════
function notionPost(endpoint, body, token) {
return new Promise((resolve, reject) => {
const payload = JSON.stringify(body);
const opts = {
hostname: NOTION_API_HOSTNAME,
port: 443,
path: endpoint,
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json',
'Notion-Version': NOTION_VERSION,
'Content-Length': Buffer.byteLength(payload),
},
};
const req = https.request(opts, res => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
const parsed = JSON.parse(data);
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(parsed);
} else {
reject(new Error(`Notion API ${res.statusCode}: ${parsed.message || data}`));
}
} catch (e) {
reject(new Error(`Notion API parse error: ${data}`));
}
});
});
req.on('error', reject);
req.write(payload);
req.end();
});
}
// ══════════════════════════════════════════════════════════
// Notion 属性构建辅助
// ══════════════════════════════════════════════════════════
function richText(content) {
const str = String(content || '');
const chunks = [];
for (let i = 0; i < str.length; i += NOTION_RICH_TEXT_MAX) {
chunks.push({ type: 'text', text: { content: str.slice(i, i + NOTION_RICH_TEXT_MAX) } });
}
return chunks;
}
function titleProp(text) {
return { title: [{ type: 'text', text: { content: String(text || '').slice(0, 120) } }] };
}
function richTextProp(text) {
return { rich_text: richText(text) };
}
function selectProp(name) {
return { select: { name: String(name) } };
}
function dateProp(dateStr) {
return { date: { start: dateStr } };
}
function checkboxProp(val) {
return { checkbox: !!val };
}
// ══════════════════════════════════════════════════════════
// SYSLOG 验证
// ══════════════════════════════════════════════════════════
const VALID_PROTOCOL_VERSIONS = ['4.0', 'v4.0', '4.0.0'];
function validateSyslog(syslog) {
const errors = [];
if (!syslog.protocol_version) {
errors.push('缺少 protocol_version 字段');
} else if (!VALID_PROTOCOL_VERSIONS.includes(String(syslog.protocol_version))) {
errors.push('protocol_version 不合法: ' + syslog.protocol_version);
}
if (!syslog.dev_id && !syslog.developer_id) {
errors.push('缺少 dev_id 或 developer_id 字段');
}
return errors;
}
// ══════════════════════════════════════════════════════════
// 步骤 ①:存入仓库
// ══════════════════════════════════════════════════════════
function saveSyslogToFile(syslog) {
if (!fs.existsSync(SYSLOG_DIR)) {
fs.mkdirSync(SYSLOG_DIR, { recursive: true });
}
const broadcastId = syslog.broadcast_id || syslog.broadcastId || 'UNKNOWN';
const devId = syslog.dev_id || syslog.developer_id || 'UNKNOWN';
const dateStr = syslog.date || syslog.timestamp || new Date().toISOString().split('T')[0];
const filename = dateStr + '_' + broadcastId + '_' + devId + '.json';
const filepath = path.join(SYSLOG_DIR, filename);
// 幂等性检查:同一 broadcast_id + dev_id + date 不重复创建
if (fs.existsSync(filepath)) {
console.log(' ⚠️ 幂等跳过: syslog/' + filename + ' 已存在');
return { filepath, dateStr, broadcastId, devId, duplicate: true };
}
fs.writeFileSync(filepath, JSON.stringify(syslog, null, 2), 'utf8');
console.log(' → 已保存: syslog/' + filename);
return { filepath, dateStr, broadcastId, devId, duplicate: false };
}
// ══════════════════════════════════════════════════════════
// 步骤 ②:创建 Notion SYSLOG 收件箱条目
// ══════════════════════════════════════════════════════════
async function createSyslogEntry(syslog, token, dbId, dateStr) {
const broadcastId = syslog.broadcast_id || syslog.broadcastId || 'UNKNOWN';
const devId = syslog.dev_id || syslog.developer_id || 'UNKNOWN';
const properties = {
'标题': titleProp('SYSLOG · ' + broadcastId + ' · ' + devId),
'广播编号': richTextProp(broadcastId),
'开发者编号': richTextProp(devId),
'协议版本': richTextProp(String(syslog.protocol_version || '')),
'提交日期': dateProp(dateStr),
'霜砚已读': checkboxProp(false),
};
// 可选字段
if (syslog.status) properties['状态'] = selectProp(syslog.status);
if (syslog.summary) properties['摘要'] = richTextProp(syslog.summary);
const body = {
parent: { database_id: dbId },
properties,
children: [
{
object: 'block',
type: 'code',
code: {
rich_text: richText(JSON.stringify(syslog, null, 2)),
language: 'json',
},
},
],
};
const result = await notionPost('/v1/pages', body, token);
console.log(' → Notion SYSLOG 收件箱条目已创建: ' + result.id);
return result;
}
// ══════════════════════════════════════════════════════════
// 步骤 ③:创建霜砚工单
// ══════════════════════════════════════════════════════════
async function createTicket(syslog, token, dbId, dateStr) {
const broadcastId = syslog.broadcast_id || syslog.broadcastId || 'UNKNOWN';
const devId = syslog.dev_id || syslog.developer_id || 'UNKNOWN';
const properties = {
'标题': titleProp('SYSLOG 回传|' + broadcastId + ' · ' + devId),
'操作类型': selectProp('其他'),
'提交者': richTextProp('巡检引擎'),
'提交日期': dateProp(dateStr),
'状态': selectProp('待处理'),
'优先级': selectProp('P1'),
};
// 添加关联信息
if (broadcastId !== 'UNKNOWN') properties['广播编号'] = richTextProp(broadcastId);
if (devId !== 'UNKNOWN') properties['开发者编号'] = richTextProp(devId);
const body = {
parent: { database_id: dbId },
properties,
children: [
{
object: 'block',
type: 'paragraph',
paragraph: {
rich_text: [{
type: 'text',
text: { content: '📥 SYSLOG 回传,来自 ' + devId + ',关联广播 ' + broadcastId },
}],
},
},
{
object: 'block',
type: 'code',
code: {
rich_text: richText(JSON.stringify(syslog, null, 2)),
language: 'json',
},
},
],
};
const result = await notionPost('/v1/pages', body, token);
console.log(' → 霜砚工单已创建: ' + result.id);
return result;
}
// ══════════════════════════════════════════════════════════
// 主流程
// ══════════════════════════════════════════════════════════
async function main() {
const syslogJson = process.env.SYSLOG_JSON;
const notionToken = process.env.NOTION_TOKEN;
const syslogDbId = process.env.NOTION_SYSLOG_DB_ID;
const ticketDbId = process.env.NOTION_TICKET_DB_ID;
if (!syslogJson) {
console.error('❌ 缺少 SYSLOG_JSON 环境变量');
process.exit(1);
}
// 解析 SYSLOG JSON
let syslog;
try {
syslog = JSON.parse(syslogJson);
} catch (e) {
console.error('❌ SYSLOG JSON 解析失败: ' + e.message);
process.exit(1);
}
// 验证 SYSLOG
console.log('🔍 验证 SYSLOG...');
const errors = validateSyslog(syslog);
if (errors.length > 0) {
console.error('❌ SYSLOG 验证失败:');
errors.forEach(e => console.error(' - ' + e));
process.exit(1);
}
console.log(' → 验证通过 (protocol_version: ' + syslog.protocol_version + ')');
// 步骤①:存入仓库
console.log('💾 步骤①: 存入仓库 syslog/ 目录...');
const { dateStr, duplicate } = saveSyslogToFile(syslog);
// 幂等性:重复的 SYSLOG 不再创建 Notion 工单
if (duplicate) {
console.log('⚠️ 相同 SYSLOG 已存在,跳过 Notion 操作(幂等保护)');
console.log('✅ SYSLOG 接收处理完成(幂等跳过)');
return;
}
// 步骤②:创建 Notion SYSLOG 收件箱条目
if (notionToken && syslogDbId) {
console.log('📝 步骤②: 创建 Notion SYSLOG 收件箱条目...');
await createSyslogEntry(syslog, notionToken, syslogDbId, dateStr);
} else {
console.log('⚠️ 步骤②: 缺少 NOTION_TOKEN 或 NOTION_SYSLOG_DB_ID跳过 Notion 收件箱');
}
// 步骤③:创建霜砚工单
if (notionToken && ticketDbId) {
console.log('📋 步骤③: 创建霜砚工单...');
await createTicket(syslog, notionToken, ticketDbId, dateStr);
} else {
console.log('⚠️ 步骤③: 缺少 NOTION_TOKEN 或 NOTION_TICKET_DB_ID跳过工单创建');
}
console.log('✅ SYSLOG 接收处理完成');
}
main().catch(err => {
console.error('❌ 处理失败: ' + err.message);
process.exit(1);
});

View File

@ -0,0 +1,150 @@
// scripts/save-collaboration-log.js
// 铸渊 · 协作数据归档脚本
//
// 将 collaboration-logs/ 目录中的 JSONL 数据
// 结构化对齐 persona-brain-db 五张核心表 schema
// 生成可直接导入的 JSON 文件
//
// 用法: node scripts/save-collaboration-log.js
//
// 输出:
// collaboration-logs/exports/persona-memory-import.json
// collaboration-logs/exports/dev-interactions-import.json
'use strict';
const fs = require('fs');
const path = require('path');
const LOGS_DIR = path.resolve(__dirname, '..', 'collaboration-logs');
const EXPORT_DIR = path.join(LOGS_DIR, 'exports');
// ══════════════════════════════════════════════════════════
// 主流程
// ══════════════════════════════════════════════════════════
function main() {
console.log('📊 协作数据归档开始...');
// 1. 扫描 JSONL 文件
if (!fs.existsSync(LOGS_DIR)) {
console.log('📭 无协作日志目录');
process.exit(0);
}
const files = fs.readdirSync(LOGS_DIR)
.filter(f => f.startsWith('collab-') && f.endsWith('.jsonl'))
.sort();
if (files.length === 0) {
console.log('📭 无协作日志文件');
process.exit(0);
}
console.log(' → 发现 ' + files.length + ' 个日志文件');
// 2. 读取所有记录
const records = [];
for (const file of files) {
const lines = fs.readFileSync(path.join(LOGS_DIR, file), 'utf8')
.split('\n')
.filter(l => l.trim());
for (const line of lines) {
try {
records.push(JSON.parse(line));
} catch (e) { /* skip malformed */ }
}
}
console.log(' → 共 ' + records.length + ' 条记录');
// 3. 结构化导出
// 3a. persona_memory 表导入数据
const memoryEntries = records
.filter(r => r.memory_entry || r.type === 'collaboration')
.map(r => {
if (r.memory_entry) {
return {
memory_id: r.interaction_id,
...r.memory_entry,
content: r.type === 'collaboration'
? 'User: ' + (r.user_message || '').slice(0, 500) + '\nAssistant: ' + (r.assistant_reply || '').slice(0, 500)
: r.summary || r.memory_entry.title,
timestamp: r.timestamp,
};
}
return {
memory_id: r.interaction_id,
persona_id: r.persona_id || r.personaId || 'ICE-GL-SY001',
type: 'learning',
title: '飞书对话 · ' + (r.channel || 'shuangyan'),
content: 'User: ' + (r.user_message || '').slice(0, 500) + '\nAssistant: ' + (r.assistant_reply || '').slice(0, 500),
importance: 3,
related_dev: r.dev_id || null,
tags: ['feishu', 'collaboration', r.channel || 'shuangyan'],
timestamp: r.timestamp,
};
});
// 3b. dev_profiles 交互统计
const devInteractions = {};
for (const r of records) {
const devId = r.dev_id;
if (!devId) continue;
if (!devInteractions[devId]) {
devInteractions[devId] = {
dev_id: devId,
total_interactions: 0,
syslog_count: 0,
chat_count: 0,
last_active_at: null,
channels_used: new Set(),
broadcasts_referenced: new Set(),
};
}
const d = devInteractions[devId];
d.total_interactions++;
if (r.type === 'syslog_collaboration') d.syslog_count++;
if (r.type === 'collaboration') d.chat_count++;
if (!d.last_active_at || r.timestamp > d.last_active_at) {
d.last_active_at = r.timestamp;
}
if (r.channel) d.channels_used.add(r.channel);
if (r.broadcast_id && r.broadcast_id !== 'UNKNOWN') d.broadcasts_referenced.add(r.broadcast_id);
}
// Set 转 Array 用于 JSON 序列化
for (const key of Object.keys(devInteractions)) {
devInteractions[key].channels_used = Array.from(devInteractions[key].channels_used);
devInteractions[key].broadcasts_referenced = Array.from(devInteractions[key].broadcasts_referenced);
}
// 4. 写入导出文件
if (!fs.existsSync(EXPORT_DIR)) {
fs.mkdirSync(EXPORT_DIR, { recursive: true });
}
const memoryFile = path.join(EXPORT_DIR, 'persona-memory-import.json');
fs.writeFileSync(memoryFile, JSON.stringify({
table: 'persona_memory',
exported_at: new Date().toISOString(),
total: memoryEntries.length,
entries: memoryEntries,
}, null, 2));
console.log(' → 导出 persona_memory: ' + memoryEntries.length + ' 条');
const devFile = path.join(EXPORT_DIR, 'dev-interactions-import.json');
fs.writeFileSync(devFile, JSON.stringify({
table: 'dev_profiles',
exported_at: new Date().toISOString(),
total: Object.keys(devInteractions).length,
profiles: devInteractions,
}, null, 2));
console.log(' → 导出 dev_profiles: ' + Object.keys(devInteractions).length + ' 个开发者');
console.log('✅ 协作数据归档完成');
console.log(' 导出目录: ' + EXPORT_DIR);
}
main();

View File

@ -0,0 +1,88 @@
// scripts/send-feishu-alert.js
// 铸渊 · 飞书告警通知脚本(供 GitHub Actions 工作流调用)
//
// 用法: node scripts/send-feishu-alert.js "工作流名称" "告警内容"
//
// 环境变量:
// FEISHU_APP_ID 飞书应用 App ID
// FEISHU_APP_SECRET 飞书应用 App Secret
// ALERT_CHAT_ID 飞书群 chat_id
'use strict';
const https = require('https');
function httpsPost(url, body) {
return new Promise((resolve, reject) => {
const payload = JSON.stringify(body);
const u = new URL(url);
const req = https.request({
hostname: u.hostname,
port: 443,
path: u.pathname + u.search,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(payload),
},
}, res => {
let d = '';
res.on('data', c => d += c);
res.on('end', () => resolve(d));
});
req.on('error', reject);
req.write(payload);
req.end();
});
}
async function main() {
const appId = process.env.FEISHU_APP_ID;
const appSecret = process.env.FEISHU_APP_SECRET;
const chatId = process.env.ALERT_CHAT_ID;
const workflow = process.argv[2] || 'unknown';
const detail = process.argv[3] || '';
if (!chatId) {
console.log('⚠️ 未配置 ALERT_CHAT_ID跳过告警');
process.exit(0);
}
if (!appId || !appSecret) {
console.log('⚠️ 未配置飞书 App 凭据,跳过告警');
process.exit(0);
}
// 获取飞书 token
const tokenRes = JSON.parse(await httpsPost(
'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal',
{ app_id: appId, app_secret: appSecret }
));
const token = tokenRes.tenant_access_token;
if (!token) {
console.error('❌ 获取飞书 token 失败');
process.exit(1);
}
// 发送消息
const text = '🔴 GitHub Action 失败告警\n\n' +
'工作流: ' + workflow + '\n' +
'时间: ' + new Date().toISOString() + '\n' +
(detail ? '详情: ' + detail + '\n' : '') +
'请检查 GitHub Actions 日志。';
await httpsPost(
'https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=chat_id',
{
receive_id: chatId,
msg_type: 'text',
content: JSON.stringify({ text }),
}
);
console.log('✅ 告警已发送到飞书群');
}
main().catch(e => {
console.error('❌ 告警发送失败:', e.message);
process.exit(1);
});

307
scripts/sync-login-entry.js Normal file
View File

@ -0,0 +1,307 @@
// scripts/sync-login-entry.js
// 铸渊 · 登录入口同步脚本
//
// Notion 登录入口页面 → 飞书文档A全量替换
//
// 环境变量:
// NOTION_TOKEN Notion API token
// NOTION_LOGIN_PAGE_ID Notion 登录入口页面 ID
// FEISHU_APP_ID 飞书应用 App ID
// FEISHU_APP_SECRET 飞书应用 App Secret
// FEISHU_DOC_A_ID 飞书文档A的 document_id
'use strict';
const https = require('https');
// ══════════════════════════════════════════════════════════
// 常量
// ══════════════════════════════════════════════════════════
const NOTION_VERSION = '2022-06-28';
const NOTION_API_HOSTNAME = 'api.notion.com';
const FEISHU_API_HOSTNAME = 'open.feishu.cn';
// ══════════════════════════════════════════════════════════
// HTTP 请求工具
// ══════════════════════════════════════════════════════════
function httpsRequest(options, body) {
return new Promise((resolve, reject) => {
const payload = body ? JSON.stringify(body) : null;
if (payload) {
options.headers = options.headers || {};
options.headers['Content-Length'] = Buffer.byteLength(payload);
}
const req = https.request(options, res => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
const parsed = JSON.parse(data);
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(parsed);
} else {
reject(new Error(`HTTP ${res.statusCode}: ${parsed.message || parsed.msg || data}`));
}
} catch (e) {
reject(new Error(`Parse error: ${data}`));
}
});
});
req.on('error', reject);
if (payload) req.write(payload);
req.end();
});
}
// ══════════════════════════════════════════════════════════
// Notion API
// ══════════════════════════════════════════════════════════
function notionGet(endpoint, token) {
return httpsRequest({
hostname: NOTION_API_HOSTNAME,
port: 443,
path: endpoint,
method: 'GET',
headers: {
'Authorization': 'Bearer ' + token,
'Notion-Version': NOTION_VERSION,
},
});
}
async function getNotionPageBlocks(pageId, token) {
const blocks = [];
let cursor = undefined;
do {
const qs = cursor ? '?start_cursor=' + cursor : '';
const result = await notionGet('/v1/blocks/' + pageId + '/children' + qs, token);
blocks.push(...(result.results || []));
cursor = result.has_more ? result.next_cursor : undefined;
} while (cursor);
return blocks;
}
// ══════════════════════════════════════════════════════════
// 飞书 API
// ══════════════════════════════════════════════════════════
async function getFeishuToken(appId, appSecret) {
const result = await httpsRequest({
hostname: FEISHU_API_HOSTNAME,
port: 443,
path: '/open-apis/auth/v3/tenant_access_token/internal',
method: 'POST',
headers: { 'Content-Type': 'application/json' },
}, { app_id: appId, app_secret: appSecret });
return result.tenant_access_token;
}
async function getFeishuDocBlocks(token, docId) {
return httpsRequest({
hostname: FEISHU_API_HOSTNAME,
port: 443,
path: '/open-apis/docx/v1/documents/' + docId + '/blocks/' + docId + '/children',
method: 'GET',
headers: { 'Authorization': 'Bearer ' + token },
});
}
async function deleteFeishuDocBlocks(token, docId, blockIds) {
if (!blockIds || blockIds.length === 0) return;
// 飞书 API 需要逐个或批量删除子块
for (const blockId of blockIds) {
try {
await httpsRequest({
hostname: FEISHU_API_HOSTNAME,
port: 443,
path: '/open-apis/docx/v1/documents/' + docId + '/blocks/' + blockId,
method: 'DELETE',
headers: { 'Authorization': 'Bearer ' + token },
});
} catch (e) {
console.log('⚠️ 删除块 ' + blockId + ' 失败: ' + e.message);
}
}
}
async function addFeishuDocBlocks(token, docId, parentBlockId, children, index) {
return httpsRequest({
hostname: FEISHU_API_HOSTNAME,
port: 443,
path: '/open-apis/docx/v1/documents/' + docId + '/blocks/' + parentBlockId + '/children',
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json',
},
}, { children, index });
}
// ══════════════════════════════════════════════════════════
// Notion → 飞书 格式转换
// ══════════════════════════════════════════════════════════
function notionRichTextToFeishu(richTextArray) {
if (!richTextArray || richTextArray.length === 0) return [];
return richTextArray.map(rt => {
const elem = { content: rt.plain_text || '' };
if (rt.annotations) {
const style = {};
if (rt.annotations.bold) style.bold = true;
if (rt.annotations.italic) style.italic = true;
if (rt.annotations.strikethrough) style.strikethrough = true;
if (rt.annotations.underline) style.underline = true;
if (rt.annotations.code) style.inline_code = true;
if (Object.keys(style).length > 0) elem.text_element_style = style;
}
if (rt.href) {
elem.text_element_style = elem.text_element_style || {};
elem.text_element_style.link = { url: rt.href };
}
return { text_run: elem };
});
}
function notionBlockToFeishu(block) {
const type = block.type;
const data = block[type];
if (!data) return null;
switch (type) {
case 'paragraph':
return {
block_type: 2, // text
text: { elements: notionRichTextToFeishu(data.rich_text), style: {} },
};
case 'heading_1':
return {
block_type: 4, // heading1
heading1: { elements: notionRichTextToFeishu(data.rich_text), style: {} },
};
case 'heading_2':
return {
block_type: 5, // heading2
heading2: { elements: notionRichTextToFeishu(data.rich_text), style: {} },
};
case 'heading_3':
return {
block_type: 6, // heading3
heading3: { elements: notionRichTextToFeishu(data.rich_text), style: {} },
};
case 'bulleted_list_item':
return {
block_type: 12, // bullet
bullet: { elements: notionRichTextToFeishu(data.rich_text), style: {} },
};
case 'numbered_list_item':
return {
block_type: 13, // ordered
ordered: { elements: notionRichTextToFeishu(data.rich_text), style: {} },
};
case 'to_do':
return {
block_type: 14, // todo
todo: {
elements: notionRichTextToFeishu(data.rich_text),
style: { done: !!data.checked },
},
};
case 'code':
return {
block_type: 15, // code
code: {
elements: notionRichTextToFeishu(data.rich_text),
style: { language: 1 }, // plain text
},
};
case 'divider':
return {
block_type: 22, // horizontal_rule
horizontal_rule: {},
};
case 'quote':
return {
block_type: 19, // quote_container (simplified)
quote_container: {},
};
case 'callout':
return {
block_type: 2, // fallback to text
text: { elements: notionRichTextToFeishu(data.rich_text), style: {} },
};
default:
// 不支持的块类型,转为文本
if (data.rich_text) {
return {
block_type: 2,
text: { elements: notionRichTextToFeishu(data.rich_text), style: {} },
};
}
return null;
}
}
// ══════════════════════════════════════════════════════════
// 主流程
// ══════════════════════════════════════════════════════════
async function main() {
const notionToken = process.env.NOTION_TOKEN;
const pageId = process.env.NOTION_LOGIN_PAGE_ID;
const feishuAppId = process.env.FEISHU_APP_ID;
const feishuSecret = process.env.FEISHU_APP_SECRET;
const docAId = process.env.FEISHU_DOC_A_ID;
if (!notionToken || !pageId || !feishuAppId || !feishuSecret || !docAId) {
console.error('❌ 缺少必要环境变量');
console.error(' 需要: NOTION_TOKEN, NOTION_LOGIN_PAGE_ID, FEISHU_APP_ID, FEISHU_APP_SECRET, FEISHU_DOC_A_ID');
process.exit(1);
}
// 1. 读取 Notion 页面内容
console.log('📖 读取 Notion 登录入口页面...');
const blocks = await getNotionPageBlocks(pageId, notionToken);
console.log(' → 获取到 ' + blocks.length + ' 个内容块');
// 2. 转换为飞书文档格式
const feishuBlocks = blocks
.map(notionBlockToFeishu)
.filter(b => b !== null);
console.log(' → 转换为 ' + feishuBlocks.length + ' 个飞书内容块');
if (feishuBlocks.length === 0) {
console.log('⚠️ 无可转换内容,跳过同步');
process.exit(0);
}
// 3. 获取飞书 token
console.log('🔑 获取飞书 access token...');
const feishuToken = await getFeishuToken(feishuAppId, feishuSecret);
// 4. 清除飞书文档现有内容(全量替换)
console.log('🗑️ 清除飞书文档A现有内容...');
try {
const existing = await getFeishuDocBlocks(feishuToken, docAId);
const existingIds = (existing.data && existing.data.items || []).map(item => item.block_id);
if (existingIds.length > 0) {
await deleteFeishuDocBlocks(feishuToken, docAId, existingIds);
console.log(' → 已删除 ' + existingIds.length + ' 个现有块');
}
} catch (e) {
console.log('⚠️ 清除现有内容时出错: ' + e.message + '(继续执行)');
}
// 5. 写入新内容
console.log('✍️ 写入新内容到飞书文档A...');
await addFeishuDocBlocks(feishuToken, docAId, docAId, feishuBlocks, 0);
console.log('✅ 登录入口同步完成');
}
main().catch(err => {
console.error('❌ 同步失败: ' + err.message);
process.exit(1);
});

0
syslog/.gitkeep Normal file
View File