客户端埋点复用已有下载渠道配置
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
删除独立埋点 origin 环境变量及配置字段,按 dev/release 渠道校验官方站点。 本地开发、测试及容器的 dev 渠道允许规范回环地址,兼容可变端口。 同步环境变量示例、技术方案和运维说明,补充渠道隔离与身份校验测试。
This commit is contained in:
+2
-6
@@ -237,14 +237,10 @@ GENARRATIVE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR="false"
|
||||
|
||||
# 官网客户端下载检测渠道:dev、release 或自定义渠道;修改后重启 API 服务。
|
||||
# Windows/macOS 是系统维度,不填写 dev-win/dev-mac。
|
||||
# 客户端埋点也复用该渠道:dev 对应 https://dev.genarrative.world,release 对应 https://www.genarrative.world。
|
||||
# 埋点不接受其它渠道;本地 dev 且 GENARRATIVE_ENV 为 development(默认)/test/container 时允许 loopback 地址及可变端口。
|
||||
GENARRATIVE_CLIENT_DOWNLOAD_CHANNEL="dev"
|
||||
|
||||
# 客户端埋点接收绑定的公开 origin,由 API Server 运行时读取;修改后重启服务。
|
||||
# 必须与客户端登录地址一致,不带 /api、路径或尾部斜杠;未配置/非法时上传接口返回 503。
|
||||
# 本地端口按实际启动结果填写(端口漂移后需同步),localhost 与 127.0.0.1 不可混用。
|
||||
# dev 使用 https://dev.genarrative.world;release 使用 https://www.genarrative.world。
|
||||
GENARRATIVE_AGC_ANALYTICS_ORIGIN="http://127.0.0.1:8082"
|
||||
|
||||
# Optional: official VikingDB credentials for regenerating build-tag similarities
|
||||
# with the Python embedding script. The script auto-loads `.env.local` and uses
|
||||
# the fixed `bge-large-zh` embedding model.
|
||||
|
||||
@@ -5,11 +5,10 @@
|
||||
GENARRATIVE_ENV=container
|
||||
GENARRATIVE_API_HOST=0.0.0.0
|
||||
GENARRATIVE_API_PORT=8082
|
||||
# 客户端埋点接收绑定的公开 origin,由 API Server 运行时读取;修改后重启服务。
|
||||
# 必须与客户端登录地址一致,不带 /api、路径或尾部斜杠;未配置/非法时上传接口返回 503。
|
||||
# 以下为 compose 默认宿主机入口;更改映射端口或接入域名时同步修改,不填容器内部地址。
|
||||
# dev 使用 https://dev.genarrative.world;release 使用 https://www.genarrative.world。
|
||||
GENARRATIVE_AGC_ANALYTICS_ORIGIN=http://127.0.0.1:18080
|
||||
# 官网客户端下载与客户端埋点共用部署渠道;修改后重启 API Server。
|
||||
# dev 对应 https://dev.genarrative.world;container + dev 额外允许 loopback 地址及可变映射端口。
|
||||
# release 对应 https://www.genarrative.world,且不接受 loopback 地址;其它渠道不接收埋点。
|
||||
GENARRATIVE_CLIENT_DOWNLOAD_CHANNEL=dev
|
||||
GENARRATIVE_API_LOG=info,tower_http=info
|
||||
GENARRATIVE_API_LISTEN_BACKLOG=1024
|
||||
GENARRATIVE_API_WORKER_THREADS=4
|
||||
|
||||
Vendored
+4
-4
@@ -4,10 +4,10 @@
|
||||
GENARRATIVE_ENV=production
|
||||
GENARRATIVE_API_HOST=127.0.0.1
|
||||
GENARRATIVE_API_PORT=8082
|
||||
# 客户端埋点接收绑定的公开 origin,由 API Server 运行时读取;修改后重启服务。
|
||||
# 必须与客户端登录地址一致,不带 /api、路径或尾部斜杠;未配置/非法时上传接口返回 503。
|
||||
# 以下为 release;dev 部署改为 https://dev.genarrative.world。
|
||||
GENARRATIVE_AGC_ANALYTICS_ORIGIN=https://www.genarrative.world
|
||||
# 官网客户端下载与客户端埋点共用部署渠道;修改后重启 API Server。
|
||||
# release 对应 https://www.genarrative.world;dev 部署改为 dev,对应 https://dev.genarrative.world。
|
||||
# production 环境的埋点不接受 loopback 地址;其它渠道不接收埋点。
|
||||
GENARRATIVE_CLIENT_DOWNLOAD_CHANNEL=release
|
||||
GENARRATIVE_API_LOG=info,tower_http=info
|
||||
GENARRATIVE_API_LISTEN_BACKLOG=1024
|
||||
GENARRATIVE_API_WORKER_THREADS=4
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
|
||||
- 当前合同唯一维护入口为[客户端本地埋点与主站入库契约](../../technical/【技术方案】客户端本地埋点与主站入库契约-2026-09-21.md),原始需求作为仓库内历史来源保存;后续里程碑规范与实施计划放在 `docs/project-memory/plans/`。
|
||||
- 本地采集阶段已验收明文 JSONL 持久化;当前仍不做加密。一个项目对应一个目标,事件按业务节点采集,5 分钟封存,7 天或 20 MiB 清理;上传失败也受保留上限约束。
|
||||
- 当前上传实现已完成隔离环境验收,证据见同一主规范第 13 节:每 15 分钟上传匹配当前账号与平台的封存批次,新增一张客户端事件私有表、批次原子入库与幂等确认、成功清理及独立后台明细栏目;失败静默留待下周期重试。真实客户端文件、HTTP、数据库与后台查询已关联同一事件验证,浏览器列表/筛选/详情通过;未部署生产。保持原 12 类事件和原采集边界。上线须配置 `GENARRATIVE_AGC_ANALYTICS_ORIGIN`,按数据库、API/后台、客户端顺序发布。
|
||||
- 当前上传实现已完成隔离环境验收,证据见同一主规范第 13 节:每 15 分钟上传匹配当前账号与平台的封存批次,新增一张客户端事件私有表、批次原子入库与幂等确认、成功清理及独立后台明细栏目;失败静默留待下周期重试。真实客户端文件、HTTP、数据库与后台查询已关联同一事件验证,浏览器列表/筛选/详情通过;未部署生产。保持原 12 类事件和原采集边界。埋点接收复用 `GENARRATIVE_CLIENT_DOWNLOAD_CHANNEL` 的 dev/release 官方站点映射;仅 dev 渠道且 `GENARRATIVE_ENV` 为 development/test/container 时额外接受 loopback origin,无独立埋点环境变量。按数据库、API/后台、客户端顺序发布。
|
||||
- 已按技术负责人授权开始实施:合同与本地队列、会话窗口与项目接入、策划阶段成果、首次提交及两类 Agent run 已实现并经独立审查;定向测试、生产编译和前序 GUI 启停证据统一见主规范第 12 节。不得宣称完整产品采集已上线。
|
||||
## 2026-09-22 引用输入区改为宿主注入引用 provider,选择器面板与输入区分离
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 客户端本地埋点与主站入库契约
|
||||
|
||||
Version: 0.31
|
||||
Version: 0.32
|
||||
Status: 本地采集及上传、入库、成功清理、后台查询已实现并完成本地隔离环境验收;未部署生产
|
||||
Date: 2026-09-21
|
||||
需求来源:[Game Agent 埋点设计原始方案](./【需求来源】GameAgent埋点设计原始方案-2026-09-05.md);原始方案与后续已确认决策有差异时,以本文为准。
|
||||
@@ -609,9 +609,9 @@ session.json 是本地恢复元数据,不是待上传事件;事件文件不
|
||||
|
||||
- 一次请求只发一个已封存批次,1 至 500 条。事件数据沿用 1 MiB 上限,JSON 请求体上限 2 MiB,容纳数组及 envelope 开销;服务端不依赖 Content-Length 自报大小。
|
||||
- 批次与所有事件的 user_id 必须相同且等于鉴权主体;匿名、未知 origin 批次不上传,不补认领到后来登录的账号。
|
||||
- destination_origin 必须匹配当前服务部署的可信公开 origin 配置,不使用未经信任的 Host 头作为判据。
|
||||
- 接收端配置 `GENARRATIVE_AGC_ANALYTICS_ORIGIN` 为规范 origin(无路径和尾部斜杠):正式站 `https://www.genarrative.world`,测试站 `https://dev.genarrative.world`,本地联调填写客户端实际连接的 loopback origin。未配置/非法配置时接口返回 503,不猜测默认站点;允许 HTTPS 和本地 loopback HTTP。
|
||||
- 该变量在 API Server 运行时注入,修改后重启服务;客户端与管理后台无需新增埋点构建变量。配置示例见根目录 `.env.example`(本地默认 `http://127.0.0.1:8082`)、`deploy/env/api-server.env.example`(release,并注明 dev 值)与 `deploy/container/api-server.env.example`(compose 默认宿主机入口 `http://127.0.0.1:18080`)。实际端口、映射或域名变化时同步修改,始终与客户端登录地址一致。
|
||||
- 接收端复用 API Server 已有的 `GENARRATIVE_CLIENT_DOWNLOAD_CHANNEL`,不再单独配置埋点 origin:`dev` 只对应 `https://dev.genarrative.world`,`release` 只对应 `https://www.genarrative.world`;渠道未设置时沿用现有默认 `dev`,空值或其它渠道返回 503。destination_origin 与渠道不匹配时返回 403,不使用客户端请求的 Host / Forwarded 头推断部署渠道。
|
||||
- 本地联调例外:仅 `dev` 渠道且已有 `GENARRATIVE_ENV` 为 `development`(现有默认)、`test` 或 `container` 时,额外接受规范的 HTTP(S) loopback origin(localhost、IPv4/IPv6 回环地址,无路径、查询串、凭据或尾部斜杠)。端口可变以兼容本地漂移和容器映射;服务端不再按本地端口区分实例,客户端仍按精确登录 origin 与 user_id 隔离批次。`production` 环境或 `release` 渠道均不接受此例外。
|
||||
- 渠道与环境在 API Server 运行时读取,修改后重启服务;客户端与管理后台无需新增埋点构建变量。配置示例见根目录 `.env.example`(本地 dev)、`deploy/env/api-server.env.example`(release,dev 部署改为 dev)与 `deploy/container/api-server.env.example`(container + dev)。
|
||||
- 复用现有事件版本、枚举、字段长度与必填 nullable 校验;批内重复 event_id、未知字段或非法事件整批拒绝。
|
||||
- 本接口不进入普通成功路由 tracking 映射,不为每次上传另生成主站产品事件。
|
||||
|
||||
|
||||
@@ -571,6 +571,12 @@ curl -fsS --max-time 5 http://127.0.0.1/api/editor/showcase/resources >/dev/null
|
||||
|
||||
角色动画源帧 PUT、透明帧 PUT 和最终帧 HEAD 使用 `AppState` 内同一个 OSS HTTP Client/连接池,并受进程级 8 路 OSS permit 保护;BgFilter、阿里云抠图和本地处理不占用该 permit。每个 OSS attempt 最多 3 次(首次 + 2 次重试),退避为 250ms、500ms;只重试 timeout、无 HTTP 响应传输错误、OSS PutObject 的 `400 + RequestTimeout`、PUT 400 错误体读取失败(未解析出 `Code`,按 timeout/transport 归类)、408、429 和 500–599。动作帧 PUT 收到 400 时只读取最多 16 KiB OSS 错误 XML,提取 `Code` 和 `RequestId`;`oss_request_id` 优先使用响应头 `x-oss-request-id`,XML 字段只作回退。错误体读取超时/断流不再按确定性 400 处理:已解析出的 `Code` 优先生效;未解析出 `Code` 时按读取失败原因置 `timeout`/`transport` 并重试,message 追加「错误响应体读取失败」。日志字段包括 `frame_index`、`object_key`、`operation=source_put|final_put|final_head`、`attempt`、`max_attempts`、`retryable`、`will_retry`、`retry_delay_ms`、`permit_wait_ms`、`timeout`、`connect`、`transport`、`oss_code`、`oss_request_id`、`status` 和 `elapsed_ms`。`请求 OSS 失败` 时,`timeout/connect/transport=true` 表示传输类失败;`status=400, oss_code=RequestTimeout, timeout=true`、`status=429` 或 `500–599` 表示暂时性失败,PUT 的 `status=400`、`oss_code` 为空且 `timeout=true` 或 `transport=true`(message 含「错误响应体读取失败」)同样是暂时性失败。除 `RequestTimeout` 和该错误体读取失败两类例外外,其他 400、401/403/404、配置、URL 和签名错误是确定性失败,不会重试。最终帧 HEAD 失败只会重试 HEAD,不会重复 PUT;如果任一帧最终失败,确认整段动作已排空已启动 Future,并检查任务按现有契约退款且没有发布缺帧动画。
|
||||
|
||||
### AGC 客户端埋点接收渠道
|
||||
|
||||
埋点接收复用 API Server 运行时的 `GENARRATIVE_CLIENT_DOWNLOAD_CHANNEL`:测试站设置 `dev`,匹配 `https://dev.genarrative.world`;正式站必须设置 `release`,匹配 `https://www.genarrative.world`。修改后重启 API Server。未设置时沿用现有默认 `dev`;空值或其它渠道的埋点上传返回 503。
|
||||
|
||||
本地联调使用 `dev`,且 `GENARRATIVE_ENV` 为 `development`(默认)、`test` 或 `container` 时,允许规范 HTTP(S) loopback 地址及可变端口,无需配置独立埋点变量。客户端登录时使用实际 API 入口,容器使用宿主机映射入口。线上部署设置 `GENARRATIVE_ENV=production`,不接受 loopback 例外;详细合同见[客户端本地埋点与主站入库契约](./technical/【技术方案】客户端本地埋点与主站入库契约-2026-09-21.md)第 13 节。
|
||||
|
||||
### AGC 项目快照上传目标
|
||||
|
||||
后台“项目工程”(`/admin/#project-snapshots`)按项目列出远端快照,默认只看本部署渠道,顶部“渠道”选择框可切换远端已存在的其它渠道;列表按游标分页(每页 20/50/100,上一页复用已取得的游标,远端不给总数所以只显示当前页)。完整快照提供“下载完整工程”,按原始目录返回 ZIP;未完成同步的项目暂不可下载,旧清单缺少完整性声明时显示“完整性未知”,只能“下载已存文件”。“用户”列与“素材查询”同口径展示昵称与陶泥号,并可点开用户详情;不要直接把 OSS 的 `files/{size}-{digest}/` 目录下载当成工程。
|
||||
|
||||
@@ -14,6 +14,7 @@ use crate::{
|
||||
admin::{AuthenticatedAdmin, require_admin_auth},
|
||||
api_response::json_success_body,
|
||||
auth::{AuthenticatedAccessToken, require_bearer_auth},
|
||||
config::AppConfig,
|
||||
http_error::AppError,
|
||||
request_context::RequestContext,
|
||||
state::AppState,
|
||||
@@ -65,11 +66,7 @@ async fn upload_batch(
|
||||
let batch: AgcAnalyticsBatch = serde_json::from_value(raw).map_err(|_| {
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_message("客户端埋点字段不符合合同")
|
||||
})?;
|
||||
validate_subject_and_origin(
|
||||
&batch,
|
||||
auth.claims().user_id(),
|
||||
&state.config.agc_analytics_origin,
|
||||
)?;
|
||||
validate_subject_and_origin(&batch, auth.claims().user_id(), &state.config)?;
|
||||
module_runtime::agc_analytics::validate_agc_analytics_batch(&batch).map_err(|error| {
|
||||
let status = if error == "events_too_large" {
|
||||
StatusCode::PAYLOAD_TOO_LARGE
|
||||
@@ -106,29 +103,38 @@ async fn list_events(
|
||||
fn validate_subject_and_origin(
|
||||
batch: &AgcAnalyticsBatch,
|
||||
user_id: &str,
|
||||
expected_origin: &str,
|
||||
config: &AppConfig,
|
||||
) -> Result<(), AppError> {
|
||||
// 公开地址必须来自部署配置;客户端 body 和 Host 头均不是配置来源。
|
||||
let origin = url::Url::parse(expected_origin).ok().filter(|url| {
|
||||
let loopback = url.host_str().is_some_and(|host| {
|
||||
host == "localhost"
|
||||
|| host
|
||||
.trim_matches(['[', ']'])
|
||||
.parse::<std::net::IpAddr>()
|
||||
.is_ok_and(|ip| ip.is_loopback())
|
||||
// 复用部署渠道,不使用客户端 body 或 Host / Forwarded 头推断服务器所属平台。
|
||||
let expected_origin = match config.client_download_channel.as_str() {
|
||||
"dev" => "https://dev.genarrative.world",
|
||||
"release" => "https://www.genarrative.world",
|
||||
_ => {
|
||||
return Err(AppError::from_status(StatusCode::SERVICE_UNAVAILABLE)
|
||||
.with_message("客户端埋点接收渠道无效"));
|
||||
}
|
||||
};
|
||||
// 本地开发允许回环地址及可变端口,兼容端口漂移和容器映射;线上部署不启用此例外。
|
||||
let local_origin = config.client_download_channel == "dev"
|
||||
&& matches!(
|
||||
config.environment.as_str(),
|
||||
"development" | "test" | "container"
|
||||
)
|
||||
&& url::Url::parse(&batch.destination_origin).is_ok_and(|url| {
|
||||
matches!(url.scheme(), "http" | "https")
|
||||
&& url.host_str().is_some_and(|host| {
|
||||
host == "localhost"
|
||||
|| host
|
||||
.trim_matches(['[', ']'])
|
||||
.parse::<std::net::IpAddr>()
|
||||
.is_ok_and(|ip| ip.is_loopback())
|
||||
})
|
||||
&& url.username().is_empty()
|
||||
&& url.password().is_none()
|
||||
&& url.origin().ascii_serialization() == batch.destination_origin
|
||||
});
|
||||
(url.scheme() == "https" || (url.scheme() == "http" && loopback))
|
||||
&& url.host_str().is_some()
|
||||
&& url.username().is_empty()
|
||||
&& url.password().is_none()
|
||||
&& url.origin().ascii_serialization() == expected_origin
|
||||
});
|
||||
if origin.is_none() {
|
||||
return Err(AppError::from_status(StatusCode::SERVICE_UNAVAILABLE)
|
||||
.with_message("客户端埋点接收地址尚未配置"));
|
||||
}
|
||||
if batch.user_id != user_id
|
||||
|| batch.destination_origin != expected_origin
|
||||
|| (batch.destination_origin != expected_origin && !local_origin)
|
||||
|| batch
|
||||
.events
|
||||
.iter()
|
||||
@@ -173,41 +179,117 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn agc_analytics_subject_and_deployment_are_both_required() {
|
||||
let batch = batch();
|
||||
assert!(validate_subject_and_origin(&batch, "user-a", &batch.destination_origin).is_ok());
|
||||
for (user, origin, expected) in [
|
||||
for (channel, origin, other_origin) in [
|
||||
(
|
||||
"user-b",
|
||||
"dev",
|
||||
"https://dev.genarrative.world",
|
||||
StatusCode::FORBIDDEN,
|
||||
),
|
||||
(
|
||||
"user-a",
|
||||
"https://www.genarrative.world",
|
||||
StatusCode::FORBIDDEN,
|
||||
),
|
||||
("user-a", "", StatusCode::SERVICE_UNAVAILABLE),
|
||||
(
|
||||
"user-a",
|
||||
"https://dev.genarrative.world/path",
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
),
|
||||
(
|
||||
"user-a",
|
||||
"http://dev.genarrative.world",
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
"release",
|
||||
"https://www.genarrative.world",
|
||||
"https://dev.genarrative.world",
|
||||
),
|
||||
] {
|
||||
let config = AppConfig {
|
||||
environment: "production".into(),
|
||||
client_download_channel: channel.into(),
|
||||
..AppConfig::default()
|
||||
};
|
||||
let mut batch = batch();
|
||||
batch.destination_origin = origin.into();
|
||||
assert!(validate_subject_and_origin(&batch, "user-a", &config).is_ok());
|
||||
assert_eq!(
|
||||
validate_subject_and_origin(&batch, user, origin)
|
||||
validate_subject_and_origin(&batch, "user-b", &config)
|
||||
.unwrap_err()
|
||||
.status_code(),
|
||||
expected
|
||||
StatusCode::FORBIDDEN
|
||||
);
|
||||
for rejected in [
|
||||
other_origin,
|
||||
"http://127.0.0.1:8082",
|
||||
"https://custom.example",
|
||||
] {
|
||||
batch.destination_origin = rejected.into();
|
||||
assert_eq!(
|
||||
validate_subject_and_origin(&batch, "user-a", &config)
|
||||
.unwrap_err()
|
||||
.status_code(),
|
||||
StatusCode::FORBIDDEN,
|
||||
"{channel}: {rejected}"
|
||||
);
|
||||
}
|
||||
}
|
||||
for channel in ["", "qa-2026", "dev-win"] {
|
||||
let config = AppConfig {
|
||||
client_download_channel: channel.into(),
|
||||
..AppConfig::default()
|
||||
};
|
||||
assert_eq!(
|
||||
validate_subject_and_origin(&batch(), "user-a", &config)
|
||||
.unwrap_err()
|
||||
.status_code(),
|
||||
StatusCode::SERVICE_UNAVAILABLE
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agc_analytics_loopback_supports_local_ports_only_in_dev_environments() {
|
||||
let mut batch = batch();
|
||||
for environment in ["development", "test", "container", "production", "staging"] {
|
||||
for channel in ["dev", "release"] {
|
||||
let config = AppConfig {
|
||||
environment: environment.into(),
|
||||
client_download_channel: channel.into(),
|
||||
..AppConfig::default()
|
||||
};
|
||||
let allowed =
|
||||
channel == "dev" && matches!(environment, "development" | "test" | "container");
|
||||
for origin in [
|
||||
"http://127.0.0.1:8082",
|
||||
"http://localhost:18080",
|
||||
"http://[::1]:19001",
|
||||
"https://localhost:8443",
|
||||
] {
|
||||
batch.destination_origin = origin.into();
|
||||
assert_eq!(
|
||||
validate_subject_and_origin(&batch, "user-a", &config).is_ok(),
|
||||
allowed,
|
||||
"{environment}/{channel}: {origin}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agc_analytics_local_exception_rejects_noncanonical_and_nonloopback_origins() {
|
||||
let config = AppConfig::default();
|
||||
let mut batch = batch();
|
||||
for origin in [
|
||||
"http://192.168.1.2:8082",
|
||||
"http://localhost.example:8082",
|
||||
"http://0.0.0.0:8082",
|
||||
"http://localhost:8082/",
|
||||
"http://localhost:8082/api",
|
||||
"http://localhost:8082?x=1",
|
||||
"http://localhost:8082#fragment",
|
||||
"http://user:password@localhost:8082",
|
||||
"ftp://localhost:8082",
|
||||
"https://dev.genarrative.world/",
|
||||
"http://dev.genarrative.world",
|
||||
"",
|
||||
] {
|
||||
batch.destination_origin = origin.into();
|
||||
assert_eq!(
|
||||
validate_subject_and_origin(&batch, "user-a", &config)
|
||||
.unwrap_err()
|
||||
.status_code(),
|
||||
StatusCode::FORBIDDEN,
|
||||
"{origin}"
|
||||
);
|
||||
}
|
||||
let mut local = batch;
|
||||
local.destination_origin = "http://127.0.0.1:8082".into();
|
||||
assert!(validate_subject_and_origin(&local, "user-a", &local.destination_origin).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -282,7 +282,8 @@ mod tests {
|
||||
spacetime_server_url: db_url.into(),
|
||||
spacetime_database: connection["database"].as_str().unwrap().into(),
|
||||
spacetime_token: Some(connection["token"].as_str().unwrap().into()),
|
||||
agc_analytics_origin: origin.clone(),
|
||||
environment: "test".into(),
|
||||
client_download_channel: "dev".into(),
|
||||
admin_username: Some("analytics-smoke-owner".into()),
|
||||
admin_password: Some("analytics-smoke-fixture-password".into()),
|
||||
..AppConfig::default()
|
||||
@@ -418,7 +419,8 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn agc_analytics_upload_enforces_auth_identity_contract_and_body_limit() {
|
||||
let state = AppState::new(AppConfig {
|
||||
agc_analytics_origin: "https://dev.genarrative.world".into(),
|
||||
environment: "production".into(),
|
||||
client_download_channel: "dev".into(),
|
||||
admin_username: Some("analytics-smoke-owner".into()),
|
||||
admin_password: Some("analytics-smoke-fixture-password".into()),
|
||||
..AppConfig::default()
|
||||
@@ -444,12 +446,18 @@ mod tests {
|
||||
wrong_user["user_id"] = "other-user".into();
|
||||
let mut wrong_origin = payload.clone();
|
||||
wrong_origin["destination_origin"] = "https://www.genarrative.world".into();
|
||||
let mut local_origin = payload.clone();
|
||||
local_origin["destination_origin"] = "http://127.0.0.1:8082".into();
|
||||
let mut wrong_event_user = payload.clone();
|
||||
wrong_event_user["events"][0]["user_id"] = "other-user".into();
|
||||
let mut bad_contract = payload.clone();
|
||||
bad_contract["events"][0]["properties"]["prompt"] = "not collected".into();
|
||||
for (body, authenticated, expected) in [
|
||||
(payload.to_string(), false, StatusCode::UNAUTHORIZED),
|
||||
(wrong_user.to_string(), true, StatusCode::FORBIDDEN),
|
||||
(wrong_origin.to_string(), true, StatusCode::FORBIDDEN),
|
||||
(local_origin.to_string(), true, StatusCode::FORBIDDEN),
|
||||
(wrong_event_user.to_string(), true, StatusCode::FORBIDDEN),
|
||||
(bad_contract.to_string(), true, StatusCode::BAD_REQUEST),
|
||||
("{".into(), true, StatusCode::BAD_REQUEST),
|
||||
(
|
||||
@@ -461,6 +469,8 @@ mod tests {
|
||||
let mut request = Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/agc/analytics/batches")
|
||||
.header("host", "www.genarrative.world")
|
||||
.header("x-forwarded-host", "www.genarrative.world")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-genarrative-response-envelope", "1");
|
||||
if authenticated {
|
||||
|
||||
@@ -92,8 +92,6 @@ pub struct AppConfig {
|
||||
pub editor_bgfilter_circuit_cooldown: Duration,
|
||||
pub image_editor_agent_sidebar_enabled: bool,
|
||||
pub client_download_channel: String,
|
||||
/// 客户端埋点接收所绑定的公开 origin;未配置时拒绝上传。
|
||||
pub agc_analytics_origin: String,
|
||||
/// AGC 项目快照的部署渠道:上传与后台默认查询都按它分区。
|
||||
pub project_snapshot_channel: String,
|
||||
pub log_filter: String,
|
||||
@@ -399,7 +397,6 @@ impl Default for AppConfig {
|
||||
),
|
||||
image_editor_agent_sidebar_enabled: false,
|
||||
client_download_channel: "dev".to_string(),
|
||||
agc_analytics_origin: String::new(),
|
||||
project_snapshot_channel: "dev".to_string(),
|
||||
log_filter: "info,tower_http=info".to_string(),
|
||||
otel_enabled: false,
|
||||
@@ -723,9 +720,6 @@ impl AppConfig {
|
||||
// 显式空值或非法值也保留,由下载入口失败关闭,不能悄悄改读 dev。
|
||||
config.client_download_channel = channel.trim().to_string();
|
||||
}
|
||||
if let Ok(origin) = std::env::var("GENARRATIVE_AGC_ANALYTICS_ORIGIN") {
|
||||
config.agc_analytics_origin = origin.trim().to_string();
|
||||
}
|
||||
// 快照渠道缺省沿用同一个部署渠道(本部署的客户端渠道),显式配置优先;
|
||||
// 显式空值或非法值同样保留,由快照入口失败关闭。
|
||||
config.project_snapshot_channel = config.client_download_channel.clone();
|
||||
|
||||
Reference in New Issue
Block a user