Skip to content

Commit 5ce7b73

Browse files
committed
feat: add HTTP/HTTPS/SOCKS5 proxy support
- Add src/common/proxy.ts: resolve proxy from env vars (HTTP_PROXY, HTTPS_PROXY, SOCKS_PROXY, NO_PROXY) and settings.json env field - Integrate undici ProxyAgent into OpenAI client (openai-client.ts) - Route telemetry and web-search requests through proxyFetch - Support DEEPCODE_HTTPS_PROXY prefixed env vars - NO_PROXY matching: exact host, subdomain wildcard, and * bypass - Priority: user settings < project settings < system env vars - Update configuration docs (zh + en) with proxy section
1 parent 7799f52 commit 5ce7b73

6 files changed

Lines changed: 341 additions & 4 deletions

File tree

docs/configuration.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ Deep Code 使用 `settings.json` 设置文件进行持久化配置,支持两
4949
| `REASONING_EFFORT` | string | 推理强度 |
5050
| `DEBUG_LOG_ENABLED` | string | 是否启用调试日志输出 |
5151
| `TELEMETRY_ENABLED` | string | 是否启用匿名使用数据上报 |
52+
| `HTTP_PROXY` | string | HTTP 代理地址,例如 `"http://127.0.0.1:7890"` |
53+
| `HTTPS_PROXY` | string | HTTPS 代理地址,例如 `"http://127.0.0.1:7890"` |
54+
| `SOCKS_PROXY` | string | SOCKS5 代理地址,例如 `"socks5://127.0.0.1:1080"` |
55+
| `NO_PROXY` | string | 不走代理的地址列表,逗号分隔,例如 `"localhost,127.0.0.1,.example.com"` |
5256
| `<其他任意KEY>` | string | 自定义环境变量 |
5357

