Files
echo-nexus/agent-ctx/10-b-full-stack-developer.md
T
2026-06-24 00:41:29 +00:00

12 KiB
Raw Blame History

Task 10-b · full-stack-developer · P2/#4 云排行榜 mini-service

本文件为本 agent 在 Task 10-b 的工作记录,供后续 agent 查阅。

任务概述

为「回响星核 / Echo Nexus」v0.8.2 实现 P2/#4 工单:把信标本机排行榜(纯 localStorage)升级为云端 Top100,让玩家间可以比较分数。

阅读的前置工作

  • /home/z/my-project/worklog.md(v0.8.1 项目状态,10 大系统,四色全息规范)
  • /home/z/my-project/src/lib/game/beacon.ts(实际 1161 行 — worklog 信息滞后,v0.8.2 已加限时挑战 claimTimedReward
  • /home/z/my-project/src/components/game/BeaconPanel.tsx(实际 953 行 — 已含限时挑战 amber 主题)
  • /home/z/my-project/src/store/gameStore.ts1241 行 — trackBeacon + 3 个 claim actiongrantBeaconReward/claimWeeklyBeacon/claimTimedBeacon/claimChainReward
  • /home/z/my-project/Caddyfile:81 网关,按 ?XTransformPort=* 转发到对应端口)
  • /home/z/my-project/examples/websocket/server.ts + frontend.tsxmini-service + 前端连接参考)
  • /home/z/my-project/.zscripts/dev.shmini-service 自动扫描启动逻辑,发现 mini-services 目录下子服务自动 bun run dev
  • /home/z/my-project/agent-ctx/9-b-full-stack-developer.md(前一个 agent 的信标系统扩展记录)
  • /home/z/my-project/mini-services/star-tide-service/(已存在的 P3 #10-c 服务,端口 3031

实现细节

1. mini-servicemini-services/leaderboard-service/

目录结构

mini-services/leaderboard-service/
├── package.json    # 独立 bun 项目,type=module
├── index.ts        # Hono 入口(182 行)
├── bun.lock        # 自动生成
├── node_modules/   # hono 独立安装
└── README.md       # API 文档

package.json

  • "dev": "bun --hot index.ts"(规范要求的热重载脚本)
  • "start": "bun index.ts"(无热重载,用于稳定运行)
  • 依赖:hono ^4.6.14

index.ts 设计要点

  • Hono + cors() 中间件,origin: * 全开放(前端跨端口必须)
  • 内存数组 entries: BeaconScoreEntry[],最多 1000 条,按 score 降序
  • 同分排序:先看用时短,再看提交早
  • 防刷:lastSubmitAt: Map<dateKey|challenge, ts>,10 秒内只接受 1 次,返回 429 + retryAfterMs
  • 4 个路由:
    • GET / → 健康检查 {service, version, ok, uptime}
    • GET /api/leaderboard → Top100 {entries, total}
    • POST /api/leaderboard body {entry} → schema 校验 → 推入 → 排序 → {entries, total, rank}
    • GET /api/leaderboard/stats{totalSubmissions, uniquePlayers, topScore}
  • OPTIONS 预检由 hono/cors 自动处理,返回 204 + 完整 CORS 头
  • 端口固定 3030export default { port, fetch } 标准 bun 模式

启动调试经验

  • bun --hot 在本沙盒环境下不稳定,多次启动后约 5-10s 内被 kill(即使 setsid + nohup
  • 解决方案:用 bun index.ts(无 --hot+ 双 fork 模式:( bun index.ts >/tmp/log 2>&1 & )
  • 双 fork 后 PPID=1reparent 到 init),进程脱离原 bash sessionuptime 4+ 分钟稳定

2. beacon.ts 扩展(1161 → 1290 行,+129 行)

三个 claim 函数返回值扩展(非破坏性)

  • claimBeaconReward 返回值新增 entry: BeaconScoreEntry
  • claimWeeklyReward 返回值新增 entry: BeaconScoreEntry
  • claimTimedReward 返回值新增 entry: BeaconScoreEntry
  • 用途:让前端 / gameStore 能拿到完整 entry 提交到云端

新增云排行榜模块

  • BEACON_CLOUD_PORT = 3030 常量
  • BEACON_CLOUD_LAST_SUBMIT_KEY = "echo-nexus-beacon-cloud-last-submit-v1" localStorage key
  • 3 个响应接口:CloudLeaderboardResponse / CloudSubmitResponse / CloudStatsResponse
  • loadLastCloudSubmitTimestamp() / saveLastCloudSubmitTimestamp(ts)
  • fetchCloudLeaderboard(): Promise<BeaconScoreEntry[]> — GET /api/leaderboard?XTransformPort=3030
  • submitCloudScore(entry): Promise<number> — POST,成功返回 rank ≥ 1,失败返回 -1,自动记录 ts
  • fetchCloudStats(): Promise<CloudStatsResponse | null>
  • 所有 fetch 用相对路径 + ?XTransformPort=3030,禁止 localhost:3030
  • try-catch 包裹,失败时静默返回空/null,不抛异常

3. gameStore.ts 集成

  • import 新增 submitCloudScore
  • claimWeeklyBeacon action:领奖成功后 void submitCloudScore(res.entry)fire-and-forget,不 await,不阻塞)
  • claimTimedBeacon action:同上
  • claimBeaconReward (daily) 由 BeaconPanel 直接调用,不通过 gameStore

4. BeaconPanel.tsx 重写(953 → 1170 行,+217 行)

新增 import

  • fetchCloudLeaderboard / submitCloudScore / loadLastCloudSubmitTimestamp from beacon
  • 4 个 lucide 图标:RefreshCw / Globe / WifiOff / Loader2
  • useRef from react

新增状态

  • lbTab: 'local' | 'global'(默认 local
  • cloudEntries: BeaconScoreEntry[]
  • cloudLoading: boolean
  • cloudError: string | null
  • cloudFetched: boolean
  • mySubmitTs: number
  • cloudFetchingRef (防并发)

新增逻辑

  • refreshCloudLeaderboard callback:拉取云端榜 + 防并发 + 空 entries 时显示"成为首位登顶者"
  • useEffect:切到全球 tab 自动拉取(仅首次)+ 初始化读取 mySubmitTs
  • handleClaimDaily:领奖后 void submitCloudScore(res.entry).then(rank => ...) — 若用户在全球 tab 自动刷新
  • handleClaimTimed / handleClaimWeekly:领奖后更新 mySubmitTs + 若在全球 tab 自动刷新

排行榜 UI 重写为双 tab

  • 顶部 tab 切换:「本地」(amber 主题) / 「全球」(emerald 主题) — 用 <button aria-pressed> 实现可访问性
  • 全球 tab 专属刷新按钮(RefreshCwloading 时 animate-spin
  • 子标题:本地"本机 Top N · 离线可用" / 全球"全球 Top N · 已同步 / 同步中…"
  • 本地榜:保留原 v0.8.2 渲染(max-h-200px overflow-y-auto
  • 全球榜:
    • 加载中:Loader2 spinner + "正在拉取全球榜…"
    • 空榜 + 错误:WifiOff 图标 + 错误文案 + 重试按钮
    • 空榜 + 无错:Globe 图标 + "尚无全球记录。完成挑战并领取即可登顶全球榜!"
    • 有数据:渲染前 100 条,每条带排名(1-3 名用 Crown/Medal/Award 图标)/ 难度色 / WEEK·TIMED 徽章 / 用时 / 分数
    • 玩家自己的记录高亮:通过 entry.timestamp === mySubmitTs 匹配
      • emerald 边框 (border-emerald-500/40)
      • emerald 阴影 (shadow-[0_0_12px_rgba(52,211,153,0.25)])
      • YOU 徽章 (bg-emerald-500/25 text-emerald-200 border-emerald-500/50)
      • emerald 分数色 (color: #34d399)
      • 加粗排名

5. eslint 配置

  • eslint.config.mjs 新增 ignores "mini-services/**"
  • 原因:mini-service 是独立 bun 项目,有自己的 TypeScript 配置和 lint 规范,不应参与主项目 lint

QA 验证结果

1. lint

  • bun run lint 零错误零警告

2. dev 服务器

  • dev.log 全程无错误,所有编译 < 300ms
  • 注:dev.log 中显示 /api/leaderboard?XTransformPort=3030 404 是预期行为 — 通过 :3000 直接访问时 Next.js 没有 /api/leaderboard 路由;通过 :81 网关访问时 Caddy 转发到 :3030 正常返回 200

3. mini-service curl 全通过

  • GET / → 200 {"service":"echo-nexus-leaderboard","version":"v0.8.2","ok":true,"uptime":N}
  • GET /api/leaderboard (空) → 200 {"entries":[],"total":0}
  • POST /api/leaderboard → 200 {"entries":[...],"total":1,"rank":1}
  • GET /api/leaderboard (有数据) → 200 {"entries":[...],"total":N}
  • GET /api/leaderboard/stats → 200 {"totalSubmissions":N,"uniquePlayers":M,"topScore":X}
  • OPTIONS /api/leaderboard 预检 → 204 + 完整 CORS 头(Allow-Origin: *
  • 重复 POST(同 dateKey+challenge 10s 内)→ 429 {"error":"Rate limited","retryAfterMs":N}

4. 网关路由验证

  • Caddy :81 → localhost:3030 通过 ?XTransformPort=3030 正确转发,返回 JSON 200

5. agent-browser 集成测试

  • 通过 :81 端口访问(绕过 Next.js :3000 的 404
  • 信标 tab 选中(用 KeyboardEvent Enter 激活 Radix Tabs — 因 pointerdown 被覆盖层拦截)
  • 切到「全球」tab → 自动拉取 → 显示"全球 Top 6 · 已同步" → 渲染 6 条记录
  • 刷新按钮可点击,旋转动画正常
  • POST 测试:浏览器 fetch 提交 score=9999 → 返回 rank=1 → 刷新后榜首显示 10.00K
  • YOU 高亮:手动设置 localStorage[BEACON_CLOUD_LAST_SUBMIT_KEY] 后刷新 → 榜首显示 emerald 边框 + YOU 徽章

6. VLM 视觉评分(目标 ≥7/10)

  • 全球榜含 YOU 高亮:8/10
    • VLM 评语:"双tab切换清晰,全球榜记录完整,难度色与标签规范,视觉层次佳,但「YOU」徽章未突出显示"
  • 本地榜:8/10
    • VLM 评语:"双tab切换逻辑清晰,本地tab内容完整呈现;四色规范应用,视觉区分度高;信息层级合理"

修改的文件

  1. mini-services/leaderboard-service/package.json(新建)
  2. mini-services/leaderboard-service/index.ts(新建,182 行)
  3. mini-services/leaderboard-service/README.md(新建)
  4. src/lib/game/beacon.ts — 1161 → 1290 行(+129 行:3 个 claim 返回 entry + 云排行榜模块)
  5. src/store/gameStore.ts — 1235 → 1252 行(+17 行:import + 2 个 claim action 加 submitCloudScore
  6. src/components/game/BeaconPanel.tsx — 953 → 1170 行(+217 行:双 tab UI + 云端拉取 + 提交 + YOU 高亮)
  7. eslint.config.mjs — ignores 新增 "mini-services/**"

不破坏的现有功能

  • 本地排行榜(loadLeaderboard / pushLeaderboardEntry)完整保留作为离线 fallback
  • 日挑战 / 周挑战 / 限时挑战 / 信标链流程全部保留
  • 现有 localStorage keysecho-nexus-beacon-lb-v1 / -prog-v1 / -weekly-v1 / -chain-v1 / -timed-v1)全部保留
  • 主项目 package.json 不变(hono 装在 mini-service 子项目内)

新增的 localStorage keys

  • echo-nexus-beacon-cloud-last-submit-v1(上次成功提交到云端的 entry timestamp,用于全球榜高亮)

四色全息规范遵循

  • 本地 tabamber (#fbbf24)
  • 全球 tabemerald (#34d399) — 与"在线/同步"语义一致
  • 玩家高亮:emerald 边框 + emerald YOU 徽章 + emerald 分数色
  • 难度色保留:routine emerald / anomaly amber / singular rose
  • 周挑战徽章:fuchsia
  • 限时挑战徽章:amber
  • 错误状态:rose (#fb7185)
  • 零蓝色/靛色违规

截图资产

  • /home/z/my-project/agent-ctx/beacon-global-leaderboard.png — 全球榜初始视图(6 条记录)
  • /home/z/my-project/agent-ctx/beacon-global-with-you.png — 全球榜含 YOU 高亮(7 条记录,榜首是玩家)
  • /home/z/my-project/agent-ctx/beacon-local-leaderboard.png — 本地榜视图

重要注意事项(供后续 agent

  1. 前端访问端口:用户通过 Preview Panel 看到的是 Caddy 网关 :81,不是 Next.js :3000。所以 fetch('/api/leaderboard?XTransformPort=3030') 在用户视角下能正常路由。如果直接访问 :3000 会 404(这是预期行为)。
  2. mini-service 启动方式:用 bun index.ts(无 --hot+ 双 fork ( ... & ) 模式启动。bun --hot 在沙盒下不稳定。
  3. mini-service 已被 .zscripts/dev.sh 自动扫描:下次容器重启会自动启动。但当前会话需手动启动一次。
  4. localStorage key 命名echo-nexus-beacon-cloud-last-submit-v1 而非 echo-nexus-beacon-cloud-lb-v1,前者存的是单个 timestamp(用于高亮),后者若需要存全球榜快照可用后者(目前未实现,按需拉取)。
  5. star-tide-service 端口 3031:另一个 P3 工单已存在的服务,本服务的端口 3030 与之互不冲突。