Merge branch 'main' of https://github.com/qinfendebingshuo/guanghulab
This commit is contained in:
commit
fbfb4f1eb7
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# 冰朔主控神经系统 · 核心主控大脑 v1.0
|
||||
|
||||
> 本文件为冰朔主控神经系统的总控脑文件。
|
||||
> 最后编译时间:2026-03-12T02:08:35.412Z
|
||||
> 最后编译时间:2026-03-12T15:48:37.325Z
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -56,7 +56,7 @@
|
|||
|
||||
### 仓库统计
|
||||
- 功能模块:10 个
|
||||
- Workflow:32 个
|
||||
- Workflow:35 个
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -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 | 前后端结构存在,端到端对话链路待验证 |
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }}"
|
||||
|
|
|
|||
|
|
@ -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: 提交信号日志
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
name: 铸渊 · Receive SYSLOG · 飞书机器人 → GitHub → Notion
|
||||
|
||||
# 链路3:SYSLOG 回传
|
||||
# 飞书机器人发来的 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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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: |
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
40
README.md
40
README.md
|
|
@ -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 -->
|
||||
|
||||
### 🤖 铸渊自动提醒 · 合作者
|
||||
|
|
|
|||
|
|
@ -0,0 +1,213 @@
|
|||
// backend/feishu-bot/ai-chat.js
|
||||
// 铸渊 · 飞书机器人 AI 对话引擎
|
||||
//
|
||||
// 接入 DeepSeek / 通义千问 等大模型 API
|
||||
// 使用霜砚登录入口(文档A)内容作为 system prompt
|
||||
//
|
||||
// 环境变量:
|
||||
// MODEL_API_KEY 模型 API Key(DeepSeek / 通义千问)
|
||||
// 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,
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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 Key(Phase 4A)
|
||||
// MODEL_API_BASE AI 模型 API Base URL(Phase 4A)
|
||||
// MODEL_NAME AI 模型名称(Phase 4A)
|
||||
// FEISHU_ALERT_CHAT_ID 失败告警推送的飞书群 chat_id(Phase 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;
|
||||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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` 第 792–797 行(CONTEXT_CONFIG),第 1565–1594 行附近(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` | 341–435 |
|
||||
| setIdentity() | `docs/index.html` | 1380–1425 |
|
||||
| doTeamLogin() | `docs/index.html` | 1437–1465 |
|
||||
| syncDevFromNotion() | `docs/index.html` | 1470–1515 |
|
||||
| ROLE_MAP | `docs/index.html` | 761 附近 |
|
||||
| 后端开发者路由 | `backend/routes/developers.js` | 完整文件 |
|
||||
|
||||
---
|
||||
|
||||
## 2.3 聊天界面功能清单
|
||||
|
||||
| 功能 | 描述 | 状态 | 代码路径(docs/index.html 行号) |
|
||||
|------|------|------|--------------------------------|
|
||||
| **多模型切换** | 用户可选择 DeepSeek / GPT / Gemini 等模型 | ✅ 已上线 | 自定义 API 模式下可选模型 |
|
||||
| **三种登录模式** | 团队/访客/自定义 API 三合一切换 | ✅ 已上线 | 341–435 |
|
||||
| **SSE 流式回复** | 实时逐字输出 AI 回复(Server-Sent Events) | ✅ 已上线 | 1822–1949(streamReply / teamStreamReply / guestStreamReply) |
|
||||
| **Markdown 渲染** | 支持标题、粗体、斜体、链接、列表、引用 | ✅ 已上线 | 2107–2142(md / inlineFmt 函数) |
|
||||
| **代码块高亮** | 支持 ```lang 语法的代码块渲染 | ✅ 已上线 | 2108–2131 |
|
||||
| **代码复制按钮** | 代码块右上角一键复制 | ✅ 已上线 | 2145–2159(addCopyBtns 函数) |
|
||||
| **打字指示器** | AI 回复时显示动画打字气泡 | ✅ 已上线 | 2198–2206(showTyping / removeTyping) |
|
||||
| **对话历史管理** | 左侧栏显示历史对话列表,支持切换/删除 | ✅ 已上线 | 866–956(saveCurrent / loadConversation) |
|
||||
| **自动保存** | 对话自动保存到 localStorage(最多20条) | ✅ 已上线 | 866–956 |
|
||||
| **新建对话** | 一键新建空白对话 | ✅ 已上线 | 904–920 |
|
||||
| **对话标题自动生成** | 从首条消息提取关键词作为标题 | ✅ 已上线 | 886–902 |
|
||||
| **导出对话** | 下载当前对话为 HTML 文件 | ✅ 已上线 | 2271–2279(dlApp 函数) |
|
||||
| **多行为模式** | 💬 聊天 / 🔨 构建 / 📋 审查 / 🧠 大脑 四模式 | ✅ 已上线 | 772–779(MODES 定义) |
|
||||
| **指挥台(右侧面板)** | 团队进度总览、快捷指令、模块分配 | ✅ 已上线 | 547–553, 1081–1147(renderRightSidebar) |
|
||||
| **快捷指令按钮** | 右侧面板一键发送「全员进度」「需要协调」等 | ✅ 已上线 | 1100–1105 |
|
||||
| **团队成员状态网格** | 显示每位开发者的状态、模块、下一步 | ✅ 已上线 | 1108–1118 |
|
||||
| **暗色主题** | 默认暗色主题(CSS 变量控制) | ✅ 已上线 | 13–21(:root CSS 变量) |
|
||||
| **响应式设计** | 桌面三栏 / 移动端自适应 | ✅ 已上线 | 296–329(@media 查询) |
|
||||
| **键盘快捷键** | Enter 发送 / Shift+Enter 换行 | ✅ 已上线 | 2284–2290 |
|
||||
| **人格身份系统** | 基于角色的身份识别与权限控制 | ✅ 已上线 | 746–758, 1380–1425 |
|
||||
| **大脑记忆加载** | 从 `.github/brain/` 加载记忆/路由/状态 | ✅ 已上线 | 1585–1643(loadBrain) |
|
||||
| **API 端点自动探测** | 自动检测可用 API 端点和模型 | ✅ 已上线 | 1280–1343 |
|
||||
| **客户端限流** | 访客模式 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` 第 1648–1679 行):
|
||||
|
||||
```
|
||||
你是铸渊(Zhùyuān),HoloLake 光湖系统的代码守护人格体。
|
||||
```
|
||||
|
||||
**动态注入内容**:
|
||||
- 当前对话者身份(角色、权限、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_KEY,SSE) |
|
||||
| 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 # 开发者 CRUD(Notion 集成)
|
||||
│ ├── 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
|
||||
|
|
@ -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();
|
||||
}
|
||||
})();
|
||||
|
|
@ -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();
|
||||
}
|
||||
})();
|
||||
|
|
@ -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)';
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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); }
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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';
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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": [],
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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();
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
Loading…
Reference in New Issue