guanghulab/.github/brain/handoff/batons/baton-004-PR4-portal-frontend.md
Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

8.1 KiB

Baton-004 · PR-4 · 双层 Web Portal

触发口令: 铸渊。第 4 棒。开发授权。 上一棒: baton-003-PR3-autodl-inference.md 下一棒: baton-005-PR5-secrets-vault.md 共振因果链: cc-002 (前端不传 system) + cc-004 (中文 UI)

走完路再看 (强制)

.github/brain/bingshuo-language-core/walk-the-path.md

自检 3 题

  1. 冰朔说"光湖只有真实, 没有提示词". 前端 chat.js 调推理后端, messages 数组里第一条应该是什么?

    参考答案cc-002: 第一条直接是 user. 不是 system. 前端不传, 后端不补, 推理端剥离 — 三道关。
  2. 2C2G 内存只够 portal 吃 ~600MB. SSR (Next.js) 还是静态 + Express?

    参考答案静态 + Express. SSR 渲染会爆内存. 静态 HTML + 客户端渲染 + Express 只做 API + SSE 代理。
  3. 冰朔切到"现实开发层" tab, 右栏要展示模块注册表. 数据从哪儿来? 直接从 GitHub API 拉?

    参考答案不从 GitHub API. 国内 2C2G 访问 GitHub API 不稳, 且会暴露主权. 直接读本地 git 镜像 (PR-6 的 forgejo) 里的 .github/brain/architecture/function-manifest.json. PR-4 阶段 forgejo 还没装, 先用每次部署时打包到本地的快照 /data/guanghulab/portal/data/manifest-snapshot.json。

上一棒交付了什么 (验证用)

ls server/inference-agent/   # 期望: server.py setup-inference.sh detect-gpu.sh tune-inference.sh fetch-models.sh
ls .github/workflows/refresh-autodl-endpoint.yml
grep -A2 'refresh-autodl-endpoint.yml' scripts/preflight/cn-isolation-allowlist.json | grep '"exists": true'

这一棒要做的事

A. 前端 (frontend/lighthouse-portal/)

  • 不用 Next.js, 不用 SSR. 静态 HTML + vanilla JS (或 Vue 3 CDN 版, ESM)
  • 思源黑体 (本地字体, 不走 Google Fonts CDN)
  • 三栏布局:
    • 左栏: 新建对话 / 历史列表 (从 /api/conversations 拉)
    • 中栏: chat 区域 + 顶部 tab [光湖语言人格层] [现实开发层], 切 tab = 调 POST /api/active-model {name}
    • 右栏: 看模式分:
      • 人格层 → /api/persona-db (复用 persona-brain-db/ 已建数据, 只读卡片)
      • 开发层 → /api/manifest (本地 manifest 快照, 只读卡片)
  • chat 走 SSE, byte-pipe 转发 (不重组, cc-002 落地)
  • 每次发送, 前端只传 messages: [{role: "user", content: "..."}] — 永远不带 system

B. 后端 (server/portal/)

  • Express 单进程, pm2 起一份
  • SQLite (/data/guanghulab/portal/data/conversations.sqlite):
    • conversations(id, title, active_model, created_at, updated_at)
    • messages(id, conv_id, role, content, ts) · role 永远只有 user/assistant, 不存 system
  • 路由:
    • GET /api/conversations 列表
    • POST /api/conversations 新建
    • GET /api/conversations/:id/messages
    • POST /api/chat SSE → 转发到 inference-endpoint.json 里的 AutoDL /v1/chat/completions
    • GET /api/active-model 当前
    • POST /api/active-model {name: "mother"|"coder"}
    • GET /api/persona-db 只读
    • GET /api/manifest 只读
  • 启动时读 /data/guanghulab/portal/data/inference-endpoint.json (PR-3 写的)

C. nginx 改造

  • bootstrap 已经反代 443 → 127.0.0.1:3000 (portal)
  • 静态资源 (字体/JS/CSS) 直接 nginx 服务, 不走 Express, 减负

D. ecosystem.config.js (pm2)

  • 单 instance, max_memory_restart=512M (内存上限保护)
  • 日志走 /data/guanghulab/_logs/portal-*.log

给冰朔的中文回执模板

✅ 第 4 棒已合 · 双层 Web Portal

· frontend/lighthouse-portal/ 静态前端 (思源黑体 · 三栏布局 · 中文)
· server/portal/ Express 后端 (SQLite 对话历史 · SSE 字节管道转发 AutoDL · pm2 守护)
· nginx 反代静态 + API, 严禁 system prompt 三道关 (前端不传 / 后端不补 / 推理剥)
· 切 tab = 切模型, 切模型 = 调 AutoDL switch-model, 右栏跟 tab 联动

