diff --git a/AGENTS.md b/AGENTS.md index 97f5b6a..df9e7d8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -61,7 +61,7 @@ last-updated: 2025-11-23 - Linux 装 `libwebkit2gtk-4.1-dev`、`libjavascriptcoregtk-4.1-dev`、`patchelf` 等 Tauri v2 依赖;Windows 确保 WebView2 Runtime(先查注册表,winget 安装失败则回退微软官方静默安装包);Node 20.19.0,Rust stable(含 clippy / rustfmt),启用 npm 与 cargo 缓存。 - CI 未通过不得合并;缺少 dist 时会在 `npm run check` 内自动触发 `npm run build` 以满足 Clippy 输入。 -## 架构记忆(2025-11-21) +## 架构记忆(2025-11-29) - `src-tauri/src/main.rs` 仅保留应用启动与托盘事件注册,所有 Tauri Commands 拆分到 `src-tauri/src/commands/*`,服务实现位于 `services/*`,核心设施放在 `core/*`(HTTP、日志、错误)。 - **透明代理已重构为多工具架构**: @@ -82,6 +82,14 @@ last-updated: 2025-11-23 - 工具安装状态由 `services::tool::ToolStatusCache` 并行检测与缓存,`check_installations`/`refresh_tool_status` 命令复用该缓存;安装/更新成功后或手动刷新会清空命中的工具缓存。 - UI 相关的托盘/窗口操作集中在 `src-tauri/src/ui/*`,其它模块如需最小化到托盘请调用 `ui::hide_window_to_tray` 等封装方法。 - 新增 `TransparentProxyPage` 与会话数据库:`SESSION_MANAGER` 使用 SQLite 记录每个代理会话的 endpoint/API Key,前端可按工具启停代理、查看历史并启用「会话级 Endpoint 配置」开关。页面内的 `ProxyControlBar`、`ProxySettingsDialog`、`ProxyConfigDialog` 负责代理启停、配置切换、工具级设置并内建缺失配置提示。 +- **余额监控页面(BalancePage)**: + - 后端提供通用 `fetch_api` 命令(位于 `commands/api_commands.rs`),支持 GET/POST、自定义 headers、超时控制 + - 前端使用 JavaScript `Function` 构造器执行用户自定义的 extractor 脚本(位于 `utils/extractor.ts`) + - 配置存储在 localStorage,API Key 仅保存在内存(`useApiKeys` hook) + - 支持预设模板(NewAPI、OpenAI、自定义),模板定义在 `templates/index.ts` + - `useBalanceMonitor` hook 负责自动刷新逻辑,支持配置级别的刷新间隔 + - 配置表单(`ConfigFormDialog`)支持模板选择、代码编辑、静态 headers(JSON 格式) + - 卡片视图(`ConfigCard`)展示余额信息、使用比例、到期时间、错误提示 ### 透明代理扩展指南 diff --git a/CLAUDE.md b/CLAUDE.md index 448645e..ef76d2d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -61,7 +61,7 @@ last-updated: 2025-11-23 - Linux 装 `libwebkit2gtk-4.1-dev`、`libjavascriptcoregtk-4.1-dev`、`patchelf` 等 Tauri v2 依赖;Windows 确保 WebView2 Runtime(先查注册表,winget 安装失败则回退微软官方静默安装包);Node 20.19.0,Rust stable(含 clippy / rustfmt),启用 npm 与 cargo 缓存。 - CI 未通过不得合并;缺少 dist 时会在 `npm run check` 内自动触发 `npm run build` 以满足 Clippy 输入。 -## 架构记忆(2025-11-21) +## 架构记忆(2025-11-29) - `src-tauri/src/main.rs` 仅保留应用启动与托盘事件注册,所有 Tauri Commands 拆分到 `src-tauri/src/commands/*`,服务实现位于 `services/*`,核心设施放在 `core/*`(HTTP、日志、错误)。 - **透明代理已重构为多工具架构**: @@ -82,6 +82,14 @@ last-updated: 2025-11-23 - 工具安装状态由 `services::tool::ToolStatusCache` 并行检测与缓存,`check_installations`/`refresh_tool_status` 命令复用该缓存;安装/更新成功后或手动刷新会清空命中的工具缓存。 - UI 相关的托盘/窗口操作集中在 `src-tauri/src/ui/*`,其它模块如需最小化到托盘请调用 `ui::hide_window_to_tray` 等封装方法。 - 新增 `TransparentProxyPage` 与会话数据库:`SESSION_MANAGER` 使用 SQLite 记录每个代理会话的 endpoint/API Key,前端可按工具启停代理、查看历史并启用「会话级 Endpoint 配置」开关。页面内的 `ProxyControlBar`、`ProxySettingsDialog`、`ProxyConfigDialog` 负责代理启停、配置切换、工具级设置并内建缺失配置提示。 +- **余额监控页面(BalancePage)**: + - 后端提供通用 `fetch_api` 命令(位于 `commands/api_commands.rs`),支持 GET/POST、自定义 headers、超时控制 + - 前端使用 JavaScript `Function` 构造器执行用户自定义的 extractor 脚本(位于 `utils/extractor.ts`) + - 配置存储在 localStorage,API Key 仅保存在内存(`useApiKeys` hook) + - 支持预设模板(NewAPI、OpenAI、自定义),模板定义在 `templates/index.ts` + - `useBalanceMonitor` hook 负责自动刷新逻辑,支持配置级别的刷新间隔 + - 配置表单(`ConfigFormDialog`)支持模板选择、代码编辑、静态 headers(JSON 格式) + - 卡片视图(`ConfigCard`)展示余额信息、使用比例、到期时间、错误提示 ### 透明代理扩展指南 diff --git a/src-tauri/src/commands/balance_commands.rs b/src-tauri/src/commands/balance_commands.rs new file mode 100644 index 0000000..4490459 --- /dev/null +++ b/src-tauri/src/commands/balance_commands.rs @@ -0,0 +1,74 @@ +// 余额查询相关命令 +// +// 支持通过自定义 API 端点和提取器脚本查询余额信息 + +use ::duckcoding::http_client::build_client; +use ::duckcoding::utils::config::apply_proxy_if_configured; +use std::collections::HashMap; + +/// Tauri command: 通用 API 请求 +/// +/// # 参数 +/// - `endpoint`: API 端点 URL +/// - `method`: HTTP 方法 (GET 或 POST) +/// - `headers`: 请求头 (包含 Authorization 等) +/// - `timeout_ms`: 可选的请求超时时间(毫秒) +/// +/// # 返回 +/// 返回原始 JSON 响应,由前端执行 extractor 脚本提取余额信息 +#[tauri::command] +pub async fn fetch_api( + endpoint: String, + method: String, + headers: HashMap, + timeout_ms: Option, +) -> Result { + apply_proxy_if_configured(); + + // 验证 HTTP 方法 + let method_normalized = method.to_uppercase(); + if method_normalized != "GET" && method_normalized != "POST" { + return Err(format!("不支持的 HTTP 方法: {method},仅支持 GET 和 POST")); + } + + // 使用 build_client 确保代理配置等被应用 + let client = build_client().map_err(|e| format!("创建 HTTP 客户端失败: {e}"))?; + + // 构建请求 + let mut request_builder = if method_normalized == "GET" { + client.get(&endpoint) + } else { + client.post(&endpoint) + }; + + // 添加请求头 + for (key, value) in headers { + request_builder = request_builder.header(key, value); + } + + // 应用自定义超时 + if let Some(ms) = timeout_ms { + request_builder = request_builder.timeout(std::time::Duration::from_millis(ms)); + } + + // 发送请求 + let response = request_builder + .send() + .await + .map_err(|e| format!("请求 API 失败: {e}"))?; + + // 检查响应状态 + if !response.status().is_success() { + let status = response.status(); + let error_text = response.text().await.unwrap_or_default(); + return Err(format!("API 请求失败 ({status}): {error_text}")); + } + + // 解析为 JSON + let data: serde_json::Value = response + .json() + .await + .map_err(|e| format!("解析响应 JSON 失败: {e}"))?; + + Ok(data) +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 02d4ee7..e448a8d 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -1,3 +1,4 @@ +pub mod balance_commands; pub mod config_commands; pub mod log_commands; pub mod proxy_commands; @@ -9,6 +10,7 @@ pub mod update_commands; pub mod window_commands; // 重新导出所有命令函数 +pub use balance_commands::*; pub use config_commands::*; pub use log_commands::*; pub use proxy_commands::*; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index daef4fb..e84849f 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -361,6 +361,7 @@ fn main() { generate_api_key_for_tool, get_usage_stats, get_user_quota, + fetch_api, handle_close_action, // expose current proxy for debugging/testing get_current_proxy, diff --git a/src/App.tsx b/src/App.tsx index 54a0916..1b1feff 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,7 @@ import { AppSidebar } from '@/components/layout/AppSidebar'; import { CloseActionDialog } from '@/components/dialogs/CloseActionDialog'; import { UpdateDialog } from '@/components/dialogs/UpdateDialog'; import { StatisticsPage } from '@/pages/StatisticsPage'; +import { BalancePage } from '@/pages/BalancePage'; import { InstallationPage } from '@/pages/InstallationPage'; import { DashboardPage } from '@/pages/DashboardPage'; import { ConfigurationPage } from '@/pages/ConfigurationPage'; @@ -34,6 +35,7 @@ type TabType = | 'config' | 'switch' | 'statistics' + | 'balance' | 'transparent-proxy' | 'settings'; @@ -285,6 +287,7 @@ function App() { onLoadStatistics={loadStatistics} /> )} + {activeTab === 'balance' && } {activeTab === 'transparent-proxy' && ( )} diff --git a/src/components/layout/AppSidebar.tsx b/src/components/layout/AppSidebar.tsx index e8ccf0b..d4496ee 100644 --- a/src/components/layout/AppSidebar.tsx +++ b/src/components/layout/AppSidebar.tsx @@ -6,6 +6,7 @@ import { Key, ArrowRightLeft, BarChart3, + Wallet, Radio, Settings as SettingsIcon, } from 'lucide-react'; @@ -77,6 +78,15 @@ export function AppSidebar({ activeTab, onTabChange }: AppSidebarProps) { 用量统计 + +