feat: workspace keys 接口解析 + 账单采集健壮性
- lib.rs: 新增 KEYS_PATH 路由识别 /workspace/<id>/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 响应体,属本地调试残留,不得入库(凭据一旦进版本库, 即使后续删除仍留在历史里)。
This commit is contained in:
@@ -32,3 +32,8 @@ release-console.log*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# 本地调试残留:探针脚本与其抓包产物。
|
||||
# tmp_cookie.txt 实测含真实登录 cookie(_octo=...),tmp_api_test/ 含 login/session 响应体,
|
||||
# 一律不入库 —— 凭据进版本库后即使删除也会留在历史里。
|
||||
tmp_*
|
||||
|
||||
+62
-23
@@ -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<Vec<(String, String)>, 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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+1
-1
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user