mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-04 20:13:18 +00:00
213 lines
7.2 KiB
TypeScript
213 lines
7.2 KiB
TypeScript
/**
|
|
* Proxy Resolver — Concern D
|
|
*
|
|
* Handles proxy configuration resolution, remote proxy reachability check,
|
|
* local-backend selection, and CLIProxy binary acquisition.
|
|
*
|
|
* Extracted from executor/index.ts to isolate the proxy-resolution concern.
|
|
* All log messages, error messages, and exit semantics are byte-identical to
|
|
* the original implementation.
|
|
*/
|
|
|
|
import { ProgressIndicator } from '../../utils/progress-indicator';
|
|
import { ok, fail, info, warn } from '../../utils/ui';
|
|
import {
|
|
ensureCLIProxyBinary,
|
|
getConfiguredBackend,
|
|
getPlusBackendUnavailableMessage,
|
|
} from '../binary-manager';
|
|
import { checkRemoteProxy } from '../services/remote-proxy-client';
|
|
import { CLIProxyProvider, CLIProxyBackend, PLUS_ONLY_PROVIDERS, ExecutorConfig } from '../types';
|
|
import { resolveProxyConfig } from '../proxy/proxy-config-resolver';
|
|
import { CLIPROXY_DEFAULT_PORT, validatePort } from '../config/config-generator';
|
|
import type { ResolvedProxyConfig } from '../types';
|
|
import type { UnifiedConfig } from '../../config/schemas/unified-config';
|
|
import { isNetworkError, handleNetworkError } from './retry-handler';
|
|
|
|
export interface ResolvedExecutorProxyConfig {
|
|
/** Resolved proxy config after merging CLI > ENV > config.yaml > defaults */
|
|
proxyConfig: ResolvedProxyConfig;
|
|
/** Args after proxy-related flags are stripped out */
|
|
argsWithoutProxy: string[];
|
|
/** Mutated executor config (port resolved and validated) */
|
|
cfg: ExecutorConfig;
|
|
}
|
|
|
|
/** Result returned from resolveExecutorProxy */
|
|
export interface ResolvedProxy extends ResolvedExecutorProxyConfig {
|
|
/** Whether to use the remote proxy (vs spawning a local one) */
|
|
useRemoteProxy: boolean;
|
|
/** Which local backend binary to use ('original' | 'plus') */
|
|
localBackend: CLIProxyBackend;
|
|
/** Absolute path to CLIProxy binary; undefined when useRemoteProxy=true */
|
|
binaryPath: string | undefined;
|
|
}
|
|
|
|
/** Dependencies injected by the orchestrator */
|
|
export interface ResolveExecutorProxyContext {
|
|
unifiedConfig: UnifiedConfig;
|
|
allProviders: CLIProxyProvider[];
|
|
verbose: boolean;
|
|
cfg: ExecutorConfig;
|
|
log: (msg: string) => void;
|
|
}
|
|
|
|
/**
|
|
* Resolves side-effect-free proxy configuration and strips proxy flags.
|
|
*
|
|
* Mutates `context.cfg.port` in-place (same as original orchestrator behaviour).
|
|
*/
|
|
export function resolveExecutorProxyConfig(
|
|
args: string[],
|
|
context: ResolveExecutorProxyContext
|
|
): ResolvedExecutorProxyConfig {
|
|
const { unifiedConfig, cfg, log } = context;
|
|
|
|
// Resolve proxy config from CLI flags > ENV > config.yaml > defaults
|
|
const cliproxyServerConfig = unifiedConfig.cliproxy_server;
|
|
const { config: proxyConfig, remainingArgs: argsWithoutProxy } = resolveProxyConfig(args, {
|
|
remote: cliproxyServerConfig?.remote
|
|
? {
|
|
enabled: cliproxyServerConfig.remote.enabled,
|
|
host: cliproxyServerConfig.remote.host,
|
|
port: cliproxyServerConfig.remote.port,
|
|
protocol: cliproxyServerConfig.remote.protocol,
|
|
auth_token: cliproxyServerConfig.remote.auth_token,
|
|
management_key: cliproxyServerConfig.remote.management_key,
|
|
timeout: cliproxyServerConfig.remote.timeout,
|
|
}
|
|
: undefined,
|
|
local: cliproxyServerConfig?.local
|
|
? {
|
|
port: cliproxyServerConfig.local.port,
|
|
auto_start: cliproxyServerConfig.local.auto_start,
|
|
}
|
|
: undefined,
|
|
});
|
|
|
|
// Port resolution and validation (mutates cfg in-place)
|
|
if (cfg.port && cfg.port !== CLIPROXY_DEFAULT_PORT) {
|
|
if (proxyConfig.port !== CLIPROXY_DEFAULT_PORT) {
|
|
cfg.port = proxyConfig.port;
|
|
}
|
|
} else if (proxyConfig.port !== CLIPROXY_DEFAULT_PORT) {
|
|
cfg.port = proxyConfig.port;
|
|
}
|
|
cfg.port = validatePort(cfg.port);
|
|
|
|
log(`Proxy mode: ${proxyConfig.mode}`);
|
|
if (proxyConfig.mode === 'remote') {
|
|
log(`Remote host: ${proxyConfig.host}:${proxyConfig.port} (${proxyConfig.protocol})`);
|
|
}
|
|
|
|
return { proxyConfig, argsWithoutProxy, cfg };
|
|
}
|
|
|
|
/**
|
|
* Resolves proxy configuration, checks remote reachability, selects the local
|
|
* backend, and ensures the CLIProxy binary is present when running locally.
|
|
*/
|
|
export async function resolveExecutorProxy(
|
|
resolvedConfig: ResolvedExecutorProxyConfig,
|
|
context: ResolveExecutorProxyContext
|
|
): Promise<ResolvedProxy> {
|
|
const { allProviders, verbose: _verbose } = context;
|
|
const { proxyConfig, argsWithoutProxy, cfg } = resolvedConfig;
|
|
|
|
// Check remote proxy reachability
|
|
let useRemoteProxy = false;
|
|
let localBackend: CLIProxyBackend = 'original';
|
|
|
|
if (proxyConfig.mode === 'remote' && proxyConfig.host) {
|
|
const status = await checkRemoteProxy({
|
|
host: proxyConfig.host,
|
|
port: proxyConfig.port,
|
|
protocol: proxyConfig.protocol,
|
|
authToken: proxyConfig.authToken,
|
|
timeout: proxyConfig.timeout ?? 2000,
|
|
allowSelfSigned: proxyConfig.allowSelfSigned ?? false,
|
|
});
|
|
|
|
if (status.reachable) {
|
|
useRemoteProxy = true;
|
|
console.log(
|
|
ok(
|
|
`Connected to remote proxy at ${proxyConfig.host}:${proxyConfig.port} (${status.latencyMs}ms)`
|
|
)
|
|
);
|
|
} else {
|
|
console.error(warn(`Remote proxy unreachable: ${status.error}`));
|
|
|
|
if (proxyConfig.remoteOnly) {
|
|
throw new Error('Remote proxy unreachable and --remote-only specified');
|
|
}
|
|
|
|
if (proxyConfig.fallbackEnabled) {
|
|
if (proxyConfig.autoStartLocal) {
|
|
console.log(info('Falling back to local proxy...'));
|
|
} else {
|
|
if (process.stdin.isTTY) {
|
|
const readline = await import('readline');
|
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
const answer = await new Promise<string>((resolve) => {
|
|
rl.question('Start local proxy instead? [Y/n] ', resolve);
|
|
});
|
|
rl.close();
|
|
if (answer.toLowerCase() === 'n') {
|
|
throw new Error('Remote proxy unreachable and user declined fallback');
|
|
}
|
|
}
|
|
console.log(info('Starting local proxy...'));
|
|
}
|
|
} else {
|
|
throw new Error('Remote proxy unreachable and fallback disabled');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Local backend selection (only when not using remote proxy)
|
|
if (!useRemoteProxy) {
|
|
localBackend = getConfiguredBackend({ notifyOnPlus: true });
|
|
|
|
for (const p of allProviders) {
|
|
if (localBackend === 'original' && PLUS_ONLY_PROVIDERS.includes(p as CLIProxyProvider)) {
|
|
console.error('');
|
|
console.error(fail(getPlusBackendUnavailableMessage(p)));
|
|
console.error('');
|
|
throw new Error(`Provider ${p} requires local CLIProxy Plus backend`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Binary acquisition — skipped when using remote proxy
|
|
let binaryPath: string | undefined;
|
|
|
|
if (!useRemoteProxy) {
|
|
const spinner = new ProgressIndicator('Preparing CLIProxy');
|
|
spinner.start();
|
|
|
|
try {
|
|
binaryPath = await ensureCLIProxyBinary(_verbose, { skipAutoUpdate: true });
|
|
spinner.succeed('CLIProxy binary ready');
|
|
} catch (error) {
|
|
spinner.fail('Failed to prepare CLIProxy');
|
|
const err = error as Error;
|
|
|
|
if (isNetworkError(err)) {
|
|
handleNetworkError(err);
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
return {
|
|
proxyConfig,
|
|
useRemoteProxy,
|
|
localBackend,
|
|
binaryPath,
|
|
argsWithoutProxy,
|
|
cfg,
|
|
};
|
|
}
|