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
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:
@@ -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"
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)))?;
|
||||
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user