mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
refactor: 移除独立 hub/proxy/executor/gateway crate,统一为 gateway tunnel 架构
- 删除 aether-hub、aether-proxy 独立项目及其 Dockerfile/配置 - 删除 crates/aether-executor 和 crates/aether-gateway 全部模块 - 新增 apps/ 目录作为应用入口 - 将 hub 概念重构为 gateway tunnel transport - 将 executor 重构为 execution runtime - 新增 tunnel.rs 合约定义和 testkit tunnel/execution_runtime 模块 - 更新 Python 服务层和测试适配新架构命名
This commit is contained in:
53
apps/aether-gateway/src/video_tasks/helpers/body.rs
Normal file
53
apps/aether-gateway/src/video_tasks/helpers/body.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) fn context_text(context: &Map<String, Value>, key: &str) -> Option<String> {
|
||||
context
|
||||
.get(key)
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_string)
|
||||
}
|
||||
|
||||
pub(crate) fn context_u64(context: &Map<String, Value>, key: &str) -> Option<u64> {
|
||||
let value = context.get(key)?;
|
||||
match value {
|
||||
Value::Number(number) => number.as_u64(),
|
||||
Value::String(text) => text.trim().parse().ok(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn request_body_text(context: &Map<String, Value>, key: &str) -> Option<String> {
|
||||
context
|
||||
.get("original_request_body")
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|body| body.get(key))
|
||||
.and_then(|value| match value {
|
||||
Value::String(text) => Some(text.trim().to_string()),
|
||||
Value::Number(number) => Some(number.to_string()),
|
||||
_ => None,
|
||||
})
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
pub(crate) fn request_body_string(body: &Value, key: &str) -> Option<String> {
|
||||
body.as_object()
|
||||
.and_then(|map| map.get(key))
|
||||
.and_then(|value| match value {
|
||||
Value::String(text) => Some(text.trim().to_string()),
|
||||
Value::Number(number) => Some(number.to_string()),
|
||||
_ => None,
|
||||
})
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
pub(crate) fn request_body_u32(body: &Value, key: &str) -> Option<u32> {
|
||||
body.as_object()
|
||||
.and_then(|map| map.get(key))
|
||||
.and_then(|value| match value {
|
||||
Value::Number(number) => number.as_u64().and_then(|value| u32::try_from(value).ok()),
|
||||
Value::String(text) => text.trim().parse().ok(),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
116
apps/aether-gateway/src/video_tasks/helpers/path.rs
Normal file
116
apps/aether-gateway/src/video_tasks/helpers/path.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) fn extract_openai_task_id_from_path(path: &str) -> Option<&str> {
|
||||
let suffix = path.strip_prefix("/v1/videos/")?;
|
||||
if suffix.is_empty()
|
||||
|| suffix.contains('/')
|
||||
|| suffix.ends_with(":cancel")
|
||||
|| suffix.ends_with(":delete")
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(suffix)
|
||||
}
|
||||
|
||||
pub(crate) fn extract_gemini_short_id_from_path(path: &str) -> Option<&str> {
|
||||
let operations_index = path.find("/operations/")?;
|
||||
let suffix = &path[(operations_index + "/operations/".len())..];
|
||||
if suffix.is_empty() || suffix.contains('/') || suffix.ends_with(":cancel") {
|
||||
return None;
|
||||
}
|
||||
Some(suffix)
|
||||
}
|
||||
|
||||
pub(crate) fn extract_openai_task_id_from_cancel_path(path: &str) -> Option<&str> {
|
||||
let suffix = path.strip_prefix("/v1/videos/")?;
|
||||
suffix
|
||||
.strip_suffix("/cancel")
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
pub(crate) fn extract_openai_task_id_from_remix_path(path: &str) -> Option<&str> {
|
||||
let suffix = path.strip_prefix("/v1/videos/")?;
|
||||
suffix
|
||||
.strip_suffix("/remix")
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
pub(crate) fn extract_openai_task_id_from_content_path(path: &str) -> Option<&str> {
|
||||
let suffix = path.strip_prefix("/v1/videos/")?;
|
||||
suffix
|
||||
.strip_suffix("/content")
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
pub(crate) fn extract_gemini_short_id_from_cancel_path(path: &str) -> Option<&str> {
|
||||
let operations_index = path.find("/operations/")?;
|
||||
let suffix = &path[(operations_index + "/operations/".len())..];
|
||||
let short_id = suffix.strip_suffix(":cancel")?;
|
||||
if short_id.is_empty() || short_id.contains('/') {
|
||||
return None;
|
||||
}
|
||||
Some(short_id)
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_local_video_registry_mutation(
|
||||
truth_source_mode: VideoTaskTruthSourceMode,
|
||||
request_path: &str,
|
||||
report_kind: &str,
|
||||
) -> Option<LocalVideoTaskRegistryMutation> {
|
||||
if truth_source_mode != VideoTaskTruthSourceMode::RustAuthoritative {
|
||||
return None;
|
||||
}
|
||||
|
||||
match report_kind {
|
||||
"openai_video_delete_sync_finalize" => {
|
||||
let task_id = extract_openai_task_id_from_path(request_path)?;
|
||||
Some(LocalVideoTaskRegistryMutation::OpenAiDeleted {
|
||||
task_id: task_id.to_string(),
|
||||
})
|
||||
}
|
||||
"openai_video_cancel_sync_finalize" => {
|
||||
let task_id = extract_openai_task_id_from_cancel_path(request_path)?;
|
||||
Some(LocalVideoTaskRegistryMutation::OpenAiCancelled {
|
||||
task_id: task_id.to_string(),
|
||||
})
|
||||
}
|
||||
"gemini_video_cancel_sync_finalize" => {
|
||||
let short_id = extract_gemini_short_id_from_cancel_path(request_path)?;
|
||||
Some(LocalVideoTaskRegistryMutation::GeminiCancelled {
|
||||
short_id: short_id.to_string(),
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn current_unix_timestamp_secs() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
}
|
||||
|
||||
pub(crate) fn local_status_from_stored(status: StoredVideoTaskStatus) -> LocalVideoTaskStatus {
|
||||
match status {
|
||||
StoredVideoTaskStatus::Pending | StoredVideoTaskStatus::Submitted => {
|
||||
LocalVideoTaskStatus::Submitted
|
||||
}
|
||||
StoredVideoTaskStatus::Queued => LocalVideoTaskStatus::Queued,
|
||||
StoredVideoTaskStatus::Processing => LocalVideoTaskStatus::Processing,
|
||||
StoredVideoTaskStatus::Completed => LocalVideoTaskStatus::Completed,
|
||||
StoredVideoTaskStatus::Failed => LocalVideoTaskStatus::Failed,
|
||||
StoredVideoTaskStatus::Cancelled => LocalVideoTaskStatus::Cancelled,
|
||||
StoredVideoTaskStatus::Expired => LocalVideoTaskStatus::Expired,
|
||||
StoredVideoTaskStatus::Deleted => LocalVideoTaskStatus::Deleted,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn generate_local_short_id() -> String {
|
||||
Uuid::new_v4()
|
||||
.simple()
|
||||
.to_string()
|
||||
.chars()
|
||||
.take(12)
|
||||
.collect()
|
||||
}
|
||||
237
apps/aether-gateway/src/video_tasks/helpers/transport.rs
Normal file
237
apps/aether-gateway/src/video_tasks/helpers/transport.rs
Normal file
@@ -0,0 +1,237 @@
|
||||
use super::*;
|
||||
|
||||
impl LocalVideoTaskTransport {
|
||||
pub(crate) fn from_plan(plan: &ExecutionPlan) -> Option<Self> {
|
||||
let upstream_base_url = match plan.provider_api_format.as_str() {
|
||||
"openai:video" => plan.url.split("/v1/videos").next()?.to_string(),
|
||||
"gemini:video" => plan.url.split("/v1beta/").next()?.to_string(),
|
||||
_ => return None,
|
||||
};
|
||||
if upstream_base_url.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(Self {
|
||||
upstream_base_url,
|
||||
provider_name: plan.provider_name.clone(),
|
||||
provider_id: plan.provider_id.clone(),
|
||||
endpoint_id: plan.endpoint_id.clone(),
|
||||
key_id: plan.key_id.clone(),
|
||||
headers: plan.headers.clone(),
|
||||
content_type: plan.content_type.clone(),
|
||||
model_name: plan.model_name.clone(),
|
||||
proxy: plan.proxy.clone(),
|
||||
tls_profile: plan.tls_profile.clone(),
|
||||
timeouts: plan.timeouts.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn from_provider_transport(
|
||||
transport: &GatewayProviderTransportSnapshot,
|
||||
api_format: &str,
|
||||
model_name: Option<String>,
|
||||
) -> Option<Self> {
|
||||
let (auth_header, auth_value) = match api_format {
|
||||
"openai:video" => {
|
||||
if !supports_local_standard_transport(transport, api_format) {
|
||||
return None;
|
||||
}
|
||||
resolve_local_standard_auth(transport)?
|
||||
}
|
||||
"gemini:video" => {
|
||||
if !supports_local_gemini_transport(transport, api_format) {
|
||||
return None;
|
||||
}
|
||||
resolve_local_gemini_auth(transport)?
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let mut headers = BTreeMap::new();
|
||||
headers.insert(auth_header, auth_value);
|
||||
|
||||
Some(Self {
|
||||
upstream_base_url: transport.endpoint.base_url.clone(),
|
||||
provider_name: Some(transport.provider.name.clone()),
|
||||
provider_id: transport.provider.id.clone(),
|
||||
endpoint_id: transport.endpoint.id.clone(),
|
||||
key_id: transport.key.id.clone(),
|
||||
headers,
|
||||
content_type: Some("application/json".to_string()),
|
||||
model_name,
|
||||
proxy: None,
|
||||
tls_profile: None,
|
||||
timeouts: resolve_transport_execution_timeouts(transport),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalVideoTaskPersistence {
|
||||
pub(crate) fn from_report_context(
|
||||
report_context: &Map<String, Value>,
|
||||
plan: &ExecutionPlan,
|
||||
) -> Self {
|
||||
Self {
|
||||
request_id: context_text(report_context, "request_id")
|
||||
.unwrap_or_else(|| plan.request_id.clone()),
|
||||
username: context_text(report_context, "username"),
|
||||
api_key_name: context_text(report_context, "api_key_name"),
|
||||
client_api_format: context_text(report_context, "client_api_format")
|
||||
.unwrap_or_else(|| plan.client_api_format.clone()),
|
||||
provider_api_format: context_text(report_context, "provider_api_format")
|
||||
.unwrap_or_else(|| plan.provider_api_format.clone()),
|
||||
original_request_body: report_context
|
||||
.get("original_request_body")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Value::Object(Map::new())),
|
||||
format_converted: report_context
|
||||
.get("format_converted")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_stored_task(task: &StoredVideoTask) -> Option<Self> {
|
||||
let client_api_format = non_empty_owned(task.client_api_format.as_ref())
|
||||
.or_else(|| non_empty_owned(task.provider_api_format.as_ref()))?;
|
||||
let provider_api_format = non_empty_owned(task.provider_api_format.as_ref())
|
||||
.or_else(|| non_empty_owned(task.client_api_format.as_ref()))?;
|
||||
|
||||
Some(Self {
|
||||
request_id: task.request_id.clone(),
|
||||
username: task.username.clone(),
|
||||
api_key_name: task.api_key_name.clone(),
|
||||
client_api_format,
|
||||
provider_api_format,
|
||||
original_request_body: task
|
||||
.original_request_body
|
||||
.clone()
|
||||
.unwrap_or_else(|| Value::Object(Map::new())),
|
||||
format_converted: task.format_converted,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalVideoTaskStatus {
|
||||
pub(crate) fn as_database_status(self) -> StoredVideoTaskStatus {
|
||||
match self {
|
||||
Self::Submitted => StoredVideoTaskStatus::Submitted,
|
||||
Self::Queued => StoredVideoTaskStatus::Queued,
|
||||
Self::Processing => StoredVideoTaskStatus::Processing,
|
||||
Self::Completed => StoredVideoTaskStatus::Completed,
|
||||
Self::Failed => StoredVideoTaskStatus::Failed,
|
||||
Self::Cancelled => StoredVideoTaskStatus::Cancelled,
|
||||
Self::Expired => StoredVideoTaskStatus::Expired,
|
||||
Self::Deleted => StoredVideoTaskStatus::Deleted,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_video_content_variant(query_string: Option<&str>) -> Option<&'static str> {
|
||||
let mut variant = "video";
|
||||
if let Some(query_string) = query_string {
|
||||
for (key, value) in url::form_urlencoded::parse(query_string.as_bytes()) {
|
||||
if key == "variant" {
|
||||
variant = match value.as_ref() {
|
||||
"video" => "video",
|
||||
"thumbnail" => "thumbnail",
|
||||
"spritesheet" => "spritesheet",
|
||||
_ => return None,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(variant)
|
||||
}
|
||||
|
||||
pub(crate) fn gemini_metadata_video_url(metadata: &Value) -> Option<String> {
|
||||
metadata
|
||||
.get("response")
|
||||
.and_then(|value| value.get("generateVideoResponse"))
|
||||
.and_then(|value| value.get("generatedSamples"))
|
||||
.and_then(Value::as_array)
|
||||
.and_then(|value| value.first())
|
||||
.and_then(|value| value.get("video"))
|
||||
.and_then(|value| value.get("uri"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string)
|
||||
}
|
||||
|
||||
pub(crate) fn build_video_follow_up_report_context(
|
||||
request_id: &str,
|
||||
user_id: &str,
|
||||
api_key_id: &str,
|
||||
task_id: &str,
|
||||
model_name: Option<String>,
|
||||
transport: &LocalVideoTaskTransport,
|
||||
client_api_format: &str,
|
||||
provider_api_format: &str,
|
||||
) -> Value {
|
||||
let mut context = Map::new();
|
||||
context.insert(
|
||||
"request_id".to_string(),
|
||||
Value::String(request_id.to_string()),
|
||||
);
|
||||
context.insert("user_id".to_string(), Value::String(user_id.to_string()));
|
||||
context.insert(
|
||||
"api_key_id".to_string(),
|
||||
Value::String(api_key_id.to_string()),
|
||||
);
|
||||
context.insert("task_id".to_string(), Value::String(task_id.to_string()));
|
||||
context.insert(
|
||||
"provider_id".to_string(),
|
||||
Value::String(transport.provider_id.clone()),
|
||||
);
|
||||
context.insert(
|
||||
"endpoint_id".to_string(),
|
||||
Value::String(transport.endpoint_id.clone()),
|
||||
);
|
||||
context.insert(
|
||||
"key_id".to_string(),
|
||||
Value::String(transport.key_id.clone()),
|
||||
);
|
||||
context.insert(
|
||||
"client_api_format".to_string(),
|
||||
Value::String(client_api_format.to_string()),
|
||||
);
|
||||
context.insert(
|
||||
"provider_api_format".to_string(),
|
||||
Value::String(provider_api_format.to_string()),
|
||||
);
|
||||
if let Some(provider_name) = transport.provider_name.clone() {
|
||||
context.insert("provider_name".to_string(), Value::String(provider_name));
|
||||
}
|
||||
if let Some(model_name) = model_name.filter(|value| !value.is_empty()) {
|
||||
context.insert("model".to_string(), Value::String(model_name));
|
||||
}
|
||||
Value::Object(context)
|
||||
}
|
||||
|
||||
pub(crate) fn map_openai_task_status(status: LocalVideoTaskStatus) -> &'static str {
|
||||
match status {
|
||||
LocalVideoTaskStatus::Submitted | LocalVideoTaskStatus::Queued => "queued",
|
||||
LocalVideoTaskStatus::Processing => "processing",
|
||||
LocalVideoTaskStatus::Completed => "completed",
|
||||
LocalVideoTaskStatus::Failed
|
||||
| LocalVideoTaskStatus::Cancelled
|
||||
| LocalVideoTaskStatus::Expired => "failed",
|
||||
LocalVideoTaskStatus::Deleted => "deleted",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_follow_up_auth(
|
||||
user_id: Option<&str>,
|
||||
api_key_id: Option<&str>,
|
||||
auth_context: Option<&GatewayControlAuthContext>,
|
||||
) -> Option<(String, String)> {
|
||||
let resolved_user_id = user_id
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_string)
|
||||
.or_else(|| auth_context.map(|value| value.user_id.clone()))?;
|
||||
let resolved_api_key_id = api_key_id
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_string)
|
||||
.or_else(|| auth_context.map(|value| value.api_key_id.clone()))?;
|
||||
Some((resolved_user_id, resolved_api_key_id))
|
||||
}
|
||||
7
apps/aether-gateway/src/video_tasks/helpers/util.rs
Normal file
7
apps/aether-gateway/src/video_tasks/helpers/util.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub(crate) fn non_empty_owned(value: Option<&String>) -> Option<String> {
|
||||
value
|
||||
.map(String::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_string)
|
||||
}
|
||||
Reference in New Issue
Block a user