添加客户端特殊标识 #226

Closed
opened 2026-08-31 13:13:20 +08:00 by kdletters · 4 comments
Member
No description provided.
kdletters started working 2026-08-31 13:13:57 +08:00
kdletters worked for 15 seconds 2026-08-31 13:14:12 +08:00
lhk229 was assigned by kdletters 2026-08-31 14:05:56 +08:00
lhk229 added the due date 2026-09-04 2026-08-31 14:19:30 +08:00
lhk229 modified the due date from 2026-09-04 to 2026-09-02 2026-09-01 14:21:53 +08:00
Author
Member

打算怎么做,修改范围是哪些

打算怎么做,修改范围是哪些
Owner

AGC 主站请求标记统一注入两种方案

更新时间:2026-09-01
关联 Issue:#226 添加客户端特殊标识#225 添加客户端埋点统计
状态:方案说明,未实施代码修改

1. 背景

AGC 客户端同时存在两套 HTTP 出口:

  1. Web/TS 层,通过 fetchClientHttp 使用浏览器 fetch 或 Tauri HTTP 插件。
  2. Tauri/Rust 层,通过多个 reqwest::Client 直接调用主站、对象存储、签名下载地址、LLM Provider 和本地工具桥。

目标是让 AGC 发往主站的请求统一携带来源标记,而不是在每个业务 API 调用点重复写 Header。

推荐标记:

X-Genarrative-Client: agc

该标记只用于来源审计、埋点和统计,不参与鉴权、权限、计费或账号归属判断。

2. 目标与不做项

2.1 目标

  • AGC 发往当前选定主站 origin 的请求统一携带客户端标记。
  • 同时覆盖登录账号态和开发者 API Key 态。
  • 同时覆盖普通业务请求、生成提交和异步任务轮询。
  • 避免在每个 /api/... 调用点重复设置 Header。
  • 不把标记发送给 OSS、签名资源地址、LLM Provider、网页搜索或本地工具桥。
  • 保持现有 access token、API Key、幂等键和请求体语义不变。

2.2 不做项

  • 本方案不定义主站数据库字段和后台页面实现,主站接收与落库属于 #225
  • 不使用客户端标记替代真实认证主体。
  • 不修改 External v1 请求 DTO 或 OpenAPI 请求体。
  • 不通过 DNS、系统代理或网络抓包层注入 Header。
  • 不给进程内所有 reqwest 请求无差别设置默认 Header。

3. 两种方案的共同部分

无论 Rust 层选择哪种方案,Web/TS 层都可以直接在统一出口注入。

3.1 Web/TS 统一出口

文件:

处理方式:

  1. fetchClientHttp 解析最终请求地址。
  2. 确认目标 origin 等于当前选定的 serverBaseUrl
  3. 在传给浏览器 fetch 或 Tauri HTTP 插件前设置:
X-Genarrative-Client: agc

示意代码:

const headers = new Headers(init.headers);
headers.set('X-Genarrative-Client', 'agc');

return transport(target.url, {
  ...init,
  headers,
});

需要保留调用方原有 Header,尤其是:

  • Authorization
  • Content-Type
  • x-genarrative-response-envelope
  • X-Request-ID,如果调用方已提供

3.2 主站 origin 的定义

不能只写死以下两个地址:

https://dev.genarrative.world
https://www.genarrative.world

AGC 还支持自定义服务器,判断依据应是当前请求绑定的权威 serverBaseUrl / apiBaseUrl,并比较规范化后的 origin:

scheme + host + effective port

路径、query 和 fragment 不属于 origin 判断条件。

3.3 必须排除的请求

以下请求不能携带主站客户端标记:

  • direct-upload-tickets 返回的对象存储 multipart 上传地址。
  • read-url 返回的临时签名下载地址。
  • LLM Provider 的 /responses 等请求。
  • AGC 受控网页搜索请求。
  • 本地 loopback 工具桥请求。
  • 游戏试玩页面、外部网页或用户输入的任意 URL。

3.4 CORS

浏览器环境向不同 origin 的主站发送自定义 Header 时可能触发 CORS 预检。主站及自定义部署需要允许:

Access-Control-Allow-Headers: X-Genarrative-Client

