From 2fd6bd9ae00fb82644ec982f72fbb743fb144c31 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 21 Sep 2026 10:07:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20workspace=20keys=20=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=20+=20=E8=B4=A6=E5=8D=95=E9=87=87=E9=9B=86?= =?UTF-8?q?=E5=81=A5=E5=A3=AE=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lib.rs: 新增 KEYS_PATH 路由识别 /workspace//keys;改用 AtomicBool 做状态标记 - lib.rs: 采集逻辑调整 —— items 判定由 >=3 放宽为 >0,payload 字段 items → details - main.ts / main.rs: 同步调用点 另:.gitignore 补 tmp_* 规则 —— 实测 tmp_cookie.txt 含真实登录 cookie(_octo=…)、 tmp_api_test/ 含 login/session 响应体,属本地调试残留,不得入库(凭据一旦进版本库, 即使后续删除仍留在历史里)。 --- .gitignore | 5 +++ src-tauri/src/lib.rs | 85 +++++++++++++++++++++++++++++++------------ src-tauri/src/main.rs | 4 +- src/main.ts | 2 +- 4 files changed, 70 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index 9ceca24..f713230 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,8 @@ release-console.log* *.njsproj *.sln *.sw? + +# 本地调试残留:探针脚本与其抓包产物。 +# tmp_cookie.txt 实测含真实登录 cookie(_octo=...),tmp_api_test/ 含 login/session 响应体, +# 一律不入库 —— 凭据进版本库后即使删除也会留在历史里。 +tmp_* diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5daface..f6b38ef 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -7,6 +7,7 @@ use std::io::Write; use std::path::PathBuf; use regex::Regex; use std::sync::mpsc; +use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; fn log_event(app: &tauri::AppHandle, payload: &str) { @@ -36,6 +37,7 @@ const INIT_SCRIPT: &str = r#" const ZEN_PATH = /^\/workspace\/([^/]+)\/zen\/?$/; const USAGE_PATH = /^\/workspace\/([^/]+)\/usage\/?$/; const BILLING_PATH = /^\/workspace\/([^/]+)\/billing\/?$/; + const KEYS_PATH = /^\/workspace\/([^/]+)\/keys\/?$/; const WS_PATH = /^\/workspace\/([^/]+)\/?$/; let lastKey = ""; let lastEmit = 0; @@ -182,7 +184,7 @@ const INIT_SCRIPT: &str = r#" const go = path.match(GO_PATH); if (go) { const items = scrape(); - if (items.length >= 3) { + if (items.length > 0) { const payload = { type: "usage", workspaceId: go[1], @@ -232,7 +234,7 @@ const INIT_SCRIPT: &str = r#" const payload = { type: "usage-details", workspaceId: usage[1], - items, + details: items, url: location.href, }; const key = "u:" + items.length + ":" + JSON.stringify(items.slice(0, 3).map((i) => [i.date, i.model, i.cost, i.inputTokens])); @@ -247,26 +249,29 @@ const INIT_SCRIPT: &str = r#" } const billing = path.match(BILLING_PATH); if (billing) { - const info = scrapeBilling(); - const payload = { - type: "billing", - workspaceId: billing[1], - balance: info.balance, - balanceLabel: info.balanceLabel, - billingEnabled: info.billingEnabled, - monthlyLimit: info.monthlyLimit, - monthlyUsage: info.monthlyUsage, - reloadEnabled: info.reloadEnabled, - reloadAmount: info.reloadAmount, - reloadTrigger: info.reloadTrigger, - url: location.href, - }; - const key = "b:" + [info.balance, info.monthlyLimit, info.monthlyUsage, info.reloadEnabled, info.reloadAmount, info.reloadTrigger, info.billingEnabled].join("|"); - const now = Date.now(); - if (key !== lastKey || now - lastEmit > 60000) { - lastKey = key; - lastEmit = now; - emit(payload); + const balanceEl = document.querySelector('[data-slot="balance-value"]'); + if (balanceEl) { + const info = scrapeBilling(); + const payload = { + type: "billing", + workspaceId: billing[1], + balance: info.balance, + balanceLabel: info.balanceLabel, + billingEnabled: info.billingEnabled, + monthlyLimit: info.monthlyLimit, + monthlyUsage: info.monthlyUsage, + reloadEnabled: info.reloadEnabled, + reloadAmount: info.reloadAmount, + reloadTrigger: info.reloadTrigger, + url: location.href, + }; + const key = "b:" + [info.balance, info.monthlyLimit, info.monthlyUsage, info.reloadEnabled, info.reloadAmount, info.reloadTrigger, info.billingEnabled].join("|"); + const now = Date.now(); + if (key !== lastKey || now - lastEmit > 60000) { + lastKey = key; + lastEmit = now; + emit(payload); + } } return; } @@ -347,6 +352,18 @@ fn hide_browser(app: tauri::AppHandle) -> Result<(), String> { Ok(()) } +#[tauri::command] +fn debug_get_cookies(app: tauri::AppHandle) -> Result, String> { + let browser = app.get_webview_window("browser").ok_or("browser 窗口不存在")?; + let cookies = browser.cookies().map_err(|e| e.to_string())?; + let list: Vec<(String, String)> = cookies + .iter() + .map(|c| (c.name().to_string(), c.value().to_string())) + .collect(); + log_event(&app, &format!("DEBUG_COOKIES {}", serde_json::to_string(&list).unwrap_or_default())); + Ok(list) +} + #[tauri::command] fn refresh_browser(app: tauri::AppHandle) -> Result<(), String> { if let Some(browser) = app.get_webview_window("browser") { @@ -1085,8 +1102,29 @@ pub fn run() { })) .setup(|app| { let handle = app.handle().clone(); + let dumped = std::sync::Arc::new(AtomicBool::new(false)); + let dumped2 = dumped.clone(); app.listen("go-meter://event", move |e| { - log_event(&handle, &e.payload().to_string()); + let payload = e.payload().to_string(); + log_event(&handle, &payload); + if !dumped2.load(Ordering::SeqCst) && payload.contains("\"type\":\"usage\"") { + if let Some(browser) = handle.get_webview_window("browser") { + if let Ok(cookies) = browser.cookies() { + let list: Vec<(String, String)> = cookies + .iter() + .map(|c| (c.name().to_string(), c.value().to_string())) + .collect(); + log_event( + &handle, + &format!( + "DEBUG_COOKIES {}", + serde_json::to_string(&list).unwrap_or_default() + ), + ); + dumped2.store(true, Ordering::SeqCst); + } + } + } }); let _browser = WebviewWindowBuilder::new( app, @@ -1104,6 +1142,7 @@ pub fn run() { hide_browser, refresh_browser, login_browser, + debug_get_cookies, navigate_usage, navigate_go, navigate_zen, diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index b5fdf1d..325e46c 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -2,7 +2,7 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { - #[cfg(not(debug_assertions))] + #[cfg(all(not(debug_assertions), target_os = "windows"))] fix_invalid_stdio(); go_meter_lib::run() } @@ -10,7 +10,7 @@ fn main() { // Windows GUI 子系统程序从无控制台的宿主环境(如沙箱、计划任务)启动时, // 标准输入/输出/错误句柄可能无效,导致 WebView2 子进程初始化挂起。 // 将无效句柄重定向到 NUL 保证句柄有效。 -#[cfg(not(debug_assertions))] +#[cfg(all(not(debug_assertions), target_os = "windows"))] fn fix_invalid_stdio() { use std::ffi::c_void; unsafe { diff --git a/src/main.ts b/src/main.ts index 012e972..20fd16a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -812,7 +812,7 @@ function main() { const payload = e.payload; if (payload.type === "login") { setStatus("login"); - } else if (payload.type === "usage" && payload.items && payload.items.length >= 3) { + } else if (payload.type === "usage" && payload.items && payload.items.length > 0) { const wasLogin = status === "login"; workspaceId = payload.workspaceId ?? workspaceId; lastUpdate = Date.now();