背景 链接到标题
Pi Coding Agent 统一通过 HTTP_PROXY 环境变量控制所有 fetch 请求(LLM API 调用 + 工具层请求)是否走代理。代理服务器自身配置例外规则(如国内地址直连),deepseek 等国内服务实际不绕路。
配置方式:在 config.json 中写入代理地址,项目持久化,重启后自动生效。
SDK 网络层 链接到标题
Pi Coding Agent 用 undici.setGlobalDispatcher() 替换全局 fetch,底层由 undici.EnvHttpProxyAgent 控制。该 dispatcher 同时服务于 LLM API 请求和工具 execute 中的 fetch,HTTP_PROXY / HTTPS_PROXY 环境变量同时影响两端。
AgentSession
├── LLM API 请求(OpenAI / Anthropic / DeepSeek)
└── 工具 execute fetch(search / fetch / curl 等)
│
▼
undici 全局 dispatcher
│
▼
EnvHttpProxyAgent(读取 HTTP_PROXY)
│
▼
代理服务器
(自己做例外规则)
配置方式 链接到标题
config.json 新增 httpProxy
链接到标题
{
"provider": "deepseek",
"model": "deepseek-v4-flash",
"apiKey": "sk-xxx",
"cwd": "~/project",
"httpProxy": "http://127.0.0.1:7890",
"windmillUrl": "http://ape:3900",
"windmillToken": ""
}
留空则不设代理。
main.js 读取并写入环境变量 链接到标题
// 在 session:create 中,auth 设置之后
const proxy = config.httpProxy?.trim();
if (proxy) {
process.env.HTTP_PROXY ??= proxy;
process.env.HTTPS_PROXY ??= proxy;
}
??= 不覆盖已有的环境变量,启动前手动 export HTTP_PROXY=... 仍有效。
UI 配置入口 链接到标题
初次配置表单和重连弹窗均增加 HTTP Proxy 输入框:
初次配置: [API Key] [Provider] [Model] [工作目录] [HTTP Proxy] [Windmill URL] [Windmill Token]
重连弹窗: 同上
输入后保存并重连,配置写入 config.json,下次启动自动读取。
工具层 fetch 自动走代理 链接到标题
扩展 execute 函数中直接用 fetch,无需额外配置:
async execute(_toolCallId, params, signal) {
const resp = await fetch("https://www.google.com/search", {
method: "GET",
signal,
});
const data = await resp.json();
return {
content: [{ type: "text", text: JSON.stringify(data) }],
details: {},
};
}
fetch 使用全局 dispatcher,HTTP_PROXY 自动生效。如需工具单独走不同代理,可手动创建 https-proxy-agent 透传给 fetch。
踩坑 链接到标题
applyHttpProxySettings 不在包导出路径 链接到标题
SDK 内部有 applyHttpProxySettings 函数,但 package.json 的 exports 字段只开放 . 和 ./rpc-entry,dist/core/http-dispatcher.js 无法直接 import:
// ❌ 报错:Package subpath ... is not defined by "exports"
const { applyHttpProxySettings } = await import('@earendil-works/pi-coding-agent/dist/core/http-dispatcher.js');
解决:直接内联,process.env.HTTP_PROXY ??= proxy,行为与 applyHttpProxySettings 一致。
ipcMain.handle 在 session:create 内重复注册 链接到标题
ipcMain.handle('ui:response') 放在 session:create 内部,每次重连(如修改代理后保存)会再次注册同一 channel,报 Attempted to register a second handler。
解决:pendingUI map 和 ipcMain.handle('ui:response') 提升到模块级别,只注册一次。session:create 内只 pendingUI = new Map() 重置。