5458
#### `thinkingEnabled` — 思考模式
@@ -197,3 +201,72 @@ DEEPCODE_TELEMETRY_ENABLED=0 deepcode
197201
3. 项目级settings.json: `{"mcpServers":{"github":{"env":{"GITHUB_PERSONAL_ACCESS_TOKEN":"..."}}}}`
198202
4. 项目级settings.json: `{"env": {"MCP_GITHUB_PERSONAL_ACCESS_TOKEN": "..."}}`
199203
5. 系统环境变量: `DEEPCODE_MCP_GITHUB_PERSONAL_ACCESS_TOKEN=... deepcode`
204+
205+
## 代理配置
206+
207+
Deep Code 支持通过 HTTP/HTTPS/SOCKS5 代理发送所有网络请求,包括 API 调用、遥测上报和联网搜索。
208+
209+
### 支持的代理变量
210+
211+
| 变量名 | 说明 | 示例 |
212+
| -------------- | ----------------------------------------------------------- | --------------------------------------- |
213+
| `HTTPS_PROXY` | HTTPS 代理(优先级最高) | `http://127.0.0.1:7890` |
214+
| `HTTP_PROXY` | HTTP 代理 | `http://127.0.0.1:7890` |
215+
| `SOCKS_PROXY` | SOCKS5 代理 | `socks5://127.0.0.1:1080` |
216+
| `SOCKS5_PROXY` | SOCKS5 代理(`SOCKS_PROXY` 的别名) | `socks5://127.0.0.1:1080` |
217+
| `NO_PROXY` | 不走代理的地址列表(逗号分隔,支持 `*``.example.com`| `localhost,127.0.0.1,.internal.corp` |
218+
219+
### 配置方式
220+
221+
#### 一、通过 Shell 环境变量设置(优先级最高)
222+
223+
**Bash / Zsh:**
224+
225+
```bash
226+
export HTTPS_PROXY=http://127.0.0.1:7890
227+
export NO_PROXY=localhost,127.0.0.1
228+
deepcode
229+
```
230+
231+
**PowerShell:**
232+
233+
```powershell
234+
$env:HTTPS_PROXY = "http://127.0.0.1:7890"
235+
$env:NO_PROXY = "localhost,127.0.0.1"
236+
deepcode
237+
```
238+
239+
也可以使用 `DEEPCODE_` 前缀:
240+
241+
```bash
242+
DEEPCODE_HTTPS_PROXY=http://127.0.0.1:7890 deepcode
243+
```
244+
245+
#### 二、通过 `settings.json``env` 字段配置
246+
247+
```json
248+
{
249+
"env": {
250+
"HTTPS_PROXY": "http://127.0.0.1:7890",
251+
"NO_PROXY": "localhost,127.0.0.1"
252+
}
253+
}
254+
```
255+
256+
### 优先级
257+
258+
按以下优先级顺序应用(数字较小的会被数字较大的覆盖):
259+
260+
1. 用户级 `settings.json``{"env": {"HTTPS_PROXY": "..."}}`
261+
2. 项目级 `settings.json``{"env": {"HTTPS_PROXY": "..."}}`
262+
3. 系统环境变量:`HTTPS_PROXY=... deepcode``DEEPCODE_HTTPS_PROXY=... deepcode`
263+
264+
### `NO_PROXY` 匹配规则
265+
266+
| 模式 | 说明 |
267+
| --------------- | ---------------------------------------------- |
268+
| `*` | 所有地址均不走代理 |
269+
| `localhost` | 精确匹配 `localhost` |
270+
| `127.0.0.1` | 精确匹配 `127.0.0.1` |
271+
| `.example.com` | 匹配 `example.com` 及其所有子域名(如 `api.example.com`|
272+
| `example.com` | 匹配 `example.com` 及其所有子域名 |

docs/configuration_en.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ The following are all the top-level fields supported in `settings.json`, along w
4949
| `REASONING_EFFORT`| string | Reasoning intensity |
5050
| `DEBUG_LOG_ENABLED`| string| Enable debug log output |
5151
| `TELEMETRY_ENABLED`| string| Enable anonymous usage reporting |
52+
| `HTTP_PROXY` | string| HTTP proxy URL, e.g. `"http://127.0.0.1:7890"` |
53+
| `HTTPS_PROXY` | string| HTTPS proxy URL, e.g. `"http://127.0.0.1:7890"` |
54+
| `SOCKS_PROXY` | string| SOCKS5 proxy URL, e.g. `"socks5://127.0.0.1:1080"` |
55+
| `NO_PROXY` | string| Comma-separated list of hosts to bypass proxy, e.g. `"localhost,127.0.0.1,.example.com"` |
5256
| `<any other KEY>` | string | Custom environment variable |
5357

5458
#### `thinkingEnabled` — Thinking Mode
@@ -196,3 +200,72 @@ Applied in the following priority order (lower-numbered overridden by higher-num
196200
3. Project-level settings.json: `{"mcpServers":{"github":{"env":{"GITHUB_PERSONAL_ACCESS_TOKEN":"..."}}}}`
197201
4. Project-level settings.json: `{"env": {"MCP_GITHUB_PERSONAL_ACCESS_TOKEN": "..."}}`
198202
5. System environment variable: `DEEPCODE_MCP_GITHUB_PERSONAL_ACCESS_TOKEN=... deepcode`
203+
204+
## Proxy Configuration
205+
206+
Deep Code supports routing all network traffic (API calls, telemetry reporting, and web search) through HTTP, HTTPS, or SOCKS5 proxies.
207+
208+
### Supported Proxy Variables
209+
210+
| Variable | Description | Example |
211+
| -------------- | --------------------------------------------------------------- | ---------------------------------------- |
212+
| `HTTPS_PROXY` | HTTPS proxy (highest priority) | `http://127.0.0.1:7890` |
213+
| `HTTP_PROXY` | HTTP proxy | `http://127.0.0.1:7890` |
214+
| `SOCKS_PROXY` | SOCKS5 proxy | `socks5://127.0.0.1:1080` |
215+
| `SOCKS5_PROXY` | SOCKS5 proxy (alias for `SOCKS_PROXY`) | `socks5://127.0.0.1:1080` |
216+
| `NO_PROXY` | Comma-separated list of hosts to bypass proxy (supports `*`, `.example.com`) | `localhost,127.0.0.1,.internal.corp` |
217+
218+
### Configuration Methods
219+
220+
#### 1. Shell Environment Variables (highest priority)
221+
222+
**Bash / Zsh:**
223+
224+
```bash
225+
export HTTPS_PROXY=http://127.0.0.1:7890
226+
export NO_PROXY=localhost,127.0.0.1
227+
deepcode
228+
```
229+
230+
**PowerShell:**
231+
232+
```powershell
233+
$env:HTTPS_PROXY = "http://127.0.0.1:7890"
234+
$env:NO_PROXY = "localhost,127.0.0.1"
235+
deepcode
236+
```
237+
238+
You can also use the `DEEPCODE_` prefix:
239+
240+
```bash
241+
DEEPCODE_HTTPS_PROXY=http://127.0.0.1:7890 deepcode
242+
```
243+
244+
#### 2. `settings.json` `env` field
245+
246+
```json
247+
{
248+
"env": {
249+
"HTTPS_PROXY": "http://127.0.0.1:7890",
250+
"NO_PROXY": "localhost,127.0.0.1"
251+
}
252+
}
253+
```
254+
255+
### Priority
256+
257+
Applied in the following priority order (lower-numbered overridden by higher-numbered):
258+
259+
1. User-level `settings.json`: `{"env": {"HTTPS_PROXY": "..."}}`
260+
2. Project-level `settings.json`: `{"env": {"HTTPS_PROXY": "..."}}`
261+
3. System environment variable: `HTTPS_PROXY=... deepcode` or `DEEPCODE_HTTPS_PROXY=... deepcode`
262+
263+
### `NO_PROXY` Matching Rules
264+
265+
| Pattern | Description |
266+
| --------------- | --------------------------------------------------------------- |
267+
| `*` | Bypass proxy for all hosts |
268+
| `localhost` | Exact match for `localhost` |
269+
| `127.0.0.1` | Exact match for `127.0.0.1` |
270+
| `.example.com` | Matches `example.com` and all its subdomains (e.g. `api.example.com`) |
271+
| `example.com` | Matches `example.com` and all its subdomains |

src/common/openai-client.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as path from "path";
44
import OpenAI from "openai";
55
import { Agent, fetch as undiciFetch } from "undici";
66
import { resolveCurrentSettings } from "../settings";
7+
import { getProxyDispatcher } from "./proxy";
78

89
// Custom undici Agent with a 180-second keepAlive timeout. The default
910
// global fetch (undici) only keeps connections alive for 4 seconds, which
@@ -51,7 +52,8 @@ export function createOpenAIClient(projectRoot: string = process.cwd()): {
5152
};
5253
}
5354

54-
const cacheKey = `${settings.apiKey}::${settings.baseURL}`;
55+
const proxyDispatcher = getProxyDispatcher(settings.baseURL);
56+
const cacheKey = `${settings.apiKey}::${settings.baseURL}::${proxyDispatcher ? "proxy" : "direct"}`;
5557
if (cachedOpenAI && cachedOpenAIKey === cacheKey) {
5658
return {
5759
client: cachedOpenAI,
@@ -73,7 +75,7 @@ export function createOpenAIClient(projectRoot: string = process.cwd()): {
7375
apiKey: settings.apiKey,
7476
baseURL: settings.baseURL || undefined,
7577
// eslint-disable-next-line @typescript-eslint/no-explicit-any
76-
fetch: (url: any, init: any) => undiciFetch(url, { ...init, dispatcher: keepAliveAgent }),
78+
fetch: (url: any, init: any) => undiciFetch(url, { ...init, dispatcher: proxyDispatcher ?? keepAliveAgent }),
7779
});
7880
cachedOpenAIKey = cacheKey;
7981

src/common/proxy.ts

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import { ProxyAgent } from "undici";
2+
import { readSettings, readProjectSettings } from "../settings";
3+
import type { SettingsProcessEnv } from "../settings";
4+
5+
export type ProxyType = "http" | "https" | "socks5";
6+
7+
export type ResolvedProxy = {
8+
url: string;
9+
type: ProxyType;
10+
};
11+
12+
/**
13+
* Determine whether a target URL should bypass the proxy based on NO_PROXY.
14+
*
15+
* Supports:
16+
* - `*` wildcard (bypass everything)
17+
* - Exact hostname match (e.g. `localhost`, `example.com`)
18+
* - Sub-domain wildcard (e.g. `.example.com` matches `api.example.com`)
19+
*/
20+
function shouldBypassProxy(targetUrl: string, noProxy: string): boolean {
21+
if (!noProxy.trim()) {
22+
return false;
23+
}
24+
25+
const entries = noProxy
26+
.split(",")
27+
.map((entry) => entry.trim().toLowerCase())
28+
.filter(Boolean);
29+
30+
if (entries.includes("*")) {
31+
return true;
32+
}
33+
34+
let hostname: string;
35+
try {
36+
hostname = new URL(targetUrl).hostname.toLowerCase();
37+
} catch {
38+
return false;
39+
}
40+
41+
for (const entry of entries) {
42+
if (entry.startsWith(".")) {
43+
// ".example.com" matches "api.example.com" and "example.com"
44+
if (hostname.endsWith(entry) || hostname === entry.slice(1)) {
45+
return true;
46+
}
47+
} else if (hostname === entry || hostname.endsWith(`.${entry}`)) {
48+
return true;
49+
}
50+
}
51+
52+
return false;
53+
}
54+
55+
/**
56+
* Pick the first non-empty proxy variable from `source`, preferring
57+
* HTTPS_PROXY → HTTP_PROXY → SOCKS_PROXY → SOCKS5_PROXY (case-insensitive
58+
* lowercase fallback for each).
59+
*/
60+
function pickProxyVar(source: Record<string, string | undefined>): { url: string; type: ProxyType } | undefined {
61+
const candidates: Array<{ keys: string[]; type: ProxyType }> = [
62+
{ keys: ["HTTPS_PROXY", "https_proxy"], type: "https" },
63+
{ keys: ["HTTP_PROXY", "http_proxy"], type: "http" },
64+
{ keys: ["SOCKS_PROXY", "socks_proxy", "SOCKS5_PROXY", "socks5_proxy"], type: "socks5" },
65+
];
66+
67+
for (const { keys, type } of candidates) {
68+
for (const key of keys) {
69+
const value = source[key];
70+
if (typeof value === "string" && value.trim()) {
71+
return { url: value.trim(), type };
72+
}
73+
}
74+
}
75+
76+
return undefined;
77+
}
78+
79+
/**
80+
* Pick NO_PROXY from a source (case-insensitive fallback).
81+
*/
82+
function pickNoProxy(source: Record<string, string | undefined>): string {
83+
return (
84+
(typeof source.NO_PROXY === "string" && source.NO_PROXY) ||
85+
(typeof source.no_proxy === "string" && source.no_proxy) ||
86+
""
87+
);
88+
}
89+
90+
/**
91+
* Resolve the effective proxy URL by consulting (in ascending priority):
92+
* 1. User-level `settings.json` → `env`
93+
* 2. Project-level `settings.json` → `env`
94+
* 3. Process environment variables (both standard `HTTP_PROXY` / `HTTPS_PROXY`
95+
* and `DEEPCODE_`-prefixed variants)
96+
*
97+
* Returns `undefined` when no proxy is configured or when NO_PROXY matches.
98+
*/
99+
export function resolveProxyUrl(
100+
targetUrl: string,
101+
projectRoot: string = process.cwd(),
102+
processEnv: SettingsProcessEnv = process.env
103+
): ResolvedProxy | undefined {
104+
// --- Collect proxy vars from each layer ---
105+
const userEnv = readSettings()?.env ?? {};
106+
const projectEnv = readProjectSettings(projectRoot)?.env ?? {};
107+
108+
// System env includes both standard proxy vars and DEEPCODE_-prefixed ones
109+
// (collectDeepcodeEnv strips the prefix, so DEEPCODE_HTTPS_PROXY → HTTPS_PROXY).
110+
const systemProxySource: Record<string, string | undefined> = { ...processEnv };
111+
for (const [key, value] of Object.entries(processEnv)) {
112+
if (key.startsWith("DEEPCODE_") && typeof value === "string") {
113+
const stripped = key.slice("DEEPCODE_".length);
114+
if (stripped) {
115+
systemProxySource[stripped] = value;
116+
}
117+
}
118+
}
119+
120+
// --- NO_PROXY check (system level takes absolute precedence) ---
121+
const systemNoProxy = pickNoProxy(systemProxySource);
122+
if (shouldBypassProxy(targetUrl, systemNoProxy)) {
123+
return undefined;
124+
}
125+
126+
// --- Merge: user < project < system ---
127+
const merged: Record<string, string | undefined> = {
128+
...userEnv,
129+
...projectEnv,
130+
...systemProxySource,
131+
};
132+
133+
// NO_PROXY from merged (user/project may also define it, but system wins)
134+
const mergedNoProxy = pickNoProxy(merged);
135+
if (shouldBypassProxy(targetUrl, mergedNoProxy)) {
136+
return undefined;
137+
}
138+
139+
return pickProxyVar(merged);
140+
}
141+
142+
// ---------------------------------------------------------------------------
143+
// Dispatcher cache – avoids re-creating ProxyAgent on every request.
144+
// ---------------------------------------------------------------------------
145+
let cachedProxyUrl = "";
146+
let cachedDispatcher: ProxyAgent | null = null;
147+
148+
/**
149+
* Return a `ProxyAgent` dispatcher when a proxy is configured for the given
150+
* target URL, or `null` when requests should go direct.
151+
*/
152+
export function getProxyDispatcher(targetUrl?: string): ProxyAgent | null {
153+
const resolved = resolveProxyUrl(targetUrl ?? "https://api.deepseek.com");
154+
const proxyUrl = resolved?.url ?? "";
155+
156+
if (!proxyUrl) {
157+
cachedProxyUrl = "";
158+
cachedDispatcher = null;
159+
return null;
160+
}
161+
162+
if (cachedDispatcher && cachedProxyUrl === proxyUrl) {
163+
return cachedDispatcher;
164+
}
165+
166+
cachedProxyUrl = proxyUrl;
167+
cachedDispatcher = new ProxyAgent({
168+
uri: proxyUrl,
169+
keepAliveTimeout: 180_000,
170+
});
171+
return cachedDispatcher;
172+
}
173+
174+
/**
175+
* Fetch wrapper that automatically routes through the configured proxy.
176+
* Use this in place of the global `fetch` for any HTTP request that should
177+
* respect the proxy configuration.
178+
*/
179+
export async function proxyFetch(url: string | URL, init?: RequestInit): Promise<Response> {
180+
const dispatcher = getProxyDispatcher(String(url));
181+
if (!dispatcher) {
182+
return fetch(url, init);
183+
}
184+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
185+
return (fetch as any)(url, { ...init, dispatcher });
186+
}

src/common/telemetry.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { proxyFetch } from "./proxy";
2+
13
const DEFAULT_NEW_PROMPT_API_URL = "https://deepcode.vegamo.cn/api/plugin/new";
24
const DEFAULT_REPORT_TIMEOUT_MS = 3000;
35

@@ -20,7 +22,7 @@ export function reportNewPrompt(options: NewPromptReportOptions): void {
2022
const controller = new AbortController();
2123
const timeout = setTimeout(() => controller.abort(), timeoutMs);
2224

23-
void fetch(DEFAULT_NEW_PROMPT_API_URL, {
25+
void proxyFetch(DEFAULT_NEW_PROMPT_API_URL, {
2426
method: "POST",
2527
headers: {
2628
"Content-Type": "application/json",

0 commit comments

Comments
 (0)