feat(deeplink): 深链支持用量查询配置 (#400)

## 新增功能
- 深链导入支持用量查询配置参数:
  - `usageEnabled`: 是否启用用量查询
  - `usageScript`: Base64 编码的用量查询脚本
  - `usageApiKey`: 用量查询专用 API Key
  - `usageBaseUrl`: 用量查询专用 Base URL
  - `usageAccessToken`: 访问令牌(NewAPI 模板)
  - `usageUserId`: 用户 ID(NewAPI 模板)
  - `usageAutoInterval`: 自动查询间隔(分钟)

## 修改文件
- **mod.rs**: DeepLinkImportRequest 结构体添加用量查询字段
- **parser.rs**: 解析 URL 中的用量查询参数
- **provider.rs**: 构建 ProviderMeta 包含 UsageScript 配置
- **deeplink.ts**: 添加 TypeScript 类型定义
- **DeepLinkImportDialog.tsx**: 确认对话框显示用量查询配置

## Bug 修复
- **formatters.ts**: 修复 formatJSON() 格式化时删除 "env" 键的问题

## 深链格式示例
```
ccswitch://v1/import?resource=provider&app=claude&name=xxx&usageEnabled=true&usageScript={base64}&usageAutoInterval=30
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
千羽
2025-12-14 22:52:13 +09:00
committed by GitHub
parent bfe9bb6a0c
commit c49cfa5ac5
6 changed files with 222 additions and 16 deletions

View File

@@ -113,4 +113,27 @@ pub struct DeepLinkImportRequest {
/// Remote config URL
#[serde(skip_serializing_if = "Option::is_none")]
pub config_url: Option<String>,
// ============ Usage script fields (v3.9+) ============
/// Whether to enable usage query (default: true if usage_script is provided)
#[serde(skip_serializing_if = "Option::is_none")]
pub usage_enabled: Option<bool>,
/// Base64 encoded usage query script code
#[serde(skip_serializing_if = "Option::is_none")]
pub usage_script: Option<String>,
/// Usage query API key (if different from provider API key)
#[serde(skip_serializing_if = "Option::is_none")]
pub usage_api_key: Option<String>,
/// Usage query base URL (if different from provider endpoint)
#[serde(skip_serializing_if = "Option::is_none")]
pub usage_base_url: Option<String>,
/// Usage query access token (for NewAPI template)
#[serde(skip_serializing_if = "Option::is_none")]
pub usage_access_token: Option<String>,
/// Usage query user ID (for NewAPI template)
#[serde(skip_serializing_if = "Option::is_none")]
pub usage_user_id: Option<String>,
/// Auto query interval in minutes (0 to disable)
#[serde(skip_serializing_if = "Option::is_none")]
pub usage_auto_interval: Option<u64>,
}

View File

@@ -122,6 +122,19 @@ fn parse_provider_deeplink(
let config_url = params.get("configUrl").cloned();
let enabled = params.get("enabled").and_then(|v| v.parse::<bool>().ok());
// Extract usage script fields (v3.9+)
let usage_enabled = params
.get("usageEnabled")
.and_then(|v| v.parse::<bool>().ok());
let usage_script = params.get("usageScript").cloned();
let usage_api_key = params.get("usageApiKey").cloned();
let usage_base_url = params.get("usageBaseUrl").cloned();
let usage_access_token = params.get("usageAccessToken").cloned();
let usage_user_id = params.get("usageUserId").cloned();
let usage_auto_interval = params
.get("usageAutoInterval")
.and_then(|v| v.parse::<u64>().ok());
Ok(DeepLinkImportRequest {
version,
resource,
@@ -146,6 +159,13 @@ fn parse_provider_deeplink(
config,
config_format,
config_url,
usage_enabled,
usage_script,
usage_api_key,
usage_base_url,
usage_access_token,
usage_user_id,
usage_auto_interval,
})
}
@@ -206,6 +226,13 @@ fn parse_prompt_deeplink(
config: None,
config_format: None,
config_url: None,
usage_enabled: None,
usage_script: None,
usage_api_key: None,
usage_base_url: None,
usage_access_token: None,
usage_user_id: None,
usage_auto_interval: None,
})
}
@@ -261,6 +288,13 @@ fn parse_mcp_deeplink(
directory: None,
branch: None,
config_url: None,
usage_enabled: None,
usage_script: None,
usage_api_key: None,
usage_base_url: None,
usage_access_token: None,
usage_user_id: None,
usage_auto_interval: None,
})
}
@@ -309,5 +343,12 @@ fn parse_skill_deeplink(
config: None,
config_format: None,
config_url: None,
usage_enabled: None,
usage_script: None,
usage_api_key: None,
usage_base_url: None,
usage_access_token: None,
usage_user_id: None,
usage_auto_interval: None,
})
}

View File

@@ -5,7 +5,7 @@
use super::utils::{decode_base64_param, infer_homepage_from_endpoint};
use super::DeepLinkImportRequest;
use crate::error::AppError;
use crate::provider::Provider;
use crate::provider::{Provider, ProviderMeta, UsageScript};
use crate::services::ProviderService;
use crate::store::AppState;
use crate::AppType;
@@ -117,6 +117,9 @@ pub(crate) fn build_provider_from_request(
AppType::Gemini => build_gemini_settings(request),
};
// Build usage script configuration if provided
let meta = build_provider_meta(request)?;
let provider = Provider {
id: String::new(), // Will be generated by caller
name: request.name.clone().unwrap_or_default(),
@@ -126,7 +129,7 @@ pub(crate) fn build_provider_from_request(
created_at: None,
sort_index: None,
notes: request.notes.clone(),
meta: None,
meta,
icon: request.icon.clone(),
icon_color: None,
is_proxy_target: None,
@@ -135,6 +138,57 @@ pub(crate) fn build_provider_from_request(
Ok(provider)
}
/// Build provider meta with usage script configuration
fn build_provider_meta(request: &DeepLinkImportRequest) -> Result<Option<ProviderMeta>, AppError> {
// Check if any usage script fields are provided
if request.usage_script.is_none()
&& request.usage_enabled.is_none()
&& request.usage_api_key.is_none()
&& request.usage_base_url.is_none()
&& request.usage_access_token.is_none()
&& request.usage_user_id.is_none()
&& request.usage_auto_interval.is_none()
{
return Ok(None);
}
// Decode usage script code if provided
let code = if let Some(script_b64) = &request.usage_script {
let decoded = decode_base64_param("usage_script", script_b64)?;
String::from_utf8(decoded)
.map_err(|e| AppError::InvalidInput(format!("Invalid UTF-8 in usage_script: {e}")))?
} else {
String::new()
};
// Determine enabled state: explicit param > has code > false
let enabled = request.usage_enabled.unwrap_or(!code.is_empty());
// Build UsageScript - use provider's API key and endpoint as defaults
let usage_script = UsageScript {
enabled,
language: "javascript".to_string(),
code,
timeout: Some(10),
api_key: request
.usage_api_key
.clone()
.or_else(|| request.api_key.clone()),
base_url: request
.usage_base_url
.clone()
.or_else(|| request.endpoint.clone()),
access_token: request.usage_access_token.clone(),
user_id: request.usage_user_id.clone(),
auto_query_interval: request.usage_auto_interval,
};
Ok(Some(ProviderMeta {
usage_script: Some(usage_script),
..Default::default()
}))
}
/// Build Claude settings configuration
fn build_claude_settings(request: &DeepLinkImportRequest) -> serde_json::Value {
let mut env = serde_json::Map::new();