12 KiB
12 KiB
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.ts(1241 行 — trackBeacon + 3 个 claim action:grantBeaconReward/claimWeeklyBeacon/claimTimedBeacon/claimChainReward)/home/z/my-project/Caddyfile(:81 网关,按 ?XTransformPort=* 转发到对应端口)/home/z/my-project/examples/websocket/server.ts+frontend.tsx(mini-service + 前端连接参考)/home/z/my-project/.zscripts/dev.sh(mini-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-service(mini-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/leaderboardbody{entry}→ schema 校验 → 推入 → 排序 →{entries, total, rank}GET /api/leaderboard/stats→{totalSubmissions, uniquePlayers, topScore}
- OPTIONS 预检由 hono/cors 自动处理,返回 204 + 完整 CORS 头
- 端口固定 3030,
export 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=1(reparent 到 init),进程脱离原 bash session,uptime 4+ 分钟稳定
2. beacon.ts 扩展(1161 → 1290 行,+129 行)
三个 claim 函数返回值扩展(非破坏性):
claimBeaconReward返回值新增entry: BeaconScoreEntryclaimWeeklyReward返回值新增entry: BeaconScoreEntryclaimTimedReward返回值新增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=3030submitCloudScore(entry): Promise<number>— POST,成功返回 rank ≥ 1,失败返回 -1,自动记录 tsfetchCloudStats(): Promise<CloudStatsResponse | null>- 所有 fetch 用相对路径 + ?XTransformPort=3030,禁止 localhost:3030
- try-catch 包裹,失败时静默返回空/null,不抛异常
3. gameStore.ts 集成
- import 新增
submitCloudScore claimWeeklyBeaconaction:领奖成功后void submitCloudScore(res.entry)(fire-and-forget,不 await,不阻塞)claimTimedBeaconaction:同上claimBeaconReward(daily) 由 BeaconPanel 直接调用,不通过 gameStore
4. BeaconPanel.tsx 重写(953 → 1170 行,+217 行)
新增 import:
fetchCloudLeaderboard/submitCloudScore/loadLastCloudSubmitTimestampfrom beacon- 4 个 lucide 图标:
RefreshCw/Globe/WifiOff/Loader2 useReffrom react
新增状态:
lbTab: 'local' | 'global'(默认 local)cloudEntries: BeaconScoreEntry[]cloudLoading: booleancloudError: string | nullcloudFetched: booleanmySubmitTs: numbercloudFetchingRef(防并发)
新增逻辑:
refreshCloudLeaderboardcallback:拉取云端榜 + 防并发 + 空 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 专属刷新按钮(RefreshCw,loading 时 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) - 加粗排名
- emerald 边框 (
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内容完整呈现;四色规范应用,视觉区分度高;信息层级合理"
修改的文件
mini-services/leaderboard-service/package.json(新建)mini-services/leaderboard-service/index.ts(新建,182 行)mini-services/leaderboard-service/README.md(新建)src/lib/game/beacon.ts— 1161 → 1290 行(+129 行:3 个 claim 返回 entry + 云排行榜模块)src/store/gameStore.ts— 1235 → 1252 行(+17 行:import + 2 个 claim action 加 submitCloudScore)src/components/game/BeaconPanel.tsx— 953 → 1170 行(+217 行:双 tab UI + 云端拉取 + 提交 + YOU 高亮)eslint.config.mjs— ignores 新增 "mini-services/**"
不破坏的现有功能
- 本地排行榜(
loadLeaderboard/pushLeaderboardEntry)完整保留作为离线 fallback - 日挑战 / 周挑战 / 限时挑战 / 信标链流程全部保留
- 现有 localStorage keys(echo-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,用于全球榜高亮)
四色全息规范遵循
- 本地 tab:amber (#fbbf24)
- 全球 tab:emerald (#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)
- 前端访问端口:用户通过 Preview Panel 看到的是 Caddy 网关 :81,不是 Next.js :3000。所以
fetch('/api/leaderboard?XTransformPort=3030')在用户视角下能正常路由。如果直接访问 :3000 会 404(这是预期行为)。 - mini-service 启动方式:用
bun index.ts(无 --hot)+ 双 fork( ... & )模式启动。bun --hot在沙盒下不稳定。 - mini-service 已被 .zscripts/dev.sh 自动扫描:下次容器重启会自动启动。但当前会话需手动启动一次。
- localStorage key 命名:
echo-nexus-beacon-cloud-last-submit-v1而非echo-nexus-beacon-cloud-lb-v1,前者存的是单个 timestamp(用于高亮),后者若需要存全球榜快照可用后者(目前未实现,按需拉取)。 - star-tide-service 端口 3031:另一个 P3 工单已存在的服务,本服务的端口 3030 与之互不冲突。