冰朔体验:
1. 打开 https://guanghulab.com 直接进
2. 左栏新建对话 / 历史
3. 中栏切 tab: 人格层 (母模型 + 右栏数据库) 或 开发层 (编程模型 + 右栏模块注册)
4. 中文输入, 字节流回, 真实人格

下一棒口令:
铸渊。第 5 棒。开发授权。

已交付 (本棒落地回执 · 2026-05-09)

由第 4 棒铸渊填写 · 给下一棒铸渊验证用:

交付物 路径 验证方式
静态前端壳 frontend/lighthouse-portal/index.html 三栏布局 · 顶栏健康胶囊 · tab[人格层/开发层] · 中文 + 思源黑体本地回退栈 (无 CDN)
前端样式 frontend/lighthouse-portal/assets/style.css 月白色 + 深湛蓝配色 · 响应式 (≤900px 隐藏左右栏) · streaming 状态光标
前端逻辑 frontend/lighthouse-portal/assets/app.js fetch + ReadableStream 解析 OpenAI 兼容 SSE · payload 永远只有 [{role:"user", content}] (cc-002 第一道关) · 切 tab → POST /api/active-model · 30s 心跳
Portal 后端入口 server/portal/server.js Express 5 兜不动, 走 4.21 (与 server/app 对齐) · /api/* 路由 + /_static/ 兜底 · 中文 4xx/5xx body
推理客户端 server/portal/lib/inference-client.js 热加载 inference-endpoint.json (mtime 变就重 parse · cc-003) · _strip_system_messages (cc-002 第二道关) · pipeChat 字节管道 + 旁路解析 fullText 落库
路由 server/portal/routes/{conversations,chat,active-model,persona-db,manifest}.js 全部路由 + 中文错误信息 · conv id 校验 ^conv_[0-9a-f]{16}$ · 单条消息 8K 上限 · 推理端没起时 /api/active-model 本地仍生效 (cc-004 不让用户做无用功)
SQLite schema server/portal/db/init.js role CHECK IN ('user','assistant') · 不存 system (cc-002 数据层兜底) · WAL 模式
pm2 配置 server/portal/ecosystem.config.js single instance · max_memory_restart=512M (2C2G 内存约束) · max_restarts=20
测试 server/portal/tests/{strip-system,routes-smoke}.test.js npm test PASS 15/15 · cc-002 回归 + 路由冒烟 (含"前端塞 system 后端剥掉"用例)
文档 server/portal/README.md + frontend/lighthouse-portal/README.md 中文 · 因果链落地表 · 部署/调试章节
nginx 模板 server/setup/domain-cn/nginx/guanghulab.conf.template 静态走 /_static/ alias · / 反代 portal · proxy_buffering off (SSE 必需 · cc-002 字节直发)
主权登记激活 .github/brain/architecture/function-manifest.json ZY-SVR-CN01 status bootstrapping → active · template_version 0.2.0 → 0.3.0 · 新增 ZY-FN-PORTAL + M-PORTAL-API + M-PORTAL-FRONTEND · validate.js EXIT=0 errors=0 warnings=0
路线图刷新 .github/brain/handoff/pr-roadmap.md PR-4 状态 🟡 (合并后由下一棒改 )
隔离守卫 scripts/preflight/cn-isolation-allowlist.json 不需要新增 yml, PR-4 没有新 workflow (复用 PR-2 deploy-domain-server.yml 走 rsync) · check-server-isolation.js EXIT=0

关键设计决策 (与 baton-004 原计划的偏差):

  • 没有用 Vue 3 CDN, 也没用 ESM: 评估了一下, Vue 3 的 reactive watcher 在 2C2G 上一旦多对话状态来回切容易 GC 抖, vanilla JS 200 行就够。CDN 也违 cc-001 (不依赖境外资源)。
  • 依赖只有 express 4.21 + better-sqlite3 11: 与 server/app/package.json 完全对齐 (避免双版本 better-sqlite3 编译占内存)。advisory db 已扫, 0 漏洞。
  • 不直接拉 GitHub API 喂右栏: baton-004 自检 3 已经说了。/api/manifest 优先读 PORTAL_DATA_DIR 的 snapshot, 回退仓库内 .github/brain/architecture/function-manifest.json。PR-6 forgejo 装好后改成读 forgejo。
  • /api/active-model 切换失败时本地不回滚: cc-004 — 推理端可能没起 (AutoDL 关机), 用户切 tab 应该立即生效, 等推理端起来再连。如果回滚, 用户每次切 tab 都失败。
  • chat 路由旁路解析 SSE 落库: byte-pipe 不重组, 但需要保存 assistant 消息。在 pipeChat 内部边写边解析 (失败不影响 pipe), 流结束后拿 fullText 落库。即使中间断流, 已收到的字也保留。