pi-harness-runtime 0.3.2-beta.2 → 0.4.0
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/CHANGELOG.md +18 -4
- package/README.md +20 -0
- package/harness/auto-compact.ts +326 -0
- package/harness/e2e/index.ts +44 -0
- package/harness/e2e/playwright-runner.ts +92 -53
- package/harness/e2e/test-engine.ts +9 -13
- package/harness/index.ts +105 -0
- package/harness/notification-events.ts +299 -0
- package/harness/output-limit-handler.ts +316 -0
- package/harness/partial-recovery.ts +407 -0
- package/package.json +5 -2
- package/packages/auth/src/minimax-browser-auth.ts +830 -180
- package/packages/auth/src/run-minimax-auth.ts +55 -7
- package/packages/notification/adapters/email-adapter.ts +126 -0
- package/packages/notification/adapters/ntfy-adapter.ts +100 -0
- package/packages/notification/adapters/telegram-adapter.ts +107 -0
- package/packages/notification/adapters/webhook-adapter.ts +75 -0
- package/packages/notification/base-adapter.ts +75 -0
- package/packages/notification/index.ts +51 -0
- package/packages/notification/notification-center.ts +286 -0
- package/packages/notification/types.ts +89 -0
- package/packages/providers/adapters.ts +102 -1
- package/packages/types/src/runtime-types.ts +3 -1
- package/skills/harness-runtime/SKILL.md +5 -1
|
@@ -1,19 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* minimax-browser-auth.ts
|
|
3
3
|
*
|
|
4
|
-
* MiniMax browser authentication using
|
|
4
|
+
* MiniMax browser authentication using a real Chrome profile.
|
|
5
5
|
*
|
|
6
6
|
* SECURITY RULES:
|
|
7
7
|
* - Human owns authentication. Agent never receives credentials.
|
|
8
8
|
* - No username, password, raw cookies, or session tokens stored.
|
|
9
|
-
* -
|
|
9
|
+
* - Real Chrome owns the login flow via a persistent profile.
|
|
10
10
|
* - Profile is stored at ~/.pi-harness-runtime/browser-profiles/minimax/
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import * as fs from "fs";
|
|
14
|
-
import * as path from "path";
|
|
15
|
-
import * as os from "os";
|
|
16
|
-
import
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import * as os from "node:os";
|
|
16
|
+
import * as net from "node:net";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { createRequire } from "node:module";
|
|
19
|
+
import { execFileSync, spawn, type ChildProcess } from "node:child_process";
|
|
20
|
+
import { createInterface } from "node:readline/promises";
|
|
21
|
+
import {
|
|
22
|
+
chromium,
|
|
23
|
+
type Browser,
|
|
24
|
+
type BrowserContext,
|
|
25
|
+
type Page,
|
|
26
|
+
} from "playwright";
|
|
27
|
+
|
|
28
|
+
// ESM __dirname shim (not available in ESM without this)
|
|
29
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
30
|
+
const __dirname = path.dirname(__filename);
|
|
31
|
+
const _require = createRequire(import.meta.url);
|
|
17
32
|
|
|
18
33
|
export interface MinimaxAuthStatus {
|
|
19
34
|
provider: "minimax";
|
|
@@ -22,6 +37,7 @@ export interface MinimaxAuthStatus {
|
|
|
22
37
|
page_url: string;
|
|
23
38
|
detected_text_sample: string | null;
|
|
24
39
|
profile_path: string;
|
|
40
|
+
usage_lines?: string[];
|
|
25
41
|
error_message?: string;
|
|
26
42
|
}
|
|
27
43
|
|
|
@@ -29,6 +45,12 @@ export interface MinimaxBrowserAuthConfig {
|
|
|
29
45
|
profilePath?: string;
|
|
30
46
|
statusPath?: string;
|
|
31
47
|
targetUrl?: string;
|
|
48
|
+
chromeExecutablePath?: string;
|
|
49
|
+
authTimeoutMs?: number;
|
|
50
|
+
headless?: boolean;
|
|
51
|
+
cdpPort?: number;
|
|
52
|
+
/** Override live session detection — used by tests to bypass real daemon. */
|
|
53
|
+
forceNoLiveSession?: boolean;
|
|
32
54
|
}
|
|
33
55
|
|
|
34
56
|
const DEFAULT_USAGE_KEYWORDS = [
|
|
@@ -58,6 +80,19 @@ export function getStatusPath(): string {
|
|
|
58
80
|
return path.join(getRuntimeDir(), "auth", "minimax-auth-status.json");
|
|
59
81
|
}
|
|
60
82
|
|
|
83
|
+
export interface MinimaxLiveBrowserSession {
|
|
84
|
+
profile_path: string;
|
|
85
|
+
target_url: string;
|
|
86
|
+
chrome_path: string;
|
|
87
|
+
debugging_port: number;
|
|
88
|
+
pid: number;
|
|
89
|
+
started_at: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function getLiveSessionPath(): string {
|
|
93
|
+
return path.join(getRuntimeDir(), "auth", "minimax-live-browser.json");
|
|
94
|
+
}
|
|
95
|
+
|
|
61
96
|
function ensureDirs(config: MinimaxBrowserAuthConfig): void {
|
|
62
97
|
const profileDir = config.profilePath ?? getProfileDir();
|
|
63
98
|
const statusPath = config.statusPath ?? getStatusPath();
|
|
@@ -74,6 +109,52 @@ export function saveAuthStatus(
|
|
|
74
109
|
fs.writeFileSync(statusPath, JSON.stringify(status, null, 2));
|
|
75
110
|
}
|
|
76
111
|
|
|
112
|
+
function loadSavedAuthStatus(
|
|
113
|
+
config?: MinimaxBrowserAuthConfig,
|
|
114
|
+
): MinimaxAuthStatus | null {
|
|
115
|
+
const statusPath = config?.statusPath ?? getStatusPath();
|
|
116
|
+
if (!fs.existsSync(statusPath)) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
return JSON.parse(
|
|
121
|
+
fs.readFileSync(statusPath, "utf-8"),
|
|
122
|
+
) as MinimaxAuthStatus;
|
|
123
|
+
} catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function saveLiveBrowserSession(
|
|
129
|
+
session: MinimaxLiveBrowserSession,
|
|
130
|
+
liveSessionPath?: string,
|
|
131
|
+
): void {
|
|
132
|
+
const p = liveSessionPath ?? getLiveSessionPath();
|
|
133
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
134
|
+
fs.writeFileSync(p, JSON.stringify(session, null, 2));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function loadLiveBrowserSession(
|
|
138
|
+
liveSessionPath?: string,
|
|
139
|
+
): MinimaxLiveBrowserSession | null {
|
|
140
|
+
const p = liveSessionPath ?? getLiveSessionPath();
|
|
141
|
+
if (!fs.existsSync(p)) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
return JSON.parse(fs.readFileSync(p, "utf-8")) as MinimaxLiveBrowserSession;
|
|
146
|
+
} catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function clearLiveBrowserSession(liveSessionPath?: string): void {
|
|
152
|
+
const p = liveSessionPath ?? getLiveSessionPath();
|
|
153
|
+
if (fs.existsSync(p)) {
|
|
154
|
+
fs.rmSync(p, { force: true });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
77
158
|
export function detectUsagePage(bodyText: string): {
|
|
78
159
|
detected: boolean;
|
|
79
160
|
sample: string | null;
|
|
@@ -88,18 +169,222 @@ export function detectUsagePage(bodyText: string): {
|
|
|
88
169
|
return { detected, sample };
|
|
89
170
|
}
|
|
90
171
|
|
|
172
|
+
const DEFAULT_AUTH_TIMEOUT_MS = 300000;
|
|
173
|
+
|
|
174
|
+
function delay(ms: number): Promise<void> {
|
|
175
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function hasProfileData(profileDir: string): boolean {
|
|
179
|
+
if (!fs.existsSync(profileDir)) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
const markers = [
|
|
183
|
+
path.join(profileDir, "Local State"),
|
|
184
|
+
path.join(profileDir, "First Run"),
|
|
185
|
+
path.join(profileDir, "Default", "Preferences"),
|
|
186
|
+
path.join(profileDir, "Default", "Cookies"),
|
|
187
|
+
path.join(profileDir, "Default", "History"),
|
|
188
|
+
];
|
|
189
|
+
if (markers.some((marker) => fs.existsSync(marker))) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
return fs.readdirSync(profileDir).length > 0;
|
|
194
|
+
} catch {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function getChromeExecutablePath(
|
|
200
|
+
config: MinimaxBrowserAuthConfig = {},
|
|
201
|
+
): string {
|
|
202
|
+
const configuredPath =
|
|
203
|
+
config.chromeExecutablePath ??
|
|
204
|
+
process.env.PI_HARNESS_CHROME_PATH ??
|
|
205
|
+
process.env.GOOGLE_CHROME_BIN ??
|
|
206
|
+
process.env.CHROME_PATH;
|
|
207
|
+
const candidates = configuredPath ? [configuredPath] : [];
|
|
208
|
+
switch (process.platform) {
|
|
209
|
+
case "darwin":
|
|
210
|
+
candidates.push(
|
|
211
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
212
|
+
"/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta",
|
|
213
|
+
);
|
|
214
|
+
break;
|
|
215
|
+
case "win32":
|
|
216
|
+
candidates.push(
|
|
217
|
+
"C:Program FilesGoogleChromeApplicationchrome.exe",
|
|
218
|
+
"C:Program Files (x86)GoogleChromeApplicationchrome.exe",
|
|
219
|
+
path.join(
|
|
220
|
+
process.env.LOCALAPPDATA ?? "",
|
|
221
|
+
"Google",
|
|
222
|
+
"Chrome",
|
|
223
|
+
"Application",
|
|
224
|
+
"chrome.exe",
|
|
225
|
+
),
|
|
226
|
+
);
|
|
227
|
+
break;
|
|
228
|
+
default:
|
|
229
|
+
candidates.push(
|
|
230
|
+
"/usr/bin/google-chrome",
|
|
231
|
+
"/usr/bin/google-chrome-stable",
|
|
232
|
+
"/snap/bin/google-chrome",
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
for (const candidate of candidates) {
|
|
236
|
+
if (candidate && fs.existsSync(candidate)) {
|
|
237
|
+
return candidate;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
throw new Error(
|
|
241
|
+
"Google Chrome executable not found. Install Google Chrome or set PI_HARNESS_CHROME_PATH.",
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function getChromeArgs(): string[] {
|
|
246
|
+
const args = ["--no-first-run", "--no-default-browser-check"];
|
|
247
|
+
if (process.platform === "linux") {
|
|
248
|
+
args.push("--password-store=basic");
|
|
249
|
+
}
|
|
250
|
+
return args;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function getAuthTimeoutMs(config: MinimaxBrowserAuthConfig): number {
|
|
254
|
+
const configuredTimeout =
|
|
255
|
+
config.authTimeoutMs ??
|
|
256
|
+
(process.env.PI_HARNESS_AUTH_TIMEOUT_MS
|
|
257
|
+
? Number(process.env.PI_HARNESS_AUTH_TIMEOUT_MS)
|
|
258
|
+
: undefined);
|
|
259
|
+
if (
|
|
260
|
+
typeof configuredTimeout === "number" &&
|
|
261
|
+
Number.isFinite(configuredTimeout) &&
|
|
262
|
+
configuredTimeout > 0
|
|
263
|
+
) {
|
|
264
|
+
return configuredTimeout;
|
|
265
|
+
}
|
|
266
|
+
return DEFAULT_AUTH_TIMEOUT_MS;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function getFreePort(preferredPort?: number): Promise<number> {
|
|
270
|
+
if (preferredPort) return preferredPort;
|
|
271
|
+
return new Promise((resolve, reject) => {
|
|
272
|
+
const server = net.createServer();
|
|
273
|
+
server.unref();
|
|
274
|
+
server.on("error", reject);
|
|
275
|
+
server.listen(0, "127.0.0.1", () => {
|
|
276
|
+
const address = server.address();
|
|
277
|
+
if (!address || typeof address === "string") {
|
|
278
|
+
server.close();
|
|
279
|
+
reject(new Error("Could not allocate a Chrome debugging port"));
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const { port } = address;
|
|
283
|
+
server.close((closeError) => {
|
|
284
|
+
if (closeError) {
|
|
285
|
+
reject(closeError);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
resolve(port);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function connectToChromeOverCdp(
|
|
295
|
+
port: number,
|
|
296
|
+
timeoutMs = 15000,
|
|
297
|
+
): Promise<Browser> {
|
|
298
|
+
const endpoint = `http://127.0.0.1:${port}`;
|
|
299
|
+
const deadline = Date.now() + timeoutMs;
|
|
300
|
+
let lastError: unknown;
|
|
301
|
+
while (Date.now() < deadline) {
|
|
302
|
+
try {
|
|
303
|
+
return await chromium.connectOverCDP(endpoint);
|
|
304
|
+
} catch (error) {
|
|
305
|
+
lastError = error;
|
|
306
|
+
await delay(500);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
throw new Error(
|
|
310
|
+
`Could not connect to Chrome DevTools at ${endpoint}: ${String(lastError)}`,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function getCurrentPage(context: BrowserContext): Page | null {
|
|
315
|
+
const pages = context.pages().filter((p) => !p.isClosed());
|
|
316
|
+
return pages.length > 0 ? pages[pages.length - 1] : null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function waitForPage(
|
|
320
|
+
context: BrowserContext,
|
|
321
|
+
timeoutMs = 15000,
|
|
322
|
+
): Promise<Page> {
|
|
323
|
+
const deadline = Date.now() + timeoutMs;
|
|
324
|
+
while (Date.now() < deadline) {
|
|
325
|
+
const page = getCurrentPage(context);
|
|
326
|
+
if (page) return page;
|
|
327
|
+
await delay(250);
|
|
328
|
+
}
|
|
329
|
+
throw new Error("Chrome launched but no page became available");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function closeBrowserResources(
|
|
333
|
+
browser: Browser | null,
|
|
334
|
+
chromeProcess: ChildProcess | null,
|
|
335
|
+
): Promise<void> {
|
|
336
|
+
if (browser) await browser.close().catch(() => {});
|
|
337
|
+
if (
|
|
338
|
+
chromeProcess &&
|
|
339
|
+
chromeProcess.exitCode === null &&
|
|
340
|
+
!chromeProcess.killed
|
|
341
|
+
) {
|
|
342
|
+
chromeProcess.kill("SIGTERM");
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function launchChromeForAuthentication(
|
|
347
|
+
profileDir: string,
|
|
348
|
+
targetUrl: string,
|
|
349
|
+
config: MinimaxBrowserAuthConfig,
|
|
350
|
+
): Promise<{
|
|
351
|
+
browser: Awaited<ReturnType<typeof connectToChromeOverCdp>>;
|
|
352
|
+
chromeProcess: ChildProcess;
|
|
353
|
+
chromePath: string;
|
|
354
|
+
cdpPort: number;
|
|
355
|
+
}> {
|
|
356
|
+
const chromePath = getChromeExecutablePath(config);
|
|
357
|
+
const cdpPort = await getFreePort(config.cdpPort);
|
|
358
|
+
const chromeArgs = [
|
|
359
|
+
`--remote-debugging-port=${cdpPort}`,
|
|
360
|
+
`--user-data-dir=${profileDir}`,
|
|
361
|
+
"--new-window",
|
|
362
|
+
...getChromeArgs(),
|
|
363
|
+
targetUrl,
|
|
364
|
+
];
|
|
365
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
366
|
+
const chromeProcess = spawn(chromePath, chromeArgs, { stdio: "ignore" });
|
|
367
|
+
const browser = await connectToChromeOverCdp(cdpPort);
|
|
368
|
+
const context = browser.contexts()[0] as BrowserContext;
|
|
369
|
+
if (!context) {
|
|
370
|
+
await closeBrowserResources(browser, chromeProcess);
|
|
371
|
+
throw new Error(
|
|
372
|
+
"Connected to Chrome, but no browser context was available",
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
return { browser, chromeProcess, chromePath, cdpPort };
|
|
376
|
+
}
|
|
377
|
+
|
|
91
378
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
379
|
+
* Manual authentication flow: launches real Chrome so Google login works,
|
|
380
|
+
* waits for user to sign in, then confirms at the TTY.
|
|
94
381
|
*/
|
|
95
382
|
export async function authenticateWithPersistentBrowser(
|
|
96
383
|
config: MinimaxBrowserAuthConfig = {},
|
|
97
384
|
): Promise<MinimaxAuthStatus> {
|
|
98
385
|
const profileDir = config.profilePath ?? getProfileDir();
|
|
99
|
-
const statusPath = config.statusPath ?? getStatusPath();
|
|
100
386
|
const targetUrl =
|
|
101
387
|
config.targetUrl ?? "https://platform.minimax.io/console/usage";
|
|
102
|
-
|
|
103
388
|
ensureDirs(config);
|
|
104
389
|
|
|
105
390
|
console.log("=".repeat(60));
|
|
@@ -109,127 +394,146 @@ export async function authenticateWithPersistentBrowser(
|
|
|
109
394
|
console.log("SECURITY: Human owns authentication.");
|
|
110
395
|
console.log("- Login once, profile is saved for reuse");
|
|
111
396
|
console.log("");
|
|
112
|
-
console.log(
|
|
397
|
+
console.log("Profile directory: " + profileDir);
|
|
113
398
|
console.log("");
|
|
114
399
|
|
|
115
|
-
let
|
|
400
|
+
let browser: Browser | null = null;
|
|
401
|
+
let chromeProcess: ChildProcess | null = null;
|
|
116
402
|
|
|
117
403
|
try {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
// Launch persistent context - profile auto-saves!
|
|
122
|
-
console.log("🚀 Launching persistent browser...");
|
|
404
|
+
const isFirstRun = !hasProfileData(profileDir);
|
|
405
|
+
console.log("🚀 Launching real Google Chrome...");
|
|
123
406
|
if (isFirstRun) {
|
|
124
|
-
console.log(" First run - creating new profile");
|
|
407
|
+
console.log(" First run - creating new Chrome profile");
|
|
125
408
|
} else {
|
|
126
|
-
console.log(" Reusing existing profile
|
|
409
|
+
console.log(" Reusing existing Chrome profile");
|
|
127
410
|
}
|
|
128
|
-
console.log("");
|
|
129
411
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
412
|
+
const launched = await launchChromeForAuthentication(
|
|
413
|
+
profileDir,
|
|
414
|
+
targetUrl,
|
|
415
|
+
config,
|
|
416
|
+
);
|
|
417
|
+
browser = launched.browser;
|
|
418
|
+
chromeProcess = launched.chromeProcess;
|
|
419
|
+
const context = browser.contexts()[0];
|
|
420
|
+
console.log(" Chrome: " + launched.chromePath);
|
|
421
|
+
console.log(" DevTools port: " + launched.cdpPort);
|
|
422
|
+
console.log("");
|
|
423
|
+
console.log("🖥️ Waiting for MiniMax / Google login flow...");
|
|
136
424
|
|
|
137
|
-
|
|
425
|
+
let page = await waitForPage(context, 15000);
|
|
426
|
+
await page
|
|
427
|
+
.waitForLoadState("domcontentloaded", { timeout: 15000 })
|
|
428
|
+
.catch(() => {});
|
|
138
429
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
});
|
|
145
|
-
await page.waitForTimeout(2000);
|
|
430
|
+
let loginComplete = false;
|
|
431
|
+
let lastLoggedUrl = "";
|
|
432
|
+
let loginHintShown = false;
|
|
433
|
+
const startTime = Date.now();
|
|
434
|
+
const authTimeoutMs = getAuthTimeoutMs(config);
|
|
146
435
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
436
|
+
while (Date.now() - startTime < authTimeoutMs) {
|
|
437
|
+
page = getCurrentPage(context) ?? page;
|
|
438
|
+
if (!page || page.isClosed()) {
|
|
439
|
+
console.log("Browser closed by user.");
|
|
440
|
+
const status: MinimaxAuthStatus = {
|
|
441
|
+
provider: "minimax",
|
|
442
|
+
authenticated: false,
|
|
443
|
+
checked_at: new Date().toISOString(),
|
|
444
|
+
page_url: "",
|
|
445
|
+
detected_text_sample: null,
|
|
446
|
+
profile_path: profileDir,
|
|
447
|
+
error_message: "Browser closed by user",
|
|
448
|
+
};
|
|
449
|
+
saveAuthStatus(status, config);
|
|
450
|
+
return status;
|
|
451
|
+
}
|
|
150
452
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
console.log(" (Profile will auto-save when you navigate)");
|
|
157
|
-
console.log("");
|
|
158
|
-
console.log("⏳ Waiting for you to log in and navigate to usage page...");
|
|
159
|
-
console.log(" Press Enter when done, or wait 60 seconds...");
|
|
160
|
-
console.log("");
|
|
453
|
+
const currentUrl = page.url();
|
|
454
|
+
if (currentUrl && currentUrl !== lastLoggedUrl) {
|
|
455
|
+
console.log(" 🔗 URL: " + currentUrl);
|
|
456
|
+
lastLoggedUrl = currentUrl;
|
|
457
|
+
}
|
|
161
458
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
resolve();
|
|
174
|
-
}, 60000),
|
|
175
|
-
),
|
|
176
|
-
]);
|
|
177
|
-
}
|
|
459
|
+
const isGoogleLogin = currentUrl.includes("accounts.google.com");
|
|
460
|
+
const isMiniMaxLogin =
|
|
461
|
+
currentUrl.includes("unified-login") || currentUrl.includes("login");
|
|
462
|
+
if ((isGoogleLogin || isMiniMaxLogin) && !loginHintShown) {
|
|
463
|
+
console.log("");
|
|
464
|
+
console.log("🔓 Sign in in the Chrome window that just opened");
|
|
465
|
+
console.log(" This uses real Chrome so Google login is allowed");
|
|
466
|
+
console.log(" Close browser or press Ctrl+C to cancel");
|
|
467
|
+
console.log("");
|
|
468
|
+
loginHintShown = true;
|
|
469
|
+
}
|
|
178
470
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
471
|
+
const bodyText = (await page.textContent("body").catch(() => "")) ?? "";
|
|
472
|
+
const { detected } = detectUsagePage(bodyText);
|
|
473
|
+
if (detected) {
|
|
474
|
+
loginComplete = true;
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
182
477
|
|
|
183
|
-
|
|
184
|
-
|
|
478
|
+
await delay(1500);
|
|
479
|
+
}
|
|
185
480
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
481
|
+
if (!loginComplete) {
|
|
482
|
+
console.log("");
|
|
483
|
+
console.log("⏰ Browser stayed open after timeout. Please close it.");
|
|
484
|
+
console.log("");
|
|
485
|
+
await waitForChromeToExit(chromeProcess);
|
|
486
|
+
const status: MinimaxAuthStatus = {
|
|
487
|
+
provider: "minimax",
|
|
488
|
+
authenticated: false,
|
|
489
|
+
checked_at: new Date().toISOString(),
|
|
490
|
+
page_url: "",
|
|
491
|
+
detected_text_sample: null,
|
|
492
|
+
profile_path: profileDir,
|
|
493
|
+
error_message: "Login not completed within timeout",
|
|
494
|
+
};
|
|
495
|
+
saveAuthStatus(status, config);
|
|
496
|
+
browser = null;
|
|
497
|
+
chromeProcess = null;
|
|
498
|
+
return status;
|
|
499
|
+
}
|
|
189
500
|
|
|
501
|
+
// Usage page detected — wait for human to close the browser
|
|
190
502
|
console.log("");
|
|
191
|
-
console.log(
|
|
192
|
-
console.log(
|
|
193
|
-
if (sample) {
|
|
194
|
-
console.log(` Sample: ${sample.substring(0, 100)}...`);
|
|
195
|
-
}
|
|
503
|
+
console.log("✅ Usage page reached!");
|
|
504
|
+
console.log("💾 Profile is being saved automatically.");
|
|
196
505
|
console.log("");
|
|
197
|
-
|
|
198
|
-
// Profile is auto-saved by Playwright - no manual save needed
|
|
199
|
-
console.log("💾 Profile auto-saved to:");
|
|
200
|
-
console.log(` ${profileDir}`);
|
|
506
|
+
console.log("⚠️ Please close the Chrome window now.");
|
|
201
507
|
console.log("");
|
|
508
|
+
await waitForChromeToExit(chromeProcess);
|
|
202
509
|
|
|
510
|
+
const confirmed = await confirmUsagePageReached(30000);
|
|
203
511
|
const status: MinimaxAuthStatus = {
|
|
204
512
|
provider: "minimax",
|
|
205
|
-
authenticated:
|
|
513
|
+
authenticated: confirmed,
|
|
206
514
|
checked_at: new Date().toISOString(),
|
|
207
|
-
page_url:
|
|
208
|
-
detected_text_sample:
|
|
515
|
+
page_url: targetUrl,
|
|
516
|
+
detected_text_sample: confirmed ? "Usage page confirmed by user" : null,
|
|
209
517
|
profile_path: profileDir,
|
|
210
518
|
};
|
|
211
|
-
|
|
212
519
|
saveAuthStatus(status, config);
|
|
213
520
|
|
|
214
|
-
|
|
215
|
-
|
|
521
|
+
console.log("");
|
|
522
|
+
if (confirmed) {
|
|
523
|
+
console.log(
|
|
524
|
+
"✅ Authentication saved! Future scrapes will use this profile.",
|
|
525
|
+
);
|
|
216
526
|
} else {
|
|
217
|
-
console.log("⚠️
|
|
527
|
+
console.log("⚠️ Auth not confirmed. Run 'auth' again after login.");
|
|
218
528
|
}
|
|
219
529
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
context = null;
|
|
223
|
-
|
|
530
|
+
browser = null;
|
|
531
|
+
chromeProcess = null;
|
|
224
532
|
return status;
|
|
225
533
|
} catch (error) {
|
|
226
534
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
227
|
-
console.error(
|
|
228
|
-
|
|
229
|
-
if (context) {
|
|
230
|
-
await context.close().catch(() => {});
|
|
231
|
-
}
|
|
232
|
-
|
|
535
|
+
console.error("❌ Error: " + errorMessage);
|
|
536
|
+
await closeBrowserResources(browser, chromeProcess);
|
|
233
537
|
const status: MinimaxAuthStatus = {
|
|
234
538
|
provider: "minimax",
|
|
235
539
|
authenticated: false,
|
|
@@ -239,32 +543,394 @@ export async function authenticateWithPersistentBrowser(
|
|
|
239
543
|
profile_path: profileDir,
|
|
240
544
|
error_message: errorMessage,
|
|
241
545
|
};
|
|
546
|
+
saveAuthStatus(status, config);
|
|
547
|
+
return status;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async function waitForChromeToExit(chromeProcess: ChildProcess): Promise<void> {
|
|
552
|
+
return new Promise((resolve) => {
|
|
553
|
+
// macOS: helper processes keep chrome running even after window closes
|
|
554
|
+
// Use process group to kill all related processes
|
|
555
|
+
const timeout = setTimeout(() => {
|
|
556
|
+
console.log(
|
|
557
|
+
" (timeout waiting for Chrome exit — killing process group)",
|
|
558
|
+
);
|
|
559
|
+
try {
|
|
560
|
+
process.kill(chromeProcess.pid!, "SIGTERM");
|
|
561
|
+
} catch (_e) {
|
|
562
|
+
/* ignore */
|
|
563
|
+
}
|
|
564
|
+
resolve();
|
|
565
|
+
}, 15000);
|
|
566
|
+
|
|
567
|
+
chromeProcess.once("exit", () => {
|
|
568
|
+
clearTimeout(timeout);
|
|
569
|
+
resolve();
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
// Also resolve on Enter (TTY fallback)
|
|
573
|
+
if (process.stdin.isTTY) {
|
|
574
|
+
console.log(" Press ENTER after closing Chrome...");
|
|
575
|
+
process.stdin.once("data", () => {
|
|
576
|
+
clearTimeout(timeout);
|
|
577
|
+
resolve();
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async function confirmUsagePageReached(_timeoutMs = 30000): Promise<boolean> {
|
|
584
|
+
if (!process.stdin.isTTY) {
|
|
585
|
+
console.log("(non-TTY: auto-confirming)");
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
589
|
+
const answer = await rl.question(
|
|
590
|
+
"✅ Did you reach the MiniMax usage page? (y/N): ",
|
|
591
|
+
);
|
|
592
|
+
rl.close();
|
|
593
|
+
return answer.trim().toLowerCase() === "y";
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Extract usage-relevant lines from page body text.
|
|
598
|
+
* Splits on newlines AND HTML tag boundaries, then filters for keywords.
|
|
599
|
+
*/
|
|
600
|
+
export function extractUsageLines(bodyText: string): string[] {
|
|
601
|
+
// Strip <script> JSON noise first
|
|
602
|
+
let clean = bodyText.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
|
|
603
|
+
clean = clean.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
|
|
604
|
+
clean = clean.replace(/\{\s*"[^{}]*\}/g, "");
|
|
605
|
+
|
|
606
|
+
// Split on both newlines and HTML tag boundaries
|
|
607
|
+
const htmlTagRe =
|
|
608
|
+
/\n|<div[^>]*>|<\/div>|<span[^>]*>|<\/span>|<p[^>]*>|<\/p>|<br\s*\/?>|\s{2,}/gi;
|
|
609
|
+
// Replace tag matches with spaces so adjacent text doesn't concatenate
|
|
610
|
+
clean = clean.replace(htmlTagRe, " ");
|
|
611
|
+
const parts = clean.split(/\s+/);
|
|
242
612
|
|
|
613
|
+
const lines: string[] = [];
|
|
614
|
+
const keywords = DEFAULT_USAGE_KEYWORDS.map((k) => k.toLowerCase());
|
|
615
|
+
|
|
616
|
+
for (const part of parts) {
|
|
617
|
+
const trimmed = part.trim();
|
|
618
|
+
if (trimmed.length < 3 || trimmed.length > 200) continue;
|
|
619
|
+
const lower = trimmed.toLowerCase();
|
|
620
|
+
const hasKeyword = keywords.some((k) => lower.includes(k));
|
|
621
|
+
const hasNumber = /\d/.test(trimmed);
|
|
622
|
+
const hasPercent = /%|\d+\/\d+/.test(trimmed);
|
|
623
|
+
if (hasKeyword || (hasNumber && hasPercent)) {
|
|
624
|
+
// Deduplicate
|
|
625
|
+
const normalized = trimmed.replace(/\s+/g, " ");
|
|
626
|
+
if (!lines.includes(normalized)) {
|
|
627
|
+
lines.push(normalized);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return lines;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Scrape usage data by attaching to a live browser session (via Node CDP).
|
|
637
|
+
* Writes attach script to a temp .js file to avoid shell escaping issues.
|
|
638
|
+
*/
|
|
639
|
+
export async function scrapeViaLiveBrowserSession(
|
|
640
|
+
config: MinimaxBrowserAuthConfig = {},
|
|
641
|
+
): Promise<MinimaxAuthStatus> {
|
|
642
|
+
const liveSession = loadLiveBrowserSession();
|
|
643
|
+
if (!liveSession) {
|
|
644
|
+
return {
|
|
645
|
+
provider: "minimax",
|
|
646
|
+
authenticated: false,
|
|
647
|
+
checked_at: new Date().toISOString(),
|
|
648
|
+
page_url: "",
|
|
649
|
+
detected_text_sample: null,
|
|
650
|
+
profile_path: config.profilePath ?? getProfileDir(),
|
|
651
|
+
error_message: "No live browser session. Run 'open' first.",
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const profileDir = liveSession.profile_path;
|
|
656
|
+
const targetUrl = "https://platform.minimax.io/console/usage";
|
|
657
|
+
const port = liveSession.debugging_port;
|
|
658
|
+
|
|
659
|
+
console.log("Attaching to live Chrome session (port " + port + ")...");
|
|
660
|
+
console.log("Profile: " + profileDir);
|
|
661
|
+
|
|
662
|
+
// Resolve playwright from the project root so Node can find it in the temp script.
|
|
663
|
+
// Walk up from this file's location to find the project root (has node_modules).
|
|
664
|
+
let projectRoot = __dirname;
|
|
665
|
+
while (
|
|
666
|
+
projectRoot !== "/" &&
|
|
667
|
+
projectRoot !== "" &&
|
|
668
|
+
!fs.existsSync(path.join(projectRoot, "node_modules", "playwright"))
|
|
669
|
+
) {
|
|
670
|
+
projectRoot = path.dirname(projectRoot);
|
|
671
|
+
}
|
|
672
|
+
if (!fs.existsSync(path.join(projectRoot, "node_modules", "playwright"))) {
|
|
673
|
+
// Fallback: try CWD
|
|
674
|
+
projectRoot = process.cwd();
|
|
675
|
+
}
|
|
676
|
+
const playwrightPath = _require.resolve("playwright", {
|
|
677
|
+
paths: [projectRoot],
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
// Build attach script as a plain string (no template literals with regex)
|
|
681
|
+
const scriptLines = [
|
|
682
|
+
"const { chromium } = require('" +
|
|
683
|
+
playwrightPath.replace(/\\/g, "\\\\") +
|
|
684
|
+
"');",
|
|
685
|
+
"(async () => {",
|
|
686
|
+
" const browser = await chromium.connectOverCDP('http://127.0.0.1:" +
|
|
687
|
+
port +
|
|
688
|
+
"');",
|
|
689
|
+
" const context = browser.contexts()[0];",
|
|
690
|
+
" if (!context) { console.error('NO_CONTEXT'); process.exit(1); }",
|
|
691
|
+
" const pages = context.pages().filter(p => !p.isClosed());",
|
|
692
|
+
" const page = pages.length > 0 ? pages[pages.length - 1] : await context.newPage();",
|
|
693
|
+
" await page.goto('" +
|
|
694
|
+
targetUrl +
|
|
695
|
+
"', { waitUntil: 'domcontentloaded', timeout: 30000 });",
|
|
696
|
+
" await page.waitForTimeout(3000);",
|
|
697
|
+
" const url = page.url();",
|
|
698
|
+
" const bodyText = await page.textContent('body').catch(() => '');",
|
|
699
|
+
" const result = JSON.stringify({ url, bodyText });",
|
|
700
|
+
" console.log('RESULT:' + result);",
|
|
701
|
+
" await browser.close();",
|
|
702
|
+
"})().catch(e => { console.error('ERROR:' + e.message); process.exit(1); });",
|
|
703
|
+
];
|
|
704
|
+
const scriptContent = scriptLines.join("\n");
|
|
705
|
+
|
|
706
|
+
// Write to temp file to avoid shell escaping issues
|
|
707
|
+
const tmpDir = os.tmpdir();
|
|
708
|
+
const scriptPath = path.join(
|
|
709
|
+
tmpDir,
|
|
710
|
+
"pi-harness-minimax-attach-" + Date.now() + ".js",
|
|
711
|
+
);
|
|
712
|
+
fs.writeFileSync(scriptPath, scriptContent);
|
|
713
|
+
|
|
714
|
+
try {
|
|
715
|
+
const raw = execFileSync("node", [scriptPath], {
|
|
716
|
+
encoding: "utf-8",
|
|
717
|
+
timeout: 60000,
|
|
718
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
719
|
+
}).trim();
|
|
720
|
+
|
|
721
|
+
fs.unlinkSync(scriptPath);
|
|
722
|
+
|
|
723
|
+
const resultMatch = raw.match(/RESULT:(.+)/s);
|
|
724
|
+
if (!resultMatch) {
|
|
725
|
+
throw new Error("No RESULT from attach script: " + raw.substring(0, 200));
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const parsed = JSON.parse(resultMatch[1]);
|
|
729
|
+
const bodyText: string = parsed.bodyText ?? "";
|
|
730
|
+
const url: string = parsed.url ?? "";
|
|
731
|
+
const { detected, sample } = detectUsagePage(bodyText);
|
|
732
|
+
const usage_lines = extractUsageLines(bodyText);
|
|
733
|
+
|
|
734
|
+
console.log("URL: " + url);
|
|
735
|
+
console.log("Detected usage page: " + detected);
|
|
736
|
+
if (usage_lines.length > 0) {
|
|
737
|
+
console.log("Usage lines (" + usage_lines.length + "):");
|
|
738
|
+
usage_lines.slice(0, 20).forEach((l) => console.log(" " + l));
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const status: MinimaxAuthStatus = {
|
|
742
|
+
provider: "minimax",
|
|
743
|
+
authenticated: detected,
|
|
744
|
+
checked_at: new Date().toISOString(),
|
|
745
|
+
page_url: url,
|
|
746
|
+
detected_text_sample: sample,
|
|
747
|
+
profile_path: profileDir,
|
|
748
|
+
usage_lines,
|
|
749
|
+
};
|
|
243
750
|
saveAuthStatus(status, config);
|
|
244
751
|
return status;
|
|
752
|
+
} catch (error) {
|
|
753
|
+
try {
|
|
754
|
+
fs.unlinkSync(scriptPath);
|
|
755
|
+
} catch (_e) {
|
|
756
|
+
/* ignore */
|
|
757
|
+
}
|
|
758
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
759
|
+
console.error("Attach failed: " + errorMessage);
|
|
760
|
+
return {
|
|
761
|
+
provider: "minimax",
|
|
762
|
+
authenticated: false,
|
|
763
|
+
checked_at: new Date().toISOString(),
|
|
764
|
+
page_url: "",
|
|
765
|
+
detected_text_sample: null,
|
|
766
|
+
profile_path: profileDir,
|
|
767
|
+
error_message: "Live attach failed: " + errorMessage,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Start a persistent browser daemon (keep-open mode).
|
|
774
|
+
* Launches real Chrome with --remote-debugging-port and saves session.
|
|
775
|
+
*/
|
|
776
|
+
export async function startPersistentBrowserDaemon(
|
|
777
|
+
config: MinimaxBrowserAuthConfig = {},
|
|
778
|
+
): Promise<MinimaxLiveBrowserSession> {
|
|
779
|
+
const profileDir = config.profilePath ?? getProfileDir();
|
|
780
|
+
const targetUrl =
|
|
781
|
+
config.targetUrl ?? "https://platform.minimax.io/console/usage";
|
|
782
|
+
ensureDirs(config);
|
|
783
|
+
|
|
784
|
+
// Kill any existing daemon for this profile
|
|
785
|
+
await stopPersistentBrowserDaemon(config);
|
|
786
|
+
|
|
787
|
+
const chromePath = getChromeExecutablePath(config);
|
|
788
|
+
const cdpPort = await getFreePort(config.cdpPort ?? 9222);
|
|
789
|
+
const chromeArgs = [
|
|
790
|
+
"--remote-debugging-port=" + cdpPort,
|
|
791
|
+
"--user-data-dir=" + profileDir,
|
|
792
|
+
"--new-window",
|
|
793
|
+
...getChromeArgs(),
|
|
794
|
+
targetUrl,
|
|
795
|
+
];
|
|
796
|
+
|
|
797
|
+
console.log("Launching persistent Chrome daemon...");
|
|
798
|
+
console.log(" Chrome: " + chromePath);
|
|
799
|
+
console.log(" Port: " + cdpPort);
|
|
800
|
+
console.log(" Profile: " + profileDir);
|
|
801
|
+
|
|
802
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
803
|
+
const chromeProcess = spawn(chromePath, chromeArgs, {
|
|
804
|
+
stdio: "ignore",
|
|
805
|
+
detached: true,
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
const session: MinimaxLiveBrowserSession = {
|
|
809
|
+
profile_path: profileDir,
|
|
810
|
+
target_url: targetUrl,
|
|
811
|
+
chrome_path: chromePath,
|
|
812
|
+
debugging_port: cdpPort,
|
|
813
|
+
pid: chromeProcess.pid!,
|
|
814
|
+
started_at: new Date().toISOString(),
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
saveLiveBrowserSession(session);
|
|
818
|
+
console.log("Live session saved. PID: " + chromeProcess.pid);
|
|
819
|
+
return session;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Stop the persistent browser daemon.
|
|
824
|
+
*/
|
|
825
|
+
export async function stopPersistentBrowserDaemon(
|
|
826
|
+
config: MinimaxBrowserAuthConfig = {},
|
|
827
|
+
): Promise<boolean> {
|
|
828
|
+
const liveSession = loadLiveBrowserSession(getLiveSessionPath());
|
|
829
|
+
if (!liveSession) return false;
|
|
830
|
+
|
|
831
|
+
console.log("Stopping live Chrome session (PID " + liveSession.pid + ")...");
|
|
832
|
+
const killed: string[] = [];
|
|
833
|
+
|
|
834
|
+
// Try process group kill
|
|
835
|
+
try {
|
|
836
|
+
process.kill(-liveSession.pid, "SIGTERM");
|
|
837
|
+
killed.push("process group " + liveSession.pid);
|
|
838
|
+
} catch (_e) {
|
|
839
|
+
/* ignore */
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Try direct PID
|
|
843
|
+
try {
|
|
844
|
+
process.kill(liveSession.pid, "SIGTERM");
|
|
845
|
+
killed.push("PID " + liveSession.pid);
|
|
846
|
+
} catch (_e) {
|
|
847
|
+
/* ignore */
|
|
245
848
|
}
|
|
849
|
+
|
|
850
|
+
// Kill by profile path (macOS helper processes)
|
|
851
|
+
try {
|
|
852
|
+
const out = execFileSync(
|
|
853
|
+
"pgrep",
|
|
854
|
+
["-f", "user-data-dir=" + liveSession.profile_path],
|
|
855
|
+
{ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] },
|
|
856
|
+
);
|
|
857
|
+
for (const pid of out.trim().split("\n")) {
|
|
858
|
+
try {
|
|
859
|
+
process.kill(parseInt(pid), "SIGTERM");
|
|
860
|
+
killed.push("profile PID " + pid);
|
|
861
|
+
} catch (_e2) {
|
|
862
|
+
/* ignore */
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
} catch (_e) {
|
|
866
|
+
/* ignore */
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
clearLiveBrowserSession();
|
|
870
|
+
console.log(
|
|
871
|
+
"Stopped: " + (killed.length > 0 ? killed.join(", ") : "nothing found"),
|
|
872
|
+
);
|
|
873
|
+
return true;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Get active live browser session info.
|
|
878
|
+
*/
|
|
879
|
+
export function getActiveLiveBrowserSession(): MinimaxLiveBrowserSession | null {
|
|
880
|
+
return loadLiveBrowserSession();
|
|
246
881
|
}
|
|
247
882
|
|
|
248
883
|
/**
|
|
249
|
-
*
|
|
884
|
+
* Scrape usage data. Tries live session first, falls back to headless Playwright.
|
|
250
885
|
*/
|
|
251
886
|
export async function scrapeWithExistingProfile(
|
|
252
887
|
config: MinimaxBrowserAuthConfig = {},
|
|
253
888
|
): Promise<MinimaxAuthStatus> {
|
|
254
889
|
const profileDir = config.profilePath ?? getProfileDir();
|
|
255
|
-
const statusPath = config.statusPath ?? getStatusPath();
|
|
256
890
|
const targetUrl =
|
|
257
891
|
config.targetUrl ?? "https://platform.minimax.io/console/usage";
|
|
258
|
-
|
|
259
892
|
ensureDirs(config);
|
|
260
893
|
|
|
261
894
|
console.log("=".repeat(60));
|
|
262
|
-
console.log("📊 MiniMax Usage Scraper
|
|
895
|
+
console.log("📊 MiniMax Usage Scraper");
|
|
263
896
|
console.log("=".repeat(60));
|
|
264
897
|
console.log("");
|
|
265
898
|
|
|
266
|
-
//
|
|
267
|
-
const
|
|
899
|
+
// 1. Try live session first (skip if forced off for testing)
|
|
900
|
+
const liveSession = config.forceNoLiveSession
|
|
901
|
+
? null
|
|
902
|
+
: loadLiveBrowserSession();
|
|
903
|
+
if (liveSession) {
|
|
904
|
+
console.log("Live browser session found (PID " + liveSession.pid + ")");
|
|
905
|
+
console.log("Trying live attach...");
|
|
906
|
+
const result = await scrapeViaLiveBrowserSession(config);
|
|
907
|
+
if (result.authenticated || result.usage_lines) {
|
|
908
|
+
console.log("✅ Live session scrape successful!");
|
|
909
|
+
return result;
|
|
910
|
+
}
|
|
911
|
+
// Live session exists but attach failed — do NOT fall back to headless
|
|
912
|
+
// because that would try to lock the same profile (already locked by the daemon)
|
|
913
|
+
console.log("⚠️ Live session exists but attach failed.");
|
|
914
|
+
console.log(" Make sure your Chrome window is still open.");
|
|
915
|
+
console.log(
|
|
916
|
+
" To stop and restart: bun packages/auth/src/run-minimax-auth.ts stop",
|
|
917
|
+
);
|
|
918
|
+
return {
|
|
919
|
+
provider: "minimax",
|
|
920
|
+
authenticated: false,
|
|
921
|
+
checked_at: new Date().toISOString(),
|
|
922
|
+
page_url: "",
|
|
923
|
+
detected_text_sample: null,
|
|
924
|
+
profile_path: liveSession.profile_path,
|
|
925
|
+
error_message:
|
|
926
|
+
"Live attach failed: " +
|
|
927
|
+
(result.error_message ?? "unknown error") +
|
|
928
|
+
". Is the Chrome window still open?",
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// 2. Fall back to headless Playwright with existing profile
|
|
933
|
+
const profileExists = hasProfileData(profileDir);
|
|
268
934
|
if (!profileExists) {
|
|
269
935
|
console.log("⚠️ No profile found. Run auth first:");
|
|
270
936
|
console.log(" bun packages/auth/src/run-minimax-auth.ts auth");
|
|
@@ -279,90 +945,74 @@ export async function scrapeWithExistingProfile(
|
|
|
279
945
|
};
|
|
280
946
|
}
|
|
281
947
|
|
|
282
|
-
|
|
948
|
+
console.log("Launching headless browser with saved profile...");
|
|
283
949
|
|
|
950
|
+
let context: BrowserContext | null = null;
|
|
284
951
|
try {
|
|
285
|
-
// Launch with existing profile (headless for scraping)
|
|
286
|
-
console.log("🚀 Launching browser with saved profile...");
|
|
287
|
-
console.log(" (Silent/headless mode for scraping)");
|
|
288
|
-
|
|
289
952
|
context = await chromium.launchPersistentContext(profileDir, {
|
|
290
|
-
|
|
953
|
+
executablePath: getChromeExecutablePath(config),
|
|
954
|
+
headless: true,
|
|
291
955
|
chromiumSandbox: false,
|
|
292
|
-
args:
|
|
956
|
+
args: getChromeArgs(),
|
|
293
957
|
viewport: { width: 1280, height: 800 },
|
|
294
958
|
});
|
|
295
959
|
|
|
296
960
|
const page = context.pages()[0] ?? (await context.newPage());
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
961
|
+
console.log("Navigating to " + targetUrl + "...");
|
|
962
|
+
await page.goto(targetUrl, {
|
|
963
|
+
waitUntil: "domcontentloaded",
|
|
964
|
+
timeout: 30000,
|
|
965
|
+
});
|
|
966
|
+
await page.waitForTimeout(3000);
|
|
302
967
|
|
|
303
968
|
const url = page.url();
|
|
304
969
|
const bodyText = (await page.textContent("body")) ?? "";
|
|
305
970
|
const { detected, sample } = detectUsagePage(bodyText);
|
|
971
|
+
const usage_lines = extractUsageLines(bodyText);
|
|
306
972
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
const usageData: string[] = [];
|
|
313
|
-
const elements = await page.$$("body *");
|
|
314
|
-
for (const el of elements.slice(0, 300)) {
|
|
315
|
-
const text = await el.textContent();
|
|
316
|
-
if (
|
|
317
|
-
text &&
|
|
318
|
-
(text.includes("%") ||
|
|
319
|
-
text.match(/\d+\s*[/:]\s*\d+/) ||
|
|
320
|
-
(/^\d+$/.test(text.trim()) && text.length < 10))
|
|
321
|
-
) {
|
|
322
|
-
const clean = text.trim().substring(0, 50);
|
|
323
|
-
if (!usageData.includes(clean)) {
|
|
324
|
-
usageData.push(clean);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
973
|
+
// Check if redirected to login
|
|
974
|
+
const redirectedToLogin =
|
|
975
|
+
url.includes("unified-login") || url.includes("login");
|
|
976
|
+
const savedStatus = loadSavedAuthStatus(config);
|
|
977
|
+
const wasPreviouslyAuthenticated = savedStatus?.authenticated ?? false;
|
|
328
978
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
979
|
+
let authenticated = detected;
|
|
980
|
+
let error_message: string | undefined;
|
|
981
|
+
|
|
982
|
+
if (redirectedToLogin && wasPreviouslyAuthenticated) {
|
|
983
|
+
console.log(
|
|
984
|
+
"⚠️ Redirected to login but previously authenticated. Profile may have expired.",
|
|
985
|
+
);
|
|
986
|
+
authenticated = false;
|
|
987
|
+
error_message = "Session expired - redirected to login";
|
|
335
988
|
}
|
|
336
989
|
|
|
337
|
-
console.log("");
|
|
990
|
+
console.log("URL: " + url);
|
|
991
|
+
console.log("Detected usage page: " + detected);
|
|
992
|
+
if (usage_lines.length > 0) {
|
|
993
|
+
console.log("Usage lines (" + usage_lines.length + "):");
|
|
994
|
+
usage_lines.slice(0, 20).forEach((l) => console.log(" " + l));
|
|
995
|
+
}
|
|
338
996
|
|
|
339
997
|
const status: MinimaxAuthStatus = {
|
|
340
998
|
provider: "minimax",
|
|
341
|
-
authenticated
|
|
999
|
+
authenticated,
|
|
342
1000
|
checked_at: new Date().toISOString(),
|
|
343
1001
|
page_url: url,
|
|
344
1002
|
detected_text_sample: sample,
|
|
345
1003
|
profile_path: profileDir,
|
|
1004
|
+
usage_lines,
|
|
1005
|
+
error_message,
|
|
346
1006
|
};
|
|
347
|
-
|
|
348
1007
|
saveAuthStatus(status, config);
|
|
349
1008
|
|
|
350
|
-
if (detected) {
|
|
351
|
-
console.log("✅ Scraped successfully!");
|
|
352
|
-
}
|
|
353
|
-
|
|
354
1009
|
await context.close();
|
|
355
1010
|
context = null;
|
|
356
|
-
|
|
357
1011
|
return status;
|
|
358
1012
|
} catch (error) {
|
|
359
1013
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
360
|
-
console.error(
|
|
361
|
-
|
|
362
|
-
if (context) {
|
|
363
|
-
await context.close().catch(() => {});
|
|
364
|
-
}
|
|
365
|
-
|
|
1014
|
+
console.error("❌ Error: " + errorMessage);
|
|
1015
|
+
if (context) await context.close().catch(() => {});
|
|
366
1016
|
const status: MinimaxAuthStatus = {
|
|
367
1017
|
provider: "minimax",
|
|
368
1018
|
authenticated: false,
|
|
@@ -372,23 +1022,20 @@ export async function scrapeWithExistingProfile(
|
|
|
372
1022
|
profile_path: profileDir,
|
|
373
1023
|
error_message: errorMessage,
|
|
374
1024
|
};
|
|
375
|
-
|
|
376
1025
|
saveAuthStatus(status, config);
|
|
377
1026
|
return status;
|
|
378
1027
|
}
|
|
379
1028
|
}
|
|
380
1029
|
|
|
381
1030
|
/**
|
|
382
|
-
* Check auth status
|
|
1031
|
+
* Check auth status.
|
|
383
1032
|
*/
|
|
384
1033
|
export async function checkAuthStatus(
|
|
385
1034
|
config: MinimaxBrowserAuthConfig = {},
|
|
386
1035
|
): Promise<MinimaxAuthStatus> {
|
|
387
1036
|
const statusPath = config.statusPath ?? getStatusPath();
|
|
388
1037
|
const profileDir = config.profilePath ?? getProfileDir();
|
|
389
|
-
|
|
390
1038
|
console.log("Checking MiniMax authentication status...");
|
|
391
|
-
|
|
392
1039
|
if (fs.existsSync(statusPath)) {
|
|
393
1040
|
const content = fs.readFileSync(statusPath, "utf-8");
|
|
394
1041
|
try {
|
|
@@ -399,29 +1046,36 @@ export async function checkAuthStatus(
|
|
|
399
1046
|
? "✅ User is logged in"
|
|
400
1047
|
: "⚠️ User is NOT logged in",
|
|
401
1048
|
);
|
|
402
|
-
|
|
403
|
-
|
|
1049
|
+
if (status.usage_lines && status.usage_lines.length > 0) {
|
|
1050
|
+
console.log("Usage lines available: " + status.usage_lines.length);
|
|
1051
|
+
}
|
|
1052
|
+
console.log("Checked at: " + status.checked_at);
|
|
1053
|
+
console.log("Profile: " + status.profile_path);
|
|
404
1054
|
return status;
|
|
405
1055
|
} catch {
|
|
406
|
-
console.log("
|
|
1056
|
+
console.log("⚠️ Status file is corrupted");
|
|
407
1057
|
}
|
|
1058
|
+
} else {
|
|
1059
|
+
console.log("⚠️ No status file found. Run auth first.");
|
|
408
1060
|
}
|
|
409
1061
|
|
|
410
|
-
const
|
|
411
|
-
if (
|
|
412
|
-
console.log(
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
1062
|
+
const liveSession = loadLiveBrowserSession();
|
|
1063
|
+
if (liveSession) {
|
|
1064
|
+
console.log(
|
|
1065
|
+
"Live browser session active (PID " +
|
|
1066
|
+
liveSession.pid +
|
|
1067
|
+
", port " +
|
|
1068
|
+
liveSession.debugging_port +
|
|
1069
|
+
")",
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
if (!hasProfileData(profileDir)) {
|
|
1074
|
+
console.log("⚠️ No Chrome profile found at " + profileDir);
|
|
1075
|
+
} else {
|
|
1076
|
+
console.log("✅ Chrome profile exists at " + profileDir);
|
|
422
1077
|
}
|
|
423
1078
|
|
|
424
|
-
console.log("No profile found. Run auth first.");
|
|
425
1079
|
return {
|
|
426
1080
|
provider: "minimax",
|
|
427
1081
|
authenticated: false,
|
|
@@ -429,9 +1083,5 @@ export async function checkAuthStatus(
|
|
|
429
1083
|
page_url: "",
|
|
430
1084
|
detected_text_sample: null,
|
|
431
1085
|
profile_path: profileDir,
|
|
432
|
-
error_message: "No profile found - run auth first",
|
|
433
1086
|
};
|
|
434
1087
|
}
|
|
435
|
-
|
|
436
|
-
// Alias for backward compatibility
|
|
437
|
-
export const authenticateWithCurator = authenticateWithPersistentBrowser;
|