feat(server,web,desktop): admin "use system HTTP proxy" switch (#225)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yaowei ZhengandClaude Fable 5 authored and GitHub committed 2026-08-06 20:21:26 +08:00
1 parent e8499befd4
commit 91f3292ab5
29 files changed
+710 -19

No files matched your search

+9
View File
@@ -50,6 +50,8 @@ import type {
ScheduleItem,
SchedulesResponse,
ScheduleUpsertRequest,
ServerSettingsResponse,
ServerSettingsUpdateRequest,
SessionCategory,
SessionCreateRequest,
SessionCreateResponse,
@@ -114,6 +116,13 @@ export const adminResetPassword = (userId: string, body: AdminPasswordResetReque
export const adminDeleteUser = (userId: string) =>
apiFetch<void>(`/api/admin/users/${encodeURIComponent(userId)}`, { method: "DELETE" });
/** Server-global settings (admin only): currently the "use system HTTP proxy" switch. */
export const adminGetSettings = () => apiFetch<ServerSettingsResponse>("/api/admin/settings");
/** Omitted fields keep their current value; applies immediately (no restart). */
export const adminPutSettings = (body: ServerSettingsUpdateRequest) =>
apiFetch<ServerSettingsResponse>("/api/admin/settings", { method: "PUT", body });
// Project & members --------------------------------------------------------------
export const listProjects = () => apiFetch<ProjectsResponse>("/api/projects");
+53 -2
View File
@@ -234,6 +234,35 @@ export function Sidebar({
setUpdateChecking(false);
}
};
/**
* Admin-only "use system HTTP proxy" switch (server-global, design § "出网与系统代理"):
* null = not hydrated yet. Fetched lazily the first time an ADMIN opens the dropdown —
* same laziness as the version info above, and non-admins never call the endpoint (the
* row is not rendered for them either). A failed hydration stays null (row disabled)
* and is retried on the next open.
*/
const [useSystemProxy, setUseSystemProxyState] = useState<boolean | null>(null);
useEffect(() => {
if (!userOpen || user?.isAdmin !== true || useSystemProxy !== null) return;
let cancelled = false;
void api
.adminGetSettings()
.then((res) => {
if (!cancelled) setUseSystemProxyState(res.settings.useSystemProxy);
})
.catch(() => undefined);
return () => {
cancelled = true;
};
}, [userOpen, user?.isAdmin, useSystemProxy]);
/** Saved immediately on toggle (the user-menu quick-control convention): optimistic flip, reverted with a toast on failure. */
const setUseSystemProxy = (value: boolean) => {
setUseSystemProxyState(value);
void api.adminPutSettings({ useSystemProxy: value }).catch((e: unknown) => {
setUseSystemProxyState(!value);
toastError(apiErrorText(e));
});
};
const currentProjectId = currentProject?.projectId ?? null;
const collapseStoreKey = currentProjectId === null ? null : collapsedGroupsKey(currentProjectId);
const pinStoreKey = currentProjectId === null ? null : pinnedGroupsKey(currentProjectId);
@@ -1005,6 +1034,19 @@ export function Sidebar({
<SettingRow label={S.settings.showCliSessions}>
<Switch checked={showCliSessions} onChange={setShowCliSessions} />
</SettingRow>
{/* Admin-only, server-global (all users), saved immediately on toggle; the
tooltip spells out the scope and the loopback exemption. Disabled (showing
the default: on) until the stored value has hydrated, so a click can never
write a value the admin was not looking at. */}
{user?.isAdmin && (
<SettingRow label={S.settings.useSystemProxy} title={S.settings.useSystemProxyHint}>
<Switch
checked={useSystemProxy ?? true}
onChange={setUseSystemProxy}
disabled={useSystemProxy === null}
/>
</SettingRow>
)}
</div>
<div className="mt-1 border-t border-gray-100 pt-1 dark:border-gray-800">
<button
@@ -1332,9 +1374,18 @@ function SessionRow({
);
}
function SettingRow({ label, children }: { label: string; children: ReactNode }) {
function SettingRow({
label,
title,
children,
}: {
label: string;
/** Optional row tooltip (e.g. the system-proxy row's scope + loopback-exemption hint). */
title?: string;
children: ReactNode;
}) {
return (
<div>
<div {...(title !== undefined ? { title } : {})}>
<p className="mb-1 text-[11px] font-medium text-gray-500 dark:text-gray-400">{label}</p>
{children}
</div>
+5
View File
@@ -33,6 +33,11 @@ export const en: Strings = {
language: "Language",
/** Sidebar Session list: also show CLI-created Sessions (default off — the list then never scans the Trace directories). */
showCliSessions: "Show CLI sessions",
/** Admin-only server-global switch (design § "出网与系统代理"): saved immediately on toggle. */
useSystemProxy: "Use system HTTP proxy",
/** Row tooltip: scope (server + its child processes, server-wide) and the loopback exemption. */
useSystemProxyHint:
"Whether the server and its child processes (update checks, LLM requests, agent commands) reach the internet through the proxy named by HTTP_PROXY / HTTPS_PROXY. Applies server-wide; loopback addresses (localhost, 127.0.0.1, ::1) always connect directly. When off, the server connects directly and the proxy variables are removed from agent command subprocess environments.",
theme: "Theme",
themeLight: "Light",
themeDark: "Dark",
+5
View File
@@ -35,6 +35,11 @@ export const zh = {
language: "语言",
/** Sidebar Session list: also show CLI-created Sessions (default off — the list then never scans the Trace directories). */
showCliSessions: "显示 CLI 会话",
/** Admin-only server-global switch (design § "出网与系统代理"): saved immediately on toggle. */
useSystemProxy: "使用系统 HTTP 代理",
/** Row tooltip: scope (server + its child processes, server-wide) and the loopback exemption. */
useSystemProxyHint:
"服务端及其子进程(更新检查、LLM 请求、Agent 命令)出网时是否使用 HTTP_PROXY / HTTPS_PROXY 环境变量指定的代理,对整个服务端全局生效;回环地址(localhost、127.0.0.1、::1)始终直连。关闭后服务端一律直连,并从 Agent 命令子进程环境中移除代理变量。",
theme: "主题",
themeLight: "浅色",
themeDark: "深色",