openzoo 0.50.37 → 0.50.39
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/bin/claude-zoo.js +100 -0
- package/bin/openzoo.js +8 -0
- package/lib/cursorbackend.js +464 -99
- package/lib/grokbotAccount.js +134 -8
- package/lib/grokbotUploads.js +168 -0
- package/lib/grokbotweb-shim.js +766 -0
- package/lib/grokbotweb.js +933 -0
- package/lib/grokcli.js +47 -22
- package/lib/ozSpendChip.js +482 -0
- package/lib/pay.js +17 -24
- package/lib/proxy.js +33 -11
- package/lib/spendProof.js +307 -0
- package/lib/x402.js +3 -1
- package/package.json +1 -1
package/lib/grokcli.js
CHANGED
|
@@ -22,6 +22,11 @@ import { dirname, join } from 'node:path';
|
|
|
22
22
|
import { fileURLToPath } from 'node:url';
|
|
23
23
|
|
|
24
24
|
import { config } from './config.js';
|
|
25
|
+
import {
|
|
26
|
+
GROKBOT_CDP_PORT,
|
|
27
|
+
grokBotChromiumArgs,
|
|
28
|
+
injectSpendChipInBackground,
|
|
29
|
+
} from './ozSpendChip.js';
|
|
25
30
|
|
|
26
31
|
const GROK_HOME = process.env.GROK_HOME || join(homedir(), '.grok');
|
|
27
32
|
const CONFIG_PATH = join(GROK_HOME, 'config.toml');
|
|
@@ -134,12 +139,14 @@ function somethingTalksToPort(port, run = execSync) {
|
|
|
134
139
|
}
|
|
135
140
|
|
|
136
141
|
/** Is the running Grok Bot already pointed at our aiserver? */
|
|
137
|
-
export function inspectGrokBotHijack(url, port = 8443, run = execSync) {
|
|
142
|
+
export function inspectGrokBotHijack(url, port = 8443, run = execSync, cdpPort = GROKBOT_CDP_PORT) {
|
|
138
143
|
const pids = grokBotPids(run);
|
|
139
144
|
const needle = `CURSOR_API_BASE_URL=${url}`;
|
|
140
145
|
const envHit = pids.some((pid) => pidHasNeedle(pid, needle, run));
|
|
141
146
|
const tcpHit = somethingTalksToPort(port, run);
|
|
142
|
-
|
|
147
|
+
const cdpNeedle = `--remote-debugging-port=${Number(cdpPort)}`;
|
|
148
|
+
const chipDebug = pids.some((pid) => pidHasNeedle(pid, cdpNeedle, run));
|
|
149
|
+
return { running: pids.length > 0, pids, hijacked: envHit || tcpHit, chipDebug };
|
|
143
150
|
}
|
|
144
151
|
|
|
145
152
|
/**
|
|
@@ -147,7 +154,13 @@ export function inspectGrokBotHijack(url, port = 8443, run = execSync) {
|
|
|
147
154
|
* forceQuit = --quit / OZ_NO_QUIT=0
|
|
148
155
|
* neverQuit = --no-quit / OZ_NO_QUIT=1
|
|
149
156
|
*/
|
|
150
|
-
export function grokBotQuitPlan({
|
|
157
|
+
export function grokBotQuitPlan({
|
|
158
|
+
forceQuit = false,
|
|
159
|
+
neverQuit = false,
|
|
160
|
+
running = false,
|
|
161
|
+
hijacked = false,
|
|
162
|
+
chipDebug,
|
|
163
|
+
} = {}) {
|
|
151
164
|
if (forceQuit) return { quit: true, spawn: true, reason: 'forced --quit' };
|
|
152
165
|
if (neverQuit) {
|
|
153
166
|
return {
|
|
@@ -159,6 +172,13 @@ export function grokBotQuitPlan({ forceQuit = false, neverQuit = false, running
|
|
|
159
172
|
};
|
|
160
173
|
}
|
|
161
174
|
if (!running) return { quit: false, spawn: true, reason: 'Grok Bot not running' };
|
|
175
|
+
if (hijacked && chipDebug === false) {
|
|
176
|
+
return {
|
|
177
|
+
quit: true,
|
|
178
|
+
spawn: true,
|
|
179
|
+
reason: 'hijacked but no spend-chip debug port — bouncing so the ⓘ bubble can inject',
|
|
180
|
+
};
|
|
181
|
+
}
|
|
162
182
|
if (hijacked) return { quit: false, spawn: false, reason: 'already hijacked — leaving the session' };
|
|
163
183
|
return {
|
|
164
184
|
quit: true,
|
|
@@ -181,6 +201,10 @@ export function grokBotQuitPlan({ forceQuit = false, neverQuit = false, running
|
|
|
181
201
|
* stubs oauth if it is the unpatched npx copy — do not point the app there.
|
|
182
202
|
*/
|
|
183
203
|
export async function runBot(argv = []) {
|
|
204
|
+
if (argv.includes('--web')) {
|
|
205
|
+
const { runGrokBotWeb } = await import('./grokbotweb.js');
|
|
206
|
+
return runGrokBotWeb(argv);
|
|
207
|
+
}
|
|
184
208
|
const port = 8443;
|
|
185
209
|
const url = `https://127.0.0.1:${port}`;
|
|
186
210
|
const bin = `${APP}/Contents/MacOS/Grok Bot`;
|
|
@@ -190,13 +214,9 @@ export async function runBot(argv = []) {
|
|
|
190
214
|
}
|
|
191
215
|
|
|
192
216
|
const proxyBase = `http://localhost:${config.port}/v1`;
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
console.error('openzoo: starting proxy on', proxyBase);
|
|
197
|
-
const { startProxy } = await import('./proxy.js');
|
|
198
|
-
await startProxy({ silent: true, autoTunnel: process.env.OPENZOO_NO_TUNNEL === '1' ? false : true });
|
|
199
|
-
}
|
|
217
|
+
console.error('openzoo: starting proxy on', proxyBase);
|
|
218
|
+
const { startProxy } = await import('./proxy.js');
|
|
219
|
+
await startProxy({ silent: true, autoTunnel: process.env.OPENZOO_NO_TUNNEL === '1' ? false : true });
|
|
200
220
|
|
|
201
221
|
process.env.OPENZOO_BYOK = '1';
|
|
202
222
|
const sniff = argv.includes('--sniff') || argv.includes('--passthru');
|
|
@@ -264,6 +284,7 @@ export async function runBot(argv = []) {
|
|
|
264
284
|
neverQuit: argv.includes('--no-quit') || process.env.OZ_NO_QUIT === '1',
|
|
265
285
|
running: state.running,
|
|
266
286
|
hijacked: state.hijacked,
|
|
287
|
+
chipDebug: state.chipDebug,
|
|
267
288
|
});
|
|
268
289
|
console.error(`openzoo: grokbot ${plan.reason}`);
|
|
269
290
|
if (plan.quit) {
|
|
@@ -274,7 +295,7 @@ export async function runBot(argv = []) {
|
|
|
274
295
|
if (plan.spawn) {
|
|
275
296
|
console.error('openzoo: launching Grok Bot');
|
|
276
297
|
console.error(` CURSOR_API_BASE_URL=${url}`);
|
|
277
|
-
spawn(bin,
|
|
298
|
+
spawn(bin, grokBotChromiumArgs(), {
|
|
278
299
|
stdio: 'ignore',
|
|
279
300
|
detached: true,
|
|
280
301
|
env: {
|
|
@@ -294,6 +315,12 @@ export async function runBot(argv = []) {
|
|
|
294
315
|
console.error('openzoo: Grok Bot already hijacked — not spawning another copy');
|
|
295
316
|
}
|
|
296
317
|
|
|
318
|
+
injectSpendChipInBackground({
|
|
319
|
+
port: GROKBOT_CDP_PORT,
|
|
320
|
+
log: (m) => console.error(m),
|
|
321
|
+
delayMs: plan.spawn ? 500 : 200,
|
|
322
|
+
});
|
|
323
|
+
|
|
297
324
|
console.error('openzoo: leave this running. ctrl-c stops the backend.');
|
|
298
325
|
if (argv.includes('--once')) return;
|
|
299
326
|
await new Promise(() => {});
|
|
@@ -303,19 +330,17 @@ export async function setupGrokBot(argv = []) {
|
|
|
303
330
|
const base = `http://localhost:${config.port}/v1`;
|
|
304
331
|
const launch = !argv.includes('--no-launch');
|
|
305
332
|
|
|
306
|
-
// 1. proxy up, or nothing can pay
|
|
333
|
+
// 1. proxy up, or nothing can pay. Always startProxy: it kills a stale
|
|
334
|
+
// listener on the port instead of reusing a PayClient that still thinks $0.
|
|
335
|
+
console.error('openzoo: starting the proxy...');
|
|
336
|
+
const { startProxy } = await import('./proxy.js');
|
|
337
|
+
await startProxy({ silent: true, autoTunnel: true });
|
|
307
338
|
let up = false;
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const { startProxy } = await import('./proxy.js');
|
|
312
|
-
await startProxy({ silent: true, autoTunnel: true });
|
|
313
|
-
for (let i = 0; i < 25 && !up; i++) {
|
|
314
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
315
|
-
try { up = (await fetch(`${base}/models`, { signal: AbortSignal.timeout(2000) })).ok; } catch { /* wait */ }
|
|
316
|
-
}
|
|
317
|
-
if (!up) { console.error(`openzoo: proxy did not come up on ${base}`); process.exit(1); }
|
|
339
|
+
for (let i = 0; i < 25 && !up; i++) {
|
|
340
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
341
|
+
try { up = (await fetch(`${base}/models`, { signal: AbortSignal.timeout(2000) })).ok; } catch { /* wait */ }
|
|
318
342
|
}
|
|
343
|
+
if (!up) { console.error(`openzoo: proxy did not come up on ${base}`); process.exit(1); }
|
|
319
344
|
|
|
320
345
|
const models = await grokModels(base);
|
|
321
346
|
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cafe spend chip — the ⓘ $N <details> that folds formatSpendFooter.
|
|
3
|
+
*
|
|
4
|
+
* Cafe injects this via grokbotweb-shim concatenation. Grok Bot.app cannot
|
|
5
|
+
* be patched (asar integrity), so `openzoo bot` launches Chromium with a
|
|
6
|
+
* localhost CDP port and evaluates the same IIFE in the renderer.
|
|
7
|
+
*/
|
|
8
|
+
import net from 'node:net';
|
|
9
|
+
import crypto from 'node:crypto';
|
|
10
|
+
|
|
11
|
+
export const GROKBOT_CDP_PORT = Number(process.env.OZ_GROKBOT_CDP_PORT || 9444);
|
|
12
|
+
|
|
13
|
+
export function grokBotChromiumArgs(port = GROKBOT_CDP_PORT) {
|
|
14
|
+
return [
|
|
15
|
+
'--ignore-certificate-errors',
|
|
16
|
+
`--remote-debugging-port=${Number(port)}`,
|
|
17
|
+
'--remote-allow-origins=*',
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Pure. Same split the renderer IIFE uses. */
|
|
22
|
+
export function splitSpendText(text) {
|
|
23
|
+
const s = String(text || '');
|
|
24
|
+
let m = s.match(/::oz-spend::(\$[0-9.]+|[^\n]*)\s*([\s\S]*)$/);
|
|
25
|
+
if (m && (m[2] || '').length > 8) {
|
|
26
|
+
let summary = (m[1] || '').trim();
|
|
27
|
+
const body = m[2].trim();
|
|
28
|
+
if (!/^\$[0-9.]+$/.test(summary) || summary === '$0.0000') {
|
|
29
|
+
const spent = body.match(/spent \$([0-9.]+)/i);
|
|
30
|
+
const call = body.match(/this call \$([0-9.]+)/i);
|
|
31
|
+
const n = Number((call && Number(call[1]) > 0.00005 ? call[1] : null) || (spent && spent[1]) || 0);
|
|
32
|
+
if (n > 0) summary = `$${n.toFixed(4)}`;
|
|
33
|
+
}
|
|
34
|
+
return { head: s.slice(0, m.index), summary: summary || 'spend', body };
|
|
35
|
+
}
|
|
36
|
+
m = s.match(/(?:this call \$|spent \$)[\s\S]*$/i);
|
|
37
|
+
if (m) {
|
|
38
|
+
const body = m[0].trim();
|
|
39
|
+
const call = body.match(/this call \$([0-9.]+)/i);
|
|
40
|
+
const spent = body.match(/spent \$([0-9.]+)/i);
|
|
41
|
+
const n = Number((call && Number(call[1]) > 0.00005 ? call[1] : null) || (spent && spent[1]) || 0);
|
|
42
|
+
return { head: s.slice(0, m.index), summary: n > 0 ? `$${n.toFixed(4)}` : 'spend', body };
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function spendOnlyText(t) {
|
|
48
|
+
const s = String(t || '').replace(/\u00a0/g, ' ').trim();
|
|
49
|
+
if (!s) return false;
|
|
50
|
+
if (!/::oz-spend::|this call \$|spent \$[0-9.]+[\s\S]*OpenRouter/i.test(s)) return false;
|
|
51
|
+
const rest = s
|
|
52
|
+
.replace(/::oz-spend::\$?[0-9.]*/gi, ' ')
|
|
53
|
+
.replace(/this call \$[0-9.]+/gi, ' ')
|
|
54
|
+
.replace(/OpenRouter(?: would)? \$[0-9.]+/gi, ' ')
|
|
55
|
+
.replace(/spent \$[0-9.]+/gi, ' ')
|
|
56
|
+
.replace(/saved \$[0-9.]+/gi, ' ')
|
|
57
|
+
.replace(/balance \$[0-9.]+/gi, ' ')
|
|
58
|
+
.replace(/\(\s*\d+%\s*\)/g, ' ')
|
|
59
|
+
.replace(/\b(?:tx|memo|proves)\b[^\n]*/gi, ' ')
|
|
60
|
+
.replace(/https?:\/\/\S+/gi, ' ')
|
|
61
|
+
.replace(/[·•.,;:/$%\d\s()[\]+\-]/g, '');
|
|
62
|
+
return rest.length < 4;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function ozEnsureSpendCss() {
|
|
66
|
+
if (document.getElementById('oz-spend-css')) return;
|
|
67
|
+
const s = document.createElement('style');
|
|
68
|
+
s.id = 'oz-spend-css';
|
|
69
|
+
s.textContent = [
|
|
70
|
+
'.oz-spend{margin:.55rem 0 0;font-size:12px;color:inherit;opacity:.82;max-width:36em}',
|
|
71
|
+
'.oz-spend>summary{cursor:help;list-style:none;display:inline-flex;align-items:center;gap:.35rem;',
|
|
72
|
+
'padding:3px 9px;border-radius:999px;border:1px solid rgba(255,255,255,.18)}',
|
|
73
|
+
'.oz-spend>summary::-webkit-details-marker{display:none}',
|
|
74
|
+
'.oz-spend-body{white-space:pre-wrap;margin:.55rem 0 0;font-size:11px;line-height:1.45;opacity:.88;overflow-wrap:anywhere}',
|
|
75
|
+
'[data-oz-spend-hide]{display:none!important}',
|
|
76
|
+
].join('');
|
|
77
|
+
(document.head || document.documentElement).appendChild(s);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function ozSpendHost(el) {
|
|
81
|
+
let cur = el;
|
|
82
|
+
let found = el;
|
|
83
|
+
let card = null;
|
|
84
|
+
for (let i = 0; i < 16 && cur && cur !== document.body && cur.id !== 'root'; i += 1) {
|
|
85
|
+
const t = cur.textContent || '';
|
|
86
|
+
if (/::oz-spend::|this call \$|spent \$/i.test(t)) found = cur;
|
|
87
|
+
const cls = cur.className && String(cur.className);
|
|
88
|
+
if (cls && /sand-message-card|sand-message-block/.test(cls)) {
|
|
89
|
+
card = cur;
|
|
90
|
+
return cur;
|
|
91
|
+
}
|
|
92
|
+
if (cls && /(^|\s)sand-message(\s|$)/.test(cls)) card = cur;
|
|
93
|
+
cur = cur.parentElement;
|
|
94
|
+
}
|
|
95
|
+
if (card) return card;
|
|
96
|
+
if (found && found !== document.body && found.id !== 'root' && (found.textContent || '').length < 12000) {
|
|
97
|
+
return found;
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function ozVisibleSpendText(root) {
|
|
103
|
+
let s = '';
|
|
104
|
+
const w = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null);
|
|
105
|
+
while (w.nextNode()) {
|
|
106
|
+
const tn = w.currentNode;
|
|
107
|
+
if (tn.parentElement && tn.parentElement.closest && tn.parentElement.closest('.oz-spend')) continue;
|
|
108
|
+
s += tn.nodeValue || '';
|
|
109
|
+
}
|
|
110
|
+
return s;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function ozBlankAfter(root, keepLen) {
|
|
114
|
+
let off = 0;
|
|
115
|
+
const w = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null);
|
|
116
|
+
const tns = [];
|
|
117
|
+
while (w.nextNode()) tns.push(w.currentNode);
|
|
118
|
+
for (let i = 0; i < tns.length; i += 1) {
|
|
119
|
+
const tn = tns[i];
|
|
120
|
+
if (tn.parentElement && tn.parentElement.closest && tn.parentElement.closest('.oz-spend')) continue;
|
|
121
|
+
const s = tn.nodeValue || '';
|
|
122
|
+
const start = off;
|
|
123
|
+
off += s.length;
|
|
124
|
+
if (off <= keepLen) continue;
|
|
125
|
+
if (start >= keepLen) tn.nodeValue = '';
|
|
126
|
+
else tn.nodeValue = s.slice(0, keepLen - start);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function ozHideSpendLeftovers() {
|
|
131
|
+
const nodes = document.body.querySelectorAll('div, p, span, li');
|
|
132
|
+
for (let i = 0; i < nodes.length; i += 1) {
|
|
133
|
+
const el = nodes[i];
|
|
134
|
+
if (el.closest && el.closest('.oz-spend')) continue;
|
|
135
|
+
if (el.querySelector && el.querySelector('.oz-spend')) continue;
|
|
136
|
+
const vis = ozVisibleSpendText(el);
|
|
137
|
+
if (!spendOnlyText(vis)) continue;
|
|
138
|
+
if (vis.length > 4000) continue;
|
|
139
|
+
if (el.children && el.children.length > 8) continue;
|
|
140
|
+
el.setAttribute('data-oz-spend-hide', '1');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function ozPreviousMessageCard(el) {
|
|
145
|
+
let cur = el;
|
|
146
|
+
for (let i = 0; i < 8 && cur && cur !== document.body; i += 1) {
|
|
147
|
+
let sib = cur.previousElementSibling;
|
|
148
|
+
while (sib) {
|
|
149
|
+
if (sib === el) {
|
|
150
|
+
sib = sib.previousElementSibling;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const cls = String(sib.className || '');
|
|
154
|
+
if (/sand-message-card|sand-message-block/.test(cls) || /(^|\s)sand-message(\s|$)/.test(cls)) {
|
|
155
|
+
return sib;
|
|
156
|
+
}
|
|
157
|
+
const inner = sib.querySelector && sib.querySelector('.sand-message-card, .sand-message-block');
|
|
158
|
+
if (inner) return inner;
|
|
159
|
+
sib = sib.previousElementSibling;
|
|
160
|
+
}
|
|
161
|
+
cur = cur.parentElement;
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function ozAttachSpendChip(host, split) {
|
|
167
|
+
if (!host || !split) return;
|
|
168
|
+
if (host.querySelector && host.querySelector('.oz-spend')) return;
|
|
169
|
+
const d = document.createElement('details');
|
|
170
|
+
d.className = 'oz-spend';
|
|
171
|
+
const sum = document.createElement('summary');
|
|
172
|
+
sum.textContent = 'ⓘ ' + split.summary;
|
|
173
|
+
sum.title = split.body;
|
|
174
|
+
const body = document.createElement('div');
|
|
175
|
+
body.className = 'oz-spend-body';
|
|
176
|
+
body.textContent = split.body;
|
|
177
|
+
d.appendChild(sum);
|
|
178
|
+
d.appendChild(body);
|
|
179
|
+
try { host.appendChild(d); } catch { /* react owns some nodes */ }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function ozCollapseSpend() {
|
|
183
|
+
if (!document.body) return;
|
|
184
|
+
const hosts = [];
|
|
185
|
+
const seen = new Set();
|
|
186
|
+
const nodes = document.body.querySelectorAll('div, p, span, li');
|
|
187
|
+
for (let i = 0; i < nodes.length; i += 1) {
|
|
188
|
+
const el = nodes[i];
|
|
189
|
+
if (el.closest && el.closest('.oz-spend')) continue;
|
|
190
|
+
const t = ozVisibleSpendText(el);
|
|
191
|
+
if (!/::oz-spend::|this call \$|spent \$/i.test(t)) continue;
|
|
192
|
+
const host = ozSpendHost(el);
|
|
193
|
+
if (!host || seen.has(host)) continue;
|
|
194
|
+
seen.add(host);
|
|
195
|
+
hosts.push(host);
|
|
196
|
+
}
|
|
197
|
+
for (let h = 0; h < hosts.length; h += 1) {
|
|
198
|
+
const host = hosts[h];
|
|
199
|
+
const vis = ozVisibleSpendText(host);
|
|
200
|
+
const split = splitSpendText(vis);
|
|
201
|
+
if (!split || !split.body || split.body.length < 12) continue;
|
|
202
|
+
ozEnsureSpendCss();
|
|
203
|
+
if (spendOnlyText(vis)) {
|
|
204
|
+
const prev = ozPreviousMessageCard(host);
|
|
205
|
+
if (prev && prev !== host) {
|
|
206
|
+
ozAttachSpendChip(prev, split);
|
|
207
|
+
host.setAttribute('data-oz-spend-hide', '1');
|
|
208
|
+
ozBlankAfter(host, 0);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
ozBlankAfter(host, split.head.length);
|
|
213
|
+
ozAttachSpendChip(host, split);
|
|
214
|
+
}
|
|
215
|
+
ozHideSpendLeftovers();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function ozWatchSpend() {
|
|
219
|
+
const run = () => { try { ozCollapseSpend(); } catch { /* */ } };
|
|
220
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', run);
|
|
221
|
+
else run();
|
|
222
|
+
try {
|
|
223
|
+
const mo = new MutationObserver(run);
|
|
224
|
+
const start = () => { if (document.body) mo.observe(document.body, { childList: true, subtree: true }); };
|
|
225
|
+
if (document.body) start();
|
|
226
|
+
else document.addEventListener('DOMContentLoaded', start);
|
|
227
|
+
} catch { /* */ }
|
|
228
|
+
setInterval(run, 1500);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function spendChipSource() {
|
|
232
|
+
return [
|
|
233
|
+
'(function ozSpendChip(){',
|
|
234
|
+
"'use strict';",
|
|
235
|
+
'if (window.__OZ_SPEND_CHIP__) return;',
|
|
236
|
+
'window.__OZ_SPEND_CHIP__ = 1;',
|
|
237
|
+
splitSpendText.toString(),
|
|
238
|
+
spendOnlyText.toString(),
|
|
239
|
+
ozEnsureSpendCss.toString(),
|
|
240
|
+
ozSpendHost.toString(),
|
|
241
|
+
ozVisibleSpendText.toString(),
|
|
242
|
+
ozBlankAfter.toString(),
|
|
243
|
+
ozHideSpendLeftovers.toString(),
|
|
244
|
+
ozPreviousMessageCard.toString(),
|
|
245
|
+
ozAttachSpendChip.toString(),
|
|
246
|
+
ozCollapseSpend.toString(),
|
|
247
|
+
ozWatchSpend.toString(),
|
|
248
|
+
'ozWatchSpend();',
|
|
249
|
+
'})();',
|
|
250
|
+
].join('\n');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function wsMaskFrame(payload, opcode = 0x1) {
|
|
254
|
+
const data = Buffer.isBuffer(payload) ? payload : Buffer.from(String(payload));
|
|
255
|
+
const mask = crypto.randomBytes(4);
|
|
256
|
+
const len = data.length;
|
|
257
|
+
let header;
|
|
258
|
+
if (len < 126) {
|
|
259
|
+
header = Buffer.alloc(6);
|
|
260
|
+
header[0] = 0x80 | opcode;
|
|
261
|
+
header[1] = 0x80 | len;
|
|
262
|
+
mask.copy(header, 2);
|
|
263
|
+
} else if (len < 65536) {
|
|
264
|
+
header = Buffer.alloc(8);
|
|
265
|
+
header[0] = 0x80 | opcode;
|
|
266
|
+
header[1] = 0x80 | 126;
|
|
267
|
+
header.writeUInt16BE(len, 2);
|
|
268
|
+
mask.copy(header, 4);
|
|
269
|
+
} else {
|
|
270
|
+
header = Buffer.alloc(14);
|
|
271
|
+
header[0] = 0x80 | opcode;
|
|
272
|
+
header[1] = 0x80 | 127;
|
|
273
|
+
header.writeBigUInt64BE(BigInt(len), 2);
|
|
274
|
+
mask.copy(header, 10);
|
|
275
|
+
}
|
|
276
|
+
const masked = Buffer.alloc(len);
|
|
277
|
+
for (let i = 0; i < len; i += 1) masked[i] = data[i] ^ mask[i % 4];
|
|
278
|
+
return Buffer.concat([header, masked]);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function decodeWsFrames(buf) {
|
|
282
|
+
const frames = [];
|
|
283
|
+
let rest = buf;
|
|
284
|
+
while (rest.length >= 2) {
|
|
285
|
+
const opcode = rest[0] & 0x0f;
|
|
286
|
+
const masked = (rest[1] & 0x80) !== 0;
|
|
287
|
+
let len = rest[1] & 0x7f;
|
|
288
|
+
let offset = 2;
|
|
289
|
+
if (len === 126) {
|
|
290
|
+
if (rest.length < 4) break;
|
|
291
|
+
len = rest.readUInt16BE(2);
|
|
292
|
+
offset = 4;
|
|
293
|
+
} else if (len === 127) {
|
|
294
|
+
if (rest.length < 10) break;
|
|
295
|
+
len = Number(rest.readBigUInt64BE(2));
|
|
296
|
+
offset = 10;
|
|
297
|
+
}
|
|
298
|
+
const maskLen = masked ? 4 : 0;
|
|
299
|
+
if (rest.length < offset + maskLen + len) break;
|
|
300
|
+
let payload = rest.subarray(offset + maskLen, offset + maskLen + len);
|
|
301
|
+
if (masked) {
|
|
302
|
+
const mask = rest.subarray(offset, offset + 4);
|
|
303
|
+
const decoded = Buffer.alloc(len);
|
|
304
|
+
for (let i = 0; i < len; i += 1) decoded[i] = payload[i] ^ mask[i % 4];
|
|
305
|
+
payload = decoded;
|
|
306
|
+
}
|
|
307
|
+
frames.push({ opcode, payload });
|
|
308
|
+
rest = rest.subarray(offset + maskLen + len);
|
|
309
|
+
}
|
|
310
|
+
return { frames, rest };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function cdpSession(wsUrl, { timeoutMs = 8000 } = {}) {
|
|
314
|
+
return new Promise((resolve, reject) => {
|
|
315
|
+
let u;
|
|
316
|
+
try { u = new URL(wsUrl); } catch (e) { reject(e); return; }
|
|
317
|
+
const key = crypto.randomBytes(16).toString('base64');
|
|
318
|
+
const sock = net.connect({ host: u.hostname, port: Number(u.port || 80) });
|
|
319
|
+
let buf = Buffer.alloc(0);
|
|
320
|
+
let upgraded = false;
|
|
321
|
+
let settled = false;
|
|
322
|
+
let nextId = 0;
|
|
323
|
+
const pending = new Map();
|
|
324
|
+
const timer = setTimeout(() => {
|
|
325
|
+
if (!upgraded) fail(new Error('cdp timeout'));
|
|
326
|
+
}, timeoutMs);
|
|
327
|
+
|
|
328
|
+
function fail(err) {
|
|
329
|
+
if (settled) {
|
|
330
|
+
sock.destroy();
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
settled = true;
|
|
334
|
+
clearTimeout(timer);
|
|
335
|
+
sock.destroy();
|
|
336
|
+
reject(err);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
sock.on('error', fail);
|
|
340
|
+
sock.on('connect', () => {
|
|
341
|
+
sock.write(
|
|
342
|
+
`GET ${u.pathname}${u.search} HTTP/1.1\r\n`
|
|
343
|
+
+ `Host: ${u.host}\r\n`
|
|
344
|
+
+ 'Upgrade: websocket\r\n'
|
|
345
|
+
+ 'Connection: Upgrade\r\n'
|
|
346
|
+
+ 'Sec-WebSocket-Version: 13\r\n'
|
|
347
|
+
+ `Sec-WebSocket-Key: ${key}\r\n`
|
|
348
|
+
+ `Origin: http://${u.host}\r\n`
|
|
349
|
+
+ '\r\n',
|
|
350
|
+
);
|
|
351
|
+
});
|
|
352
|
+
sock.on('data', (chunk) => {
|
|
353
|
+
buf = Buffer.concat([buf, chunk]);
|
|
354
|
+
if (!upgraded) {
|
|
355
|
+
const idx = buf.indexOf('\r\n\r\n');
|
|
356
|
+
if (idx < 0) return;
|
|
357
|
+
const head = buf.subarray(0, idx).toString('utf8');
|
|
358
|
+
buf = buf.subarray(idx + 4);
|
|
359
|
+
if (!/^HTTP\/1\.1 101/i.test(head)) {
|
|
360
|
+
fail(new Error(`cdp upgrade ${head.split('\r\n')[0] || 'failed'}`));
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
upgraded = true;
|
|
364
|
+
settled = true;
|
|
365
|
+
clearTimeout(timer);
|
|
366
|
+
resolve({
|
|
367
|
+
send(method, params) {
|
|
368
|
+
const id = ++nextId;
|
|
369
|
+
return new Promise((res, rej) => {
|
|
370
|
+
const t = setTimeout(() => {
|
|
371
|
+
pending.delete(id);
|
|
372
|
+
rej(new Error(`cdp ${method} timeout`));
|
|
373
|
+
}, Math.max(1000, timeoutMs));
|
|
374
|
+
pending.set(id, {
|
|
375
|
+
res: (v) => { clearTimeout(t); res(v); },
|
|
376
|
+
rej: (e) => { clearTimeout(t); rej(e); },
|
|
377
|
+
});
|
|
378
|
+
try { sock.write(wsMaskFrame(JSON.stringify({ id, method, params }))); } catch (e) {
|
|
379
|
+
clearTimeout(t);
|
|
380
|
+
pending.delete(id);
|
|
381
|
+
rej(e);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
},
|
|
385
|
+
close() {
|
|
386
|
+
clearTimeout(timer);
|
|
387
|
+
try { sock.destroy(); } catch { /* */ }
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
const decoded = decodeWsFrames(buf);
|
|
392
|
+
buf = decoded.rest;
|
|
393
|
+
for (const f of decoded.frames) {
|
|
394
|
+
if (f.opcode === 0x9) {
|
|
395
|
+
try { sock.write(wsMaskFrame(f.payload, 0xa)); } catch { /* */ }
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (f.opcode === 0x8) {
|
|
399
|
+
sock.destroy();
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (f.opcode !== 0x1 && f.opcode !== 0x0) continue;
|
|
403
|
+
let msg;
|
|
404
|
+
try { msg = JSON.parse(f.payload.toString('utf8')); } catch { continue; }
|
|
405
|
+
if (msg && msg.id != null && pending.has(msg.id)) {
|
|
406
|
+
const { res, rej } = pending.get(msg.id);
|
|
407
|
+
pending.delete(msg.id);
|
|
408
|
+
if (msg.error) rej(new Error(msg.error.message || JSON.stringify(msg.error)));
|
|
409
|
+
else res(msg.result);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export async function listCdpPages(port, fetchImpl = fetch) {
|
|
417
|
+
for (const path of ['/json/list', '/json']) {
|
|
418
|
+
try {
|
|
419
|
+
const r = await fetchImpl(`http://127.0.0.1:${Number(port)}${path}`, {
|
|
420
|
+
signal: AbortSignal.timeout(1500),
|
|
421
|
+
});
|
|
422
|
+
if (!r.ok) continue;
|
|
423
|
+
const j = await r.json();
|
|
424
|
+
const arr = Array.isArray(j) ? j : [];
|
|
425
|
+
return arr.filter((t) => t && t.webSocketDebuggerUrl && t.type !== 'service_worker' && t.type !== 'worker');
|
|
426
|
+
} catch { /* try next */ }
|
|
427
|
+
}
|
|
428
|
+
return [];
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function injectTarget(target, source, connect) {
|
|
432
|
+
const session = await connect(target.webSocketDebuggerUrl);
|
|
433
|
+
try {
|
|
434
|
+
await session.send('Page.enable').catch(() => {});
|
|
435
|
+
await session.send('Runtime.enable').catch(() => {});
|
|
436
|
+
await session.send('Page.addScriptToEvaluateOnNewDocument', { source }).catch(() => {});
|
|
437
|
+
await session.send('Runtime.evaluate', { expression: source, awaitPromise: false });
|
|
438
|
+
} finally {
|
|
439
|
+
session.close();
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export async function injectSpendChip({
|
|
444
|
+
port = GROKBOT_CDP_PORT,
|
|
445
|
+
source = spendChipSource(),
|
|
446
|
+
log = () => {},
|
|
447
|
+
fetchImpl = fetch,
|
|
448
|
+
connect = cdpSession,
|
|
449
|
+
tries = 40,
|
|
450
|
+
delayMs = 400,
|
|
451
|
+
} = {}) {
|
|
452
|
+
let targets = [];
|
|
453
|
+
for (let i = 0; i < Math.max(1, tries); i += 1) {
|
|
454
|
+
targets = await listCdpPages(port, fetchImpl);
|
|
455
|
+
if (targets.length) break;
|
|
456
|
+
if (i + 1 < tries && delayMs) await new Promise((r) => setTimeout(r, delayMs));
|
|
457
|
+
}
|
|
458
|
+
if (!targets.length) {
|
|
459
|
+
log(`openzoo: spend chip CDP :${port} has no pages yet`);
|
|
460
|
+
return { ok: false, injected: 0 };
|
|
461
|
+
}
|
|
462
|
+
let injected = 0;
|
|
463
|
+
for (const t of targets) {
|
|
464
|
+
try {
|
|
465
|
+
await injectTarget(t, source, connect);
|
|
466
|
+
injected += 1;
|
|
467
|
+
} catch (e) {
|
|
468
|
+
log(`openzoo: spend chip inject ${e.message}`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (injected) log(`openzoo: spend chip folded into ${injected} renderer(s)`);
|
|
472
|
+
return { ok: injected > 0, injected };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Fire-and-forget wrapper used by `openzoo bot`. */
|
|
476
|
+
export function injectSpendChipInBackground(opts = {}) {
|
|
477
|
+
const log = opts.log || ((m) => console.error(m));
|
|
478
|
+
injectSpendChip({ ...opts, log }).catch((e) => {
|
|
479
|
+
log(`openzoo: spend chip inject failed ${e.message}`);
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|