@@ -186,7 +186,8 @@ pub struct AppConfig {
|
||||
/// LLM Router 账号级代理固定配置;普通客户端不能覆盖这些值。
|
||||
pub llm_router_base_url: String,
|
||||
pub llm_router_model: String,
|
||||
/// Router 凭据本地加密密钥;缺省时从 JWT secret 做域分离派生。
|
||||
/// Router 用户密码派生根,仅从部署侧 secret/env/file 读取。
|
||||
pub llm_router_provisioning_secret: Option<String>,
|
||||
pub llm_router_api_key_encryption_secret: Option<String>,
|
||||
pub llm_router_admin_token: Option<String>,
|
||||
pub dashscope_base_url: String,
|
||||
@@ -493,6 +494,7 @@ impl Default for AppConfig {
|
||||
llm_retry_backoff_ms: DEFAULT_RETRY_BACKOFF_MS,
|
||||
llm_router_base_url: OFFICIAL_LLM_ROUTER_BASE_URL.to_string(),
|
||||
llm_router_model: OFFICIAL_LLM_ROUTER_MODEL.to_string(),
|
||||
llm_router_provisioning_secret: None,
|
||||
llm_router_api_key_encryption_secret: None,
|
||||
llm_router_admin_token: None,
|
||||
dashscope_base_url: "https://dashscope.aliyuncs.com/api/v1".to_string(),
|
||||
@@ -1210,6 +1212,10 @@ impl AppConfig {
|
||||
config.llm_retry_backoff_ms = llm_retry_backoff_ms;
|
||||
}
|
||||
|
||||
config.llm_router_provisioning_secret = read_secret_env_or_file(
|
||||
&["GENARRATIVE_LLM_ROUTER_PROVISIONING_SECRET"],
|
||||
&["GENARRATIVE_LLM_ROUTER_PROVISIONING_SECRET_FILE"],
|
||||
);
|
||||
config.llm_router_api_key_encryption_secret = read_secret_env_or_file(
|
||||
&["GENARRATIVE_LLM_ROUTER_API_KEY_ENCRYPTION_SECRET"],
|
||||
&["GENARRATIVE_LLM_ROUTER_API_KEY_ENCRYPTION_SECRET_FILE"],
|
||||
|
||||
@@ -56,12 +56,6 @@ const EXTERNAL_API_KEY_PURPOSE_EXTERNAL_EDITOR: &str = "external-editor";
|
||||
const EXTERNAL_API_KEY_PURPOSE_LLM_ROUTER: &str = "llm-router";
|
||||
const LLM_ROUTER_RECONCILIATION_ERROR_PREFIX: &str = "llm-router-reconciliation-required:";
|
||||
const LLM_ROUTER_PROVISIONING_CREDENTIAL_VERSION: u32 = 1;
|
||||
// 阶段性取舍而非设计缺陷:公共 Router 与独立部署数据库当前仍需共享同一
|
||||
// 派生根。暂时将 provisioning secret 固定在 api-server 服务端,避免把它
|
||||
// 暴露为客户端或部署侧可随意覆盖的配置项;后续统一密钥管理和账号迁移方案
|
||||
// 落地后,再按 credential version 迁移,不能静默造成已有账号密码漂移。
|
||||
const LLM_ROUTER_PROVISIONING_SECRET_V1: &[u8] =
|
||||
b"Jq67uCuRN1fs1l08e9ayImp4Uzc4TAY1IaP0UmHz5qTM7pQnEgwzFYhF_SPmK1Bm";
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
|
||||
@@ -348,7 +342,11 @@ pub(crate) async fn ensure_llm_router_account(
|
||||
// safely replaced with the current deterministic values. Unknown remote
|
||||
// outcomes with a known account id remain reconciliation-blocked above.
|
||||
let expected_username = router_username_for_owner(owner_user_id);
|
||||
let expected_password = generate_router_account_password(owner_user_id)?;
|
||||
let provisioning_secret = llm_router_provisioning_secret(state)?;
|
||||
let expected_password = generate_router_account_password_with_secret(
|
||||
owner_user_id,
|
||||
provisioning_secret.as_bytes(),
|
||||
)?;
|
||||
let stale_pending_credentials = persisted.as_ref().is_some_and(|account| {
|
||||
account.router_account_id.is_none()
|
||||
&& bundle.as_ref().is_some_and(|value| {
|
||||
@@ -1351,7 +1349,10 @@ async fn provision_router_account_via_new_api(
|
||||
.or_else(|| pending_login.map(|login| (login.username.clone(), login.password.clone())))
|
||||
.unwrap_or((
|
||||
router_username_for_owner(owner_user_id),
|
||||
generate_router_account_password(owner_user_id)?,
|
||||
generate_router_account_password_with_secret(
|
||||
owner_user_id,
|
||||
llm_router_provisioning_secret(state)?.as_bytes(),
|
||||
)?,
|
||||
));
|
||||
let admin_token = state
|
||||
.config
|
||||
@@ -1975,13 +1976,47 @@ fn router_username_for_owner(owner_user_id: &str) -> String {
|
||||
format!("agc_user_{short_code}")
|
||||
}
|
||||
|
||||
fn llm_router_provisioning_secret(state: &AppState) -> Result<String, String> {
|
||||
if let Some(secret) = state
|
||||
.config
|
||||
.llm_router_provisioning_secret
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
return Ok(secret.to_string());
|
||||
}
|
||||
#[cfg(test)]
|
||||
{
|
||||
return Ok("test-only-router-provisioning-secret".to_string());
|
||||
}
|
||||
#[cfg(not(test))]
|
||||
{
|
||||
Err("LLM Router 账号密码派生密钥未配置".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn generate_router_account_password(owner_user_id: &str) -> Result<String, String> {
|
||||
generate_router_account_password_with_secret(
|
||||
owner_user_id,
|
||||
b"test-only-router-provisioning-secret",
|
||||
)
|
||||
}
|
||||
|
||||
fn generate_router_account_password_with_secret(
|
||||
owner_user_id: &str,
|
||||
provisioning_secret: &[u8],
|
||||
) -> Result<String, String> {
|
||||
let owner_user_id = owner_user_id.trim();
|
||||
if owner_user_id.is_empty() {
|
||||
return Err("LLM Router 账号密码派生参数缺失".to_string());
|
||||
}
|
||||
|
||||
let mut signer = HmacSha256::new_from_slice(LLM_ROUTER_PROVISIONING_SECRET_V1)
|
||||
if provisioning_secret.is_empty() {
|
||||
return Err("LLM Router 账号密码派生密钥未配置".to_string());
|
||||
}
|
||||
let mut signer = HmacSha256::new_from_slice(provisioning_secret)
|
||||
.map_err(|_| "LLM Router 账号密码派生密钥无效".to_string())?;
|
||||
signer.update(
|
||||
format!(
|
||||
|
||||
Reference in New Issue
Block a user