feat: add provider usage query with JavaScript scripting support (#101)

* feat: add provider usage query functionality

- Updated `Cargo.toml` to include `regex` and `rquickjs` dependencies for usage script execution.
- Implemented `query_provider_usage` command in `commands.rs` to handle usage queries.
- Created `UsageScript` and `UsageData` structs in `provider.rs` for managing usage script configurations and results.
- Added `execute_usage_script` function in `usage_script.rs` to run user-defined scripts for querying usage.
- Enhanced `ProviderList` component to include a button for configuring usage scripts and a modal for editing scripts.
- Introduced `UsageFooter` component to display usage information and status.
- Added `UsageScriptModal` for editing and testing usage scripts with preset templates.
- Updated Tauri API to support querying provider usage.
- Modified types in `types.ts` to include structures for usage scripts and results.

* feat(usage): support multi-plan usage display for providers

- 【Feature】
  - Update `UsageResult` to support an array of `UsageData` for displaying multiple usage plans per provider.
  - Refactor `query_provider_usage` command to parse both single `UsageData` objects (for backward compatibility) and arrays of `UsageData`.
  - Enhance `usage_script` validation to accept either a single usage object or an array of usage objects.
- 【Frontend】
  - Redesign `UsageFooter` to iterate and display details for all available usage plans, introducing `UsagePlanItem` for individual plan rendering.
  - Improve usage display with color-coded remaining balance and clear plan information.
  - Update `UsageScriptModal` test notification to summarize all returned plans.
  - Remove redundant `isCurrent` prop from `UsageFooter` in `ProviderList`.
- 【Build】
  - Change frontend development server port from `3000` to `3005` in `tauri.conf.json` and `vite.config.mts`.

* feat(usage): enhance query flexibility and display
- 【`src/types.ts`, `src-tauri/src/provider.rs`】Make `UsageData` fields optional and introduce `extra` and `invalidMessage` for more flexible reporting.
  - `expiresAt` replaced by generic `extra` field.
  - `isValid`, `remaining`, `unit` are now optional.
  - Added `invalidMessage` to provide specific reasons for invalid status.
- 【`src-tauri/src/usage_script.rs`】Relax usage script result validation to accommodate optional fields in `UsageData`.
- 【`src/components/UsageFooter.tsx`】Update UI to display `extra` field and `invalidMessage`, and conditionally render `remaining` and `unit` based on availability.
- 【`src/components/UsageScriptModal.tsx`】
  - Add a new `NewAPI` preset template demonstrating advanced extractor logic for complex API responses.
  - Update script instructions to reflect optional fields and new variable syntax (`{{apiKey}}`).
  - Remove old "DeepSeek" and "OpenAI" templates.
  - Remove basic syntax check for `return` statement.
- 【`.vscode/settings.json`】Add `dish-ai-commit.base.language` setting.
- 【`src-tauri/src/commands.rs`】Adjust usage logging to handle optional `remaining` and `unit` fields.

* chore(config): remove VS Code settings from version control

- delete .vscode/settings.json to remove editor-specific configurations
- add /.vscode to .gitignore to prevent tracking of local VS Code settings
- ensure personalized editor preferences are not committed to the repository

* fix(provider): preserve usage script during provider update

- When updating a provider, the `usage_script` configuration within `ProviderMeta` was not explicitly merged.
- This could lead to the accidental loss of `usage_script` settings if the incoming `provider` object in the update request did not contain this field.
- Ensure `usage_script` is cloned from the existing provider's meta when merging `ProviderMeta` during an update.

* refactor(provider): enforce base_url for usage scripts and update dev ports
- 【Backend】
    - `src-tauri/src/commands.rs`: Made `ANTHROPIC_BASE_URL` a required field for Claude providers and `base_url` a required field in `config.toml` for Codex providers when extracting credentials for usage script execution. This improves error handling by explicitly failing if these critical URLs are missing or malformed.
- 【Frontend】
    - `src/App.tsx`, `src/components/ProviderList.tsx`: Passed `appType` prop to `ProviderList` component to ensure `updateProvider` calls within `handleSaveUsageScript` correctly identify the application type.
- 【Config】
    - `src-tauri/tauri.conf.json`, `vite.config.mts`: Updated development server ports from `3005` to `3000` to standardize local development environment.

* refactor(usage): improve usage data fetching logic

- Prevent redundant API calls by tracking last fetched parameters in `useEffect`.
- Avoid concurrent API requests by adding a guard in `fetchUsage`.
- Clear usage data and last fetch parameters when usage query is disabled.
- Add `queryProviderUsage` API declaration to `window.api` interface.

* fix(usage-script): ensure usage script updates and improve reactivity

- correctly update `usage_script` from new provider meta during updates
- replace full page reload with targeted provider data refresh after saving usage script settings
- trigger usage data fetch or clear when `usageEnabled` status changes in `UsageFooter`
- reduce logging verbosity for usage script execution in backend commands and script execution

* style(usage-footer): adjust usage plan item layout

- Decrease width of extra field column from 35% to 30%
- Increase width of usage information column from 40% to 45%
- Improve visual balance and readability of usage plan items
This commit is contained in:
Sirhexs
2025-10-15 09:15:25 +08:00
committed by GitHub
parent 59644b29e6
commit 3e4df2c96a
18 changed files with 1179 additions and 10 deletions

View File

@@ -246,6 +246,7 @@ pub async fn update_provider(
}
updated.meta = Some(crate::provider::ProviderMeta {
custom_endpoints: merged_map,
usage_script: new_meta.usage_script.clone(),
});
}
// 旧 meta 不存在:使用入参(可能为 None
@@ -798,6 +799,172 @@ pub async fn validate_mcp_command(cmd: String) -> Result<bool, String> {
claude_mcp::validate_command_in_path(&cmd)
}
// =====================
// 用量查询命令
// =====================
/// 查询供应商用量
#[tauri::command]
pub async fn query_provider_usage(
state: State<'_, AppState>,
provider_id: Option<String>,
providerId: Option<String>,
app_type: Option<AppType>,
app: Option<String>,
appType: Option<String>,
) -> Result<crate::provider::UsageResult, String> {
use crate::provider::{UsageData, UsageResult};
// 解析参数
let provider_id = provider_id
.or(providerId)
.ok_or("缺少 providerId 参数")?;
let app_type = app_type
.or_else(|| app.as_deref().map(|s| s.into()))
.or_else(|| appType.as_deref().map(|s| s.into()))
.unwrap_or(AppType::Claude);
// 1. 获取供应商配置并克隆所需数据
let (api_key, base_url, usage_script_code, timeout) = {
let config = state
.config
.lock()
.map_err(|e| format!("获取锁失败: {}", e))?;
let manager = config
.get_manager(&app_type)
.ok_or("应用类型不存在")?;
let provider = manager
.providers
.get(&provider_id)
.ok_or("供应商不存在")?;
// 2. 检查脚本配置
let usage_script = provider
.meta
.as_ref()
.and_then(|m| m.usage_script.as_ref())
.ok_or("未配置用量查询脚本")?;
if !usage_script.enabled {
return Err("用量查询未启用".to_string());
}
// 3. 提取凭证和脚本配置
let (api_key, base_url) = extract_credentials(provider, &app_type)?;
let timeout = usage_script.timeout.unwrap_or(10);
let code = usage_script.code.clone();
// 显式释放锁
drop(config);
(api_key, base_url, code, timeout)
};
// 5. 执行脚本
let result = crate::usage_script::execute_usage_script(
&usage_script_code,
&api_key,
&base_url,
timeout,
)
.await;
// 6. 构建结果(支持单对象或数组)
match result {
Ok(data) => {
// 尝试解析为数组
let usage_list: Vec<UsageData> = if data.is_array() {
// 直接解析为数组
serde_json::from_value(data)
.map_err(|e| format!("数据格式错误: {}", e))?
} else {
// 单对象包装为数组(向后兼容)
let single: UsageData = serde_json::from_value(data)
.map_err(|e| format!("数据格式错误: {}", e))?;
vec![single]
};
Ok(UsageResult {
success: true,
data: Some(usage_list),
error: None,
})
}
Err(e) => {
Ok(UsageResult {
success: false,
data: None,
error: Some(e),
})
}
}
}
/// 从供应商配置中提取 API Key 和 Base URL
fn extract_credentials(
provider: &crate::provider::Provider,
app_type: &AppType,
) -> Result<(String, String), String> {
match app_type {
AppType::Claude => {
let env = provider
.settings_config
.get("env")
.and_then(|v| v.as_object())
.ok_or("配置格式错误: 缺少 env")?;
let api_key = env
.get("ANTHROPIC_AUTH_TOKEN")
.and_then(|v| v.as_str())
.ok_or("缺少 API Key")?
.to_string();
let base_url = env
.get("ANTHROPIC_BASE_URL")
.and_then(|v| v.as_str())
.ok_or("缺少 ANTHROPIC_BASE_URL 配置")?
.to_string();
Ok((api_key, base_url))
}
AppType::Codex => {
let auth = provider
.settings_config
.get("auth")
.and_then(|v| v.as_object())
.ok_or("配置格式错误: 缺少 auth")?;
let api_key = auth
.get("OPENAI_API_KEY")
.and_then(|v| v.as_str())
.ok_or("缺少 API Key")?
.to_string();
// 从 config TOML 中提取 base_url
let config_toml = provider
.settings_config
.get("config")
.and_then(|v| v.as_str())
.unwrap_or("");
let base_url = if config_toml.contains("base_url") {
let re = regex::Regex::new(r#"base_url\s*=\s*["']([^"']+)["']"#).unwrap();
re.captures(config_toml)
.and_then(|caps| caps.get(1))
.map(|m| m.as_str().to_string())
.ok_or("config.toml 中 base_url 格式错误")?
} else {
return Err("config.toml 中缺少 base_url 配置".to_string());
};
Ok((api_key, base_url))
}
}
}
// =====================
// 新:集中以 config.json 为 SSOT 的 MCP 配置命令
// =====================