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(non_snake_case)]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn testUsageScript(
|
pub async fn testUsageScript(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
@@ -130,6 +131,8 @@ pub async fn testUsageScript(
|
|||||||
app: String,
|
app: String,
|
||||||
#[allow(non_snake_case)] scriptCode: String,
|
#[allow(non_snake_case)] scriptCode: String,
|
||||||
timeout: Option<u64>,
|
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)] accessToken: Option<String>,
|
||||||
#[allow(non_snake_case)] userId: Option<String>,
|
#[allow(non_snake_case)] userId: Option<String>,
|
||||||
) -> Result<crate::provider::UsageResult, String> {
|
) -> Result<crate::provider::UsageResult, String> {
|
||||||
@@ -140,6 +143,8 @@ pub async fn testUsageScript(
|
|||||||
&providerId,
|
&providerId,
|
||||||
&scriptCode,
|
&scriptCode,
|
||||||
timeout.unwrap_or(10),
|
timeout.unwrap_or(10),
|
||||||
|
apiKey.as_deref(),
|
||||||
|
baseUrl.as_deref(),
|
||||||
accessToken.as_deref(),
|
accessToken.as_deref(),
|
||||||
userId.as_deref(),
|
userId.as_deref(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -63,11 +63,19 @@ pub struct UsageScript {
|
|||||||
pub code: String,
|
pub code: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub timeout: Option<u64>,
|
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(skip_serializing_if = "Option::is_none")]
|
||||||
#[serde(rename = "accessToken")]
|
#[serde(rename = "accessToken")]
|
||||||
pub access_token: Option<String>,
|
pub access_token: Option<String>,
|
||||||
/// 用户ID(用于需要用户标识的接口)
|
/// 用户ID(用于需要用户标识的接口,NewAPI 模板使用)
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
#[serde(rename = "userId")]
|
#[serde(rename = "userId")]
|
||||||
pub user_id: Option<String>,
|
pub user_id: Option<String>,
|
||||||
|
|||||||
@@ -802,7 +802,7 @@ impl ProviderService {
|
|||||||
app_type: AppType,
|
app_type: AppType,
|
||||||
provider_id: &str,
|
provider_id: &str,
|
||||||
) -> Result<UsageResult, AppError> {
|
) -> 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 config = state.config.read().map_err(AppError::from)?;
|
||||||
let manager = config
|
let manager = config
|
||||||
.get_manager(&app_type)
|
.get_manager(&app_type)
|
||||||
@@ -814,38 +814,37 @@ impl ProviderService {
|
|||||||
format!("Provider not found: {}", provider_id),
|
format!("Provider not found: {}", provider_id),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let (script_code, timeout, access_token, user_id) = {
|
|
||||||
let usage_script = provider
|
|
||||||
.meta
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|m| m.usage_script.as_ref())
|
|
||||||
.ok_or_else(|| {
|
|
||||||
AppError::localized(
|
|
||||||
"provider.usage.script.missing",
|
|
||||||
"未配置用量查询脚本",
|
|
||||||
"Usage script is not configured",
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
if !usage_script.enabled {
|
|
||||||
return Err(AppError::localized(
|
|
||||||
"provider.usage.disabled",
|
|
||||||
"用量查询未启用",
|
|
||||||
"Usage query is disabled",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
(
|
|
||||||
usage_script.code.clone(),
|
|
||||||
usage_script.timeout.unwrap_or(10),
|
|
||||||
usage_script.access_token.clone(),
|
|
||||||
usage_script.user_id.clone(),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
(provider, script_code, timeout, access_token, user_id)
|
let usage_script = provider
|
||||||
|
.meta
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|m| m.usage_script.as_ref())
|
||||||
|
.ok_or_else(|| {
|
||||||
|
AppError::localized(
|
||||||
|
"provider.usage.script.missing",
|
||||||
|
"未配置用量查询脚本",
|
||||||
|
"Usage script is not configured",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if !usage_script.enabled {
|
||||||
|
return Err(AppError::localized(
|
||||||
|
"provider.usage.disabled",
|
||||||
|
"用量查询未启用",
|
||||||
|
"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(),
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let (api_key, base_url) = Self::extract_credentials(&provider, &app_type)?;
|
|
||||||
|
|
||||||
Self::execute_and_format_usage_result(
|
Self::execute_and_format_usage_result(
|
||||||
&script_code,
|
&script_code,
|
||||||
&api_key,
|
&api_key,
|
||||||
@@ -858,36 +857,23 @@ impl ProviderService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 测试用量脚本(使用临时脚本内容,不保存)
|
/// 测试用量脚本(使用临时脚本内容,不保存)
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn test_usage_script(
|
pub async fn test_usage_script(
|
||||||
state: &AppState,
|
_state: &AppState,
|
||||||
app_type: AppType,
|
_app_type: AppType,
|
||||||
provider_id: &str,
|
_provider_id: &str,
|
||||||
script_code: &str,
|
script_code: &str,
|
||||||
timeout: u64,
|
timeout: u64,
|
||||||
|
api_key: Option<&str>,
|
||||||
|
base_url: Option<&str>,
|
||||||
access_token: Option<&str>,
|
access_token: Option<&str>,
|
||||||
user_id: Option<&str>,
|
user_id: Option<&str>,
|
||||||
) -> Result<UsageResult, AppError> {
|
) -> 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(
|
Self::execute_and_format_usage_result(
|
||||||
script_code,
|
script_code,
|
||||||
&api_key,
|
api_key.unwrap_or(""),
|
||||||
&base_url,
|
base_url.unwrap_or(""),
|
||||||
timeout,
|
timeout,
|
||||||
access_token,
|
access_token,
|
||||||
user_id,
|
user_id,
|
||||||
@@ -1137,6 +1123,7 @@ impl ProviderService {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
fn extract_credentials(
|
fn extract_credentials(
|
||||||
provider: &Provider,
|
provider: &Provider,
|
||||||
app_type: &AppType,
|
app_type: &AppType,
|
||||||
|
|||||||
@@ -166,6 +166,8 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
|||||||
appId,
|
appId,
|
||||||
script.code,
|
script.code,
|
||||||
script.timeout,
|
script.timeout,
|
||||||
|
script.apiKey,
|
||||||
|
script.baseUrl,
|
||||||
script.accessToken,
|
script.accessToken,
|
||||||
script.userId,
|
script.userId,
|
||||||
);
|
);
|
||||||
@@ -225,23 +227,40 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
|||||||
const handleUsePreset = (presetName: string) => {
|
const handleUsePreset = (presetName: string) => {
|
||||||
const preset = PRESET_TEMPLATES[presetName];
|
const preset = PRESET_TEMPLATES[presetName];
|
||||||
if (preset) {
|
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({
|
setScript({
|
||||||
...script,
|
...script,
|
||||||
code: preset,
|
code: preset,
|
||||||
accessToken: undefined,
|
accessToken: undefined,
|
||||||
userId: undefined,
|
userId: undefined,
|
||||||
});
|
});
|
||||||
} else {
|
} else if (presetName === TEMPLATE_KEYS.NEW_API) {
|
||||||
setScript({ ...script, code: preset });
|
// NewAPI:清空 apiKey(NewAPI 不使用通用的 apiKey)
|
||||||
|
setScript({
|
||||||
|
...script,
|
||||||
|
code: preset,
|
||||||
|
apiKey: undefined,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
setSelectedTemplate(presetName); // 记录选择的模板
|
setSelectedTemplate(presetName); // 记录选择的模板
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 判断是否应该显示高级配置(仅 NewAPI 模板需要)
|
// 判断是否应该显示凭证配置区域
|
||||||
const shouldShowAdvancedConfig = selectedTemplate === TEMPLATE_KEYS.NEW_API;
|
const shouldShowCredentialsConfig =
|
||||||
|
selectedTemplate === TEMPLATE_KEYS.GENERAL || selectedTemplate === TEMPLATE_KEYS.NEW_API;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||||
@@ -296,38 +315,97 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 高级配置:Access Token 和 User ID(仅 NewAPI 模板显示) */}
|
{/* 凭证配置区域:通用和 NewAPI 模板显示 */}
|
||||||
{shouldShowAdvancedConfig && (
|
{shouldShowCredentialsConfig && (
|
||||||
<div className="space-y-3 p-4 bg-gray-50 dark:bg-gray-800/50 rounded-lg">
|
<div className="space-y-3 p-4 bg-gray-50 dark:bg-gray-800/50 rounded-lg">
|
||||||
<label className="block">
|
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
<span className="text-xs text-gray-600 dark:text-gray-400">
|
{t("usageScript.credentialsConfig")}
|
||||||
{t("usageScript.accessToken")}
|
</h4>
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={script.accessToken || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
setScript({ ...script, accessToken: e.target.value })
|
|
||||||
}
|
|
||||||
placeholder={t("usageScript.accessTokenPlaceholder")}
|
|
||||||
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">
|
{/* 通用模板:显示 apiKey + baseUrl */}
|
||||||
<span className="text-xs text-gray-600 dark:text-gray-400">
|
{selectedTemplate === TEMPLATE_KEYS.GENERAL && (
|
||||||
{t("usageScript.userId")}
|
<>
|
||||||
</span>
|
<label className="block">
|
||||||
<input
|
<span className="text-xs text-gray-600 dark:text-gray-400">
|
||||||
type="text"
|
API Key
|
||||||
value={script.userId || ""}
|
</span>
|
||||||
onChange={(e) =>
|
<input
|
||||||
setScript({ ...script, userId: e.target.value })
|
type="password"
|
||||||
}
|
value={script.apiKey || ""}
|
||||||
placeholder={t("usageScript.userIdPlaceholder")}
|
onChange={(e) =>
|
||||||
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"
|
setScript({ ...script, apiKey: e.target.value })
|
||||||
/>
|
}
|
||||||
</label>
|
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")}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={script.accessToken || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setScript({ ...script, accessToken: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder={t("usageScript.accessTokenPlaceholder")}
|
||||||
|
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.userId")}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={script.userId || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setScript({ ...script, userId: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder={t("usageScript.userIdPlaceholder")}
|
||||||
|
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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -356,6 +356,7 @@
|
|||||||
"templateCustom": "Custom",
|
"templateCustom": "Custom",
|
||||||
"templateGeneral": "General",
|
"templateGeneral": "General",
|
||||||
"templateNewAPI": "NewAPI",
|
"templateNewAPI": "NewAPI",
|
||||||
|
"credentialsConfig": "Credentials",
|
||||||
"accessToken": "Access Token",
|
"accessToken": "Access Token",
|
||||||
"accessTokenPlaceholder": "Generate in 'Security Settings'",
|
"accessTokenPlaceholder": "Generate in 'Security Settings'",
|
||||||
"userId": "User ID",
|
"userId": "User ID",
|
||||||
|
|||||||
@@ -356,6 +356,7 @@
|
|||||||
"templateCustom": "自定义",
|
"templateCustom": "自定义",
|
||||||
"templateGeneral": "通用模板",
|
"templateGeneral": "通用模板",
|
||||||
"templateNewAPI": "NewAPI",
|
"templateNewAPI": "NewAPI",
|
||||||
|
"credentialsConfig": "凭证配置",
|
||||||
"accessToken": "访问令牌",
|
"accessToken": "访问令牌",
|
||||||
"accessTokenPlaceholder": "在'安全设置'里生成",
|
"accessTokenPlaceholder": "在'安全设置'里生成",
|
||||||
"userId": "用户 ID",
|
"userId": "用户 ID",
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ export const usageApi = {
|
|||||||
appId: AppId,
|
appId: AppId,
|
||||||
scriptCode: string,
|
scriptCode: string,
|
||||||
timeout?: number,
|
timeout?: number,
|
||||||
|
apiKey?: string,
|
||||||
|
baseUrl?: string,
|
||||||
accessToken?: string,
|
accessToken?: string,
|
||||||
userId?: string,
|
userId?: string,
|
||||||
): Promise<UsageResult> {
|
): Promise<UsageResult> {
|
||||||
@@ -41,6 +43,8 @@ export const usageApi = {
|
|||||||
app: appId,
|
app: appId,
|
||||||
scriptCode: scriptCode,
|
scriptCode: scriptCode,
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
|
apiKey: apiKey,
|
||||||
|
baseUrl: baseUrl,
|
||||||
accessToken: accessToken,
|
accessToken: accessToken,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -45,8 +45,10 @@ export interface UsageScript {
|
|||||||
language: "javascript"; // 脚本语言
|
language: "javascript"; // 脚本语言
|
||||||
code: string; // 脚本代码(JSON 格式配置)
|
code: string; // 脚本代码(JSON 格式配置)
|
||||||
timeout?: number; // 超时时间(秒,默认 10)
|
timeout?: number; // 超时时间(秒,默认 10)
|
||||||
accessToken?: string; // 访问令牌(用于需要登录的接口)
|
apiKey?: string; // 用量查询专用的 API Key(通用模板使用)
|
||||||
userId?: string; // 用户ID(用于需要用户标识的接口)
|
baseUrl?: string; // 用量查询专用的 Base URL(通用和 NewAPI 模板使用)
|
||||||
|
accessToken?: string; // 访问令牌(NewAPI 模板使用)
|
||||||
|
userId?: string; // 用户ID(NewAPI 模板使用)
|
||||||
autoQueryInterval?: number; // 自动查询间隔(单位:分钟,0 表示禁用)
|
autoQueryInterval?: number; // 自动查询间隔(单位:分钟,0 表示禁用)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user