OpenHuman Rewards & Referrals 完整解析:推荐奖励、优惠券兑换、Discord 社区成就与邀请码机制
【免费下载链接】openhumanOpenHuman is an open source personal AI for Mac, Windows and Linux — local-first memory, agent orchestration, and deep research.项目地址: https://gitcode.com/GitHub_Trending/op/openhuman
本文以 OpenHuman 的官方功能文档(gitbooks/features/rewards-and-referrals.md)为主线,结合仓库中 Rust 核心(src/openhuman/hosted/referral/)与 React 前端(app/src/pages/Rewards.tsx、app/src/pages/Invites.tsx、app/src/services/api/)的源码实现,系统讲解应用内“奖励与推荐”体系的三个松散耦合的成长机制:推荐计划(Referrals)、促销优惠券(Coupons)与社区奖励(Community Rewards),外加独立的邀请码(Invite Codes)管理。读完本文,你将掌握各功能面的数据模型、状态机、前后端调用链与底层 RPC 适配层原理,并能据此排查“本地会话下功能不可用”“WebView fetch 失败”“角色未分配”等实际现象。
一、功能概览:三个机制共用一个界面
OpenHuman 将三类“增长玩法”捆绑在同一个Rewards界面下,它们业务上彼此独立,只是共享一个入口:
| 机制 | 作用 |
|---|---|
| 推荐计划(Referral program) | 分享你的推荐码,好友完成转化后你获得信用额度(credit) |
| 促销优惠券(Promo coupons) | 兑换促销码获得促销信用(promotional credit),与推荐奖励相互独立 |
| 社区奖励(Community rewards) | 绑定 Discord 账号,随使用量里程碑解锁角色与奖励 |
邀请码管理则独立于 Rewards 界面,位于单独的/invites页面。
一个关键前提:以上所有功能都要求已登录的后端会话(signed-in backend session)。在纯本地会话(local-only session)下,Rewards 页面只显示一个引导登录的空状态(empty state),所有这些功能离线不可用。对应前端实现见 Rewards.tsx:当isLocalSessionToken(coreSnapshot.sessionToken)为真时,直接渲染EmptyStateCard,提示文案由 i18n 键rewards.localUnavailable提供,并引导用户跳转/settings/account。
二、Rewards 界面:三个视图与地址化导航
功能文档描述/rewards拥有三个 chip 标签页,中间默认选中Rewards(社区)。从当前仓库源码看,这一交互已演化为更彻底的形式:三个表面不再是同一页面上的标签,而是各有一个侧边栏条目(sidebar entry)的独立视图,并用 URL 参数?view=作为地址,从而支持刷新后状态保持与直接链接跳转。
// app/src/pages/Rewards.tsx type RewardsView = 'rewards' | 'referrals' | 'redeem'; const VIEWS: readonly RewardsView[] = ['rewards', 'referrals', 'redeem'] as const;| 视图 | ?view=值 | 侧边栏图标 | 对应内容 |
|---|---|---|---|
| Rewards(社区) | 缺省 /?view=rewards | Gift | Discord 连接、进度环、可解锁的社区角色 |
| Referrals(推荐) | ?view=referrals | Users | 推荐码、收益、被推荐用户活动 |
| Coupons(优惠券) | ?view=redeem | Ticket | 兑换促销码、兑换历史 |
视图切换通过setView写入 URL:view === 'rewards'时删除view参数,其他视图则写入对应值(Rewards.tsx)。未识别或缺失的?view=一律回落(fallback)到rewards,保证侧边栏始终恰好高亮一个条目。
另外,Rewards 页面注册了一个oauth:success事件监听:任何 OAuth 连接(如 Discord)完成后,深链监听器会派发该事件,页面随即静默刷新快照,使 Discord 用户名与连接状态实时更新(Rewards.tsx)。
三、Referrals 推荐计划
3.1 推荐码与分享
每个账号拥有唯一推荐码。你可以复制它,或使用Share(优先调用系统原生分享面板,失败时回退到剪贴板)发送一条预填充消息,内容包含你的推荐码与应用下载链接。
3.2 数据展示:四个磁贴 + 活动表
Referrals 标签页展示四块信息:
- 你的推荐码(code)
- 累计收益(total earned,USD)
- 待转化推荐(pending referrals)
- 已完成推荐(completed referrals)
下方是一张活动表,列出每条被推荐记录:被推荐人的掩码身份(masked identity,如j***@gmail.com)、状态徽章、奖励金额与时间戳。掩码优先取自后端下发的referredUserMasked字段,若后端只提供referredDisplayName或用户 id,前端归一化逻辑也会兜底处理(见 referralApi.ts)。
数据模型定义在 app/src/types/referral.ts:ReferralStats包含referralCode、referralLink、totals(totalRewardUsd/pendingCount/convertedCount)、referrals行数组、appliedReferralCode与canApplyReferral。
3.3 推荐状态机
| Referral 状态 | 含义 |
|---|---|
| Joined(加入) | 被推荐用户已注册但尚未转化 |
| Completed(完成) | 被推荐用户已转化,推荐奖励已入账 |
| Expired(过期) | 关系失效(预留状态,由后端驱动) |
注意 UI 层与后端的措辞差异:后端只区分pending | converted,expired为预留值;前端归一化函数把后端的joined映射为pending、completed/complete映射为converted(referralApi.ts)。因此文档表格里的 “Joined / Completed / Expired” 本质上是归一化后的 UI 状态名。
3.4 应用他人推荐码(Apply)
如果你是被他人推荐且仍符合资格的用户,界面会显示一个apply 表单让你输入对方的推荐码。资格判断(canApplyReferral)完全由后端决定——典型规则是:尚未订阅、且尚未应用过任何推荐码的用户才有资格。一旦应用成功,表单会被“已关联推荐码”的确认信息替换。
金额、转化规则与资格判定全部服务端强制(server-side enforced)。桌面端核心在这里只是一个薄适配层(thin adapter)。
3.5 底层原理:referral 域是“无状态 RPC 适配器”
从源码结构看,referral 域(src/openhuman/hosted/referral/)不持有任何业务逻辑、状态或自有 schema——它只是一个无状态 RPC 适配器,用带认证的reqwest请求调用托管后端(hosted backend)的/referral/*接口,并把原始data负载原样返回给 CLI / JSON-RPC 客户端。
它之所以存在,是因为桌面 WebView 的fetch到后端可能抛出笼统的 “Load failed”(源于 CORS / TLS / WebKit 限制),因此这些调用复用了与 billing 域相同的服务端reqwest路径。这一点在 ops.rs 的模块注释和 README.md 中均有明确说明。
对外暴露两个 RPC 方法(注册见 src/core/all.rs):
| RPC 方法 | 后端调用 | 用途 | 输入 |
|---|---|---|---|
openhuman.referral_get_stats(即referral.get_stats) | GET /referral/stats | 拉取推荐码、链接、总额与被推荐人列表 | 无 |
openhuman.referral_claim(即referral.claim) | POST /referral/claim | 应用推荐码(可带设备指纹作为滥用信号) | code(必填)、deviceFingerprint(可选) |
RPC schema 定义于 schemas.rs:referral_get_stats输出stats(JSON,来自后端data字段);referral_claim输出result(JSON),入参通过ReferralClaimParams以 camelCase 反序列化(device_fingerprint字段带#[serde(default)])。未知方法名会返回一个unknown占位 schema 且只含error输出。
关键实现细节(ops.rs):
require_token私有助手从凭据库读取会话令牌:先get_session_token,然后trim并拒绝空串,否则fail closed,返回错误"no backend session token; run auth_store_session first"——两个方法在没有会话时都以此方式失败。get_stats通过effective_backend_api_url(&config.api_url)解析有效后端地址,构造BackendOAuthClient后执行authed_json(GET, "/referral/stats")。claim_referral构造请求体时会对code做trim,对deviceFingerprint同样 trim 并丢弃纯空白字符串后才转发;schema handler(handle_referral_claim)侧也做了一遍防御性的重复过滤。
前端 referralApi.ts 通过callCoreCommand('openhuman.referral_get_stats')/callCoreCommand('openhuman.referral_claim', { code, deviceFingerprint })调用上述 RPC。设备指纹由 deviceFingerprint.ts 提供:以openhuman_device_fingerprint_v1为键存入localStorage,优先使用crypto.randomUUID(),不可用时回退到时间戳 + 随机串,是一个稳定的匿名标识(stable anonymous id),供后端做滥用信号分析。
四、Coupons 优惠券兑换
Coupons 标签页用于兑换**促销码(promo codes)**换取促销信用,与推荐奖励相互独立。界面包含两块统计磁贴:
- 促销信用余额(promo credit balance,USD)
- 已兑换码数量(count of redeemed codes)
输入一个码并兑换即可;兑换结果要么立即生效(applied),要么在依赖后续操作时先被接受为pending(待定)。下方是一张近期兑换记录表,列出每个码、奖励金额、状态与兑换时间。
| Coupon 状态 | 含义 |
|---|---|
| Applied(已应用) | 已兑现——信用已进入你的账户 |
| Pending action(待触发) | 条件性优惠券,等待某个触发动作 |
| Redeemed(已兑换) | 已被接受,但尚未兑现 |
五、Community Rewards 与 Discord
5.1 进度环与角色奖励
Rewards(社区)标签页将使用量“游戏化”。一个**进度环(progress ring)**展示你已解锁成就数占总成就数的比例;**角色与奖励(roles & rewards)**列表则描述每个里程碑(部分里程碑附带可选的 USD 信用)。页面底部用状态徽章(当前连续天数 current streak、累计 token 数)汇总你的活动。
5.2 围绕 Discord 的三个操作
奖励以Discord 角色的形式发放,因此该标签页围绕“绑定 Discord 账号”构建:
- Connect Discord:执行 OAuth 授权流程(
openhuman.auth.oauth_connect,provider 为discord);成功后快照刷新并显示你的 Discord 用户名。 - Join Discord:打开社区服务器邀请链接。
- Disconnect:解除账号绑定(清除存储的 Discord ID,操作幂等)。
从 rewardsApi.ts 可以看到对应的 HTTP 接口:GET /rewards/me拉取快照(15 秒超时,REWARDS_SNAPSHOT_TIMEOUT_MS = 15_000),POST /rewards/claim(body 为{ rewardType })领取奖励,DELETE /rewards/discord解绑 Discord。
5.3 快照数据模型与角色状态
GET /rewards/me的快照结构定义在 app/src/types/rewards.ts,前端归一化逻辑见 rewardsApi.ts:
discord:linked、discordId、username、inviteUrl、membershipStatus;summary:unlockedCount、totalCount、assignedDiscordRoleCount、claimableCount(后端不支持领取功能时该字段缺省)、plan(FREE | BASIC | PRO)、hasActiveSubscription;metrics:currentStreakDays、longestStreakDays、cumulativeTokens、featuresUsedCount、trackedFeaturesCount、lastEvaluatedAt、lastSyncedAt;achievements:成就数组,含title、description、unlocked、progressLabel、roleId、discordRoleStatus、creditAmountUsd、rewardTokens、rewardRecurring,以及领取相关的claimable / claimed / claimedAt / claimPeriod(后四者带缺省值,保证旧后端返回的快照依然合法)。
绑定完成后,每个已解锁成就都会展示其 Discord 角色分配状态:
| 角色状态 | 含义 |
|---|---|
| Assigned(已分配) | 角色已在服务器上授予 |
| Pending(待分配) | 已解锁,但角色尚未分配 |
| Join to claim(加入后领取) | 已绑定但未加入服务器——加入即可获得角色 |
底层角色状态枚举更细:assigned | not_assigned | not_linked | not_in_guild | not_configured | unavailable(rewards.ts);成员状态为member | not_in_guild | not_linked | unavailable。如果你已解锁带角色的成就但未加入服务器,会出现一条claim banner提示你加入领取。POST /rewards/claim的返回(RewardsClaimResult)还携带alreadyClaimed(幂等重复领取标识)、tokens、amountUsd与newPromoBalanceUsd(领取后新的促销余额),说明部分成就是“可领取的月度/一次性 token 或 USD 信用”。
5.4 与 GitHub 贡献者奖励的边界
基于 GitHub 的贡献者奖励是独立机制:一个 GitHub Actions 工作流会在贡献者首个 PR 合并后发布 Discord/周边(merch)邀请评论。它不属于应用内 Rewards 界面,也不使用应用内的 GitHub OAuth。
也就是说,仓库里的contributor-rewards工作流与应用内奖励体系完全解耦,不要混为一谈。
六、Invite Codes 邀请码(/invites)
/invites页面与推荐码是两码事。它管理的是门控新用户注册的个人邀请码(invite codes):
- Redeem(兑换):如果你尚未被邀请,输入一个邀请码来抢占名额。
- Your invite codes(我的邀请码):发给你的一批邀请码列表。每行展示等宽字体(monospace)的码、复制按钮与enabled/disabled状态。当
currentUses >= maxUses(使用次数耗尽)时,该码自动翻转为禁用,并显示是谁兑换了它。
数据模型见 app/src/types/invite.ts:
export interface InviteCode { _id: string; code: string; owner: string; type: 'USER' | 'CAMPAIGN'; // 邀请码类型 maxUses: number; // 最大可用次数 currentUses: number; // 当前已用次数 usageHistory: UsageHistoryEntry[]; // 谁在何时兑换 isActive: boolean; createdAt: string; }type:USER(用户发放)或CAMPAIGN(活动发放);maxUses/currentUses:使用额度计数器,currentUses >= maxUses即失效;usageHistory:兑换记录,每项包含兑换用户信息(userId及其username/firstName等)与usedAt时间戳。
页面实现(Invites.tsx)中,已兑尽的邀请码显示disabled徽章,未兑尽则显示enabled徽章,并有剪贴板反馈(useClipboardFeedback)。
七、本地会话与后端会话的行为差异
汇总一张表,便于快速判断你在哪种会话下会遇到什么:
| 场景 | 表现 |
|---|---|
本地会话(local-only)打开/rewards | 显示空状态卡片,提示需登录,CTA 跳转/settings/account |
| 已登录后端会话,但未存储会话令牌时调用 referral RPC | fail closed,错误"no backend session token; run auth_store_session first" |
| Discord 未绑定 | 成就的discordRoleStatus为not_linked,需先执行 OAuth 连接 |
| 已绑定但未加入服务器 | 角色状态为not_in_guild,出现 claim banner 提示加入 |
八、总结与延伸阅读
OpenHuman 的奖励与推荐体系在架构上刻意保持“薄客户端、厚服务端”:业务规则(金额、转化条件、资格、角色分配)全部由托管后端裁决,桌面端只做两件事——React 前端负责交互与展示,Rust core 中的 referral 域以无状态 RPC 适配器方式代理请求以规避 WebViewfetch的 “Load failed” 问题。这种设计既保证了规则可以随时在服务端调整,也让离线/本地模式下的降级路径非常清晰(直接空状态 + 引导登录)。
想继续深入,可以阅读:
- Billing & usage——推荐、优惠券与成就信用最终在哪里被消费;
- Welcome(文档首页);
- referral 域实现:README.md、ops.rs、schemas.rs;
- 前端页面与组件:Rewards.tsx、Invites.tsx、RewardsCommunityTab.tsx、ReferralRewardsSection.tsx、RewardsCouponSection.tsx;
- 类型与 API 层:referral.ts、rewards.ts、invite.ts、referralApi.ts、rewardsApi.ts;
- 设备指纹:deviceFingerprint.ts。
【免费下载链接】openhumanOpenHuman is an open source personal AI for Mac, Windows and Linux — local-first memory, agent orchestration, and deep research.项目地址: https://gitcode.com/GitHub_Trending/op/openhuman
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考