refactor: 拆分 gateway 单体为独立 crate,新增 systemd 部署方案

将 gateway 内部的 model-fetch、provider-transport、scheduler-core、
usage-runtime、video-tasks-core 模块提取为独立 crate;重构 gateway
内部模块结构(state/router/cache/data/query 等);移除大量遗留模块
文件;新增 systemd 二进制部署骨架及相关文档;更新前端 usage 相关
API 和组件。
This commit is contained in:
fawney19
2026-04-05 20:23:16 +08:00
parent cbc811f6ce
commit 763ff03a7b
777 changed files with 42659 additions and 21469 deletions

View File

@@ -0,0 +1,213 @@
use std::collections::BTreeMap;
use serde_json::Value;
use super::super::snapshot::GatewayProviderTransportSnapshot;
pub const ANTIGRAVITY_PROVIDER_TYPE: &str = "antigravity";
pub const ANTIGRAVITY_REQUEST_USER_AGENT: &str = "antigravity";
const ANTIGRAVITY_CLIENT_NAME: &str = "antigravity";
const ANTIGRAVITY_GOOG_API_CLIENT: &str = "gl-node/18.18.2 fire/0.8.6 grpc/1.10.x";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AntigravityRequestAuth {
pub project_id: String,
pub client_version: Option<String>,
pub session_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AntigravityRequestAuthSupport {
Supported(AntigravityRequestAuth),
Unsupported(AntigravityRequestAuthUnsupportedReason),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AntigravityRequestAuthUnsupportedReason {
WrongProviderType,
MissingAuthConfig,
InvalidAuthConfigJson,
ComplexDynamicAuthConfig,
MissingProjectId,
}
pub fn resolve_local_antigravity_request_auth(
transport: &GatewayProviderTransportSnapshot,
) -> AntigravityRequestAuthSupport {
if !transport
.provider
.provider_type
.trim()
.eq_ignore_ascii_case(ANTIGRAVITY_PROVIDER_TYPE)
{
return AntigravityRequestAuthSupport::Unsupported(
AntigravityRequestAuthUnsupportedReason::WrongProviderType,
);
}
let Some(raw_auth_config) = transport
.key
.decrypted_auth_config
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
else {
return AntigravityRequestAuthSupport::Unsupported(
AntigravityRequestAuthUnsupportedReason::MissingAuthConfig,
);
};
let Ok(auth_config) = serde_json::from_str::<Value>(raw_auth_config) else {
return AntigravityRequestAuthSupport::Unsupported(
AntigravityRequestAuthUnsupportedReason::InvalidAuthConfigJson,
);
};
if contains_blocked_auth_fields(&auth_config) {
return AntigravityRequestAuthSupport::Unsupported(
AntigravityRequestAuthUnsupportedReason::ComplexDynamicAuthConfig,
);
}
let Some(project_id) = find_string_by_paths(
&auth_config,
&[
&["project_id"],
&["projectId"],
&["project", "id"],
&["project", "project_id"],
&["project", "projectId"],
&["antigravity", "project_id"],
&["antigravity", "projectId"],
&["metadata", "project_id"],
&["metadata", "projectId"],
],
) else {
return AntigravityRequestAuthSupport::Unsupported(
AntigravityRequestAuthUnsupportedReason::MissingProjectId,
);
};
let client_version = find_string_by_paths(
&auth_config,
&[
&["client_version"],
&["clientVersion"],
&["antigravity", "client_version"],
&["antigravity", "clientVersion"],
&["metadata", "client_version"],
&["metadata", "clientVersion"],
],
);
let session_id = find_string_by_paths(
&auth_config,
&[
&["session_id"],
&["sessionId"],
&["antigravity", "session_id"],
&["antigravity", "sessionId"],
&["metadata", "session_id"],
&["metadata", "sessionId"],
],
);
AntigravityRequestAuthSupport::Supported(AntigravityRequestAuth {
project_id,
client_version,
session_id,
})
}
pub fn build_antigravity_static_identity_headers(
auth: &AntigravityRequestAuth,
) -> BTreeMap<String, String> {
let mut headers = BTreeMap::from([
(
String::from("x-client-name"),
String::from(ANTIGRAVITY_CLIENT_NAME),
),
(
String::from("x-goog-api-client"),
String::from(ANTIGRAVITY_GOOG_API_CLIENT),
),
]);
if let Some(client_version) = auth
.client_version
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
headers.insert(String::from("x-client-version"), client_version.to_string());
}
if let Some(session_id) = auth
.session_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
headers.insert(String::from("x-vscode-sessionid"), session_id.to_string());
}
headers
}
fn find_string_by_paths(value: &Value, paths: &[&[&str]]) -> Option<String> {
for path in paths {
let mut current = value;
let mut matched = true;
for segment in *path {
let Some(next) = current.get(*segment) else {
matched = false;
break;
};
current = next;
}
if !matched {
continue;
}
if let Some(string) = current
.as_str()
.map(str::trim)
.filter(|item| !item.is_empty())
{
return Some(string.to_string());
}
}
None
}
fn contains_blocked_auth_fields(value: &Value) -> bool {
match value {
Value::Object(map) => map.iter().any(|(key, inner)| {
is_blocked_auth_key(key.as_str()) || contains_blocked_auth_fields(inner)
}),
Value::Array(items) => items.iter().any(contains_blocked_auth_fields),
_ => false,
}
}
fn is_blocked_auth_key(key: &str) -> bool {
matches!(
key.trim().to_ascii_lowercase().as_str(),
"private_key"
| "privateKey"
| "private_key_id"
| "privateKeyId"
| "service_account"
| "serviceAccount"
| "service_account_json"
| "serviceAccountJson"
| "service_account_key"
| "serviceAccountKey"
| "credential_source"
| "credentialSource"
| "token_url"
| "tokenUrl"
| "auth_uri"
| "authUri"
| "subject"
| "audience"
)
}

