Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e71671abb | ||
|
|
af36a817fb | ||
|
|
e908d71eb6 |
+12
-3
@@ -8,8 +8,12 @@ namespace ClaudeCheckerWindows;
|
|||||||
|
|
||||||
public partial class App : Application
|
public partial class App : Application
|
||||||
{
|
{
|
||||||
public static UsageViewModel ViewModel { get; } = new();
|
public static UsageViewModel ViewModel { get; } = new();
|
||||||
public static UpdateManager Updater { get; } = new();
|
public static UpdateManager Updater { get; } = new();
|
||||||
|
// Persistent hidden WebView2 — shares the same user data folder as LoginWindow
|
||||||
|
// so its cookies (including cf_clearance) are always live. Used on every refresh
|
||||||
|
// for endpoints that need a real browser session (overage, prepaid).
|
||||||
|
public static WebViewFetchWindow BackgroundBrowser { get; } = new();
|
||||||
|
|
||||||
private Forms.NotifyIcon? _tray;
|
private Forms.NotifyIcon? _tray;
|
||||||
private PopupWindow? _popup;
|
private PopupWindow? _popup;
|
||||||
@@ -36,10 +40,15 @@ public partial class App : Application
|
|||||||
ShowPopup();
|
ShowPopup();
|
||||||
ScheduleTimer(ViewModel.RefreshInterval);
|
ScheduleTimer(ViewModel.RefreshInterval);
|
||||||
|
|
||||||
|
// Show background browser window on the UI thread before Task.Run
|
||||||
|
BackgroundBrowser.Show();
|
||||||
|
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
await ViewModel.LoadFromCacheAsync();
|
await ViewModel.LoadFromCacheAsync();
|
||||||
await Task.Delay(1000);
|
// Initialize the persistent background browser (navigates to claude.ai once)
|
||||||
|
await Application.Current.Dispatcher.InvokeAsync(
|
||||||
|
() => BackgroundBrowser.InitAsync()).Task.Unwrap();
|
||||||
await ViewModel.RefreshAsync();
|
await ViewModel.RefreshAsync();
|
||||||
await Updater.CheckForUpdatesAsync();
|
await Updater.CheckForUpdatesAsync();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -268,14 +268,53 @@ public class UsageViewModel : INotifyPropertyChanged
|
|||||||
catch { /* best-effort */ }
|
catch { /* best-effort */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If HttpClient couldn't reach overage/prepaid endpoints (expired cf_clearance),
|
||||||
|
// fall back to the persistent background WebView2 which always has live cookies.
|
||||||
|
var overageResult = ot.Result;
|
||||||
|
var prepaidResult = pt.Result;
|
||||||
|
if ((overageResult == null || prepaidResult == null) && orgId != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var browser = App.BackgroundBrowser;
|
||||||
|
if (browser != null)
|
||||||
|
{
|
||||||
|
var wvScript = $@"(async()=>{{try{{
|
||||||
|
const h={{headers:{{accept:'application/json'}}}};
|
||||||
|
const [ov,pp]=await Promise.all([
|
||||||
|
fetch('/api/organizations/{orgId}/overage_spend_limit',h).then(r=>r.ok?r.json():null).catch(()=>null),
|
||||||
|
fetch('/api/organizations/{orgId}/prepaid/credits',h).then(r=>r.ok?r.json():null).catch(()=>null)
|
||||||
|
]);
|
||||||
|
window.chrome.webview.postMessage({{overage:ov,prepaid:pp}});
|
||||||
|
}}catch(ex){{window.chrome.webview.postMessage(null);}}}})()";
|
||||||
|
|
||||||
|
var wvJson = await Application.Current.Dispatcher.InvokeAsync(
|
||||||
|
() => browser.RunScriptAsync(wvScript)).Task.Unwrap();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(wvJson) && wvJson != "null")
|
||||||
|
{
|
||||||
|
using var wvDoc = JsonDocument.Parse(wvJson);
|
||||||
|
var wvRoot = wvDoc.RootElement;
|
||||||
|
if (overageResult == null && wvRoot.TryGetProperty("overage", out var ovEl)
|
||||||
|
&& ovEl.ValueKind == JsonValueKind.Object)
|
||||||
|
overageResult = JsonSerializer.Deserialize<OverageSpendLimit>(ovEl.GetRawText(), JsonOpts);
|
||||||
|
if (prepaidResult == null && wvRoot.TryGetProperty("prepaid", out var ppEl)
|
||||||
|
&& ppEl.ValueKind == JsonValueKind.Object)
|
||||||
|
prepaidResult = JsonSerializer.Deserialize<PrepaidCredits>(ppEl.GetRawText(), JsonOpts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { /* best-effort — never block the refresh */ }
|
||||||
|
}
|
||||||
|
|
||||||
// Persist fresh usage so cache reflects live data
|
// Persist fresh usage so cache reflects live data
|
||||||
AppSettings.Default.UsageJson = usageJson;
|
AppSettings.Default.UsageJson = usageJson;
|
||||||
if (!string.IsNullOrEmpty(planLabel)) AppSettings.Default.PlanLabel = planLabel;
|
if (!string.IsNullOrEmpty(planLabel)) AppSettings.Default.PlanLabel = planLabel;
|
||||||
if (ot.Result != null) AppSettings.Default.OverageJson = JsonSerializer.Serialize(ot.Result);
|
if (overageResult != null) AppSettings.Default.OverageJson = JsonSerializer.Serialize(overageResult);
|
||||||
if (pt.Result != null) AppSettings.Default.PrepaidJson = JsonSerializer.Serialize(pt.Result);
|
if (prepaidResult != null) AppSettings.Default.PrepaidJson = JsonSerializer.Serialize(prepaidResult);
|
||||||
AppSettings.Default.Save();
|
AppSettings.Default.Save();
|
||||||
|
|
||||||
return (BuildLimits(usage), ot.Result, pt.Result, usage?.ExtraUsage, email, orgId, planLabel, true);
|
return (BuildLimits(usage), overageResult, prepaidResult, usage?.ExtraUsage, email, orgId, planLabel, true);
|
||||||
}
|
}
|
||||||
catch { throw; }
|
catch { throw; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,42 +2,103 @@ using Microsoft.Web.WebView2.Core;
|
|||||||
using Microsoft.Web.WebView2.Wpf;
|
using Microsoft.Web.WebView2.Wpf;
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
|
||||||
namespace ClaudeCheckerWindows;
|
namespace ClaudeCheckerWindows;
|
||||||
|
|
||||||
// Invisible 1×1 window that hosts a WebView2 for authenticated API calls.
|
// Invisible 1×1 window that hosts a WebView2 for authenticated API calls.
|
||||||
// Uses the same user data folder as LoginWindow so the session is shared.
|
// Uses the same user data folder as LoginWindow so the session (including
|
||||||
|
// cf_clearance) is always shared and fresh — equivalent to macOS's WKWebsiteDataStore.
|
||||||
|
//
|
||||||
|
// Two usage modes:
|
||||||
|
// • Persistent (App.BackgroundBrowser): created once at startup, navigates to
|
||||||
|
// claude.ai once, then scripts run directly on the live page every refresh.
|
||||||
|
// No re-navigation = no memory accumulation.
|
||||||
|
// • One-shot (LoginWindow): created, used, closed — same as before.
|
||||||
internal sealed class WebViewFetchWindow : Window
|
internal sealed class WebViewFetchWindow : Window
|
||||||
{
|
{
|
||||||
private readonly WebView2 _wv = new();
|
private readonly WebView2 _wv = new();
|
||||||
|
private bool _initialized;
|
||||||
|
private bool _readyForScript; // true after first navigation to claude.ai completes
|
||||||
|
|
||||||
|
private static readonly string UserDataFolder =
|
||||||
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
|
"ClaudeChecker", "WebView2");
|
||||||
|
|
||||||
public WebViewFetchWindow()
|
public WebViewFetchWindow()
|
||||||
{
|
{
|
||||||
Width = 1;
|
Width = 1;
|
||||||
Height = 1;
|
Height = 1;
|
||||||
Left = -9999;
|
Left = -9999;
|
||||||
Top = -9999;
|
Top = -9999;
|
||||||
ShowInTaskbar = false;
|
ShowInTaskbar = false;
|
||||||
WindowStyle = WindowStyle.None;
|
WindowStyle = WindowStyle.None;
|
||||||
AllowsTransparency = true;
|
AllowsTransparency = true;
|
||||||
Opacity = 0;
|
Opacity = 0;
|
||||||
Content = _wv;
|
Content = _wv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Persistent-mode init ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Call once at startup. Navigates to claude.ai so the session/cookies are
|
||||||
|
// established and cf_clearance is fresh. Subsequent RunScriptAsync calls
|
||||||
|
// skip navigation and just execute JS on the live page.
|
||||||
|
public async Task InitAsync()
|
||||||
|
{
|
||||||
|
await EnsureInitAsync();
|
||||||
|
|
||||||
|
var navDone = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
EventHandler<CoreWebView2NavigationCompletedEventArgs>? h = null;
|
||||||
|
h = (_, e) => { _wv.CoreWebView2.NavigationCompleted -= h; navDone.TrySetResult(e.IsSuccess); };
|
||||||
|
_wv.CoreWebView2.NavigationCompleted += h;
|
||||||
|
_wv.CoreWebView2.Navigate("https://claude.ai");
|
||||||
|
|
||||||
|
// Wait up to 15 s for initial navigation
|
||||||
|
await Task.WhenAny(navDone.Task, Task.Delay(15000));
|
||||||
|
_readyForScript = navDone.Task.IsCompletedSuccessfully && navDone.Task.Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run a JS script on the already-loaded claude.ai page.
|
||||||
|
// The script must call window.chrome.webview.postMessage(result).
|
||||||
|
public async Task<string?> RunScriptAsync(string script, int timeoutMs = 10000)
|
||||||
|
{
|
||||||
|
if (!_readyForScript) return null;
|
||||||
|
|
||||||
|
var tcs = new TaskCompletionSource<string?>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
EventHandler<CoreWebView2WebMessageReceivedEventArgs>? msgHandler = null;
|
||||||
|
msgHandler = (_, args) =>
|
||||||
|
{
|
||||||
|
_wv.CoreWebView2.WebMessageReceived -= msgHandler;
|
||||||
|
tcs.TrySetResult(args.WebMessageAsJson);
|
||||||
|
};
|
||||||
|
_wv.CoreWebView2.WebMessageReceived += msgHandler;
|
||||||
|
|
||||||
|
try { await _wv.CoreWebView2.ExecuteScriptAsync(script); }
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_wv.CoreWebView2.WebMessageReceived -= msgHandler;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = Task.Delay(timeoutMs).ContinueWith(_ =>
|
||||||
|
{
|
||||||
|
_wv.CoreWebView2.WebMessageReceived -= msgHandler;
|
||||||
|
tcs.TrySetResult(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
return await tcs.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── One-shot mode (LoginWindow) ───────────────────────────────────────────
|
||||||
|
|
||||||
public async Task<string?> FetchAsync(string navigateUrl, string script, int timeoutMs = 20000)
|
public async Task<string?> FetchAsync(string navigateUrl, string script, int timeoutMs = 20000)
|
||||||
{
|
{
|
||||||
var env = await CoreWebView2Environment.CreateAsync(userDataFolder:
|
await EnsureInitAsync();
|
||||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
|
||||||
"ClaudeChecker", "WebView2"));
|
|
||||||
|
|
||||||
await _wv.EnsureCoreWebView2Async(env);
|
|
||||||
|
|
||||||
var tcs = new TaskCompletionSource<string?>(TaskCreationOptions.RunContinuationsAsynchronously);
|
var tcs = new TaskCompletionSource<string?>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
|
||||||
// Use WebMessageReceived so the script can post back asynchronously without
|
|
||||||
// relying on Promise-awaiting support in the WebView2 runtime version.
|
|
||||||
EventHandler<CoreWebView2WebMessageReceivedEventArgs>? msgHandler = null;
|
EventHandler<CoreWebView2WebMessageReceivedEventArgs>? msgHandler = null;
|
||||||
msgHandler = (_, args) =>
|
msgHandler = (_, args) =>
|
||||||
{
|
{
|
||||||
@@ -65,4 +126,18 @@ internal sealed class WebViewFetchWindow : Window
|
|||||||
|
|
||||||
return await tcs.Task;
|
return await tcs.Task;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Shared init ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private int _initGuard;
|
||||||
|
|
||||||
|
private async Task EnsureInitAsync()
|
||||||
|
{
|
||||||
|
if (_initialized) return;
|
||||||
|
if (Interlocked.Exchange(ref _initGuard, 1) != 0) return;
|
||||||
|
|
||||||
|
var env = await CoreWebView2Environment.CreateAsync(userDataFolder: UserDataFolder);
|
||||||
|
await _wv.EnsureCoreWebView2Async(env);
|
||||||
|
_initialized = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user