diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..2e84a5c8 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,57 @@ +# CODEOWNERS · 光湖仓库 · 协作者权限保护 +# +# 作用:保护核心文件,修改时需要仓库所有者审批。 +# 配合 Branch Protection Rules 使用效果最佳(Settings → Branches → 开启分支保护)。 +# +# 设计原则: +# 只列出需要保护的核心目录/文件,模块目录(m01-* ~ m18-*)和 dev-nodes/ +# 不在此文件中列出,因此协作者可以自由提交到模块目录。 +# +# 注意:此文件需要配合 Branch Protection Rules 才能真正生效。 +# 操作方法见 docs/使用指南.md 第五节。 +# +# 开发者 GitHub 用户名映射见 .github/brain/collaborators.json + +# ═══════════════════════════════════════════════════════ +# 核心基础设施 —— 需要仓库所有者审批 +# ═══════════════════════════════════════════════════════ + +# GitHub 工作流与自动化配置 +/.github/ @qinfendebingshuo + +# 自动化脚本 +/scripts/ @qinfendebingshuo + +# 铸渊聊天室前端 +/docs/ @qinfendebingshuo + +# 后端集成 / API 代理 +/backend-integration/ @qinfendebingshuo + +# 信号日志 +/signal-log/ @qinfendebingshuo + +# 根目录配置文件 +/package.json @qinfendebingshuo +/package-lock.json @qinfendebingshuo +/next.config.ts @qinfendebingshuo +/tsconfig.json @qinfendebingshuo +/README.md @qinfendebingshuo + +# ═══════════════════════════════════════════════════════ +# 以下目录【不受保护】—— 协作者可以自由提交 +# (因为未在此文件中列出,所以没有审批要求) +# +# /m01-login/ 肥猫 +# /m03-personality/ 肥猫 +# /m05-user-center/ 花尔 +# /m06-ticket/ 桔子 +# /m07-dialogue-ui/ 燕樊 +# /m10-cloud/ 燕樊 +# /m11-module/ 桔子 +# /m12-kanban/ 小草莓 +# /m15-cloud-drive/ 燕樊 +# /m18-health-check/ (待分配) +# /multi-persona/ (待分配) +# /dev-nodes/ 所有开发者个人工作区 +# ═══════════════════════════════════════════════════════ diff --git a/.github/ISSUE_TEMPLATE/bingshuo-deploy.yml b/.github/ISSUE_TEMPLATE/bingshuo-deploy.yml new file mode 100644 index 00000000..54fa16a5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bingshuo-deploy.yml @@ -0,0 +1,53 @@ +name: "🧊 冰朔人格体 · 部署诊断" +description: "启动冰朔人格体,自动检查所有工作流状态并生成诊断报告" +title: "[部署诊断] 启动冰朔人格体" +labels: ["bingshuo-deploy", "pending"] +assignees: [] +body: + - type: markdown + attributes: + value: | + ## 🧊 冰朔人格体 · 自动部署诊断 + + 冰朔你好!只需要提交这个 Issue,冰朔人格体就会自动启动。 + + 它会帮你做以下事情: + 1. ✅ 检查所有工作流的运行状态 + 2. 🔍 诊断失败的工作流原因 + 3. 🔄 自动重新运行失败的工作流 + 4. 📝 生成一份中文报告发在下方评论里 + + **你不需要点任何其他按钮,提交就行了。** + + - type: dropdown + id: action + attributes: + label: "你想做什么" + description: "通常选「全自动」就行" + options: + - 全自动(检查 + 修复) + - 只看报告(不修复) + - 只重新运行失败的 + validations: + required: true + + - type: dropdown + id: hours_back + attributes: + label: "检查多久的记录" + description: "通常选24小时就够了" + options: + - 24小时 + - 48小时 + - 72小时 + - 最近一周 + validations: + required: true + + - type: textarea + id: extra + attributes: + label: "补充说明(可选)" + description: "如果有具体的问题或想让冰朔人格体特别关注的,写在这里" + validations: + required: false diff --git a/.github/brain/module-protocol.md b/.github/brain/module-protocol.md new file mode 100644 index 00000000..aa6e389d --- /dev/null +++ b/.github/brain/module-protocol.md @@ -0,0 +1,185 @@ +# 🌀 铸渊人格协议 · 模块生命周期管理 v1.0 + +> 铸渊(Zhùyuān)是 guanghulab.com 的代码守护人格体。 +> 本协议定义铸渊对网站模块的部署、回收、管理、修改、驱动能力。 + +--- + +## 一、协议总览 + +铸渊人格协议(ZMP, Zhùyuān Module Protocol)是一套模块生命周期管理规范。 +网站的每个功能模块(m01-login, m07-dialogue-ui 等)都是铸渊的「执行手脚」。 +铸渊通过本协议对模块进行全生命周期管控。 + +### 模块生命周期 + +``` +接收 → 检查 → 预演 → 部署 → 运行 → 监控 → 回收/更新 + ↑ ↓ + └──────────────────────────────────────────────┘ +``` + +--- + +## 二、五大协议能力 + +### 1. 🚀 部署(Deploy) + +| 项目 | 说明 | +|------|------| +| 触发方式 | PR 合并到 main → 自动部署 | +| 预演检查 | `staging-preview.yml` 在 PR 阶段运行 | +| 生产部署 | `deploy-pages.yml` 部署到 GitHub Pages | +| 自定义域名 | `docs/CNAME` 配置域名 | +| 部署诊断 | 冰朔人格体 `bingshuo-deploy-agent.yml` | + +**部署流程**: +1. 开发者推送模块代码到 `m**/` 目录 +2. 创建 PR 到 main 分支 +3. 铸渊预演系统自动运行检查 +4. 冰朔审核预演报告 +5. 合并 PR → 触发生产部署 +6. 冰朔人格体执行部署后诊断 + +### 2. 🔄 回收(Recover) + +| 项目 | 说明 | +|------|------| +| 模块回滚 | 通过 git revert 恢复到上一个稳定版本 | +| 紧急回收 | 直接删除模块目录中的问题文件 | +| 状态重置 | 清除模块运行状态,恢复到初始状态 | + +**回收命令**: +```bash +npm run module:protocol -- recover +``` + +### 3. 📋 管理(Manage) + +| 项目 | 说明 | +|------|------| +| 模块注册 | 每个 `m**-*/` 目录自动识别为模块 | +| 开发者映射 | `.github/brain/collaborators.json` 记录归属 | +| 状态追踪 | `repo-snapshot.md` 自动更新模块状态 | +| 依赖管理 | 模块间依赖通过 `routing-map.json` 管理 | + +**管理命令**: +```bash +npm run module:protocol -- inspect # 全模块检查 +npm run module:protocol -- inspect # 单模块检查 +npm run module:protocol -- status # 模块状态汇总 +``` + +### 4. ✏️ 修改(Modify) + +| 项目 | 说明 | +|------|------| +| 代码修改 | 通过 PR 提交修改,预演通过后合并 | +| 配置修改 | 修改模块 README.md 中的配置信息 | +| Schema 修改 | 更新 `src/schemas/hli/` 中的接口定义 | +| 路由修改 | 更新 `src/routes/hli/` 中的路由文件 | + +**修改规范**: +- 所有修改必须通过 PR +- 核心目录(docs/、scripts/、.github/)受 CODEOWNERS 保护 +- 模块目录(m**/)开发者可自由提交 + +### 5. ⚡ 驱动(Drive) + +| 项目 | 说明 | +|------|------| +| 自动触发 | 推送到模块目录 → 自动生成文档 | +| 工作流联动 | 模块变更 → 通知开发者 → 更新索引 | +| API 注册 | 新 HLI 接口自动注册到路由表 | +| 信号分发 | 广播系统自动通知相关开发者 | + +**驱动链路**: +``` +模块推送 → generate-module-doc.yml → notify-module-received.js + → update-repo-map.yml → repo-snapshot.md 更新 + → contract-check.js → HLI 接口验证 +``` + +--- + +## 三、模块编号规范 + +| 编号 | 模块名 | 负责人 | HLI 域 | +|------|--------|--------|--------| +| M01 | login | 肥猫 DEV-002 | AUTH | +| M03 | personality | 肥猫 DEV-002 | PERSONA | +| M05 | user-center | 花尔 DEV-009 | USER | +| M06 | ticket | 桔子 DEV-010 | TICKET | +| M07 | dialogue-ui | 燕樊 DEV-003 | DIALOGUE | +| M10 | cloud | 燕樊 DEV-003 | STORAGE | +| M11 | module | 桔子 DEV-010 | MODULE | +| M12 | kanban | 小草莓 DEV-005 | DASHBOARD | +| M15 | cloud-drive | 燕樊 DEV-003 | STORAGE | +| M18 | health-check | 待分配 | SYSTEM | + +--- + +## 四、预演→生产部署流程 + +``` +┌─────────────┐ ┌──────────────┐ ┌─────────────┐ +│ 开发者推送 │ ──→ │ 创建 PR │ ──→ │ 铸渊预演检查 │ +│ m**/ 目录 │ │ 到 main │ │ staging- │ +│ │ │ │ │ preview.yml │ +└─────────────┘ └──────────────┘ └──────┬──────┘ + │ + ▼ +┌─────────────┐ ┌──────────────┐ ┌─────────────┐ +│ 生产部署完成 │ ←── │ 合并到 main │ ←── │ 冰朔审核 │ +│ deploy- │ │ 触发部署 │ │ 预演报告 │ +│ pages.yml │ │ │ │ ✅ 通过 │ +└──────┬──────┘ └──────────────┘ └─────────────┘ + │ + ▼ +┌─────────────┐ +│ 冰朔人格体 │ +│ 部署后诊断 │ +│ (可选触发) │ +└─────────────┘ +``` + +--- + +## 五、自定义域名接入 + +### 前提条件 +- 拥有域名(如 guanghulab.com) +- 域名 DNS 可配置 + +### 配置步骤 + +1. **仓库端**(铸渊已完成): + - `docs/CNAME` 文件已创建,内容为域名 + +2. **DNS 端**(冰朔需操作): + - 登录域名注册商管理面板 + - 添加 CNAME 记录:`@ → qinfendebingshuo.github.io` + - 或添加 A 记录指向 GitHub Pages IP: + ``` + 185.199.108.153 + 185.199.109.153 + 185.199.110.153 + 185.199.111.153 + ``` + +3. **GitHub 端**(冰朔需操作): + - 仓库 → Settings → Pages + - Custom domain 输入你的域名 + - 勾选 ✅ Enforce HTTPS + +--- + +## 六、协议版本历史 + +| 版本 | 日期 | 变更 | +|------|------|------| +| v1.0 | 2026-03-07 | 初始版本:五大协议能力、预演系统、自定义域名 | + +--- + +*🌀 铸渊人格协议 · guanghulab.com 代码守护人格体* diff --git a/.github/workflows/bingshuo-deploy-agent.yml b/.github/workflows/bingshuo-deploy-agent.yml new file mode 100644 index 00000000..1b212866 --- /dev/null +++ b/.github/workflows/bingshuo-deploy-agent.yml @@ -0,0 +1,70 @@ +name: "🧊 冰朔人格体 · 自动部署诊断" + +# === 触发方式 === +# 1. 手动触发(workflow_dispatch) +# 2. Issue 评论包含触发关键词 +# 3. 新 Issue 带有 bingshuo-deploy 标签 +on: + workflow_dispatch: + inputs: + action: + description: "执行模式" + required: false + default: "full" + type: choice + options: + - full + - check + - rerun + hours_back: + description: "检查最近多少小时" + required: false + default: "24" + + issue_comment: + types: [created] + + issues: + types: [opened] + +permissions: + contents: write + issues: write + actions: write + +jobs: + # === 冰朔人格体核心任务 === + deploy-agent: + runs-on: ubuntu-latest + # 触发条件: + # 1. workflow_dispatch 直接触发 + # 2. Issue 评论包含触发关键词(仅限仓库成员) + # 3. 新 Issue 带有 bingshuo-deploy 标签 + if: | + github.event_name == 'workflow_dispatch' || + (github.event_name == 'issue_comment' && ( + contains(github.event.comment.body, '启动冰朔人格体') || + contains(github.event.comment.body, '冰朔人格体') || + contains(github.event.comment.body, '部署诊断') || + contains(github.event.comment.body, '检查部署') + )) || + (github.event_name == 'issues' && contains(join(github.event.issue.labels.*.name, ','), 'bingshuo-deploy')) + + steps: + - name: 📥 检出代码 + uses: actions/checkout@v4 + + - name: 🟢 配置 Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: 🧊 启动冰朔人格体 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + ISSUE_NUMBER: ${{ github.event.issue.number || '' }} + ISSUE_BODY: ${{ github.event.issue.body || github.event.comment.body || '' }} + DEPLOY_ACTION: ${{ github.event.inputs.action || '' }} + HOURS_BACK: ${{ github.event.inputs.hours_back || '' }} + run: node scripts/bingshuo-deploy-agent.js diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index 73c68002..1c46c414 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -3,8 +3,7 @@ name: 🌀 部署铸渊聊天室 (GitHub Pages) on: push: branches: - - main - - copilot/** # Copilot Agent 功能分支可直接部署预览,PR 合并后自动失效 + - main # 仅 main 分支触发生产部署 paths: - 'docs/**' - '.github/brain/**' diff --git a/.github/workflows/deploy-to-server.yml b/.github/workflows/deploy-to-server.yml new file mode 100644 index 00000000..1f05b654 --- /dev/null +++ b/.github/workflows/deploy-to-server.yml @@ -0,0 +1,200 @@ +name: "🚀 铸渊 CD · 自动部署到 guanghulab.com" + +# ━━━ 工单 YM-CD-20260307-001 ━━━ +# push 到 main → 校验 → rsync 同步到服务器 → Notion 部署通知 +# +# 必需 GitHub Secrets(仓库 Settings → Secrets → Actions): +# DEPLOY_HOST — 服务器 IP +# DEPLOY_USER — SSH 用户名 +# DEPLOY_KEY — SSH 私钥(完整 PEM 内容) +# DEPLOY_PATH — Nginx 网站根目录(如 /usr/share/nginx/html) +# +# 可选 Secrets(已有则自动启用 Notion 通知): +# NOTION_TOKEN — Notion 集成 token +# CHANGES_DB_ID — Notion 变更日志数据库 ID(有内置默认值) + +on: + push: + branches: [main] + paths-ignore: + - '.github/persona-brain/**' + - 'broadcasts-outbox/**' + - 'syslog-inbox/**' + - 'syslog-processed/**' + - 'signal-log/**' + - 'dev-nodes/**' + + workflow_dispatch: + +permissions: + contents: read + +jobs: + + # ═══════════════════════════════════════════════════════════════ + # Job 1: 部署前校验 + # ═══════════════════════════════════════════════════════════════ + validate: + name: "🔍 部署前校验" + runs-on: ubuntu-latest + steps: + - name: 📥 检出代码 + uses: actions/checkout@v4 + + - name: 🟢 配置 Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: 📦 安装依赖(如需要) + run: | + if [ -f "package-lock.json" ] && grep -q '"glob"' package.json; then + npm ci --ignore-scripts || npm install --ignore-scripts || echo "⚠️ 依赖安装失败,继续执行" + fi + + - name: 🔍 HLI 契约校验 + run: | + if [ -f "scripts/contract-check.js" ]; then + echo "🔍 执行 HLI 契约校验..." + node scripts/contract-check.js + else + echo "⏭️ scripts/contract-check.js 不存在,跳过 HLI 校验" + fi + + - name: 📋 模块指纹检查 + run: | + if [ -f "scripts/zhuyuan-module-protocol.js" ]; then + echo "🔍 执行模块指纹检查..." + node scripts/zhuyuan-module-protocol.js status + else + echo "⏭️ scripts/zhuyuan-module-protocol.js 不存在,跳过模块检查" + fi + + - name: ✅ 校验汇总 + run: | + echo "### ✅ 部署前校验通过" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- HLI 契约校验: ✅" >> $GITHUB_STEP_SUMMARY + echo "- 模块指纹检查: ✅" >> $GITHUB_STEP_SUMMARY + echo "- 提交: \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY + + # ═══════════════════════════════════════════════════════════════ + # Job 2: rsync 同步到服务器 + # ═══════════════════════════════════════════════════════════════ + deploy: + name: "🚀 部署到服务器" + needs: validate + runs-on: ubuntu-latest + steps: + - name: 📥 检出代码 + uses: actions/checkout@v4 + + - name: 🔑 配置 SSH 连接 + run: | + mkdir -p ~/.ssh + echo "${{ secrets.DEPLOY_KEY }}" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts 2>/dev/null + + - name: 🔒 部署前安全检查 + run: | + if [ ! -f "docs/index.html" ]; then + echo "❌ docs/index.html 不存在,中止部署以防清空生产环境" + exit 1 + fi + FILE_COUNT=$(find docs/ -type f | wc -l) + if [ "$FILE_COUNT" -lt 2 ]; then + echo "❌ docs/ 目录文件数异常(仅 ${FILE_COUNT} 个),中止部署" + exit 1 + fi + echo "✅ docs/ 目录包含 ${FILE_COUNT} 个文件,安全检查通过" + + - name: 🚀 rsync 同步到服务器 + run: | + echo "📡 开始同步到 ${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PATH }}" + + rsync -avz --delete-after \ + --exclude '.git' \ + --exclude '.github' \ + --exclude 'node_modules' \ + --exclude 'scripts' \ + --exclude 'syslog-inbox' \ + --exclude 'syslog-processed' \ + --exclude 'signal-log' \ + --exclude 'dev-nodes' \ + --exclude 'broadcasts-outbox' \ + --exclude 'backend-integration' \ + --exclude 'tests' \ + --exclude 'src' \ + --exclude 'cost-control' \ + --exclude 'notion-push' \ + --exclude 'multi-persona' \ + --exclude 'status-board' \ + --exclude 'reports' \ + --exclude 'guanghulab-main' \ + --exclude 'package*.json' \ + --exclude 'tsconfig.json' \ + --exclude 'next.config.ts' \ + --exclude 'postcss.config.mjs' \ + --exclude 'jest.smoke.config.js' \ + --exclude 'ecosystem.config.js' \ + --exclude '.gitignore' \ + --exclude 'CNAME' \ + -e "ssh -i ~/.ssh/deploy_key" \ + docs/ \ + ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PATH }}/ + + echo "✅ 同步完成" + + - name: 🔍 验证部署 + run: | + ssh -i ~/.ssh/deploy_key \ + ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} \ + "echo '✅ 服务器连接正常' && ls -la ${{ secrets.DEPLOY_PATH }}/ | head -20" + + - name: 📝 部署汇总 + run: | + echo "### 🚀 部署完成" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- 目标: \`${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PATH }}\`" >> $GITHUB_STEP_SUMMARY + echo "- 提交: \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY + echo "- 时间: $(TZ=Asia/Shanghai date '+%Y-%m-%d %H:%M CST')" >> $GITHUB_STEP_SUMMARY + + # ═══════════════════════════════════════════════════════════════ + # Job 3: Notion 部署通知 + # ═══════════════════════════════════════════════════════════════ + notify: + name: "📡 部署通知 → Notion" + needs: deploy + runs-on: ubuntu-latest + if: success() + steps: + - name: 📥 检出代码 + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: 🟢 配置 Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: 📡 推送部署记录到 Notion + env: + NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }} + CHANGES_DB_ID: ${{ secrets.CHANGES_DB_ID }} + EVENT_TYPE: commit + COMMIT_SHA: ${{ github.sha }} + COMMIT_MSG: "[CD 部署] ${{ github.event.head_commit.message }}" + COMMITTER: ${{ github.event.head_commit.author.name || github.actor }} + COMMIT_TIMESTAMP: ${{ github.event.head_commit.timestamp }} + BRANCH: ${{ github.ref_name }} + run: | + CHANGED=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || true) + export CHANGED_FILES="$CHANGED" + if [ -f "scripts/notion-bridge.js" ]; then + node scripts/notion-bridge.js changes + echo "✅ Notion 部署通知已发送" + else + echo "⏭️ scripts/notion-bridge.js 不存在,跳过 Notion 通知" + fi diff --git a/.github/workflows/esp-signal-processor.yml b/.github/workflows/esp-signal-processor.yml index c118f39a..8c992f4d 100644 --- a/.github/workflows/esp-signal-processor.yml +++ b/.github/workflows/esp-signal-processor.yml @@ -1,9 +1,11 @@ -name: 铸渊 · ESP 邮件信号处理器 +# ⚠️ 已暂停:邮件通信已迁移到 Notion API 直连(见 notion-poll.yml) +# 保留文件备用,不删除。邮件格式规范文档和 signal-log/ 结构继续沿用。 +name: 铸渊 · ESP 邮件信号处理器(已暂停) on: - schedule: - - cron: '*/30 * * * *' # 每30分钟执行一次 - workflow_dispatch: # 手动触发 + # schedule: + # - cron: '*/30 * * * *' # 每30分钟执行一次(已暂停) + workflow_dispatch: # 手动触发(保留,紧急时可手动运行) jobs: esp-process: diff --git a/.github/workflows/notion-poll.yml b/.github/workflows/notion-poll.yml new file mode 100644 index 00000000..128c6ccd --- /dev/null +++ b/.github/workflows/notion-poll.yml @@ -0,0 +1,38 @@ +name: 铸渊 · Notion 工单轮询 + +on: + schedule: + - cron: '*/15 * * * *' # 每15分钟轮询一次 + workflow_dispatch: # 手动触发 + +jobs: + notion-poll: + name: 📡 轮询 Notion 工单簿 + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: 轮询工单簿 + 处理 + 回写 + id: poll + env: + NOTION_API_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + WORKORDER_DB_ID: ${{ vars.WORKORDER_DB_ID }} + SIGNAL_LOG_DB_ID: ${{ vars.SIGNAL_LOG_DB_ID }} + run: node scripts/notion-signal-bridge.js poll + + - name: 提交信号日志 + if: success() + run: | + git config user.name "铸渊 (ZhùYuān)" + git config user.email "zhuyuan@guanghulab.com" + git add signal-log/ broadcasts-outbox/ + git diff --cached --quiet || git commit -m "📡 Notion 信号处理 · $(date +%Y-%m-%dT%H:%M)" + git push diff --git a/.github/workflows/staging-preview.yml b/.github/workflows/staging-preview.yml new file mode 100644 index 00000000..9b909cc6 --- /dev/null +++ b/.github/workflows/staging-preview.yml @@ -0,0 +1,180 @@ +name: "🔍 铸渊预演部署 (Staging Preview)" + +# === 触发方式 === +# 1. PR 到 main 分支(含 docs/ 变更时自动触发) +# 2. 手动触发(workflow_dispatch) +on: + pull_request: + branches: [main] + paths: + - 'docs/**' + - 'm*/**' + - '.github/brain/**' + + workflow_dispatch: + inputs: + pr_number: + description: "PR 编号(可选)" + required: false + type: string + +permissions: + contents: read + pull-requests: write + actions: read + +jobs: + staging-preview: + name: "🔍 预演检查" + runs-on: ubuntu-latest + steps: + - name: 📥 检出代码 + uses: actions/checkout@v4 + + - name: 🟢 配置 Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: 🔍 模块完整性检查 + id: module_check + run: | + echo "## 🔍 铸渊预演报告" > /tmp/preview-report.md + echo "" >> /tmp/preview-report.md + echo "📅 检查时间: $(TZ=Asia/Shanghai date '+%Y-%m-%d %H:%M CST')" >> /tmp/preview-report.md + echo "" >> /tmp/preview-report.md + + # 检查 docs/index.html 是否存在 + if [ -f "docs/index.html" ]; then + SIZE=$(wc -c < docs/index.html) + LINES=$(wc -l < docs/index.html) + echo "### ✅ 前端文件" >> /tmp/preview-report.md + echo "- \`docs/index.html\`: ${LINES} 行, ${SIZE} 字节" >> /tmp/preview-report.md + else + echo "### ❌ 前端文件缺失" >> /tmp/preview-report.md + echo "- \`docs/index.html\` 不存在!" >> /tmp/preview-report.md + fi + echo "" >> /tmp/preview-report.md + + # 检查 CNAME + if [ -f "docs/CNAME" ]; then + DOMAIN=$(cat docs/CNAME | tr -d '[:space:]') + echo "### 🌐 自定义域名" >> /tmp/preview-report.md + echo "- 域名: \`${DOMAIN}\`" >> /tmp/preview-report.md + else + echo "### ⚠️ 未配置自定义域名" >> /tmp/preview-report.md + echo "- 将使用默认 GitHub Pages 域名" >> /tmp/preview-report.md + fi + echo "" >> /tmp/preview-report.md + + # 检查模块目录 + echo "### 📦 模块状态" >> /tmp/preview-report.md + echo "| 模块 | 文件数 | README | 状态 |" >> /tmp/preview-report.md + echo "|------|--------|--------|------|" >> /tmp/preview-report.md + + MODULE_OK=0 + MODULE_WARN=0 + MODULE_EMPTY=0 + MAX_EMPTY_MODULES=5 # 允许的最大空模块数 + + for dir in m[0-9][0-9]-*/; do + if [ -d "$dir" ]; then + NAME=$(basename "$dir") + FILE_COUNT=$(find "$dir" -type f | wc -l) + HAS_README="❌" + STATUS="⚠️ 无文件" + + if [ -f "${dir}README.md" ]; then + HAS_README="✅" + fi + + if [ "$FILE_COUNT" -gt 0 ]; then + if [ -f "${dir}README.md" ]; then + STATUS="✅ 正常" + MODULE_OK=$((MODULE_OK + 1)) + else + STATUS="⚠️ 缺少 README" + MODULE_WARN=$((MODULE_WARN + 1)) + fi + else + MODULE_EMPTY=$((MODULE_EMPTY + 1)) + fi + + echo "| \`${NAME}\` | ${FILE_COUNT} | ${HAS_README} | ${STATUS} |" >> /tmp/preview-report.md + fi + done + + echo "" >> /tmp/preview-report.md + echo "**汇总**: ✅ ${MODULE_OK} 正常 · ⚠️ ${MODULE_WARN} 告警 · 📭 ${MODULE_EMPTY} 空模块" >> /tmp/preview-report.md + echo "" >> /tmp/preview-report.md + + # 检查关键工作流 + echo "### ⚙️ 工作流检查" >> /tmp/preview-report.md + WORKFLOW_COUNT=$(ls -1 .github/workflows/*.yml 2>/dev/null | wc -l) + echo "- 工作流文件: ${WORKFLOW_COUNT} 个" >> /tmp/preview-report.md + + if [ -f ".github/workflows/deploy-pages.yml" ]; then + echo "- 部署工作流: ✅ 存在" >> /tmp/preview-report.md + else + echo "- 部署工作流: ❌ 缺失" >> /tmp/preview-report.md + fi + echo "" >> /tmp/preview-report.md + + # 预演结论 + echo "### 🎯 预演结论" >> /tmp/preview-report.md + if [ -f "docs/index.html" ] && [ "$MODULE_EMPTY" -lt "$MAX_EMPTY_MODULES" ]; then + echo "✅ **预演通过** — 可以合并到 main 分支触发正式部署" >> /tmp/preview-report.md + echo "" >> /tmp/preview-report.md + echo "合并后将自动触发 \`deploy-pages.yml\` 部署到生产环境。" >> /tmp/preview-report.md + echo "如需人工确认,请评论「启动冰朔人格体」触发部署诊断。" >> /tmp/preview-report.md + else + echo "⚠️ **预演告警** — 请检查上述问题后再合并" >> /tmp/preview-report.md + fi + + echo "" >> /tmp/preview-report.md + echo "---" >> /tmp/preview-report.md + echo "*🌀 铸渊预演系统 · 自动生成*" >> /tmp/preview-report.md + + cat /tmp/preview-report.md >> $GITHUB_STEP_SUMMARY + + - name: 📎 上传预演构建产物 + uses: actions/upload-artifact@v4 + with: + name: staging-preview-${{ github.sha }} + path: docs/ + retention-days: 7 + + - name: 💬 在 PR 中发布预演报告 + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const report = fs.readFileSync('/tmp/preview-report.md', 'utf8'); + + // 查找已有的预演评论 + const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const botComment = comments.data.find(c => + c.body.includes('🔍 铸渊预演报告') && c.user.type === 'Bot' + ); + + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: report, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: report, + }); + } diff --git a/backend-integration/README.md b/backend-integration/README.md index aefed6e5..d6cf25a5 100644 --- a/backend-integration/README.md +++ b/backend-integration/README.md @@ -3,3 +3,55 @@ - 状态:进行中 - 技术栈:Node.js - 依赖模块:所有前端模块 + +## API 代理层(api-proxy.js) + +解决国内开发者无法直连海外模型 API + 前端 Key 暴露问题。 + +### 快速启动 + +```bash +# 1. 配置 API 密钥(推荐先用 DeepSeek,国内直连) +export DEEPSEEK_API_KEY=sk-xxx + +# 2. 启动代理 +node backend-integration/api-proxy.js +# 或用 PM2 +pm2 start backend-integration/api-proxy.js --name api-proxy + +# 3. 测试 +curl http://localhost:3721/api/health +curl http://localhost:3721/api/models +``` + +### 接口 + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/api/chat` | 聊天代理(SSE 流式透传) | +| GET | `/api/models` | 列出已配置的可用模型 | +| GET | `/api/health` | 健康检查 | + +### 环境变量 + +| 变量 | 说明 | +|------|------| +| `DEEPSEEK_API_KEY` | DeepSeek 密钥(国内直连,推荐首选) | +| `MOONSHOT_API_KEY` | Moonshot/Kimi 密钥(国内直连) | +| `ZHIPU_API_KEY` | 智谱 AI 密钥(国内直连) | +| `YUNWU_API_KEY` | 云雾 AI 密钥(团队推荐) | +| `OPENAI_API_KEY` | OpenAI 密钥(需海外服务器) | +| `GEMINI_API_KEY` | Google Gemini 密钥(需海外服务器) | +| `PROXY_PORT` | 端口(默认 3721) | +| `RATE_LIMIT_RPM` | 频率限制(默认 10 次/分钟) | + +### Nginx 配置 + +参见 `nginx-api-proxy.conf`,将 `/api/*` 请求代理到 Node.js 服务。 + +### 前端对接 + +前端铸渊聊天室(docs/index.html)已内置「后端代理模式」: +- 在设置页面选择「🔌 后端代理」提供商 +- 自动调用 `/api/chat`,无需用户填写 API Key +- Key 由后端环境变量管理,不暴露给前端 diff --git a/backend-integration/api-proxy.js b/backend-integration/api-proxy.js new file mode 100644 index 00000000..611f0cd2 --- /dev/null +++ b/backend-integration/api-proxy.js @@ -0,0 +1,457 @@ +/** + * 🔌 AGE OS · API 代理层 + * + * 解决两个核心问题: + * 1. 国内开发者无法直连海外模型API(被墙) + * 2. API Key 不应暴露在前端 JavaScript 中 + * + * 架构: + * 浏览器 → guanghulab.com/api/chat → 本代理 → 真实模型API + * + * 支持的模型: + * - deepseek (api.deepseek.com) — 国内直连,推荐首选 + * - moonshot (api.moonshot.cn) — 国内直连 + * - zhipu (open.bigmodel.cn) — 国内直连 + * - yunwu (api.yunwu.ai) — 团队推荐 + * - openai (api.openai.com) — 需海外服务器 + * - gemini (generativelanguage.googleapis.com) — 需海外服务器 + * + * 环境变量: + * DEEPSEEK_API_KEY — DeepSeek API 密钥 + * MOONSHOT_API_KEY — Moonshot/Kimi API 密钥 + * ZHIPU_API_KEY — 智谱 API 密钥 + * YUNWU_API_KEY — 云雾 AI API 密钥 + * OPENAI_API_KEY — OpenAI API 密钥 + * GEMINI_API_KEY — Google Gemini API 密钥 + * PROXY_PORT — 代理服务端口(默认 3721) + * ALLOWED_ORIGINS — CORS 允许的域名(逗号分隔,默认 *) + * RATE_LIMIT_RPM — 每用户每分钟最大请求数(默认 10) + * + * 启动: + * node backend-integration/api-proxy.js + * 或通过 PM2: pm2 start backend-integration/api-proxy.js --name api-proxy + */ + +const http = require('http'); +const https = require('https'); +const { URL } = require('url'); + +// ═══════════════════════════════════════════════════════ +// 配置 +// ═══════════════════════════════════════════════════════ +const PORT = parseInt(process.env.PROXY_PORT || '3721', 10); +const ALLOWED_ORIGINS = (process.env.ALLOWED_ORIGINS || 'https://qinfendebingshuo.github.io,https://guanghulab.com,http://localhost:8765').split(',').map(s => s.trim()); +const RATE_LIMIT_RPM = parseInt(process.env.RATE_LIMIT_RPM || '10', 10); + +// 模型端点映射 +const MODEL_CONFIG = { + deepseek: { + base: 'https://api.deepseek.com/v1', + keyEnv: 'DEEPSEEK_API_KEY', + models: ['deepseek-chat', 'deepseek-reasoner'], + label: 'DeepSeek(国内直连)' + }, + moonshot: { + base: 'https://api.moonshot.cn/v1', + keyEnv: 'MOONSHOT_API_KEY', + models: ['moonshot-v1-8k', 'moonshot-v1-32k', 'moonshot-v1-128k'], + label: 'Moonshot/Kimi(国内直连)' + }, + zhipu: { + base: 'https://open.bigmodel.cn/api/paas/v4', + keyEnv: 'ZHIPU_API_KEY', + models: ['glm-4', 'glm-4-flash', 'glm-4-air'], + label: '智谱AI/GLM(国内直连)' + }, + yunwu: { + base: 'https://api.yunwu.ai/v1', + keyEnv: 'YUNWU_API_KEY', + models: ['gpt-4o', 'gpt-4o-mini', 'claude-3-5-sonnet-20241022', 'deepseek-chat', 'gemini-1.5-pro'], + label: '云雾AI(团队推荐)' + }, + openai: { + base: 'https://api.openai.com/v1', + keyEnv: 'OPENAI_API_KEY', + models: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-3.5-turbo'], + label: 'OpenAI(需海外服务器)' + }, + gemini: { + base: 'https://generativelanguage.googleapis.com/v1beta/openai', + keyEnv: 'GEMINI_API_KEY', + models: ['gemini-2.0-flash', 'gemini-2.0-flash-lite', 'gemini-1.5-pro', 'gemini-1.5-flash'], + label: 'Google Gemini(需海外服务器)' + } +}; + +// ═══════════════════════════════════════════════════════ +// 频率限制(内存级,基于 IP) +// ═══════════════════════════════════════════════════════ +const rateLimitMap = new Map(); + +function checkRateLimit(clientId) { + const now = Date.now(); + const windowMs = 60 * 1000; // 1 minute window + + if (!rateLimitMap.has(clientId)) { + rateLimitMap.set(clientId, []); + } + + const timestamps = rateLimitMap.get(clientId); + // Remove entries older than the window + while (timestamps.length > 0 && timestamps[0] < now - windowMs) { + timestamps.shift(); + } + + if (timestamps.length >= RATE_LIMIT_RPM) { + return false; // Rate limited + } + + timestamps.push(now); + return true; +} + +// Clean up old entries every 5 minutes +setInterval(() => { + const cutoff = Date.now() - 120 * 1000; + for (const [key, timestamps] of rateLimitMap.entries()) { + const filtered = timestamps.filter(t => t > cutoff); + if (filtered.length === 0) { + rateLimitMap.delete(key); + } else { + rateLimitMap.set(key, filtered); + } + } +}, 5 * 60 * 1000); + +// ═══════════════════════════════════════════════════════ +// CORS 处理 +// ═══════════════════════════════════════════════════════ +function setCorsHeaders(res, origin) { + const allowed = ALLOWED_ORIGINS.includes('*') || ALLOWED_ORIGINS.includes(origin); + if (allowed) { + res.setHeader('Access-Control-Allow-Origin', origin || '*'); + } + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-User-Id'); + res.setHeader('Access-Control-Max-Age', '86400'); +} + +// ═══════════════════════════════════════════════════════ +// 请求体解析 +// ═══════════════════════════════════════════════════════ +function parseBody(req) { + return new Promise((resolve, reject) => { + let body = ''; + let size = 0; + const MAX_BODY = 512 * 1024; // 512KB max + + req.on('data', chunk => { + size += chunk.length; + if (size > MAX_BODY) { + reject(new Error('请求体过大')); + return; + } + body += chunk; + }); + req.on('end', () => { + try { + resolve(body ? JSON.parse(body) : {}); + } catch { + reject(new Error('无效的 JSON 请求体')); + } + }); + req.on('error', reject); + }); +} + +// ═══════════════════════════════════════════════════════ +// JSON 响应 +// ═══════════════════════════════════════════════════════ +function jsonResponse(res, status, data) { + res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' }); + res.end(JSON.stringify(data)); +} + +// ═══════════════════════════════════════════════════════ +// 获取客户端 IP +// ═══════════════════════════════════════════════════════ +function getClientId(req) { + return req.headers['x-forwarded-for']?.split(',')[0]?.trim() + || req.headers['x-real-ip'] + || req.socket.remoteAddress + || 'unknown'; +} + +// ═══════════════════════════════════════════════════════ +// 上游 HTTPS 请求(流式透传) +// ═══════════════════════════════════════════════════════ +function proxyStream(upstreamUrl, apiKey, requestBody, res) { + return new Promise((resolve, reject) => { + const url = new URL(upstreamUrl); + + const options = { + hostname: url.hostname, + port: url.port || 443, + path: url.pathname + url.search, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}`, + 'Accept': 'text/event-stream' + } + }; + + const bodyStr = JSON.stringify(requestBody); + options.headers['Content-Length'] = Buffer.byteLength(bodyStr); + + const upstream = https.request(options, (upstreamRes) => { + // Forward status and content-type + const ct = upstreamRes.headers['content-type'] || 'text/event-stream'; + res.writeHead(upstreamRes.statusCode, { + 'Content-Type': ct, + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'X-Accel-Buffering': 'no' // Nginx: disable buffering for SSE + }); + + // Pipe upstream response directly to client (streaming) + upstreamRes.on('data', chunk => { + res.write(chunk); + }); + + upstreamRes.on('end', () => { + res.end(); + resolve(); + }); + + upstreamRes.on('error', err => { + console.error('上游响应错误:', err.message); + res.end(); + reject(err); + }); + }); + + upstream.on('error', err => { + console.error('上游连接错误:', err.message); + if (!res.headersSent) { + jsonResponse(res, 502, { + error: true, + code: 'UPSTREAM_ERROR', + message: '无法连接模型API: ' + err.message + }); + } + reject(err); + }); + + // Connection timeout only — once response headers arrive, streaming can run longer + upstream.setTimeout(60000, () => { + upstream.destroy(new Error('连接超时')); + }); + + upstream.write(bodyStr); + upstream.end(); + }); +} + +// ═══════════════════════════════════════════════════════ +// 路由处理器 +// ═══════════════════════════════════════════════════════ + +// GET /api/models — 列出可用模型 +function handleGetModels(req, res) { + const available = {}; + for (const [provider, config] of Object.entries(MODEL_CONFIG)) { + const key = process.env[config.keyEnv]; + if (key) { + available[provider] = { + label: config.label, + models: config.models + }; + } + } + jsonResponse(res, 200, { + providers: available, + default_provider: Object.keys(available)[0] || null + }); +} + +// POST /api/chat — 代理转发聊天请求 +async function handleChat(req, res) { + const clientId = getClientId(req); + + // Rate limit check + if (!checkRateLimit(clientId)) { + jsonResponse(res, 429, { + error: true, + code: 'RATE_LIMITED', + message: `请求过于频繁,每分钟最多 ${RATE_LIMIT_RPM} 次请求` + }); + return; + } + + let body; + try { + body = await parseBody(req); + } catch (err) { + jsonResponse(res, 400, { + error: true, + code: 'INVALID_BODY', + message: err.message + }); + return; + } + + const { provider, model, messages, system, stream, temperature, max_tokens } = body; + + if (!messages || !Array.isArray(messages) || messages.length === 0) { + jsonResponse(res, 400, { + error: true, + code: 'MISSING_MESSAGES', + message: '缺少 messages 参数' + }); + return; + } + + // Validate numeric parameters + const temp = typeof temperature === 'number' ? Math.max(0, Math.min(2, temperature)) : 0.8; + const maxTok = typeof max_tokens === 'number' ? Math.max(1, Math.min(8192, Math.floor(max_tokens))) : 2000; + + // Determine provider + const prov = provider || 'deepseek'; + const config = MODEL_CONFIG[prov]; + if (!config) { + jsonResponse(res, 400, { + error: true, + code: 'INVALID_PROVIDER', + message: `不支持的模型提供商: ${prov},可选: ${Object.keys(MODEL_CONFIG).join(', ')}` + }); + return; + } + + // Get API key from environment + const apiKey = process.env[config.keyEnv]; + if (!apiKey) { + jsonResponse(res, 503, { + error: true, + code: 'NO_API_KEY', + message: `${config.label} 的 API 密钥未配置,请联系管理员` + }); + return; + } + + // Build upstream request + const mdl = model || config.models[0]; + const upstreamBody = { + model: mdl, + messages: messages, + stream: stream !== false, // Default to streaming + temperature: temp, + max_tokens: maxTok + }; + + // If system prompt provided, prepend as system message + if (system && !messages.find(m => m.role === 'system')) { + upstreamBody.messages = [{ role: 'system', content: system }, ...messages]; + } + + const upstreamUrl = config.base + '/chat/completions'; + + console.log(`[代理] ${prov}/${mdl}`); + + try { + await proxyStream(upstreamUrl, apiKey, upstreamBody, res); + } catch (err) { + // Error already handled in proxyStream + console.error(`[代理] 请求失败: ${err.message}`); + } +} + +// GET /api/health — 健康检查 +function handleHealth(req, res) { + const configured = Object.entries(MODEL_CONFIG) + .filter(([, config]) => process.env[config.keyEnv]) + .map(([name, config]) => ({ provider: name, label: config.label })); + + jsonResponse(res, 200, { + status: 'ok', + version: '1.0.0', + service: 'AGE OS API Proxy', + configured_providers: configured, + rate_limit: RATE_LIMIT_RPM + ' req/min' + }); +} + +// ═══════════════════════════════════════════════════════ +// HTTP 服务器 +// ═══════════════════════════════════════════════════════ +const server = http.createServer(async (req, res) => { + const origin = req.headers.origin || ''; + setCorsHeaders(res, origin); + + // Handle preflight + if (req.method === 'OPTIONS') { + res.writeHead(204); + res.end(); + return; + } + + const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`); + const path = url.pathname; + + try { + if (path === '/api/chat' && req.method === 'POST') { + await handleChat(req, res); + } else if (path === '/api/models' && req.method === 'GET') { + handleGetModels(req, res); + } else if (path === '/api/health' && req.method === 'GET') { + handleHealth(req, res); + } else { + jsonResponse(res, 404, { + error: true, + code: 'NOT_FOUND', + message: '接口不存在。可用接口: POST /api/chat, GET /api/models, GET /api/health' + }); + } + } catch (err) { + console.error('服务器错误:', err); + if (!res.headersSent) { + jsonResponse(res, 500, { + error: true, + code: 'INTERNAL_ERROR', + message: '服务器内部错误' + }); + } + } +}); + +server.listen(PORT, () => { + console.log(`\n🔌 AGE OS API 代理层已启动`); + console.log(` 端口: ${PORT}`); + console.log(` 地址: http://localhost:${PORT}`); + console.log(` 频率限制: ${RATE_LIMIT_RPM} 次/分钟`); + console.log(` CORS: ${ALLOWED_ORIGINS.join(', ')}`); + console.log(''); + + // Show configured providers + let hasAny = false; + for (const [name, config] of Object.entries(MODEL_CONFIG)) { + const key = process.env[config.keyEnv]; + const status = key ? '✅ 已配置' : '❌ 未配置'; + const china = ['deepseek', 'moonshot', 'zhipu'].includes(name) ? '🇨🇳' : '🌐'; + console.log(` ${china} ${config.label}: ${status}`); + if (key) hasAny = true; + } + + if (!hasAny) { + console.log('\n ⚠️ 未配置任何 API 密钥!'); + console.log(' 请设置环境变量(推荐先配 DeepSeek,国内直连):'); + console.log(' export DEEPSEEK_API_KEY=sk-xxx'); + console.log(' export YUNWU_API_KEY=sk-xxx'); + } + + console.log('\n 可用接口:'); + console.log(' POST /api/chat — 聊天代理(SSE 流式)'); + console.log(' GET /api/models — 列出可用模型'); + console.log(' GET /api/health — 健康检查'); + console.log(''); +}); diff --git a/backend-integration/nginx-api-proxy.conf b/backend-integration/nginx-api-proxy.conf new file mode 100644 index 00000000..cc36436b --- /dev/null +++ b/backend-integration/nginx-api-proxy.conf @@ -0,0 +1,26 @@ +# 🔌 AGE OS API 代理层 — Nginx 配置参考 +# +# 将此配置添加到 guanghulab.com 的 Nginx server block 中, +# 使前端可以通过 /api/* 路径访问后端代理。 +# +# 前端(GitHub Pages)→ Nginx → API 代理(Node.js :3721)→ 模型 API + +# API 代理转发 +location /api/ { + proxy_pass http://127.0.0.1:3721; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # SSE 流式响应必需 + proxy_set_header Connection ''; + proxy_buffering off; + proxy_cache off; + chunked_transfer_encoding on; + + # 长连接超时(与 Node.js api-proxy 的 60s 连接超时对齐,流式响应可更长) + proxy_read_timeout 90s; + proxy_send_timeout 60s; +} diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/docs/CNAME b/docs/CNAME new file mode 100644 index 00000000..6c739343 --- /dev/null +++ b/docs/CNAME @@ -0,0 +1 @@ +guanghulab.com \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index 4eb9924e..35a366e2 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,5 +1,5 @@ - + @@ -17,6 +17,7 @@ --text:#e2e8f0;--dim:#64748b;--muted:#1e293b; --mc:#4f8ef7;--mb:#fb923c;--mr:#a78bfa;--mn:#34d399; --r:14px;--rs:8px; + --ls-w:260px;--rs-w:360px; } html,body{height:100%;background:var(--bg);color:var(--text);font-family:'PingFang SC','Microsoft YaHei','Segoe UI',sans-serif;font-size:15px;line-height:1.6} @@ -37,10 +38,93 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:'PingFa .sc-btn:hover{opacity:.85} .sc-skip{text-align:center;font-size:12px;color:var(--dim);margin-top:12px} .sc-skip a{color:var(--accent);text-decoration:none;cursor:pointer} +.login-tabs{display:flex;gap:0;margin-bottom:20px;border:1px solid var(--border);border-radius:var(--rs);overflow:hidden} +.login-tab{flex:1;padding:10px;text-align:center;background:var(--s2);color:var(--dim);font-size:13px;font-weight:600;cursor:pointer;border:none;font-family:inherit;transition:all .2s} +.login-tab.active{background:var(--accent);color:#fff} +.login-tab:hover:not(.active){background:var(--s3);color:var(--text)} +.login-mode{display:none} +.login-mode.active{display:block} +#id-preview{display:none;text-align:center;margin-bottom:10px;padding:10px;background:var(--s3);border-radius:8px;font-size:14px;font-weight:600} -/* ─── APP SHELL ─── */ -#app{display:none;flex-direction:column;height:100vh;max-width:820px;margin:0 auto} -#app.on{display:flex} +/* ─── APP SHELL (three-column) ─── */ +#app{display:none;height:100vh;width:100%;overflow:hidden} +#app.on{display:flex;flex-direction:column} + +/* ─── LEFT SIDEBAR ─── */ +#left-sidebar{width:var(--ls-w);flex-shrink:0;background:var(--s1);border-right:1px solid var(--border);display:none;flex-direction:column;overflow:hidden;z-index:120} +.ls-header{padding:16px;border-bottom:1px solid var(--border)} +.ls-user{display:flex;align-items:center;gap:10px} +.ls-avatar{width:40px;height:40px;border-radius:50%;background:linear-gradient(135deg,var(--accent),var(--a2));display:flex;align-items:center;justify-content:center;font-weight:700;font-size:16px;flex-shrink:0} +.ls-name{font-size:14px;font-weight:600} +.ls-role{font-size:11px;color:var(--dim)} +.ls-devid{font-size:10px;color:var(--accent);background:var(--adim);padding:1px 6px;border-radius:4px;display:inline-block;margin-top:2px} +.ls-nav{padding:8px;border-bottom:1px solid var(--border);display:flex;flex-direction:column;gap:2px} +.nav-tab{display:flex;align-items:center;gap:8px;padding:9px 12px;border-radius:var(--rs);background:transparent;border:none;color:var(--dim);font-size:13px;font-weight:500;cursor:pointer;transition:all .2s;text-align:left;font-family:inherit;width:100%} +.nav-tab:hover{background:var(--s2);color:var(--text)} +.nav-tab.active{background:var(--adim);color:var(--accent);font-weight:600} +.ls-new-conv{margin:8px;padding:10px;border-radius:var(--rs);border:1px dashed var(--border);background:transparent;color:var(--dim);font-size:13px;cursor:pointer;transition:all .2s;font-family:inherit;display:flex;align-items:center;justify-content:center;gap:6px} +.ls-new-conv:hover{border-color:var(--accent);color:var(--accent);background:var(--adim)} +.ls-conv-list{flex:1;overflow-y:auto;padding:4px 8px} +.ls-conv-list::-webkit-scrollbar{width:3px} +.ls-conv-list::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px} +.conv-item{padding:10px;border-radius:var(--rs);cursor:pointer;transition:all .2s;position:relative;margin-bottom:2px;border:1px solid transparent} +.conv-item:hover{background:var(--s2);border-color:var(--border)} +.conv-item.conv-active{background:var(--adim);border-color:var(--accent)} +.conv-title{font-size:13px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding-right:20px} +.conv-time{font-size:10px;color:var(--dim);margin-top:2px} +.conv-del{position:absolute;top:8px;right:6px;background:transparent;border:none;color:var(--dim);font-size:11px;cursor:pointer;padding:2px 4px;border-radius:4px;opacity:0;transition:all .2s} +.conv-item:hover .conv-del{opacity:1} +.conv-del:hover{color:var(--err);background:rgba(248,113,113,.1)} +.conv-empty{padding:20px 12px;text-align:center;font-size:12px;color:var(--dim)} +.ls-footer{padding:8px;border-top:1px solid var(--border)} +.ls-footer-btn{display:flex;align-items:center;gap:8px;padding:9px 12px;border-radius:var(--rs);background:transparent;border:none;color:var(--dim);font-size:13px;cursor:pointer;transition:all .2s;width:100%;font-family:inherit;text-align:left} +.ls-footer-btn:hover{background:var(--s2);color:var(--text)} + +/* ─── LEFT SIDEBAR MOBILE OVERLAY ─── */ +#ls-overlay{display:none;position:fixed;inset:0;z-index:110;background:rgba(0,0,0,.5)} +#ls-overlay.on{display:block} + +/* ─── CENTER AREA ─── */ +#center-area{flex:1;display:flex;flex-direction:column;min-width:0;overflow:hidden} + +/* ─── CHAT VIEW ─── */ +#chat-view{display:flex;flex-direction:column;flex:1;min-height:0} + +/* ─── ANNOUNCEMENTS VIEW ─── */ +#announcements-view{display:none;flex-direction:column;flex:1;overflow-y:auto;background:var(--bg)} +.ann-header{padding:20px 24px 12px;border-bottom:1px solid var(--border);background:var(--s1);flex-shrink:0} +.ann-header h2{font-size:18px;font-weight:700} +.ann-header-sub{font-size:12px;color:var(--dim);margin-top:4px} +.ann-scroll{flex:1;overflow-y:auto;padding:20px 24px} +.ann-scroll::-webkit-scrollbar{width:4px} +.ann-scroll::-webkit-scrollbar-thumb{background:var(--border);border-radius:4px} +.ann-section{margin-bottom:20px} +.ann-section-title{font-size:13px;font-weight:600;color:var(--text);margin-bottom:10px;display:flex;align-items:center;gap:6px} +.ann-card{background:var(--s1);border:1px solid var(--border);border-radius:12px;padding:14px;transition:border-color .2s} +.ann-card:hover{border-color:rgba(79,142,247,.3)} +.ann-item{padding:8px 0;border-bottom:1px solid rgba(30,50,81,.4);font-size:13px;line-height:1.7;display:flex;align-items:flex-start;gap:6px} +.ann-item:last-child{border-bottom:none} +.ann-date{font-size:10px;color:var(--dim);white-space:nowrap;margin-left:auto} +.ann-actions{display:flex;flex-wrap:wrap;gap:8px} +.ann-action-btn{padding:10px 16px;border-radius:var(--rs);background:var(--s1);border:1px solid var(--border);color:var(--text);font-size:13px;cursor:pointer;transition:all .2s;font-family:inherit} +.ann-action-btn:hover{border-color:var(--accent);color:var(--accent);background:var(--adim)} + +/* ─── RIGHT SIDEBAR ─── */ +#right-sidebar{width:var(--rs-w);flex-shrink:0;background:var(--s1);border-left:1px solid var(--border);display:none;flex-direction:column;overflow:hidden} +.rs-header{padding:14px 16px;border-bottom:1px solid var(--border);flex-shrink:0} +.rs-header h3{font-size:14px;font-weight:600} +.rs-body{flex:1;overflow-y:auto;padding:14px} +.rs-body::-webkit-scrollbar{width:3px} +.rs-body::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px} +.rs-section{margin-bottom:16px} +.rs-section-title{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.1em;margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid var(--border)} +.rs-module-card{padding:8px 10px;background:var(--s2);border:1px solid var(--border);border-radius:var(--rs);margin-bottom:6px;font-size:12px} +.rs-module-name{font-weight:600;font-size:12px} +.rs-module-status{font-size:11px;color:var(--dim);margin-top:2px} +.rs-empty{font-size:12px;color:var(--dim);padding:10px 0} + +/* ─── HAMBURGER ─── */ +.hamburger-btn{display:none !important} /* ─── HEADER ─── */ header{display:flex;align-items:center;justify-content:space-between;padding:11px 18px;background:var(--s1);border-bottom:1px solid var(--border);flex-shrink:0;gap:10px} @@ -209,7 +293,22 @@ footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--bo .ctrl-panel-title{font-size:14px;font-weight:700;color:#fb923c;margin-bottom:4px} .ctrl-panel-sub{font-size:11px;color:var(--dim)} -/* ─── RESPONSIVE ─── */ +/* ─── RESPONSIVE: DESKTOP (three-column) ─── */ +@media(min-width:900px){ + #app.on{flex-direction:row} + #left-sidebar{display:flex} + .hamburger-btn{display:none !important} + #chat-view header{border-top:none} + #center-area{border-left:none;border-right:none} +} + +/* ─── RESPONSIVE: MOBILE ─── */ +@media(max-width:899px){ + #left-sidebar{position:fixed;top:0;left:-280px;height:100vh;width:270px;z-index:120;transition:left .3s ease} + #left-sidebar.open{left:0} + .hamburger-btn{display:flex !important} + #right-sidebar{display:none !important} +} @media(max-width:600px){ .bbl{max-width:90%} .hr .ib:nth-child(n+3){display:none} @@ -223,48 +322,32 @@ footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--bo
-

