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
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.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 std::path::PathBuf;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
fn log_event(app: &tauri::AppHandle, payload: &str) {
|
fn log_event(app: &tauri::AppHandle, payload: &str) {
|
||||||
@@ -36,6 +37,7 @@ const INIT_SCRIPT: &str = r#"
|
|||||||
const ZEN_PATH = /^\/workspace\/([^/]+)\/zen\/?$/;
|
const ZEN_PATH = /^\/workspace\/([^/]+)\/zen\/?$/;
|
||||||
const USAGE_PATH = /^\/workspace\/([^/]+)\/usage\/?$/;
|
const USAGE_PATH = /^\/workspace\/([^/]+)\/usage\/?$/;
|
||||||
const BILLING_PATH = /^\/workspace\/([^/]+)\/billing\/?$/;
|
const BILLING_PATH = /^\/workspace\/([^/]+)\/billing\/?$/;
|
||||||
|
const KEYS_PATH = /^\/workspace\/([^/]+)\/keys\/?$/;
|
||||||
const WS_PATH = /^\/workspace\/([^/]+)\/?$/;
|
const WS_PATH = /^\/workspace\/([^/]+)\/?$/;
|
||||||
let lastKey = "";
|
let lastKey = "";
|
||||||
let lastEmit = 0;
|
let lastEmit = 0;
|
||||||
@@ -182,7 +184,7 @@ const INIT_SCRIPT: &str = r#"
|
|||||||
const go = path.match(GO_PATH);
|
const go = path.match(GO_PATH);
|
||||||
if (go) {
|
if (go) {
|
||||||
const items = scrape();
|
const items = scrape();
|
||||||
if (items.length >= 3) {
|
if (items.length > 0) {
|
||||||
const payload = {
|
const payload = {
|
||||||
type: "usage",
|
type: "usage",
|
||||||
workspaceId: go[1],
|
workspaceId: go[1],
|
||||||
@@ -232,7 +234,7 @@ const INIT_SCRIPT: &str = r#"
|
|||||||
const payload = {
|
const payload = {
|
||||||
type: "usage-details",
|
type: "usage-details",
|
||||||
workspaceId: usage[1],
|
workspaceId: usage[1],
|
||||||
items,
|
details: items,
|
||||||
url: location.href,
|
url: location.href,
|
||||||
};
|
};
|
||||||
const key = "u:" + items.length + ":" + JSON.stringify(items.slice(0, 3).map((i) => [i.date, i.model, i.cost, i.inputTokens]));
|
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);
|
const billing = path.match(BILLING_PATH);
|
||||||
if (billing) {
|
if (billing) {
|
||||||
const info = scrapeBilling();
|
const balanceEl = document.querySelector('[data-slot="balance-value"]');
|
||||||
const payload = {
|
if (balanceEl) {
|
||||||
type: "billing",
|
const info = scrapeBilling();
|
||||||
workspaceId: billing[1],
|
const payload = {
|
||||||
balance: info.balance,
|
type: "billing",
|
||||||
balanceLabel: info.balanceLabel,
|
workspaceId: billing[1],
|
||||||
billingEnabled: info.billingEnabled,
|
balance: info.balance,
|
||||||
monthlyLimit: info.monthlyLimit,
|
balanceLabel: info.balanceLabel,
|
||||||
monthlyUsage: info.monthlyUsage,
|
billingEnabled: info.billingEnabled,
|
||||||
reloadEnabled: info.reloadEnabled,
|
monthlyLimit: info.monthlyLimit,
|
||||||
reloadAmount: info.reloadAmount,
|
monthlyUsage: info.monthlyUsage,
|
||||||
reloadTrigger: info.reloadTrigger,
|
reloadEnabled: info.reloadEnabled,
|
||||||
url: location.href,
|
reloadAmount: info.reloadAmount,
|
||||||
};
|
reloadTrigger: info.reloadTrigger,
|
||||||
const key = "b:" + [info.balance, info.monthlyLimit, info.monthlyUsage, info.reloadEnabled, info.reloadAmount, info.reloadTrigger, info.billingEnabled].join("|");
|
url: location.href,
|
||||||
const now = Date.now();
|
};
|
||||||
if (key !== lastKey || now - lastEmit > 60000) {
|
const key = "b:" + [info.balance, info.monthlyLimit, info.monthlyUsage, info.reloadEnabled, info.reloadAmount, info.reloadTrigger, info.billingEnabled].join("|");
|
||||||
lastKey = key;
|
const now = Date.now();
|
||||||
lastEmit = now;
|
if (key !== lastKey || now - lastEmit > 60000) {
|
||||||
emit(payload);
|
lastKey = key;
|
||||||
|
lastEmit = now;
|
||||||
|
emit(payload);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -347,6 +352,18 @@ fn hide_browser(app: tauri::AppHandle) -> Result<(), String> {
|
|||||||
Ok(())
|
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]
|
#[tauri::command]
|
||||||
fn refresh_browser(app: tauri::AppHandle) -> Result<(), String> {
|
fn refresh_browser(app: tauri::AppHandle) -> Result<(), String> {
|
||||||
if let Some(browser) = app.get_webview_window("browser") {
|
if let Some(browser) = app.get_webview_window("browser") {
|
||||||
@@ -1085,8 +1102,29 @@ pub fn run() {
|
|||||||
}))
|
}))
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
let handle = app.handle().clone();
|
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| {
|
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(
|
let _browser = WebviewWindowBuilder::new(
|
||||||
app,
|
app,
|
||||||
@@ -1104,6 +1142,7 @@ pub fn run() {
|
|||||||
hide_browser,
|
hide_browser,
|
||||||
refresh_browser,
|
refresh_browser,
|
||||||
login_browser,
|
login_browser,
|
||||||
|
debug_get_cookies,
|
||||||
navigate_usage,
|
navigate_usage,
|
||||||
navigate_go,
|
navigate_go,
|
||||||
navigate_zen,
|
navigate_zen,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
#[cfg(not(debug_assertions))]
|
#[cfg(all(not(debug_assertions), target_os = "windows"))]
|
||||||
fix_invalid_stdio();
|
fix_invalid_stdio();
|
||||||
go_meter_lib::run()
|
go_meter_lib::run()
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,7 @@ fn main() {
|
|||||||
// Windows GUI 子系统程序从无控制台的宿主环境(如沙箱、计划任务)启动时,
|
// Windows GUI 子系统程序从无控制台的宿主环境(如沙箱、计划任务)启动时,
|
||||||
// 标准输入/输出/错误句柄可能无效,导致 WebView2 子进程初始化挂起。
|
// 标准输入/输出/错误句柄可能无效,导致 WebView2 子进程初始化挂起。
|
||||||
// 将无效句柄重定向到 NUL 保证句柄有效。
|
// 将无效句柄重定向到 NUL 保证句柄有效。
|
||||||
#[cfg(not(debug_assertions))]
|
#[cfg(all(not(debug_assertions), target_os = "windows"))]
|
||||||
fn fix_invalid_stdio() {
|
fn fix_invalid_stdio() {
|
||||||
use std::ffi::c_void;
|
use std::ffi::c_void;
|
||||||
unsafe {
|
unsafe {
|
||||||
|
|||||||
+1
-1
@@ -812,7 +812,7 @@ function main() {
|
|||||||
const payload = e.payload;
|
const payload = e.payload;
|
||||||
if (payload.type === "login") {
|
if (payload.type === "login") {
|
||||||
setStatus("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";
|
const wasLogin = status === "login";
|
||||||
workspaceId = payload.workspaceId ?? workspaceId;
|
workspaceId = payload.workspaceId ?? workspaceId;
|
||||||
lastUpdate = Date.now();
|
lastUpdate = Date.now();
|
||||||
|
|||||||
Reference in New Issue
Block a user