Skip to content

Commit 8a8f4a1

Browse files
deepziyujackwener
andauthored
fix(antigravity): implement configurable timeout and auto-reconnect for serve (#859)
* fix(antigravity): implement configurable timeout and auto-reconnect for serve * fix(antigravity): avoid private runtime import * docs(antigravity): document serve timeout options --------- Co-authored-by: jackwener <jakevingoo@gmail.com>
1 parent ab44d9f commit 8a8f4a1

7 files changed

Lines changed: 111 additions & 34 deletions

File tree

README.zh-CN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ npm link
201201
| **discord-app** | `status` `send` `read` `channels` `servers` `search` `members` | 桌面端 |
202202
| **v2ex** | `hot` `latest` `topic` `node` `user` `member` `replies` `nodes` `daily` `me` `notifications` | 公开 / 浏览器 |
203203
| **xueqiu** | `feed` `hot-stock` `hot` `search` `stock` `comments` `watchlist` `earnings-date` `fund-holdings` `fund-snapshot` | 浏览器 |
204-
| **antigravity** | `status` `send` `read` `new` `dump` `extract-code` `model` `watch` | 桌面端 |
204+
| **antigravity** | `status` `send` `read` `new` `dump` `extract-code` `model` `watch` `serve` | 桌面端 |
205205
| **chatgpt-app** | `status` `new` `send` `read` `ask` `model` | 桌面端 |
206206
| **xiaohongshu** | `search` `notifications` `feed` `user` `download` `publish` `creator-notes` `creator-note-detail` `creator-notes-summary` `creator-profile` `creator-stats` | 浏览器 |
207207
| **xiaoe** | `courses` `detail` `catalog` `play-url` `content` | 浏览器 |

clis/antigravity/serve.js

Lines changed: 71 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,20 @@ function jsonResponse(res, status, data) {
5454
function sleep(ms) {
5555
return new Promise(resolve => setTimeout(resolve, ms));
5656
}
57+
function parseTimeoutValue(val, label, fallback) {
58+
if (val === undefined) {
59+
return fallback;
60+
}
61+
const parsed = typeof val === 'number' ? val : parseInt(String(val), 10);
62+
if (Number.isNaN(parsed) || parsed <= 0) {
63+
console.error(`[serve] Invalid ${label}="${val}", using default ${fallback}s`);
64+
return fallback;
65+
}
66+
return parsed;
67+
}
68+
function parseEnvTimeout(envVar, fallback) {
69+
return parseTimeoutValue(process.env[envVar], envVar, fallback);
70+
}
5771
// ─── DOM helpers ─────────────────────────────────────────────────────
5872
/**
5973
* Click the 'New Conversation' button to reset context.
@@ -267,41 +281,65 @@ async function waitForReply(page, beforeText, opts = {}) {
267281
let lastText = beforeText;
268282
let stableCount = 0;
269283
const stableThreshold = 4; // 4 * 500ms = 2s of stability fallback
284+
let reconnectCount = 0;
270285
while (Date.now() < deadline) {
271-
const generating = await isGenerating(page);
272-
const currentText = await getConversationText(page);
273-
const textChanged = currentText !== beforeText && currentText.length > 0;
274-
if (generating) {
275-
hasStartedGenerating = true;
276-
stableCount = 0; // Reset stability while generating
277-
}
278-
else {
279-
if (hasStartedGenerating) {
280-
// It actively generated and now it stopped -> DONE
281-
// Provide a small buffer to let React render the final message fully
282-
await sleep(500);
283-
return;
286+
try {
287+
const generating = await isGenerating(page);
288+
const currentText = await getConversationText(page);
289+
const textChanged = currentText !== beforeText && currentText.length > 0;
290+
if (generating) {
291+
hasStartedGenerating = true;
292+
stableCount = 0; // Reset stability while generating
284293
}
285-
// Fallback: If it never showed "Generating/Cancel", but text changed and is stable
286-
if (textChanged) {
287-
if (currentText === lastText) {
288-
stableCount++;
289-
if (stableCount >= stableThreshold) {
290-
return; // Text has been stable for 2 seconds -> DONE
294+
else {
295+
if (hasStartedGenerating) {
296+
// It actively generated and now it stopped -> DONE
297+
// Provide a small buffer to let React render the final message fully
298+
await sleep(500);
299+
return page;
300+
}
301+
// Fallback: If it never showed "Generating/Cancel", but text changed and is stable
302+
if (textChanged) {
303+
if (currentText === lastText) {
304+
stableCount++;
305+
if (stableCount >= stableThreshold) {
306+
return page; // Text has been stable for 2 seconds -> DONE
307+
}
308+
}
309+
else {
310+
stableCount = 0;
311+
lastText = currentText;
291312
}
292313
}
293-
else {
314+
}
315+
}
316+
catch (err) {
317+
const msg = err.message || String(err);
318+
const isSessionLoss = /closed|lost|not open|websocket/i.test(msg);
319+
if (opts.reconnect && isSessionLoss && reconnectCount < 2) {
320+
reconnectCount++;
321+
console.error(`[serve] CDP session loss detected (${msg}), attempting to reconnect (${reconnectCount}/2)...`);
322+
try {
323+
page = await opts.reconnect();
324+
// Reset stability tracking after reconnect
294325
stableCount = 0;
295-
lastText = currentText;
326+
lastText = beforeText;
327+
continue;
328+
}
329+
catch (reconnectErr) {
330+
console.error(`[serve] Reconnection failed: ${reconnectErr.message}`);
331+
throw err; // Throw original error if reconnection itself fails
296332
}
297333
}
334+
throw err;
298335
}
299336
await sleep(pollInterval);
300337
}
301-
throw new Error('Timeout waiting for Antigravity reply');
338+
throw new Error(`Timeout waiting for Antigravity reply after ${timeout / 1000}s`);
302339
}
303340
// ─── Request Handlers ────────────────────────────────────────────────
304-
async function handleMessages(body, page, bridge) {
341+
async function handleMessages(body, page, opts = {}) {
342+
const { bridge, timeout, reconnect } = opts;
305343
// Extract the last user message
306344
const userMessages = body.messages.filter(m => m.role === 'user');
307345
if (userMessages.length === 0) {
@@ -328,7 +366,7 @@ async function handleMessages(body, page, bridge) {
328366
await sendMessage(page, userText, bridge);
329367
// Poll for reply (change detection)
330368
console.error('[serve] Waiting for reply...');
331-
await waitForReply(page, beforeText);
369+
page = await waitForReply(page, beforeText, { timeout, reconnect });
332370
// Extract the actual reply text precisely from the DOM
333371
const replyText = await getLastAssistantReply(page, userText);
334372
console.error(`[serve] Got reply: "${replyText.slice(0, 80)}${replyText.length > 80 ? '...' : ''}"`);
@@ -349,6 +387,10 @@ async function handleMessages(body, page, bridge) {
349387
// ─── Server ──────────────────────────────────────────────────────────
350388
export async function startServe(opts = {}) {
351389
const port = opts.port ?? 8082;
390+
const envTimeoutSeconds = parseEnvTimeout('OPENCLI_ANTIGRAVITY_TIMEOUT', 120);
391+
const effectiveTimeoutSeconds = parseTimeoutValue(opts.timeout, '--timeout', envTimeoutSeconds);
392+
const effectiveTimeout = effectiveTimeoutSeconds * 1000;
393+
console.error(`[serve] Starting Antigravity API proxy on port ${port} (timeout: ${effectiveTimeout / 1000}s)`);
352394
// Lazy CDP connection — connect when first request comes in
353395
let cdp = null;
354396
let page = null;
@@ -462,7 +504,11 @@ export async function startServe(opts = {}) {
462504
}
463505
// Lazy connect on first request
464506
const activePage = await ensureConnected();
465-
const response = await handleMessages(body, activePage, cdp ?? undefined);
507+
const response = await handleMessages(body, activePage, {
508+
bridge: cdp,
509+
timeout: effectiveTimeout,
510+
reconnect: ensureConnected,
511+
});
466512
jsonResponse(res, 200, response);
467513
}
468514
finally {

docs/adapters/desktop/antigravity.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,21 @@ Quickly target and switch the active LLM engine. Example: `opencli antigravity m
4747

4848
### `opencli antigravity watch`
4949
A long-running, streaming process that continuously polls the Antigravity UI for chat updates and outputs them in real-time to standard output.
50+
51+
### `opencli antigravity serve`
52+
Start an Anthropic-compatible `/v1/messages` proxy server backed by the local Antigravity desktop app.
53+
54+
```bash
55+
opencli antigravity serve --port 8082
56+
opencli antigravity serve --timeout 300
57+
OPENCLI_ANTIGRAVITY_TIMEOUT=300 opencli antigravity serve
58+
```
59+
60+
- `--port <port>`: HTTP listen port, default `8082`
61+
- `--timeout <seconds>`: maximum time to wait for one reply before returning a timeout error, default `120`
62+
- `OPENCLI_ANTIGRAVITY_TIMEOUT`: default timeout in seconds when `--timeout` is not provided
63+
64+
Runtime notes:
65+
66+
- reply polling only reconnects on session-loss style CDP errors such as closed/lost websocket connections
67+
- reconnect attempts are bounded; DOM/logic errors are surfaced directly instead of being retried as reconnects

skills/opencli-usage/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ Type legend: 🌐 = Browser (needs Chrome login) · ✅ = Public API (no browser
142142

143143
| App | Commands |
144144
|-----|----------|
145-
| **antigravity** | `status` `send` `read` `new` `dump` `extract-code` `model` `watch` |
145+
| **antigravity** | `status` `send` `read` `new` `dump` `extract-code` `model` `watch` `serve` |
146146
| **chatgpt** | `status` `new` `send` `read` `ask` `model` |
147147
| **chatwise** | `status` `new` `send` `read` `ask` `model` `history` `export` `screenshot` |
148148
| **codex** | `status` `send` `read` `new` `dump` `extract-diff` `model` `ask` `screenshot` `history` `export` |

skills/opencli-usage/desktop.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ opencli doubao-app send "message" # 发送消息
9292

9393
```bash
9494
opencli antigravity status # 检查 CDP 连接状态
95-
opencli antigravity serve # 启动 Anthropic 兼容 API 代理
95+
opencli antigravity serve --timeout 300 # 启动 Anthropic 兼容 API 代理,等待回复最多 300s
9696
opencli antigravity dump # 导出 DOM 调试信息
9797
opencli antigravity extract-code # 提取对话中的代码块
9898
opencli antigravity model <name> # 切换底层模型
@@ -101,3 +101,5 @@ opencli antigravity read # 读取聊天记录
101101
opencli antigravity send "hello" # 发送文本到当前聊天框
102102
opencli antigravity watch # 流式监听增量消息
103103
```
104+
105+
也可以通过 `OPENCLI_ANTIGRAVITY_TIMEOUT=300` 设置 `serve` 的默认等待时长(单位:秒)。

src/cli.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1128,10 +1128,15 @@ cli({
11281128
.command('serve')
11291129
.description('Start Anthropic-compatible API proxy for Antigravity')
11301130
.option('--port <port>', 'Server port (default: 8082)', '8082')
1131+
.option('--timeout <seconds>', 'Maximum time to wait for a reply (default: 120s)')
11311132
.action(async (opts) => {
11321133
// @ts-expect-error JS adapter — no type declarations
11331134
const { startServe } = await import('../clis/antigravity/serve.js');
1134-
await startServe({ port: parseInt(opts.port) });
1135+
const { parseTimeoutValue } = await import('./runtime.js');
1136+
await startServe({
1137+
port: parseInt(opts.port, 10),
1138+
timeout: opts.timeout ? parseTimeoutValue(opts.timeout, '--timeout', 120) : undefined,
1139+
});
11351140
});
11361141

11371142
// ── Dynamic adapter commands ──────────────────────────────────────────────

src/runtime.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,23 @@ export function getBrowserFactory(site?: string): new () => IBrowserFactory {
1313
return BrowserBridge;
1414
}
1515

16-
function parseEnvTimeout(envVar: string, fallback: number): number {
17-
const raw = process.env[envVar];
18-
if (raw === undefined) return fallback;
19-
const parsed = parseInt(raw, 10);
16+
/**
17+
* Validates and parses a timeout value (seconds).
18+
*/
19+
export function parseTimeoutValue(val: string | number | undefined, label: string, fallback: number): number {
20+
if (val === undefined) return fallback;
21+
const parsed = typeof val === 'number' ? val : parseInt(String(val), 10);
2022
if (Number.isNaN(parsed) || parsed <= 0) {
21-
log.warn(`[runtime] Invalid ${envVar}="${raw}", using default ${fallback}s`);
23+
console.error(`[runtime] Invalid ${label}="${val}", using default ${fallback}s`);
2224
return fallback;
2325
}
2426
return parsed;
2527
}
2628

29+
export function parseEnvTimeout(envVar: string, fallback: number): number {
30+
return parseTimeoutValue(process.env[envVar], envVar, fallback);
31+
}
32+
2733
export const DEFAULT_BROWSER_CONNECT_TIMEOUT = parseEnvTimeout('OPENCLI_BROWSER_CONNECT_TIMEOUT', 30);
2834
export const DEFAULT_BROWSER_COMMAND_TIMEOUT = parseEnvTimeout('OPENCLI_BROWSER_COMMAND_TIMEOUT', 60);
2935
export const DEFAULT_BROWSER_EXPLORE_TIMEOUT = parseEnvTimeout('OPENCLI_BROWSER_EXPLORE_TIMEOUT', 120);

0 commit comments

Comments
 (0)