View File

@@ -0,0 +1,113 @@
use serde_json::Value;
use super::super::snapshot::GatewayProviderTransportSnapshot;
use super::auth::{
resolve_local_antigravity_request_auth, AntigravityRequestAuth, AntigravityRequestAuthSupport,
AntigravityRequestAuthUnsupportedReason, ANTIGRAVITY_PROVIDER_TYPE,
};
use super::request::{
classify_antigravity_safe_request_body, AntigravityEnvelopeRequestType,
AntigravityRequestEnvelopeUnsupportedReason,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AntigravityRequestSideSpec {
pub auth: AntigravityRequestAuth,
pub request_type: AntigravityEnvelopeRequestType,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AntigravityRequestSideSupport {
Supported(AntigravityRequestSideSpec),
Unsupported(AntigravityRequestSideUnsupportedReason),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AntigravityRequestSideUnsupportedReason {
InactiveTransport,
WrongProviderType,
UnsupportedApiFormat,
UnsupportedCustomPath,
UnsupportedHeaderRules,
UnsupportedBodyRules,
UnsupportedNetworkConfig,
UnsupportedAuth(AntigravityRequestAuthUnsupportedReason),
UnsupportedEnvelope(AntigravityRequestEnvelopeUnsupportedReason),
}
pub fn classify_local_antigravity_request_support(
transport: &GatewayProviderTransportSnapshot,
request_body: &Value,
request_type: AntigravityEnvelopeRequestType,
) -> AntigravityRequestSideSupport {
if !transport.provider.is_active || !transport.endpoint.is_active || !transport.key.is_active {
return AntigravityRequestSideSupport::Unsupported(
AntigravityRequestSideUnsupportedReason::InactiveTransport,
);
}
if !transport
.provider
.provider_type
.trim()
.eq_ignore_ascii_case(ANTIGRAVITY_PROVIDER_TYPE)
{
return AntigravityRequestSideSupport::Unsupported(
AntigravityRequestSideUnsupportedReason::WrongProviderType,
);
}
let endpoint_format = transport.endpoint.api_format.trim();
if !endpoint_format.eq_ignore_ascii_case("gemini:chat")
&& !endpoint_format.eq_ignore_ascii_case("gemini:cli")
{
return AntigravityRequestSideSupport::Unsupported(
AntigravityRequestSideUnsupportedReason::UnsupportedApiFormat,
);
}
if transport
.endpoint
.custom_path
.as_deref()
.is_some_and(|value| !value.trim().is_empty())
{
return AntigravityRequestSideSupport::Unsupported(
AntigravityRequestSideUnsupportedReason::UnsupportedCustomPath,
);
}
if transport.endpoint.header_rules.is_some() {
return AntigravityRequestSideSupport::Unsupported(
AntigravityRequestSideUnsupportedReason::UnsupportedHeaderRules,
);
}
if transport.endpoint.body_rules.is_some() {
return AntigravityRequestSideSupport::Unsupported(
AntigravityRequestSideUnsupportedReason::UnsupportedBodyRules,
);
}
if transport.provider.proxy.is_some()
|| transport.endpoint.proxy.is_some()
|| transport.key.proxy.is_some()
|| transport.key.fingerprint.is_some()
{
return AntigravityRequestSideSupport::Unsupported(
AntigravityRequestSideUnsupportedReason::UnsupportedNetworkConfig,
);
}
let auth = match resolve_local_antigravity_request_auth(transport) {
AntigravityRequestAuthSupport::Supported(auth) => auth,
AntigravityRequestAuthSupport::Unsupported(reason) => {
return AntigravityRequestSideSupport::Unsupported(
AntigravityRequestSideUnsupportedReason::UnsupportedAuth(reason),
);
}
};
if let Err(reason) = classify_antigravity_safe_request_body(request_body) {
return AntigravityRequestSideSupport::Unsupported(
AntigravityRequestSideUnsupportedReason::UnsupportedEnvelope(reason),
);
}
AntigravityRequestSideSupport::Supported(AntigravityRequestSideSpec { auth, request_type })
}

View File

@@ -0,0 +1,120 @@
use serde_json::{Map, Value};
use super::auth::{AntigravityRequestAuth, ANTIGRAVITY_REQUEST_USER_AGENT};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AntigravityEnvelopeRequestType {
Agent,
EndpointTest,
}
impl AntigravityEnvelopeRequestType {
fn as_str(self) -> &'static str {
match self {
Self::Agent => "agent",
Self::EndpointTest => "endpoint_test",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum AntigravityRequestEnvelopeSupport {
Supported(Value),
Unsupported(AntigravityRequestEnvelopeUnsupportedReason),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AntigravityRequestEnvelopeUnsupportedReason {
NonObjectBody,
MissingContents,
MissingRequestId,
MissingModel,
ComplexEnvelopeTransform,
}
pub fn classify_antigravity_safe_request_body(
request_body: &Value,
) -> Result<(), AntigravityRequestEnvelopeUnsupportedReason> {
let Value::Object(map) = request_body else {
return Err(AntigravityRequestEnvelopeUnsupportedReason::NonObjectBody);
};
if !map.contains_key("contents") {
return Err(AntigravityRequestEnvelopeUnsupportedReason::MissingContents);
}
if contains_blocked_request_features(request_body) {
return Err(AntigravityRequestEnvelopeUnsupportedReason::ComplexEnvelopeTransform);
}
Ok(())
}
pub fn build_antigravity_safe_v1internal_request(
auth: &AntigravityRequestAuth,
request_id: &str,
model: &str,
request_body: &Value,
request_type: AntigravityEnvelopeRequestType,
) -> AntigravityRequestEnvelopeSupport {
if request_id.trim().is_empty() {
return AntigravityRequestEnvelopeSupport::Unsupported(
AntigravityRequestEnvelopeUnsupportedReason::MissingRequestId,
);
}
if model.trim().is_empty() {
return AntigravityRequestEnvelopeSupport::Unsupported(
AntigravityRequestEnvelopeUnsupportedReason::MissingModel,
);
}
if let Err(reason) = classify_antigravity_safe_request_body(request_body) {
return AntigravityRequestEnvelopeSupport::Unsupported(reason);
}
let Value::Object(source) = request_body else {
return AntigravityRequestEnvelopeSupport::Unsupported(
AntigravityRequestEnvelopeUnsupportedReason::NonObjectBody,
);
};
let mut inner_request: Map<String, Value> = source.clone();
inner_request.remove("model");
inner_request.remove("safetySettings");
inner_request.remove("safety_settings");
AntigravityRequestEnvelopeSupport::Supported(serde_json::json!({
"project": auth.project_id,
"requestId": request_id,
"request": Value::Object(inner_request),
"model": model,
"userAgent": ANTIGRAVITY_REQUEST_USER_AGENT,
"requestType": request_type.as_str(),
}))
}
fn contains_blocked_request_features(value: &Value) -> bool {
match value {
Value::Object(map) => map.iter().any(|(key, inner)| {
is_blocked_request_key(key.as_str()) || contains_blocked_request_features(inner)
}),
Value::Array(items) => items.iter().any(contains_blocked_request_features),
_ => false,
}
}
fn is_blocked_request_key(key: &str) -> bool {
matches!(
key.trim(),
"systemInstruction"
| "system_instruction"
| "tools"
| "toolConfig"
| "tool_config"
| "thinkingConfig"
| "thinking_config"
| "imageConfig"
| "image_config"
| "functionCall"
| "function_call"
| "functionResponse"
| "function_response"
)
}

View File

@@ -0,0 +1,69 @@
use std::collections::BTreeMap;
use url::form_urlencoded;
pub const ANTIGRAVITY_V1INTERNAL_PATH_TEMPLATE: &str = "/v1internal:{action}";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AntigravityRequestUrlAction {
GenerateContent,
StreamGenerateContent,
}
impl AntigravityRequestUrlAction {
fn as_str(self) -> &'static str {
match self {
Self::GenerateContent => "generateContent",
Self::StreamGenerateContent => "streamGenerateContent",
}
}
fn is_stream(self) -> bool {
matches!(self, Self::StreamGenerateContent)
}
}
pub fn build_antigravity_v1internal_url(
base_url: &str,
action: AntigravityRequestUrlAction,
query: Option<&BTreeMap<String, String>>,
) -> Option<String> {
let trimmed_base = base_url.trim();
if trimmed_base.is_empty() {
return None;
}
let path = ANTIGRAVITY_V1INTERNAL_PATH_TEMPLATE.replace("{action}", action.as_str());
let mut url = format!("{}{}", trimmed_base.trim_end_matches('/'), path);
let mut params = BTreeMap::new();
if let Some(query) = query {
for (key, value) in query {
let key = key.trim();
let value = value.trim();
if key.is_empty() || value.is_empty() || key.eq_ignore_ascii_case("beta") {
continue;
}
params.insert(key.to_string(), value.to_string());
}
}
if action.is_stream() {
params
.entry(String::from("alt"))
.or_insert_with(|| String::from("sse"));
}
if !params.is_empty() {
let mut serializer = form_urlencoded::Serializer::new(String::new());
for (key, value) in params {
serializer.append_pair(key.as_str(), value.as_str());
}
let query_string = serializer.finish();
if !query_string.is_empty() {
url.push('?');
url.push_str(&query_string);
}
}
Some(url)
}