feat(usage-query): decouple credentials from provider config
Add independent credential fields for usage query to support different query endpoints and authentication methods. Changes: - Add `apiKey` and `baseUrl` fields to UsageScript struct - Remove dependency on provider config credentials in query_usage - Update test_usage_script to accept independent credential parameters - Add credential input fields in UsageScriptModal based on template: * General: apiKey + baseUrl * NewAPI: baseUrl + accessToken + userId * Custom: no additional fields (full freedom) - Auto-clear irrelevant fields when switching templates - Add i18n text for "credentialsConfig" Benefits: - Query API can use different endpoint/key than provider config - Better separation of concerns - More flexible for various usage query scenarios
This commit is contained in:
@@ -123,6 +123,7 @@ pub async fn queryProviderUsage(
|
||||
|
||||
/// 测试用量脚本(使用当前编辑器中的脚本,不保存)
|
||||
#[allow(non_snake_case)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[tauri::command]
|
||||
pub async fn testUsageScript(
|
||||
state: State<'_, AppState>,
|
||||
@@ -130,6 +131,8 @@ pub async fn testUsageScript(
|
||||
app: String,
|
||||
#[allow(non_snake_case)] scriptCode: String,
|
||||
timeout: Option<u64>,
|
||||
#[allow(non_snake_case)] apiKey: Option<String>,
|
||||
#[allow(non_snake_case)] baseUrl: Option<String>,
|
||||
#[allow(non_snake_case)] accessToken: Option<String>,
|
||||
#[allow(non_snake_case)] userId: Option<String>,
|
||||
) -> Result<crate::provider::UsageResult, String> {
|
||||
@@ -140,6 +143,8 @@ pub async fn testUsageScript(
|
||||
&providerId,
|
||||
&scriptCode,
|
||||
timeout.unwrap_or(10),
|
||||
apiKey.as_deref(),
|
||||
baseUrl.as_deref(),
|
||||
accessToken.as_deref(),
|
||||
userId.as_deref(),
|
||||
)
|
||||
|
||||
@@ -63,11 +63,19 @@ pub struct UsageScript {
|
||||
pub code: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub timeout: Option<u64>,
|
||||
/// 访问令牌(用于需要登录的接口)
|
||||
/// 用量查询专用的 API Key(通用模板使用)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(rename = "apiKey")]
|
||||
pub api_key: Option<String>,
|
||||
/// 用量查询专用的 Base URL(通用和 NewAPI 模板使用)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(rename = "baseUrl")]
|
||||
pub base_url: Option<String>,
|
||||
/// 访问令牌(用于需要登录的接口,NewAPI 模板使用)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(rename = "accessToken")]
|
||||
pub access_token: Option<String>,
|
||||
/// 用户ID(用于需要用户标识的接口)
|
||||
/// 用户ID(用于需要用户标识的接口,NewAPI 模板使用)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(rename = "userId")]
|
||||
pub user_id: Option<String>,
|
||||
|
||||
@@ -802,7 +802,7 @@ impl ProviderService {
|
||||
app_type: AppType,
|
||||
provider_id: &str,
|
||||
) -> Result<UsageResult, AppError> {
|
||||
let (provider, script_code, timeout, access_token, user_id) = {
|
||||
let (script_code, timeout, api_key, base_url, access_token, user_id) = {
|
||||
let config = state.config.read().map_err(AppError::from)?;
|
||||
let manager = config
|
||||
.get_manager(&app_type)
|
||||
@@ -814,7 +814,7 @@ impl ProviderService {
|
||||
format!("Provider not found: {}", provider_id),
|
||||
)
|
||||
})?;
|
||||
let (script_code, timeout, access_token, user_id) = {
|
||||
|
||||
let usage_script = provider
|
||||
.meta
|
||||
.as_ref()
|
||||
@@ -833,19 +833,18 @@ impl ProviderService {
|
||||
"Usage query is disabled",
|
||||
));
|
||||
}
|
||||
|
||||
// 直接从 UsageScript 中获取凭证,不再从供应商配置提取
|
||||
(
|
||||
usage_script.code.clone(),
|
||||
usage_script.timeout.unwrap_or(10),
|
||||
usage_script.api_key.clone().unwrap_or_default(),
|
||||
usage_script.base_url.clone().unwrap_or_default(),
|
||||
usage_script.access_token.clone(),
|
||||
usage_script.user_id.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
(provider, script_code, timeout, access_token, user_id)
|
||||
};
|
||||
|
||||
let (api_key, base_url) = Self::extract_credentials(&provider, &app_type)?;
|
||||
|
||||
Self::execute_and_format_usage_result(
|
||||
&script_code,
|
||||
&api_key,
|
||||
@@ -858,36 +857,23 @@ impl ProviderService {
|
||||
}
|
||||
|
||||
/// 测试用量脚本(使用临时脚本内容,不保存)
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn test_usage_script(
|
||||
state: &AppState,
|
||||
app_type: AppType,
|
||||
provider_id: &str,
|
||||
_state: &AppState,
|
||||
_app_type: AppType,
|
||||
_provider_id: &str,
|
||||
script_code: &str,
|
||||
timeout: u64,
|
||||
api_key: Option<&str>,
|
||||
base_url: Option<&str>,
|
||||
access_token: Option<&str>,
|
||||
user_id: Option<&str>,
|
||||
) -> Result<UsageResult, AppError> {
|
||||
// 获取 provider 的 API 凭证
|
||||
let provider = {
|
||||
let config = state.config.read().map_err(AppError::from)?;
|
||||
let manager = config
|
||||
.get_manager(&app_type)
|
||||
.ok_or_else(|| Self::app_not_found(&app_type))?;
|
||||
manager.providers.get(provider_id).cloned().ok_or_else(|| {
|
||||
AppError::localized(
|
||||
"provider.not_found",
|
||||
format!("供应商不存在: {}", provider_id),
|
||||
format!("Provider not found: {}", provider_id),
|
||||
)
|
||||
})?
|
||||
};
|
||||
|
||||
let (api_key, base_url) = Self::extract_credentials(&provider, &app_type)?;
|
||||
|
||||
// 直接使用传入的凭证参数进行测试
|
||||
Self::execute_and_format_usage_result(
|
||||
script_code,
|
||||
&api_key,
|
||||
&base_url,
|
||||
api_key.unwrap_or(""),
|
||||
base_url.unwrap_or(""),
|
||||
timeout,
|
||||
access_token,
|
||||
user_id,
|
||||
@@ -1137,6 +1123,7 @@ impl ProviderService {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn extract_credentials(
|
||||
provider: &Provider,
|
||||
app_type: &AppType,
|
||||
|
||||
@@ -166,6 +166,8 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
appId,
|
||||
script.code,
|
||||
script.timeout,
|
||||
script.apiKey,
|
||||
script.baseUrl,
|
||||
script.accessToken,
|
||||
script.userId,
|
||||
);
|
||||
@@ -225,23 +227,40 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
const handleUsePreset = (presetName: string) => {
|
||||
const preset = PRESET_TEMPLATES[presetName];
|
||||
if (preset) {
|
||||
// 如果选择的不是 NewAPI 模板,清空高级配置字段
|
||||
if (presetName !== TEMPLATE_KEYS.NEW_API) {
|
||||
// 根据模板类型清空不同的字段
|
||||
if (presetName === TEMPLATE_KEYS.CUSTOM) {
|
||||
// 自定义:清空所有凭证字段
|
||||
setScript({
|
||||
...script,
|
||||
code: preset,
|
||||
apiKey: undefined,
|
||||
baseUrl: undefined,
|
||||
accessToken: undefined,
|
||||
userId: undefined,
|
||||
});
|
||||
} else if (presetName === TEMPLATE_KEYS.GENERAL) {
|
||||
// 通用:保留 apiKey 和 baseUrl,清空 NewAPI 字段
|
||||
setScript({
|
||||
...script,
|
||||
code: preset,
|
||||
accessToken: undefined,
|
||||
userId: undefined,
|
||||
});
|
||||
} else {
|
||||
setScript({ ...script, code: preset });
|
||||
} else if (presetName === TEMPLATE_KEYS.NEW_API) {
|
||||
// NewAPI:清空 apiKey(NewAPI 不使用通用的 apiKey)
|
||||
setScript({
|
||||
...script,
|
||||
code: preset,
|
||||
apiKey: undefined,
|
||||
});
|
||||
}
|
||||
setSelectedTemplate(presetName); // 记录选择的模板
|
||||
}
|
||||
};
|
||||
|
||||
// 判断是否应该显示高级配置(仅 NewAPI 模板需要)
|
||||
const shouldShowAdvancedConfig = selectedTemplate === TEMPLATE_KEYS.NEW_API;
|
||||
// 判断是否应该显示凭证配置区域
|
||||
const shouldShowCredentialsConfig =
|
||||
selectedTemplate === TEMPLATE_KEYS.GENERAL || selectedTemplate === TEMPLATE_KEYS.NEW_API;
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||
@@ -296,9 +315,66 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 高级配置:Access Token 和 User ID(仅 NewAPI 模板显示) */}
|
||||
{shouldShowAdvancedConfig && (
|
||||
{/* 凭证配置区域:通用和 NewAPI 模板显示 */}
|
||||
{shouldShowCredentialsConfig && (
|
||||
<div className="space-y-3 p-4 bg-gray-50 dark:bg-gray-800/50 rounded-lg">
|
||||
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{t("usageScript.credentialsConfig")}
|
||||
</h4>
|
||||
|
||||
{/* 通用模板:显示 apiKey + baseUrl */}
|
||||
{selectedTemplate === TEMPLATE_KEYS.GENERAL && (
|
||||
<>
|
||||
<label className="block">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400">
|
||||
API Key
|
||||
</span>
|
||||
<input
|
||||
type="password"
|
||||
value={script.apiKey || ""}
|
||||
onChange={(e) =>
|
||||
setScript({ ...script, apiKey: e.target.value })
|
||||
}
|
||||
placeholder="sk-xxxxx"
|
||||
className="mt-1 w-full px-3 py-2 border border-border-default dark:border-border-default rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400">
|
||||
Base URL
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
value={script.baseUrl || ""}
|
||||
onChange={(e) =>
|
||||
setScript({ ...script, baseUrl: e.target.value })
|
||||
}
|
||||
placeholder="https://api.example.com"
|
||||
className="mt-1 w-full px-3 py-2 border border-border-default dark:border-border-default rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm"
|
||||
/>
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* NewAPI 模板:显示 baseUrl + accessToken + userId */}
|
||||
{selectedTemplate === TEMPLATE_KEYS.NEW_API && (
|
||||
<>
|
||||
<label className="block">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400">
|
||||
Base URL
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
value={script.baseUrl || ""}
|
||||
onChange={(e) =>
|
||||
setScript({ ...script, baseUrl: e.target.value })
|
||||
}
|
||||
placeholder="https://api.newapi.com"
|
||||
className="mt-1 w-full px-3 py-2 border border-border-default dark:border-border-default rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400">
|
||||
{t("usageScript.accessToken")}
|
||||
@@ -328,6 +404,8 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
className="mt-1 w-full px-3 py-2 border border-border-default dark:border-border-default rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm"
|
||||
/>
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -356,6 +356,7 @@
|
||||
"templateCustom": "Custom",
|
||||
"templateGeneral": "General",
|
||||
"templateNewAPI": "NewAPI",
|
||||
"credentialsConfig": "Credentials",
|
||||
"accessToken": "Access Token",
|
||||
"accessTokenPlaceholder": "Generate in 'Security Settings'",
|
||||
"userId": "User ID",
|
||||
|
||||
@@ -356,6 +356,7 @@
|
||||
"templateCustom": "自定义",
|
||||
"templateGeneral": "通用模板",
|
||||
"templateNewAPI": "NewAPI",
|
||||
"credentialsConfig": "凭证配置",
|
||||
"accessToken": "访问令牌",
|
||||
"accessTokenPlaceholder": "在'安全设置'里生成",
|
||||
"userId": "用户 ID",
|
||||
|
||||
@@ -32,6 +32,8 @@ export const usageApi = {
|
||||
appId: AppId,
|
||||
scriptCode: string,
|
||||
timeout?: number,
|
||||
apiKey?: string,
|
||||
baseUrl?: string,
|
||||
accessToken?: string,
|
||||
userId?: string,
|
||||
): Promise<UsageResult> {
|
||||
@@ -41,6 +43,8 @@ export const usageApi = {
|
||||
app: appId,
|
||||
scriptCode: scriptCode,
|
||||
timeout: timeout,
|
||||
apiKey: apiKey,
|
||||
baseUrl: baseUrl,
|
||||
accessToken: accessToken,
|
||||
userId: userId,
|
||||
});
|
||||
|
||||
@@ -45,8 +45,10 @@ export interface UsageScript {
|
||||
language: "javascript"; // 脚本语言
|
||||
code: string; // 脚本代码(JSON 格式配置)
|
||||
timeout?: number; // 超时时间(秒,默认 10)
|
||||
accessToken?: string; // 访问令牌(用于需要登录的接口)
|
||||
userId?: string; // 用户ID(用于需要用户标识的接口)
|
||||
apiKey?: string; // 用量查询专用的 API Key(通用模板使用)
|
||||
baseUrl?: string; // 用量查询专用的 Base URL(通用和 NewAPI 模板使用)
|
||||
accessToken?: string; // 访问令牌(NewAPI 模板使用)
|
||||
userId?: string; // 用户ID(NewAPI 模板使用)
|
||||
autoQueryInterval?: number; // 自动查询间隔(单位:分钟,0 表示禁用)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user