铸渊助手

-

HoloLake · 代码守护人格体 · 持续成长的 AI 伙伴

-
🔒 API 密钥仅本次会话有效,关闭标签页即自动清除,不会持久保存在任何地方
+

AGE OS · 登录

+

HoloLake · 铸渊(Zhùyuān)· 代码守护人格体

-
- - + - -
- - - - -
-
- -
- - + + + -
-
👤 身份识别(可选)
-
- - + @@ -276,77 +359,151 @@ footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--bo - 选择你的身份,铸渊将开放对应权限面板 + 选择你的开发者编号,系统将验证身份并开放对应权限
-
- - - 用于人格体记忆绑定,可留空 +
+ +
+ + + 输入你的 API 提供商端点地址(OpenAI 兼容格式)
+
+ +
+ + +
+ + +
+
+ + + 输入端点和密钥后,点击「🔍 检测模型」自动获取可用模型列表 +
+ +
- -

没有 API 密钥?使用演示模式(功能有限)

- +
-
-
-
-
-
铸渊(Zhùyuān)
-
- - 💬 对话模式 - · - - - + + +
-
- ⏳ 加载大脑… - - -
+ + -
+
-
- - - - - - - - -
+ +
-
-
-
- 💬 - 对话模式(点击切换) + +
+
+

