OpenHuman referral 域名解析:基于托管后端 /referral/* 的薄 RPC 适配器
2026/9/10 10:34:08 网站建设 项目流程

OpenHuman referral 域名解析:基于托管后端 /referral/* 的薄 RPC 适配器

【免费下载链接】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 的 referral(推荐奖励)程序并不在客户端实现任何业务逻辑,而是由一个刻意保持"无状态、无 schema、无持久化"的薄 RPC 适配器域名(src/openhuman/hosted/referral/)负责:它用服务端reqwest通道向托管后端发出已认证的 HTTP 请求,再把后端的原始data载荷原样暴露给 CLI 与 JSON-RPC 客户端。本文将基于该模块的 README 与 ops.rs、schemas.rs 源码,完整拆解它的职责边界、两个控制器(referral.get_stats/referral.claim)的调用链、会话令牌的 fail-closed 前置校验、参数清洗规则、响应包装协议,以及它背后的工程动机——规避桌面 WebViewfetch的 "Load failed"(CORS / TLS / WebKit)问题。

一、模块定位:一个刻意"无状态"的 RPC 适配器

referral 域在整个 OpenHuman 架构中的定位非常明确,README 第一句话就划定了边界:

Thin RPC adapter domain for the referral program. It doesnotown any business logic, state, or schema of its own.

也就是说,这个域名不拥有任何自己的业务逻辑、状态或 schema,它的全部工作只有三件事:

  1. 用已认证的reqwest调用托管后端的/referral/*端点;
  2. 把后端响应中的原始data载荷透传出去;
  3. 通过统一的控制器注册表暴露给 CLI / JSON-RPC 客户端。

从目录结构看,该目录只包含 6 个文件(README.mdmod.rsops.rsops_tests.rsschemas.rsschemas_tests.rs),没有types.rsstore.rstools.rsbus.rs——这是判断它"纯 RPC 适配器而非有状态域"的最直接证据:它不挂 agent 工具(tools),不订阅事件总线(bus),也没有自己的存储(store)。README 的 Notes/gotchas 一节也明确列了这一条。

为什么需要这样一个"夹层"?

README 给出了核心工程动机:

the desktop WebViewfetchto the backend can fail with a generic "Load failed" (CORS / TLS / WebKit), so these ops reuse the same server-sidereqwestpath as the billing domain.

在 Tauri 桌面端,渲染进程(WebView)直接对托管后端发起fetch时,可能因为 CORS、TLS 或 WebKit 底层网络栈的差异而失败,报出笼统的 "Load failed"。因此 referral 域刻意复用与 billing(计费)域完全相同的服务端reqwest通道——所有请求都由 Rust 侧发出,绕开 WebView 网络栈,从而获得与计费功能一致的稳定性和可观测性。这个设计取舍在 ops.rs 的模块文档中也有同样的说明。

二、职责清单(Responsibilities)

按 README,该域名的职责可以归纳为五条:

职责说明
拉取推荐统计通过GET /referral/stats获取推荐码(code)、推荐链接(link)、累计数据(totals)以及被推荐用户的行列表(referred-user rows)
认领推荐码通过POST /referral/claim为当前用户认领推荐码,可附带可选的设备指纹(device fingerprint)作为滥用信号
会话前置校验任何调用之前都要求解析出后端会话 token;没有存储 token 时 fail closed,并返回清晰的错误信息
参数清洗转发前 trim 推荐码code;对deviceFingerprint做 trim 并丢弃纯空白值
响应包装将后端响应包装为RpcOutcome<Value>,附带 grep 友好的日志行

其中"fail closed"是一个重要的安全语义:宁可调用失败,也不允许未认证请求穿透到后端。两个 ops 在拿不到会话 token 时都会返回固定错误:"no backend session token; run auth_store_session first"

三、关键文件与分工

文件角色
mod.rs仅做导出(export-only):pub use ops::*,并重新导出 schema/controller 对(all_referral_controller_schemasall_referral_registered_controllersreferral_schemas
ops.rs业务逻辑核心:require_token(私有)、get_statsclaim_referral。基于有效后端 URL 构造BackendOAuthClient并发起认证 JSON 请求;附带针对 Axum mock 后端的行内测试(ops_tests.rs)
schemas.rs控制器 schema 定义 +handle_*函数(加载 Config 并委托给 ops);定义ReferralClaimParams(camelCase 反序列化)与辅助函数(to_jsondeserialize_paramsjson_output

三层职责分离得很干净:mod.rs只管对外导出,schemas.rs只管 RPC 契约与参数解析,ops.rs只管实际的后端调用。

四、公开 API 表面(Public surface)

通过mod.rs的 re-export,该模块对外暴露以下 5 个符号:

// 拉取推荐统计,返回 CLI 兼容 JSON pub async fn get_stats(config: &Config) -> Result<RpcOutcome<Value>, String> // 认领推荐码;code 必填,device_fingerprint 可选 pub async fn claim_referral( config: &Config, code: &str, device_fingerprint: Option<&str>, ) -> Result<RpcOutcome<Value>, String> // 返回全部控制器 schema(referral_get_stats + referral_claim) pub fn all_referral_controller_schemas() -> Vec<ControllerSchema> // 返回已注册控制器(schema + handler 对) pub fn all_referral_registered_controllers() -> Vec<RegisteredController> // 按函数名查单个 schema;未知名返回 unknown 占位 pub fn referral_schemas(function: &str) -> ControllerSchema

注意require_token是私有辅助函数,不对外导出(README 特别注明)。

五、RPC 控制器:referral.get_stats 与 referral.claim

两个控制器都位于referral命名空间,通过 src/core/all.rs 注册进全局控制器注册表(all_referral_registered_controllers()),从而同时暴露给 CLI 与 JSON-RPC。完整契约如下:

Method输入输出后端调用
referral_get_statsreferral.get_statsstats(JSON)GET /referral/stats
referral_claimreferral.claimcode(string,必填)、deviceFingerprint(string,可选)result(JSON)POST /referral/claim

get_stats:零输入,纯透传

get_stats的 handler(schemas.rs)不解析任何参数,直接加载 Config 后调用 ops:

fn handle_referral_get_stats(_params: Map<String, Value>) -> ControllerFuture { Box::pin(async move { let config = config_rpc::load_config_with_timeout().await?; to_json(crate::openhuman::hosted::referral::get_stats(&config).await?) }) }

对应的 ops 实现(ops.rs):

pub async fn get_stats(config: &Config) -> Result<RpcOutcome<Value>, String> { let token = require_token(config)?; let api_url = effective_backend_api_url(&config.api_url); let client = BackendOAuthClient::new(&api_url).map_err(|e| e.to_string())?; let data = client .authed_json(&token, Method::GET, "/referral/stats", None) .await .map_err(|e| e.to_string())?; Ok(RpcOutcome::single_log( data, "referral stats fetched from backend GET /referral/stats", )) }

claim:两个输入字段的完整处理链

claim的 schema 定义了输入契约(schemas.rs):

  • codeTypeSchema::Stringrequired,注释为 "Referral code to claim.";
  • deviceFingerprintTypeSchema::Option(String)optional,注释为 "Optional client fingerprint for abuse signals."

handler(schemas.rs)先反序列化参数,再经过与 ops 内部一致的 trim/空白过滤后委托给 ops:

fn handle_referral_claim(params: Map<String, Value>) -> ControllerFuture { Box::pin(async move { let config = config_rpc::load_config_with_timeout().await?; let payload = deserialize_params::<ReferralClaimParams>(params)?; let fp = payload .device_fingerprint .as_deref() .map(str::trim) .filter(|s| !s.is_empty()); to_json( crate::openhuman::hosted::referral::claim_referral(&config, payload.code.trim(), fp) .await?, ) }) }

ops 侧的claim_referral(ops.rs)会构建请求体并转发:

pub async fn claim_referral( config: &Config, code: &str, device_fingerprint: Option<&str>, ) -> Result<RpcOutcome<Value>, String> { let token = require_token(config)?; let api_url = effective_backend_api_url(&config.api_url); let client = BackendOAuthClient::new(&api_url).map_err(|e| e.to_string())?; let mut body = Map::new(); body.insert("code".to_string(), json!(code.trim())); if let Some(fp) = device_fingerprint.map(str::trim).filter(|s| !s.is_empty()) { body.insert("deviceFingerprint".to_string(), json!(fp)); } let data = client .authed_json( &token, Method::POST, "/referral/claim", Some(Value::Object(body)), ) .await .map_err(|e| e.to_string())?; Ok(RpcOutcome::single_log( data, "referral claim accepted by backend POST /referral/claim", )) }

unknown 占位 schema

schema.rsreferral_schemas对任何无法识别的函数名返回一个unknown占位 schema(schemas.rs):namespace仍为referralfunctionunknown,输出只有一个必填的error字段(TypeSchema::String,注释 "Lookup error details.")。这让外部调用者在拼错函数名时能拿到结构化错误而非静默失败,schemas_tests.rs 中unknown_function_returns_unknown_placeholder测试对该行为做了断言。

六、会话令牌:fail-closed 的认证前置

两个 ops 在发起任何网络请求前,都会先经过私有的require_token(ops.rs):

fn require_token(config: &Config) -> Result<String, String> { get_session_token(config)? .and_then(|v| { let t = v.trim().to_string(); if t.is_empty() { None } else { Some(t) } }) .ok_or_else(|| "no backend session token; run auth_store_session first".to_string()) }

这里的语义是三层防御

  1. 从凭据库读取会话 token(get_session_token,见 session_support.rs,经 src/api/jwt.rs 转发、src/api/mod.rs 统一导出);
  2. trim 掉首尾空白;
  3. 若结果为空白字符串,等价于"无 token",直接 fail closed。

require_token返回Result<String, String>:要么拿到干净可用的 token 字符串,要么返回错误"no backend session token; run auth_store_session first"——错误信息中明确提示了补救命令auth_store_session。三个单元测试分别覆盖了"无存储 token 报错"、"存储值被 trim"、"纯空白 token 被拒绝"三种情况(ops_tests.rs)。

测试中还展示了如何用AuthService向凭据库播种会话 token(ops_tests.rs),这正好印证了 README Dependencies 中的说明:测试专用依赖为crate::openhuman::security::credentials::{AuthService, APP_SESSION_PROVIDER, DEFAULT_AUTH_PROFILE_NAME}

七、参数清洗与防御性冗余过滤

codedeviceFingerprint的清洗规则是一致且双重的:

  • code:始终执行trim()。测试claim_referral_posts_trimmed_code_and_drops_whitespace_fingerprint(ops_tests.rs)验证了" ABC-123 "会被转成"ABC-123"再发送;
  • deviceFingerprint:先trim(),再用filter(|s| !s.is_empty())丢弃纯空白值。同样是上述测试验证:传Some(" ")时,请求体里不出现deviceFingerprint字段(assert!(out.value["echoed"].get("deviceFingerprint").is_none()));而传Some(" fp-1 ")时会被清洗为"fp-1"(见claim_referral_forwards_non_empty_device_fingerprint_trimmed,ops_tests.rs)。

README 特别强调:这套 trim/空白丢弃逻辑在ops::claim_referral和 schema 处理器handle_referral_claim两处各做了一遍(README)。这是刻意的防御性冗余过滤——无论调用方走哪一层入口,输入都能被清洗,避免出现"CLI 路径干净、JSON-RPC 路径脏"的不一致。

八、响应包装:RpcOutcome 与 CLI 兼容 JSON

所有 ops 的返回值都是RpcOutcome<Value>——该类型定义在 src/rpc/mod.rs,包含两个字段:value: T(RPC 调用返回的真实数据)和logs: Vec<String>(审计/调试用日志)。

两个 ops 都用RpcOutcome::single_log构造返回值(src/rpc/mod.rs),并附带grep 友好的固定前缀日志行

  • get_stats"referral stats fetched from backend GET /referral/stats"
  • claim_referral"referral claim accepted by backend POST /referral/claim"

在 schema handler 层,to_json调用outcome.into_cli_compatible_json()(src/rpc/mod.rs)将其转为 CLI 兼容的 JSON。根据 src/rpc/mod.rs 中定义的唯一规则(log envelope):

logs.is_empty() -> value (bare,直接输出值) otherwise -> { "result": value, "logs": … } (wrapped,包一层信封)

由于 referral 的两个 ops 总是写入一条日志,实际响应形状会是{ "result": …, "logs": […] }。schemas_tests 中的to_json_wraps_result_and_logs(schemas_tests.rs)对该行为做了验证。

九、后端 URL 解析链:effective_backend_api_url

referral ops 使用effective_backend_api_url(&config.api_url)解析后端 API 基址(src/api/config.rs)。这是 OpenHuman 所有托管后端调用(auth、billing、team、referral、webhooks、credentials、channels、voice 等)共用的单一真相源,其解析顺序为:

  1. 用户显式配置config.api_url——但只有当它不像推理端点时才会被采用:
    • looks_like_local_ai_endpoint(Ollama/vLLM/LM Studio 等本地模型服务,端口 11434/8000/8080/1234 等或含/v1/chat/completions路径);
    • looks_like_inference_provider_endpoint(openrouter.ai、openai.com、anthropic.com 等托管推理服务商域名,或/v1/api/v1基路径);
    • is_cloud_inference(内置云服务商主机名);
    • 以上任一命中且不是 OpenHuman 自有后端(api.tinyhumans.ai/staging-api.tinyhumans.ai)时,跳过用户覆盖,回退到环境/默认链。
  2. 环境变量BACKEND_URLVITE_BACKEND_URL(运行时优先,其次编译期option_env!内嵌)。
  3. 环境感知默认值OPENHUMAN_APP_ENV=staging时回落到https://staging-api.tinyhumans.ai,否则使用https://api.tinyhumans.ai(src/api/config.rs)。

这套守卫的意义在于:用户把config.api_url指向本地 Ollama 或第三方推理服务时,referral、billing 等控制面调用不会误路由到推理服务商(否则会得到 400/404/500),从而保证了 referral 请求始终落在真正的托管后端上。README Dependencies 对该依赖的注释是 "resolves the effective backend API base URL fromconfig.api_url"。

十、测试策略:基于 Axum mock 后端的全链路验证

该模块的测试质量很高,ops_tests.rs采用真实 HTTP 环回验证,而非 mock 函数返回值:

  • spawn_mock(ops_tests.rs)在127.0.0.1:0上启动一个真实 Axum 服务并做就绪探测(指数退避,最长 2 秒),返回其临时端口 URL;
  • 测试配置把config.api_url指向该 mock 基址,并用AuthService::store_provider_token播种"test-session-token"config_with_backend,ops_tests.rs);
  • mock 路由直接返回 JSON:GET /referral/stats返回{"referrals": 3, "earned_cents": 1500}POST /referral/claim回显请求体{"echoed": body},以便断言清洗后的字段。

覆盖的用例包括:无会话报错(stats/claim 各一个)、stats 载荷透传与日志存在性、claim 的 code trim 与空白 fingerprint 丢弃、非空 fingerprint 的 trim 转发。schemas_tests.rs则从 RPC 契约角度验证:schema 数量与控制器数量一致、get_stats 无输入且输出必填、claim 的 code 必填而 fingerprint 可选、camelCase 参数解析、缺失 fingerprint 容错、缺失 code 报错、类型错误返回invalid params前缀、unknown 占位 schema 等。

十一、设计约束与注意事项(gotchas)

结合 README 与源码,这个域名的工程约束可以总结为以下几点:

  1. 刻意零内部结构:没有types.rs/store.rs/tools.rs/bus.rs——纯 RPC 适配器,不持有状态,不提供 agent 工具,不订阅事件总线。判断一个模块"是不是有状态域",看它是否挂载这些文件即可。
  2. 无持久化:完全无状态,只通过get_session_token从凭据库读取后端会话 token,自身不落盘任何数据。
  3. fail-closed 语义:两个 ops 在缺少会话 token 时一律失败,错误信息固定为"no backend session token; run auth_store_session first"
  4. 资格判定在后端claim的准入条件("only users who have not yet subscribed"——仅限尚未订阅的用户)由托管后端强制执行,本模块只做请求转发,不参与判定(README)。
  5. 双重防御性清洗:code 与 fingerprint 的 trim/空白丢弃在 ops 层与 schema 层各执行一次,防止入口不一致。
  6. 刻意复用 billing 通道:绕开 WebViewfetch(CORS/TLS/WebKit "Load failed"),所有请求走 Rust 侧reqwest,保证与计费域同等稳定的网络行为与统一的可观测性日志。

小结

referral 域是 OpenHuman "托管后端 + 客户端薄适配器"架构的一个典型样本:它把所有业务复杂度留在服务端,客户端只做认证、清洗、转发、包装四件事,同时通过统一的控制器注册表(src/core/all.rs)向 CLI 与 JSON-RPC 提供一致的调用面。理解它的关键,不在于它写了多少逻辑,而在于它刻意不写哪些逻辑——无状态、无 schema、无工具、无持久化,配合 fail-closed 的会话校验、双层的参数清洗和 grep 友好的日志,构成一个高度可审计、可替换、易测试的薄适配层。如果你的目标是扩展 OpenHuman 的托管后端能力(如新增一个/referral/*端点),只需沿这条路径:在ops.rs增加一个认证请求函数、在schemas.rs增加 schema 与 handler、在all.rs完成注册,并在ops_tests.rs用 Axum mock 补上链路测试即可。

【免费下载链接】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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询