diff --git a/README.md b/README.md index c9e651c38..3cc1b9366 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,7 @@ OpenCLI is not only for websites. It can also: | `OPENCLI_LIVE` | `false` | Set to `1` to keep the automation lease open after an adapter command finishes (useful for inspection). The `--live` flag sets this. | | `OPENCLI_BROWSER_CONNECT_TIMEOUT` | `30` | Seconds to wait for browser connection | | `OPENCLI_BROWSER_COMMAND_TIMEOUT` | `60` | Seconds to wait for a single browser command | +| `OPENCLI_GEMINI_ASK_TIMEOUT` | `60` | Default seconds to wait for `opencli gemini ask` responses | | `OPENCLI_CDP_ENDPOINT` | — | Chrome DevTools Protocol endpoint for remote browser or Electron apps | | `OPENCLI_CDP_TARGET` | — | Filter CDP targets by URL substring (e.g. `detail.1688.com`) | | `OPENCLI_VERBOSE` | `false` | Enable verbose logging (`-v` flag also works) | diff --git a/README.zh-CN.md b/README.zh-CN.md index 8d1e279e9..c27d5b769 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -169,6 +169,7 @@ OpenCLI 不只是网站 CLI,还可以: | `OPENCLI_LIVE` | `false` | 设为 `1` 时 adapter 命令执行完后保留 automation 窗口不关闭(适合检查页面)。`--live` 标志会设置此变量 | | `OPENCLI_BROWSER_CONNECT_TIMEOUT` | `30` | 浏览器连接超时(秒) | | `OPENCLI_BROWSER_COMMAND_TIMEOUT` | `60` | 单个浏览器命令超时(秒) | +| `OPENCLI_GEMINI_ASK_TIMEOUT` | `60` | `opencli gemini ask` 等待回答的默认秒数 | | `OPENCLI_CDP_ENDPOINT` | — | Chrome DevTools Protocol 端点,用于远程浏览器或 Electron 应用 | | `OPENCLI_CDP_TARGET` | — | 按 URL 子串过滤 CDP target(如 `detail.1688.com`) | | `OPENCLI_VERBOSE` | `false` | 启用详细日志(`-v` 也可以) | diff --git a/cli-manifest.json b/cli-manifest.json index b55b50858..8e137fd75 100644 --- a/cli-manifest.json +++ b/cli-manifest.json @@ -6980,9 +6980,8 @@ { "name": "timeout", "type": "str", - "default": "60", "required": false, - "help": "Max seconds to wait (default: 60)" + "help": "Max seconds to wait (default: OPENCLI_GEMINI_ASK_TIMEOUT or 60)" }, { "name": "new", @@ -6995,7 +6994,7 @@ "columns": [ "response" ], - "timeout": 180, + "timeout": 3600, "type": "js", "modulePath": "gemini/ask.js", "sourceFile": "gemini/ask.js", diff --git a/clis/gemini/ask.js b/clis/gemini/ask.js index fef019f24..7329c579d 100644 --- a/clis/gemini/ask.js +++ b/clis/gemini/ask.js @@ -1,11 +1,17 @@ import { cli, Strategy } from '@jackwener/opencli/registry'; import { GEMINI_DOMAIN, readGeminiSnapshot, sendGeminiMessage, startNewGeminiChat, waitForGeminiResponse, waitForGeminiSubmission } from './utils.js'; +export function parseGeminiAskTimeout(value, fallback) { + const parsed = parseInt(String(value ?? ''), 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; +} function normalizeBooleanFlag(value) { if (typeof value === 'boolean') return value; const normalized = String(value ?? '').trim().toLowerCase(); return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on'; } +export const DEFAULT_GEMINI_ASK_TIMEOUT_SECONDS = parseGeminiAskTimeout(process.env.OPENCLI_GEMINI_ASK_TIMEOUT, 60); +const GEMINI_ASK_COMMAND_TIMEOUT_SECONDS = 3600; const NO_RESPONSE_PREFIX = '[NO RESPONSE]'; export const askCommand = cli({ site: 'gemini', @@ -16,16 +22,16 @@ export const askCommand = cli({ browser: true, navigateBefore: false, defaultFormat: 'plain', - timeoutSeconds: 180, + timeoutSeconds: GEMINI_ASK_COMMAND_TIMEOUT_SECONDS, args: [ { name: 'prompt', required: true, positional: true, help: 'Prompt to send' }, - { name: 'timeout', required: false, help: 'Max seconds to wait (default: 60)', default: '60' }, + { name: 'timeout', required: false, help: 'Max seconds to wait (default: OPENCLI_GEMINI_ASK_TIMEOUT or 60)' }, { name: 'new', required: false, help: 'Start a new chat first (true/false, default: false)', default: 'false' }, ], columns: ['response'], func: async (page, kwargs) => { const prompt = kwargs.prompt; - const timeout = parseInt(kwargs.timeout, 10) || 60; + const timeout = parseGeminiAskTimeout(kwargs.timeout, DEFAULT_GEMINI_ASK_TIMEOUT_SECONDS); const startFresh = normalizeBooleanFlag(kwargs.new); if (startFresh) await startNewGeminiChat(page); diff --git a/clis/gemini/ask.test.js b/clis/gemini/ask.test.js index ddcc50262..218ef8b1a 100644 --- a/clis/gemini/ask.test.js +++ b/clis/gemini/ask.test.js @@ -39,7 +39,7 @@ vi.mock('./utils.js', async () => { waitForGeminiResponse: mocks.waitForGeminiResponse, }; }); -import { askCommand } from './ask.js'; +import { DEFAULT_GEMINI_ASK_TIMEOUT_SECONDS, askCommand, parseGeminiAskTimeout } from './ask.js'; function createPageMock() { return { goto: vi.fn().mockResolvedValue(undefined), @@ -70,6 +70,29 @@ describe('gemini ask orchestration', () => { beforeEach(() => { vi.clearAllMocks(); }); + it('parses positive timeout values and falls back for invalid values', () => { + expect(parseGeminiAskTimeout('120', 60)).toBe(120); + expect(parseGeminiAskTimeout(300, 60)).toBe(300); + expect(parseGeminiAskTimeout('0', 60)).toBe(60); + expect(parseGeminiAskTimeout('-1', 60)).toBe(60); + expect(parseGeminiAskTimeout('not-a-number', 60)).toBe(60); + }); + it('uses the environment-backed default timeout when no timeout argument is provided', async () => { + const page = createPageMock(); + mocks.readGeminiSnapshot.mockResolvedValueOnce(baseline); + mocks.sendGeminiMessage.mockResolvedValueOnce('button'); + mocks.waitForGeminiSubmission.mockResolvedValueOnce(null); + const result = await askCommand.func(page, { prompt: '请只回复:OK', new: 'false' }); + expect(mocks.waitForGeminiSubmission).toHaveBeenCalledWith(page, baseline, DEFAULT_GEMINI_ASK_TIMEOUT_SECONDS); + expect(result).toEqual([{ response: `💬 [NO RESPONSE] No Gemini response within ${DEFAULT_GEMINI_ASK_TIMEOUT_SECONDS}s.` }]); + }); + it('keeps the outer browser command timeout high enough for long explicit waits', () => { + expect(askCommand.timeoutSeconds).toBeGreaterThanOrEqual(3600); + }); + it('leaves timeout without a static default so the runtime env default can apply', () => { + const timeoutArg = askCommand.args.find((arg) => arg.name === 'timeout'); + expect(timeoutArg.default).toBeUndefined(); + }); it('captures baseline, sends, waits for confirmed submission, then waits with the remaining timeout', async () => { vi.spyOn(Date, 'now') .mockReturnValueOnce(0)