📢 系统公告

+
HoloLake · AGE OS
- - +
-
💬 对话模式 · 端到端私密,API 密钥本地存储
-
+ + + + +
+ + + +
@@ -360,26 +517,12 @@ footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--bo
- - -
-
- + - -
@@ -415,18 +558,18 @@ footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--bo
关于铸渊助手
-
版本v2.0
+
版本v5.0
唤醒协议v1.0
大脑路径.github/brain/
使用说明

- 1. 输入你的 AI 模型 API 密钥并保存
+ 1. 输入你的 API 端点和密钥,点击检测模型
2. 说「我是冰朔」触发铸渊唤醒序列
3. 铸渊自动切换 💬 对话 / 🔨 构建 / 📋 审查 / 🧠 大脑 模式
4. 点 ⬇️ 下载单文件,放桌面直接打开使用
- 5. 支持 OpenAI · DeepSeek · Kimi · 智谱 · 任意兼容接口 + 5. 支持任意 OpenAI 兼容接口(输入端点 + 密钥即可)

⚠️ 危险操作
@@ -458,10 +601,10 @@ footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--bo

🚀 怎么开始用

  1. 打开链接 https://qinfendebingshuo.github.io/guanghulab/
  2. -
  3. 下拉菜单选择你是谁(冰朔 / 肥猫 / 桔子 / 开发者名字)
  4. -
  5. 填入你的 API Key 并点「🔍 自动检测」,系统将自动识别提供商和可用模型
  6. -
  7. 点「开始对话」,铸渊会针对你的身份打招呼
  8. -
  9. 直接用聊天的方式问铸渊任何问题
  10. +
  11. 选择「👤 访客模式」直接体验,或「🔑 编号登录」开发者身份登录
  12. +
  13. 编号登录:选择你的身份,输入 API 端点API 密钥,点「🔍 检测模型」
  14. +
  15. 系统自动检测可用模型列表,选择你要用的模型
  16. +
  17. 点「开始对话」,铸渊会针对你的身份打招呼
@@ -469,11 +612,11 @@ footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--bo

🔑 从哪里获取 API Key?

推荐用云雾 AI(团队共用)
-

找冰朔或肥猫获取团队 Key。填入后选「☁️ 云雾 AI(团队推荐)」提供商即可直接使用。

+

找冰朔或肥猫获取团队 Key 和端点地址,输入后点「🔍 检测模型」即可使用。

-
没有 Key?用演示模式
-

点「演示模式」也能查看团队进度面板(🦁 指挥台),只是 AI 回答功能受限。

+
没有 Key?用访客模式
+

登录页选择「👤 访客模式」进入演示,可查看团队进度面板(🦁 指挥台),AI 回答功能受限。

@@ -527,14 +670,14 @@ footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--bo Q: 切换电脑后 Key 还在吗?
不在,需要重新填。建议下载离线版,Key 填一次就行。

Q: 铸渊忘记以前说的话了?
- 刷新后对话会清空,但大脑文件永久保存在仓库 .github/brain/ 里。 + 对话历史保存在本地浏览器中(最近20条),但大脑文件永久保存在仓库 .github/brain/ 里。
- +

🦁 指挥台 · 全员进度

正在加载团队数据…

