diff --git a/.agents/skills/cpr-plugin-dev/references/implementation.md b/.agents/skills/cpr-plugin-dev/references/implementation.md index 7f1abaea0..ce18296db 100644 --- a/.agents/skills/cpr-plugin-dev/references/implementation.md +++ b/.agents/skills/cpr-plugin-dev/references/implementation.md @@ -6,7 +6,7 @@ ## 清单与处理器 - `publisher` 与机器短名 `name` 派生插件 ID,`displayName` 用于展示;复制示例后同时调整扩展项 ID 与注册结果 -- `contributes` 每种能力最多一项,普通贡献项可省略派生 ID、能力版本和固定阶段;中间件必须声明版本 `3` 并显式选择阶段。自定义完整扩展项 ID 属于自己的插件命名空间,运行注册与规范化清单一致 +- `contributes` 每种能力最多一项,普通贡献项可省略派生 ID、能力版本和固定阶段;中间件必须声明版本 `4` 并显式选择阶段,上游适配器必须声明版本 `2`。自定义完整扩展项 ID 属于自己的插件命名空间,运行注册与规范化清单一致 - 作者清单用 `Manifest::from_author_slice` 规范化,不手写 `package`、固定阶段或重复注册;CLI 生成平台、协议与资源摘要,版本范围不能使用全版本通配 - 安装后宿主会按 `configurationSchema` 准备默认配置,配置完整即可启用;默认绑定的空范围不限制请求。需要业务参数时声明真实必填项,通过 `secretFields` 声明敏感字段,不依赖用户再走一遍自建安装向导 - Rust 插件只依赖公开 `gateway-plugin-sdk`;需要异步会话辅助时开启 `io`,使用 `PluginSession` 管理握手、回调关联、流控、取消和关闭 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5bd673674..370d1f886 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -177,8 +177,6 @@ jobs: host_path = "backend/crates/gateway-plugin/runtime/plugin-host-compatibility.json" host = JSON.parse(File.read(host_path)) abort "Invalid plugin host compatibility declaration: #{host_path}" unless host["schema_version"] == 2 - # 保留旧更新器可解析的传输格式,实际支持范围仍取自当前宿主合同。 - host = host.merge("schema_version" => 1, "permissions" => []) # 清单只依赖版本与兼容声明,平台归档和镜像消费同一份封口结果。 manifest = { "schema_version" => 1, diff --git a/backend/crates/gateway-api/src/admin/system.rs b/backend/crates/gateway-api/src/admin/system.rs index faf4353db..2a933f7c2 100644 --- a/backend/crates/gateway-api/src/admin/system.rs +++ b/backend/crates/gateway-api/src/admin/system.rs @@ -114,7 +114,6 @@ struct SystemUpdateDetailView { notes: Option, cached: bool, update_supported: bool, - restart_confirmation_supported: bool, unsupported_reason: Option, warning: Option, } @@ -134,7 +133,6 @@ impl From for SystemUpdateDetailView { notes: detail.notes, cached: detail.cached, update_supported: detail.update_supported, - restart_confirmation_supported: true, unsupported_reason: detail.unsupported_reason, warning: detail.warning, } diff --git a/backend/crates/gateway-plugin/runtime/plugin-api-deprecations.json b/backend/crates/gateway-plugin/runtime/plugin-api-deprecations.json index 9a72a77a0..fe51488c7 100644 --- a/backend/crates/gateway-plugin/runtime/plugin-api-deprecations.json +++ b/backend/crates/gateway-plugin/runtime/plugin-api-deprecations.json @@ -1,20 +1 @@ -[ - { - "capability": "middleware", - "version": 3, - "replacement_version": 4, - "introduced_in": "3.20.0", - "last_counted_in": "3.22.2", - "remaining_releases": 0, - "migration": "将执行设置和来源中的 disable_fast 改为 fast_mode,使用 default、enabled、disabled 三态,并将 middleware 声明升级到 v4" - }, - { - "capability": "upstream_adapter", - "version": 1, - "replacement_version": 2, - "introduced_in": "3.20.0", - "last_counted_in": "3.22.2", - "remaining_releases": 0, - "migration": "使用新版 SDK 读取 fast_mode 三态,并将 upstream_adapter 声明升级到 v2" - } -] +[] diff --git a/backend/crates/gateway-plugin/runtime/plugin-host-compatibility.json b/backend/crates/gateway-plugin/runtime/plugin-host-compatibility.json index fbdeaea03..135d8f695 100644 --- a/backend/crates/gateway-plugin/runtime/plugin-host-compatibility.json +++ b/backend/crates/gateway-plugin/runtime/plugin-host-compatibility.json @@ -8,8 +8,8 @@ { "capability": "model_router", "versions": [1] }, { "capability": "model_catalog", "versions": [1] }, { "capability": "retry_policy", "versions": [1] }, - { "capability": "middleware", "versions": [3, 4] }, - { "capability": "upstream_adapter", "versions": [1, 2] }, + { "capability": "middleware", "versions": [4] }, + { "capability": "upstream_adapter", "versions": [2] }, { "capability": "observer", "versions": [1] }, { "capability": "command_line", "versions": [1] }, { "capability": "management", "versions": [1] }, diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/middleware/http.rs b/backend/crates/gateway-plugin/runtime/src/adapter/middleware/http.rs index 854359d56..85ba7426d 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/middleware/http.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/middleware/http.rs @@ -33,12 +33,7 @@ pub(super) async fn invoke( .extensions() .get::() .and_then(|settings| settings.runtime.as_ref()) - .map(|settings| { - crate::compatibility::FastSettings::middleware(&ports.session).sources(settings) - }) - .transpose() - .map_err(|_| MiddlewareError::InvalidState)? - .unwrap_or(serde_json::Value::Null); + .map_or(serde_json::Value::Null, |settings| settings.inspect()); let (invocation, request) = Invocation::new(request, next, entry.instance_id.clone())?; let mut call = ports.session.context(Stage::Http, timeout); call.request_id = Some(context.request_id.clone()); diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/middleware/request.rs b/backend/crates/gateway-plugin/runtime/src/adapter/middleware/request.rs index 9c348e7e5..943dac135 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/middleware/request.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/middleware/request.rs @@ -52,13 +52,7 @@ pub(super) async fn invoke( if remaining.is_zero() || context.cancellation().is_cancelled() { return Err(MiddlewareError::Fault); } - let invocation = MiddlewareInvocation::new( - &context, - request, - next, - entry.instance_id.clone(), - crate::compatibility::FastSettings::middleware(&invocation_ports.session), - ); + let invocation = MiddlewareInvocation::new(&context, request, next, entry.instance_id.clone()); let (protocol, headers, payload) = match invocation.request_parts() { Ok(projection) => projection, Err(_) => return recover_rpc(&entry, &invocation, RpcError::Context(None)).await, @@ -96,9 +90,7 @@ pub(super) async fn invoke( scope, )?; let head = MiddlewareRequestHead { - settings_sources: invocation - .settings_sources() - .map_err(|_| MiddlewareError::InvalidState)?, + settings_sources: invocation.settings_sources(), settings: invocation .settings() .map_err(|_| MiddlewareError::InvalidState)?, diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/upstream_adapter/execution.rs b/backend/crates/gateway-plugin/runtime/src/adapter/upstream_adapter/execution.rs index cb2d99cf3..dada1aa02 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/upstream_adapter/execution.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/upstream_adapter/execution.rs @@ -235,16 +235,15 @@ impl Execution { .collect(), continuation: previous.map(|state| state.continuation), }; - let payload = crate::compatibility::FastSettings::upstream_request( - &self.adapter.session, - request, - invocation - .operation - .middleware_body() - .map_err(|source| invalid().with_source(source))? - .to_vec(), - ) - .map_err(|source| invalid().with_source(source))?; + let payload = request + .encode( + invocation + .operation + .middleware_body() + .map_err(|source| invalid().with_source(source))? + .to_vec(), + ) + .map_err(|source| invalid().with_source(source))?; invocation.context.trace().record("plugin.upstream", serde_json::json!({ "instanceId": self.adapter.instance_id, "adapterId": declaration.id, "generation": context.generation, "transport": declaration.transport.as_str() })); let stream = tokio::select! { biased; diff --git a/backend/crates/gateway-plugin/runtime/src/callback/middleware/mod.rs b/backend/crates/gateway-plugin/runtime/src/callback/middleware/mod.rs index 23fae55dd..d3792438d 100644 --- a/backend/crates/gateway-plugin/runtime/src/callback/middleware/mod.rs +++ b/backend/crates/gateway-plugin/runtime/src/callback/middleware/mod.rs @@ -86,7 +86,6 @@ fn request_feature( pub(crate) struct MiddlewareInvocation { instance_id: String, - settings_contract: crate::compatibility::FastSettings, capabilities_allowed: bool, transport: ClientTransport, state: Mutex, @@ -141,28 +140,27 @@ impl MiddlewareInvocation { .state .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); - state - .original_request - .settings() - .map(|settings| self.settings_contract.execution(settings)) - .unwrap_or(Ok(serde_json::Value::Null)) + serde_json::to_value( + state + .original_request + .settings() + .and_then(|settings| settings.execution_values()), + ) + .map_err(|_| invalid()) } - pub(crate) fn settings_sources(&self) -> Result { + pub(crate) fn settings_sources(&self) -> serde_json::Value { self.request_settings() - .map(|settings| self.settings_contract.sources(&settings)) - .unwrap_or(Ok(serde_json::Value::Null)) + .map_or(serde_json::Value::Null, |settings| settings.inspect()) } pub(crate) fn new( context: &MiddlewareContext, request: MiddlewareRequest, next: MiddlewareNext, instance_id: String, - settings_contract: crate::compatibility::FastSettings, ) -> Arc { Arc::new(Self { instance_id, - settings_contract, capabilities_allowed: context.mount() == gateway_core::engine::middleware::MiddlewareMount::Request && context.operation() == Some(gateway_core::operation::OperationKind::Generate), @@ -343,7 +341,7 @@ impl MiddlewareInvocation { "execution settings are not available at this middleware boundary", ) })?; - let values = self.settings_contract.decode(settings, current)?; + let values = serde_json::from_value(settings).map_err(|_| invalid())?; let updated = current .replace_execution(&values, &self.instance_id) .map_err(|_| invalid())?; diff --git a/backend/crates/gateway-plugin/runtime/src/compatibility/mod.rs b/backend/crates/gateway-plugin/runtime/src/compatibility.rs similarity index 90% rename from backend/crates/gateway-plugin/runtime/src/compatibility/mod.rs rename to backend/crates/gateway-plugin/runtime/src/compatibility.rs index d4193d573..f583b5c47 100644 --- a/backend/crates/gateway-plugin/runtime/src/compatibility/mod.rs +++ b/backend/crates/gateway-plugin/runtime/src/compatibility.rs @@ -1,8 +1,4 @@ -//! 有期限的插件合同适配;旧字段只在此边界出现,不进入 Core 或持久化模型 - -mod fast; - -pub(crate) use fast::FastSettings; +//! 插件合同弃用声明、管理端提示与加载日志 use std::sync::OnceLock; @@ -26,7 +22,7 @@ struct Deprecation { fn declarations() -> Result<&'static [Deprecation], AdminError> { static DECLARATIONS: OnceLock, serde_json::Error>> = OnceLock::new(); DECLARATIONS - .get_or_init(|| serde_json::from_str(include_str!("../../plugin-api-deprecations.json"))) + .get_or_init(|| serde_json::from_str(include_str!("../plugin-api-deprecations.json"))) .as_deref() .map_err(|_| AdminError::internal("插件接口弃用声明不合法")) } diff --git a/backend/crates/gateway-plugin/runtime/src/compatibility/fast.rs b/backend/crates/gateway-plugin/runtime/src/compatibility/fast.rs deleted file mode 100644 index c030b4c89..000000000 --- a/backend/crates/gateway-plugin/runtime/src/compatibility/fast.rs +++ /dev/null @@ -1,109 +0,0 @@ -//! 在旧插件合同边界投影 Fast 设置并保留宿主三态语义 - -use gateway_core::{ - account::FastMode, - routing::request_settings::{ExecutionSettings, RequestSettings}, -}; -use gateway_plugin_sdk::{ - Capability, ErrorCode, PluginFault, - call::{model::ExecutionEncodingError, upstream_adapter::UpstreamAdapterRequest}, -}; -use serde_json::Value; - -use crate::RpcSession; - -#[derive(Clone, Copy)] -pub(crate) struct FastSettings { - legacy: bool, -} - -impl FastSettings { - pub(crate) fn middleware(session: &RpcSession) -> Self { - Self { - legacy: session.capability_version(Capability::Middleware) == Some(3), - } - } - - pub(crate) fn execution(self, settings: &RequestSettings) -> Result { - let mut value = serde_json::to_value(settings.execution_values()).map_err(|_| invalid())?; - if self.legacy { - project_fast(&mut value)?; - } - Ok(value) - } - - pub(crate) fn sources(self, settings: &RequestSettings) -> Result { - let mut value = settings.inspect(); - if self.legacy - && let Some(execution) = value["execution"].as_object_mut() - { - project_fast(execution.get_mut("input").ok_or_else(invalid)?)?; - let mut source = execution.remove("fast_mode").ok_or_else(invalid)?; - if !source.is_null() { - source["value"] = disabled(&source["value"])?; - } - execution.insert("disable_fast".into(), source); - } - Ok(value) - } - - pub(crate) fn decode( - self, - mut value: Value, - current: &RequestSettings, - ) -> Result { - if self.legacy { - let object = value.as_object_mut().ok_or_else(invalid)?; - if object.contains_key("fast_mode") { - return Err(invalid()); - } - let next = object - .remove("disable_fast") - .and_then(|value| value.as_bool()) - .ok_or_else(invalid)?; - let current = current.execution_values().ok_or_else(invalid)?.fast_mode; - // 原样回传 false 不能把三态的 enabled 降为 default,只有实际变化才覆盖 - let mode = if next == (current == FastMode::Disabled) { - current - } else if next { - FastMode::Disabled - } else { - FastMode::Default - }; - object.insert("fast_mode".into(), Value::String(mode.as_str().into())); - } - serde_json::from_value(value).map_err(|_| invalid()) - } - - pub(crate) fn upstream_request( - session: &RpcSession, - request: UpstreamAdapterRequest, - body: Vec, - ) -> Result, ExecutionEncodingError> { - if session.capability_version(Capability::UpstreamAdapter) != Some(1) { - return request.encode(body); - } - let mut metadata = serde_json::to_value(request).map_err(|_| ExecutionEncodingError)?; - project_fast(&mut metadata).map_err(|_| ExecutionEncodingError)?; - gateway_plugin_sdk::call::upstream_adapter::encode_request_metadata(&metadata, body) - } -} - -fn project_fast(value: &mut Value) -> Result<(), PluginFault> { - if value.is_null() { - return Ok(()); - } - let object = value.as_object_mut().ok_or_else(invalid)?; - let mode = object.remove("fast_mode").ok_or_else(invalid)?; - object.insert("disable_fast".into(), disabled(&mode)?); - Ok(()) -} - -fn disabled(value: &Value) -> Result { - let mode: FastMode = serde_json::from_value(value.clone()).map_err(|_| invalid())?; - Ok(Value::Bool(mode == FastMode::Disabled)) -} - -fn invalid() -> PluginFault { - PluginFault::new(ErrorCode::InvalidInput, "invalid plugin execution settings") -} diff --git a/backend/crates/gateway-plugin/runtime/src/rpc/session.rs b/backend/crates/gateway-plugin/runtime/src/rpc/session.rs index 70c401968..26bdc9769 100644 --- a/backend/crates/gateway-plugin/runtime/src/rpc/session.rs +++ b/backend/crates/gateway-plugin/runtime/src/rpc/session.rs @@ -205,16 +205,6 @@ impl RpcSession { error }) } - - pub(crate) fn capability_version( - &self, - capability: gateway_plugin_sdk::Capability, - ) -> Option { - self.handshake - .contributes - .get(&capability) - .map(|declaration| declaration.version) - } } pub(crate) struct Shared { diff --git a/backend/crates/gateway-plugin/runtime/tests/generation/policy/compatibility.rs b/backend/crates/gateway-plugin/runtime/tests/generation/policy/compatibility.rs index 157ee2e2b..f39890c05 100644 --- a/backend/crates/gateway-plugin/runtime/tests/generation/policy/compatibility.rs +++ b/backend/crates/gateway-plugin/runtime/tests/generation/policy/compatibility.rs @@ -1,17 +1,17 @@ -//! 验证旧中间件的 Fast 设置适配与不兼容插件的实例故障隔离 +//! 验证中间件 Fast 三态设置与不兼容插件的实例故障隔离 use super::*; use gateway_core::{policy::ClientPolicy, routing::request_settings::RequestSettings}; use serde_json::{Value, json}; #[tokio::test] -async fn legacy_middleware_preserves_three_state_fast_and_projects_sources() { - for (mode, write, expected) in [ - (FastMode::Default, false, FastMode::Default), - (FastMode::Enabled, false, FastMode::Enabled), - (FastMode::Disabled, true, FastMode::Disabled), - (FastMode::Enabled, true, FastMode::Disabled), - (FastMode::Disabled, false, FastMode::Default), +async fn middleware_preserves_three_state_fast_and_sources() { + for (mode, expected) in [ + (FastMode::Default, FastMode::Default), + (FastMode::Enabled, FastMode::Enabled), + (FastMode::Disabled, FastMode::Disabled), + (FastMode::Enabled, FastMode::Disabled), + (FastMode::Disabled, FastMode::Default), ] { let snapshot = gateway_core::routing::RuntimeSnapshot::new( ConfigRevision::new(1).unwrap(), @@ -46,34 +46,40 @@ async fn legacy_middleware_preserves_three_state_fast_and_projects_sources() { let mut seed = settings.execution_values().unwrap(); seed.fast_mode = mode; let settings = settings.replace_execution(&seed, "seed").unwrap(); - let mut wire = serde_json::to_value(&seed).unwrap(); - wire.as_object_mut().unwrap().remove("fast_mode"); - wire["disable_fast"] = json!(mode == FastMode::Disabled); + let wire = serde_json::to_value(&seed).unwrap(); let mut updated = wire.clone(); - updated["disable_fast"] = json!(write); + updated["fast_mode"] = json!(expected.as_str()); updated["timeout_ms"] = json!(90_000); - let mut contribution = crate::support::contribution( + let contribution = crate::support::contribution( Capability::Middleware, vec![Stage::Request], vec!["openai".into()], vec!["openai".into()], ); - contribution.1.version = 3; let worker = std::fs::read(env!("CARGO_BIN_EXE_gateway-plugin-test-middleware")).unwrap(); let package = crate::support::package_with_contributions( &worker, Contributions::from([contribution]), ); - let (cache, runtime) = setup_package(vec![InstanceFixture { - id: "legacy-settings", - configuration: json!({ - "mode":"settings", "middleware_version":3, "expected_settings":wire, "settings":updated, - "expected_legacy_source": if mode == FastMode::Default { Value::Null } else { - json!({"instance_id":"seed", "order":1, "value":mode == FastMode::Disabled}) - }, - }), - bindings: vec![binding(MIDDLEWARE_CONTRIBUTION,"request",0,PluginFailurePolicy::Reject)], - }], package).await; + let (cache, runtime) = setup_package( + vec![InstanceFixture { + id: "fast-settings", + configuration: json!({ + "mode":"settings", "expected_settings":wire, "settings":updated, + "expected_fast_source": if mode == FastMode::Default { Value::Null } else { + json!({"instance_id":"seed", "order":1, "value":mode.as_str()}) + }, + }), + bindings: vec![binding( + MIDDLEWARE_CONTRIBUTION, + "request", + 0, + PluginFailurePolicy::Reject, + )], + }], + package, + ) + .await; let generation = prepare(&runtime).await; let plan = runtime .execution_registry() @@ -105,7 +111,7 @@ async fn legacy_middleware_preserves_three_state_fast_and_projects_sources() { ); let sources = settings.inspect(); assert!(sources["execution"].get("disable_fast").is_none()); - if mode == FastMode::Enabled && !write { + if mode != FastMode::Default && mode == expected { assert_eq!(sources["execution"]["fast_mode"]["instance_id"], "seed"); } next.run(request).await diff --git a/backend/crates/gateway-plugin/runtime/tests/generation/upstream_adapter.rs b/backend/crates/gateway-plugin/runtime/tests/generation/upstream_adapter.rs index 7589dd7a8..7df17189b 100644 --- a/backend/crates/gateway-plugin/runtime/tests/generation/upstream_adapter.rs +++ b/backend/crates/gateway-plugin/runtime/tests/generation/upstream_adapter.rs @@ -102,18 +102,12 @@ async fn setup( Arc, gateway_plugin_runtime::PluginRuntime, ) { - let mut contributes = Contributions::from([crate::support::contribution( + let contributes = Contributions::from([crate::support::contribution( Capability::UpstreamAdapter, vec![Stage::Upstream], vec!["openai".into()], vec!["openai".into()], )]); - if let Some(version) = config["upstream_version"].as_u64() { - contributes - .get_mut(&Capability::UpstreamAdapter) - .unwrap() - .version = version.try_into().unwrap(); - } let (cache, store, runtime) = super::setup_with_contributions_and_restart_circuit(contributes, Default::default()).await; { @@ -226,7 +220,7 @@ fn execute_with_context( } #[tokio::test] -async fn legacy_adapter_decodes_exact_v1_metadata_for_all_fast_modes() { +async fn adapter_preserves_three_state_fast_in_request_metadata() { use gateway_core::account::FastMode; for mode in [FastMode::Default, FastMode::Enabled, FastMode::Disabled] { let server = MockServer::start().await; @@ -237,8 +231,7 @@ async fn legacy_adapter_decodes_exact_v1_metadata_for_all_fast_modes() { .mount(&server) .await; let mut config = configuration(&server.uri(), false); - config["upstream_version"] = json!(1); - config["expected_disable_fast"] = json!(mode == FastMode::Disabled); + config["expected_fast_mode"] = json!(mode.as_str()); let (cache, _, runtime) = setup(config).await; let generation = ExtensionPreparationPort::prepare(&runtime, ConfigRevision::new(1).unwrap()) diff --git a/backend/crates/gateway-plugin/runtime/tests/package/compatibility.rs b/backend/crates/gateway-plugin/runtime/tests/package/compatibility.rs index 6f634f13b..c479f4474 100644 --- a/backend/crates/gateway-plugin/runtime/tests/package/compatibility.rs +++ b/backend/crates/gateway-plugin/runtime/tests/package/compatibility.rs @@ -43,9 +43,9 @@ fn host_compatibility_requires_the_trusted_middleware_contract() { .expect("compatibility JSON"); assert!(!compatibility.supports_capability("middleware", 1)); assert!(!compatibility.supports_capability("middleware", 2)); - assert!(compatibility.supports_capability("middleware", 3)); + assert!(!compatibility.supports_capability("middleware", 3)); assert!(compatibility.supports_capability("middleware", 4)); - assert!(compatibility.supports_capability("upstream_adapter", 1)); + assert!(!compatibility.supports_capability("upstream_adapter", 1)); assert!(compatibility.supports_capability("upstream_adapter", 2)); assert!(!compatibility.supports_capability("openai", 1)); } @@ -81,7 +81,7 @@ async fn package_inspector_returns_static_requirements_without_starting_the_plug } #[tokio::test] -async fn deprecated_contracts_are_loadable_and_only_old_versions_receive_notices() { +async fn retired_contracts_remain_loadable_with_compatibility_warnings() { for legacy in [false, true] { let mut middleware = crate::support::contribution( Capability::Middleware, @@ -105,13 +105,19 @@ async fn deprecated_contracts_are_loadable_and_only_old_versions_receive_notices ); let inspector = PackageInspector::new(PackageLimits::default(), semver::Version::new(1, 0, 0)); - let artifact = inspector.inspect(archive, None).await.unwrap(); + let artifact = inspector.inspect(archive.clone(), None).await.unwrap(); let notices = inspector.api_deprecations(&artifact.metadata).unwrap(); - assert_eq!(notices.len(), if legacy { 2 } else { 0 }); - for notice in notices { - assert!(notice.replacement_version > notice.version); - assert!(!notice.migration.is_empty()); - assert!(notice.remaining_releases <= 7); + assert!(notices.is_empty()); + let warning = inspector + .compatibility_warning(archive, artifact.metadata.sha256) + .await + .unwrap(); + if legacy { + let warning = warning.expect("retired contracts must remain visible"); + assert!(warning.contains("middleware v3")); + assert!(warning.contains("upstream_adapter v1")); + } else { + assert!(warning.is_none()); } } } diff --git a/backend/crates/gateway-plugin/runtime/tests/support/middleware/mod.rs b/backend/crates/gateway-plugin/runtime/tests/support/middleware/mod.rs index 7d628d7c1..4d0f76670 100644 --- a/backend/crates/gateway-plugin/runtime/tests/support/middleware/mod.rs +++ b/backend/crates/gateway-plugin/runtime/tests/support/middleware/mod.rs @@ -489,12 +489,12 @@ async fn main() -> Result<(), Box> { call.request.head.settings, configuration["expected_settings"] ); - if let Some(expected) = configuration.get("expected_legacy_source") { + if let Some(expected) = configuration.get("expected_fast_source") { let sources = &call.request.head.settings_sources["execution"]; - assert!(sources.get("fast_mode").is_none()); - assert!(sources["input"].get("fast_mode").is_none()); - assert_eq!(sources["input"]["disable_fast"], false); - assert_eq!(&sources["disable_fast"], expected); + assert!(sources.get("disable_fast").is_none()); + assert!(sources["input"].get("disable_fast").is_none()); + assert_eq!(sources["input"]["fast_mode"], "default"); + assert_eq!(&sources["fast_mode"], expected); } if let Some(settings) = configuration.get("settings") { call.request.head.settings = settings.clone(); diff --git a/backend/crates/gateway-plugin/runtime/tests/support/worker/legacy_upstream.rs b/backend/crates/gateway-plugin/runtime/tests/support/worker/legacy_upstream.rs deleted file mode 100644 index a9319673b..000000000 --- a/backend/crates/gateway-plugin/runtime/tests/support/worker/legacy_upstream.rs +++ /dev/null @@ -1,53 +0,0 @@ -//! 为测试子进程严格解码上游适配器 v1 的请求元数据 - -use gateway_plugin_sdk::call::upstream_adapter::{ - BuiltinProvider, UpstreamAdapterRequest, UpstreamContinuation, -}; -use serde::Deserialize; - -// 固定 v1 的严格解码器,不能用新版 DTO 的宽松解析掩盖旧插件加载失败 -#[derive(Deserialize)] -#[serde(deny_unknown_fields)] -struct Request { - adapter_id: String, - provider: BuiltinProvider, - upstream_model: String, - client_key_id: String, - account_id: String, - credential_revision: u64, - protocol: String, - client_transport: String, - disable_fast: bool, - headers: Vec<(String, Vec)>, - continuation: Option, -} - -pub fn decode(payload: &[u8], expected_disabled: bool) -> (UpstreamAdapterRequest, Vec) { - assert_eq!(&payload[..4], b"GPAQ"); - let metadata_length = u32::from_be_bytes(payload[4..8].try_into().unwrap()) as usize; - let body_length = u32::from_be_bytes(payload[8..12].try_into().unwrap()) as usize; - assert_eq!(payload.len(), 12 + metadata_length + body_length); - let old: Request = serde_json::from_slice(&payload[12..12 + metadata_length]).unwrap(); - assert_eq!(old.disable_fast, expected_disabled); - ( - UpstreamAdapterRequest { - adapter_id: old.adapter_id, - provider: old.provider, - upstream_model: old.upstream_model, - client_key_id: old.client_key_id, - account_id: old.account_id, - credential_revision: old.credential_revision, - protocol: old.protocol, - client_transport: old.client_transport, - fast_mode: if old.disable_fast { - "disabled" - } else { - "default" - } - .into(), - headers: old.headers, - continuation: old.continuation, - }, - payload[12 + metadata_length..].to_vec(), - ) -} diff --git a/backend/crates/gateway-plugin/runtime/tests/support/worker/mod.rs b/backend/crates/gateway-plugin/runtime/tests/support/worker/mod.rs index fce20bbbc..f3eb38500 100644 --- a/backend/crates/gateway-plugin/runtime/tests/support/worker/mod.rs +++ b/backend/crates/gateway-plugin/runtime/tests/support/worker/mod.rs @@ -1,7 +1,5 @@ //! 真实子进程协议对端;只依赖公开 SDK,Cargo 为集成测试构建此辅助二进制 -mod legacy_upstream; - use std::{ collections::BTreeMap, io::Write as _, @@ -1481,16 +1479,10 @@ async fn main() { impl Peer { async fn upstream_adapter(&self, id: u64, payload: Vec) -> Result<(), PluginFault> { - let (request, body) = if self.configuration["upstream_version"] == 1 { - legacy_upstream::decode( - &payload, - self.configuration["expected_disable_fast"] - .as_bool() - .unwrap(), - ) - } else { - UpstreamAdapterRequest::decode(&payload).unwrap() - }; + let (request, body) = UpstreamAdapterRequest::decode(&payload).unwrap(); + if let Some(expected) = self.configuration.get("expected_fast_mode") { + assert_eq!(request.fast_mode, expected.as_str().unwrap()); + } // 假凭据用于检测宿主是否把已选账号令牌放入了插件输入 assert!(!String::from_utf8_lossy(&payload).contains("fixture-native-token")); self.append_observation_marker("upstream_marker", &json!({"key":request.client_key_id,"account":request.account_id,"continuation":request.continuation})); diff --git a/backend/crates/gateway-plugin/sdk/docs/manifest.md b/backend/crates/gateway-plugin/sdk/docs/manifest.md index 31e188f41..97062b93a 100644 --- a/backend/crates/gateway-plugin/sdk/docs/manifest.md +++ b/backend/crates/gateway-plugin/sdk/docs/manifest.md @@ -42,6 +42,9 @@ 当前支持范围由[宿主兼容声明](../../runtime/plugin-host-compatibility.json)给出,弃用状态、剩余兼容窗口及作者说明以[弃用清单](../../runtime/plugin-api-deprecations.json)为准,管理端和加载日志展示同一提示 +执行设置与来源使用 `fast_mode` 的 `default`、`enabled`、`disabled` 三态,宿主不转换旧 `disable_fast` 字段。 +使用 `middleware v3` 或 `upstream_adapter v1` 的插件需按当前 SDK 更新字段处理、升级能力版本并重新构建,仅修改版本声明不能替代迁移 + 兼容窗口自替代合同首次正式发布起至少覆盖 7 个后续正式版本;主版本、次版本和补丁版本各计一次,预发行与同标签重试不计。 窗口内提供适配,窗口外不保证兼容;是否使用过特定字段不改变判断,版本号差值也不能代替清单中的计数 diff --git a/backend/crates/gateway-plugin/sdk/src/call/upstream_adapter/codec.rs b/backend/crates/gateway-plugin/sdk/src/call/upstream_adapter/codec.rs index f5909e6b4..a8f0c47f1 100644 --- a/backend/crates/gateway-plugin/sdk/src/call/upstream_adapter/codec.rs +++ b/backend/crates/gateway-plugin/sdk/src/call/upstream_adapter/codec.rs @@ -39,15 +39,7 @@ pub(super) fn encode_request( request: UpstreamAdapterRequest, body: Vec, ) -> Result, ExecutionEncodingError> { - encode_request_metadata(&request, body) -} - -/// 封装按合同版本投影的请求元数据,保持 GPAQ 分帧与载荷限制统一 -pub fn encode_request_metadata( - metadata: &impl Serialize, - body: Vec, -) -> Result, ExecutionEncodingError> { - pack(*b"GPAQ", metadata, [body]) + pack(*b"GPAQ", &request, [body]) } pub(super) fn decode_request( diff --git a/backend/crates/gateway-plugin/sdk/src/call/upstream_adapter/mod.rs b/backend/crates/gateway-plugin/sdk/src/call/upstream_adapter/mod.rs index 963b35ad5..b4e7a929a 100644 --- a/backend/crates/gateway-plugin/sdk/src/call/upstream_adapter/mod.rs +++ b/backend/crates/gateway-plugin/sdk/src/call/upstream_adapter/mod.rs @@ -5,7 +5,6 @@ use serde::{Deserialize, Serialize}; use super::model::ExecutionEvent; mod codec; -pub use codec::encode_request_metadata; pub const REGISTER_METHOD: &str = "upstream_adapter.register"; pub const EXECUTE_METHOD: &str = "upstream_adapter.execute"; diff --git a/backend/crates/gateway-plugin/sdk/src/capability.rs b/backend/crates/gateway-plugin/sdk/src/capability.rs index 3e82b5156..eb92a77b6 100644 --- a/backend/crates/gateway-plugin/sdk/src/capability.rs +++ b/backend/crates/gateway-plugin/sdk/src/capability.rs @@ -26,7 +26,7 @@ impl Capability { #[must_use] pub const fn contract_versions(self) -> &'static [u32] { match self { - Self::Middleware => &[3, 4], + Self::Middleware => &[4], Self::FrontendAuthentication | Self::Scheduler | Self::ModelRouter @@ -36,7 +36,7 @@ impl Capability { | Self::CommandLine | Self::Management | Self::Maintenance => &[1], - Self::UpstreamAdapter => &[1, 2], + Self::UpstreamAdapter => &[2], } } diff --git a/backend/crates/gateway-plugin/sdk/tests/manifest.rs b/backend/crates/gateway-plugin/sdk/tests/manifest.rs index 35ba5a5c0..073fa8067 100644 --- a/backend/crates/gateway-plugin/sdk/tests/manifest.rs +++ b/backend/crates/gateway-plugin/sdk/tests/manifest.rs @@ -132,7 +132,7 @@ fn installation_checks_protocol_engine_and_target() { fn capability_contract_versions_are_checked_independently_of_rpc_version() { let mut manifest = packaged_manifest(); assert!(manifest.validate().is_ok()); - for version in [0, 1, 2, 5, u32::MAX] { + for version in [0, 1, 2, 3, 5, u32::MAX] { manifest .contributes .get_mut(&Capability::Middleware) @@ -155,6 +155,29 @@ fn capability_contract_versions_are_checked_independently_of_rpc_version() { assert_eq!(manifest.validate(), Err(ManifestError::Invalid)); } +#[test] +fn retired_upstream_contract_requires_an_author_upgrade() { + let mut manifest = source_manifest(); + manifest.contributes.clear(); + manifest.contributes.insert( + Capability::UpstreamAdapter, + ContributionDeclaration { + id: "9acme.request-tags.upstream".into(), + version: 2, + stages: vec![Stage::Upstream], + input_formats: vec!["openai".into()], + output_formats: vec!["openai".into()], + }, + ); + assert!(manifest.validate().is_ok()); + manifest + .contributes + .get_mut(&Capability::UpstreamAdapter) + .unwrap() + .version = 1; + assert_eq!(manifest.validate(), Err(ManifestError::Invalid)); +} + #[test] fn package_rejects_file_directory_conflicts_and_missing_declared_files() { let mut manifest = packaged_manifest(); diff --git a/deploy/README.md b/deploy/README.md index fc8cb639d..f9ebcd4f2 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -601,6 +601,8 @@ alpha、beta、rc、exp 在 GitHub 标记为 Pre-release,不覆盖 GitHub Late ### 管理端在线更新 官方构建按上述规则在线更新;源码等非官方构建返回不支持原因,不提供可用更新。 +最低受支持的在线升级起点为 **v3.18.1**。更旧版本需手动安装 v3.18.1 或新版完整发行包/镜像, +不能依赖旧更新页面直接升级到最新版或选择已不是最新版的过渡版本。 点击「下载并更新」后,操作日志显示下载、校验与安装过程,完成后显示「待重启」及待生效版本,重启后才使用新程序。 「当前版本」始终表示正在运行的版本,「通道最新」表示所选通道的检查结果 diff --git a/docs/api.md b/docs/api.md index e0a999a64..ba202f8c4 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1933,8 +1933,7 @@ Provider metadata 分别保留 `requestedServiceTier` 与 `upstreamServiceTier` 下载并解包后校验发行身份,插件版本不匹配不阻止更新或回滚,兼容性风险在重启前提示。 两条路径在文件交换前后复核全局配置版本,文件替换失败或取消时成组恢复二进制、Web 资源和官方插件目录 -详情响应的 `restartConfirmationSupported=true` 表示运行进程支持重启前确认。 -`restart/check` 返回 `targetVersion`、`releaseManifestSha256`、`configRevision` 和 `incompatiblePlugins`, +客户端重启前调用 `restart/check`,返回 `targetVersion`、`releaseManifestSha256`、`configRevision` 和 `incompatiblePlugins`, 每项包含 `instanceId`、`name`、`reason`,检查不修改插件状态。源码运行的目标版本与发行摘要为空,检查当前宿主兼容性。 存在不兼容插件时,客户端展示列表并取得确认后,将完整检查结果作为 `confirmation` 提交重启请求。 服务端在重启锁内重新检查,目标或配置变化返回 `40901`,须重新检查并确认。确认后保留启用配置,重启时逐个尝试加载插件。 diff --git a/frontend/src/api/modules/settings/system.ts b/frontend/src/api/modules/settings/system.ts index 8e609aa33..cb03f9997 100644 --- a/frontend/src/api/modules/settings/system.ts +++ b/frontend/src/api/modules/settings/system.ts @@ -34,7 +34,6 @@ export interface SystemUpdateDetail { notes: string | null cached: boolean updateSupported: boolean - restartConfirmationSupported?: boolean unsupportedReason: string | null warning: string | null } diff --git a/frontend/src/layout/components/system-update-modal/index.vue b/frontend/src/layout/components/system-update-modal/index.vue index 8e463b1ac..2f20454d8 100644 --- a/frontend/src/layout/components/system-update-modal/index.vue +++ b/frontend/src/layout/components/system-update-modal/index.vue @@ -196,11 +196,6 @@ async function handleRestart() { try { if (!updateInfo.value) throw new Error('请等待系统更新信息加载完成') - // 文件已更新但旧进程仍在运行时,旧 API 继续使用安装前的兼容预检。 - if (!updateInfo.value.restartConfirmationSupported) { - await restartNow() - return - } restartPlan.value = await checkSystemRestart() if (restartPlan.value.incompatiblePlugins.length) { restartConfirmOpen.value = true diff --git a/frontend/src/stores/modules/system-update.ts b/frontend/src/stores/modules/system-update.ts index b46764256..7e18827b3 100644 --- a/frontend/src/stores/modules/system-update.ts +++ b/frontend/src/stores/modules/system-update.ts @@ -452,7 +452,7 @@ export const useSystemUpdateStore = defineStore('system-update', () => { setPhase({ kind: 'failed' }) } - async function restartNow(confirmation?: SystemRestartPlan) { + async function restartNow(confirmation: SystemRestartPlan) { if (restarting.value) return diff --git a/modules/plugins b/modules/plugins index cfd37537f..ff9ab2fe4 160000 --- a/modules/plugins +++ b/modules/plugins @@ -1 +1 @@ -Subproject commit cfd37537f99f2c957639c6fa2c14d40dcb0c3368 +Subproject commit ff9ab2fe4ed4b4256269a2936bc616768c0e932e