Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/cli/src/gemini.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ import { getUserStartupWarnings } from './utils/userStartupWarnings.js';
import { getCliVersion } from './utils/version.js';
import { writeStderrLine } from './utils/stdioHelpers.js';
import { computeWindowTitle } from './utils/windowTitle.js';
import { preconnectApi } from './utils/apiPreconnect.js';
import { startEarlyInputCapture } from './utils/earlyInputCapture.js';
import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js';
import { showResumeSessionPicker } from './ui/components/StandaloneSessionPicker.js';
import { initializeLlmOutputLanguage } from './utils/languageUtils.js';
Expand Down Expand Up @@ -212,6 +214,18 @@ export async function startInteractiveUI(
export async function main() {
setupUnhandledRejectionHandler();
const settings = loadSettings();

// 启动优化:预连接 API(与后续操作并行)
// 预估 authType(实际值在 loadCliConfig 中确定)
const probableAuthType =
process.env['QWEN_CODE_AUTH_TYPE'] ||
(settings.merged.security?.auth?.selectedType as string | undefined);
preconnectApi(probableAuthType, {
Comment thread
doudouOUC marked this conversation as resolved.
Resolved
settingsBaseUrl: settings.merged.security?.auth?.baseUrl as
| string
| undefined,
});

await cleanupCheckpoints();

let argv = await parseArguments();
Expand Down Expand Up @@ -382,6 +396,9 @@ export async function main() {
// input showing up in the output.
process.stdin.setRawMode(true);

// 启动优化:开始早期输入捕获
startEarlyInputCapture();

// This cleanup isn't strictly needed but may help in certain situations.
process.on('SIGTERM', () => {
process.stdin.setRawMode(wasRaw);
Expand Down
24 changes: 24 additions & 0 deletions packages/cli/src/ui/contexts/KeypressContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ import {
import { clipboardHasImage } from '../utils/clipboardUtils.js';

import { FOCUS_IN, FOCUS_OUT } from '../hooks/useFocus.js';
import {
stopEarlyInputCapture,
getAndClearCapturedInput,
} from '../../utils/earlyInputCapture.js';

const ESC = '\u001B';
export const PASTE_MODE_PREFIX = `${ESC}[200~`;
Expand Down Expand Up @@ -158,6 +162,10 @@ export function KeypressProvider({
setRawMode(true);
}

// 启动优化:停止早期输入捕获并获取已捕获的输入
stopEarlyInputCapture();
const capturedInput = getAndClearCapturedInput();

const keypressStream = new PassThrough();
let usePassthrough = false;
// Use passthrough mode when pasteWorkaround is enabled,
Expand Down Expand Up @@ -985,6 +993,22 @@ export function KeypressProvider({
stdin.on('keypress', handleKeypress);
}

// 启动优化:如果有捕获的输入,延迟处理后重放
if (capturedInput.length > 0) {
debugLogger.debug(
`Replaying ${capturedInput.length} bytes of captured input`,
);
// 在下一个事件循环中处理,确保订阅者已准备好
setImmediate(() => {
if (usePassthrough) {
keypressStream.write(capturedInput);
} else {
// 直接触发 stdin 的 data 事件
stdin.emit('data', capturedInput);
}
});
}

return () => {
if (usePassthrough) {
keypressStream.removeListener('keypress', handleKeypress);
Expand Down
141 changes: 141 additions & 0 deletions packages/cli/src/utils/apiPreconnect.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { preconnectApi, resetPreconnectState } from './apiPreconnect.js';

// Mock fetch
const mockFetch = vi.fn().mockResolvedValue(undefined);
global.fetch = mockFetch;

describe('apiPreconnect', () => {
beforeEach(() => {
resetPreconnectState();
mockFetch.mockClear();
mockFetch.mockResolvedValue(undefined);
delete process.env['HTTPS_PROXY'];
delete process.env['https_proxy'];
delete process.env['HTTP_PROXY'];
delete process.env['http_proxy'];
delete process.env['OPENAI_BASE_URL'];
delete process.env['ANTHROPIC_BASE_URL'];
delete process.env['GEMINI_BASE_URL'];
delete process.env['QWEN_CODE_DISABLE_PRECONNECT'];
delete process.env['NODE_EXTRA_CA_CERTS'];
delete process.env['SANDBOX'];
});

afterEach(() => {
vi.restoreAllMocks();
});

describe('shouldSkipPreconnect', () => {
it('should skip when HTTPS_PROXY is set', () => {
process.env['HTTPS_PROXY'] = 'http://proxy.example.com:8080';
preconnectApi('qwen-oauth');
expect(mockFetch).not.toHaveBeenCalled();
});

it('should skip when https_proxy is set', () => {
process.env['https_proxy'] = 'http://proxy.example.com:8080';
preconnectApi('qwen-oauth');
expect(mockFetch).not.toHaveBeenCalled();
});

it('should skip when HTTP_PROXY is set', () => {
process.env['HTTP_PROXY'] = 'http://proxy.example.com:8080';
preconnectApi('qwen-oauth');
expect(mockFetch).not.toHaveBeenCalled();
});

it('should skip when http_proxy is set', () => {
process.env['http_proxy'] = 'http://proxy.example.com:8080';
preconnectApi('qwen-oauth');
expect(mockFetch).not.toHaveBeenCalled();
});

it('should skip when NODE_EXTRA_CA_CERTS is set', () => {
process.env['NODE_EXTRA_CA_CERTS'] = '/path/to/ca.pem';
preconnectApi('qwen-oauth');
expect(mockFetch).not.toHaveBeenCalled();
});

it('should skip when custom baseUrl is set', () => {
preconnectApi('openai', { settingsBaseUrl: 'https://custom.api.com/v1' });
expect(mockFetch).not.toHaveBeenCalled();
});

it('should not skip when baseUrl is a default URL', () => {
preconnectApi('openai', { settingsBaseUrl: 'https://api.openai.com/v1' });
expect(mockFetch).toHaveBeenCalled();
});
});

describe('preconnect behavior', () => {
it('should use default baseUrl for qwen-oauth', () => {
preconnectApi('qwen-oauth');
expect(mockFetch).toHaveBeenCalledWith(
'https://coding.dashscope.aliyuncs.com',
expect.objectContaining({ method: 'HEAD' }),
);
});

it('should use default baseUrl for openai', () => {
preconnectApi('openai');
expect(mockFetch).toHaveBeenCalledWith(
'https://api.openai.com',
expect.objectContaining({ method: 'HEAD' }),
);
});

it('should use default baseUrl for anthropic', () => {
preconnectApi('anthropic');
expect(mockFetch).toHaveBeenCalledWith(
'https://api.anthropic.com',
expect.objectContaining({ method: 'HEAD' }),
);
});

it('should use settings baseUrl when available', () => {
preconnectApi('openai', {
settingsBaseUrl: 'https://custom.openai.com/v1',
});
// Should skip because it's a custom URL
expect(mockFetch).not.toHaveBeenCalled();
});

it('should use environment variable baseUrl when available', () => {
process.env['OPENAI_BASE_URL'] = 'https://custom.env.com/v1';
preconnectApi('openai');
// Should skip because it's a custom URL
expect(mockFetch).not.toHaveBeenCalled();
});

it('should not fire twice', () => {
preconnectApi('qwen-oauth');
preconnectApi('openai');
expect(mockFetch).toHaveBeenCalledTimes(1);
});

it('should handle fetch errors gracefully', async () => {
mockFetch.mockRejectedValue(new Error('Network error'));
// Should not throw
expect(() => preconnectApi('qwen-oauth')).not.toThrow();
});

it('should skip when QWEN_CODE_DISABLE_PRECONNECT is set', () => {
process.env['QWEN_CODE_DISABLE_PRECONNECT'] = '1';
preconnectApi('qwen-oauth');
expect(mockFetch).not.toHaveBeenCalled();
});

it('should skip in sandbox mode', () => {
process.env['SANDBOX'] = '1';
preconnectApi('qwen-oauth');
expect(mockFetch).not.toHaveBeenCalled();
});
});
});
Loading
Loading