mirror of
https://github.com/jackwener/OpenCLI.git
synced 2026-09-14 18:25:42 +08:00
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>
This commit is contained in:
+1
-1
@@ -201,7 +201,7 @@ npm link
|
||||
| **discord-app** | `status` `send` `read` `channels` `servers` `search` `members` | 桌面端 |
|
||||
| **v2ex** | `hot` `latest` `topic` `node` `user` `member` `replies` `nodes` `daily` `me` `notifications` | 公开 / 浏览器 |
|
||||
| **xueqiu** | `feed` `hot-stock` `hot` `search` `stock` `comments` `watchlist` `earnings-date` `fund-holdings` `fund-snapshot` | 浏览器 |
|
||||
| **antigravity** | `status` `send` `read` `new` `dump` `extract-code` `model` `watch` | 桌面端 |
|
||||
| **antigravity** | `status` `send` `read` `new` `dump` `extract-code` `model` `watch` `serve` | 桌面端 |
|
||||
| **chatgpt-app** | `status` `new` `send` `read` `ask` `model` | 桌面端 |
|
||||
| **xiaohongshu** | `search` `notifications` `feed` `user` `download` `publish` `creator-notes` `creator-note-detail` `creator-notes-summary` `creator-profile` `creator-stats` | 浏览器 |
|
||||
| **xiaoe** | `courses` `detail` `catalog` `play-url` `content` | 浏览器 |
|
||||
|
||||
+71
-25
@@ -54,6 +54,20 @@ function jsonResponse(res, status, data) {
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
function parseTimeoutValue(val, label, fallback) {
|
||||
if (val === undefined) {
|
||||
return fallback;
|
||||
}
|
||||
const parsed = typeof val === 'number' ? val : parseInt(String(val), 10);
|
||||
if (Number.isNaN(parsed) || parsed <= 0) {
|
||||
console.error(`[serve] Invalid ${label}="${val}", using default ${fallback}s`);
|
||||
return fallback;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
function parseEnvTimeout(envVar, fallback) {
|
||||
return parseTimeoutValue(process.env[envVar], envVar, fallback);
|
||||
}
|
||||
// ─── DOM helpers ─────────────────────────────────────────────────────
|
||||
/**
|
||||
* Click the 'New Conversation' button to reset context.
|
||||
@@ -267,41 +281,65 @@ async function waitForReply(page, beforeText, opts = {}) {
|
||||
let lastText = beforeText;
|
||||
let stableCount = 0;
|
||||
const stableThreshold = 4; // 4 * 500ms = 2s of stability fallback
|
||||
let reconnectCount = 0;
|
||||
while (Date.now() < deadline) {
|
||||
const generating = await isGenerating(page);
|
||||
const currentText = await getConversationText(page);
|
||||
const textChanged = currentText !== beforeText && currentText.length > 0;
|
||||
if (generating) {
|
||||
hasStartedGenerating = true;
|
||||
stableCount = 0; // Reset stability while generating
|
||||
}
|
||||
else {
|
||||
if (hasStartedGenerating) {
|
||||
// It actively generated and now it stopped -> DONE
|
||||
// Provide a small buffer to let React render the final message fully
|
||||
await sleep(500);
|
||||
return;
|
||||
try {
|
||||
const generating = await isGenerating(page);
|
||||
const currentText = await getConversationText(page);
|
||||
const textChanged = currentText !== beforeText && currentText.length > 0;
|
||||
if (generating) {
|
||||
hasStartedGenerating = true;
|
||||
stableCount = 0; // Reset stability while generating
|
||||
}
|
||||
// Fallback: If it never showed "Generating/Cancel", but text changed and is stable
|
||||
if (textChanged) {
|
||||
if (currentText === lastText) {
|
||||
stableCount++;
|
||||
if (stableCount >= stableThreshold) {
|
||||
return; // Text has been stable for 2 seconds -> DONE
|
||||
else {
|
||||
if (hasStartedGenerating) {
|
||||
// It actively generated and now it stopped -> DONE
|
||||
// Provide a small buffer to let React render the final message fully
|
||||
await sleep(500);
|
||||
return page;
|
||||
}
|
||||
// Fallback: If it never showed "Generating/Cancel", but text changed and is stable
|
||||
if (textChanged) {
|
||||
if (currentText === lastText) {
|
||||
stableCount++;
|
||||
if (stableCount >= stableThreshold) {
|
||||
return page; // Text has been stable for 2 seconds -> DONE
|
||||
}
|
||||
}
|
||||
else {
|
||||
stableCount = 0;
|
||||
lastText = currentText;
|
||||
}
|
||||
}
|
||||
else {
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
const msg = err.message || String(err);
|
||||
const isSessionLoss = /closed|lost|not open|websocket/i.test(msg);
|
||||
if (opts.reconnect && isSessionLoss && reconnectCount < 2) {
|
||||
reconnectCount++;
|
||||
console.error(`[serve] CDP session loss detected (${msg}), attempting to reconnect (${reconnectCount}/2)...`);
|
||||
try {
|
||||
page = await opts.reconnect();
|
||||
// Reset stability tracking after reconnect
|
||||
stableCount = 0;
|
||||
lastText = currentText;
|
||||
lastText = beforeText;
|
||||
continue;
|
||||
}
|
||||
catch (reconnectErr) {
|
||||
console.error(`[serve] Reconnection failed: ${reconnectErr.message}`);
|
||||
throw err; // Throw original error if reconnection itself fails
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
await sleep(pollInterval);
|
||||
}
|
||||
throw new Error('Timeout waiting for Antigravity reply');
|
||||
throw new Error(`Timeout waiting for Antigravity reply after ${timeout / 1000}s`);
|
||||
}
|
||||
// ─── Request Handlers ────────────────────────────────────────────────
|
||||
async function handleMessages(body, page, bridge) {
|
||||
async function handleMessages(body, page, opts = {}) {
|
||||
const { bridge, timeout, reconnect } = opts;
|
||||
// Extract the last user message
|
||||
const userMessages = body.messages.filter(m => m.role === 'user');
|
||||
if (userMessages.length === 0) {
|
||||
@@ -328,7 +366,7 @@ async function handleMessages(body, page, bridge) {
|
||||
await sendMessage(page, userText, bridge);
|
||||
// Poll for reply (change detection)
|
||||
console.error('[serve] Waiting for reply...');
|
||||
await waitForReply(page, beforeText);
|
||||
page = await waitForReply(page, beforeText, { timeout, reconnect });
|
||||
// Extract the actual reply text precisely from the DOM
|
||||
const replyText = await getLastAssistantReply(page, userText);
|
||||
console.error(`[serve] Got reply: "${replyText.slice(0, 80)}${replyText.length > 80 ? '...' : ''}"`);
|
||||
@@ -349,6 +387,10 @@ async function handleMessages(body, page, bridge) {
|
||||
// ─── Server ──────────────────────────────────────────────────────────
|
||||
export async function startServe(opts = {}) {
|
||||
const port = opts.port ?? 8082;
|
||||
const envTimeoutSeconds = parseEnvTimeout('OPENCLI_ANTIGRAVITY_TIMEOUT', 120);
|
||||
const effectiveTimeoutSeconds = parseTimeoutValue(opts.timeout, '--timeout', envTimeoutSeconds);
|
||||
const effectiveTimeout = effectiveTimeoutSeconds * 1000;
|
||||
console.error(`[serve] Starting Antigravity API proxy on port ${port} (timeout: ${effectiveTimeout / 1000}s)`);
|
||||
// Lazy CDP connection — connect when first request comes in
|
||||
let cdp = null;
|
||||
let page = null;
|
||||
@@ -462,7 +504,11 @@ export async function startServe(opts = {}) {
|
||||
}
|
||||
// Lazy connect on first request
|
||||
const activePage = await ensureConnected();
|
||||
const response = await handleMessages(body, activePage, cdp ?? undefined);
|
||||
const response = await handleMessages(body, activePage, {
|
||||
bridge: cdp,
|
||||
timeout: effectiveTimeout,
|
||||
reconnect: ensureConnected,
|
||||
});
|
||||
jsonResponse(res, 200, response);
|
||||
}
|
||||
finally {
|
||||
|
||||
@@ -47,3 +47,21 @@ Quickly target and switch the active LLM engine. Example: `opencli antigravity m
|
||||
|
||||
### `opencli antigravity watch`
|
||||
A long-running, streaming process that continuously polls the Antigravity UI for chat updates and outputs them in real-time to standard output.
|
||||
|
||||
### `opencli antigravity serve`
|
||||
Start an Anthropic-compatible `/v1/messages` proxy server backed by the local Antigravity desktop app.
|
||||
|
||||
```bash
|
||||
opencli antigravity serve --port 8082
|
||||
opencli antigravity serve --timeout 300
|
||||
OPENCLI_ANTIGRAVITY_TIMEOUT=300 opencli antigravity serve
|
||||
```
|
||||
|
||||
- `--port <port>`: HTTP listen port, default `8082`
|
||||
- `--timeout <seconds>`: maximum time to wait for one reply before returning a timeout error, default `120`
|
||||
- `OPENCLI_ANTIGRAVITY_TIMEOUT`: default timeout in seconds when `--timeout` is not provided
|
||||
|
||||
Runtime notes:
|
||||
|
||||
- reply polling only reconnects on session-loss style CDP errors such as closed/lost websocket connections
|
||||
- reconnect attempts are bounded; DOM/logic errors are surfaced directly instead of being retried as reconnects
|
||||
|
||||
@@ -142,7 +142,7 @@ Type legend: 🌐 = Browser (needs Chrome login) · ✅ = Public API (no browser
|
||||
|
||||
| App | Commands |
|
||||
|-----|----------|
|
||||
| **antigravity** | `status` `send` `read` `new` `dump` `extract-code` `model` `watch` |
|
||||
| **antigravity** | `status` `send` `read` `new` `dump` `extract-code` `model` `watch` `serve` |
|
||||
| **chatgpt** | `status` `new` `send` `read` `ask` `model` |
|
||||
| **chatwise** | `status` `new` `send` `read` `ask` `model` `history` `export` `screenshot` |
|
||||
| **codex** | `status` `send` `read` `new` `dump` `extract-diff` `model` `ask` `screenshot` `history` `export` |
|
||||
|
||||
@@ -92,7 +92,7 @@ opencli doubao-app send "message" # 发送消息
|
||||
|
||||
```bash
|
||||
opencli antigravity status # 检查 CDP 连接状态
|
||||
opencli antigravity serve # 启动 Anthropic 兼容 API 代理
|
||||
opencli antigravity serve --timeout 300 # 启动 Anthropic 兼容 API 代理,等待回复最多 300s
|
||||
opencli antigravity dump # 导出 DOM 调试信息
|
||||
opencli antigravity extract-code # 提取对话中的代码块
|
||||
opencli antigravity model <name> # 切换底层模型
|
||||
@@ -101,3 +101,5 @@ opencli antigravity read # 读取聊天记录
|
||||
opencli antigravity send "hello" # 发送文本到当前聊天框
|
||||
opencli antigravity watch # 流式监听增量消息
|
||||
```
|
||||
|
||||
也可以通过 `OPENCLI_ANTIGRAVITY_TIMEOUT=300` 设置 `serve` 的默认等待时长(单位:秒)。
|
||||
|
||||
+6
-1
@@ -1128,10 +1128,15 @@ cli({
|
||||
.command('serve')
|
||||
.description('Start Anthropic-compatible API proxy for Antigravity')
|
||||
.option('--port <port>', 'Server port (default: 8082)', '8082')
|
||||
.option('--timeout <seconds>', 'Maximum time to wait for a reply (default: 120s)')
|
||||
.action(async (opts) => {
|
||||
// @ts-expect-error JS adapter — no type declarations
|
||||
const { startServe } = await import('../clis/antigravity/serve.js');
|
||||
await startServe({ port: parseInt(opts.port) });
|
||||
const { parseTimeoutValue } = await import('./runtime.js');
|
||||
await startServe({
|
||||
port: parseInt(opts.port, 10),
|
||||
timeout: opts.timeout ? parseTimeoutValue(opts.timeout, '--timeout', 120) : undefined,
|
||||
});
|
||||
});
|
||||
|
||||
// ── Dynamic adapter commands ──────────────────────────────────────────────
|
||||
|
||||
+11
-5
@@ -13,17 +13,23 @@ export function getBrowserFactory(site?: string): new () => IBrowserFactory {
|
||||
return BrowserBridge;
|
||||
}
|
||||
|
||||
function parseEnvTimeout(envVar: string, fallback: number): number {
|
||||
const raw = process.env[envVar];
|
||||
if (raw === undefined) return fallback;
|
||||
const parsed = parseInt(raw, 10);
|
||||
/**
|
||||
* Validates and parses a timeout value (seconds).
|
||||
*/
|
||||
export function parseTimeoutValue(val: string | number | undefined, label: string, fallback: number): number {
|
||||
if (val === undefined) return fallback;
|
||||
const parsed = typeof val === 'number' ? val : parseInt(String(val), 10);
|
||||
if (Number.isNaN(parsed) || parsed <= 0) {
|
||||
log.warn(`[runtime] Invalid ${envVar}="${raw}", using default ${fallback}s`);
|
||||
console.error(`[runtime] Invalid ${label}="${val}", using default ${fallback}s`);
|
||||
return fallback;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export function parseEnvTimeout(envVar: string, fallback: number): number {
|
||||
return parseTimeoutValue(process.env[envVar], envVar, fallback);
|
||||
}
|
||||
|
||||
export const DEFAULT_BROWSER_CONNECT_TIMEOUT = parseEnvTimeout('OPENCLI_BROWSER_CONNECT_TIMEOUT', 30);
|
||||
export const DEFAULT_BROWSER_COMMAND_TIMEOUT = parseEnvTimeout('OPENCLI_BROWSER_COMMAND_TIMEOUT', 60);
|
||||
export const DEFAULT_BROWSER_EXPLORE_TIMEOUT = parseEnvTimeout('OPENCLI_BROWSER_EXPLORE_TIMEOUT', 120);
|
||||
|
||||
Reference in New Issue
Block a user