diff --git a/backend/crates/gateway-plugin/runtime/tests/callback/keys.rs b/backend/crates/gateway-plugin/runtime/tests/callback/keys.rs index e4437d25c..9a5bbf25e 100644 --- a/backend/crates/gateway-plugin/runtime/tests/callback/keys.rs +++ b/backend/crates/gateway-plugin/runtime/tests/callback/keys.rs @@ -106,7 +106,7 @@ async fn plugin_process_manages_native_budget_through_client_key_service() { "client_key_id":"key_budget", "daily_limit_usd":"10", "weekly_limit_usd":"20", "daily_used_usd":"3", "weekly_used_usd":"0", "daily_resets_at_ms":before.budget.daily_resets_at.map(|time| chrono::DateTime::::from(time).timestamp_millis()), - "weekly_resets_at_ms":before.budget.weekly_resets_at.map(|time| chrono::DateTime::::from(time).timestamp_millis()), + "weekly_resets_at_ms":null, }); assert_eq!(results[6], expected); assert_eq!(results[7], json!({"client_key_id":"key_budget"})); @@ -126,10 +126,7 @@ async fn plugin_process_manages_native_budget_through_client_key_service() { 1 ); assert_eq!(after.budget.daily_resets_at, before.budget.daily_resets_at); - assert_eq!( - after.budget.weekly_resets_at, - before.budget.weekly_resets_at - ); + assert_eq!(after.budget.weekly_resets_at, None); assert_eq!(audits.len(), 1); assert!(audits[0].starts_with(&format!("plugin:{}:scope:", view.target.instance_id))); } diff --git a/backend/crates/gateway-plugin/runtime/tests/support/environment.rs b/backend/crates/gateway-plugin/runtime/tests/support/environment.rs index 046323836..1e25fb32d 100644 --- a/backend/crates/gateway-plugin/runtime/tests/support/environment.rs +++ b/backend/crates/gateway-plugin/runtime/tests/support/environment.rs @@ -644,8 +644,24 @@ impl Environment { } async fn create_with_store_mode(command_line: bool) -> Option { - let database = std::env::var("CPR_PLUGIN_TEST_DATABASE_URL").ok()?; - let redis = std::env::var("CPR_PLUGIN_TEST_REDIS_URL").expect("isolated plugin Redis URL"); + // 插件专用服务优先;CI 的标准测试服务同样通过随机 schema 隔离数据库。 + let (database, redis) = if let Ok(database) = std::env::var("CPR_PLUGIN_TEST_DATABASE_URL") + { + ( + database, + std::env::var("CPR_PLUGIN_TEST_REDIS_URL").expect("isolated plugin Redis URL"), + ) + } else { + let database = std::env::var("CPR_TEST_DATABASE_URL").ok(); + assert!( + database.is_some() || std::env::var_os("CI").is_none(), + "CI requires plugin or standard test services" + ); + ( + database?, + std::env::var("CPR_TEST_REDIS_URL").expect("isolated test Redis URL"), + ) + }; let schema = format!("cpr_plugin_{}", uuid::Uuid::new_v4().simple()); let admin = PgPoolOptions::new() .max_connections(1) diff --git a/backend/crates/gateway-plugin/sdk/docs/capabilities.md b/backend/crates/gateway-plugin/sdk/docs/capabilities.md index 58e9117ad..b05a17382 100644 --- a/backend/crates/gateway-plugin/sdk/docs/capabilities.md +++ b/backend/crates/gateway-plugin/sdk/docs/capabilities.md @@ -186,6 +186,12 @@ SDK 不自动重试。超时或断连不能证明写入未提交;重试上限 不存在的 Key 返回 `rejected`;写入事务复验发现实例停用或版本变化时返回 `conflict`, 非法输入返回 `invalid_input`。账号关联、预算分配和重置触发由插件决定,宿主不自动串联上述接口 +插件的外部周期、截止时间和同步进度保存在自身 `host.state.*` 中,并通过自己的管理页面展示; +这些数据不会改写宿主日/周窗口及其显示。插件可在 `request` 中间件中按自己的策略提前拒绝请求, +在确认新周期后调用预算重置或上限修改接口;调用仍遵循上述原生语义,宿主窗口继续按自身日期规则滚动。 +私有状态写入与预算操作不是一个事务,不能把私有状态标记当作预算操作已提交的证明;结果未知时须按上述合同处理。 +停用插件会停止其后续拦截与同步,已执行的额度修改或重置不会自动撤销 + ### Key、模型与模型调用 模型调用方法: diff --git a/backend/crates/gateway-plugin/sdk/src/client/keys.rs b/backend/crates/gateway-plugin/sdk/src/client/keys.rs index 28dd9a318..c912a0118 100644 --- a/backend/crates/gateway-plugin/sdk/src/client/keys.rs +++ b/backend/crates/gateway-plugin/sdk/src/client/keys.rs @@ -53,7 +53,7 @@ impl HostClient { serde_json::from_value(reply.result).map_err(|_| invalid()) } - /// 清零指定周期,保留限额和到期时间;结果未知时不能盲目重试。 + /// 清零并关闭指定周期,保留限额,下次使用时重新开启;结果未知时不能盲目重试。 /// /// # Errors /// 实例过期、Key 不存在、宿主写入失败时返回错误。 diff --git a/docs/architecture.md b/docs/architecture.md index a4e0f08de..d55f4465a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -561,7 +561,8 @@ Admin 的手动重置复用同一账本与 Key 行锁,在一个事务中清零 插件预算回调通过 `PluginClientKeyAccess` 进入同一 `ClientKeyService` 和 `ClientKeyStore`,存储事务先复核插件实例版本, 再执行原生预算操作;不建立另一份预算状态。查询复用账本投影,不触发准入。局部更新金额上限不改用量或窗口, 仅在值变化时提交配置 revision、审计并发布;重置仍不推进配置版本。重复调用及结果未知的合同见 -[SDK 预算接口](../backend/crates/gateway-plugin/sdk/docs/capabilities.md#client-key-预算) +[SDK 预算接口](../backend/crates/gateway-plugin/sdk/docs/capabilities.md#client-key-预算)。插件的外部周期和截止时间属于插件私有状态,展示在插件页面; +宿主预算接口和管理页面只表达原生窗口事实,不为插件周期修改滚动规则或重置语义 - 日窗口按北京时间零点划分;七天窗口从首次准入当天零点开始,到期后由下一次使用重新开启,不固定为周一 - 金额优先使用 Provider 上报的 USD,否则按现有模型价格估算;订阅账号的估算费用不代表上游订阅账单。 @@ -849,7 +850,8 @@ RUST_MIN_STACK=16777216 cargo +1.97.0 test --manifest-path backend/Cargo.toml -- 其他检查与界面验证按 [贡献与审查](../CONTRIBUTING.md#验证) 执行 插件 Runtime 的真实子进程与持久化测试使用 `CPR_PLUGIN_TEST_DATABASE_URL` 和 -`CPR_PLUGIN_TEST_REDIS_URL` 指向专用实例,密码及隔离要求与上述 Store 测试一致。 +`CPR_PLUGIN_TEST_REDIS_URL` 指向专用实例;未提供插件专用变量时复用 `CPR_TEST_DATABASE_URL` 与 `CPR_TEST_REDIS_URL`。 +密码及隔离要求与上述 Store 测试一致,CI 缺少服务配置时直接失败。 设置 `CPR_PLUGIN_TEST_LIVE_HTTP=1` 会额外请求 GitHub 公共 HTTPS API,验证受管出站链路;这不代表模型推理验收。 若测试环境使用 Fake-IP 或私网 DNS,需通过 `CPR_PLUGIN_TEST_LIVE_NETWORK_RANGES` 显式提供逗号分隔的 CIDR 授权。该选项只用于真实网络测试,默认为空,不改变生产网络策略或其他测试的授权