AGC 官方 LLM Router 账号链路与流式联网输出 #242
@@ -950,7 +950,6 @@ function buildExternalApiKeyQuery(query: AdminExternalApiKeyListQuery) {
|
||||
appendQueryParam(params, 'createdAfter', query.createdAfter);
|
||||
appendQueryParam(params, 'createdBefore', query.createdBefore);
|
||||
appendQueryParam(params, 'status', query.status);
|
||||
appendQueryParam(params, 'purpose', query.purpose);
|
||||
if (typeof query.limit === 'number' && Number.isFinite(query.limit)) {
|
||||
params.set('limit', String(Math.floor(query.limit)));
|
||||
}
|
||||
|
||||
@@ -284,7 +284,6 @@ export interface AdminExternalApiKeyListQuery {
|
||||
createdAfter?: string;
|
||||
createdBefore?: string;
|
||||
status?: 'active' | 'revoked';
|
||||
purpose?: 'external-editor' | 'llm-router';
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
sortColumn?:
|
||||
@@ -292,7 +291,6 @@ export interface AdminExternalApiKeyListQuery {
|
||||
| 'ownerUserId'
|
||||
| 'name'
|
||||
| 'keyPrefix'
|
||||
| 'purpose'
|
||||
| 'createdAt'
|
||||
| 'lastUsedAt'
|
||||
| 'updatedAt';
|
||||
@@ -304,7 +302,6 @@ export interface AdminExternalApiKeyPayload {
|
||||
ownerUserId: string;
|
||||
name: string;
|
||||
keyPrefix: string;
|
||||
purpose: string;
|
||||
scopes: string[];
|
||||
createdAt: string;
|
||||
lastUsedAt: string | null;
|
||||
|
||||
@@ -109,7 +109,6 @@ test('external_api_key 使用专用安全查询且详情不展示原始 JSON', a
|
||||
ownerUserId: 'user-1',
|
||||
name: 'agc_auto_generate',
|
||||
keyPrefix: 'tnr_sk_fixture',
|
||||
purpose: 'llm-router',
|
||||
scopes: ['llm:responses'],
|
||||
createdAt: '2026-08-29T00:00:00Z',
|
||||
lastUsedAt: null,
|
||||
@@ -135,7 +134,7 @@ test('external_api_key 使用专用安全查询且详情不展示原始 JSON', a
|
||||
await waitFor(() => {
|
||||
expect(getAdminExternalApiKeys).toHaveBeenLastCalledWith(
|
||||
'admin-token',
|
||||
expect.objectContaining({ ownerUserId: 'user-1', purpose: undefined }),
|
||||
expect.objectContaining({ ownerUserId: 'user-1' }),
|
||||
);
|
||||
});
|
||||
expect(await screen.findByText('tnr_sk_fixture')).toBeTruthy();
|
||||
|
||||
@@ -47,7 +47,6 @@ type ExternalApiKeySortColumn =
|
||||
| 'ownerUserId'
|
||||
| 'name'
|
||||
| 'keyPrefix'
|
||||
| 'purpose'
|
||||
| 'createdAt'
|
||||
| 'lastUsedAt'
|
||||
| 'updatedAt';
|
||||
@@ -1219,9 +1218,6 @@ function AdminExternalApiKeysPanel({
|
||||
const [createdAfter, setCreatedAfter] = useState('');
|
||||
const [createdBefore, setCreatedBefore] = useState('');
|
||||
const [status, setStatus] = useState<'' | 'active' | 'revoked'>('');
|
||||
const [purpose, setPurpose] = useState<'' | 'external-editor' | 'llm-router'>(
|
||||
'',
|
||||
);
|
||||
const [limit, setLimit] = useState('100');
|
||||
const [sortColumn, setSortColumn] = useState<ExternalApiKeySortColumn>('');
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
|
||||
@@ -1269,7 +1265,6 @@ function AdminExternalApiKeysPanel({
|
||||
createdAfter: createdAfter.trim() || undefined,
|
||||
createdBefore: createdBefore.trim() || undefined,
|
||||
status: status || undefined,
|
||||
purpose: purpose || undefined,
|
||||
limit: parseLimit(limit),
|
||||
offset: nextOffset,
|
||||
sortColumn: sortColumn || undefined,
|
||||
@@ -1294,7 +1289,6 @@ function AdminExternalApiKeysPanel({
|
||||
setCreatedAfter('');
|
||||
setCreatedBefore('');
|
||||
setStatus('');
|
||||
setPurpose('');
|
||||
setLimit('100');
|
||||
setSortColumn('');
|
||||
setSortDirection('desc');
|
||||
@@ -1359,19 +1353,6 @@ function AdminExternalApiKeysPanel({
|
||||
onChange={(event) => setName(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label className="admin-field">
|
||||
<span>用途</span>
|
||||
<select
|
||||
value={purpose}
|
||||
onChange={(event) =>
|
||||
setPurpose(event.target.value as typeof purpose)
|
||||
}
|
||||
>
|
||||
<option value="">全部</option>
|
||||
<option value="external-editor">external-editor</option>
|
||||
<option value="llm-router">llm-router</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="admin-field">
|
||||
<span>状态</span>
|
||||
<select
|
||||
@@ -1422,7 +1403,6 @@ function AdminExternalApiKeysPanel({
|
||||
<option value="ownerUserId">Owner</option>
|
||||
<option value="name">名称</option>
|
||||
<option value="keyPrefix">Key 前缀</option>
|
||||
<option value="purpose">用途</option>
|
||||
<option value="createdAt">创建时间</option>
|
||||
<option value="lastUsedAt">最近使用</option>
|
||||
<option value="updatedAt">更新时间</option>
|
||||
@@ -1483,7 +1463,6 @@ function AdminExternalApiKeysPanel({
|
||||
<th>Owner</th>
|
||||
<th>名称</th>
|
||||
<th>前缀</th>
|
||||
<th>用途</th>
|
||||
<th>Scope</th>
|
||||
<th>创建时间</th>
|
||||
<th>状态</th>
|
||||
@@ -1498,7 +1477,6 @@ function AdminExternalApiKeysPanel({
|
||||
<td>{key.ownerUserId}</td>
|
||||
<td>{key.name}</td>
|
||||
<td>{key.keyPrefix}</td>
|
||||
<td>{key.purpose}</td>
|
||||
<td>{key.scopes.join(', ')}</td>
|
||||
<td>{formatSafeDate(key.createdAt)}</td>
|
||||
<td>{key.status}</td>
|
||||
@@ -1516,7 +1494,7 @@ function AdminExternalApiKeysPanel({
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={9}>
|
||||
<td colSpan={8}>
|
||||
{result ? '暂无数据' : '请先提供精确范围并查询'}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -1597,8 +1575,6 @@ function AdminExternalApiKeysPanel({
|
||||
<dd>{selectedKey.name}</dd>
|
||||
<dt>前缀</dt>
|
||||
<dd>{selectedKey.keyPrefix}</dd>
|
||||
<dt>用途</dt>
|
||||
<dd>{selectedKey.purpose}</dd>
|
||||
<dt>Scope</dt>
|
||||
<dd>{selectedKey.scopes.join(', ') || '-'}</dd>
|
||||
<dt>创建时间</dt>
|
||||
|
||||
@@ -631,7 +631,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
|
||||
|
||||
- Rust 结构体:`ExternalApiKey`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/external_api_key_storage.rs`
|
||||
- 说明:本表只承载普通外部 OpenAPI/MCP API Key。明文只在 `/api/profile/api-keys` 创建接口返回一次,服务端保存 `key_hash`、`key_prefix`、作用域、用途和撤销状态;本需求不向该表增加 Router 字段,也不写入 `purpose=llm-router` 数据。`purpose=external-editor` 是默认用途,v1 默认作用域为 `editor:project`、`editor:canvas`、`editor:image-generate`、`editor:asset`。
|
||||
- 说明:本表只承载普通外部 OpenAPI/MCP API Key。明文只在 `/api/profile/api-keys` 创建接口返回一次,服务端保存 `key_hash`、`key_prefix`、作用域和撤销状态;本需求不向该表增加 Router 字段,也不写入 Router 账号数据。v1 默认作用域为 `editor:project`、`editor:canvas`、`editor:image-generate`、`editor:asset`。
|
||||
- 索引:`by_external_api_key_owner_user_id` 用于登录态 API Key 列表;`key_hash` 唯一索引用于外部 API 鉴权。
|
||||
- 2026-08-31 修订:Router 账号状态、API Key 核心字段、账号元数据和加密凭据统一写入 `llm_router_account`;`external_api_key` 保持普通外部 OpenAPI/MCP Key 的原有链路不变。公共 Router、独立数据库的部署必须共享同一版本 provisioning secret,数据库缺行时才能恢复同一远端账号。Responses 请求成功后再按 usage 写入钱包扣费流水,失败请求不扣费。
|
||||
- 2026-09-01 修订:Router provisioning 允许所有环境使用官方固定 Router 控制面,以便独立开发数据库通过完整 owner `user_id` 的稳定派生凭据和 `agc_auto_generate` Token 复用同一远端账号/Key。由于 New API 的 `username`、`password`、`display_name` 均限制 20 个字符,用户名固定为 `agc_user_` 加 11 位 URL-safe SHA-256 短码,密码为基于完整 owner `user_id` 与 provisioning secret 派生的 20 位 hex;完整 owner `user_id` 通过 New API 用户 `remark` 字段保存,并在本地 `llm_router_account.owner_user_id` 保留权威映射。非官方公网地址仍拒绝,loopback 仅用于本地 fixture。使用共享官方 Router 的非生产环境启动时告警,提醒会触及线上账号与额度。api-server 不再提供任何 fallback Key 路径;没有已完成 provisioning 的账号行时,LLM 请求必须在本地解析阶段失败关闭。
|
||||
@@ -642,7 +642,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
|
||||
|
||||
### `llm_router_account`
|
||||
|
||||
- 当前 AGC Router 需求由本表单独承载:API Key 核心字段、加密凭据、Router 账号元数据、生命周期与 provisioning 状态均在本表;不再依赖 `external_api_key`。api-server 首次需要上游调用时解密凭据,并按 owner + route 使用 10 分钟进程内缓存;轮换或撤销时清理缓存。
|
||||
- 当前 AGC Router 需求由 `llm_router_account` 表单独承载:API Key 核心字段、加密凭据、Router 账号元数据、生命周期与 provisioning 状态均在该表;不依赖 `external_api_key`。api-server 首次需要上游调用时解密凭据,并按 owner + route 使用 10 分钟进程内缓存;轮换或撤销时清理缓存。
|
||||
|
||||
- Rust 结构体:`LlmRouterAccount`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/llm_router_account.rs`
|
||||
|
||||
@@ -440,7 +440,6 @@ struct ValidatedAdminExternalApiKeyQuery {
|
||||
created_after_micros: Option<i64>,
|
||||
created_before_micros: Option<i64>,
|
||||
status: Option<String>,
|
||||
purpose: Option<String>,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
sort_column: Option<String>,
|
||||
@@ -475,13 +474,6 @@ fn validate_admin_external_api_key_query(
|
||||
return Err(AppError::from_status(StatusCode::BAD_REQUEST)
|
||||
.with_message("status 只能是 active 或 revoked"));
|
||||
}
|
||||
let purpose = normalized_non_empty(query.purpose.as_deref()).map(str::to_owned);
|
||||
if let Some(purpose) = purpose.as_deref()
|
||||
&& !matches!(purpose, "external-editor" | "llm-router")
|
||||
{
|
||||
return Err(AppError::from_status(StatusCode::BAD_REQUEST)
|
||||
.with_message("purpose 只能是 external-editor 或 llm-router"));
|
||||
}
|
||||
let created_after_micros = normalized_non_empty(query.created_after.as_deref())
|
||||
.map(|value| {
|
||||
parse_timestamp_text_to_micros(value).ok_or_else(|| {
|
||||
@@ -518,7 +510,6 @@ fn validate_admin_external_api_key_query(
|
||||
| "ownerUserId"
|
||||
| "name"
|
||||
| "keyPrefix"
|
||||
| "purpose"
|
||||
| "createdAt"
|
||||
| "lastUsedAt"
|
||||
| "updatedAt"
|
||||
@@ -542,7 +533,6 @@ fn validate_admin_external_api_key_query(
|
||||
created_after_micros,
|
||||
created_before_micros,
|
||||
status,
|
||||
purpose,
|
||||
limit: query.limit.unwrap_or(100).clamp(1, 500),
|
||||
offset: query.offset.unwrap_or(0),
|
||||
sort_column,
|
||||
@@ -574,7 +564,6 @@ async fn fetch_admin_external_api_keys(
|
||||
query.key_id.as_deref(),
|
||||
query.key_prefix.as_deref(),
|
||||
query.status.as_deref(),
|
||||
query.purpose.as_deref(),
|
||||
);
|
||||
let payload = fetch_spacetime_sql_json_limited(
|
||||
&client,
|
||||
@@ -629,12 +618,6 @@ async fn fetch_admin_external_api_keys(
|
||||
.contains(&value.to_ascii_lowercase())
|
||||
})
|
||||
})
|
||||
.filter(|entry| {
|
||||
query
|
||||
.purpose
|
||||
.as_deref()
|
||||
.is_none_or(|value| entry.purpose == value)
|
||||
})
|
||||
.filter(|entry| match query.status.as_deref() {
|
||||
Some("active") => entry.status == "active",
|
||||
Some("revoked") => entry.status == "revoked",
|
||||
@@ -659,7 +642,6 @@ async fn fetch_admin_external_api_keys(
|
||||
"ownerUserId" => left.owner_user_id.cmp(&right.owner_user_id),
|
||||
"name" => left.name.cmp(&right.name),
|
||||
"keyPrefix" => left.key_prefix.cmp(&right.key_prefix),
|
||||
"purpose" => left.purpose.cmp(&right.purpose),
|
||||
"lastUsedAt" => left.last_used_at.cmp(&right.last_used_at),
|
||||
"updatedAt" => left.updated_at.cmp(&right.updated_at),
|
||||
_ => left.created_at.cmp(&right.created_at),
|
||||
@@ -696,7 +678,6 @@ fn build_admin_external_api_key_sql(
|
||||
key_id: Option<&str>,
|
||||
key_prefix: Option<&str>,
|
||||
status: Option<&str>,
|
||||
purpose: Option<&str>,
|
||||
) -> String {
|
||||
let mut conditions = Vec::new();
|
||||
if let Some(owner) = owner_user_id {
|
||||
@@ -713,9 +694,6 @@ fn build_admin_external_api_key_sql(
|
||||
// filter below. Name/time filters deliberately stay in the bounded scan
|
||||
// layer because LIKE/Timestamp literal syntax varies across standalone
|
||||
// versions; the response advertises when that bounded result is partial.
|
||||
if let Some(value) = purpose {
|
||||
conditions.push(format!("purpose = {}", quote_sql_string(value)));
|
||||
}
|
||||
if let Some(value) = status {
|
||||
conditions.push(match value {
|
||||
"active" => "revoked_at IS NULL".to_string(),
|
||||
@@ -729,7 +707,7 @@ fn build_admin_external_api_key_sql(
|
||||
format!(" WHERE {}", conditions.join(" AND "))
|
||||
};
|
||||
format!(
|
||||
"SELECT key_id,owner_user_id,name,key_prefix,scopes_json,created_at,last_used_at,revoked_at,updated_at,purpose FROM external_api_key{where_clause} LIMIT {}",
|
||||
"SELECT key_id,owner_user_id,name,key_prefix,scopes_json,created_at,last_used_at,revoked_at,updated_at FROM external_api_key{where_clause} LIMIT {}",
|
||||
ADMIN_EXTERNAL_API_KEY_SCAN_LIMIT + 1
|
||||
)
|
||||
}
|
||||
@@ -798,8 +776,6 @@ fn parse_admin_external_api_key_row(
|
||||
let owner_user_id = value_to_string(get("owner_user_id")?)?;
|
||||
let name = value_to_string(get("name")?)?;
|
||||
let key_prefix = value_to_string(get("key_prefix")?)?;
|
||||
let purpose = value_to_string(get("purpose").unwrap_or(&Value::Null))
|
||||
.unwrap_or_else(|| "external-editor".to_string());
|
||||
let scopes = value_to_string(get("scopes_json").unwrap_or(&Value::Null))
|
||||
.and_then(|text| serde_json::from_str::<Vec<String>>(&text).ok())
|
||||
.unwrap_or_default();
|
||||
@@ -812,7 +788,6 @@ fn parse_admin_external_api_key_row(
|
||||
owner_user_id,
|
||||
name,
|
||||
key_prefix,
|
||||
purpose,
|
||||
scopes,
|
||||
created_at,
|
||||
last_used_at,
|
||||
@@ -5771,7 +5746,6 @@ mod tests {
|
||||
"last_used_at".to_string(),
|
||||
"revoked_at".to_string(),
|
||||
"updated_at".to_string(),
|
||||
"purpose".to_string(),
|
||||
];
|
||||
let entry = parse_admin_external_api_key_row(
|
||||
&json!([
|
||||
@@ -5783,15 +5757,13 @@ mod tests {
|
||||
"2026-08-29T00:00:00Z",
|
||||
null,
|
||||
null,
|
||||
"2026-08-29T00:00:00Z",
|
||||
"llm-router"
|
||||
"2026-08-29T00:00:00Z"
|
||||
]),
|
||||
&columns,
|
||||
)
|
||||
.expect("safe API key metadata");
|
||||
|
||||
assert_eq!(entry.key_id, "external-api-key-1");
|
||||
assert_eq!(entry.purpose, "llm-router");
|
||||
assert_eq!(entry.scopes, vec!["llm:responses"]);
|
||||
assert_eq!(entry.status, "active");
|
||||
let serialized = serde_json::to_string(&entry).expect("serialize safe metadata");
|
||||
@@ -5806,12 +5778,10 @@ mod tests {
|
||||
None,
|
||||
Some("tnr_sk_abc"),
|
||||
Some("active"),
|
||||
Some("llm-router"),
|
||||
);
|
||||
assert!(sql.contains("owner_user_id = 'user-''1'"));
|
||||
assert!(sql.contains("key_prefix = 'tnr_sk_abc'"));
|
||||
assert!(sql.contains("revoked_at IS NULL"));
|
||||
assert!(sql.contains("purpose = 'llm-router'"));
|
||||
assert!(!sql.contains("key_hash"));
|
||||
assert!(!sql.contains("secret_ciphertext"));
|
||||
assert!(sql.contains("LIMIT 5001"));
|
||||
@@ -5847,18 +5817,6 @@ mod tests {
|
||||
.contains("status")
|
||||
);
|
||||
|
||||
let invalid_purpose = AdminExternalApiKeyListQuery {
|
||||
owner_user_id: Some("owner-1".to_string()),
|
||||
purpose: Some("router-admin".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
validate_admin_external_api_key_query(&invalid_purpose)
|
||||
.expect_err("invalid purpose")
|
||||
.message()
|
||||
.contains("purpose")
|
||||
);
|
||||
|
||||
let invalid_sort = AdminExternalApiKeyListQuery {
|
||||
owner_user_id: Some("owner-1".to_string()),
|
||||
sort_column: Some("keyHash".to_string()),
|
||||
@@ -5907,7 +5865,6 @@ mod tests {
|
||||
created_after: Some("2026-08-29T00:00:00Z".to_string()),
|
||||
created_before: Some("2026-08-29T23:59:59Z".to_string()),
|
||||
status: Some("active".to_string()),
|
||||
purpose: Some("llm-router".to_string()),
|
||||
limit: Some(5000),
|
||||
offset: Some(77),
|
||||
sort_column: Some("createdAt".to_string()),
|
||||
@@ -5920,7 +5877,6 @@ mod tests {
|
||||
assert_eq!(query.name.as_deref(), Some("AGC"));
|
||||
assert_eq!(query.key_prefix.as_deref(), Some("tnr_sk_"));
|
||||
assert_eq!(query.status.as_deref(), Some("active"));
|
||||
assert_eq!(query.purpose.as_deref(), Some("llm-router"));
|
||||
assert_eq!(query.limit, 500);
|
||||
assert_eq!(query.offset, 77);
|
||||
assert_eq!(query.sort_direction.as_deref(), Some("asc"));
|
||||
|
||||
@@ -53,8 +53,6 @@ const LLM_ROUTER_TOKEN_GROUP: &str = "default";
|
||||
const LLM_ROUTER_API_KEY_SCOPES: [&str; 1] = ["llm:responses"];
|
||||
const LLM_ROUTER_SUBSCRIPTION_PLAN_ID: i64 = 1;
|
||||
const LLM_ROUTER_SUBSCRIPTION_RENEWAL_THRESHOLD_SECONDS: i64 = 24 * 60 * 60;
|
||||
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;
|
||||
const LLM_ROUTER_CREDENTIAL_CACHE_TTL: Duration = Duration::from_secs(600);
|
||||
@@ -126,7 +124,6 @@ pub struct ExternalApiKeyPayload {
|
||||
name: String,
|
||||
key_prefix: String,
|
||||
scopes: Vec<String>,
|
||||
purpose: String,
|
||||
created_at: String,
|
||||
last_used_at: Option<String>,
|
||||
revoked_at: Option<String>,
|
||||
@@ -163,10 +160,6 @@ pub async fn list_external_api_keys(
|
||||
.await
|
||||
.map_err(map_external_api_key_error)?
|
||||
.into_iter()
|
||||
// LLM Router credentials are an internal server-side implementation
|
||||
// detail. The ordinary profile API-key surface must never expose
|
||||
// even their safe metadata or allow users to manage their lifecycle.
|
||||
.filter(|record| record.purpose == EXTERNAL_API_KEY_PURPOSE_EXTERNAL_EDITOR)
|
||||
.map(external_api_key_payload_from_record)
|
||||
.collect();
|
||||
|
||||
@@ -198,13 +191,6 @@ pub async fn create_external_api_key(
|
||||
key_hash: hash_external_api_key(raw_key.as_str()),
|
||||
scopes: default_external_api_key_scopes(),
|
||||
now_micros: current_utc_micros(),
|
||||
purpose: EXTERNAL_API_KEY_PURPOSE_EXTERNAL_EDITOR.to_string(),
|
||||
secret_ciphertext: None,
|
||||
provider_account_id: None,
|
||||
provider_base_url: None,
|
||||
provider_model: None,
|
||||
provider_account_json: None,
|
||||
credential_version: 1,
|
||||
})
|
||||
.await
|
||||
.map_err(map_external_api_key_error)?;
|
||||
@@ -2246,7 +2232,6 @@ fn external_api_key_payload_from_record(record: ExternalApiKeyRecord) -> Externa
|
||||
name: record.name,
|
||||
key_prefix: record.key_prefix,
|
||||
scopes: record.scopes,
|
||||
purpose: record.purpose,
|
||||
created_at: record.created_at,
|
||||
last_used_at: record.last_used_at,
|
||||
revoked_at: record.revoked_at,
|
||||
@@ -2265,7 +2250,6 @@ fn external_api_key_payload_from_router_account(
|
||||
.iter()
|
||||
.map(|scope| (*scope).to_string())
|
||||
.collect(),
|
||||
purpose: EXTERNAL_API_KEY_PURPOSE_LLM_ROUTER.to_string(),
|
||||
created_at: account.created_at,
|
||||
last_used_at: account.last_used_at,
|
||||
revoked_at: account.revoked_at,
|
||||
|
||||
@@ -735,7 +735,6 @@ pub struct AdminExternalApiKeyListQuery {
|
||||
pub created_after: Option<String>,
|
||||
pub created_before: Option<String>,
|
||||
pub status: Option<String>,
|
||||
pub purpose: Option<String>,
|
||||
pub limit: Option<u32>,
|
||||
pub offset: Option<u32>,
|
||||
pub sort_column: Option<String>,
|
||||
@@ -750,7 +749,6 @@ pub struct AdminExternalApiKeyPayload {
|
||||
pub owner_user_id: String,
|
||||
pub name: String,
|
||||
pub key_prefix: String,
|
||||
pub purpose: String,
|
||||
pub scopes: Vec<String>,
|
||||
pub created_at: String,
|
||||
pub last_used_at: Option<String>,
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
use super::*;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ExternalApiKeyRecord {
|
||||
pub key_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub name: String,
|
||||
pub key_prefix: String,
|
||||
pub scopes: Vec<String>,
|
||||
pub purpose: String,
|
||||
pub created_at: String,
|
||||
pub last_used_at: Option<String>,
|
||||
pub revoked_at: Option<String>,
|
||||
pub updated_at: String,
|
||||
pub secret_ciphertext: Option<String>,
|
||||
pub provider_account_id: Option<String>,
|
||||
pub provider_base_url: Option<String>,
|
||||
pub provider_model: Option<String>,
|
||||
pub provider_account_json: Option<String>,
|
||||
pub credential_version: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -30,39 +22,6 @@ pub struct ExternalApiKeyCreateRecordInput {
|
||||
pub key_hash: String,
|
||||
pub scopes: Vec<String>,
|
||||
pub now_micros: i64,
|
||||
pub purpose: String,
|
||||
pub secret_ciphertext: Option<String>,
|
||||
pub provider_account_id: Option<String>,
|
||||
pub provider_base_url: Option<String>,
|
||||
pub provider_model: Option<String>,
|
||||
pub provider_account_json: Option<String>,
|
||||
pub credential_version: u32,
|
||||
}
|
||||
|
||||
impl fmt::Debug for ExternalApiKeyRecord {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("ExternalApiKeyRecord")
|
||||
.field("key_id", &self.key_id)
|
||||
.field("owner_user_id", &self.owner_user_id)
|
||||
.field("name", &self.name)
|
||||
.field("key_prefix", &self.key_prefix)
|
||||
.field("scopes", &self.scopes)
|
||||
.field("purpose", &self.purpose)
|
||||
.field(
|
||||
"secret_ciphertext",
|
||||
&self.secret_ciphertext.as_ref().map(|_| "<redacted>"),
|
||||
)
|
||||
.field("provider_account_id", &self.provider_account_id)
|
||||
.field("provider_base_url", &self.provider_base_url)
|
||||
.field("provider_model", &self.provider_model)
|
||||
.field("provider_account_json", &self.provider_account_json)
|
||||
.field("credential_version", &self.credential_version)
|
||||
.field("created_at", &self.created_at)
|
||||
.field("last_used_at", &self.last_used_at)
|
||||
.field("revoked_at", &self.revoked_at)
|
||||
.field("updated_at", &self.updated_at)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -88,13 +47,6 @@ impl From<ExternalApiKeyCreateRecordInput> for crate::module_bindings::ExternalA
|
||||
key_hash: input.key_hash,
|
||||
scopes_json: serde_json::to_string(&input.scopes).unwrap_or_else(|_| "[]".to_string()),
|
||||
now_micros: input.now_micros,
|
||||
purpose: input.purpose,
|
||||
secret_ciphertext: input.secret_ciphertext,
|
||||
provider_account_id: input.provider_account_id,
|
||||
provider_base_url: input.provider_base_url,
|
||||
provider_model: input.provider_model,
|
||||
provider_account_json: input.provider_account_json,
|
||||
credential_version: input.credential_version,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,16 +110,9 @@ fn map_external_api_key_snapshot(
|
||||
key_prefix: snapshot.key_prefix,
|
||||
scopes: serde_json::from_str::<Vec<String>>(snapshot.scopes_json.as_str())
|
||||
.map_err(SpacetimeClientError::validation_failed)?,
|
||||
purpose: snapshot.purpose,
|
||||
created_at: format_timestamp_micros(snapshot.created_at_micros),
|
||||
last_used_at: snapshot.last_used_at_micros.map(format_timestamp_micros),
|
||||
revoked_at: snapshot.revoked_at_micros.map(format_timestamp_micros),
|
||||
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
|
||||
secret_ciphertext: snapshot.secret_ciphertext,
|
||||
provider_account_id: snapshot.provider_account_id,
|
||||
provider_base_url: snapshot.provider_base_url,
|
||||
provider_model: snapshot.provider_model,
|
||||
provider_account_json: snapshot.provider_account_json,
|
||||
credential_version: snapshot.credential_version,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -67,31 +67,6 @@ impl SpacetimeClient {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn revoke_llm_router_api_key(
|
||||
&self,
|
||||
input: ExternalApiKeyRevokeRecordInput,
|
||||
) -> Result<ExternalApiKeyRecord, SpacetimeClientError> {
|
||||
let procedure_input = input.into();
|
||||
|
||||
self.call_after_connect(
|
||||
"revoke_llm_router_api_key_and_return",
|
||||
move |connection, sender| {
|
||||
connection
|
||||
.procedures()
|
||||
.revoke_llm_router_api_key_and_return_then(
|
||||
procedure_input,
|
||||
move |_, result| {
|
||||
let mapped = result
|
||||
.map_err(SpacetimeClientError::from_sdk_error)
|
||||
.and_then(map_external_api_key_single_procedure_result);
|
||||
send_once(&sender, mapped);
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn authenticate_external_api_key(
|
||||
&self,
|
||||
input: ExternalApiKeyAuthenticateRecordInput,
|
||||
|
||||
@@ -682,7 +682,6 @@ pub mod resolve_editor_reference_and_return_procedure;
|
||||
pub mod resolve_profile_recharge_refund_manual_review_and_return_procedure;
|
||||
pub mod revoke_database_migration_operator_procedure;
|
||||
pub mod revoke_external_api_key_and_return_procedure;
|
||||
pub mod revoke_llm_router_api_key_and_return_procedure;
|
||||
pub mod rollback_editor_canvas_layout_and_return_procedure;
|
||||
pub mod rotate_editor_generation_runtime_service_identity_and_return_procedure;
|
||||
pub mod rpg_agent_draft_card_kind_type;
|
||||
@@ -1583,7 +1582,6 @@ pub use resolve_editor_reference_and_return_procedure::resolve_editor_reference_
|
||||
pub use resolve_profile_recharge_refund_manual_review_and_return_procedure::resolve_profile_recharge_refund_manual_review_and_return;
|
||||
pub use revoke_database_migration_operator_procedure::revoke_database_migration_operator;
|
||||
pub use revoke_external_api_key_and_return_procedure::revoke_external_api_key_and_return;
|
||||
pub use revoke_llm_router_api_key_and_return_procedure::revoke_llm_router_api_key_and_return;
|
||||
pub use rollback_editor_canvas_layout_and_return_procedure::rollback_editor_canvas_layout_and_return;
|
||||
pub use rotate_editor_generation_runtime_service_identity_and_return_procedure::rotate_editor_generation_runtime_service_identity_and_return;
|
||||
pub use rpg_agent_draft_card_kind_type::RpgAgentDraftCardKind;
|
||||
|
||||
-7
@@ -14,13 +14,6 @@ pub struct ExternalApiKeyCreateInput {
|
||||
pub key_hash: String,
|
||||
pub scopes_json: String,
|
||||
pub now_micros: i64,
|
||||
pub purpose: String,
|
||||
pub secret_ciphertext: Option<String>,
|
||||
pub provider_account_id: Option<String>,
|
||||
pub provider_base_url: Option<String>,
|
||||
pub provider_model: Option<String>,
|
||||
pub provider_account_json: Option<String>,
|
||||
pub credential_version: u32,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for ExternalApiKeyCreateInput {
|
||||
|
||||
-7
@@ -12,17 +12,10 @@ pub struct ExternalApiKeySnapshot {
|
||||
pub name: String,
|
||||
pub key_prefix: String,
|
||||
pub scopes_json: String,
|
||||
pub purpose: String,
|
||||
pub created_at_micros: i64,
|
||||
pub last_used_at_micros: Option<i64>,
|
||||
pub revoked_at_micros: Option<i64>,
|
||||
pub updated_at_micros: i64,
|
||||
pub secret_ciphertext: Option<String>,
|
||||
pub provider_account_id: Option<String>,
|
||||
pub provider_base_url: Option<String>,
|
||||
pub provider_model: Option<String>,
|
||||
pub provider_account_json: Option<String>,
|
||||
pub credential_version: u32,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for ExternalApiKeySnapshot {
|
||||
|
||||
@@ -17,13 +17,6 @@ pub struct ExternalApiKey {
|
||||
pub last_used_at: Option<__sdk::Timestamp>,
|
||||
pub revoked_at: Option<__sdk::Timestamp>,
|
||||
pub updated_at: __sdk::Timestamp,
|
||||
pub purpose: String,
|
||||
pub secret_ciphertext: Option<String>,
|
||||
pub provider_account_id: Option<String>,
|
||||
pub provider_base_url: Option<String>,
|
||||
pub provider_model: Option<String>,
|
||||
pub provider_account_json: Option<String>,
|
||||
pub credential_version: u32,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for ExternalApiKey {
|
||||
@@ -44,13 +37,6 @@ pub struct ExternalApiKeyCols {
|
||||
pub last_used_at: __sdk::__query_builder::Col<ExternalApiKey, Option<__sdk::Timestamp>>,
|
||||
pub revoked_at: __sdk::__query_builder::Col<ExternalApiKey, Option<__sdk::Timestamp>>,
|
||||
pub updated_at: __sdk::__query_builder::Col<ExternalApiKey, __sdk::Timestamp>,
|
||||
pub purpose: __sdk::__query_builder::Col<ExternalApiKey, String>,
|
||||
pub secret_ciphertext: __sdk::__query_builder::Col<ExternalApiKey, Option<String>>,
|
||||
pub provider_account_id: __sdk::__query_builder::Col<ExternalApiKey, Option<String>>,
|
||||
pub provider_base_url: __sdk::__query_builder::Col<ExternalApiKey, Option<String>>,
|
||||
pub provider_model: __sdk::__query_builder::Col<ExternalApiKey, Option<String>>,
|
||||
pub provider_account_json: __sdk::__query_builder::Col<ExternalApiKey, Option<String>>,
|
||||
pub credential_version: __sdk::__query_builder::Col<ExternalApiKey, u32>,
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::HasCols for ExternalApiKey {
|
||||
@@ -67,19 +53,6 @@ impl __sdk::__query_builder::HasCols for ExternalApiKey {
|
||||
last_used_at: __sdk::__query_builder::Col::new(table_name, "last_used_at"),
|
||||
revoked_at: __sdk::__query_builder::Col::new(table_name, "revoked_at"),
|
||||
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
|
||||
purpose: __sdk::__query_builder::Col::new(table_name, "purpose"),
|
||||
secret_ciphertext: __sdk::__query_builder::Col::new(table_name, "secret_ciphertext"),
|
||||
provider_account_id: __sdk::__query_builder::Col::new(
|
||||
table_name,
|
||||
"provider_account_id",
|
||||
),
|
||||
provider_base_url: __sdk::__query_builder::Col::new(table_name, "provider_base_url"),
|
||||
provider_model: __sdk::__query_builder::Col::new(table_name, "provider_model"),
|
||||
provider_account_json: __sdk::__query_builder::Col::new(
|
||||
table_name,
|
||||
"provider_account_json",
|
||||
),
|
||||
credential_version: __sdk::__query_builder::Col::new(table_name, "credential_version"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::external_api_key_procedure_result_type::ExternalApiKeyProcedureResult;
|
||||
use super::external_api_key_revoke_input_type::ExternalApiKeyRevokeInput;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
struct RevokeLlmRouterApiKeyAndReturnArgs {
|
||||
pub input: ExternalApiKeyRevokeInput,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for RevokeLlmRouterApiKeyAndReturnArgs {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the procedure `revoke_llm_router_api_key_and_return`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteProcedures`].
|
||||
pub trait revoke_llm_router_api_key_and_return {
|
||||
fn revoke_llm_router_api_key_and_return(&self, input: ExternalApiKeyRevokeInput) {
|
||||
self.revoke_llm_router_api_key_and_return_then(input, |_, _| {});
|
||||
}
|
||||
|
||||
fn revoke_llm_router_api_key_and_return_then(
|
||||
&self,
|
||||
input: ExternalApiKeyRevokeInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<ExternalApiKeyProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
);
|
||||
}
|
||||
|
||||
impl revoke_llm_router_api_key_and_return for super::RemoteProcedures {
|
||||
fn revoke_llm_router_api_key_and_return_then(
|
||||
&self,
|
||||
input: ExternalApiKeyRevokeInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<ExternalApiKeyProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
) {
|
||||
self.imp
|
||||
.invoke_procedure_with_callback::<_, ExternalApiKeyProcedureResult>(
|
||||
"revoke_llm_router_api_key_and_return",
|
||||
RevokeLlmRouterApiKeyAndReturnArgs { input },
|
||||
__callback,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,6 @@ use crate::*;
|
||||
|
||||
const EXTERNAL_API_KEY_MAX_NAME_CHARS: usize = 80;
|
||||
const EXTERNAL_API_KEY_DEFAULT_NAME: &str = "外部 API Key";
|
||||
pub const EXTERNAL_API_KEY_PURPOSE_EXTERNAL_EDITOR: &str = "external-editor";
|
||||
pub const EXTERNAL_API_KEY_PURPOSE_LLM_ROUTER: &str = "llm-router";
|
||||
const EXTERNAL_API_KEY_DEFAULT_SCOPES_JSON: &str =
|
||||
"[\"editor:project\",\"editor:canvas\",\"editor:image-generate\",\"editor:asset\"]";
|
||||
|
||||
@@ -24,24 +22,6 @@ pub struct ExternalApiKey {
|
||||
pub last_used_at: Option<Timestamp>,
|
||||
pub revoked_at: Option<Timestamp>,
|
||||
pub updated_at: Timestamp,
|
||||
#[default(EXTERNAL_API_KEY_PURPOSE_EXTERNAL_EDITOR)]
|
||||
pub purpose: String,
|
||||
/// LLM Router 凭据仅保存服务端可解密的密文;普通外部 Key 保持 None。
|
||||
#[default(None)]
|
||||
pub secret_ciphertext: Option<String>,
|
||||
/// Router 侧账号标识及固定路由元数据,仅用于 `llm-router` purpose。
|
||||
#[default(None)]
|
||||
pub provider_account_id: Option<String>,
|
||||
#[default(None)]
|
||||
pub provider_base_url: Option<String>,
|
||||
#[default(None)]
|
||||
pub provider_model: Option<String>,
|
||||
/// LLM Router 账号脱敏 JSON 元数据;普通外部 Key 保持 None。
|
||||
#[default(None)]
|
||||
pub provider_account_json: Option<String>,
|
||||
/// LLM Router 凭据契约版本,用于后续迁移;历史行默认为 1。
|
||||
#[default(1u32)]
|
||||
pub credential_version: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
@@ -53,13 +33,6 @@ pub struct ExternalApiKeyCreateInput {
|
||||
pub key_hash: String,
|
||||
pub scopes_json: String,
|
||||
pub now_micros: i64,
|
||||
pub purpose: String,
|
||||
pub secret_ciphertext: Option<String>,
|
||||
pub provider_account_id: Option<String>,
|
||||
pub provider_base_url: Option<String>,
|
||||
pub provider_model: Option<String>,
|
||||
pub provider_account_json: Option<String>,
|
||||
pub credential_version: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
@@ -87,17 +60,10 @@ pub struct ExternalApiKeySnapshot {
|
||||
pub name: String,
|
||||
pub key_prefix: String,
|
||||
pub scopes_json: String,
|
||||
pub purpose: String,
|
||||
pub created_at_micros: i64,
|
||||
pub last_used_at_micros: Option<i64>,
|
||||
pub revoked_at_micros: Option<i64>,
|
||||
pub updated_at_micros: i64,
|
||||
pub secret_ciphertext: Option<String>,
|
||||
pub provider_account_id: Option<String>,
|
||||
pub provider_base_url: Option<String>,
|
||||
pub provider_model: Option<String>,
|
||||
pub provider_account_json: Option<String>,
|
||||
pub credential_version: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
@@ -135,23 +101,7 @@ pub fn revoke_external_api_key_and_return(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: ExternalApiKeyRevokeInput,
|
||||
) -> ExternalApiKeyProcedureResult {
|
||||
match ctx.try_with_tx(|tx| revoke_external_api_key(tx, input.clone(), false)) {
|
||||
Ok(key) => external_api_key_single_ok(key),
|
||||
Err(message) => external_api_key_error(message),
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal server-side path for invalidating an LLM Router credential.
|
||||
///
|
||||
/// The public profile API must never be able to revoke LLM Router credentials, but
|
||||
/// api-server needs a separate trusted procedure to mark a Router key revoked
|
||||
/// after a deterministic 401/403 before issuing a replacement key.
|
||||
#[spacetimedb::procedure]
|
||||
pub fn revoke_llm_router_api_key_and_return(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: ExternalApiKeyRevokeInput,
|
||||
) -> ExternalApiKeyProcedureResult {
|
||||
match ctx.try_with_tx(|tx| revoke_external_api_key(tx, input.clone(), true)) {
|
||||
match ctx.try_with_tx(|tx| revoke_external_api_key(tx, input.clone())) {
|
||||
Ok(key) => external_api_key_single_ok(key),
|
||||
Err(message) => external_api_key_error(message),
|
||||
}
|
||||
@@ -201,13 +151,6 @@ fn create_external_api_key(
|
||||
last_used_at: None,
|
||||
revoked_at: None,
|
||||
updated_at: now,
|
||||
purpose: normalize_key_purpose(input.purpose.as_str()),
|
||||
secret_ciphertext: normalize_optional_secret(input.secret_ciphertext),
|
||||
provider_account_id: normalize_optional_metadata(input.provider_account_id),
|
||||
provider_base_url: normalize_optional_metadata(input.provider_base_url),
|
||||
provider_model: normalize_optional_metadata(input.provider_model),
|
||||
provider_account_json: normalize_optional_metadata(input.provider_account_json),
|
||||
credential_version: input.credential_version.max(1),
|
||||
});
|
||||
ctx.db
|
||||
.external_api_key()
|
||||
@@ -241,7 +184,6 @@ fn list_external_api_keys(
|
||||
fn revoke_external_api_key(
|
||||
ctx: &ReducerContext,
|
||||
input: ExternalApiKeyRevokeInput,
|
||||
allow_llm_router: bool,
|
||||
) -> Result<ExternalApiKeySnapshot, String> {
|
||||
let key_id = normalize_required(&input.key_id, "external_api_key.key_id")?;
|
||||
let owner_user_id = normalize_required(&input.owner_user_id, "external_api_key.owner_user_id")?;
|
||||
@@ -254,9 +196,6 @@ fn revoke_external_api_key(
|
||||
if row.owner_user_id != owner_user_id {
|
||||
return Err("无权访问该 API Key".to_string());
|
||||
}
|
||||
if is_llm_router_purpose(row.purpose.as_str()) && !allow_llm_router {
|
||||
return Err("LLM Router Key 由服务端管理,不能通过通用 API Key 接口撤销".to_string());
|
||||
}
|
||||
let now = Timestamp::from_micros_since_unix_epoch(input.revoked_at_micros);
|
||||
ctx.db.external_api_key().key_id().delete(&key_id);
|
||||
ctx.db.external_api_key().insert(ExternalApiKey {
|
||||
@@ -308,7 +247,6 @@ fn external_api_key_snapshot_from_row(row: ExternalApiKey) -> ExternalApiKeySnap
|
||||
name: row.name,
|
||||
key_prefix: row.key_prefix,
|
||||
scopes_json: row.scopes_json,
|
||||
purpose: row.purpose,
|
||||
created_at_micros: row.created_at.to_micros_since_unix_epoch(),
|
||||
last_used_at_micros: row
|
||||
.last_used_at
|
||||
@@ -317,12 +255,6 @@ fn external_api_key_snapshot_from_row(row: ExternalApiKey) -> ExternalApiKeySnap
|
||||
.revoked_at
|
||||
.map(|value| value.to_micros_since_unix_epoch()),
|
||||
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
||||
secret_ciphertext: row.secret_ciphertext,
|
||||
provider_account_id: row.provider_account_id,
|
||||
provider_base_url: row.provider_base_url,
|
||||
provider_model: row.provider_model,
|
||||
provider_account_json: row.provider_account_json,
|
||||
credential_version: row.credential_version,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,29 +284,6 @@ fn normalize_scopes_json(value: String) -> Result<String, String> {
|
||||
Ok(normalized.to_string())
|
||||
}
|
||||
|
||||
fn normalize_key_purpose(value: &str) -> String {
|
||||
match value.trim() {
|
||||
EXTERNAL_API_KEY_PURPOSE_LLM_ROUTER => EXTERNAL_API_KEY_PURPOSE_LLM_ROUTER.to_string(),
|
||||
_ => EXTERNAL_API_KEY_PURPOSE_EXTERNAL_EDITOR.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_llm_router_purpose(value: &str) -> bool {
|
||||
value.trim() == EXTERNAL_API_KEY_PURPOSE_LLM_ROUTER
|
||||
}
|
||||
|
||||
fn normalize_optional_secret(value: Option<String>) -> Option<String> {
|
||||
value
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
fn normalize_optional_metadata(value: Option<String>) -> Option<String> {
|
||||
value
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
fn external_api_key_single_ok(key: ExternalApiKeySnapshot) -> ExternalApiKeyProcedureResult {
|
||||
ExternalApiKeyProcedureResult {
|
||||
ok: true,
|
||||
|
||||
@@ -1418,32 +1418,6 @@ where
|
||||
|
||||
fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde_json::Value {
|
||||
let mut next_value = value.clone();
|
||||
if table_name == "external_api_key" {
|
||||
if let Some(object) = next_value.as_object_mut() {
|
||||
// 中文注释:API Key 用途字段晚于外部 API Key 表加入,旧迁移包按外部编辑器 Key 兼容。
|
||||
object
|
||||
.entry("purpose".to_string())
|
||||
.or_insert_with(|| serde_json::Value::from("external-editor"));
|
||||
object
|
||||
.entry("secret_ciphertext".to_string())
|
||||
.or_insert(serde_json::Value::Null);
|
||||
object
|
||||
.entry("provider_account_id".to_string())
|
||||
.or_insert(serde_json::Value::Null);
|
||||
object
|
||||
.entry("provider_base_url".to_string())
|
||||
.or_insert(serde_json::Value::Null);
|
||||
object
|
||||
.entry("provider_model".to_string())
|
||||
.or_insert(serde_json::Value::Null);
|
||||
object
|
||||
.entry("provider_account_json".to_string())
|
||||
.or_insert(serde_json::Value::Null);
|
||||
object
|
||||
.entry("credential_version".to_string())
|
||||
.or_insert_with(|| serde_json::Value::from(1));
|
||||
}
|
||||
}
|
||||
if table_name == "profile_wallet_config" {
|
||||
if let Some(object) = next_value.as_object_mut() {
|
||||
// 中文注释:旧迁移包没有每日免费额度字段,导入时保持原有每日 20 泥点语义。
|
||||
|
||||
Reference in New Issue
Block a user