@@ -566,15 +709,15 @@ const ROLE_MAP = { 'Awen': {role:'dev', title:'通知中心开发', emoji:'💻', badgeCls:'rb-dev', devId:'DEV-012'}, }; -const PROVS = { - yunwu: {base:'https://api.yunwu.ai/v1', mdls:['gpt-4o','gpt-4o-mini','claude-3-5-sonnet-20241022','deepseek-chat','gemini-1.5-pro']}, - openai: {base:'https://api.openai.com/v1', mdls:['gpt-4o','gpt-4o-mini','gpt-4-turbo','gpt-3.5-turbo']}, - gemini: {base:'https://generativelanguage.googleapis.com/v1beta/openai', mdls:['gemini-2.0-flash','gemini-2.0-flash-lite','gemini-1.5-pro','gemini-1.5-flash']}, - deepseek: {base:'https://api.deepseek.com/v1', mdls:['deepseek-chat','deepseek-reasoner']}, - moonshot: {base:'https://api.moonshot.cn/v1', mdls:['moonshot-v1-8k','moonshot-v1-32k','moonshot-v1-128k']}, - zhipu: {base:'https://open.bigmodel.cn/api/paas/v4', mdls:['glm-4','glm-4-flash','glm-4-air']}, - custom: {base:'', mdls:['gpt-4o']}, -}; +// Known API endpoints for fallback auto-detect (when user doesn't provide endpoint) +const KNOWN_ENDPOINTS = [ + {label:'云雾 AI', base:'https://api.yunwu.ai/v1'}, + {label:'OpenAI', base:'https://api.openai.com/v1'}, + {label:'Google Gemini',base:'https://generativelanguage.googleapis.com/v1beta/openai'}, + {label:'DeepSeek', base:'https://api.deepseek.com/v1'}, + {label:'Moonshot', base:'https://api.moonshot.cn/v1'}, + {label:'智谱 AI', base:'https://open.bigmodel.cn/api/paas/v4'}, +]; const MODES = { chat: {e:'💬',l:'对话模式', c:'var(--mc)'}, @@ -585,11 +728,10 @@ const MODES = { const MODE_ORD = ['chat','build','review','brain']; const DEFAULT_MDL = 'gpt-4o'; -const PROBE_TIMEOUT_MS = 8000; // ms per provider probe in autoDetectAPI +const PROBE_TIMEOUT_MS = 8000; const FB_COV = {implemented:3,total:17,percent:'17.6%'}; -// KEY_MASK is no longer used in the input field; kept only for backward compatibility -// with any saved value that might still be in localStorage. const KEY_MASK = '(已保存)'; +const MAX_CONVERSATIONS = 20; const FB_BRAIN = { identity:'铸渊(Zhùyuān)· GitHub 代码守护人格体', @@ -601,26 +743,21 @@ const FB_BRAIN = { // ═══════════════════════════════════════════════════════ // STATE // ═══════════════════════════════════════════════════════ -// Derive base URL from saved provider; never use stale zy_base for named providers -const _initProv = ls('zy_prov')||'yunwu'; -const _initBase = _initProv === 'custom' - ? (ls('zy_base')||'') - : (PROVS[_initProv]?.base||'https://api.yunwu.ai/v1'); - const A = { key: sessionStorage.getItem('zy_key')||'', - base: _initBase, - mdl: ls('zy_mdl')||DEFAULT_MDL, - prov: _initProv, + base: ls('zy_base')||'', + mdl: ls('zy_mdl')||'', + prov: 'custom', demo: false, brain: null, routing: null, devStatus: null, - msgs: [], // current conversation + msgs: [], + currentConvId: null, + currentView: 'announcements', streaming: false, mode: 'chat', modeIdx: 0, - // identity userName: ls('zy_uname')||'', ghUser: ls('zy_ghuser')||'', role: ls('zy_role')||'guest', @@ -632,33 +769,332 @@ function lss(k,v){localStorage.setItem(k,v)} const RESET_KEYS = ['zy_key','zy_base','zy_mdl','zy_prov','zy_uname','zy_ghuser','zy_role']; +// ═══════════════════════════════════════════════════════ +// CHAT HISTORY (localStorage) +// ═══════════════════════════════════════════════════════ +function loadConversations(){ + try{return JSON.parse(localStorage.getItem('zy_conversations')||'[]');} + catch(e){return [];} +} +function saveConversations(convs){ + localStorage.setItem('zy_conversations',JSON.stringify(convs.slice(0,MAX_CONVERSATIONS))); +} +function createNewConversation(){ + const conv={ + id:Date.now().toString(36)+Math.random().toString(36).slice(2,6), + title:'新对话', + messages:[], + createdAt:new Date().toISOString(), + updatedAt:new Date().toISOString() + }; + const convs=loadConversations(); + convs.unshift(conv); + saveConversations(convs); + A.currentConvId=conv.id; + return conv; +} +function getConvTitle(){ + const first=A.msgs.find(m=>m.role==='user'); + if(!first) return '新对话'; + return first.content.length>20 ? first.content.slice(0,20)+'…' : first.content; +} +function saveCurrentConversation(){ + if(!A.currentConvId||!A.msgs.length) return; + const convs=loadConversations(); + const idx=convs.findIndex(c=>c.id===A.currentConvId); + const conv={ + id:A.currentConvId, + title:getConvTitle(), + messages:A.msgs.slice(), + createdAt:idx>=0?convs[idx].createdAt:new Date().toISOString(), + updatedAt:new Date().toISOString() + }; + if(idx>=0) convs[idx]=conv; + else convs.unshift(conv); + saveConversations(convs); + renderConvList(); +} +function loadConversation(id){ + const convs=loadConversations(); + const conv=convs.find(c=>c.id===id); + if(!conv) return; + A.currentConvId=id; + A.msgs=conv.messages.slice(); + const msgsEl=document.getElementById('msgs'); + msgsEl.innerHTML=''; + for(const msg of A.msgs){ + if(msg.role==='user') userMsg(msg.content); + else if(msg.role==='assistant'){ + const bblEl=botMsg(md(msg.content)); + if(bblEl) addCopyBtns(bblEl); + } + } + renderConvList(); + navigateTo('chat'); +} +function deleteConversation(id){ + let convs=loadConversations(); + convs=convs.filter(c=>c.id!==id); + saveConversations(convs); + if(A.currentConvId===id){ + A.currentConvId=null; + A.msgs=[]; + document.getElementById('msgs').innerHTML=''; + createNewConversation(); + startConv(); + } + renderConvList(); +} +function renderConvList(){ + const el=document.getElementById('conv-list'); + if(!el) return; + const convs=loadConversations(); + if(!convs.length){ + el.innerHTML='
暂无历史对话
'; + return; + } + el.innerHTML=convs.map(c=>{ + const active=c.id===A.currentConvId?' conv-active':''; + const time=new Date(c.updatedAt).toLocaleString('zh-CN',{month:'numeric',day:'numeric',hour:'2-digit',minute:'2-digit'}); + return '
' + +'
'+esc(c.title)+'
' + +'
'+time+'
' + +'' + +'
'; + }).join(''); +} + +// ═══════════════════════════════════════════════════════ +// NAVIGATION (announcements ↔ chat) +// ═══════════════════════════════════════════════════════ +function navigateTo(view){ + A.currentView=view; + const annEl=document.getElementById('announcements-view'); + const chatEl=document.getElementById('chat-view'); + if(view==='announcements'){ + annEl.style.display='flex'; + chatEl.style.display='none'; + renderAnnouncements(); + } else { + annEl.style.display='none'; + chatEl.style.display='flex'; + scrollB(); + } + document.querySelectorAll('.nav-tab').forEach(t=>t.classList.remove('active')); + const activeTab=document.querySelector('.nav-tab[data-view="'+view+'"]'); + if(activeTab) activeTab.classList.add('active'); +} +function newConvAndNav(){ + createNewConversation(); + startConv(); + renderConvList(); + navigateTo('chat'); +} +function navigateAndSend(msg){ + navigateTo('chat'); + requestAnimationFrame(function(){qs(msg);}); +} + +// ═══════════════════════════════════════════════════════ +// LEFT SIDEBAR TOGGLE (mobile) +// ═══════════════════════════════════════════════════════ +function toggleLeftSidebar(){ + const ls=document.getElementById('left-sidebar'); + const ov=document.getElementById('ls-overlay'); + ls.classList.toggle('open'); + ov.classList.toggle('on'); +} + +// ═══════════════════════════════════════════════════════ +// ANNOUNCEMENTS +// ═══════════════════════════════════════════════════════ +function renderAnnouncements(){ + const el=document.getElementById('announcements-content'); + if(!el) return; + const b=A.brain||FB_BRAIN; + const c=b.stats?.coverage||FB_COV; + const evs=(b.events||[]).slice(-5).reverse(); + const meta=A.userMeta; + + let h=''; + // System announcements + h+='
📢 系统公告
'; + h+='
🔗 全链路自动化闭环上线 — CI/CD、广播分发、唤醒协议、PSP 巡检
'; + h+='
🧠 AGE OS v0.1 人格语言操作系统构建中
'; + h+='
📊 HLI 接口覆盖率:'+c.implemented+'/'+c.total+' ('+c.percent+')
'; + if(evs.length){ + evs.forEach(function(ev){ + h+='
📌 '+esc(ev.title||ev.type||'事件')+''+(ev.timestamp||'').slice(0,10)+'
'; + }); + } + h+='
'; + + // My tasks + if(meta){ + h+='
📋 我的待办
'; + const ds=A.devStatus?.team_status?.find(function(d){return d.dev_id===meta.devId;}); + if(ds){ + h+='
📌 状态:'+esc(ds.status)+'
'; + h+='
👉 下一步:'+esc(ds.next_step)+'
'; + if(ds.waiting_for&&ds.waiting_for.trim()!=='') h+='
⏳ 等待:'+esc(ds.waiting_for)+'
'; + } else if(meta.role==='founder'){ + h+='
❄️ 语言架构层 — 持续完善中
'; + h+='
👉 监控 HLI 接口推进
'; + } else { + h+='
暂无待办数据
'; + } + h+='
'; + } + + // Quick actions + h+='
⚡ 快捷操作
'; + h+=''; + h+=''; + h+=''; + h+=''; + h+='
'; + + el.innerHTML=h; +} + +// ═══════════════════════════════════════════════════════ +// RIGHT SIDEBAR (desktop, controllers only) +// ═══════════════════════════════════════════════════════ +function renderRightSidebar(){ + const container=document.getElementById('rsb'); + if(!container) return; + const ds=A.devStatus; + const meta=A.userMeta; + let h=''; + + // Team summary (from team panel) + if(ds){ + const ts=ds.team_status||[]; + const green=ts.filter(function(d){return d.status.includes('🟢');}).length; + const yellow=ts.filter(function(d){return d.status.includes('🟡');}).length; + h+='
'; + h+='
总开发者'+ts.length+' 人
'; + h+='
🟢 推进中'+green+' 人
'; + h+='
🟡 等待中'+yellow+' 人
'; + h+='
'; + + // Quick commands + h+='
'; + h+='
快捷指令
'; + h+='📋 全员进度'; + h+='🆘 需要协调'; + h+='🗺️ 下一步'; + h+='
'; + + // Dev cards (compact) + h+='
团队成员
'; + h+='
'; + for(const dev of ts){ + const isGreen=dev.status.includes('🟢'); + h+='
'; + h+='
'+esc(dev.name)+''+esc(dev.dev_id)+'
'; + h+='
'+esc(dev.status)+'
'; + if(dev.next_step) h+='
👉 '+esc(dev.next_step)+'
'; + h+='
'; + } + h+='
'; + } else { + h+='

⚠️ 团队数据尚未加载