Tauri HTTP 插件和 Rust reqwest 不受浏览器 CORS 限制,但仍应遵守相同的目标 origin 边界。

4. 方案一:专用主站 HTTP Client 工厂

4.1 核心思路

为 Rust 层建立一个只允许服务于主站 API 的 reqwest::Client 构造函数,并通过 default_headers 自动加入客户端标记。

第三方上传、签名下载、Provider 和搜索继续使用现有独立 Client,不带标记。

示意代码:

fn build_agc_main_site_client(timeout: Duration) -> Result<reqwest::Client, String> {
    let mut headers = reqwest::header::HeaderMap::new();
    headers.insert(
        reqwest::header::HeaderName::from_static("x-genarrative-client"),
        reqwest::header::HeaderValue::from_static("agc"),
    );

    reqwest::Client::builder()
        .default_headers(headers)
        .connect_timeout(Duration::from_secs(10))
        .timeout(timeout)
        .redirect(reqwest::redirect::Policy::none())
        .build()
        .map_err(|error| format!("创建 AGC 主站 HTTP 客户端失败:{error}"))
}

调用代码仍然保持普通 reqwest 写法:

let client = build_agc_main_site_client(Duration::from_secs(60))?;

let response = client
    .get(format!("{api_base_url}{route}"))
    .bearer_auth(token)
    .send()
    .await?;

业务请求不再逐个调用 .header("X-Genarrative-Client", "agc")

4.2 预计改动范围

主要改动是把主站用途的 reqwest::Client::new() / Client::builder() 替换为统一工厂,而不是修改每个 API endpoint。

潜在涉及文件:

不应迁移到主站 Client 的文件或请求:

  • codex_provider_proxy.rs
  • build_external_asset_download_client 创建的签名资源下载 Client。
  • OSS multipart 上传 Client。
  • 受控搜索和 loopback 工具桥 Client。

4.3 优点

  • 实现简单,符合当前仓库优先简单设计的原则。
  • 业务请求代码基本不变。
  • Header 只配置一次。
  • 不需要新增 HTTP middleware 框架或复杂泛型包装。
  • 便于为普通请求和长时间生成提交提供不同 timeout,但共享同一标记配置。
  • 可以通过单元测试检查 Client 发出的请求自动带标记。

4.4 缺点和风险

  • default_headers 对该 Client 发出的所有请求生效,不会再次判断目标域名。
  • 如果后续有人误用主站 Client 请求 OSS、签名 URL 或 Provider,标记会被带到第三方。
  • 当前存在多个不同 timeout 的 Client,需要提供少量参数或几个明确的工厂函数。
  • 仍需要替换现有主站 Client 的创建点,无法做到完全零调用点改动。

4.5 风险控制

保持控制简单,不引入复杂网络状态机:

  • 主站 Client 只在持有权威 apiBaseUrl 的模块中创建。
  • 主站 Client 不传给通用下载函数。
  • 签名下载继续由 build_external_asset_download_client 单独创建。
  • OSS 上传继续使用单独 Client。
  • 工厂名称明确包含 main_site,避免误用。
  • 定向测试断言主站 mock 收到标记、第三方 mock 未收到标记。

5. 方案二:按目标 origin 的请求包装器

5.1 核心思路

不依赖 Client 的默认 Header,而是统一通过一个请求构造入口创建主站 RequestBuilder。

包装器在构造请求时:

  1. 解析目标 URL。
  2. 解析当前权威 apiBaseUrl
  3. 比较两者 origin。
  4. 只有同源时加入 AGC 标记。
  5. 非同源时拒绝,或明确返回不带标记的普通请求。

更安全的做法是主站包装器直接拒绝非同源 URL。

示意代码:

fn main_site_request(
    client: &reqwest::Client,
    method: reqwest::Method,
    api_base_url: &str,
    route: &str,
) -> Result<reqwest::RequestBuilder, String> {
    let base = url::Url::parse(api_base_url)
        .map_err(|_| "AGC 主站地址无效".to_string())?;
    let target = base
        .join(route)
        .map_err(|_| "AGC 主站请求路径无效".to_string())?;

    if target.origin() != base.origin() {
        return Err("AGC 主站请求越出当前服务器 origin".to_string());
    }

    Ok(client
        .request(method, target)
        .header("X-Genarrative-Client", "agc"))
}

