feat: 全栈功能增强 - 扩展 provider/pool 管理、完善调度与数据层、重构前端 Pool 页面

后端:
- 扩展 pool_admin payloads 和 provider query models,增强 endpoint key 管理
- 完善 scheduler-core 候选排序与请求候选逻辑
- 增强 usage-runtime 写入、provider-transport 网络层与 OAuth 刷新
- 改进 AI pipeline 响应转换与流式处理
- 扩展 global_models/provider_catalog 数据层查询能力
- 增强 video-tasks-core 多 provider 支持
- 新增大量集成测试覆盖 pool/keys/provider_query/frontdoor

前端:
- 重构 PoolManagement 页面,拆分状态管理/对话框逻辑到独立模块
- 新增 poolAdvancedDialog/poolSchedulingDialog/poolManagementState/poolMobilePresentation 工具函数及测试
- 改进 Dialog 组件与 provider tabs 显示

部署:
- 更新 Rust CI workflow 和 Dockerfile 构建配置

Closes #275
Co-authored-by: AAEE86 <ppk0227@hotmail.com>
This commit is contained in:
fawney19
2026-04-09 13:51:50 +08:00
parent 4fc95adfb9
commit b0b40c16ff
97 changed files with 5816 additions and 1881 deletions

View File

@@ -1,18 +1,35 @@
use serde_json::{Map, Value};
pub fn build_video_follow_up_report_context(
request_id: &str,
user_id: &str,
api_key_id: &str,
task_id: &str,
provider_id: &str,
endpoint_id: &str,
key_id: &str,
provider_name: Option<&str>,
model_name: Option<&str>,
client_api_format: &str,
provider_api_format: &str,
) -> Value {
#[derive(Clone, Copy, Debug)]
pub struct VideoFollowUpReportContextInput<'a> {
pub request_id: &'a str,
pub user_id: &'a str,
pub api_key_id: &'a str,
pub task_id: &'a str,
pub provider_id: &'a str,
pub endpoint_id: &'a str,
pub key_id: &'a str,
pub provider_name: Option<&'a str>,
pub model_name: Option<&'a str>,
pub client_api_format: &'a str,
pub provider_api_format: &'a str,
}
pub fn build_video_follow_up_report_context(input: VideoFollowUpReportContextInput<'_>) -> Value {
let VideoFollowUpReportContextInput {
request_id,
user_id,
api_key_id,
task_id,
provider_id,
endpoint_id,
key_id,
provider_name,
model_name,
client_api_format,
provider_api_format,
} = input;
let mut context = Map::new();
context.insert(
"request_id".to_string(),
@@ -84,23 +101,26 @@ pub fn resolve_follow_up_auth(
#[cfg(test)]
mod tests {
use super::{build_video_follow_up_report_context, resolve_follow_up_auth};
use super::{
build_video_follow_up_report_context, resolve_follow_up_auth,
VideoFollowUpReportContextInput,
};
#[test]
fn builds_follow_up_report_context_with_transport_metadata() {
let context = build_video_follow_up_report_context(
"req_123",
"user_123",
"key_123",
"task_123",
"provider_123",
"endpoint_123",
"transport_key_123",
Some("provider-name"),
Some("model-name"),
"openai:video",
"openai:video",
);
let context = build_video_follow_up_report_context(VideoFollowUpReportContextInput {
request_id: "req_123",
user_id: "user_123",
api_key_id: "key_123",
task_id: "task_123",
provider_id: "provider_123",
endpoint_id: "endpoint_123",
key_id: "transport_key_123",
provider_name: Some("provider-name"),
model_name: Some("model-name"),
client_api_format: "openai:video",
provider_api_format: "openai:video",
});
assert_eq!(context["request_id"].as_str(), Some("req_123"));
assert_eq!(context["provider_id"].as_str(), Some("provider_123"));

View File

@@ -8,7 +8,7 @@ use crate::{
build_video_follow_up_report_context, current_unix_timestamp_secs, gemini_metadata_video_url,
request_body_string, request_body_u32, resolve_follow_up_auth, GeminiVideoTaskSeed,
LocalVideoTaskFollowUpPlan, LocalVideoTaskReadResponse, LocalVideoTaskSnapshot,
LocalVideoTaskStatus, DEFAULT_VIDEO_TASK_MAX_POLL_COUNT,
LocalVideoTaskStatus, VideoFollowUpReportContextInput, DEFAULT_VIDEO_TASK_MAX_POLL_COUNT,
DEFAULT_VIDEO_TASK_POLL_INTERVAL_SECONDS,
};
@@ -233,17 +233,19 @@ impl GeminiVideoTaskSeed {
},
report_kind: Some("gemini_video_cancel_sync_finalize".to_string()),
report_context: Some(build_video_follow_up_report_context(
&self.persistence.request_id,
&user_id,
&api_key_id,
&self.local_short_id,
&self.transport.provider_id,
&self.transport.endpoint_id,
&self.transport.key_id,
self.transport.provider_name.as_deref(),
Some(self.model.as_str()),
"gemini:video",
"gemini:video",
VideoFollowUpReportContextInput {
request_id: &self.persistence.request_id,
user_id: &user_id,
api_key_id: &api_key_id,
task_id: &self.local_short_id,
provider_id: &self.transport.provider_id,
endpoint_id: &self.transport.endpoint_id,
key_id: &self.transport.key_id,
provider_name: self.transport.provider_name.as_deref(),
model_name: Some(self.model.as_str()),
client_api_format: "gemini:video",
provider_api_format: "gemini:video",
},
)),
})
}

View File

@@ -18,7 +18,9 @@ mod util;
pub use body::{
context_text, context_u64, request_body_string, request_body_text, request_body_u32,
};
pub use follow_up::{build_video_follow_up_report_context, resolve_follow_up_auth};
pub use follow_up::{
build_video_follow_up_report_context, resolve_follow_up_auth, VideoFollowUpReportContextInput,
};
pub use gemini::map_gemini_stored_task_to_read_response;
pub use openai::map_openai_stored_task_to_read_response;
pub use path::{

View File

@@ -11,7 +11,8 @@ use crate::{
parse_video_content_variant, request_body_string, request_body_u32, resolve_follow_up_auth,
LocalVideoTaskContentAction, LocalVideoTaskFollowUpPlan, LocalVideoTaskReadResponse,
LocalVideoTaskSnapshot, LocalVideoTaskStatus, OpenAiVideoTaskSeed,
DEFAULT_VIDEO_TASK_MAX_POLL_COUNT, DEFAULT_VIDEO_TASK_POLL_INTERVAL_SECONDS,
VideoFollowUpReportContextInput, DEFAULT_VIDEO_TASK_MAX_POLL_COUNT,
DEFAULT_VIDEO_TASK_POLL_INTERVAL_SECONDS,
};
pub fn map_openai_stored_task_to_read_response(
@@ -208,34 +209,36 @@ impl OpenAiVideoTaskSeed {
)
};
Some(LocalVideoTaskContentAction::StreamPlan(ExecutionPlan {
request_id: trace_id.to_string(),
candidate_id: None,
provider_name: self.transport.provider_name.clone(),
provider_id: self.transport.provider_id.clone(),
endpoint_id: self.transport.endpoint_id.clone(),
key_id: self.transport.key_id.clone(),
method: "GET".to_string(),
url,
headers,
content_type: None,
content_encoding: None,
body: RequestBody {
json_body: None,
body_bytes_b64: None,
body_ref: None,
Some(LocalVideoTaskContentAction::StreamPlan(Box::new(
ExecutionPlan {
request_id: trace_id.to_string(),
candidate_id: None,
provider_name: self.transport.provider_name.clone(),
provider_id: self.transport.provider_id.clone(),
endpoint_id: self.transport.endpoint_id.clone(),
key_id: self.transport.key_id.clone(),
method: "GET".to_string(),
url,
headers,
content_type: None,
content_encoding: None,
body: RequestBody {
json_body: None,
body_bytes_b64: None,
body_ref: None,
},
stream: true,
client_api_format: "openai:video".to_string(),
provider_api_format: "openai:video".to_string(),
model_name: self
.model
.clone()
.or_else(|| self.transport.model_name.clone()),
proxy: self.transport.proxy.clone(),
tls_profile: self.transport.tls_profile.clone(),
timeouts: self.transport.timeouts.clone(),
},
stream: true,
client_api_format: "openai:video".to_string(),
provider_api_format: "openai:video".to_string(),
model_name: self
.model
.clone()
.or_else(|| self.transport.model_name.clone()),
proxy: self.transport.proxy.clone(),
tls_profile: self.transport.tls_profile.clone(),
timeouts: self.transport.timeouts.clone(),
}))
)))
}
pub fn client_body_json(&self) -> Value {
@@ -342,17 +345,19 @@ impl OpenAiVideoTaskSeed {
},
report_kind: Some("openai_video_delete_sync_finalize".to_string()),
report_context: Some(build_video_follow_up_report_context(
&self.persistence.request_id,
&user_id,
&api_key_id,
&self.local_task_id,
&self.transport.provider_id,
&self.transport.endpoint_id,
&self.transport.key_id,
self.transport.provider_name.as_deref(),
model_name.as_deref(),
"openai:video",
"openai:video",
VideoFollowUpReportContextInput {
request_id: &self.persistence.request_id,
user_id: &user_id,
api_key_id: &api_key_id,
task_id: &self.local_task_id,
provider_id: &self.transport.provider_id,
endpoint_id: &self.transport.endpoint_id,
key_id: &self.transport.key_id,
provider_name: self.transport.provider_name.as_deref(),
model_name: model_name.as_deref(),
client_api_format: "openai:video",
provider_api_format: "openai:video",
},
)),
})
}
@@ -466,17 +471,19 @@ impl OpenAiVideoTaskSeed {
},
report_kind: Some("openai_video_cancel_sync_finalize".to_string()),
report_context: Some(build_video_follow_up_report_context(
&self.persistence.request_id,
&user_id,
&api_key_id,
&self.local_task_id,
&self.transport.provider_id,
&self.transport.endpoint_id,
&self.transport.key_id,
self.transport.provider_name.as_deref(),
model_name.as_deref(),
"openai:video",
"openai:video",
VideoFollowUpReportContextInput {
request_id: &self.persistence.request_id,
user_id: &user_id,
api_key_id: &api_key_id,
task_id: &self.local_task_id,
provider_id: &self.transport.provider_id,
endpoint_id: &self.transport.endpoint_id,
key_id: &self.transport.key_id,
provider_name: self.transport.provider_name.as_deref(),
model_name: model_name.as_deref(),
client_api_format: "openai:video",
provider_api_format: "openai:video",
},
)),
})
}
@@ -513,19 +520,20 @@ impl OpenAiVideoTaskSeed {
.entry("content-type".to_string())
.or_insert_with(|| content_type.clone());
let mut report_context = build_video_follow_up_report_context(
&self.persistence.request_id,
&user_id,
&api_key_id,
&self.local_task_id,
&self.transport.provider_id,
&self.transport.endpoint_id,
&self.transport.key_id,
self.transport.provider_name.as_deref(),
model_name.as_deref(),
"openai:video",
"openai:video",
);
let mut report_context =
build_video_follow_up_report_context(VideoFollowUpReportContextInput {
request_id: &self.persistence.request_id,
user_id: &user_id,
api_key_id: &api_key_id,
task_id: &self.local_task_id,
provider_id: &self.transport.provider_id,
endpoint_id: &self.transport.endpoint_id,
key_id: &self.transport.key_id,
provider_name: self.transport.provider_name.as_deref(),
model_name: model_name.as_deref(),
client_api_format: "openai:video",
provider_api_format: "openai:video",
});
if let Some(report_context_object) = report_context.as_object_mut() {
report_context_object.insert("original_request_body".to_string(), body_json.clone());
}

View File

@@ -42,7 +42,7 @@ pub struct LocalVideoTaskReadRefreshPlan {
#[derive(Debug, Clone, PartialEq)]
pub enum LocalVideoTaskContentAction {
Immediate { status_code: u16, body_json: Value },
StreamPlan(ExecutionPlan),
StreamPlan(Box<ExecutionPlan>),
}
#[derive(Debug, Clone, PartialEq, Eq)]