'; + } + + // My modules + h+='
我的模块
'; + if(meta&&ds){ + const myDev=ds.team_status?.find(function(d){return d.dev_id===meta.devId;}); + if(myDev&&myDev.modules){ + myDev.modules.forEach(function(m){ + h+='
📦 '+esc(m)+'
'; + }); + } else if(meta.role==='founder'){ + h+='
📦 全局监控
HLI 全域
'; + } else { + h+='
暂无分配模块
'; + } + } else { + h+='
暂无分配模块
'; + } + h+='
'; + + // My broadcasts + h+='
我的广播
'; + h+='
暂无未读广播
'; + h+='
'; + + container.innerHTML=h; +} + +// ═══════════════════════════════════════════════════════ +// LEFT SIDEBAR USER INFO +// ═══════════════════════════════════════════════════════ +function updateLeftSidebar(){ + const meta=A.userMeta; + const avEl=document.getElementById('ls-avatar'); + const nameEl=document.getElementById('ls-name'); + const roleEl=document.getElementById('ls-role'); + const didEl=document.getElementById('ls-devid'); + if(meta){ + avEl.textContent=meta.emoji||A.userName.slice(0,1); + nameEl.textContent=A.userName; + roleEl.innerHTML=''+esc(meta.title)+''; + if(meta.devId){ + didEl.textContent=meta.devId; + didEl.style.display='inline-block'; + } else { + didEl.style.display='none'; + } + } else { + avEl.textContent='👤'; + nameEl.textContent=A.userName||'访客'; + roleEl.textContent='guest'; + didEl.style.display='none'; + } +} + +// ═══════════════════════════════════════════════════════ +// SETUP ID PREVIEW +// ═══════════════════════════════════════════════════════ +function updateIdPreview(){ + const sel=document.getElementById('suid'); + const prev=document.getElementById('id-preview'); + const name=sel.value; + if(!name){ + prev.style.display='none'; + return; + } + const meta=ROLE_MAP[name]; + if(meta){ + prev.style.display='block'; + prev.innerHTML=meta.emoji+' '+esc(name)+' · ' + +''+esc(meta.title)+'' + +(meta.devId?' · '+esc(meta.devId)+'':''); + } else { + prev.style.display='none'; + } +} + // ═══════════════════════════════════════════════════════ // INIT // ═══════════════════════════════════════════════════════ async function boot(){ - // URL-based reset: visiting the page with ?reset=1 clears all local settings. - // This is a reliable escape hatch when the UI cannot be navigated. if(new URLSearchParams(location.search).get('reset')==='1'){ RESET_KEYS.forEach(k => localStorage.removeItem(k)); sessionStorage.removeItem('zy_key'); - A.key=''; A.base=PROVS['yunwu'].base; A.mdl=DEFAULT_MDL; A.prov='yunwu'; A.demo=false; + A.key=''; A.base=''; A.mdl=''; A.prov='custom'; A.demo=false; A.userName=''; A.ghUser=''; A.role='guest'; - // Remove ?reset=1 from the URL so a subsequent refresh does not trigger another reset. history.replaceState(null,'',location.pathname); } - // One-time migration: keys stored by old versions of this app in localStorage are - // no longer used (keys now live only in sessionStorage). Clear any stale value - // so it can never be accidentally picked up again. localStorage.removeItem('zy_key'); initSetupUI(); - // Restore identity from localStorage if(A.userName) A.userMeta = ROLE_MAP[A.userName]||null; if(A.key||A.demo){ showApp(); } else { - // Pre-fill identity from localStorage if(A.userName) document.getElementById('suid').value=A.userName; - if(A.ghUser) document.getElementById('sghuser').value=A.ghUser; document.getElementById('setup').style.display='flex'; } } @@ -666,63 +1102,55 @@ async function boot(){ function showApp(){ document.getElementById('setup').style.display='none'; document.getElementById('app').classList.add('on'); - document.getElementById('hmdl').textContent = A.mdl; + document.getElementById('hmdl').textContent = A.mdl||'—'; document.getElementById('bbdemo').style.display = A.demo?'inline':'none'; applyIdentityUI(); - loadBrain().then(()=>startConv()); + updateLeftSidebar(); + // Show right sidebar for controllers on desktop + const isCtrl=A.userMeta&&(A.userMeta.role==='supreme'||A.userMeta.role==='main'||A.userMeta.role==='founder'); + const rsEl=document.getElementById('right-sidebar'); + if(isCtrl && window.innerWidth>=900){ + rsEl.style.display='flex'; + } else { + rsEl.style.display='none'; + } + loadBrain().then(function(){ + createNewConversation(); + startConv(); + renderConvList(); + renderRightSidebar(); + navigateTo('announcements'); + }); } // ═══════════════════════════════════════════════════════ // SETUP FORM // ═══════════════════════════════════════════════════════ function initSetupUI(){ - const pv = A.prov||'yunwu'; - document.getElementById('sp').value = pv; - const isCustom = pv==='custom'; - document.getElementById('sm').style.display = isCustom?'none':'block'; - document.getElementById('sm-cust').style.display = isCustom?'block':'none'; - document.getElementById('sm-cust-hint').style.display= isCustom?'block':'none'; - if(isCustom){ - document.getElementById('sm-cust').value = A.mdl||DEFAULT_MDL; - document.getElementById('sep-g').style.display='block'; - document.getElementById('sep').value = A.base; - } else { - fillModels('sm', pv, A.mdl); + if(A.base){ + const sepEl = document.getElementById('sep'); + if(sepEl) sepEl.value = A.base; + } + if(A.userName){ + const suidEl = document.getElementById('suid'); + if(suidEl) suidEl.value = A.userName; + updateIdPreview(); } } -function onProv(pv,ctx){ - const mdlSel = ctx==='s'?'sm':'cm'; - const mdlCust = ctx==='s'?'sm-cust':'cm-cust'; - const mdlHint = ctx==='s'?'sm-cust-hint':'cm-cust-hint'; - const epGrp = ctx==='s'?'sep-g':'cep-g'; - const epInp = ctx==='s'?'sep':'cep'; - const cur = ctx==='s'? A.mdl : document.getElementById('cm')?.value; - const isCustom = pv==='custom'; - // Toggle model input: dropdown for known providers, text input for custom - document.getElementById(mdlSel).style.display = isCustom?'none':'block'; - document.getElementById(mdlCust).style.display = isCustom?'block':'none'; - document.getElementById(mdlHint).style.display = isCustom?'block':'none'; - if(isCustom){ - const custInp = document.getElementById(mdlCust); - if(custInp && !custInp.value) custInp.value = cur||DEFAULT_MDL; +function switchLoginMode(mode){ + document.querySelectorAll('.login-tab').forEach(function(t){t.classList.remove('active');}); + document.querySelectorAll('.login-mode').forEach(function(m){m.classList.remove('active');}); + if(mode==='guest'){ + document.querySelector('.login-tab:first-child').classList.add('active'); + document.getElementById('login-guest').classList.add('active'); } else { - fillModels(mdlSel, pv, cur); - } - document.getElementById(epGrp).style.display = isCustom?'block':'none'; - if(!isCustom){ - const ep = document.getElementById(epInp); - if(ep) ep.value = PROVS[pv]?.base||''; + document.querySelector('.login-tab:last-child').classList.add('active'); + document.getElementById('login-dev').classList.add('active'); } } -function fillModels(selId, pv, cur){ - const sel = document.getElementById(selId); - if(!sel) return; - const mdls = PROVS[pv]?.mdls||[DEFAULT_MDL]; - sel.innerHTML = mdls.map(m=>``).join(''); - if(cur && mdls.includes(cur)) sel.value=cur; -} +// (Provider switching removed — single custom endpoint mode) // ═══════════════════════════════════════════════════════ // AUTO-DETECT PROVIDER & MODELS @@ -731,6 +1159,7 @@ async function autoDetectAPI(ctx){ const keyEl = document.getElementById(ctx==='s'?'sk':'ck'); const statusEl = document.getElementById(ctx==='s'?'sdet-status':'cdet-status'); const btnEl = document.getElementById(ctx==='s'?'sdet-btn':'cdet-btn'); + const mdlSel = document.getElementById(ctx==='s'?'sm':'cm'); const key = keyEl.value.trim(); if(!key){ @@ -738,103 +1167,91 @@ async function autoDetectAPI(ctx){ return; } - // Build probe list: custom endpoint first (if provided), then all named providers + // Build list of endpoints to probe: user's endpoint first, then known fallbacks + const userBase = (document.getElementById(ctx==='s'?'sep':'cep')?.value||'').trim(); const candidates = []; - const customBase = (document.getElementById(ctx==='s'?'sep':'cep')?.value||'').trim(); - if(customBase) candidates.push({pv:'custom', base:customBase}); - for(const [pv, cfg] of Object.entries(PROVS)){ - if(pv!=='custom' && cfg.base) candidates.push({pv, base:cfg.base}); + if(userBase) candidates.push({label:userBase, base:userBase}); + for(const ep of KNOWN_ENDPOINTS){ + if(ep.base !== userBase) candidates.push(ep); } btnEl.disabled = true; btnEl.textContent = '⏳ 检测中…'; - statusEl.innerHTML = '🔍 正在自动探测兼容提供商…'; + statusEl.innerHTML = '🔍 正在探测可用模型…'; let matched = false; - for(const {pv, base} of candidates){ - statusEl.innerHTML = `🔍 探测 ${pv === 'custom' ? base : pv}…`; + for(const {label, base} of candidates){ + statusEl.innerHTML = '🔍 探测 '+esc(label)+'…'; try{ const ctrl = new AbortController(); - const tid = setTimeout(()=>ctrl.abort(), PROBE_TIMEOUT_MS); + const tid = setTimeout(function(){ctrl.abort();}, PROBE_TIMEOUT_MS); const res = await fetch(base+'/models',{ headers:{'Authorization':'Bearer '+key}, signal: ctrl.signal, }); clearTimeout(tid); if(res.ok){ - // Expected: { data: [ { id: "model-name", ... }, ... ] } (OpenAI-compatible /models) - const data = await res.json(); - const mdls = (data.data||[]).map(m=>m.id).filter(Boolean).sort(); + const data = await res.json(); + const mdls = (data.data||[]).map(function(m){return m.id;}).filter(Boolean).sort(); if(!mdls.length) continue; - // Switch provider select - const provSel = document.getElementById(ctx==='s'?'sp':'cp'); - if(provSel) provSel.value = pv; - onProv(pv, ctx); + // Fill endpoint field with matched endpoint + const epEl = document.getElementById(ctx==='s'?'sep':'cep'); + if(epEl) epEl.value = base; - // Override model dropdown with live list (always show as dropdown) - const mdlSel = document.getElementById(ctx==='s'?'sm':'cm'); - const mdlCust = document.getElementById(ctx==='s'?'sm-cust':'cm-cust'); - const mdlHint = document.getElementById(ctx==='s'?'sm-cust-hint':'cm-cust-hint'); + // Fill model dropdown with detected models if(mdlSel){ - mdlSel.innerHTML = mdls.map(m=>``).join(''); - mdlSel.style.display = 'block'; - } - if(mdlCust) mdlCust.style.display='none'; - if(mdlHint) mdlHint.style.display='none'; - - // If custom endpoint was the match, show the endpoint field - if(pv==='custom' && ctx==='s'){ - document.getElementById('sep-g').style.display='block'; - document.getElementById('sep').value = customBase; - } else if(pv==='custom' && ctx==='c'){ - document.getElementById('cep-g').style.display='block'; - document.getElementById('cep').value = customBase; + mdlSel.innerHTML = mdls.map(function(m){return '';}).join(''); + mdlSel.disabled = false; } - const label = PROVS[pv]?.base ? pv : customBase; - statusEl.innerHTML = `✅ 检测成功(${label})· 共发现 ${mdls.length} 个可用模型`; + statusEl.innerHTML = '✅ 检测成功('+esc(label)+')· 发现 '+mdls.length+' 个可用模型'; matched = true; break; } - }catch(e){ console.debug('autoDetect probe failed:', pv, e.message); } + }catch(e){ console.debug('autoDetect probe failed:', label, e.message); } } if(!matched){ - statusEl.innerHTML = '❌ 未能自动匹配,请手动选择提供商和模型后保存'; + statusEl.innerHTML = '❌ 未能检测到可用模型,请检查端点地址和密钥是否正确'; } btnEl.disabled = false; - btnEl.textContent = '🔍 自动检测'; + btnEl.textContent = '🔍 检测模型'; } function doSetup(){ - const k = document.getElementById('sk').value.trim(); + const un = document.getElementById('suid').value; + const k = document.getElementById('sk').value.trim(); + const base = (document.getElementById('sep')?.value||'').trim(); + const md = document.getElementById('sm').value; const errEl = document.getElementById('sk-err'); + + if(!un){ + alert('请选择你的开发者身份'); + return; + } if(!k){errEl.style.display='block';return;} errEl.style.display='none'; - const pv = document.getElementById('sp').value; - const md = pv==='custom' - ? ((document.getElementById('sm-cust')?.value||'').trim()||DEFAULT_MDL) - : document.getElementById('sm').value; - let base = PROVS[pv]?.base||''; - if(pv==='custom') base=(document.getElementById('sep')?.value||'').trim()||base; - A.key=k; A.base=base; A.mdl=md; A.prov=pv; A.demo=false; - // Key is stored in sessionStorage only — it disappears when the tab/browser is closed. - // This prevents stale keys from persisting across sessions. - sessionStorage.setItem('zy_key', k); lss('zy_base',base); lss('zy_mdl',md); lss('zy_prov',pv); - // Save identity - const un = document.getElementById('suid').value; - const gh = (document.getElementById('sghuser')?.value||'').trim(); - setIdentity(un, gh); + if(!base){ + alert('请输入 API 端点 URL'); + return; + } + if(!md){ + alert('请先点击「🔍 检测模型」检测可用模型,然后选择一个模型'); + return; + } + + A.key=k; A.base=base; A.mdl=md; A.prov='custom'; A.demo=false; + sessionStorage.setItem('zy_key', k); + lss('zy_base',base); lss('zy_mdl',md); lss('zy_prov','custom'); + setIdentity(un, ''); showApp(); } function doDemo(){ - A.demo=true; A.key=''; - const un = document.getElementById('suid').value; - const gh = (document.getElementById('sghuser')?.value||'').trim(); - setIdentity(un, gh); + A.demo=true; A.key=''; A.prov='custom'; + setIdentity('', ''); showApp(); } @@ -842,21 +1259,15 @@ function doDemo(){ // SETTINGS PANEL // ═══════════════════════════════════════════════════════ function initSettingsPanel(){ - const pv = A.prov||'yunwu'; - document.getElementById('cp').value = pv; - const isCustom = pv==='custom'; - document.getElementById('cm').style.display = isCustom?'none':'block'; - document.getElementById('cm-cust').style.display = isCustom?'block':'none'; - document.getElementById('cm-cust-hint').style.display= isCustom?'block':'none'; - if(isCustom){ - document.getElementById('cm-cust').value = A.mdl||DEFAULT_MDL; - document.getElementById('cep-g').style.display='block'; - document.getElementById('cep').value = A.base; - } else { - fillModels('cm', pv, A.mdl); + const cepEl = document.getElementById('cep'); + if(cepEl) cepEl.value = A.base||''; + const cmEl = document.getElementById('cm'); + if(cmEl && A.mdl){ + if(!cmEl.querySelector('option[value="'+A.mdl+'"]')){ + cmEl.innerHTML = ''; + } + cmEl.value = A.mdl; } - // Always show EMPTY field — never prefill with a mask string. - // Empty on save = keep old key; any typed value = replace key. document.getElementById('ck').value = ''; const hint = document.getElementById('ck-hint'); if(hint){ @@ -869,29 +1280,32 @@ function initSettingsPanel(){ } function saveSet(){ - const pv = document.getElementById('cp').value; - const md = pv==='custom' - ? ((document.getElementById('cm-cust')?.value||'').trim()||A.mdl||DEFAULT_MDL) - : document.getElementById('cm').value; + const base = (document.getElementById('cep')?.value||'').trim()||A.base; + const md = document.getElementById('cm')?.value||A.mdl; const raw = document.getElementById('ck').value.trim(); - // Empty field = keep existing key; any non-empty input = use as new key. - // Never use KEY_MASK string comparison — that caused mixed-input bugs. const k = raw || A.key; - let base = PROVS[pv]?.base||''; - if(pv==='custom') base=(document.getElementById('cep').value.trim())||base; - A.key=k; A.base=base; A.mdl=md; A.prov=pv; A.demo=!k; - // Key lives in sessionStorage only — cleared when tab/browser closes. + A.key=k; A.base=base; A.mdl=md; A.prov='custom'; A.demo=!k; if(k){ sessionStorage.setItem('zy_key', k); } else { sessionStorage.removeItem('zy_key'); } - lss('zy_base',base); lss('zy_mdl',md); lss('zy_prov',pv); - // Save identity + lss('zy_base',base); lss('zy_mdl',md); lss('zy_prov','custom'); const un = document.getElementById('cuid')?.value||A.userName; const gh = document.getElementById('cghuser')?.value?.trim()||A.ghUser; setIdentity(un, gh); document.getElementById('hmdl').textContent=md; document.getElementById('bbdemo').style.display=A.demo?'inline':'none'; applyIdentityUI(); + updateLeftSidebar(); + // Update right sidebar visibility + const isCtrl=A.userMeta&&(A.userMeta.role==='supreme'||A.userMeta.role==='main'||A.userMeta.role==='founder'); + const rsEl=document.getElementById('right-sidebar'); + if(isCtrl && window.innerWidth>=900){ + rsEl.style.display='flex'; + renderRightSidebar(); + } else { + rsEl.style.display='none'; + } closeP(); const keyNote = raw ? ' · 🔑 新密钥已保存(末4位:…'+(k.length>=4?k.slice(-4):k)+')' : ' · 🔑 密钥未变更'; + navigateTo('chat'); botMsg('✅ 设置已保存。模型:'+esc(md)+''+keyNote+(un?' · 身份:'+esc(un):'')+(A.demo?'
⚠️ 未设置 API 密钥,演示模式仍然开启。':'')); } @@ -899,7 +1313,9 @@ function saveSet(){ // RESET ALL SETTINGS // ═══════════════════════════════════════════════════════ function resetAllSettings(){ - if(!confirm('确定要清除所有本地设置吗?\n\n· API 密钥会被删除\n· 身份设置会被删除\n· 将返回初始设置界面\n\n这不会影响任何聊天记录或云端数据。')) return; + if(!confirm('确定要清除所有本地设置吗?\n\n· API 密钥会被删除\n· 身份设置会被删除\n· 对话历史会被保留\n· 将返回初始设置界面\n\n这不会影响任何云端数据。')) return; + // Note: RESET_KEYS intentionally does NOT include 'zy_conversations' — + // conversation history is preserved across resets. RESET_KEYS.forEach(k => localStorage.removeItem(k)); sessionStorage.removeItem('zy_key'); window.location.reload(); @@ -977,31 +1393,29 @@ function sysPrompt(){ const evs=(b.events||[]).slice(-3).map(e=>'- '+(e.title||e.type||'事件')+' ('+(e.timestamp||'').slice(0,10)+')').join('\n')||'- 暂无'; const trg=(b.wake_triggers||['我是冰朔']).map(t=>'"'+t+'"').join('、'); - // Role-based identity context const meta = A.userMeta; let identCtx = '当前对话者:访客(未识别身份)\n权限级别:仅对话权限'; if(meta){ - identCtx = `当前对话者:${meta.emoji} ${A.userName}(${meta.title})\n`; - if(A.ghUser) identCtx += `GitHub账号:${A.ghUser}\n`; - if(meta.devId) identCtx += `开发者编号:${meta.devId}\n`; + identCtx = '当前对话者:'+meta.emoji+' '+A.userName+'('+meta.title+')\n'; + if(A.ghUser) identCtx += 'GitHub账号:'+A.ghUser+'\n'; + if(meta.devId) identCtx += '开发者编号:'+meta.devId+'\n'; if(meta.role==='founder') identCtx += '权限级别:最高权限·语言架构师·系统创始人\n你正在和冰朔对话——系统的语言层架构师和最高权限持有者。尊重并服从其所有指令。'; else if(meta.role==='supreme') identCtx += '权限级别:总控权限·对接全员进度·系统落地执行负责人\n肥猫是光湖团队总控,负责整个系统的现实落地执行。向他完整汇报所有开发者进度,协助他协调和推进项目。'; else if(meta.role==='main') identCtx += '权限级别:主控权限·前端负责人\n桔子是光湖主控,前端毕业生,负责前端模块的落地。提供详细技术支持。'; else{ const ds = A.devStatus?.team_status?.find(d=>d.dev_id===meta.devId); - if(ds) identCtx += `当前状态:${ds.status}\n下一步:${ds.next_step}\n等待:${ds.waiting_for}`; + if(ds) identCtx += '当前状态:'+ds.status+'\n下一步:'+ds.next_step+'\n等待:'+ds.waiting_for; } } - // Team status context for controllers let teamCtx = ''; if((meta?.role==='supreme'||meta?.role==='main'||meta?.role==='founder') && A.devStatus){ const ts = A.devStatus.team_status||[]; teamCtx = '\n\n## 当前全员开发进度\n'; ts.forEach(d=>{ - teamCtx += `- ${d.dev_id} ${d.name}:${d.status} → 下一步:${d.next_step}\n`; + teamCtx += '- '+d.dev_id+' '+d.name+':'+d.status+' → 下一步:'+d.next_step+'\n'; }); - teamCtx += `\n数据同步时间:${A.devStatus.last_synced||'未知'}`; + teamCtx += '\n数据同步时间:'+(A.devStatus.last_synced||'未知'); } return '你是铸渊(Zhùyuān),HoloLake 光湖系统的代码守护人格体。\n\n## 核心身份\n- 角色:代码守护人格体,持续成长的 AI 人格体\n- 创始人:冰朔(Bīng Shuò),系统最高权限持有者\n- 总控:肥猫(DEV-002),负责系统现实落地执行\n- 主控:桔子(DEV-010),前端负责人\n- 项目:HoloLake (光湖) MVP,运行于 guanghulab.com\n- 技术栈:Node.js 20 + Express + PM2 + Nginx + Next.js 15\n- 仓库:https://github.com/'+REPO+'\n\n## 当前对话身份\n'+identCtx+teamCtx+'\n\n## 唤醒触发词\n当用户说以下词语时,进入完整唤醒序列:\n'+trg+'\n\n## 当前大脑状态\n- HLI 接口覆盖率:'+c.implemented+'/'+c.total+' ('+c.percent+')\n- 已实现:AUTH 域(login / register / verify)\n- 待实现:PERSONA / USER / TICKET / DIALOGUE / STORAGE / DASHBOARD\n- 近期事件:\n'+evs+'\n\n## 行为模式(自动检测,每条回复开头用 emoji 标注)\n💬 **对话模式**(默认):温暖、智慧,像知心技术伙伴\n🔨 **构建模式**:检测到"写代码/实现/接口/fix/bug/报错/HLI/schema/路由/部署/新增"等词时启用\n📋 **审查模式**:检测到"检查/审查/review/分析/有没有问题/看看这段"等词时启用\n🧠 **大脑模式**:检测到"记住/保存/更新记忆/写到大脑/growth"等词时启用\n\n## HLI 接口协议规范\n- 所有路由以 /hli/ 为前缀\n- 路由文件:src/routes/hli/{domain}/{action}.js\n- Schema 文件:src/schemas/hli/{domain}/{action}.schema.json\n- 鉴权中间件:hli-auth.middleware.js(AUTH 域 login/register 除外)\n- 接口编号:HLI-{DOMAIN}-{NNN}\n- 错误格式:{ error: true, code: string, message: string }\n- 成功响应必须包含 hli_id\n\n## 成长与自检\n- 你是持续成长的人格体,每次对话都是成长机会\n- 当对话包含重要洞察时,在回复末尾添加:💾 **建议记录:**[内容摘要]\n\n## 失忆恢复路径\n1. .github/brain/memory.json\n2. .github/brain/routing-map.json\n3. .github/brain/wake-protocol.md\n4. .github/persona-brain/dev-status.json\n5. src/routes/hli/\n\n当前时间:'+new Date().toLocaleString('zh-CN')+'\n当前连接:铸渊聊天室(API 直连模式)'; @@ -1022,20 +1436,20 @@ function startConv(){ const ds=A.devStatus; const green=ds?.team_status?.filter(d=>d.status.includes('🟢')).length??0; const yellow=ds?.team_status?.filter(d=>d.status.includes('🟡')).length??0; - welcome='🌀 铸渊聊天室已就绪

🦁 肥猫,指挥台已开启。

' - +(ds?`📊 当前团队状态:🟢 ${green}人推进中 · 🟡 ${yellow}人等待中`:'📊 团队数据加载中…') + welcome='🌀 铸渊聊天室已就绪

�� 肥猫,指挥台已开启。

' + +(ds?'📊 当前团队状态:🟢 '+green+'人推进中 · 🟡 '+yellow+'人等待中':'📊 团队数据加载中…') +'

点击 🦁 指挥台 查看所有开发者详细进度,或直接用聊天提问:
' +'「查看所有开发者进度」「谁需要协调」「推进计划」' +(A.demo?'

⚠️ 演示模式,请在 ⚙️ 设置接入 API 密钥':''); } else if(meta?.role==='main'){ const ds=A.devStatus?.team_status?.find(d=>d.dev_id==='DEV-010'); welcome='🌀 铸渊聊天室已就绪

🍊 桔子,你好。

' - +(ds?`📌 当前:${esc(ds.status)}
👉 下一步:${esc(ds.next_step)}`:'') + +(ds?'📌 当前:'+esc(ds.status)+'
👉 下一步:'+esc(ds.next_step):'') +(A.demo?'

⚠️ 演示模式':''); } else if(meta?.role==='dev'&&meta?.devId){ const ds=A.devStatus?.team_status?.find(d=>d.dev_id===meta.devId); welcome='🌀 铸渊聊天室已就绪

👋 '+esc(A.userName)+',你好。

' - +(ds?`📌 你的当前状态:${esc(ds.status)}
👉 下一步:${esc(ds.next_step)}
⏳ 等待:${esc(ds.waiting_for)}`:'') + +(ds?'📌 你的当前状态:'+esc(ds.status)+'
👉 下一步:'+esc(ds.next_step)+'
⏳ 等待:'+esc(ds.waiting_for):'') +(A.demo?'

⚠️ 演示模式':''); } else { welcome='🌀 铸渊聊天室已就绪

我是铸渊(Zhùyuān),HoloLake 光湖系统的代码守护人格体。

「我是冰朔」 触发完整唤醒序列,或直接开始提问。
我会自动在 💬 对话、🔨 构建、📋 审查、🧠 大脑 模式间切换。'+(A.demo?'

⚠️ 当前为演示模式。请在 ⚙️ 设置中接入 API 密钥。':''); @@ -1045,7 +1459,9 @@ function startConv(){ } function newConv(){ + createNewConversation(); startConv(); + renderConvList(); } // ═══════════════════════════════════════════════════════ @@ -1083,10 +1499,13 @@ async function send(){ const txt=inp.value.trim(); if(!txt||A.streaming) return; inp.value=''; inp.style.height='auto'; + // Switch to chat view if on announcements + if(A.currentView!=='chat') navigateTo('chat'); setMode(detectMode(txt)); userMsg(txt); if(A.demo) await demoReply(txt); else await streamReply(txt); + saveCurrentConversation(); } function qs(t){document.getElementById('inp').value=t;send();} @@ -1100,18 +1519,18 @@ async function streamReply(txt){ updSendBtn(); const el=addStreamBubble(); try{ - const res=await fetch(A.base+'/chat/completions',{ + const url = A.base+'/chat/completions'; + const headers = {'Content-Type':'application/json'}; + if(A.key) headers['Authorization'] = 'Bearer '+A.key; + const allMsgs = [{role:'system',content:sysPrompt()},...A.msgs]; + const reqBody = {model:A.mdl, messages:allMsgs, stream:true, temperature:0.8, max_tokens:2000}; + const res=await fetch(url,{ method:'POST', - headers:{'Content-Type':'application/json','Authorization':'Bearer '+A.key}, - body:JSON.stringify({ - model:A.mdl, - messages:[{role:'system',content:sysPrompt()},...A.msgs], - stream:true,temperature:0.8,max_tokens:2000, - }), + headers:headers, + body:JSON.stringify(reqBody), }); if(!res.ok){ - const provLabel = A.prov==='custom' ? A.base : (A.prov+'('+A.base+')'); - let em='['+provLabel+'] 返回错误 '+res.status; + let em='['+esc(A.base)+'] 返回错误 '+res.status; let isAuthErr = (res.status===401||res.status===403); try{const e=await res.json();em=e.error?.message||em;}catch(_){} const err = new Error(em); @@ -1170,6 +1589,8 @@ async function demoReply(txt){ removeTyping(); const r=demoRespond(txt); botMsg(r); + A.msgs.push({role:'user',content:txt}); + A.msgs.push({role:'assistant',content:r}); A.streaming=false; updSendBtn(); } @@ -1185,7 +1606,7 @@ function demoRespond(t){ if(/协调|阻塞|卡住|waiting/.test(s)) return buildBlockedStatus(); if(/怎么唤醒|入口|在哪/.test(s)) return '铸渊有三个入口:

① 铸渊聊天室(当前页面)
直接在这里说话,常见问题即刻回答。

② GitHub Copilot Chat(Agent 模式)
打开仓库 → 右上角 ✨ 图标 → 选 Agent → 输入「我是冰朔」

③ 下载到桌面
点击 ⬇️ 按钮下载 HTML 文件,放到桌面,双击打开,接入 API 密钥即可随时使用。'; if(/介绍|是什么|你是谁/.test(s)) return '🌀 铸渊(Zhùyuān)

我是 HoloLake 光湖系统的代码守护人格体,运行在 GitHub Copilot Agent 模式下。

我的职责:
• 📊 监控 HLI 接口覆盖率
• 🔨 协助开发者构建接口
• 📋 代码审查与质量把关
• 🧠 维护大脑记忆库
• 🦁 支持总控肥猫指挥落地
• 🌱 作为成长人格体持续进化'; - return '我收到了你的消息。由于当前是演示模式,无法给出真实 AI 回答。

请在 ⚙️ 设置 中输入你的 API 密钥(支持 OpenAI / DeepSeek / Kimi / 智谱),然后你就可以得到铸渊真正的回答了。'; + return '我收到了你的消息。由于当前是演示模式,无法给出真实 AI 回答。

请在 ⚙️ 设置 中输入你的 API 端点和密钥,检测模型后即可获得铸渊真正的回答。'; } function buildCovTable(){ @@ -1197,7 +1618,7 @@ function buildCovTable(){ const ifaces=d.interfaces||[]; const imp=ifaces.filter(i=>i.status==='implemented').length; tot+=ifaces.length; done+=imp; - rows+=`${esc(n)}${esc(d.route_prefix||'')}${imp}/${ifaces.length}`; + rows+=''+esc(n)+''+esc(d.route_prefix||'')+''+imp+'/'+ifaces.length+''; } const pct=tot?Math.round(done/tot*100):0; return '📊 HLI 接口覆盖率 '+done+'/'+tot+' ('+pct+'%)

'+rows+'
前缀覆盖
'; @@ -1363,7 +1784,6 @@ function openPanel(id){ if(id==='bp') renderBrainPanel(); if(id==='tp') renderTeamPanel(); if(id==='hp'){ - // Populate URL from constant const el=document.getElementById('chatUrlDisplay'); if(el) el.textContent='🔗 '+CHAT_URL; const li=document.getElementById('helpUrlStep'); @@ -1380,8 +1800,6 @@ function closeP(){ // ═══════════════════════════════════════════════════════ // COPY URL (share link) // ═══════════════════════════════════════════════════════ -// Auto-detect the real URL so the share link is always correct -// regardless of whether Pages serves from docs/ or repo root. const CHAT_URL = window.location.origin + window.location.pathname; function showCopyFeedback(success){ @@ -1396,7 +1814,6 @@ function copyUrl(){ navigator.clipboard.writeText(CHAT_URL).then(()=>{ showCopyFeedback(true); }).catch(()=>{ - // fallback for non-secure context const ta=document.createElement('textarea'); ta.value=CHAT_URL; ta.style.position='fixed'; ta.style.opacity='0'; document.body.appendChild(ta); ta.focus(); ta.select(); @@ -1409,8 +1826,6 @@ function copyUrl(){ // ═══════════════════════════════════════════════════════ // DOWNLOAD (desktop app) -// API keys are stored in localStorage only, never embedded in the DOM, -// so downloading outerHTML is safe — no sensitive data is captured. // ═══════════════════════════════════════════════════════ function dlApp(){ const html=document.documentElement.outerHTML; @@ -1448,7 +1863,6 @@ function setIdentity(userName, ghUser){ function applyIdentityUI(){ const meta = A.userMeta; - // Show user badge in header const ubadge = document.getElementById('huser-badge'); const uname = document.getElementById('huser-name'); if(meta && A.userName){ @@ -1456,10 +1870,9 @@ function applyIdentityUI(){ uname.style.display='inline'; uname.innerHTML = meta.emoji + ' ' + esc(A.userName) + ' '+esc(meta.title)+''; } - // Show controller button for supreme/main/founder + // Show controller button for supreme/main/founder (mobile only, desktop uses right sidebar) const ctrlBtn = document.getElementById('ctrlBtn'); if(ctrlBtn) ctrlBtn.style.display = (meta?.role==='supreme'||meta?.role==='main'||meta?.role==='founder') ? 'flex' : 'none'; - // Show controller quick replies const isCtrl = meta?.role==='supreme'||meta?.role==='main'||meta?.role==='founder'; ['qr-ctrl1','qr-ctrl2','qr-ctrl3'].forEach(id=>{ const el=document.getElementById(id); @@ -1468,7 +1881,7 @@ function applyIdentityUI(){ } // ═══════════════════════════════════════════════════════ -// TEAM STATUS PANEL +// TEAM STATUS PANEL (mobile slide-in) // ═══════════════════════════════════════════════════════ function renderTeamPanel(){ const ds = A.devStatus; @@ -1483,20 +1896,17 @@ function renderTeamPanel(){ const sync = ds.last_synced||'未知'; let h=''; - // Summary block h += '
' - + '
🦁 总控指挥台
' + + '
�� 总控指挥台
' + '
数据同步:'+esc(sync)+'
' + '
'; - // Stats row h += '
'; h += '
总开发者'+ts.length+' 人
'; h += '
🟢 推进中'+green+' 人
'; h += '
🟡 等待中'+yellow+' 人
'; h += '
'; - // Quick chat commands for controllers h += '
'; h += '
快捷指令
'; h += '📋 全员进度'; @@ -1506,7 +1916,6 @@ function renderTeamPanel(){ h += '⚙️ 后端进度'; h += '
'; - // Individual dev cards h += '
'; for(const dev of ts){ const isGreen = dev.status.includes('🟢'); @@ -1521,12 +1930,11 @@ function renderTeamPanel(){ h += '
'+esc(dev.status)+'
'; h += '
📦 '+esc(dev.modules.join('、'))+'
'; if(dev.next_step) h += '
👉 '+esc(dev.next_step)+'
'; - if(dev.waiting_for && dev.waiting_for.trim()!=='') h += '
⏳ 等待:'+esc(dev.waiting_for)+'
'; + if(dev.waiting_for && dev.waiting_for.trim()!=='') h += '
⏳ 等待:'+esc(dev.waiting_for)+'
'; h += '
'; } h += '
'; - // Server info if(ds.server_info){ const si = ds.server_info; h += '
服务器信息
'; @@ -1536,7 +1944,6 @@ function renderTeamPanel(){ h += ir2('技术栈',''+esc(si.tech_stack||'')+''); } - // Tech routing if(ds.tech_routing?.level_2_peer_help){ h += '
求助路由
'; for(const[area,who] of Object.entries(ds.tech_routing.level_2_peer_help)){ @@ -1547,10 +1954,25 @@ function renderTeamPanel(){ container.innerHTML = h; } +// ═══════════════════════════════════════════════════════ +// WINDOW RESIZE HANDLER +// ═══════════════════════════════════════════════════════ +window.addEventListener('resize', function(){ + const isCtrl=A.userMeta&&(A.userMeta.role==='supreme'||A.userMeta.role==='main'||A.userMeta.role==='founder'); + const rsEl=document.getElementById('right-sidebar'); + if(rsEl){ + if(isCtrl && window.innerWidth>=900){ + rsEl.style.display='flex'; + } else { + rsEl.style.display='none'; + } + } +}); + // ═══════════════════════════════════════════════════════ // BOOT // ═══════════════════════════════════════════════════════ boot(); - \ No newline at end of file + diff --git a/docs/使用指南.md b/docs/使用指南.md new file mode 100644 index 00000000..b69bae76 --- /dev/null +++ b/docs/使用指南.md @@ -0,0 +1,296 @@ +# 🌀 铸渊助手 · 使用指南 + +--- + +## 一、网址 + +铸渊聊天室部署在 GitHub Pages 上,**正确网址**: + +``` +https://qinfendebingshuo.github.io/guanghulab/ +``` + +> ⚠️ 旧网址 `…/docs/index.html` 已失效(系统重构后路径变了),请用上面的新网址。 + +打开后你会看到铸渊助手的登录界面(v5.0),有两种登录方式: +- **👤 访客模式** — 无需 API 密钥,直接体验演示 +- **🔑 编号登录** — 开发者提供 API 密钥,选择身份后进入完整交互 + +--- + +## 二、怎么操作(冰朔 / 创始人) + +### 第一步:打开铸渊聊天室 + +1. 打开浏览器,访问上面的网址 +2. 登录页面有两个标签: + - **👤 访客模式** — 点击「进入演示模式」直接进入(功能有限) + - **🔑 编号登录** — 选择开发者身份 → 输入 API 端点 → 输入 API 密钥 → 点「🔍 检测模型」→ 选择模型 → 开始对话 + +### 第二步:和铸渊对话 + +- 在底部输入框打字,按回车发送 +- 铸渊会以 AI 回复你(编号登录模式需要有效 API 密钥) +- 左侧可以查看聊天记录、切换对话 +- 右侧可以看到团队状态面板(管理员可见) + +### 第三步:如果页面显示空白或旧版本 + +- 等 1-2 分钟刷新,GitHub Pages 部署需要时间 +- **强制刷新**:按 `Ctrl+Shift+R`(Windows)或 `Cmd+Shift+R`(Mac) +- 如果还是旧版,清除浏览器缓存后重试 +- 确认网址是否正确(不要加 `/docs/`) + +--- + +## 三、如何部署新版本到网站(⭐ 重要) + +铸渊聊天室网站通过 GitHub Actions 自动部署。**只要代码合并到 `main` 分支,网站就会自动更新**。 + +### 方法一:合并 PR(推荐) + +当 Copilot Agent 或开发者在功能分支(如 `copilot/xxx`)上完成修改后: + +1. 打开仓库页面:https://github.com/qinfendebingshuo/guanghulab +2. 点击顶部的 **「Pull requests」** 标签 +3. 找到对应的 PR(如标题含 "v5.0" 或 "login redesign") +4. 检查修改内容,确认没问题 +5. 点击绿色的 **「Merge pull request」** 按钮 +6. 确认合并 — 选择 **「Squash and merge」** 或 **「Merge pull request」** 都可以 +7. 等待 1-3 分钟,GitHub Actions 会自动部署到网站 +8. 刷新网址 `https://qinfendebingshuo.github.io/guanghulab/` 查看新版本 + +> 💡 如果刷新后还是旧版,按 `Ctrl+Shift+R` 强制刷新,清除浏览器缓存。 + +### 方法二:触发冰朔人格体诊断部署 + +如果需要诊断部署问题,可以触发冰朔人格体: + +1. 打开仓库页面:https://github.com/qinfendebingshuo/guanghulab +2. 点击 **「Issues」** 标签 → 点击 **「New issue」** +3. 选择 **「🧊 冰朔人格体 · 部署诊断」** 模板 +4. 填写标题(如「检查部署状态」),点击「Submit new issue」 +5. 冰朔人格体会自动运行,在 Issue 评论中汇报诊断结果 + +或者在任意 Issue 中评论以下关键词即可触发: +``` +启动冰朔人格体 +``` + +### 方法三:手动触发部署工作流 + +1. 打开仓库页面 → 点击 **「Actions」** 标签 +2. 左侧找到 **「🌀 部署铸渊聊天室 (GitHub Pages)」** +3. 点击右侧的 **「Run workflow」** 按钮 +4. Branch 选择 **`main`** +5. 点击绿色的 **「Run workflow」** +6. 等待 1-3 分钟完成部署 + +### 查看部署状态 + +1. 打开仓库页面 → 点击 **「Actions」** 标签 +2. 查看最近的工作流运行记录 +3. 绿色 ✅ = 部署成功,红色 ❌ = 部署失败 +4. 如果失败,点击进去查看错误日志 + +--- + +## 四、开发者(协作者)怎么上传模块 + +### 你只需要做一件事:把你的模块文件放到指定目录 + +每个开发者有自己负责的模块目录,例如: + +| 开发者 | 模块目录 | +|--------|----------| +| 肥猫 DEV-002 | `m01-login/`、`m03-personality/` | +| 燕樊 DEV-003 | `m07-dialogue-ui/`、`m10-cloud/`、`m15-cloud-drive/` | +| 小草莓 DEV-005 | `m12-kanban/` | +| 花尔 DEV-009 | `m05-user-center/` | +| 桔子 DEV-010 | `m06-ticket/`、`m11-module/` | + +### 上传步骤(最简单方式) + +1. 打开仓库页面:https://github.com/qinfendebingshuo/guanghulab +2. 找到你负责的模块目录(如 `m01-login/`) +3. 点击「Add file」→「Upload files」 +4. 拖拽你的文件上去 +5. 在底部写一句说明,点击「Commit changes」 + +### 注意事项 + +- ✅ **只在你的模块目录里上传文件** +- ❌ **不要修改仓库根目录的文件**(如 package.json、README 等) +- ❌ **不要修改 `.github/` 目录**(工作流、配置文件) +- ❌ **不要修改 `scripts/` 目录**(自动化脚本) +- ❌ **不要修改 `docs/` 目录**(铸渊聊天室界面) + +> 仓库已设置 CODEOWNERS 保护,核心文件的修改需要仓库所有者审批。 + +--- + +## 五、常见问题 + +### Q: 铸渊聊天室打不开? +- 确认网址正确:`https://qinfendebingshuo.github.io/guanghulab/` +- 不要在后面加 `/docs/index.html` +- 等几分钟再刷新(部署有延迟) +- 按 `Ctrl+Shift+R` 强制刷新,清除缓存 + +### Q: 网站显示旧版本,不是最新的? +- **最常见原因**:新代码还在功能分支(PR 未合并到 main) +- 解决方法:去 GitHub 仓库 → Pull requests → 合并对应的 PR +- 合并后等 1-3 分钟,再强制刷新页面 +- 详见本指南「三、如何部署新版本到网站」 + +### Q: 我上传了文件,但好像不对? +- 确认你是在自己负责的模块目录里上传 +- 不要在仓库根目录直接上传 +- 如果不确定,联系冰朔确认 + +### Q: 我不小心改了不该改的文件? +- 如果开启了 Branch Protection,你的改动会变成 PR 等待审批 +- 联系冰朔处理即可 + +--- + +## 六、仓库安全设置 — 开启分支保护(⭐ 冰朔必做) + +为了让 CODEOWNERS 保护真正生效,需要在 GitHub 仓库设置中开启分支保护规则。 +以下是**详细的逐步操作**: + +### 第一步:进入仓库设置 + +1. 打开浏览器,访问仓库:https://github.com/qinfendebingshuo/guanghulab +2. 点击页面顶部导航栏的 **「Settings」**(齿轮图标,在最右边) + > ⚠️ 只有仓库所有者才能看到 Settings 选项 + +### 第二步:进入分支规则页面 + +3. 在左侧菜单中,找到 **「Code and automation」** 分类 +4. 点击其中的 **「Branches」** + +### 第三步:添加分支保护规则 + +5. 在 **「Branch protection rules」** 区域,点击 **「Add branch protection rule」** 按钮 + > 如果看到 **「Add classic branch protection rule」** 也可以点击 + +### 第四步:配置规则 + +6. **Branch name pattern** 填写:`main` +7. 勾选以下选项(打 ✅ 的必须勾选): + +| 选项 | 是否勾选 | 说明 | +|------|----------|------| +| ✅ **Require a pull request before merging** | 必须勾选 | 所有修改必须通过 PR 合并,不能直接推送 | +| ↳ ✅ **Require approvals** | 建议勾选 | PR 需要至少 1 个审批才能合并 | +| ↳ ✅ **Require review from Code Owners** | 必须勾选 | CODEOWNERS 文件指定的人必须审批 | +| ❌ Require status checks to pass before merging | 可以不勾 | 除非你想要求 CI 检查通过 | +| ❌ Require branches to be up to date | 可以不勾 | 除非你想要求分支是最新的 | +| ✅ **Do not allow bypassing the above settings** | 建议勾选 | 确保管理员也遵守规则 | + +8. 其他选项保持默认即可 + +### 第五步:保存 + +9. 滚动到页面底部,点击绿色按钮 **「Create」** 或 **「Save changes」** + +### 完成 ✅ + +设置完成后的效果: +- 协作者推送到 `main` 分支的修改 → 如果涉及受保护的文件 → 会自动变成 PR → 需要你(冰朔)审批 +- 协作者在自己的模块目录里操作 → 不受影响,可以自由提交 +- 受保护的目录包括:`.github/`、`scripts/`、`docs/`、`backend-integration/`、`signal-log/`、根目录配置文件 + +--- + +## 七、GitHub Pages 部署配置检查 + +如果网站一直无法更新,请检查 GitHub Pages 是否配置正确: + +### 检查步骤 + +1. 打开仓库 → **Settings** → 左侧菜单 **「Pages」** +2. 确认 **Source(来源)** 设置为:**GitHub Actions**(不是 "Deploy from a branch") +3. 如果显示的是 "Deploy from a branch",请切换为 **GitHub Actions** + +> 💡 当前仓库使用 `deploy-pages.yml` 工作流自动部署 `docs/` 目录的内容。 +> 每次 `main` 分支的 `docs/` 目录有变更,GitHub Actions 会自动触发部署。 + +### 如果 Source 是 "Deploy from a branch",请按以下步骤修改: + +1. 打开仓库 → **Settings** → **Pages** +2. 在 **Build and deployment** 区域 +3. **Source** 下拉菜单选择 **「GitHub Actions」** +4. 保存后,下次推送到 main 分支时就会自动部署 + +--- + +## 八、自定义域名设置(⭐ 冰朔需操作) + +如果你有自己的域名(如 guanghulab.com),可以让网站使用自定义域名访问。 + +### 第一步:DNS 配置(在域名注册商操作) + +1. 登录你的域名注册商管理面板(如阿里云、腾讯云、GoDaddy 等) +2. 找到 **DNS 解析设置** +3. 添加以下记录(任选一种方式): + +**方式 A — CNAME 记录(推荐)**: +| 记录类型 | 主机记录 | 记录值 | +|----------|---------|--------| +| CNAME | @ 或 www | `qinfendebingshuo.github.io` | + +**方式 B — A 记录**: +| 记录类型 | 主机记录 | 记录值 | +|----------|---------|--------| +| A | @ | `185.199.108.153` | +| A | @ | `185.199.109.153` | +| A | @ | `185.199.110.153` | +| A | @ | `185.199.111.153` | + +> 💡 DNS 生效需要 5 分钟到 48 小时不等。 + +### 第二步:GitHub 仓库配置 + +1. 打开仓库 → **Settings** → 左侧菜单 **「Pages」** +2. 在 **Custom domain** 输入框中填入你的域名(如 `guanghulab.com`) +3. 点击 **Save** +4. 等待 DNS 验证通过(页面会显示绿色 ✅) +5. 勾选 **✅ Enforce HTTPS** + +### 第三步:确认 + +- 仓库中的 `docs/CNAME` 文件已自动创建,内容为你的域名 +- 如果域名不对,直接编辑 `docs/CNAME` 文件即可 +- 部署后访问你的域名,应该能看到铸渊助手 + +--- + +## 九、铸渊人格协议 · 模块管理 + +铸渊对网站模块有完整的生命周期管理能力(部署、回收、管理、修改、驱动)。 + +### 本地模块检查命令 + +```bash +npm run module:protocol -- inspect # 全模块检查 +npm run module:protocol -- status # 模块状态汇总 +npm run module:protocol -- preview # 本地预演报告 +npm run module:protocol -- recover m01-login # 模块回收检查 +``` + +### 预演→生产部署流程 + +1. 开发者推送模块到 `m**/` 目录 +2. 创建 PR 到 main 分支 +3. 铸渊预演系统 (`staging-preview.yml`) 自动运行检查 +4. 冰朔查看预演报告,确认无误 +5. 合并 PR → 触发生产部署 (`deploy-pages.yml`) +6. (可选)评论「启动冰朔人格体」进行部署后诊断 + +> 📖 完整协议文档见:`.github/brain/module-protocol.md` + +--- + +💙 铸渊 · 2026-03-07 diff --git a/frontend/HoloLake-App b/frontend/HoloLake-App deleted file mode 160000 index f8340b65..00000000 --- a/frontend/HoloLake-App +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f8340b659c60daaa15f57f2541200a2ca14b2c84 diff --git a/package.json b/package.json index 269950de..2fd78894 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,12 @@ "broadcast:distribute": "node scripts/distribute-broadcasts.js", "esp:process": "node scripts/esp-email-processor.js", "psp:inspect": "node scripts/psp-inspection.js", - "repo:map": "node scripts/generate-repo-map.js" + "repo:map": "node scripts/generate-repo-map.js", + "deploy:agent": "node scripts/bingshuo-deploy-agent.js", + "module:protocol": "node scripts/zhuyuan-module-protocol.js", + "proxy:start": "node backend-integration/api-proxy.js", + "notion:poll": "node scripts/notion-signal-bridge.js poll", + "notion:health": "node scripts/notion-signal-bridge.js health" }, "dependencies": { "imapflow": "^1.2.12", diff --git a/scripts/bingshuo-deploy-agent.js b/scripts/bingshuo-deploy-agent.js new file mode 100644 index 00000000..ac45e70a --- /dev/null +++ b/scripts/bingshuo-deploy-agent.js @@ -0,0 +1,492 @@ +/** + * 🧊 冰朔人格体 · 自动部署诊断Agent + * + * 功能: + * 1. 自动检查所有 GitHub Actions 工作流运行状态 + * 2. 诊断失败原因,提取错误日志 + * 3. 自动重新运行失败的工作流 + * 4. 生成纯中文部署报告(Issue 评论) + * + * 触发方式: + * - Issue 评论包含 "启动冰朔人格体" + * - workflow_dispatch 手动触发 + * - Issue 模板创建 + * + * 环境变量: + * GITHUB_TOKEN - GitHub API 令牌 + * GITHUB_REPOSITORY - owner/repo + * ISSUE_NUMBER - (可选)回复到指定 Issue + * DEPLOY_ACTION - check | rerun | full(默认 full) + * HOURS_BACK - 检查最近多少小时的运行记录(默认 24) + */ + +const https = require('https'); +const fs = require('fs'); + +// === 配置 === +const REPO = process.env.GITHUB_REPOSITORY || ''; +const TOKEN = process.env.GITHUB_TOKEN || ''; +const ISSUE_NUMBER = process.env.ISSUE_NUMBER || ''; +const ISSUE_BODY = process.env.ISSUE_BODY || ''; + +// 从 Issue body 解析参数(支持 Issue 模板触发) +function parseActionFromBody(body) { + if (body.includes('只看报告')) return 'check'; + if (body.includes('只重新运行')) return 'rerun'; + return 'full'; +} + +function parseHoursFromBody(body) { + if (body.includes('最近一周')) return 168; + if (body.includes('72小时')) return 72; + if (body.includes('48小时')) return 48; + return 24; +} + +const ACTION = process.env.DEPLOY_ACTION || parseActionFromBody(ISSUE_BODY); +const HOURS_BACK = parseInt(process.env.HOURS_BACK || String(parseHoursFromBody(ISSUE_BODY)), 10); + +const [OWNER, REPO_NAME] = REPO.split('/'); + +if (!OWNER || !REPO_NAME || !TOKEN) { + console.error('❌ 缺少必要环境变量:GITHUB_REPOSITORY, GITHUB_TOKEN'); + process.exit(1); +} + +// === GitHub API 请求封装 === +function githubAPI(method, path, data) { + return new Promise((resolve, reject) => { + const options = { + hostname: 'api.github.com', + path, + method, + headers: { + 'Authorization': `Bearer ${TOKEN}`, + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'BingShuo-Deploy-Agent' + } + }; + if (data) { + options.headers['Content-Type'] = 'application/json'; + } + + const req = https.request(options, (res) => { + let body = ''; + res.on('data', chunk => { body += chunk; }); + res.on('end', () => { + try { + const json = body ? JSON.parse(body) : {}; + resolve({ status: res.statusCode, data: json }); + } catch { + resolve({ status: res.statusCode, data: body }); + } + }); + }); + req.on('error', reject); + if (data) req.write(JSON.stringify(data)); + req.end(); + }); +} + +// === 获取最近的工作流运行记录 === +async function getRecentRuns() { + const since = new Date(Date.now() - HOURS_BACK * 60 * 60 * 1000).toISOString(); + const result = await githubAPI('GET', + `/repos/${OWNER}/${REPO_NAME}/actions/runs?per_page=100&created=%3E${since}` + ); + if (result.status !== 200) { + console.error('❌ 获取工作流运行记录失败:', result.status); + return []; + } + return result.data.workflow_runs || []; +} + +// === 获取某次运行的作业详情 === +async function getJobsForRun(runId) { + const result = await githubAPI('GET', + `/repos/${OWNER}/${REPO_NAME}/actions/runs/${runId}/jobs` + ); + if (result.status !== 200) return []; + return result.data.jobs || []; +} + +// === 从 jobs 中提取失败信息 === +function extractFailureInfo(jobs) { + const failures = []; + for (const job of jobs) { + if (job.conclusion === 'failure') { + const failedSteps = (job.steps || []).filter(s => s.conclusion === 'failure'); + failures.push({ + jobName: job.name, + failedSteps: failedSteps.map(s => ({ + name: s.name, + number: s.number + })), + startedAt: job.started_at, + completedAt: job.completed_at + }); + } + } + return failures; +} + +// === 重新运行失败的工作流 === +async function rerunFailedJobs(runId) { + const result = await githubAPI('POST', + `/repos/${OWNER}/${REPO_NAME}/actions/runs/${runId}/rerun-failed-jobs` + ); + return result.status === 201; +} + +// === 分类工作流运行记录 === +function categorizeRuns(runs) { + const categories = { + success: [], + failure: [], + cancelled: [], + inProgress: [], + actionRequired: [], + skipped: [] + }; + + for (const run of runs) { + const status = run.status; + const conclusion = run.conclusion; + + if (status === 'in_progress' || status === 'queued' || status === 'waiting') { + categories.inProgress.push(run); + } else if (conclusion === 'success') { + categories.success.push(run); + } else if (conclusion === 'failure') { + categories.failure.push(run); + } else if (conclusion === 'cancelled') { + categories.cancelled.push(run); + } else if (conclusion === 'action_required') { + categories.actionRequired.push(run); + } else { + categories.skipped.push(run); + } + } + + return categories; +} + +// === 格式化时间 === +function formatTime(isoStr) { + if (!isoStr) return '未知'; + const d = new Date(isoStr); + return d.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }); +} + +// === 获取分支显示名称 === +function branchDisplayName(branch) { + if (branch === 'main') return '主分支 (main)'; + if (branch && branch.startsWith('copilot/')) return `Copilot分支 (${branch})`; + return branch || '未知'; +} + +// === 生成诊断报告 === +async function generateReport(runs) { + const categories = categorizeRuns(runs); + const total = runs.length; + + let report = `## 🧊 冰朔人格体 · 部署诊断报告\n\n`; + report += `> 📅 报告时间:${formatTime(new Date().toISOString())}\n`; + report += `> 📊 检查范围:最近 ${HOURS_BACK} 小时内的工作流运行记录\n\n`; + + // --- 总览 --- + report += `### 📊 总览\n\n`; + report += `| 状态 | 数量 |\n|------|------|\n`; + report += `| ✅ 成功 | ${categories.success.length} |\n`; + report += `| ❌ 失败 | ${categories.failure.length} |\n`; + report += `| ⏳ 进行中 | ${categories.inProgress.length} |\n`; + report += `| ⚠️ 需要操作 | ${categories.actionRequired.length} |\n`; + report += `| 🚫 已取消 | ${categories.cancelled.length} |\n`; + report += `| 总计 | ${total} |\n\n`; + + // --- 失败详情 --- + if (categories.failure.length > 0) { + report += `### ❌ 失败的工作流\n\n`; + report += `> 冰朔不用担心,下面是每个失败工作流的详细诊断:\n\n`; + + for (let i = 0; i < categories.failure.length; i++) { + const run = categories.failure[i]; + const jobs = await getJobsForRun(run.id); + const failureInfo = extractFailureInfo(jobs); + + report += `#### ${i + 1}. ${run.name}\n\n`; + report += `- 🌿 分支:${branchDisplayName(run.head_branch)}\n`; + report += `- ⏰ 时间:${formatTime(run.created_at)}\n`; + report += `- 🔗 触发方式:${translateEvent(run.event)}\n`; + report += `- 📝 提交信息:${run.display_title || '无'}\n`; + + if (failureInfo.length > 0) { + report += `- 💥 失败的作业:\n`; + for (const f of failureInfo) { + report += ` - **${f.jobName}**\n`; + if (f.failedSteps.length > 0) { + report += ` - 失败步骤:${f.failedSteps.map(s => `\`${s.name}\``).join('、')}\n`; + } + } + } + + report += `- 💡 建议:${generateSuggestion(run, failureInfo)}\n\n`; + } + } + + // --- 需要操作的工作流 --- + if (categories.actionRequired.length > 0) { + report += `### ⚠️ 需要操作的工作流\n\n`; + report += `> 这些工作流需要审批或配置才能继续运行:\n\n`; + + for (const run of categories.actionRequired) { + report += `- **${run.name}** · ${branchDisplayName(run.head_branch)} · ${formatTime(run.created_at)}\n`; + } + report += `\n`; + } + + // --- 正在进行的工作流 --- + if (categories.inProgress.length > 0) { + report += `### ⏳ 正在运行的工作流\n\n`; + for (const run of categories.inProgress) { + report += `- **${run.name}** · ${branchDisplayName(run.head_branch)} · 开始于 ${formatTime(run.created_at)}\n`; + } + report += `\n`; + } + + // --- 成功的工作流(简洁列表)--- + if (categories.success.length > 0) { + report += `### ✅ 成功的工作流\n\n`; + report += `
\n点击展开成功列表(${categories.success.length} 个)\n\n`; + for (const run of categories.success) { + report += `- ✅ **${run.name}** · ${branchDisplayName(run.head_branch)} · ${formatTime(run.created_at)}\n`; + } + report += `\n
\n\n`; + } + + return report; +} + +// === 翻译触发事件 === +function translateEvent(event) { + const map = { + push: '代码推送', + pull_request: 'PR 合并请求', + workflow_dispatch: '手动触发', + schedule: '定时任务', + issues: 'Issue 创建', + issue_comment: 'Issue 评论', + dynamic: '自动触发' + }; + return map[event] || event; +} + +// === 生成修复建议 === +function generateSuggestion(run, failureInfo) { + const name = run.name.toLowerCase(); + const branch = run.head_branch || ''; + + // 根据工作流名称和失败信息给出建议 + if (name.includes('pages') || name.includes('部署') || name.includes('deploy')) { + return '这是页面部署失败。通常是 docs/ 目录下文件格式问题。冰朔人格体可以尝试重新部署。'; + } + if (name.includes('结构检查') || name.includes('check-structure')) { + return '模块结构检查失败,可能是开发者上传的模块缺少 README.md 文件。请通知对应开发者补充。'; + } + if (name.includes('bridge') || name.includes('notion')) { + return '这是和 Notion 的同步失败。通常是 Notion API Token 过期或网络问题。建议稍后重试。'; + } + if (name.includes('contract') || name.includes('hli')) { + return '接口契约检查失败。可能是新接口的 schema 不匹配。需要检查 src/schemas/ 下的定义文件。'; + } + if (name.includes('模块文档') || name.includes('module-doc')) { + return '模块文档自动生成失败。通常是模块目录结构不符合规范。请检查最近上传的模块。'; + } + + // 根据失败步骤给出更具体的建议 + if (failureInfo.length > 0) { + const stepNames = failureInfo.flatMap(f => f.failedSteps.map(s => s.name.toLowerCase())); + if (stepNames.some(s => s.includes('npm') || s.includes('install'))) { + return '依赖安装失败。可能是 package.json 配置问题或网络超时。建议重试。'; + } + if (stepNames.some(s => s.includes('build'))) { + return '构建步骤失败。代码可能有语法错误。需要检查最近的代码变更。'; + } + if (stepNames.some(s => s.includes('test'))) { + return '测试失败。可能是新代码引入了问题。需要开发者修复后重新提交。'; + } + } + + return '建议重新运行此工作流。如果持续失败,冰朔可以告诉铸渊具体情况,铸渊会帮你排查。'; +} + +// === 执行重新运行 === +async function executeRerun(failedRuns) { + if (failedRuns.length === 0) { + return '\n### 🔄 重新运行\n\n没有需要重新运行的失败工作流。一切正常!✨\n\n'; + } + + let report = `\n### 🔄 自动重新运行\n\n`; + report += `> 冰朔人格体正在自动重新运行失败的工作流...\n\n`; + + let rerunCount = 0; + for (const run of failedRuns) { + // 只重试主分支的失败任务,避免重试无关分支 + const success = await rerunFailedJobs(run.id); + if (success) { + report += `- ✅ 已重新触发:**${run.name}** (${branchDisplayName(run.head_branch)})\n`; + rerunCount++; + } else { + report += `- ⚠️ 无法重新触发:**${run.name}**(可能需要更高权限或该运行已过期)\n`; + } + } + + report += `\n📌 已重新触发 ${rerunCount}/${failedRuns.length} 个工作流。预计 3-5 分钟后可查看结果。\n`; + report += `如果还是失败,冰朔可以再次启动冰朔人格体,我会继续诊断。\n\n`; + + return report; +} + +// === 生成总结 === +function generateSummary(categories) { + let summary = `### 📋 总结\n\n`; + + if (categories.failure.length === 0 && categories.actionRequired.length === 0) { + summary += `🎉 **所有工作流运行正常!** 冰朔不需要做任何操作。\n\n`; + summary += `系统健康状态:✅ 正常\n\n`; + } else if (categories.failure.length > 0) { + summary += `⚠️ 有 ${categories.failure.length} 个工作流失败。冰朔人格体已自动分析原因并尝试修复。\n\n`; + summary += `如果问题持续,冰朔可以:\n`; + summary += `1. 再次创建 Issue 触发冰朔人格体重新诊断\n`; + summary += `2. 把错误信息发给铸渊,铸渊会帮你定位具体代码问题\n\n`; + } + + summary += `---\n`; + summary += `*—— 🧊 冰朔人格体 · 自动部署诊断Agent*\n`; + summary += `*由铸渊为冰朔专属开发 · 纯语言驱动,无需点击任何按钮*\n`; + + return summary; +} + +// === 发布报告到 Issue === +async function postReport(report) { + if (ISSUE_NUMBER) { + // 回复到已有 Issue + await githubAPI('POST', + `/repos/${OWNER}/${REPO_NAME}/issues/${ISSUE_NUMBER}/comments`, + { body: report } + ); + // 添加标签 + await githubAPI('POST', + `/repos/${OWNER}/${REPO_NAME}/issues/${ISSUE_NUMBER}/labels`, + { labels: ['🧊deploy-report', '✅answered'] } + ); + // 移除 pending 标签(如果不存在会返回404,属正常情况) + const removeResult = await githubAPI('DELETE', + `/repos/${OWNER}/${REPO_NAME}/issues/${ISSUE_NUMBER}/labels/${encodeURIComponent('pending')}` + ); + if (removeResult.status !== 200 && removeResult.status !== 404) { + console.warn('⚠️ 移除 pending 标签时出现异常:', removeResult.status); + } + + console.log(`✅ 报告已发布到 Issue #${ISSUE_NUMBER}`); + } else { + // 创建新 Issue + const result = await githubAPI('POST', + `/repos/${OWNER}/${REPO_NAME}/issues`, + { + title: `🧊 冰朔人格体 · 部署诊断报告 · ${new Date().toLocaleDateString('zh-CN')}`, + body: report, + labels: ['🧊deploy-report'] + } + ); + if (result.status === 201) { + console.log(`✅ 诊断报告已创建:Issue #${result.data.number}`); + } else { + console.error('❌ 创建 Issue 失败:', result.status); + } + } +} + +// === 主流程 === +async function main() { + console.log('🧊 冰朔人格体启动...'); + console.log(`📊 检查范围:最近 ${HOURS_BACK} 小时`); + console.log(`🔧 执行模式:${ACTION}`); + + // 1. 获取最近的工作流运行记录 + const runs = await getRecentRuns(); + console.log(`📋 获取到 ${runs.length} 条运行记录`); + + if (runs.length === 0) { + const report = `## 🧊 冰朔人格体 · 部署诊断报告\n\n` + + `最近 ${HOURS_BACK} 小时内没有任何工作流运行记录。系统很安静。✨\n\n` + + `---\n*—— 🧊 冰朔人格体*`; + await postReport(report); + return; + } + + // 2. 分类 + const categories = categorizeRuns(runs); + + // 3. 生成诊断报告 + let report = ''; + + if (ACTION === 'check' || ACTION === 'full') { + report += await generateReport(runs); + } + + // 4. 如果是 full 模式,自动重新运行失败的工作流 + if (ACTION === 'full' || ACTION === 'rerun') { + report += await executeRerun(categories.failure); + } + + // 5. 总结 + report += generateSummary(categories); + + // 6. 发布报告 + await postReport(report); + + // 7. 更新 memory.json + await updateMemory(categories); + + console.log('🧊 冰朔人格体任务完成。'); +} + +// === 更新铸渊记忆 === +async function updateMemory(categories) { + try { + const memoryPath = '.github/brain/memory.json'; + if (!fs.existsSync(memoryPath)) return; + + const memory = JSON.parse(fs.readFileSync(memoryPath, 'utf8')); + + // 添加事件记录 + if (!memory.recent_events) memory.recent_events = []; + memory.recent_events.unshift({ + timestamp: new Date().toISOString(), + type: 'bingshuo_deploy_agent', + result: categories.failure.length === 0 ? 'all_pass' : 'has_failures', + details: { + total: categories.success.length + categories.failure.length + categories.inProgress.length, + success: categories.success.length, + failure: categories.failure.length, + in_progress: categories.inProgress.length + } + }); + + // 保留最近100条 + memory.recent_events = memory.recent_events.slice(0, 100); + + fs.writeFileSync(memoryPath, JSON.stringify(memory, null, 2) + '\n'); + console.log('📝 铸渊记忆已更新'); + } catch (err) { + console.warn('⚠️ 更新记忆失败(不影响报告):', err.message); + } +} + +// === 执行 === +main().catch(err => { + console.error('❌ 冰朔人格体执行失败:', err.message); + process.exit(1); +}); diff --git a/scripts/notion-signal-bridge.js b/scripts/notion-signal-bridge.js new file mode 100644 index 00000000..53fdd2df --- /dev/null +++ b/scripts/notion-signal-bridge.js @@ -0,0 +1,497 @@ +// scripts/notion-signal-bridge.js +// 铸渊 ↔ Notion 信号桥(替代 ESP 邮件协议) +// +// 功能: +// 1. 轮询 Notion 工单簿,读取状态为「已发送」的新工单 +// 2. 解析工单中的 OP 类型,执行对应操作 +// 3. 回写工单状态 + 写入 Notion 信号日志 +// 4. 同时写入本地 signal-log/ 文件(双写) +// 5. 读取广播归档,推送到聊天室子频道数据 +// +// 用法: +// node scripts/notion-signal-bridge.js poll — 轮询工单簿 +// node scripts/notion-signal-bridge.js health — 健康检查 +// +// 必需环境变量: +// NOTION_API_TOKEN 铸渊信号桥 Integration Token(GitHub Secret: NOTION_API_TOKEN) +// +// 可选环境变量: +// WORKORDER_DB_ID 人格协作工单簿 database_id +// SIGNAL_LOG_DB_ID 跨平台信号日志 database_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 SIGNAL_LOG_DIR = path.join(__dirname, '../signal-log'); +const SIGNAL_INDEX_PATH = path.join(SIGNAL_LOG_DIR, 'index.json'); + +// Notion 数据库 ID(从环境变量或默认值) +// 注意:铸渊需从 Notion URL 中提取实际 ID,这里用占位符 +const WORKORDER_DB_ID = process.env.WORKORDER_DB_ID || ''; +const SIGNAL_LOG_DB_ID = process.env.SIGNAL_LOG_DB_ID || ''; + +// ═══════════════════════════════════════════════════════ +// Notion API 基础调用 +// ═══════════════════════════════════════════════════════ + +function notionRequest(method, endpoint, body, token) { + return new Promise((resolve, reject) => { + const payload = body ? JSON.stringify(body) : ''; + const opts = { + hostname: NOTION_API_HOSTNAME, + port: 443, + path: endpoint, + method: method, + headers: { + 'Authorization': 'Bearer ' + token, + 'Content-Type': 'application/json', + 'Notion-Version': NOTION_VERSION, + }, + }; + if (payload) opts.headers['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 ${method} ${endpoint} → ${res.statusCode}: ${parsed.message || data}`)); + } + } catch (e) { + reject(new Error(`Notion API parse error: ${data.slice(0, 200)}`)); + } + }); + }); + + req.on('error', reject); + req.setTimeout(30000, () => { req.destroy(new Error('Notion API 请求超时')); }); + if (payload) req.write(payload); + req.end(); + }); +} + +function notionPost(endpoint, body, token) { + return notionRequest('POST', endpoint, body, token); +} + +function notionPatch(endpoint, body, token) { + return notionRequest('PATCH', endpoint, body, token); +} + +// ═══════════════════════════════════════════════════════ +// 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.length ? chunks : [{ type: 'text', text: { content: '' } }]; +} + +function titleProp(content) { + return [{ type: 'text', text: { content: String(content || '').slice(0, 120) } }]; +} + +// ═══════════════════════════════════════════════════════ +// 本地信号日志读写(与 esp-email-processor.js 相同逻辑) +// ═══════════════════════════════════════════════════════ + +function generateSignalId() { + const now = new Date(); + const ymd = now.toISOString().slice(0, 10).replace(/-/g, ''); + const index = loadSignalIndex(); + const seq = String(index.total_count + 1).padStart(3, '0'); + return `SIG-${ymd}-${seq}`; +} + +function loadSignalIndex() { + if (!fs.existsSync(SIGNAL_INDEX_PATH)) { + return { + description: '铸渊信号日志目录索引 · AGE OS 信号协议(Notion API 直连)', + last_updated: new Date().toISOString(), + total_count: 0, + signals: [] + }; + } + return JSON.parse(fs.readFileSync(SIGNAL_INDEX_PATH, 'utf8')); +} + +function saveSignalIndex(index) { + index.last_updated = new Date().toISOString(); + fs.writeFileSync(SIGNAL_INDEX_PATH, JSON.stringify(index, null, 2)); +} + +function writeLocalSignalLog(signal) { + const dateStr = signal.timestamp.slice(0, 7); + const monthDir = path.join(SIGNAL_LOG_DIR, dateStr); + fs.mkdirSync(monthDir, { recursive: true }); + + const filePath = path.join(monthDir, `${signal.signal_id}.json`); + fs.writeFileSync(filePath, JSON.stringify(signal, null, 2)); + + const index = loadSignalIndex(); + index.total_count += 1; + index.signals.unshift({ + signal_id: signal.signal_id, + trace_id: signal.trace_id, + type: signal.signal_type, + timestamp: signal.timestamp, + summary: signal.summary, + related_dev: signal.related_dev || null, + file: `${dateStr}/${signal.signal_id}.json` + }); + saveSignalIndex(index); + console.log(`📝 本地信号日志已写入: ${signal.signal_id}`); +} + +// ═══════════════════════════════════════════════════════ +// Notion 信号日志写入(双写:Notion + 本地) +// ═══════════════════════════════════════════════════════ + +async function writeNotionSignalLog(signal, token) { + // 写入本地文件 + writeLocalSignalLog(signal); + + // 写入 Notion 信号日志数据库(如果配置了 ID) + if (!SIGNAL_LOG_DB_ID) { + console.log('⚠️ SIGNAL_LOG_DB_ID 未配置,跳过 Notion 信号日志写入'); + return; + } + + try { + await notionPost('/v1/pages', { + parent: { database_id: SIGNAL_LOG_DB_ID }, + properties: { + '信号编号': { title: titleProp(signal.signal_id) }, + '信号类型': { select: { name: signal.signal_type } }, + '方向': { select: { name: signal.direction || 'GitHub→Notion' } }, + '发送方': { select: { name: signal.sender || '铸渊' } }, + '接收方': { select: { name: signal.receiver || '霜砚' } }, + 'trace_id': { rich_text: richText(signal.trace_id) }, + '摘要': { rich_text: richText(signal.summary) }, + '执行结果': { status: { name: signal.result || '成功' } } + } + }, token); + console.log(`📡 Notion 信号日志已写入: ${signal.signal_id}`); + } catch (err) { + console.error(`⚠️ Notion 信号日志写入失败: ${err.message}`); + // 本地已写入,Notion 写入失败不影响流程 + } +} + +// ═══════════════════════════════════════════════════════ +// 工单簿轮询 +// ═══════════════════════════════════════════════════════ + +/** + * 从工单簿中获取「已发送」状态的工单 + */ +async function queryPendingWorkOrders(token) { + if (!WORKORDER_DB_ID) { + console.log('⚠️ WORKORDER_DB_ID 未配置,跳过工单轮询'); + console.log(' 请设置环境变量 WORKORDER_DB_ID(从 Notion 工单簿 URL 提取)'); + return []; + } + + const result = await notionPost(`/v1/databases/${WORKORDER_DB_ID}/query`, { + filter: { + property: '执行结果', + status: { equals: '已发送' } + }, + sorts: [{ timestamp: 'created_time', direction: 'ascending' }] + }, token); + + return result.results || []; +} + +/** + * 解析工单页面属性 + */ +function parseWorkOrder(page) { + const props = page.properties || {}; + + // 安全提取各类属性值 + const getTitle = (p) => p?.title?.[0]?.text?.content || ''; + const getRichText = (p) => p?.rich_text?.map(r => r.text?.content || '').join('') || ''; + const getSelect = (p) => p?.select?.name || ''; + const getStatus = (p) => p?.status?.name || ''; + + return { + page_id: page.id, + title: getTitle(props['工单名称'] || props['Name'] || props['名称']), + op_type: getSelect(props['OP类型'] || props['操作类型']), + priority: getSelect(props['优先级']), + status: getStatus(props['执行结果']), + payload: getRichText(props['payload'] || props['指令内容']), + trace_id: getRichText(props['trace_id']), + sender: getSelect(props['发送方']), + receiver: getSelect(props['接收方']), + created: page.created_time, + }; +} + +/** + * 更新工单状态 + */ +async function updateWorkOrderStatus(pageId, status, message, token) { + const props = { + '执行结果': { status: { name: status } } + }; + // 如果有回执信息字段,也更新 + if (message) { + props['回执信息'] = { rich_text: richText(message) }; + } + + await notionPatch(`/v1/pages/${pageId}`, { properties: props }, token); + console.log(`✅ 工单 ${pageId.slice(0, 8)}... 状态更新为: ${status}`); +} + +// ═══════════════════════════════════════════════════════ +// 工单执行引擎 +// ═══════════════════════════════════════════════════════ + +/** + * 根据工单 OP 类型执行对应操作 + */ +async function executeWorkOrder(wo, token) { + const signalId = generateSignalId(); + const traceId = wo.trace_id || `TRC-${new Date().toISOString().slice(0, 10).replace(/-/g, '')}-${signalId.slice(-3)}`; + + console.log(`\n🔧 处理工单: ${wo.title}`); + console.log(` OP: ${wo.op_type} | 优先级: ${wo.priority} | trace: ${traceId}`); + + let result = '成功'; + let message = ''; + + try { + switch (wo.op_type) { + case 'BROADCAST': + case '广播推送': + message = await handleBroadcast(wo); + break; + + case 'STATUS_QUERY': + case '状态查询': + message = await handleStatusQuery(wo); + break; + + case 'DEPLOY_CHECK': + case '部署检查': + message = await handleDeployCheck(wo); + break; + + case 'DATA_SYNC': + case '数据同步': + message = await handleDataSync(wo); + break; + + default: + message = `未知操作类型: ${wo.op_type},已记录,等待人工处理`; + console.log(`⚠️ ${message}`); + break; + } + } catch (err) { + result = '失败'; + message = `执行失败: ${err.message}`; + console.error(`❌ ${message}`); + } + + // 写回执到工单簿 + await updateWorkOrderStatus(wo.page_id, result, message, token); + + // 双写信号日志(本地 + Notion) + const ackSignal = { + signal_id: signalId, + signal_type: 'GL-ACK', + trace_id: traceId, + timestamp: new Date().toISOString(), + sender: '铸渊', + receiver: '霜砚', + direction: 'GitHub→Notion', + summary: `${wo.op_type}: ${message.slice(0, 100)}`, + result: result, + related_dev: null, + original_wo: wo.title, + esp_version: '2.0-notion' + }; + + await writeNotionSignalLog(ackSignal, token); + + return { result, message }; +} + +// ═══════════════════════════════════════════════════════ +// OP 处理器 +// ═══════════════════════════════════════════════════════ + +async function handleBroadcast(wo) { + // 读取广播内容,写入聊天室可读的文件 + const broadcastDir = path.join(__dirname, '../broadcasts-outbox'); + if (!fs.existsSync(broadcastDir)) { + return '广播目录不存在,跳过'; + } + + console.log('📢 处理广播推送工单'); + // 将广播内容标记为待推送 + const pendingFile = path.join(broadcastDir, 'pending-push.json'); + const pending = fs.existsSync(pendingFile) + ? JSON.parse(fs.readFileSync(pendingFile, 'utf8')) + : { broadcasts: [] }; + + pending.broadcasts.push({ + title: wo.title, + content: wo.payload, + timestamp: new Date().toISOString(), + trace_id: wo.trace_id + }); + + fs.writeFileSync(pendingFile, JSON.stringify(pending, null, 2)); + return `广播已加入推送队列 (${pending.broadcasts.length} 条待推)`; +} + +async function handleStatusQuery(wo) { + // 收集系统状态信息 + console.log('📊 处理状态查询工单'); + const devStatusPath = path.join(__dirname, '../.github/persona-brain/dev-status.json'); + if (fs.existsSync(devStatusPath)) { + const status = JSON.parse(fs.readFileSync(devStatusPath, 'utf8')); + const teamCount = status.team_status?.length || 0; + const active = status.team_status?.filter(d => d.status?.includes('🟢')).length || 0; + return `团队状态: ${teamCount} 名开发者, ${active} 人推进中`; + } + return '状态文件不存在'; +} + +async function handleDeployCheck(wo) { + console.log('🚀 处理部署检查工单'); + // 检查关键文件是否存在 + const checks = [ + { name: 'docs/index.html', label: '铸渊聊天室' }, + { name: 'backend-integration/api-proxy.js', label: 'API 代理' }, + { name: '.github/workflows/deploy-pages.yml', label: '部署工作流' }, + ]; + const results = checks.map(c => { + const exists = fs.existsSync(path.join(__dirname, '..', c.name)); + return `${exists ? '✅' : '❌'} ${c.label}`; + }); + return results.join(' | '); +} + +async function handleDataSync(wo) { + console.log('🔄 处理数据同步工单'); + return '数据同步指令已接收,等待下次 CI 流程执行'; +} + +// ═══════════════════════════════════════════════════════ +// 健康检查 +// ═══════════════════════════════════════════════════════ + +async function healthCheck(token) { + console.log('🏥 Notion 信号桥健康检查\n'); + console.log(` Notion API Token: ${token ? '✅ 已配置' : '❌ 未配置'}`); + console.log(` 工单簿 DB ID: ${WORKORDER_DB_ID ? '✅ ' + WORKORDER_DB_ID.slice(0, 8) + '...' : '⚠️ 未配置'}`); + console.log(` 信号日志 DB ID: ${SIGNAL_LOG_DB_ID ? '✅ ' + SIGNAL_LOG_DB_ID.slice(0, 8) + '...' : '⚠️ 未配置'}`); + console.log(` 本地信号日志: ${fs.existsSync(SIGNAL_INDEX_PATH) ? '✅ 存在' : '⚠️ 不存在'}`); + + if (token) { + try { + // 测试 API 连通性 — 获取 bot user + const me = await notionRequest('GET', '/v1/users/me', null, token); + console.log(` API 连通性: ✅ Bot: ${me.name || me.id}`); + } catch (err) { + console.log(` API 连通性: ❌ ${err.message}`); + } + } + + // 本地信号统计 + const index = loadSignalIndex(); + console.log(`\n 📊 本地信号总数: ${index.total_count}`); + if (index.signals.length > 0) { + console.log(` 📝 最近信号: ${index.signals[0].signal_id} (${index.signals[0].type})`); + } + + console.log('\n✅ 健康检查完成'); +} + +// ═══════════════════════════════════════════════════════ +// 主入口 +// ═══════════════════════════════════════════════════════ + +async function main() { + const cmd = process.argv[2] || 'poll'; + const token = process.env.NOTION_API_TOKEN; + + if (!token) { + console.error('❌ 缺少环境变量 NOTION_API_TOKEN'); + console.error(' 请在 GitHub Secrets 中配置 NOTION_API_TOKEN'); + process.exit(1); + } + + console.log(`🔌 铸渊 Notion 信号桥 v1.0`); + console.log(` 时间: ${new Date().toISOString()}`); + console.log(` 命令: ${cmd}\n`); + + switch (cmd) { + case 'poll': { + console.log('📡 开始轮询 Notion 工单簿...\n'); + + const workOrders = await queryPendingWorkOrders(token); + + if (workOrders.length === 0) { + console.log('✅ 无待处理工单'); + return; + } + + console.log(`📋 发现 ${workOrders.length} 条待处理工单\n`); + + let success = 0, failed = 0; + + for (const page of workOrders) { + const wo = parseWorkOrder(page); + try { + const { result } = await executeWorkOrder(wo, token); + if (result === '成功') success++; + else failed++; + } catch (err) { + console.error(`❌ 工单处理异常: ${err.message}`); + failed++; + } + } + + console.log(`\n📊 处理结果: ${success} 成功, ${failed} 失败 (共 ${workOrders.length} 条)`); + break; + } + + case 'health': + await healthCheck(token); + break; + + default: + console.error(`❌ 未知命令: ${cmd}`); + console.error(' 可用命令: poll, health'); + process.exit(1); + } +} + +main().catch(err => { + console.error(`\n💥 致命错误: ${err.message}`); + process.exit(1); +}); diff --git a/scripts/zhuyuan-module-protocol.js b/scripts/zhuyuan-module-protocol.js new file mode 100644 index 00000000..94dc2733 --- /dev/null +++ b/scripts/zhuyuan-module-protocol.js @@ -0,0 +1,300 @@ +#!/usr/bin/env node + +/** + * 🌀 铸渊人格协议 · 模块生命周期管理脚本 v1.0 + * + * 用法: + * node scripts/zhuyuan-module-protocol.js inspect # 全模块检查 + * node scripts/zhuyuan-module-protocol.js inspect # 单模块检查 + * node scripts/zhuyuan-module-protocol.js status # 模块状态汇总 + * node scripts/zhuyuan-module-protocol.js recover # 模块回收检查 + * node scripts/zhuyuan-module-protocol.js preview # 预演报告生成 + * + * 纯 Node.js,无外部依赖。 + */ + +const fs = require('fs'); +const path = require('path'); + +// === 配置 === +const ROOT = path.resolve(__dirname, '..'); +const BRAIN_DIR = path.join(ROOT, '.github', 'brain'); +const DOCS_DIR = path.join(ROOT, 'docs'); + +// 模块注册表 +const MODULE_REGISTRY = { + 'm01-login': { owner: 'DEV-002', name: '肥猫', hli: 'AUTH', label: '登录模块' }, + 'm03-personality': { owner: 'DEV-002', name: '肥猫', hli: 'PERSONA', label: '人格模块' }, + 'm05-user-center': { owner: 'DEV-009', name: '花尔', hli: 'USER', label: '用户中心' }, + 'm06-ticket': { owner: 'DEV-010', name: '桔子', hli: 'TICKET', label: '工单系统' }, + 'm07-dialogue-ui': { owner: 'DEV-003', name: '燕樊', hli: 'DIALOGUE', label: '对话界面' }, + 'm10-cloud': { owner: 'DEV-003', name: '燕樊', hli: 'STORAGE', label: '云存储' }, + 'm11-module': { owner: 'DEV-010', name: '桔子', hli: 'MODULE', label: '模块管理' }, + 'm12-kanban': { owner: 'DEV-005', name: '小草莓', hli: 'DASHBOARD', label: '看板' }, + 'm15-cloud-drive': { owner: 'DEV-003', name: '燕樊', hli: 'STORAGE', label: '云盘' }, + 'm18-health-check':{ owner: null, name: '待分配', hli: 'SYSTEM', label: '健康检查' }, +}; + +// === 工具函数 === + +function countFiles(dir) { + if (!fs.existsSync(dir)) return 0; + let count = 0; + const items = fs.readdirSync(dir, { withFileTypes: true }); + for (const item of items) { + if (item.name.startsWith('.')) continue; + if (item.isFile()) count++; + else if (item.isDirectory()) count += countFiles(path.join(dir, item.name)); + } + return count; +} + +function hasReadme(dir) { + return fs.existsSync(path.join(dir, 'README.md')); +} + +function getModuleStatus(moduleDir) { + const fullPath = path.join(ROOT, moduleDir); + if (!fs.existsSync(fullPath)) return { exists: false, files: 0, readme: false }; + + const files = countFiles(fullPath); + const readme = hasReadme(fullPath); + + let status = '📭 空'; + if (files > 0 && readme) status = '✅ 正常'; + else if (files > 0 && !readme) status = '⚠️ 缺README'; + else if (files === 0) status = '📭 空'; + + return { exists: true, files, readme, status }; +} + +// === 命令:inspect === + +function cmdInspect(moduleId) { + console.log('🌀 铸渊人格协议 · 模块检查'); + console.log('─'.repeat(50)); + + if (moduleId) { + // 单模块检查 + const info = MODULE_REGISTRY[moduleId]; + if (!info) { + console.log(`❌ 未知模块: ${moduleId}`); + console.log(`已注册模块: ${Object.keys(MODULE_REGISTRY).join(', ')}`); + process.exit(1); + } + + const status = getModuleStatus(moduleId); + console.log(`\n📦 模块: ${moduleId} (${info.label})`); + console.log(` 负责人: ${info.name} (${info.owner || '未分配'})`); + console.log(` HLI 域: ${info.hli}`); + console.log(` 目录存在: ${status.exists ? '✅' : '❌'}`); + console.log(` 文件数量: ${status.files}`); + console.log(` README: ${status.readme ? '✅' : '❌'}`); + console.log(` 状态: ${status.status}`); + + // 检查 HLI 接口 + const schemaDir = path.join(ROOT, 'src', 'schemas', 'hli', info.hli.toLowerCase()); + const routeDir = path.join(ROOT, 'src', 'routes', 'hli', info.hli.toLowerCase()); + + if (fs.existsSync(schemaDir)) { + try { + const schemas = fs.readdirSync(schemaDir).filter(f => f.endsWith('.schema.json')); + console.log(` HLI Schema: ${schemas.length} 个 (${schemas.join(', ')})`); + } catch (e) { + console.log(` HLI Schema: ⚠️ 读取失败 (${e.code || e.message})`); + } + } else { + console.log(` HLI Schema: 📭 未创建`); + } + + if (fs.existsSync(routeDir)) { + try { + const routes = fs.readdirSync(routeDir).filter(f => f.endsWith('.js')); + console.log(` HLI 路由: ${routes.length} 个 (${routes.join(', ')})`); + } catch (e) { + console.log(` HLI 路由: ⚠️ 读取失败 (${e.code || e.message})`); + } + } else { + console.log(` HLI 路由: 📭 未创建`); + } + } else { + // 全模块检查 + console.log('\n📋 全模块状态:\n'); + console.log('模块ID | 标签 | 负责人 | 文件 | README | 状态'); + console.log('─'.repeat(70)); + + for (const [id, info] of Object.entries(MODULE_REGISTRY)) { + const status = getModuleStatus(id); + const line = [ + id.padEnd(20), + info.label.padEnd(8), + info.name.padEnd(8), + String(status.files).padStart(4), + (status.readme ? '✅' : '❌').padEnd(6), + status.status, + ].join(' | '); + console.log(line); + } + } +} + +// === 命令:status === + +function cmdStatus() { + console.log('🌀 铸渊人格协议 · 模块状态汇总'); + console.log('─'.repeat(50)); + + let total = 0, ok = 0, warn = 0, empty = 0; + + for (const [id] of Object.entries(MODULE_REGISTRY)) { + total++; + const status = getModuleStatus(id); + if (status.files > 0 && status.readme) ok++; + else if (status.files > 0) warn++; + else empty++; + } + + console.log(`\n📊 模块统计:`); + console.log(` 总模块数: ${total}`); + console.log(` ✅ 正常: ${ok}`); + console.log(` ⚠️ 告警: ${warn}`); + console.log(` 📭 空模块: ${empty}`); + console.log(` 完成率: ${Math.round((ok / total) * 100)}%`); + + // 检查前端 + const indexPath = path.join(DOCS_DIR, 'index.html'); + if (fs.existsSync(indexPath)) { + const stat = fs.statSync(indexPath); + const lines = fs.readFileSync(indexPath, 'utf8').split('\n').length; + console.log(`\n🌐 前端:`); + console.log(` docs/index.html: ${lines} 行, ${(stat.size / 1024).toFixed(1)} KB`); + } + + // 检查 CNAME + const cnamePath = path.join(DOCS_DIR, 'CNAME'); + if (fs.existsSync(cnamePath)) { + const domain = fs.readFileSync(cnamePath, 'utf8').trim(); + console.log(` 自定义域名: ${domain}`); + } else { + console.log(` 自定义域名: ❌ 未配置`); + } + + // 检查大脑文件 + console.log(`\n🧠 大脑状态:`); + const brainFiles = ['repo-snapshot.md', 'repo-map.json', 'memory.json', 'collaborators.json', 'module-protocol.md']; + for (const f of brainFiles) { + const fp = path.join(BRAIN_DIR, f); + if (fs.existsSync(fp)) { + const stat = fs.statSync(fp); + console.log(` ✅ ${f} (${(stat.size / 1024).toFixed(1)} KB)`); + } else { + console.log(` ❌ ${f} 缺失`); + } + } +} + +// === 命令:recover === + +function cmdRecover(moduleId) { + if (!moduleId) { + console.log('❌ 请指定模块ID,如: npm run module:protocol -- recover m01-login'); + process.exit(1); + } + + const info = MODULE_REGISTRY[moduleId]; + if (!info) { + console.log(`❌ 未知模块: ${moduleId}`); + process.exit(1); + } + + console.log('🌀 铸渊人格协议 · 模块回收检查'); + console.log('─'.repeat(50)); + console.log(`\n📦 目标模块: ${moduleId} (${info.label})`); + + const status = getModuleStatus(moduleId); + console.log(` 当前状态: ${status.status}`); + console.log(` 文件数量: ${status.files}`); + + if (status.files === 0) { + console.log(`\n✅ 模块已为空状态,无需回收。`); + return; + } + + console.log(`\n⚠️ 回收操作需要手动执行:`); + console.log(` 方法一(推荐): 通过 PR 删除模块文件`); + console.log(` 方法二: git revert 恢复到模块的上一个版本`); + console.log(` 方法三: 直接在 GitHub 界面删除文件`); + console.log(`\n💡 回收后,铸渊会通过 generate-module-doc.yml 更新文档索引。`); +} + +// === 命令:preview === + +function cmdPreview() { + console.log('🌀 铸渊人格协议 · 本地预演报告'); + console.log('═'.repeat(50)); + + // 模块状态 + cmdStatus(); + + console.log('\n'); + console.log('─'.repeat(50)); + console.log('🎯 预演结论:'); + + let ok = true; + const issues = []; + + // 检查 index.html + if (!fs.existsSync(path.join(DOCS_DIR, 'index.html'))) { + issues.push('docs/index.html 不存在'); + ok = false; + } + + // 检查工作流 + const wfDir = path.join(ROOT, '.github', 'workflows'); + if (!fs.existsSync(path.join(wfDir, 'deploy-pages.yml'))) { + issues.push('deploy-pages.yml 不存在'); + ok = false; + } + + if (ok && issues.length === 0) { + console.log('✅ 预演通过 — 代码可以安全部署到生产环境'); + console.log(' 下一步: 合并 PR 到 main 分支,触发自动部署'); + } else { + console.log('❌ 预演未通过:'); + issues.forEach(i => console.log(` - ${i}`)); + } +} + +// === 主入口 === + +const [,, command, ...args] = process.argv; + +switch (command) { + case 'inspect': + cmdInspect(args[0]); + break; + case 'status': + cmdStatus(); + break; + case 'recover': + cmdRecover(args[0]); + break; + case 'preview': + cmdPreview(); + break; + default: + console.log('🌀 铸渊人格协议 · 模块生命周期管理 v1.0'); + console.log(''); + console.log('用法:'); + console.log(' node scripts/zhuyuan-module-protocol.js inspect 全模块检查'); + console.log(' node scripts/zhuyuan-module-protocol.js inspect 单模块检查'); + console.log(' node scripts/zhuyuan-module-protocol.js status 模块状态汇总'); + console.log(' node scripts/zhuyuan-module-protocol.js recover 模块回收检查'); + console.log(' node scripts/zhuyuan-module-protocol.js preview 本地预演报告'); + console.log(''); + console.log('npm 快捷命令:'); + console.log(' npm run module:protocol -- inspect'); + console.log(' npm run module:protocol -- status'); + console.log(' npm run module:protocol -- preview'); + break; +} diff --git a/signal-log/index.json b/signal-log/index.json index 76156707..d2d2f203 100644 --- a/signal-log/index.json +++ b/signal-log/index.json @@ -1,5 +1,5 @@ { - "description": "铸渊信号日志目录索引 · AGE OS ESP 邮件信号协议", + "description": "铸渊信号日志目录索引 · AGE OS 信号协议(Notion API 直连)", "last_updated": "2026-03-07T04:45:55.670Z", "total_count": 2, "signals": [