调用示意:

let response = main_site_request(
    &client,
    reqwest::Method::GET,
    access.api_base_url(),
    &route,
)?
.bearer_auth(access.bearer_token())
.send()
.await?;

5.2 预计改动范围

所有主站 .get(...).post(...).patch(...).delete(...) 构造点需要切换为包装器,或统一封装为一个 AgcMainSiteHttp 类型。

例如:

struct AgcMainSiteHttp {
    client: reqwest::Client,
    base_url: url::Url,
}

impl AgcMainSiteHttp {
    fn get(&self, route: &str) -> Result<reqwest::RequestBuilder, String>;
    fn post(&self, route: &str) -> Result<reqwest::RequestBuilder, String>;
}

下载、OSS 和 Provider 请求继续直接使用普通 reqwest::Client

5.3 优点

  • 每次请求都会校验真实目标 origin。
  • 即使包装器被误用于第三方 URL,也可以失败关闭,不会泄漏标记。
  • 主站 URL 拼接和 origin 校验有单一权威实现。
  • 适合未来出现更多动态 URL、多个部署 origin 或更严格的请求来源策略。
  • 可以在同一个入口继续注入 request ID、客户端版本等非敏感请求元信息。

5.4 缺点和风险

  • 需要修改更多请求构造点。
  • 容易把简单的 Header 注入扩大成新的 HTTP 抽象层。
  • 现有代码已经有 ExternalEditorBindingAccess、路由映射、冻结会话校验等概念,再新增完整 HTTP facade 会增加概念数量。
  • 如果包装器同时承接鉴权、重试、错误解析、幂等和下载,很容易过度设计。
  • 对只需要固定来源标记的当前需求,复杂度高于方案一。

5.5 风险控制

  • 包装器只负责 URL 构造、origin 校验和固定 Header,不负责业务错误解析。
  • 不在包装器中自动重试 POST。
  • 不把 token、API Key 或幂等键保存到长生命周期对象。
  • ExternalEditorBindingAccess 继续负责账号态/开发者 Key 路由映射和冻结会话校验。
  • 业务层继续明确设置 Bearer、Idempotency-Key 和请求体。

6. 两种方案对比

对比项 方案一:专用主站 Client 工厂 方案二:origin 请求包装器
Header 注入位置 reqwest::Client::default_headers 每次构造 RequestBuilder 时
业务调用点改动 较少,主要替换 Client 创建点 较多,需要替换请求构造点
第三方泄漏防护 依赖 Client 使用边界 每次请求显式 origin 校验
实现复杂度
新增概念数量 较多
支持不同 timeout 通过工厂参数或少量变体 共用 Client 或包装器配置
对动态 URL 的安全性 一般
当前需求适配度
后续扩展请求元信息 可以,但仍是 Client 级别 更灵活,可按请求控制
误用后的行为 可能把标记发给非主站 可拒绝非同源请求

7. 推荐选择

当前推荐 方案一:专用主站 HTTP Client 工厂

理由:

  • 当前需求只是给主站请求增加稳定来源标记。
  • 主站、签名下载、OSS 和 Provider 已有相对明确的客户端边界。
  • 不需要为一个固定 Header 引入新的 HTTP facade。
  • 改动集中在 Client 创建点,业务请求路径和错误语义变化较小。
  • 更符合仓库“优先简单、避免过度设计”的约束。

方案一需要明确遵守:主站 Client 不能用于签名下载、OSS 上传和 Provider 请求。如果实施时发现多个模块无法可靠维持这条边界,或存在大量由外部数据生成的动态目标 URL,再改选方案二。

不建议一开始同时实现两种方案。二选一即可,避免形成“Client 默认 Header + RequestBuilder 再加一次 Header”的重复机制。

8. 建议的 PR 边界

8.1 #226 添加客户端特殊标识

负责:

  • TS fetchClientHttp 统一注入标记。
  • Rust 按选定方案统一注入标记。
  • 主站请求携带标记。
  • 第三方请求不携带标记。
  • 必要的 CORS Header 配置验证。

不负责:

  • tracking event 落库。
  • External v1 route tracking。
  • 后台筛选和统计展示。

8.2 #225 添加客户端埋点统计

