feat(proxy): 新增全局出口代理配置

所有目标流量可经统一的全局代理出网(http/https/socks5,支持 user:pass):
- 开启流量捕获时,作为 MITM 记录代理的上游(流量照录再经代理出网);
  拦截与透传两条路径都走上游,不泄露源 IP。
- 关闭捕获时,直接注入 agent 的 bash 环境与 WebFetch;proxyEnv 增加
  ALL_PROXY 以支持 socks5。

配置存于 settings KV 表(无需迁移),前端系统配置页新增「全局代理」卡片。
与网络搜索代理、LLM 代理相互独立。
This commit is contained in:
Autumn-27 committed 2026-09-07 10:43:38 -04:00
1 parent 651b961ffc
commit b513b656aa
10 files changed
+349 -34

No files matched your search

@@ -26,6 +26,8 @@ export default function SystemSettingsPage() {
const [savingTavilyKey, setSavingTavilyKey] = React.useState(false);
const [proxyInput, setProxyInput] = React.useState("");
const [savingProxy, setSavingProxy] = React.useState(false);
const [globalProxyInput, setGlobalProxyInput] = React.useState("");
const [savingGlobalProxy, setSavingGlobalProxy] = React.useState(false);
const [testing, setTesting] = React.useState(false);
const [loaded, setLoaded] = React.useState(false);
const [saving, setSaving] = React.useState(false);
@@ -46,6 +48,7 @@ export default function SystemSettingsPage() {
setBraveKeySet(!!s.brave_key_set);
setTavilyKeySet(!!s.tavily_key_set);
setProxyInput(s.web_search_proxy || "");
setGlobalProxyInput(s.global_proxy || "");
setPyInterp(s.python_interpreter || "");
setWorkers(String(s.workers ?? 3));
setInjectPlanner(s.constraints_inject_planner !== false);
@@ -180,6 +183,18 @@ export default function SystemSettingsPage() {
.finally(() => setSavingProxy(false));
};
const saveGlobalProxy = () => {
setSavingGlobalProxy(true);
api
.setSettings({ global_proxy: globalProxyInput.trim() })
.then((s) => {
apply(s);
toast.success(globalProxyInput.trim() ? "已保存全局代理" : "已清除全局代理(改为直连)");
})
.catch((e) => toast.error("保存失败:" + (e as Error).message))
.finally(() => setSavingGlobalProxy(false));
};
// Run a real "test" search ("test") against the CURRENT form values (backend +
// proxy + entered key), falling back to saved values server-side. Toasts result.
const runTest = () => {
@@ -241,6 +256,48 @@ export default function SystemSettingsPage() {
</CardContent>
</Card>
<Card className="mb-4 break-inside-avoid md:mb-6">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<RadioTowerIcon className="size-4" />
全局代理
</CardTitle>
<CardDescription>
所有 Agent 的<b>目标流量</b>经此代理出网(隐藏源 IP / 走跳板)。支持 <b>http / https / socks5</b>,可带{" "}
<code>user:pass</code> 认证。留空=直连。
<br />
开启<b>流量捕获</b>时,它作为记录代理的<b>上游</b>(流量仍全量落库,再经此代理出网);关闭捕获时,直接注入
Agent 的 bash / WebFetch 出网。与网络搜索代理、LLM 代理相互独立。
<br />
<b>提示</b>:socks5 在<b>关闭捕获</b>时依赖各命令行工具对 <code>ALL_PROXY</code> 的支持(curl 可用,部分工具可能忽略);
若主要用 socks5,建议开启流量捕获——此路径由 MITM 亲自拨号,工具无感知、稳定生效。
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-2">
<Label htmlFor="global-proxy" className="text-sm font-normal text-muted-foreground">
代理地址
</Label>
<div className="flex items-center gap-2">
<Input
id="global-proxy"
autoComplete="off"
placeholder="socks5://user:pass@host:1080 或 http://host:port(留空=直连)"
value={globalProxyInput}
disabled={!loaded || savingGlobalProxy}
onChange={(e) => setGlobalProxyInput(e.target.value)}
/>
<Button type="button" onClick={saveGlobalProxy} disabled={!loaded || savingGlobalProxy}>
保存
</Button>
</div>
<p className="text-muted-foreground text-xs">
{globalProxyInput.trim()
? "已配置 · 所有目标流量经此代理出网"
: "未配置 · 目标流量直连出网"}
</p>
</CardContent>
</Card>
<Card className="mb-4 break-inside-avoid md:mb-6">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
+1
View File
@@ -3315,6 +3315,7 @@ export const settings: Settings = {
brave_key_set: false,
tavily_key_set: true,
web_search_proxy: "",
global_proxy: "",
python_interpreter: "/usr/bin/python3",
workers: 3,
llm_pool_enabled: true,
+3
View File
@@ -866,6 +866,9 @@ export interface Settings {
tavily_search_api_key?: string;
// 独立出口代理(http/https/socks5),用于访问搜索端点;与记录流量的 MITM 代理无关。空=直连。
web_search_proxy?: string;
// 全局出口代理(http/https/socks5,可带 user:pass),所有目标流量走它。开启流量捕获时作为
// MITM 上游;关闭捕获时直接注入 agent 的 bash/WebFetch。空=直连。
global_proxy?: string;
python_interpreter?: string; // 自定义脚本工具的 python 解释器路径(空=运行时检测)
workers?: number; // 并发工作 agent 数(默认3);对之后启动的任务生效
// 任务并发上限:同时「运行中」的任务数上限。关闭=不限;开启后新建任务超限则排队,有空位自动启动。