feat: 添加MCP调试插件并优化流式超时处理
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

refactor(relay): 将Provider Key管理路由移至model_config模块
fix(saas): 修复demo_keys与provider_keys的匹配逻辑
perf(runtime): 将流式响应超时从60秒延长至180秒以适配思考型模型
docs: 新增模块化审计和上线前功能测试方案文档
chore: 添加tauri-plugin-mcp依赖及相关配置
This commit is contained in:
iven
2026-04-08 13:39:06 +08:00
parent 81d1702484
commit ade534d1ce
18 changed files with 2051 additions and 627 deletions

View File

@@ -896,7 +896,10 @@ async fn fix_seed_data(pool: &PgPool) -> SaasResult<()> {
(format!("demo-akey-3-{}", &admin_id[..8]), "DeepSeek API Key", "sk-demo-deepseek-key-1-xxxxx", "[\"relay:use\"]"),
];
for (idx, (id, label, key_val, perms)) in demo_keys.iter().enumerate() {
let provider_id = provider_keys.get(idx).map(|(_, pid)| pid.as_str()).unwrap_or("demo-openai");
let provider_id = match provider_keys.get(idx).map(|(_, pid)| pid.as_str()) {
Some(pid) => pid,
None => continue, // skip if no matching provider exists
};
sqlx::query(
"INSERT INTO account_api_keys (id, account_id, provider_id, key_value, key_label, permissions, enabled, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, true, $7, $7) ON CONFLICT (id) DO NOTHING"

View File

@@ -4,7 +4,7 @@ pub mod types;
pub mod service;
pub mod handlers;
use axum::routing::{delete, get, post};
use axum::routing::{delete, get, post, put};
use crate::state::AppState;
/// 模型配置路由 (需要认证)
@@ -14,6 +14,10 @@ pub fn routes() -> axum::Router<AppState> {
.route("/api/v1/providers", get(handlers::list_providers).post(handlers::create_provider))
.route("/api/v1/providers/:id", get(handlers::get_provider).patch(handlers::update_provider).delete(handlers::delete_provider))
.route("/api/v1/providers/:id/models", get(handlers::list_provider_models))
// Provider Key Pool (admin only, moved from relay to avoid quota middleware)
.route("/api/v1/providers/:provider_id/keys", get(crate::relay::handlers::list_provider_keys).post(crate::relay::handlers::add_provider_key))
.route("/api/v1/providers/:provider_id/keys/:key_id/toggle", put(crate::relay::handlers::toggle_provider_key))
.route("/api/v1/providers/:provider_id/keys/:key_id", delete(crate::relay::handlers::delete_provider_key))
// Models
.route("/api/v1/models", get(handlers::list_models).post(handlers::create_model))
.route("/api/v1/models/:id", get(handlers::get_model).patch(handlers::update_model).delete(handlers::delete_model))

View File

@@ -230,7 +230,7 @@ pub async fn create_model(db: &PgPool, req: &CreateModelRequest) -> SaasResult<M
"INSERT INTO models (id, provider_id, model_id, alias, context_window, max_output_tokens, supports_streaming, supports_vision, enabled, pricing_input, pricing_output, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, true, $9, $10, $11, $11)"
)
.bind(&id).bind(&req.provider_id).bind(&req.model_id).bind(&req.alias)
.bind(&id).bind(&req.provider_id).bind(&req.model_id).bind(req.alias.as_deref().unwrap_or(&req.model_id))
.bind(ctx).bind(max_out).bind(streaming).bind(vision).bind(pi).bind(po).bind(&now)
.execute(db).await.map_err(|e| SaasError::from_sqlx_unique(e, &format!("模型 '{}' 在 Provider '{}'", req.model_id, req.provider_id)))?;

View File

@@ -66,7 +66,7 @@ pub struct ModelInfo {
pub struct CreateModelRequest {
pub provider_id: String,
pub model_id: String,
pub alias: String,
pub alias: Option<String>,
pub context_window: Option<i64>,
pub max_output_tokens: Option<i64>,
pub supports_streaming: Option<bool>,

View File

@@ -5,10 +5,10 @@ pub mod service;
pub mod handlers;
pub mod key_pool;
use axum::routing::{delete, get, post, put};
use axum::routing::{get, post};
use crate::state::AppState;
/// 中转服务路由 (需要认证)
/// 中转服务路由 (需要认证 + quota 中间件)
pub fn routes() -> axum::Router<AppState> {
axum::Router::new()
// Relay 核心端点
@@ -17,9 +17,4 @@ pub fn routes() -> axum::Router<AppState> {
.route("/api/v1/relay/tasks/:id", get(handlers::get_task))
.route("/api/v1/relay/tasks/:id/retry", post(handlers::retry_task))
.route("/api/v1/relay/models", get(handlers::list_available_models))
// Key Pool 管理 (admin only)
.route("/api/v1/providers/:provider_id/keys", get(handlers::list_provider_keys))
.route("/api/v1/providers/:provider_id/keys", post(handlers::add_provider_key))
.route("/api/v1/providers/:provider_id/keys/:key_id/toggle", put(handlers::toggle_provider_key))
.route("/api/v1/providers/:provider_id/keys/:key_id", delete(handlers::delete_provider_key))
}

View File

@@ -15,8 +15,9 @@ use super::types::*;
/// 上游无数据时,发送 SSE 心跳注释行的间隔
const STREAMBRIDGE_HEARTBEAT_INTERVAL: Duration = Duration::from_secs(15);
/// 上游无数据时,丢弃连接的超时阈值(90s = 6 个心跳,给 thinking 模型更多时间
const STREAMBRIDGE_TIMEOUT: Duration = Duration::from_secs(90);
/// 上游无数据时,丢弃连接的超时阈值(180s = 12 个心跳)
/// 实测 Kimi for Coding 的 thinking→content 间隔可达 60s+,需要更宽容的超时。
const STREAMBRIDGE_TIMEOUT: Duration = Duration::from_secs(180);
/// 流结束后延迟清理的时间窗口
const STREAMBRIDGE_CLEANUP_DELAY: Duration = Duration::from_secs(60);
@@ -767,9 +768,9 @@ fn build_stream_bridge(
idle_heartbeats as u64 * STREAMBRIDGE_HEARTBEAT_INTERVAL.as_secs(),
);
// After 6 consecutive heartbeats without real data (90s),
// After 12 consecutive heartbeats without real data (180s),
// terminate the stream to prevent connection leaks.
if idle_heartbeats >= 6 {
if idle_heartbeats >= 12 {
tracing::warn!(
"[StreamBridge] Timeout ({:?}) no real data, closing stream for task {}",
STREAMBRIDGE_TIMEOUT,