feat(saas): Phase 1 后端能力补强 — API Token 认证、真实 SSE 流式、速率限制
Phase 1.1: API Token 认证中间件 - auth_middleware 新增 zclaw_ 前缀 token 分支 (SHA-256 验证) - 合并 token 自身权限与角色权限,异步更新 last_used_at - 添加 GET /api/v1/auth/me 端点返回当前用户信息 - get_role_permissions 改为 pub(crate) 供中间件调用 Phase 1.2: 真实 SSE 流式中转 - RelayResponse::Sse 改为 axum::body::Body (bytes_stream) - 流式请求超时提升至 300s,转发 SSE headers (Cache-Control, Connection) - 添加 futures 依赖用于 StreamExt Phase 1.3: 滑动窗口速率限制中间件 - 按 account_id 做 per-minute 限流 (默认 60 rpm + 10 burst) - 超限返回 429 + Retry-After header - RateLimitConfig 支持配置化,DashMap 存储时间戳 21 tests passed, zero warnings.
This commit is contained in:
@@ -96,7 +96,15 @@ pub async fn chat_completions(
|
||||
None, "success", None,
|
||||
).await?;
|
||||
|
||||
Ok((StatusCode::OK, [(axum::http::header::CONTENT_TYPE, "text/event-stream")], body).into_response())
|
||||
// 流式响应: 直接转发 axum::body::Body
|
||||
let response = axum::response::Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(axum::http::header::CONTENT_TYPE, "text/event-stream")
|
||||
.header("Cache-Control", "no-cache")
|
||||
.header("Connection", "keep-alive")
|
||||
.body(body)
|
||||
.unwrap();
|
||||
Ok(response)
|
||||
}
|
||||
Err(e) => {
|
||||
model_service::record_usage(
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use sqlx::SqlitePool;
|
||||
use crate::error::{SaasError, SaasResult};
|
||||
use super::types::*;
|
||||
use futures::StreamExt;
|
||||
|
||||
// ============ Relay Task Management ============
|
||||
|
||||
@@ -127,7 +128,7 @@ pub async fn execute_relay(
|
||||
let _start = std::time::Instant::now();
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.timeout(std::time::Duration::from_secs(if stream { 300 } else { 30 }))
|
||||
.build()
|
||||
.map_err(|e| SaasError::Internal(format!("HTTP 客户端构建失败: {}", e)))?;
|
||||
let mut req_builder = client.post(&url)
|
||||
@@ -143,7 +144,11 @@ pub async fn execute_relay(
|
||||
match result {
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
if stream {
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
// 真实 SSE 流式: 使用 bytes_stream 而非 text().await 缓冲
|
||||
let stream = resp.bytes_stream()
|
||||
.map(|result| result.map_err(std::io::Error::other));
|
||||
let body = axum::body::Body::from_stream(stream);
|
||||
// 流式模式下无法提取 token usage,标记为 completed (usage=0)
|
||||
update_task_status(db, task_id, "completed", None, None, None).await?;
|
||||
Ok(RelayResponse::Sse(body))
|
||||
} else {
|
||||
@@ -173,7 +178,7 @@ pub async fn execute_relay(
|
||||
#[derive(Debug)]
|
||||
pub enum RelayResponse {
|
||||
Json(String),
|
||||
Sse(String),
|
||||
Sse(axum::body::Body),
|
||||
}
|
||||
|
||||
// ============ Helpers ============
|
||||
|
||||
Reference in New Issue
Block a user