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:
@@ -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 配置命令
|
||||
// =====================
|
||||
|
||||
Reference in New Issue
Block a user