负责:

  • 主站读取并校验客户端标记。
  • 结合登录用户或 ExternalApiPrincipal 记录认证主体。
  • 将标记写入 tracking_event.metadata_json 或后续确定的结构化字段。
  • 补齐 /api/external/v1/* tracking。
  • 后台查询、筛选或统计。

9. 验收清单

9.1 正向请求

  • TS 登录请求带 X-Genarrative-Client: agc
  • TS 素材、钱包请求带标记。
  • Rust 账号态 /api/editor/* 请求带标记。
  • Rust 账号态 /api/assets/* 请求带标记。
  • Rust 账号态 /api/runtime/external-generation/jobs/* 轮询带标记。
  • Rust API Key 态 /api/external/v1/* 请求带标记。
  • /api/profile/api-keys 请求带标记。
  • 自定义主站 origin 的请求带标记。

9.2 排除请求

  • OSS multipart 上传不带标记。
  • 签名 URL 下载不带标记。
  • LLM Provider 请求不带标记。
  • AGC 受控搜索不带标记。
  • 本地工具桥请求不带标记。

9.3 兼容性

  • 原 Authorization 不变。
  • 原 Idempotency-Key 不变。
  • 原请求 body 不变。
  • 原 timeout 和 redirect policy 不变。
  • 浏览器跨域预检允许 X-Genarrative-Client
  • 未识别 marker 时主站业务请求不受影响。
# AGC 主站请求标记统一注入两种方案 更新时间:2026-09-01 关联 Issue:`#226 添加客户端特殊标识`、`#225 添加客户端埋点统计` 状态:方案说明,未实施代码修改 ## 1. 背景 AGC 客户端同时存在两套 HTTP 出口: 1. Web/TS 层,通过 `fetchClientHttp` 使用浏览器 `fetch` 或 Tauri HTTP 插件。 2. Tauri/Rust 层,通过多个 `reqwest::Client` 直接调用主站、对象存储、签名下载地址、LLM Provider 和本地工具桥。 目标是让 AGC 发往主站的请求统一携带来源标记,而不是在每个业务 API 调用点重复写 Header。 推荐标记: ```http X-Genarrative-Client: agc ``` 该标记只用于来源审计、埋点和统计,不参与鉴权、权限、计费或账号归属判断。 ## 2. 目标与不做项 ### 2.1 目标 - AGC 发往当前选定主站 origin 的请求统一携带客户端标记。 - 同时覆盖登录账号态和开发者 API Key 态。 - 同时覆盖普通业务请求、生成提交和异步任务轮询。 - 避免在每个 `/api/...` 调用点重复设置 Header。 - 不把标记发送给 OSS、签名资源地址、LLM Provider、网页搜索或本地工具桥。 - 保持现有 access token、API Key、幂等键和请求体语义不变。 ### 2.2 不做项 - 本方案不定义主站数据库字段和后台页面实现,主站接收与落库属于 `#225`。 - 不使用客户端标记替代真实认证主体。 - 不修改 External v1 请求 DTO 或 OpenAPI 请求体。 - 不通过 DNS、系统代理或网络抓包层注入 Header。 - 不给进程内所有 `reqwest` 请求无差别设置默认 Header。 ## 3. 两种方案的共同部分 无论 Rust 层选择哪种方案,Web/TS 层都可以直接在统一出口注入。 ### 3.1 Web/TS 统一出口 文件: - [clientHttp.ts](C:/projects/narrative/Genarrative/apps/ai-game-creator-shell/src/services/clientHttp.ts) 处理方式: 1. `fetchClientHttp` 解析最终请求地址。 2. 确认目标 origin 等于当前选定的 `serverBaseUrl`。 3. 在传给浏览器 `fetch` 或 Tauri HTTP 插件前设置: ```http X-Genarrative-Client: agc ``` 示意代码: ```ts const headers = new Headers(init.headers); headers.set('X-Genarrative-Client', 'agc'); return transport(target.url, { ...init, headers, }); ``` 需要保留调用方原有 Header,尤其是: - `Authorization` - `Content-Type` - `x-genarrative-response-envelope` - `X-Request-ID`,如果调用方已提供 ### 3.2 主站 origin 的定义 不能只写死以下两个地址: ```text https://dev.genarrative.world https://www.genarrative.world ``` AGC 还支持自定义服务器,判断依据应是当前请求绑定的权威 `serverBaseUrl` / `apiBaseUrl`,并比较规范化后的 origin: ```text scheme + host + effective port ``` 路径、query 和 fragment 不属于 origin 判断条件。 ### 3.3 必须排除的请求 以下请求不能携带主站客户端标记: - `direct-upload-tickets` 返回的对象存储 multipart 上传地址。 - `read-url` 返回的临时签名下载地址。 - LLM Provider 的 `/responses` 等请求。 - AGC 受控网页搜索请求。 - 本地 loopback 工具桥请求。 - 游戏试玩页面、外部网页或用户输入的任意 URL。 ### 3.4 CORS 浏览器环境向不同 origin 的主站发送自定义 Header 时可能触发 CORS 预检。主站及自定义部署需要允许: ```http Access-Control-Allow-Headers: X-Genarrative-Client ``` Tauri HTTP 插件和 Rust `reqwest` 不受浏览器 CORS 限制,但仍应遵守相同的目标 origin 边界。 ## 4. 方案一:专用主站 HTTP Client 工厂 ### 4.1 核心思路 为 Rust 层建立一个只允许服务于主站 API 的 `reqwest::Client` 构造函数,并通过 `default_headers` 自动加入客户端标记。 第三方上传、签名下载、Provider 和搜索继续使用现有独立 Client,不带标记。 示意代码: ```rust fn build_agc_main_site_client(timeout: Duration) -> Result<reqwest::Client, String> { let mut headers = reqwest::header::HeaderMap::new(); headers.insert( reqwest::header::HeaderName::from_static("x-genarrative-client"), reqwest::header::HeaderValue::from_static("agc"), ); reqwest::Client::builder() .default_headers(headers) .connect_timeout(Duration::from_secs(10)) .timeout(timeout) .redirect(reqwest::redirect::Policy::none()) .build() .map_err(|error| format!("创建 AGC 主站 HTTP 客户端失败:{error}")) } ``` 调用代码仍然保持普通 `reqwest` 写法: ```rust let client = build_agc_main_site_client(Duration::from_secs(60))?; let response = client .get(format!("{api_base_url}{route}")) .bearer_auth(token) .send() .await?; ``` 业务请求不再逐个调用 `.header("X-Genarrative-Client", "agc")`。 ### 4.2 预计改动范围 主要改动是把主站用途的 `reqwest::Client::new()` / `Client::builder()` 替换为统一工厂,而不是修改每个 API endpoint。 潜在涉及文件: - [assets.rs](C:/projects/narrative/Genarrative/apps/ai-game-creator-shell/src-tauri/src/assets.rs) - [commands.rs](C:/projects/narrative/Genarrative/apps/ai-game-creator-shell/src-tauri/src/commands.rs) - [canvas_generation.rs](C:/projects/narrative/Genarrative/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs) - [direct_runtime.rs](C:/projects/narrative/Genarrative/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs) - [direct_tool_bridge.rs](C:/projects/narrative/Genarrative/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs) - [generation.rs](C:/projects/narrative/Genarrative/apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas/generation.rs) - [resource_editor.rs](C:/projects/narrative/Genarrative/apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs) 不应迁移到主站 Client 的文件或请求: - [codex_provider_proxy.rs](C:/projects/narrative/Genarrative/apps/ai-game-creator-shell/src-tauri/src/agent/codex_provider_proxy.rs) - `build_external_asset_download_client` 创建的签名资源下载 Client。 - OSS multipart 上传 Client。 - 受控搜索和 loopback 工具桥 Client。 ### 4.3 优点 - 实现简单,符合当前仓库优先简单设计的原则。 - 业务请求代码基本不变。 - Header 只配置一次。 - 不需要新增 HTTP middleware 框架或复杂泛型包装。 - 便于为普通请求和长时间生成提交提供不同 timeout,但共享同一标记配置。 - 可以通过单元测试检查 Client 发出的请求自动带标记。 ### 4.4 缺点和风险 - `default_headers` 对该 Client 发出的所有请求生效,不会再次判断目标域名。 - 如果后续有人误用主站 Client 请求 OSS、签名 URL 或 Provider,标记会被带到第三方。 - 当前存在多个不同 timeout 的 Client,需要提供少量参数或几个明确的工厂函数。 - 仍需要替换现有主站 Client 的创建点,无法做到完全零调用点改动。 ### 4.5 风险控制 保持控制简单,不引入复杂网络状态机: - 主站 Client 只在持有权威 `apiBaseUrl` 的模块中创建。 - 主站 Client 不传给通用下载函数。 - 签名下载继续由 `build_external_asset_download_client` 单独创建。 - OSS 上传继续使用单独 Client。 - 工厂名称明确包含 `main_site`,避免误用。 - 定向测试断言主站 mock 收到标记、第三方 mock 未收到标记。 ## 5. 方案二:按目标 origin 的请求包装器 ### 5.1 核心思路 不依赖 Client 的默认 Header,而是统一通过一个请求构造入口创建主站 RequestBuilder。 包装器在构造请求时: 1. 解析目标 URL。 2. 解析当前权威 `apiBaseUrl`。 3. 比较两者 origin。 4. 只有同源时加入 AGC 标记。 5. 非同源时拒绝,或明确返回不带标记的普通请求。 更安全的做法是主站包装器直接拒绝非同源 URL。 示意代码: ```rust fn main_site_request( client: &reqwest::Client, method: reqwest::Method, api_base_url: &str, route: &str, ) -> Result<reqwest::RequestBuilder, String> { let base = url::Url::parse(api_base_url) .map_err(|_| "AGC 主站地址无效".to_string())?; let target = base .join(route) .map_err(|_| "AGC 主站请求路径无效".to_string())?; if target.origin() != base.origin() { return Err("AGC 主站请求越出当前服务器 origin".to_string()); } Ok(client .request(method, target) .header("X-Genarrative-Client", "agc")) } ``` 调用示意: ```rust let response = main_site_request( &client, reqwest::Method::GET, access.api_base_url(), &route, )? .bearer_auth(access.bearer_token()) .send() .await?; ``` ### 5.2 预计改动范围 所有主站 `.get(...)`、`.post(...)`、`.patch(...)`、`.delete(...)` 构造点需要切换为包装器,或统一封装为一个 `AgcMainSiteHttp` 类型。 例如: ```rust struct AgcMainSiteHttp { client: reqwest::Client, base_url: url::Url, } impl AgcMainSiteHttp { fn get(&self, route: &str) -> Result<reqwest::RequestBuilder, String>; fn post(&self, route: &str) -> Result<reqwest::RequestBuilder, String>; } ``` 下载、OSS 和 Provider 请求继续直接使用普通 `reqwest::Client`。 ### 5.3 优点 - 每次请求都会校验真实目标 origin。 - 即使包装器被误用于第三方 URL,也可以失败关闭,不会泄漏标记。 - 主站 URL 拼接和 origin 校验有单一权威实现。 - 适合未来出现更多动态 URL、多个部署 origin 或更严格的请求来源策略。 - 可以在同一个入口继续注入 request ID、客户端版本等非敏感请求元信息。 ### 5.4 缺点和风险 - 需要修改更多请求构造点。 - 容易把简单的 Header 注入扩大成新的 HTTP 抽象层。 - 现有代码已经有 `ExternalEditorBindingAccess`、路由映射、冻结会话校验等概念,再新增完整 HTTP facade 会增加概念数量。 - 如果包装器同时承接鉴权、重试、错误解析、幂等和下载,很容易过度设计。 - 对只需要固定来源标记的当前需求,复杂度高于方案一。 ### 5.5 风险控制 - 包装器只负责 URL 构造、origin 校验和固定 Header,不负责业务错误解析。 - 不在包装器中自动重试 POST。 - 不把 token、API Key 或幂等键保存到长生命周期对象。 - `ExternalEditorBindingAccess` 继续负责账号态/开发者 Key 路由映射和冻结会话校验。 - 业务层继续明确设置 Bearer、Idempotency-Key 和请求体。 ## 6. 两种方案对比 | 对比项 | 方案一:专用主站 Client 工厂 | 方案二:origin 请求包装器 | |---|---|---| | Header 注入位置 | `reqwest::Client::default_headers` | 每次构造 RequestBuilder 时 | | 业务调用点改动 | 较少,主要替换 Client 创建点 | 较多,需要替换请求构造点 | | 第三方泄漏防护 | 依赖 Client 使用边界 | 每次请求显式 origin 校验 | | 实现复杂度 | 低 | 中 | | 新增概念数量 | 少 | 较多 | | 支持不同 timeout | 通过工厂参数或少量变体 | 共用 Client 或包装器配置 | | 对动态 URL 的安全性 | 一般 | 高 | | 当前需求适配度 | 高 | 中 | | 后续扩展请求元信息 | 可以,但仍是 Client 级别 | 更灵活,可按请求控制 | | 误用后的行为 | 可能把标记发给非主站 | 可拒绝非同源请求 | ## 7. 推荐选择 当前推荐 **方案一:专用主站 HTTP Client 工厂**。 理由: - 当前需求只是给主站请求增加稳定来源标记。 - 主站、签名下载、OSS 和 Provider 已有相对明确的客户端边界。 - 不需要为一个固定 Header 引入新的 HTTP facade。 - 改动集中在 Client 创建点,业务请求路径和错误语义变化较小。 - 更符合仓库“优先简单、避免过度设计”的约束。 方案一需要明确遵守:主站 Client 不能用于签名下载、OSS 上传和 Provider 请求。如果实施时发现多个模块无法可靠维持这条边界,或存在大量由外部数据生成的动态目标 URL,再改选方案二。 不建议一开始同时实现两种方案。二选一即可,避免形成“Client 默认 Header + RequestBuilder 再加一次 Header”的重复机制。 ## 8. 建议的 PR 边界 ### 8.1 `#226 添加客户端特殊标识` 负责: - TS `fetchClientHttp` 统一注入标记。 - Rust 按选定方案统一注入标记。 - 主站请求携带标记。 - 第三方请求不携带标记。 - 必要的 CORS Header 配置验证。 不负责: - tracking event 落库。 - External v1 route tracking。 - 后台筛选和统计展示。 ### 8.2 `#225 添加客户端埋点统计` 负责: - 主站读取并校验客户端标记。 - 结合登录用户或 `ExternalApiPrincipal` 记录认证主体。 - 将标记写入 `tracking_event.metadata_json` 或后续确定的结构化字段。 - 补齐 `/api/external/v1/*` tracking。 - 后台查询、筛选或统计。 ## 9. 验收清单 ### 9.1 正向请求 - TS 登录请求带 `X-Genarrative-Client: agc`。 - TS 素材、钱包请求带标记。 - Rust 账号态 `/api/editor/*` 请求带标记。 - Rust 账号态 `/api/assets/*` 请求带标记。 - Rust 账号态 `/api/runtime/external-generation/jobs/*` 轮询带标记。 - Rust API Key 态 `/api/external/v1/*` 请求带标记。 - `/api/profile/api-keys` 请求带标记。 - 自定义主站 origin 的请求带标记。 ### 9.2 排除请求 - OSS multipart 上传不带标记。 - 签名 URL 下载不带标记。 - LLM Provider 请求不带标记。 - AGC 受控搜索不带标记。 - 本地工具桥请求不带标记。 ### 9.3 兼容性 - 原 Authorization 不变。 - 原 Idempotency-Key 不变。 - 原请求 body 不变。 - 原 timeout 和 redirect policy 不变。 - 浏览器跨域预检允许 `X-Genarrative-Client`。 - 未识别 marker 时主站业务请求不受影响。
Author
Member

看过现状后建议把 #226 收敛一下。

Header 注入本身确实简单:TS 侧已有统一出口 fetchClientHttp,在这里用 new Headers(init.headers) 后设置 X-Genarrative-Client: agc 即可,并保留调用方现有 Authorization、Content-Type、幂等键等 Header。

Rust 侧不能给进程内所有 reqwest::Client 无差别加默认 Header。当前 client 分别服务于主站、OSS/签名资源下载、LLM Provider、受控搜索和 loopback 工具桥,且 timeout、redirect、proxy 等策略不同;全局注入会把主站标记泄漏到第三方。

本 Issue 的实现范围建议定为:

  1. TS 在 fetchClientHttp 统一注入标记;
  2. Rust 增加主站专用 client factory,通过 default_headers 注入标记,并只替换主站请求的 client 创建点;
  3. 保留各处原有 timeout、connect timeout、redirect 和认证/幂等语义;
  4. 增加两类定向测试:主站请求带标记,OSS/签名 URL/Provider/搜索/loopback 请求不带标记。

方案二的 origin wrapper 可以留作后续增强,当前没有必要为一个固定 Header 新增完整 HTTP facade。CORS 服务端配置、接收落库和后台统计也请保持在服务端/#225 的边界内。建议先按上述范围实施并提 PR,避免继续在 Issue 里并列两套方案和展开完整设计文档。

看过现状后建议把 #226 收敛一下。 Header 注入本身确实简单:TS 侧已有统一出口 fetchClientHttp,在这里用 new Headers(init.headers) 后设置 X-Genarrative-Client: agc 即可,并保留调用方现有 Authorization、Content-Type、幂等键等 Header。 Rust 侧不能给进程内所有 reqwest::Client 无差别加默认 Header。当前 client 分别服务于主站、OSS/签名资源下载、LLM Provider、受控搜索和 loopback 工具桥,且 timeout、redirect、proxy 等策略不同;全局注入会把主站标记泄漏到第三方。 本 Issue 的实现范围建议定为: 1. TS 在 fetchClientHttp 统一注入标记; 2. Rust 增加主站专用 client factory,通过 default_headers 注入标记,并只替换主站请求的 client 创建点; 3. 保留各处原有 timeout、connect timeout、redirect 和认证/幂等语义; 4. 增加两类定向测试:主站请求带标记,OSS/签名 URL/Provider/搜索/loopback 请求不带标记。 方案二的 origin wrapper 可以留作后续增强,当前没有必要为一个固定 Header 新增完整 HTTP facade。CORS 服务端配置、接收落库和后台统计也请保持在服务端/#225 的边界内。建议先按上述范围实施并提 PR,避免继续在 Issue 里并列两套方案和展开完整设计文档。
Owner

本次 #226 只做 AGC 客户端侧请求标记,不修改 #225 的主站 tracking、数据库或后台实现。

冻结 Header:X-Genarrative-Client: agc。TS 在 fetchClientHttp 统一注入;Rust 增加主站专用 Client builder factory,通过 default_headers 注入,并只替换主站请求的 Client 创建点。账号态 /api/editor/api/assets/api/runtime 与 API Key 态 /api/external/v1 都发送同一标记。

保留各调用点现有 timeout、connect timeout、redirect、no_proxy、Authorization/Bearer、API Key、Idempotency-Key、请求体和响应语义。OSS 上传、签名 URL 下载、LLM Provider、受控搜索、loopback、更新下载和外部网页请求不得带标记。

#225 接收约定:精确值 agc 识别为 AGC;缺失/空值/未知值按未标记处理且不拒绝请求。第一阶段复用 tracking_event.metadata_json,写入 client: "agc",并按主站实际 method/path 记录;不要求 #226#225 回改设计。

> 本次 #226 只做 AGC 客户端侧请求标记,不修改 #225 的主站 tracking、数据库或后台实现。 > > 冻结 Header:`X-Genarrative-Client: agc`。TS 在 `fetchClientHttp` 统一注入;Rust 增加主站专用 Client builder factory,通过 `default_headers` 注入,并只替换主站请求的 Client 创建点。账号态 `/api/editor`、`/api/assets`、`/api/runtime` 与 API Key 态 `/api/external/v1` 都发送同一标记。 > > 保留各调用点现有 timeout、connect timeout、redirect、no_proxy、Authorization/Bearer、API Key、Idempotency-Key、请求体和响应语义。OSS 上传、签名 URL 下载、LLM Provider、受控搜索、loopback、更新下载和外部网页请求不得带标记。 > > #225 接收约定:精确值 `agc` 识别为 AGC;缺失/空值/未知值按未标记处理且不拒绝请求。第一阶段复用 `tracking_event.metadata_json`,写入 `client: "agc"`,并按主站实际 method/path 记录;不要求 #226 为 #225 回改设计。
lhk229 added a new dependency 2026-09-02 11:09:21 +08:00
Sign in to join this conversation.
2 Participants
Notifications
Total Time Spent: 15 seconds
kdletters
15 seconds
Due Date
2026-09-02
Depends on
Reference: GenarrativeAI/Genarrative#226