vg-coder-cli 2.0.66 → 2.0.68
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/INTEGRATION.md +94 -0
- package/package.json +1 -1
- package/src/server/api-server.js +31 -1
- package/src/server/task-queue.js +98 -1
package/INTEGRATION.md
CHANGED
|
@@ -217,6 +217,8 @@ Server có thể chủ động list / đóng / mở tab AI Studio trong từng p
|
|
|
217
217
|
| `GET` | `/api/launcher/tabs` | `?chromeId=<uuid>` \| `?label=<email>` (optional) | List tabs trong profile (hoặc all profiles nếu bỏ pin) |
|
|
218
218
|
| `POST` | `/api/launcher/close-tab` | `{ chromeId? \| workerLabel?, tabId?, all? }` | Đóng tab cụ thể, hoặc tất cả tab AI Studio nếu bỏ `tabId` |
|
|
219
219
|
| `POST` | `/api/launcher/open-tab` | `{ chromeId? \| workerLabel?, model?, url?, active? }` | Mở tab mới. `model` mặc định `gemini-3-flash-preview`. Response v2.0.52+ kèm `requested_model` / `actual_model` / `fallback_occurred` (URL-based, **không reliable** với AI Studio versions mới — xem note) |
|
|
220
|
+
| `GET` | `/api/launcher/debug` | `?chromeId=<uuid>` \| `?workerLabel=<email>` (optional) | Dump launcher SW state: `swChromeId`, `syncStorage`, `localStorage`, AI Studio tabs, windows count, runtime info. Bỏ pin → broadcast all launchers. (v2.0.65+) |
|
|
221
|
+
| `POST` | `/api/launcher/exec` | `{ chromeId? \| workerLabel? \| all?, cmd, args?, timeoutMs? }` | Chạy 1 lệnh `chrome.*` đã định nghĩa sẵn trong launcher SW. (v2.0.66+, thay cho `eval` bị Manifest V3 CSP chặn) |
|
|
220
222
|
|
|
221
223
|
**Pin precedence**: `chromeId` > `workerLabel` > default. `chromeId` chỉ exact-match một
|
|
222
224
|
launcher cụ thể — dùng để address **profile chưa bind email** (mới cài extension,
|
|
@@ -243,6 +245,98 @@ curl -X POST -d '{"workerLabel":"alice@gmail.com","model":"gemini-3-flash-previe
|
|
|
243
245
|
-H 'Content-Type: application/json' http://127.0.0.1:6868/api/launcher/open-tab
|
|
244
246
|
```
|
|
245
247
|
|
|
248
|
+
### Remote launcher debug (v2.0.65+)
|
|
249
|
+
|
|
250
|
+
Hai endpoint dùng để inspect / điều khiển launcher SW từ xa thay vì phải mở
|
|
251
|
+
DevTools trong từng Chrome profile:
|
|
252
|
+
|
|
253
|
+
**`GET /api/launcher/debug`** — dump state cố định:
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
# Một launcher cụ thể
|
|
257
|
+
curl -s "http://127.0.0.1:6868/api/launcher/debug?chromeId=6b420bac-..." | jq
|
|
258
|
+
|
|
259
|
+
# Broadcast 5 launchers (bỏ pin)
|
|
260
|
+
curl -s http://127.0.0.1:6868/api/launcher/debug | jq
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Response per launcher:
|
|
264
|
+
|
|
265
|
+
```json
|
|
266
|
+
{
|
|
267
|
+
"ok": true,
|
|
268
|
+
"data": {
|
|
269
|
+
"swChromeId": "6b420bac-...",
|
|
270
|
+
"runtimeId": "comfeilpfnlaoijgndaikpniioglmonf",
|
|
271
|
+
"extVersion": "1.0.0",
|
|
272
|
+
"extName": "VetGo Pro",
|
|
273
|
+
"syncStorage": { "id": "6b420bac-..." },
|
|
274
|
+
"localStorage": {},
|
|
275
|
+
"profileInfo": null,
|
|
276
|
+
"socketId": "KFh5YmiCbYTGBmAzAAAB",
|
|
277
|
+
"serverUrl": "http://127.0.0.1:6868",
|
|
278
|
+
"tabs": {
|
|
279
|
+
"aistudio": [{ "id": 437926431, "url": "...", "status": "complete", "active": true }],
|
|
280
|
+
"totalCount": 12
|
|
281
|
+
},
|
|
282
|
+
"windows": 2,
|
|
283
|
+
"userAgent": "Mozilla/5.0 ..."
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Dùng để verify chromeId pipeline (launcher SW chromeId == `chrome.storage.sync.id`),
|
|
289
|
+
phát hiện profile có nhiều cửa sổ / quá nhiều tab, debug AI Studio tab missing.
|
|
290
|
+
|
|
291
|
+
**`POST /api/launcher/exec`** — chạy 1 lệnh đã định nghĩa sẵn trong launcher SW.
|
|
292
|
+
Manifest V3 cấm `new Function()` nên không có eval tự do — đây là tập 10 lệnh
|
|
293
|
+
chrome.* được whitelist, mỗi lệnh có schema args cố định:
|
|
294
|
+
|
|
295
|
+
| `cmd` | `args` | Trả về |
|
|
296
|
+
|---|---|---|
|
|
297
|
+
| `tabs.query` | `chrome.tabs.QueryInfo` (`{ url?, active?, ... }`) | Mảng tab info `{ id, windowId, url, title, status, active, ... }` |
|
|
298
|
+
| `tabs.get` | `{ tabId: number }` | 1 tab info |
|
|
299
|
+
| `tabs.update` | `{ tabId: number, ...props }` (`url`, `active`, `pinned`, ...) | `{ id, url, active }` |
|
|
300
|
+
| `tabs.reload` | `{ tabId: number, bypassCache?: bool }` | `{ ok: true, tabId }` |
|
|
301
|
+
| `storage.sync.get` | `{ keys?: string \| string[] \| null }` (null = all) | `Record<string, unknown>` |
|
|
302
|
+
| `storage.sync.set` | `{ items: Record<string, unknown> }` | `{ ok: true }` |
|
|
303
|
+
| `storage.local.get` | `{ keys?: ... }` | `Record<string, unknown>` |
|
|
304
|
+
| `runtime.reload` | — | `{ ok: true, scheduled: true }` (extension reload sau ~50ms; ack có thể không tới do SW chết) |
|
|
305
|
+
| `windows.list` | — | Mảng window info `{ id, focused, type, state, incognito }` |
|
|
306
|
+
| `cookies.get` | `{ url: string, name?: string }` | 1 cookie nếu `name`, mảng cookies nếu chỉ `url`. Mỗi cookie: `{ name, value, domain, path, expirationDate, secure, httpOnly }` |
|
|
307
|
+
|
|
308
|
+
Unknown `cmd` → `{ ok: false, error: "unknown_cmd", available: [...] }` (discovery).
|
|
309
|
+
|
|
310
|
+
Ví dụ:
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
# Phải có chromeId nào sync với storage UUID không?
|
|
314
|
+
curl -s -X POST -H 'Content-Type: application/json' \
|
|
315
|
+
-d '{"chromeId":"6b420bac-...","cmd":"storage.sync.get"}' \
|
|
316
|
+
http://127.0.0.1:6868/api/launcher/exec | jq '.value'
|
|
317
|
+
# → { "id": "6b420bac-..." }
|
|
318
|
+
|
|
319
|
+
# Profile login Google chưa?
|
|
320
|
+
curl -s -X POST -H 'Content-Type: application/json' \
|
|
321
|
+
-d '{"chromeId":"6b420bac-...","cmd":"cookies.get","args":{"url":"https://accounts.google.com","name":"SID"}}' \
|
|
322
|
+
http://127.0.0.1:6868/api/launcher/exec | jq '.value'
|
|
323
|
+
# → { "name": "SID", "value": "...", ... } nếu đã login, null nếu chưa
|
|
324
|
+
|
|
325
|
+
# Reload extension (debug last resort)
|
|
326
|
+
curl -s -X POST -H 'Content-Type: application/json' \
|
|
327
|
+
-d '{"chromeId":"6b420bac-...","cmd":"runtime.reload"}' \
|
|
328
|
+
http://127.0.0.1:6868/api/launcher/exec
|
|
329
|
+
|
|
330
|
+
# Broadcast: tab AI Studio đang mở ở profile nào?
|
|
331
|
+
curl -s -X POST -H 'Content-Type: application/json' \
|
|
332
|
+
-d '{"all":true,"cmd":"tabs.query","args":{"url":"*://aistudio.google.com/*"}}' \
|
|
333
|
+
http://127.0.0.1:6868/api/launcher/exec \
|
|
334
|
+
| jq '.[] | { chromeId: .launcher.chromeId, tabs: (.value | length) }'
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
**Thêm command mới** → sửa `execCommands` map trong [vetgo-auto/chrome/src/launcher.ts](vetgo-auto/chrome/src/launcher.ts),
|
|
338
|
+
rebuild + bump version + reload extension trong từng Chrome profile.
|
|
339
|
+
|
|
246
340
|
`open-tab` response (v2.0.52+):
|
|
247
341
|
|
|
248
342
|
```json
|
package/package.json
CHANGED
package/src/server/api-server.js
CHANGED
|
@@ -99,7 +99,12 @@ class ApiServer {
|
|
|
99
99
|
|
|
100
100
|
// Task worker registration & lifecycle
|
|
101
101
|
socket.on('worker:register', (meta) => { taskQueue.setWorker(socket, meta || {}); });
|
|
102
|
-
socket.on('worker:heartbeat', () => {
|
|
102
|
+
socket.on('worker:heartbeat', () => {
|
|
103
|
+
// Bump lastSeen so the stale-worker reaper doesn't drop a live
|
|
104
|
+
// socket that's just been quiet (no task in flight).
|
|
105
|
+
const w = taskQueue.workers.get(socket.id);
|
|
106
|
+
if (w) w.lastSeen = Date.now();
|
|
107
|
+
});
|
|
103
108
|
socket.on('task:complete', (payload) => {
|
|
104
109
|
if (payload?.taskId) taskQueue.onWorkerComplete(payload.taskId, payload, socket.id);
|
|
105
110
|
});
|
|
@@ -799,6 +804,31 @@ class ApiServer {
|
|
|
799
804
|
} catch (e) { res.status(launcherErr(e)).json({ error: e.message }); }
|
|
800
805
|
});
|
|
801
806
|
|
|
807
|
+
// Block until the pinned launcher is ready for tasks. Replaces the common
|
|
808
|
+
// open-tab → poll-launchers → poll-workers pattern with a single sync
|
|
809
|
+
// call. Body: { chromeId, timeoutMs?: 15000, requireEmail?: true,
|
|
810
|
+
// requireWorker?: true }
|
|
811
|
+
// - 200 { ok: true, launcher, worker, elapsedMs } when ready
|
|
812
|
+
// - 404 launcher_not_found if chromeId never matched a launcher
|
|
813
|
+
// - 504 wait_ready_timeout if the readiness condition didn't hold in time
|
|
814
|
+
this.app.post('/api/launcher/wait-ready', async (req, res) => {
|
|
815
|
+
try {
|
|
816
|
+
const body = req.body || {};
|
|
817
|
+
const chromeId = (body.chromeId || '').toString().trim();
|
|
818
|
+
if (!chromeId) return res.status(400).json({ error: 'chromeId required' });
|
|
819
|
+
const result = await taskQueue.waitReady(chromeId, {
|
|
820
|
+
timeoutMs: body.timeoutMs,
|
|
821
|
+
requireEmail: body.requireEmail,
|
|
822
|
+
requireWorker: body.requireWorker,
|
|
823
|
+
});
|
|
824
|
+
res.json(result);
|
|
825
|
+
} catch (e) {
|
|
826
|
+
if (e.code === 'launcher_not_found') return res.status(404).json({ error: 'launcher_not_found' });
|
|
827
|
+
if (e.code === 'wait_ready_timeout') return res.status(504).json({ error: 'wait_ready_timeout', details: e.details });
|
|
828
|
+
res.status(500).json({ error: e.message });
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
|
|
802
832
|
// Run a predefined chrome.* command inside the launcher SW. Manifest V3
|
|
803
833
|
// CSP forbids new Function() in service workers, so eval was replaced by
|
|
804
834
|
// a fixed command map. Body:
|
package/src/server/task-queue.js
CHANGED
|
@@ -9,6 +9,13 @@ const AUTO_LAUNCH_DEBOUNCE_MS = 30_000;
|
|
|
9
9
|
const INTER_TASK_GAP_MS = 2_500;
|
|
10
10
|
const RATE_LIMIT_COOLDOWN_MS = 30 * 60_000;
|
|
11
11
|
const COOLDOWN_TICK_MS = 60_000;
|
|
12
|
+
// Reap workers whose socket went silent: no heartbeat AND no task activity
|
|
13
|
+
// past this window. Heartbeat fires every 5s from task-worker.js, so 120s
|
|
14
|
+
// is ~24 missed beats — comfortably past any legitimate network hiccup but
|
|
15
|
+
// short enough that zombie sockets (Chrome tab closed without disconnect
|
|
16
|
+
// event reaching server, content-script paused by power-saver, etc.) don't
|
|
17
|
+
// linger forever and bloat _drain's idleWorkers snapshot.
|
|
18
|
+
const STALE_WORKER_TTL_MS = 120_000;
|
|
12
19
|
const MAX_TASK_ATTEMPTS = 3;
|
|
13
20
|
const RL_CODES = new Set(['rate_limit_exceeded', 'quota_exceeded', 'generation_failed']);
|
|
14
21
|
|
|
@@ -269,6 +276,62 @@ class TaskQueue {
|
|
|
269
276
|
}
|
|
270
277
|
}
|
|
271
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Poll until a launcher's profile is "ready" for tasks: launcher connected,
|
|
281
|
+
* email scraped (if requireEmail), and ≥1 idle worker registered for the
|
|
282
|
+
* chromeId (if requireWorker). Saves callers from running their own
|
|
283
|
+
* GET /api/launchers + /api/workers poll loop after open-tab.
|
|
284
|
+
*
|
|
285
|
+
* Returns a snapshot on success; throws on timeout or immediate-miss.
|
|
286
|
+
*/
|
|
287
|
+
async waitReady(chromeId, opts = {}) {
|
|
288
|
+
const timeoutMs = Math.min(Math.max(opts.timeoutMs | 0 || 15_000, 1_000), 60_000);
|
|
289
|
+
const requireEmail = opts.requireEmail !== false;
|
|
290
|
+
const requireWorker = opts.requireWorker !== false;
|
|
291
|
+
const pollMs = 250;
|
|
292
|
+
const deadline = Date.now() + timeoutMs;
|
|
293
|
+
|
|
294
|
+
// Immediate-miss: chromeId never matched a launcher → 404 contract
|
|
295
|
+
// (vs. timeout). Caller can map differently.
|
|
296
|
+
const hasLauncher = () => [...this.launchers.values()].some(l => l.chromeId === chromeId);
|
|
297
|
+
if (!hasLauncher()) {
|
|
298
|
+
const err = new Error('launcher_not_found');
|
|
299
|
+
err.code = 'launcher_not_found';
|
|
300
|
+
throw err;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
while (Date.now() < deadline) {
|
|
304
|
+
const launcher = [...this.launchers.values()].find(l => l.chromeId === chromeId);
|
|
305
|
+
if (launcher) {
|
|
306
|
+
const emailOk = !requireEmail || (launcher.email && !launcher.email.startsWith('unknown:'));
|
|
307
|
+
const worker = !requireWorker ? null : [...this.workers.values()].find(w =>
|
|
308
|
+
w.chromeId === chromeId
|
|
309
|
+
&& w.status === 'idle'
|
|
310
|
+
&& w.email && !w.email.startsWith('unknown:')
|
|
311
|
+
);
|
|
312
|
+
const workerOk = !requireWorker || !!worker;
|
|
313
|
+
if (emailOk && workerOk) {
|
|
314
|
+
return {
|
|
315
|
+
ok: true,
|
|
316
|
+
launcher: { id: launcher.id, chromeId: launcher.chromeId, email: launcher.email },
|
|
317
|
+
worker: worker ? { id: worker.id, email: worker.email, status: worker.status } : null,
|
|
318
|
+
elapsedMs: timeoutMs - (deadline - Date.now()),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
// Launcher disconnected during the wait — surface as not_found, not timeout.
|
|
323
|
+
const err = new Error('launcher_not_found');
|
|
324
|
+
err.code = 'launcher_not_found';
|
|
325
|
+
throw err;
|
|
326
|
+
}
|
|
327
|
+
await new Promise(r => setTimeout(r, pollMs));
|
|
328
|
+
}
|
|
329
|
+
const err = new Error('wait_ready_timeout');
|
|
330
|
+
err.code = 'wait_ready_timeout';
|
|
331
|
+
err.details = { chromeId, requireEmail, requireWorker, timeoutMs };
|
|
332
|
+
throw err;
|
|
333
|
+
}
|
|
334
|
+
|
|
272
335
|
// Reverse lookup: find chromeId bound to email. Returns the first match —
|
|
273
336
|
// if two launchers login the same email (test vs prod), this is ambiguous
|
|
274
337
|
// and the caller should prefer explicit chromeId routing instead.
|
|
@@ -818,13 +881,23 @@ class TaskQueue {
|
|
|
818
881
|
}
|
|
819
882
|
|
|
820
883
|
async _dispatch(task, worker) {
|
|
884
|
+
// Mark worker busy synchronously BEFORE any await so that concurrent
|
|
885
|
+
// _drain() ticks (each enqueue schedules its own setImmediate(_drain))
|
|
886
|
+
// don't both observe the same worker as idle and dispatch two tasks to
|
|
887
|
+
// it. Previously `worker.status = 'busy'` happened after `await
|
|
888
|
+
// store.saveTask` — the await yielded, a second drain saw worker idle,
|
|
889
|
+
// and a second task was dispatched onto the same worker. The worker can
|
|
890
|
+
// only run one task at a time, so the second one would hang in cache
|
|
891
|
+
// status='running' forever (worker.currentTaskId was overwritten and
|
|
892
|
+
// onWorkerComplete's `worker.currentTaskId !== taskId` guard rejected
|
|
893
|
+
// the late completion as stale).
|
|
821
894
|
task.status = 'running';
|
|
822
895
|
task.timing.startedAt = Date.now();
|
|
823
896
|
task.worker = { socketId: worker.id, email: worker.email, ...(worker.meta || {}) };
|
|
824
|
-
await store.saveTask(task);
|
|
825
897
|
this.cache.set(task.id, task);
|
|
826
898
|
worker.currentTaskId = task.id;
|
|
827
899
|
worker.status = 'busy';
|
|
900
|
+
await store.saveTask(task);
|
|
828
901
|
|
|
829
902
|
const port = this.io?.httpServer?.address?.()?.port || 6868;
|
|
830
903
|
const baseUrl = `http://127.0.0.1:${port}`;
|
|
@@ -863,9 +936,33 @@ class TaskQueue {
|
|
|
863
936
|
console.log(chalk.green(`[TaskQueue] Worker ${w.email} cooldown ended`));
|
|
864
937
|
}
|
|
865
938
|
}
|
|
939
|
+
this._reapStaleWorkers(now);
|
|
866
940
|
if (revived) setImmediate(() => this._drain());
|
|
867
941
|
}
|
|
868
942
|
|
|
943
|
+
// Drop workers whose socket has been silent past STALE_WORKER_TTL_MS. Skips
|
|
944
|
+
// busy/recycling workers — those are mid-task or mid-recycle and may
|
|
945
|
+
// legitimately not heartbeat until they emit task:complete. Reaping a
|
|
946
|
+
// zombie worker also triggers _drain because the removal frees a slot
|
|
947
|
+
// that the legitimate launcher can refill when it reconnects.
|
|
948
|
+
_reapStaleWorkers(now = Date.now()) {
|
|
949
|
+
let reaped = 0;
|
|
950
|
+
for (const w of [...this.workers.values()]) {
|
|
951
|
+
if (w.status === 'busy' || w.status === 'recycling') continue;
|
|
952
|
+
const idleFor = now - (w.lastSeen || 0);
|
|
953
|
+
if (idleFor <= STALE_WORKER_TTL_MS) continue;
|
|
954
|
+
console.log(chalk.yellow(
|
|
955
|
+
`[TaskQueue] Reaping stale worker ${w.id} (${w.email || 'unknown'}, idle ${Math.round(idleFor / 1000)}s)`
|
|
956
|
+
));
|
|
957
|
+
// Drop from registry. Don't try to disconnect the socket — if it's
|
|
958
|
+
// genuinely alive, _drain will skip dispatching to it, and the next
|
|
959
|
+
// worker:heartbeat or worker:register from that socket re-adds it.
|
|
960
|
+
this.workers.delete(w.id);
|
|
961
|
+
reaped++;
|
|
962
|
+
}
|
|
963
|
+
if (reaped) setImmediate(() => this._drain());
|
|
964
|
+
}
|
|
965
|
+
|
|
869
966
|
_autoLaunchBrowser(task = null) {
|
|
870
967
|
const now = Date.now();
|
|
871
968
|
if (now - this.launchedAt < AUTO_LAUNCH_DEBOUNCE_MS) return;
|