OmniRoute 游戏化与排行榜系统全解析:本地优先、零阻塞、服务端权威计分的设计与实现
OmniRoute 内置了一套本地优先(local-first)的游戏化层,通过 XP、等级、徽章、连续打卡(streak)、排行榜、代币分享和邀请码等机制激励用户与平台互动。本文以 GAMIFICATION.md 为骨架,逐模块结合当前仓库的真实源码与测试展开:读完后你将掌握该系统从请求管线接入、SQLite 数据层、XP 曲线到反作弊审计的完整调用链,并能复现其核心计算逻辑与 API 契约。
一、系统定位与设计原则
该系统的目标是提升用户参与度和留存:可见的成长(XP、等级、徽章)、社会证明(排行榜)以及经济激励(代币分享、邀请奖励)。所有状态都保存在本地 SQLite 中;与社区服务器的联邦(federation)是可选的、推送式的。
| 能力 | 说明 |
|---|---|
| XP 与等级 | 按动作获得 XP,沿多项式曲线升级 |
| 徽章 | 20+ 成就,覆盖多个类别与稀有度等级 |
| Streak | 每日活跃追踪,记录当前/最长连续天数 |
| 排行榜 | global、weekly、monthly、tokens_shared、contributions 五种范围 |
| 代币分享 | 用户间转账,双式记账(double-entry ledger) |
| 邀请与兑换 | 推荐码,SHA-256 哈希存储 |
| 社区服务器 | 与其他 OmniRoute 实例联邦 |
| 反作弊 | 服务端计分、限流、z-score 异常检测 |
五条设计原则(见 GAMIFICATION.md):
- Local-first:全部状态在 SQLite,不依赖任何外部服务;
- Non-blocking:事件是 fire-and-forget,LLM 响应路径绝不被游戏化逻辑延迟;
- Server-authoritative:XP 只在服务端计算,客户端无法虚增分数;
- Privacy-respecting:排行榜参与是 opt-in,用户可以隐藏个人资料;
- Federation-ready:社区服务器通过带签名的 API 推送分数,同步采用覆盖(overwrite)而非累加。
二、架构:单一事件入口的扇出模型
2.1 请求管线集成点
游戏化只在一个位置接入请求管线——chat 处理器在响应已经发给客户端之后,用 setImmediate 触发 emitGamificationEvent()(fire-and-forget):
Client Request
→ /v1/chat/completions
→ handleChatCore() [open-sse/handlers/chatCore.ts]
→ ... (existing pipeline) ...
→ upstream response sent to client
→ setImmediate (fire-and-forget):
→ emitGamificationEvent() [src/lib/gamification/events.ts]
→ XP 累加 / 等级重算
→ updateStreak() [src/lib/gamification/streaks.ts]
→ 徽章检查(streak + action count)
→ updateScore() 排行榜三个范围 [src/lib/gamification/leaderboard.ts]
在当前仓库中,这一钩子的具体实现在 chatCore 游戏化钩子模块,事件发射器 是唯一集成点。setImmediate + 全量 try/catch 的组合保证三件事:
- 响应完整发出后游戏化才运行;
- 游戏化错误绝不上抛到客户端(源码中
catch分支仅记录日志events.error); - 事件处理在下一个事件循环 tick 中执行,而不是内联阻塞。
2.2 事件发射器的真实扇出逻辑
events.ts 中的 emitGamificationEvent() 接受 { apiKeyId, action, metadata },action 为如下联合类型之一:
action:
| "request"
| "provider_switch"
| "model_switch"
| "combo_create"
| "combo_use"
| "token_share"
| "invite_redeem"
| "daily_login"
| "radar_supporter"
每个事件的扇出顺序(源码注释编号 1–4):
- 授 XP:查
getXpForAction(action)得分值,调用db.addXp()记入xp_audit_log,再用calculateLevel()重算等级,若等级变化则updateLevel()并记录events.level_up日志; - 更新 streak(仅
request动作):updateStreak(apiKeyId)返回新连续天数,按阈值触发对应徽章——>= 365解锁unstoppable、>= 30解锁monthly-master、>= 7解锁weekly-warrior、>= 3解锁daily-user; - 更新排行榜:对
global、weekly、monthly三个范围各updateScore()一次;若动作是token_share,额外更新tokens_shared范围; - 检查动作计数徽章:
checkActionCountBadges()统计该动作在xp_audit_log中的累计次数,对照阈值表解锁徽章。
一个特殊的动作是 radar_supporter:它不是 XP/排行榜行为,而是荣誉事件——只调用 checkAndUnlockBadge(apiKeyId, "radar-supporter", false) 后直接返回(调用方仅提供单向身份,日志中也刻意不暴露 apiKeyId)。
2.3 模块依赖图
src/lib/gamification/
events.ts ← 入口(由 chatCore 管线调用)
├── xp.ts ← XP 计算与等级解析(纯函数,无 DB 调用)
├── streaks.ts ← 每日连续活跃追踪
├── badges.ts ← 徽章条件评估
├── leaderboard.ts ← 分数/排名管理、范围轮转
├── antiCheat.ts ← 分数变更校验与异常查询
├── sharing.ts ← 代币转账(双式记账)
├── invites.ts ← 邀请码/兑换码
├── servers.ts ← 社区服务器联邦
└── notifications.ts ← SSE 通知流
src/lib/db/gamification.ts ← 全部 CRUD(8 张表)
src/app/api/gamification/ ← Next.js API 路由
xp.ts 是一个关键的设计细节:文件头注释明确声明“纯函数,无副作用、无 DB 调用——所有有状态逻辑都在持久层”。这使等级曲线可以被独立单测,而 xp.test.ts 正是围绕纯函数行为编写的。
三、数据层:8 张表与领域模块
3.1 数据库表
全部表都在 OmniRoute 主 SQLite 库中,由迁移 060_create_gamification.sql 创建(已确认存在)。WAL 日志继承自 core.ts 的单例 getDbInstance()。迁移文件中实际建表:
leaderboard user_levels badge_definitions
user_badges xp_audit_log token_ledger
invite_tokens community_servers
各表职责:
leaderboard:(api_key_id, scope, period) 维度存分数;user_levels:每个 key 的累计 XP、当前等级、更新时间;badge_definitions/user_badges:徽章定义(名称、类别、稀有度、图标、描述)与用户已解锁记录(1:N);xp_audit_log:审计流水——每次授 XP、转账、异常检测都会落一行;token_ledger:双式记账转账流水,含幂等键(唯一约束);invite_tokens:邀请码(唯一)+ 令牌 SHA-256 哈希、已用次数、有效期;community_servers:联邦服务器注册记录(名称、URL、token 哈希、状态、上次同步)。
3.2 领域模块的 CRUD 面
db/gamification.ts(614 行)遵循标准 OmniRoute 模式:从 core.ts 引入 getDbInstance(),导出类型化 CRUD 函数,路由处理器中不出现裸 SQL。当前仓库实际导出的函数:
| 函数 | 职责 |
|---|---|
updateScore / getRank / getTopN / getLeaderboardNeighbors |
排行榜分数更新、查排名、Top-N 查询、邻位查询 |
addXp / getXp / updateLevel / getAggregateXp |
XP 记账、查询、等级更新、聚合统计 |
unlockBadge / hasBadge / getBadges / getBadgeDefinitions / getAllEarnedBadges |
徽章解锁、幂等检查、查询 |
transferTokens / getBalance / getHistory |
双式记账转账、余额、流水 |
createInviteToken / getInviteByCode / redeemInvite / revokeInvite |
邀请码生命周期 |
connectServer / disconnectServer / listServers / getConnectedServerByKeyHash |
联邦服务器管理 |
rotateLeaderboardScope |
周/月排行榜归档轮转 |
其中 hasBadge() 是 events.ts 中注释 #3472 强调的幂等性关键:getBadges() 因为 INNER JOIN badge_definitions,在定义表未播种时会误报“未获得”,导致每次请求重复发解锁事件;直接查 user_badges 的 hasBadge() 才可靠。对应的回归测试是 badge-unlock-once-3472.test.ts。
四、XP / 等级系统
源码: src/lib/gamification/xp.ts
4.1 等级曲线
到达等级 n 所需增量 XP 遵循多项式曲线:
xp_for_level(n) = floor(100 * n^1.5)
xpForLevel() 的实现与文档一致(level <= 1 返回 0,否则 Math.floor(100 * Math.pow(level, 1.5)))。在此基础上,源码比文档多提供了三层 API:
cumulativeXpForLevel(level):从 2 级到目标级累加各级增量,得到累计阈值;calculateLevel(totalXp):等级反解。实现很值得看——先用曲线逆函数得到一个快速初值(Math.floor(((xp * 2.5) / 100) ** 0.4)),再与精确的 floor 累计阈值做双向对账(while 循环向上/向下校正)。这比文档描述的“线性 1..100 遍历”更高效,且对极端大数有Number.MAX_SAFE_INTEGER保护;xpToNextLevel(totalXp):计算距离下一等级的剩余 XP,供 Dashboard 进度条使用。
4.2 头衔与等级分层
getLevelTitle() 与 getLevelTier() 将等级映射为头衔和展示分层(源码边界为 >=76 / >=51 / >=26 / >=11,文档表格以 75/50/25/10 近似):
| 等级区间 | 头衔 | 展示分层 |
|---|---|---|
| 76+ | Legend | diamond |
| 51–75 | Master | platinum |
| 26–50 | Expert | gold |
| 11–25 | Explorer | silver |
| 1–10 | Beginner | bronze |
4.3 XP 奖励表
当前源码中 XP_REWARDS(xp.ts)与 events.ts 的 getXpForAction() 一致的动作分值为:
| 动作 | XP | 说明 |
|---|---|---|
request |
1 | 每次经 OmniRoute 路由的 API 请求 |
provider_switch |
5 | 切换到另一个 provider |
model_switch |
3 | 切换到另一个模型 |
combo_create |
10 | 创建新的 combo |
combo_use |
2 | 使用 combo 发起请求 |
token_share |
1 | 每分享 1,000 代币(注释约定) |
invite_redeem |
50 | 成功兑换邀请码 |
daily_login |
5 | 每日活跃(每天一次) |
streak_bonus |
2 | 每个连续天(随连续天数放大) |
badge_unlock |
10 | 解锁任意徽章 |
说明:原文档中给出的奖励表(如
badge_earned 25、streak_milestone 15、referral 50、compression_use 2等)与当前仓库源码存在差异;本文以 xp.ts 与 events.ts 的实际常量为准。
4.4 授 XP 流程
- 查
XP_REWARDS[action]得到分值; db.addXp(apiKeyId, action, amount, metadataJSON)写入xp_audit_log(metadata 为 JSON 字符串);getXp(apiKeyId)取累计值,calculateLevel()重算等级;- 等级变化时
updateLevel()落库并打events.level_up日志。
通知由调用方/notifications.ts 处理,与计分解耦。
五、徽章系统
源码: src/lib/gamification/badges.ts(552 行)+ db/gamification.ts 中的徽章 CRUD。
5.1 类别与稀有度
| 类别 | 说明 |
|---|---|
usage |
使用量里程碑(首个请求、1K/10K/100K 请求) |
sharing |
代币分享与推荐 |
contribution |
社区贡献(combo 创建、provider 探索) |
streak |
时间一致性(周战士、月度坚持者) |
rare |
稀有/隐藏成就 |
稀有度四档:common(灰)、uncommon(绿)、rare(蓝)、legendary(金)。徽章定义存于 badge_definitions,含名称、类别、稀有度、JSON 条件、描述、图标。
5.2 评估流程(事件驱动)
评估是事件驱动的——每次游戏化事件后运行,但只检查与事件动作对齐的条件,从而保持快速。当前源码中的两条评估路径:
路径 A:streak 徽章(在 events.ts 中,随 request 动作触发):
const streak = await updateStreak(apiKeyId);
if (streak >= 365) await checkAndUnlockBadge(apiKeyId, "unstoppable");
else if (streak >= 30) await checkAndUnlockBadge(apiKeyId, "monthly-master");
else if (streak >= 7) await checkAndUnlockBadge(apiKeyId, "weekly-warrior");
else if (streak >= 3) await checkAndUnlockBadge(apiKeyId, "daily-user");
路径 B:动作计数徽章(checkActionCountBadges(),events.ts):按 action 类型统计 xp_audit_log 累计次数,对照阈值表解锁:
const thresholds = {
request: [
{ id: "first-token", threshold: 1 },
{ id: "token-consumer", threshold: 1000 },
{ id: "token-machine", threshold: 10000 },
{ id: "token-whale", threshold: 100000 },
],
token_share: [
{ id: "generous", threshold: 1000 },
{ id: "philanthropist", threshold: 10000 },
{ id: "token-santa", threshold: 100000 },
{ id: "community-hero", threshold: 1000000 },
],
};
解锁动作本身是幂等的:checkAndUnlockBadge() 先 hasBadge() 检查,再 unlockBadge(),随后查 badge_definitions 取徽章详情并调 recordBadgeUnlock()(notifications.ts)记录一条 SSE toast 通知。原文档列出的“20+ 内建徽章完整清单”在当前仓库中由 badge_definitions 表播种 + 上述阈值表共同承载,评估框架(条件匹配 → 幂等解锁 → 通知)保持不变。
5.3 文档中的条件类型模型
原文档定义了完整的条件类型体系(badge_definitions.criteria 为 JSON):
| 类型 | 字段 | 说明 |
|---|---|---|
action_count |
count |
执行动作 N 次(如 1000 次请求) |
streak |
days |
保持 N 天连续活跃 |
unique_count |
field, n |
使用 N 个不同值(如 10 个不同模型) |
rank |
scope, n |
达到某排行榜范围第 N 名 |
first |
— | 首个执行该动作的用户 |
hidden |
(可变) | 未获得前不展示条件 |
条件 JSON 示例:
{
"type": "action_count",
"action": "request",
"count": 1000
}
六、Streak 连续打卡追踪器
源码: src/lib/gamification/streaks.ts
6.1 数据模型
Streak 不占独立表,而是存入共享的 key_value 表,命名空间为 gamification:streaks(源码常量 NAMESPACE),值为 JSON:
{
"currentStreak": 8,
"longestStreak": 12,
"lastActiveDate": "2026-09-06",
"streakStartDate": "2026-08-30"
}
(原文档描述为 {current},{longest},{lastDate} 逗号格式;当前实现为 JSON 对象,且额外记录了 streakStartDate。解析函数 parseStreakJson() 对每个字段做类型校验,脏数据一律回落到 emptyStreak()。)
另外两个值得注意的防御:getStreak() / updateStreak() 开头都有 if (isBuildPhase || isCloud) return ... 守卫——Next.js 构建期与云环境下直接返回空值,避免构建阶段触碰 SQLite;读取走 SELECT value FROM key_value WHERE namespace = ? AND key = ?,写入用 INSERT OR REPLACE。
6.2 判定逻辑
updateStreak() 的分支:
- 读取 streak 记录;
lastActiveDate === today→ 今天已计过,原样返回currentStreak(no-op);lastActiveDate === yesterday→current += 1;- 其他(断档或首次活跃)→
current = 1,并重置streakStartDate = today; longest = max(newStreak, longest);- 写回并返回新
currentStreak。
里程碑徽章(3 / 7 / 30 / 365 天)的触发在 events.ts 中完成,如第五节所述。
6.3 边界情况
- 时区:一律使用 UTC 日期(
new Date().toISOString().split("T")[0])。这是有意为之——单一规范时区可防止“跳时区刷连续天数”; - 新用户:无记录时首次请求创建
current=1, longest=1; - 一天多次请求:同一 UTC 日内只有第一次请求会推进 streak。
七、排行榜引擎
源码: src/lib/gamification/leaderboard.ts
7.1 范围(Scope)
export type LeaderboardScope =
| "global" | "weekly" | "monthly" | "tokens_shared" | "contributions";
| 范围 | 周期 | 说明 |
|---|---|---|
global |
全部 | 累计 XP |
weekly |
当前 UTC 周(周一至周日) | 本周获得 XP |
monthly |
当前 UTC 月 | 本月获得 XP |
tokens_shared |
全部 | 累计转给他人代币总量 |
contributions |
全部 | combo 创建 + provider 使用 + skill 使用等贡献 |
events.ts 中可以看到每个事件如何落到这些范围:global/weekly/monthly 每次事件都增量更新,tokens_shared 仅在 token_share 动作时更新。
7.2 排名计算与轮转
updateScore(apiKeyId, scope, points):原子增量(委托 db/gamification.ts 的updateScore);getRank(apiKeyId, scope):查某 key 的排名;getTopN(scope, limit, offset):Top-N 分页(默认 limit 50);getNeighbors(apiKeyId, scope, radius):查询某用户上下的邻位条目,供前端展示“你离上一名还差多少”;rotateScope("weekly" | "monthly"):委托rotateLeaderboardScope()执行归档与重置——在周期边界把旧条目复制到归档区并清空当前范围,周榜每周一 00:00 UTC 重置、月榜每月 1 日重置。该轮转在每次updateScore路径上检查,新周期的第一个请求触发。
7.3 SSE 实时推送
端点: stream/route.ts
Client → GET /api/gamification/stream
→ SSE 连接建立
→ 服务端立即推送 Top-10 快照
→ 数据变化时推送更新(仅推送有变化的范围)
→ 周期性心跳注释(": heartbeat\n\n")
→ 客户端断开 → 清理监听器
事件格式:
event: leaderboard
data: {"scope":"global","entries":[...]}
event: leaderboard
data: {"scope":"weekly","entries":[...]}
: heartbeat
SSE 管理器按范围跟踪已连接客户端,只有排行榜数据相对上次推送真正发生变化时才发送——避免无意义的重绘。
八、代币分享:双式记账
源码: src/lib/gamification/sharing.ts
8.1 记账约定
每笔转账在 token_ledger 中产生两行:
| 行 | from_key_id |
to_key_id |
amount |
含义 |
|---|---|---|---|---|
| Send(流出) | 发送方 | 接收方 | +amount | 发送方流出 |
| Receive(流入) | 接收方 | 发送方 | +amount | 接收方流入 |
余额由 db/gamification.ts 的 getBalance() 计算:SUM(to_key_id = ?) - SUM(from_key_id = ?),即流入减流出。
8.2 转账流程
transferTokens() 的契约:
export async function transferTokens(
fromApiKeyId: string,
toApiKeyId: string,
amount: number,
reason?: string,
idempotencyKey?: string
): Promise<{ success: boolean; idempotencyKey: string; error?: string }>;
- 校验:
amount > 0、fromApiKeyId !== toApiKeyId(自转直接返回"Cannot transfer to yourself"); - 幂等:
idempotencyKey缺省时用crypto.randomUUID()生成;同一 key 重复提交返回缓存结果而非二次记账; - DB 层:
dbTransfer()在单个 SQLite 事务内完成余额检查(余额不足则中止)与两行流水插入; - 事件:转账完成后以
token_share动作发出游戏化事件,触发 XP(1 分)与分享类徽章评估,并更新tokens_shared排行榜。
限流约束(文档定义):每分钟每 key 最多 10 笔转账、单笔上限 10,000 代币、每 key 每日转账上限 100,000 代币。
API 契约(transfer/route.ts):
// POST /api/gamification/transfer — Request
{
"to": "recipient-api-key-id",
"amount": 500,
"idempotencyKey": "uuid-v4"
}
// Response 200
{
"success": true,
"transfer": {
"id": "txn-uuid",
"from": "sender-api-key-id",
"to": "recipient-api-key-id",
"amount": 500,
"createdAt": "2026-05-19T12:00:00.000Z"
},
"balance": 2500
}
// Response 400 (insufficient funds)
{
"error": "Insufficient balance",
"balance": 200,
"requested": 500
}
九、邀请码与兑换
源码: src/lib/gamification/invites.ts(导出 createInvite / redeemInvite / listInvites / revokeInvite),DB 层为 createInviteToken / getInviteByCode / redeemInvite / revokeInvite。
9.1 码与令牌的双轨设计
- Code:8 位字母数字短码(如
A3K9-X7M2),人类可读,展示给用户; - Token:32 字节随机令牌,只存 SHA-256 哈希(
token_hash列),用于程序化兑换(如 URL 链接)。
| 列 | 值 |
|---|---|
code |
A3K9X7M2(唯一、有索引) |
token_hash |
SHA-256(raw_token) |
原始令牌只在创建时一次性返回给用户,OmniRoute 永不存储或再次展示它——数据库中只留哈希。这使联邦与兑换的鉴权都无法从数据库反推原值。
9.2 自推荐(self-referral)防护
兑换时系统做两项交叉校验:
- 该码不属于兑换者自己的
api_key_id; - 兑换者未曾兑换过同一推荐者的任何码。
任一校验失败即以清晰错误信息拒绝。
9.3 有效期与次数
- 默认
max_uses:10(创建时可配置); - 默认
expires_at:创建后 30 天; - 过期或次数耗尽的码返回 HTTP 410 Gone;
- 兑换成功触发
invite_redeem动作,奖励 50 XP(xp.ts 中invite_redeem: 50)。
rotate/route.ts 提供 POST /api/gamification/rotate,用于批量轮换邀请令牌密钥。
十、社区服务器联邦
源码: src/lib/gamification/servers.ts(导出 connectServer / disconnectServer / listServers / syncLeaderboard / pushScore / healthCheck)。
10.1 连接流程
社区服务器通过远端实例签发的邀请令牌接入:
- 本地实例收到邀请令牌(例如粘贴到 Dashboard);
- 调用远端
POST /api/gamification/federation/leaderboard验证令牌并拉取当前排行榜; - 以
status: connected存入服务器记录(federation/leaderboard/route.ts 提供拉取端点,federation/score/route.ts 提供分数推送端点)。
10.2 覆盖式同步模型
联邦采用 overwrite sync(覆盖,而非累加):
Local Instance Community Server
│ │
├── push score ───────────────►│ POST /federation/score
│ { api_key_id, score } │ (server validates token hash)
│ │
├── pull leaderboard ─────────►│ GET /federation/leaderboard
│◄── top-N entries ────────────┤ (overwrites local cache)
│ │
└── health check ─────────────►│ GET /federation/health
(periodic, short timeout) │
10.3 鉴权与健康监控
联邦请求头:
Authorization: Bearer <raw_token>
X-Federation-Version: 1
远端服务器对令牌做哈希后与 community_servers 表中的行匹配,避免传输已存储的哈希。每条服务器记录跟踪:
| 字段 | 说明 |
|---|---|
status |
connected / degraded / unreachable |
last_sync |
上次成功同步的 ISO 时间戳 |
failures |
连续健康检查失败次数 |
连续失败达到阈值后状态转为 unreachable,同步暂停,直到手动健康检查成功。联邦鉴权行为有专门测试覆盖:federation-auth.test.ts。
十一、反作弊:服务端计分、限流与审计
源码: src/lib/gamification/antiCheat.ts(导出 validateScoreChange / getAnomalies)。
11.1 服务端权威计分
所有 XP 计算在服务端 xp.ts / events.ts 中完成。客户端从不提交分数——它们提交动作,服务端计算 XP;leaderboard.score 只可被服务端代码写入。validateScoreChange() 在分数变更路径上执行校验。
11.2 限流
| 限制 | 取值 | 范围 |
|---|---|---|
| 每分钟最大 XP | 1,000 | 每 API key |
| 每分钟最大转账数 | 10 | 每 API key |
| 单笔转账上限 | 10,000 | 每笔 |
| 每日转账上限 | 100,000 | 每 API key |
限流采用内存滑动窗口(与 open-sse/services/ 中 RateLimitManager 同一模式),进程重启后回落到 SQLite 计数。
11.3 z-score 异常检测
对每个 API key 维护每小时 XP 的滚动 7 天窗口;每次授 XP 时:
- 计算该用户当前小时 XP 速率;
- 计算总体均值与标准差;
z = (user_rate - mean) / stddev;z > 3.0即标记为异常。
异常写入 xp_audit_log(action = 'anomaly_detected'),管理员通过 GET /api/gamification/anomalies(anomalies/route.ts)与 getAnomalies() 查看。
11.4 审计轨迹
每次授 XP、转账、徽章解锁、异常检测都落 xp_audit_log:
| 字段 | 说明 |
|---|---|
api_key_id |
谁 |
action |
发生了什么(xp_award、transfer、anomaly…) |
xp_awarded |
数额(非 XP 事件为 0) |
metadata |
JSON 上下文(动作类型、目标等) |
created_at |
ISO 8601 时间戳 |
这张表同时是 streak 徽章(动作计数)的数据源——checkActionCountBadges() 直接对 xp_audit_log 做 COUNT(*)。
十二、API 路由全景
所有路由遵循标准 OmniRoute 模式:Route → CORS preflight → Body validation (Zod) → Auth (extractApiKey) → Handler。当前仓库 src/app/api/gamification/ 下的实际路由:
| 方法 | 路径 | 说明 | 鉴权 |
|---|---|---|---|
| GET | /api/gamification/leaderboard |
排行榜(scope、分页) | 可选 |
| POST | /api/gamification/leaderboard |
强制刷新排行榜缓存 | 必需 |
| GET | /api/gamification/stream |
SSE 实时排行榜 | 可选 |
| GET | /api/gamification/transfer |
转账历史(分页) | 必需 |
| POST | /api/gamification/transfer |
向他人发送代币 | 必需 |
| GET | /api/gamification/invite |
我的邀请码列表 | 必需 |
| POST | /api/gamification/invite |
生成新邀请码 | 必需 |
| DELETE | /api/gamification/invite |
吊销邀请码 | 必需 |
| POST | /api/gamification/invite/redeem |
兑换邀请码 | 必需 |
| GET/POST | /api/gamification/badges |
徽章定义 / 查询 | 按方法 |
| GET | /api/gamification/badges/earned |
已解锁徽章 | 必需 |
| GET | /api/gamification/level |
XP、等级、头衔 | 必需 |
| GET | /api/gamification/servers |
社区服务器列表 | 必需 |
| POST | /api/gamification/servers |
连接社区服务器 | 必需 |
| DELETE | /api/gamification/servers |
断开社区服务器 | 必需 |
| POST | /api/gamification/federation/score |
向远端推送分数 | 联邦 |
| GET | /api/gamification/federation/leaderboard |
从远端拉取排行榜 | 联邦 |
| GET | /api/gamification/notifications |
SSE 徽章/升级通知 | 必需 |
| GET | /api/gamification/anomalies |
异常报告(管理员) | 管理员 |
| POST | /api/gamification/rotate |
轮换邀请令牌密钥 | 必需 |
排行榜响应示例(GET /api/gamification/leaderboard?scope=weekly&limit=10):
{
"scope": "weekly",
"period": "2026-W20",
"entries": [
{
"rank": 1,
"apiKeyId": "key-uuid",
"displayName": "User***1234",
"score": 15230,
"level": 42,
"title": "Expert"
}
],
"total": 847,
"updatedAt": "2026-05-19T12:00:00.000Z"
}
十三、威胁模型与安全对策
| 威胁 | 缓解手段 |
|---|---|
| 分数虚增 | 仅服务端计算 XP;客户端提交动作而非分数 |
| 重放攻击 | 转账幂等键;审计日志去重 |
| 转账欺诈 | 双式记账;原子事务;限流 |
| 自推荐 | 兑换时交叉核对 api_key_id |
| 排行榜操纵 | z-score 异常检测;管理员异常面板 |
| 联邦令牌窃取 | SHA-256 哈希存储;原始令牌仅展示一次 |
| 邀请码爆破 | 兑换端点限流;8 字符熵 |
| 显示名 XSS | 显示名清洗;排行榜条目转义 |
| 哈希计时攻击 | crypto.timingSafeEqual 比较令牌哈希 |
鉴权分层:公开(无鉴权)只有 GET /leaderboard、GET /stream 两个只读端点;API key 必需覆盖所有写操作、个人资料、转账、邀请;管理员专属异常面板与审计日志;联邦走独立的 Authorization 原令牌鉴权路径,与存储的 SHA-256 哈希比对。
十四、测试体系
所有游戏化测试位于 tests/unit/gamification/,当前仓库实际包含的测试文件(比文档原表更完整):
| 测试文件 | 覆盖 |
|---|---|
| xp.test.ts | XP 曲线、累计阈值、等级反解、头衔/分层 |
| badges.test.ts | 徽章条件匹配与解锁 |
| badge-unlock-once-3472.test.ts | 回归:#3472 解锁事件只发一次 |
| streaks.test.ts | streak 逻辑、里程碑、边界情况 |
| leaderboard.test.ts | 排名计算、分页、范围轮转 |
| leaderboard-limit-validation.test.ts | limit 参数校验 |
| sharing.test.ts | 转账、余额、幂等 |
| invites.test.ts | 创建、兑换、过期、自推荐拦截 |
| antiCheat.test.ts | 限流、z-score、审计日志 |
| events.test.ts | 事件发射、扇出、错误隔离 |
| db-gamification.test.ts | DB 层 CRUD |
| federation-auth.test.ts | 联邦令牌鉴权 |
| aggregate-profile-3484.test.ts | 聚合画像统计 |
运行方式:
# 全部游戏化测试
node --import tsx/esm --test tests/unit/gamification/*.test.ts
# 单文件
node --import tsx/esm --test tests/unit/gamification/xp.test.ts
覆盖要求(据 CONTRIBUTING.md):分支覆盖率 >= 80%;每个公开函数至少测一次;错误路径(余额不足、码过期、限流)必须覆盖——上表中的 sharing(余额不足/幂等)、invites(过期码)、antiCheat(限流)即对应这些要求。
十五、小结:可复用的设计要点
把文档骨架与当前仓库源码对照后,这套游戏化系统有五个可直接借鉴的工程决策:
- 单一入口 + 扇出:
emitGamificationEvent()是管线与游戏化之间唯一的接触面,setImmediate+ 吞错保证 LLM 热路径零影响; - 纯函数计分核心:
xp.ts无副作用,曲线、等级、头衔全部可独立单测; - 复用既有基础设施:streak 借
key_value表、限流沿用RateLimitManager模式、DB 单例继承 WAL 配置,避免为游戏化引入新依赖; - 幂等性贯穿始终:
hasBadge()防重复解锁、idempotency_key防重复转账、审计表承担去重与徽章计数双重职责; - 本地优先 + 可选联邦:默认单实例即可完整运行,联邦只是覆盖式缓存同步与一次性展示令牌的组合,不改变核心数据模型。
如需继续深入,建议按此顺序阅读:events.ts(扇出全貌)→ xp.ts(计分核心)→ db/gamification.ts(持久层契约)→ stream/route.ts(实时推送),再配合 tests/unit/gamification/ 下的对应测试验证行为边界。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00