pi-harness-runtime 1.1.80 → 1.1.82
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/harness/browser-scrapers/base-browser-scraper.d.ts +91 -0
- package/harness/browser-scrapers/chrome-profile-manager.d.ts +80 -0
- package/harness/browser-scrapers/codex-browser-scraper.d.ts +60 -0
- package/harness/browser-scrapers/index.d.ts +29 -0
- package/harness/browser-scrapers/zai-browser-scraper.d.ts +79 -0
- package/harness/e2e/browser-scrapers/base-browser-scraper.d.ts +92 -0
- package/harness/e2e/browser-scrapers/base-browser-scraper.ts +244 -0
- package/harness/e2e/browser-scrapers/chrome-profile-manager.d.ts +81 -0
- package/harness/e2e/browser-scrapers/chrome-profile-manager.ts +297 -0
- package/harness/e2e/browser-scrapers/codex-browser-scraper.d.ts +61 -0
- package/harness/e2e/browser-scrapers/codex-browser-scraper.ts +343 -0
- package/harness/e2e/browser-scrapers/index.d.ts +30 -0
- package/harness/e2e/browser-scrapers/index.ts +48 -0
- package/harness/e2e/browser-scrapers/zai-browser-scraper.d.ts +80 -0
- package/harness/e2e/browser-scrapers/zai-browser-scraper.ts +392 -0
- package/harness/e2e/glm-quota-scraper.ts +106 -15
- package/harness/e2e/index.ts +29 -0
- package/harness/e2e/unified-quota-manager.ts +364 -0
- package/index.ts +72 -11
- package/mirror.ts +32 -28
- package/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/dist/provider-id.d.ts.map +1 -1
- package/packages/providers/dist/provider-id.js +1 -2
- package/packages/providers/dist/provider-id.js.map +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/providers/src/provider-id.ts +1 -2
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Z.ai Browser Scraper — Scrapes z.ai usage data
|
|
3
|
+
*
|
|
4
|
+
* Authentication approach:
|
|
5
|
+
* - Z.ai uses cookies from browser login (different from ChatGPT)
|
|
6
|
+
* - Cookies from ~/.config/google-chrome or browser export need to be tested
|
|
7
|
+
* - Fallback: Manual cookie export from browser DevTools
|
|
8
|
+
*
|
|
9
|
+
* Note: If cookies don't work for browser automation (like Codex),
|
|
10
|
+
* we may need to find alternative auth methods (OAuth, API keys, etc.)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
|
|
17
|
+
import { BaseBrowserScraper, type BrowserScraperConfig } from "./base-browser-scraper.js";
|
|
18
|
+
|
|
19
|
+
export interface ZaiQuotaData {
|
|
20
|
+
provider: "z-ai";
|
|
21
|
+
/** Plan type (e.g., "Pro", "Team", "Enterprise") */
|
|
22
|
+
planType?: string;
|
|
23
|
+
/** Current usage percentage (0-100) */
|
|
24
|
+
usagePercent: number;
|
|
25
|
+
/** Total quota (e.g., "5 GB") */
|
|
26
|
+
totalQuota?: string;
|
|
27
|
+
/** Used quota (e.g., "1.5 GB") */
|
|
28
|
+
usedQuota?: string;
|
|
29
|
+
/** Remaining quota */
|
|
30
|
+
remainingQuota?: string;
|
|
31
|
+
/** Reset date/time */
|
|
32
|
+
resetAt?: string;
|
|
33
|
+
/** Billing period */
|
|
34
|
+
period?: string;
|
|
35
|
+
/** Timestamp */
|
|
36
|
+
scrapedAt: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ZaiScraperConfig {
|
|
40
|
+
provider?: string;
|
|
41
|
+
url?: string;
|
|
42
|
+
headless?: boolean;
|
|
43
|
+
timeout?: number;
|
|
44
|
+
viewport?: { width: number; height: number };
|
|
45
|
+
slowMo?: number;
|
|
46
|
+
/** Path to cookies file (Netscape format) */
|
|
47
|
+
cookiesFile?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Known cookie file locations to try
|
|
51
|
+
const COOKIE_LOCATIONS = [
|
|
52
|
+
// Chrome Linux
|
|
53
|
+
join(homedir(), ".config", "google-chrome", "Default", "Cookies"),
|
|
54
|
+
join(homedir(), ".config", "google-chrome", "Default", "Network", "Cookies"),
|
|
55
|
+
join(homedir(), ".config", "chromium", "Default", "Cookies"),
|
|
56
|
+
// Netscape cookie format exports
|
|
57
|
+
join(homedir(), ".pi-harness-runtime", "cookies", "zai-cookies.txt"),
|
|
58
|
+
join(homedir(), ".config", "zai-cookies.txt"),
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Z.ai Browser Scraper
|
|
63
|
+
*/
|
|
64
|
+
export class ZaiBrowserScraper extends BaseBrowserScraper<ZaiQuotaData> {
|
|
65
|
+
private cookiesFile?: string;
|
|
66
|
+
|
|
67
|
+
constructor(config: Partial<ZaiScraperConfig> = {}) {
|
|
68
|
+
super({
|
|
69
|
+
provider: "z-ai",
|
|
70
|
+
url: config.url ?? "https://z.ai/manage-apikey/coding-plan/personal/usage",
|
|
71
|
+
headless: config.headless ?? true,
|
|
72
|
+
timeout: config.timeout ?? 30000,
|
|
73
|
+
viewport: config.viewport,
|
|
74
|
+
slowMo: config.slowMo,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
this.cookiesFile = config.cookiesFile ?? this.findCookiesFile();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Find available cookies file
|
|
82
|
+
*/
|
|
83
|
+
private findCookiesFile(): string | undefined {
|
|
84
|
+
for (const location of COOKIE_LOCATIONS) {
|
|
85
|
+
if (existsSync(location)) {
|
|
86
|
+
return location;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
protected async createBrowser(): Promise<void> {
|
|
93
|
+
const { chromium } = await import("playwright");
|
|
94
|
+
|
|
95
|
+
const profileDir = join(homedir(), ".pi-harness-runtime", "browser-profiles", "zai");
|
|
96
|
+
|
|
97
|
+
// Launch with anti-detection args
|
|
98
|
+
this.browser = await chromium.launch({
|
|
99
|
+
headless: this.config.headless,
|
|
100
|
+
slowMo: this.config.slowMo,
|
|
101
|
+
args: [
|
|
102
|
+
`--user-data-dir=${profileDir}`,
|
|
103
|
+
"--disable-blink-features=AutomationControlled",
|
|
104
|
+
"--no-first-run",
|
|
105
|
+
"--no-default-browser-check",
|
|
106
|
+
"--disable-extensions",
|
|
107
|
+
],
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
this.context = await this.browser.newContext({
|
|
111
|
+
viewport: this.config.viewport,
|
|
112
|
+
// If we have a cookies file, import it
|
|
113
|
+
...(this.cookiesFile && existsSync(this.cookiesFile)
|
|
114
|
+
? { extraHTTPHeaders: await this.loadCookiesAsHeaders() }
|
|
115
|
+
: {}),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
this.page = await this.context.newPage();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Load cookies from file and convert to headers (fallback auth method)
|
|
123
|
+
* Note: This won't work for all sites - some require full cookie jar
|
|
124
|
+
*/
|
|
125
|
+
private async loadCookiesAsHeaders(): Promise<Record<string, string>> {
|
|
126
|
+
if (!this.cookiesFile || !existsSync(this.cookiesFile)) {
|
|
127
|
+
return {};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
// Try to parse as Netscape format
|
|
132
|
+
const content = readFileSync(this.cookiesFile, "utf-8");
|
|
133
|
+
const lines = content.split("\n").filter((l) => !l.startsWith("#") && l.trim());
|
|
134
|
+
|
|
135
|
+
// Extract session-relevant cookies
|
|
136
|
+
const cookies: Record<string, string> = {};
|
|
137
|
+
for (const line of lines) {
|
|
138
|
+
const parts = line.split("\t");
|
|
139
|
+
if (parts.length >= 7) {
|
|
140
|
+
const domain = parts[0];
|
|
141
|
+
const name = parts[5];
|
|
142
|
+
const value = parts[6];
|
|
143
|
+
|
|
144
|
+
// Only include z.ai cookies
|
|
145
|
+
if (domain.includes("z.ai") || domain.includes("zcloud")) {
|
|
146
|
+
cookies[name] = value;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Return as Cookie header
|
|
152
|
+
const cookieHeader = Object.entries(cookies)
|
|
153
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
154
|
+
.join("; ");
|
|
155
|
+
|
|
156
|
+
return cookieHeader ? { Cookie: cookieHeader } : {};
|
|
157
|
+
} catch {
|
|
158
|
+
return {};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
protected async isLoginPage(): Promise<boolean> {
|
|
163
|
+
if (!this.page) return false;
|
|
164
|
+
|
|
165
|
+
const url = this.page.url();
|
|
166
|
+
|
|
167
|
+
// Check for login/ auth URLs
|
|
168
|
+
if (/signin|login|auth|oauth/i.test(url)) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Check for common login indicators
|
|
173
|
+
const loginSelectors = [
|
|
174
|
+
'button:has-text("Sign in")',
|
|
175
|
+
'button:has-text("Login")',
|
|
176
|
+
'input[type="email"]',
|
|
177
|
+
'input[placeholder*="email" i]',
|
|
178
|
+
'form[action*="login" i]',
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
for (const selector of loginSelectors) {
|
|
182
|
+
try {
|
|
183
|
+
const element = await this.page.$(selector);
|
|
184
|
+
if (element != null) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
} catch {
|
|
188
|
+
// Selector might not exist
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
protected async extractData(): Promise<ZaiQuotaData | null> {
|
|
196
|
+
if (!this.page) return null;
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
// Wait for page to fully load
|
|
200
|
+
await this.page.waitForTimeout(3000);
|
|
201
|
+
|
|
202
|
+
// Extract usage percentage
|
|
203
|
+
const usagePercent = await this.extractUsagePercent();
|
|
204
|
+
if (usagePercent === null) {
|
|
205
|
+
console.log("[z.ai] Could not extract usage percentage");
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Extract additional data
|
|
210
|
+
const planType = await this.extractPlanType();
|
|
211
|
+
const quotaData = await this.extractQuotaData();
|
|
212
|
+
const resetAt = await this.extractResetDate();
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
provider: "z-ai",
|
|
216
|
+
usagePercent,
|
|
217
|
+
planType,
|
|
218
|
+
...quotaData,
|
|
219
|
+
resetAt,
|
|
220
|
+
scrapedAt: new Date().toISOString(),
|
|
221
|
+
};
|
|
222
|
+
} catch (error) {
|
|
223
|
+
console.error(`[z.ai] Extraction error: ${error}`);
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private async extractUsagePercent(): Promise<number | null> {
|
|
229
|
+
if (!this.page) return null;
|
|
230
|
+
|
|
231
|
+
// Try various selectors for usage percentage
|
|
232
|
+
const selectors = [
|
|
233
|
+
// Percentage text
|
|
234
|
+
'text=/\\d+%/',
|
|
235
|
+
// Progress bar/circle with percentage
|
|
236
|
+
"[class*='progress'] [class*='text']",
|
|
237
|
+
"[class*='usage'] [class*='percent']",
|
|
238
|
+
"[class*='quota'] [class*='percent']",
|
|
239
|
+
// Heading near percentage
|
|
240
|
+
"h1:has-text('Usage') + div",
|
|
241
|
+
"h2:has-text('Usage') + div",
|
|
242
|
+
// Data attributes
|
|
243
|
+
"[data-usage]",
|
|
244
|
+
"[data-percent]",
|
|
245
|
+
"[data-quota]",
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
for (const selector of selectors) {
|
|
249
|
+
try {
|
|
250
|
+
const element = await this.page.$(selector);
|
|
251
|
+
if (element) {
|
|
252
|
+
const text = await element.textContent();
|
|
253
|
+
const match = text?.match(/(\d+(?:\.\d+)?)\s*%/);
|
|
254
|
+
if (match) {
|
|
255
|
+
const percent = parseFloat(match[1]);
|
|
256
|
+
if (!isNaN(percent) && percent >= 0 && percent <= 100) {
|
|
257
|
+
return percent;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} catch {
|
|
262
|
+
// Continue to next selector
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private async extractPlanType(): Promise<string | undefined> {
|
|
270
|
+
if (!this.page) return undefined;
|
|
271
|
+
|
|
272
|
+
const selectors = [
|
|
273
|
+
"[class*='plan']",
|
|
274
|
+
"[class*='tier']",
|
|
275
|
+
'text=/Pro|Team|Enterprise|Starter/i',
|
|
276
|
+
];
|
|
277
|
+
|
|
278
|
+
for (const selector of selectors) {
|
|
279
|
+
try {
|
|
280
|
+
const element = await this.page.$(selector);
|
|
281
|
+
if (element) {
|
|
282
|
+
const text = await element.textContent();
|
|
283
|
+
if (text && /Pro|Team|Enterprise|Starter/i.test(text)) {
|
|
284
|
+
return text.trim();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
} catch {
|
|
288
|
+
// Continue
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private async extractQuotaData(): Promise<{
|
|
296
|
+
totalQuota?: string;
|
|
297
|
+
usedQuota?: string;
|
|
298
|
+
remainingQuota?: string;
|
|
299
|
+
}> {
|
|
300
|
+
if (!this.page) return {};
|
|
301
|
+
|
|
302
|
+
const result: { totalQuota?: string; usedQuota?: string; remainingQuota?: string } = {};
|
|
303
|
+
|
|
304
|
+
// Try to find quota amounts like "1.5 GB / 5 GB"
|
|
305
|
+
const selectors = [
|
|
306
|
+
'text=/\\d+.*(?:GB|MB|TB).*(?:\\/|of).*(?:GB|MB|TB)/i',
|
|
307
|
+
"[class*='quota'] [class*='amount']",
|
|
308
|
+
"[class*='usage'] [class*='amount']",
|
|
309
|
+
];
|
|
310
|
+
|
|
311
|
+
for (const selector of selectors) {
|
|
312
|
+
try {
|
|
313
|
+
const element = await this.page.$(selector);
|
|
314
|
+
if (element) {
|
|
315
|
+
const text = await element.textContent();
|
|
316
|
+
if (text) {
|
|
317
|
+
// Parse "1.5 GB / 5 GB" format
|
|
318
|
+
const match = text.match(/([\d,.]+)\s*(GB|MB|TB)\s*(?:\/|of)\s*([\d,.]+)\s*(GB|MB|TB)/i);
|
|
319
|
+
if (match) {
|
|
320
|
+
result.usedQuota = `${match[1]} ${match[2]}`;
|
|
321
|
+
result.totalQuota = `${match[3]} ${match[4]}`;
|
|
322
|
+
// Calculate remaining
|
|
323
|
+
const used = parseFloat(match[1].replace(/,/g, ""));
|
|
324
|
+
const total = parseFloat(match[3].replace(/,/g, ""));
|
|
325
|
+
if (!isNaN(used) && !isNaN(total)) {
|
|
326
|
+
const remaining = total - used;
|
|
327
|
+
result.remainingQuota = `${remaining.toFixed(2)} ${match[4]}`;
|
|
328
|
+
}
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
} catch {
|
|
334
|
+
// Continue
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return result;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private async extractResetDate(): Promise<string | undefined> {
|
|
342
|
+
if (!this.page) return undefined;
|
|
343
|
+
|
|
344
|
+
const selectors = [
|
|
345
|
+
'text=/resets?(?:\\s+(?:at|on|in))?\\s*/i',
|
|
346
|
+
"[class*='reset']",
|
|
347
|
+
"[class*='renews']",
|
|
348
|
+
"[class*='billing'] [class*='date']",
|
|
349
|
+
];
|
|
350
|
+
|
|
351
|
+
for (const selector of selectors) {
|
|
352
|
+
try {
|
|
353
|
+
const element = await this.page.$(selector);
|
|
354
|
+
if (element) {
|
|
355
|
+
const text = await element.textContent();
|
|
356
|
+
if (text && /\d{4}|resets?|renews|billing/i.test(text)) {
|
|
357
|
+
return text.trim();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
} catch {
|
|
361
|
+
// Continue
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return undefined;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Quick scrape function
|
|
371
|
+
*/
|
|
372
|
+
export async function scrapeZaiQuota(cookiesFile?: string): Promise<ZaiQuotaData | null> {
|
|
373
|
+
const scraper = new ZaiBrowserScraper({ cookiesFile });
|
|
374
|
+
try {
|
|
375
|
+
const result = await scraper.scrape();
|
|
376
|
+
return result.data ?? null;
|
|
377
|
+
} finally {
|
|
378
|
+
await scraper.cleanup();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Check if z.ai cookies are available
|
|
384
|
+
*/
|
|
385
|
+
export function checkZaiCookies(): { available: boolean; path?: string } {
|
|
386
|
+
for (const location of COOKIE_LOCATIONS) {
|
|
387
|
+
if (existsSync(location)) {
|
|
388
|
+
return { available: true, path: location };
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return { available: false };
|
|
392
|
+
}
|
|
@@ -44,6 +44,18 @@ export interface GLMQuotaData {
|
|
|
44
44
|
hourlyCalls: number[];
|
|
45
45
|
/** Hourly token usage */
|
|
46
46
|
hourlyTokens: number[];
|
|
47
|
+
/** 5h window usage percentage (0-100) */
|
|
48
|
+
h5UsedPct: number;
|
|
49
|
+
/** When the 5h window resets (human-readable) */
|
|
50
|
+
h5ResetsAt: string;
|
|
51
|
+
/** Epoch ms when the 5h window resets */
|
|
52
|
+
h5ResetsAtEpoch: number;
|
|
53
|
+
/** Weekly usage percentage (0-100) */
|
|
54
|
+
weeklyUsedPct: number;
|
|
55
|
+
/** When the weekly window resets (human-readable) */
|
|
56
|
+
weeklyResetsAt: string;
|
|
57
|
+
/** Epoch ms when the weekly window resets */
|
|
58
|
+
weeklyResetsAtEpoch: number;
|
|
47
59
|
/** Raw API response for diagnostics */
|
|
48
60
|
apiEndpoint?: string;
|
|
49
61
|
/** Timestamp of scrape */
|
|
@@ -141,17 +153,63 @@ export class GLMQuotaScraper {
|
|
|
141
153
|
quiet: boolean;
|
|
142
154
|
};
|
|
143
155
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
156
|
+
constructor(config: GLMScraperConfig = {}) {
|
|
157
|
+
// Resolve API key: direct value > file > null
|
|
158
|
+
const apiKey =
|
|
159
|
+
config.apiKey ?? loadApiKey(config.apiKeyFile ?? DEFAULT_API_KEY_FILE);
|
|
148
160
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
161
|
+
this.config = {
|
|
162
|
+
apiKeyFile: config.apiKeyFile ?? DEFAULT_API_KEY_FILE,
|
|
163
|
+
apiKey: apiKey ?? null,
|
|
164
|
+
quiet: config.quiet ?? false,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Calculate 5h and weekly usage from hourly data
|
|
170
|
+
*/
|
|
171
|
+
private calculateHourlyUsage(
|
|
172
|
+
timestamps: string[],
|
|
173
|
+
tokens: number[],
|
|
174
|
+
_calls: number[],
|
|
175
|
+
): { h5Tokens: number; weeklyTokens: number } {
|
|
176
|
+
const now = new Date();
|
|
177
|
+
const fiveHoursAgo = new Date(now.getTime() - 5 * 60 * 60 * 1000);
|
|
178
|
+
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
179
|
+
|
|
180
|
+
let h5Tokens = 0;
|
|
181
|
+
let weeklyTokens = 0;
|
|
182
|
+
|
|
183
|
+
for (let i = 0; i < timestamps.length; i++) {
|
|
184
|
+
const ts = timestamps[i];
|
|
185
|
+
if (!ts) continue;
|
|
186
|
+
|
|
187
|
+
// Parse timestamp: "2026-09-14 00:00"
|
|
188
|
+
const hourTime = new Date(ts.replace(" ", "T") + ":00");
|
|
189
|
+
|
|
190
|
+
if (hourTime >= fiveHoursAgo) {
|
|
191
|
+
h5Tokens += tokens[i] ?? 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (hourTime >= weekAgo) {
|
|
195
|
+
weeklyTokens += tokens[i] ?? 0;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { h5Tokens, weeklyTokens };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Find the index of the last hour with usage
|
|
204
|
+
*/
|
|
205
|
+
private findLastUsageIndex(timestamps: string[], tokens: number[]): number {
|
|
206
|
+
for (let i = timestamps.length - 1; i >= 0; i--) {
|
|
207
|
+
if ((tokens[i] ?? 0) > 0) {
|
|
208
|
+
return i;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return -1;
|
|
212
|
+
}
|
|
155
213
|
|
|
156
214
|
/**
|
|
157
215
|
* Set API key directly
|
|
@@ -211,8 +269,9 @@ export class GLMQuotaScraper {
|
|
|
211
269
|
const end = endDate ?? new Date();
|
|
212
270
|
const start = startDate ?? new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
|
|
213
271
|
|
|
214
|
-
|
|
215
|
-
const
|
|
272
|
+
// Format: yyyy-MM-dd HH:mm:ss (no milliseconds)
|
|
273
|
+
const startStr = start.toISOString().replace("T", " ").replace("Z", "").replace(/\.\d+$/, "");
|
|
274
|
+
const endStr = end.toISOString().replace("T", " ").replace("Z", "").replace(/\.\d+$/, "");
|
|
216
275
|
|
|
217
276
|
const url = `${API_BASE_URL}${USAGE_ENDPOINT}?startTime=${encodeURIComponent(startStr)}&endTime=${encodeURIComponent(endStr)}`;
|
|
218
277
|
|
|
@@ -272,6 +331,33 @@ export class GLMQuotaScraper {
|
|
|
272
331
|
const totalUsage = d.totalUsage ?? {};
|
|
273
332
|
const modelSummary = d.modelSummaryList?.[0];
|
|
274
333
|
|
|
334
|
+
// Calculate 5h and weekly usage from hourly data
|
|
335
|
+
const { h5Tokens, weeklyTokens } = this.calculateHourlyUsage(d.x_time ?? [], d.tokensUsage ?? [], d.modelCallCount ?? []);
|
|
336
|
+
|
|
337
|
+
// GLM Lite plan limits (Legacy Plan V2):
|
|
338
|
+
// Based on browser comparison: 20% weekly = 29.6M tokens
|
|
339
|
+
// So weekly quota ≈ 148M tokens, 5h quota ≈ 30M tokens
|
|
340
|
+
const H5_TOKEN_LIMIT = parseInt(process.env.GLM_5H_TOKEN_LIMIT ?? "30000000", 10); // 30M tokens per 5h
|
|
341
|
+
const WEEKLY_TOKEN_LIMIT = parseInt(process.env.GLM_WEEKLY_TOKEN_LIMIT ?? "148000000", 10); // 148M tokens per week
|
|
342
|
+
|
|
343
|
+
const h5UsedPct = Math.min(100, (h5Tokens / H5_TOKEN_LIMIT) * 100);
|
|
344
|
+
const weeklyUsedPct = Math.min(100, (weeklyTokens / WEEKLY_TOKEN_LIMIT) * 100);
|
|
345
|
+
|
|
346
|
+
// Calculate reset times
|
|
347
|
+
const now = new Date();
|
|
348
|
+
const h5ResetDurationMs = 5 * 60 * 60 * 1000; // 5 hours
|
|
349
|
+
const weeklyResetDurationMs = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
350
|
+
|
|
351
|
+
// Find last usage time to calculate when 5h window will reset
|
|
352
|
+
const lastUsageIdx = this.findLastUsageIndex(d.x_time ?? [], d.tokensUsage ?? []);
|
|
353
|
+
const lastUsageTime = lastUsageIdx >= 0 && d.x_time ? new Date(d.x_time[lastUsageIdx].replace(" ", "T") + ":00") : now;
|
|
354
|
+
const h5ResetAt = new Date(lastUsageTime.getTime() + h5ResetDurationMs);
|
|
355
|
+
const weeklyResetAt = new Date(now.getTime() + weeklyResetDurationMs);
|
|
356
|
+
|
|
357
|
+
// Calculate seconds until reset
|
|
358
|
+
const h5SecondsUntilReset = Math.max(0, Math.floor((h5ResetAt.getTime() - now.getTime()) / 1000));
|
|
359
|
+
const weeklySecondsUntilReset = Math.max(0, Math.floor((weeklyResetAt.getTime() - now.getTime()) / 1000));
|
|
360
|
+
|
|
275
361
|
const result: GLMQuotaData = {
|
|
276
362
|
provider: "glm",
|
|
277
363
|
totalCalls: totalUsage.totalModelCallCount ?? 0,
|
|
@@ -280,15 +366,20 @@ export class GLMQuotaScraper {
|
|
|
280
366
|
hourlyTimestamps: d.x_time ?? [],
|
|
281
367
|
hourlyCalls: d.modelCallCount ?? [],
|
|
282
368
|
hourlyTokens: d.tokensUsage ?? [],
|
|
369
|
+
h5UsedPct,
|
|
370
|
+
h5ResetsAt: formatRemainsSeconds(h5SecondsUntilReset),
|
|
371
|
+
h5ResetsAtEpoch: h5ResetAt.getTime(),
|
|
372
|
+
weeklyUsedPct,
|
|
373
|
+
weeklyResetsAt: formatRemainsSeconds(weeklySecondsUntilReset),
|
|
374
|
+
weeklyResetsAtEpoch: weeklyResetAt.getTime(),
|
|
283
375
|
apiEndpoint: url,
|
|
284
376
|
scrapedAt: new Date().toISOString(),
|
|
285
377
|
};
|
|
286
378
|
|
|
287
379
|
if (!this.config.quiet) {
|
|
288
380
|
console.log(
|
|
289
|
-
`[DEBUG GLMQuotaScraper]
|
|
290
|
-
`
|
|
291
|
-
`Model: ${result.modelName}`,
|
|
381
|
+
`[DEBUG GLMQuotaScraper] 5h: ${h5UsedPct.toFixed(1)}% (${(h5Tokens/1e6).toFixed(1)}M tokens), ` +
|
|
382
|
+
`Week: ${weeklyUsedPct.toFixed(1)}% (${(weeklyTokens/1e6).toFixed(1)}M tokens)`,
|
|
292
383
|
);
|
|
293
384
|
}
|
|
294
385
|
|
package/harness/e2e/index.ts
CHANGED
|
@@ -42,3 +42,32 @@ export type {
|
|
|
42
42
|
PlaywrightRunnerConfig,
|
|
43
43
|
QuotaPageData,
|
|
44
44
|
} from "./playwright-runner.js";
|
|
45
|
+
|
|
46
|
+
// Unified quota management
|
|
47
|
+
export {
|
|
48
|
+
UnifiedQuotaManager,
|
|
49
|
+
createUnifiedQuotaManagerFromEnv,
|
|
50
|
+
type QuotaData,
|
|
51
|
+
type Provider,
|
|
52
|
+
} from "./unified-quota-manager.js";
|
|
53
|
+
|
|
54
|
+
// Individual scrapers for direct access
|
|
55
|
+
export { MiniMaxQuotaManager, type MiniMaxQuotaData } from "./minimax-quota-scraper.js";
|
|
56
|
+
export { GLMQuotaManager, GLMQuotaScraper, type GLMQuotaData, type GLMSubscriptionData, parseGLMErrorResetTime, parseMinimaxOverloadResetTime } from "./glm-quota-scraper.js";
|
|
57
|
+
export { ChatGPTQuotaManager, ChatGPTQuotaScraper, type ChatGPTQuotaData } from "./chatgpt-quota-scraper.js";
|
|
58
|
+
export { OpenAIQuotaManager, OpenAIQuotaScraper, type OpenAIQuotaData } from "./openai-quota-scraper.js";
|
|
59
|
+
|
|
60
|
+
// Browser-based scrapers
|
|
61
|
+
export {
|
|
62
|
+
ChromeProfileManager,
|
|
63
|
+
getChromeProfileManager,
|
|
64
|
+
BaseBrowserScraper,
|
|
65
|
+
CodexBrowserScraper,
|
|
66
|
+
ZaiBrowserScraper,
|
|
67
|
+
scrapeCodexQuota,
|
|
68
|
+
scrapeZaiQuota,
|
|
69
|
+
checkZaiCookies,
|
|
70
|
+
type BrowserScraperConfig,
|
|
71
|
+
type CodexQuotaData,
|
|
72
|
+
type ZaiQuotaData,
|
|
73
|
+
} from "./browser-scrapers/index.js";
|