Skip to content

Commit b235de8

Browse files
committed
feat(providers): simplify usage query networking and add inline test feedback
1. 移除"允许本地网络地址"开关:本地/私网与公网地址、http 与 https 默认均可查询(仍拒绝 URL 内嵌凭据与非 HTTP 协议,标准模板仍限 Base URL 同源,内置适配器仍限 HTTPS);出网统一走应用代理配置 (system_proxy async builder,显式 no_proxy 语义,代理未启用即直连, 配置无效 fail fast),原 DNS 钉扎/私网地址过滤随门控一并移除。 2. 自动刷新固定 5 分钟:usageQueryCore 常量化 AUTO_REFRESH_INTERVAL_MS, 移除 autoRefreshMinutes 配置字段(schema/规范化/默认值/UI 同步清理, 旧配置多余字段由 serde/normalize 静默忽略)。 3. 测试查询内联反馈:模态框内新增 running/success/error 状态面板, 展示条目、错误与空数据提示;测试实刷后卡片改走 hydrateProvider 缓存补水,不再重复请求上游;按钮查询期间禁用并旋转图标。 两端(GUI/WebUI)同步修改;i18n 删除旧开关与刷新间隔文案,新增测试 反馈文案。
1 parent 1cd5ddd commit b235de8

16 files changed

Lines changed: 288 additions & 396 deletions

File tree

crates/agent-gateway/test/webui/provider-usage-query-form.test.mjs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ const usageQuery = {
1616
accessKeyId: "key-1",
1717
secretAccessKey: "",
1818
secretAccessKeyConfigured: true,
19-
autoRefreshMinutes: 15,
20-
allowLocalNetwork: false,
2119
};
2220

2321
test("WebUI usage query draft preserves configured redacted secrets when saved", () => {

crates/agent-gateway/web/src/i18n/config.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1404,11 +1404,12 @@ export const translations: Record<Locale, Record<string, string>> = {
14041404
"settings.providerUsageUserId": "User ID",
14051405
"settings.providerUsageAccessKeyId": "AccessKey ID",
14061406
"settings.providerUsageSecretAccessKey": "SecretAccessKey",
1407-
"settings.providerUsageAllowLocalNetwork": "允许本地网络地址",
1408-
"settings.providerUsageAllowLocalNetworkHint": "仅在查询端点位于受信任内网时启用。",
1409-
"settings.providerUsageAutoRefresh": "自动刷新间隔(分钟)",
1407+
"settings.providerUsageAutoRefreshFixedHint": "启用后每 5 分钟自动刷新当前使用的供应商。",
14101408
"settings.providerUsageTest": "测试查询",
14111409
"settings.providerUsageTestSavedHint": "请先保存供应商,才能测试查询。",
1410+
"settings.providerUsageTestRunning": "正在查询…",
1411+
"settings.providerUsageTestFailed": "查询失败",
1412+
"settings.providerUsageTestEmpty": "查询成功,但没有返回可展示的数据。",
14121413
"settings.providerUsageCustomConfirmTitle": "启用自定义用量脚本?",
14131414
"settings.providerUsageCustomConfirmDescription":
14141415
"自定义脚本可以向其定义的目标发起网络请求。请仅使用可信脚本和端点。",
@@ -3528,12 +3529,13 @@ export const translations: Record<Locale, Record<string, string>> = {
35283529
"settings.providerUsageUserId": "User ID",
35293530
"settings.providerUsageAccessKeyId": "AccessKey ID",
35303531
"settings.providerUsageSecretAccessKey": "SecretAccessKey",
3531-
"settings.providerUsageAllowLocalNetwork": "Allow local network addresses",
3532-
"settings.providerUsageAllowLocalNetworkHint":
3533-
"Enable only when the query endpoint is on a trusted local network.",
3534-
"settings.providerUsageAutoRefresh": "Auto-refresh interval (minutes)",
3532+
"settings.providerUsageAutoRefreshFixedHint":
3533+
"When enabled, the active provider refreshes automatically every 5 minutes.",
35353534
"settings.providerUsageTest": "Test query",
35363535
"settings.providerUsageTestSavedHint": "Save the provider before testing its query.",
3536+
"settings.providerUsageTestRunning": "Querying…",
3537+
"settings.providerUsageTestFailed": "Query failed",
3538+
"settings.providerUsageTestEmpty": "The query succeeded but returned no displayable data.",
35373539
"settings.providerUsageCustomConfirmTitle": "Enable custom usage script?",
35383540
"settings.providerUsageCustomConfirmDescription":
35393541
"A custom script can make network requests to the destinations it defines. Use only trusted scripts and endpoints.",

crates/agent-gateway/web/src/lib/providers/usageQueryCore.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ export type UsageQueryProvider = {
2121
id: string;
2222
usageQuery?: {
2323
enabled?: boolean;
24-
autoRefreshMinutes?: number;
2524
};
2625
};
2726

@@ -45,6 +44,9 @@ export type UsageQuery = (
4544

4645
type UsageRequestSource = "timer" | "other";
4746

47+
// 自动刷新固定 5 分钟:仅对当前选中模型所属、已启用用量查询的供应商生效。
48+
export const AUTO_REFRESH_INTERVAL_MS = 5 * 60_000;
49+
4850
export function reduceUsageState(
4951
state: ProviderUsageState,
5052
action: UsageStateAction,
@@ -73,8 +75,7 @@ export function getAutoRefreshProvider<T extends UsageQueryProvider>(
7375
): T | null {
7476
if (!selectedModel) return null;
7577
const provider = providers.find((item) => item.id === selectedModel.customProviderId);
76-
const autoRefreshMinutes = provider?.usageQuery?.autoRefreshMinutes ?? 0;
77-
if (!provider?.usageQuery?.enabled || autoRefreshMinutes <= 0) return null;
78+
if (!provider?.usageQuery?.enabled) return null;
7879
return provider;
7980
}
8081

@@ -90,7 +91,7 @@ export function getUsageRefreshPlan<T extends UsageQueryProvider>(
9091
timer: autoRefreshProvider
9192
? {
9293
providerId: autoRefreshProvider.id,
93-
intervalMs: (autoRefreshProvider.usageQuery?.autoRefreshMinutes ?? 0) * 60_000,
94+
intervalMs: AUTO_REFRESH_INTERVAL_MS,
9495
}
9596
: null,
9697
};
@@ -263,5 +264,12 @@ export function useProviderUsageWithQuery(
263264
[coordinator],
264265
);
265266

266-
return { ...snapshot, refreshProvider };
267+
// 命中桌面端缓存的补水入口:模态框内测试已实刷后,供应商卡片用它同步
268+
// 展示结果而不再重复打一次上游。
269+
const hydrateProvider = useCallback(
270+
(providerId: string) => coordinator.request(providerId, false),
271+
[coordinator],
272+
);
273+
274+
return { ...snapshot, refreshProvider, hydrateProvider };
267275
}

crates/agent-gateway/web/src/lib/settings/index.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -286,8 +286,6 @@ export type UsageQueryConfig = {
286286
accessKeyId: string;
287287
secretAccessKey: string;
288288
secretAccessKeyConfigured?: boolean;
289-
autoRefreshMinutes: number;
290-
allowLocalNetwork: boolean;
291289
};
292290

293291
export function getDefaultUsageQueryConfig(): UsageQueryConfig {
@@ -302,8 +300,6 @@ export function getDefaultUsageQueryConfig(): UsageQueryConfig {
302300
accessKeyId: "",
303301
secretAccessKey: "",
304302
secretAccessKeyConfigured: false,
305-
autoRefreshMinutes: 0,
306-
allowLocalNetwork: false,
307303
};
308304
}
309305

@@ -1342,12 +1338,6 @@ function normalizeUsageQueryMode(input: unknown): UsageQueryMode {
13421338
}
13431339
}
13441340

1345-
function normalizeUsageQueryAutoRefreshMinutes(input: unknown): number {
1346-
const value = typeof input === "number" || typeof input === "string" ? Number(input) : 0;
1347-
if (!Number.isFinite(value)) return 0;
1348-
return Math.min(1_440, Math.max(0, Math.floor(value)));
1349-
}
1350-
13511341
function normalizeUsageQueryConfig(input: unknown): UsageQueryConfig {
13521342
const obj = (input && typeof input === "object" ? input : {}) as Record<string, unknown>;
13531343
const accessToken = normalizeApiKey(typeof obj.accessToken === "string" ? obj.accessToken : "");
@@ -1366,8 +1356,6 @@ function normalizeUsageQueryConfig(input: unknown): UsageQueryConfig {
13661356
accessKeyId: typeof obj.accessKeyId === "string" ? obj.accessKeyId.trim() : "",
13671357
secretAccessKey,
13681358
secretAccessKeyConfigured: secretAccessKey.length > 0 || obj.secretAccessKeyConfigured === true,
1369-
autoRefreshMinutes: normalizeUsageQueryAutoRefreshMinutes(obj.autoRefreshMinutes),
1370-
allowLocalNetwork: obj.allowLocalNetwork === true,
13711359
};
13721360
}
13731361

crates/agent-gateway/web/src/lib/settings/sync.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,6 @@ const DEFAULT_USAGE_QUERY_CONFIG: AppSettings["customProviders"][number]["usageQ
110110
accessKeyId: "",
111111
secretAccessKey: "",
112112
secretAccessKeyConfigured: false,
113-
autoRefreshMinutes: 0,
114-
allowLocalNetwork: false,
115113
};
116114

117115
function usageQueryConfig(

crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx

Lines changed: 89 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ import {
5050
} from "../../lib/providers/modelVendor";
5151
import {
5252
getProviderUsageCardDisplay,
53+
type ProviderUsageEntry,
5354
type ProviderUsageState,
55+
queryProviderUsage,
5456
useProviderUsage,
5557
} from "../../lib/providers/usageQuery";
5658
import {
@@ -297,6 +299,12 @@ function ProviderModal({ providerType, initialData, onSave, onTestUsage, onClose
297299
const canFetchModels = baseUrl.trim().length > 0 && apiKeyForRequest.length > 0;
298300
const persistedUsageQueryProviderId = getPersistedUsageQueryProviderId(initialData);
299301
const { confirm: requestUsageQueryConfirm, dialog: usageQueryConfirmDialog } = useConfirmDialog();
302+
const [usageQueryTest, setUsageQueryTest] = useState<{
303+
status: "idle" | "running" | "success" | "error";
304+
entries: ProviderUsageEntry[];
305+
error: string | null;
306+
}>({ status: "idle", entries: [], error: null });
307+
const usageQueryTestSeqRef = useRef(0);
300308

301309
const doFetch = useCallback(
302310
async (url: string, key: string) => {
@@ -701,8 +709,28 @@ function ProviderModal({ providerType, initialData, onSave, onTestUsage, onClose
701709
requestClose();
702710
}
703711

704-
function handleTestUsageQuery() {
705-
if (persistedUsageQueryProviderId) onTestUsage?.(persistedUsageQueryProviderId);
712+
async function handleTestUsageQuery() {
713+
if (!persistedUsageQueryProviderId) return;
714+
const seq = ++usageQueryTestSeqRef.current;
715+
setUsageQueryTest({ status: "running", entries: [], error: null });
716+
try {
717+
const result = await queryProviderUsage(persistedUsageQueryProviderId, true);
718+
if (usageQueryTestSeqRef.current !== seq) return;
719+
if (result?.error) {
720+
setUsageQueryTest({ status: "error", entries: result.entries ?? [], error: result.error });
721+
} else {
722+
setUsageQueryTest({ status: "success", entries: result?.entries ?? [], error: null });
723+
}
724+
// 测试已实刷桌面端缓存;通知外层卡片按缓存补水,避免重复打上游。
725+
onTestUsage?.(persistedUsageQueryProviderId);
726+
} catch (error) {
727+
if (usageQueryTestSeqRef.current !== seq) return;
728+
setUsageQueryTest({
729+
status: "error",
730+
entries: [],
731+
error: error instanceof Error ? error.message : String(error),
732+
});
733+
}
706734
}
707735

708736
const isEditing = Boolean(initialData);
@@ -1831,70 +1859,69 @@ function ProviderModal({ providerType, initialData, onSave, onTestUsage, onClose
18311859
</div>
18321860
) : null}
18331861

1834-
<div className="mt-4 rounded-xl border bg-card px-4 py-3">
1835-
<div className="flex items-center gap-3">
1836-
<div className="min-w-0 flex-1">
1837-
<div className="text-sm font-medium">
1838-
{t("settings.providerUsageAllowLocalNetwork")}
1839-
</div>
1840-
<div className="text-xs text-muted-foreground">
1841-
{t("settings.providerUsageAllowLocalNetworkHint")}
1842-
</div>
1843-
</div>
1844-
<DialogSwitch
1845-
checked={usageQuery.allowLocalNetwork}
1846-
onCheckedChange={(allowLocalNetwork) =>
1847-
setUsageQuery((previous) => ({ ...previous, allowLocalNetwork }))
1848-
}
1849-
ariaLabel={t("settings.providerUsageAllowLocalNetwork")}
1850-
/>
1851-
</div>
1852-
</div>
1853-
1854-
<div className="mt-4 grid grid-cols-[minmax(0,1fr)_auto] items-end gap-3 max-[720px]:grid-cols-1">
1855-
<div className="space-y-1.5">
1856-
<Label htmlFor="usage-query-auto-refresh">
1857-
{t("settings.providerUsageAutoRefresh")}
1858-
</Label>
1859-
<Input
1860-
id="usage-query-auto-refresh"
1861-
type="number"
1862-
min="0"
1863-
max="1440"
1864-
value={usageQuery.autoRefreshMinutes}
1865-
onChange={(event) => {
1866-
const value = Number.parseInt(event.currentTarget.value, 10);
1867-
setUsageQuery((previous) => ({
1868-
...previous,
1869-
autoRefreshMinutes: Number.isFinite(value) ? Math.max(0, value) : 0,
1870-
}));
1871-
}}
1862+
<div className="mt-4 flex flex-wrap items-center gap-3">
1863+
<Button
1864+
type="button"
1865+
variant="outline"
1866+
size="sm"
1867+
className="h-10 gap-1.5"
1868+
disabled={!persistedUsageQueryProviderId || usageQueryTest.status === "running"}
1869+
onClick={() => void handleTestUsageQuery()}
1870+
title={t("settings.providerUsageTest")}
1871+
aria-label={t("settings.providerUsageTest")}
1872+
>
1873+
<RefreshCw
1874+
className={cn(
1875+
"h-3.5 w-3.5",
1876+
usageQueryTest.status === "running" && "animate-spin",
1877+
)}
18721878
/>
1873-
</div>
1874-
<div className="space-y-1.5">
1875-
<span className="block text-xs text-muted-foreground max-[720px]:hidden">
1876-
&nbsp;
1877-
</span>
1878-
<Button
1879-
type="button"
1880-
variant="outline"
1881-
size="sm"
1882-
className="h-10 gap-1.5"
1883-
disabled={!persistedUsageQueryProviderId}
1884-
onClick={handleTestUsageQuery}
1885-
title={t("settings.providerUsageTest")}
1886-
aria-label={t("settings.providerUsageTest")}
1887-
>
1888-
<RefreshCw className="h-3.5 w-3.5" />
1889-
{t("settings.providerUsageTest")}
1890-
</Button>
1891-
</div>
1879+
{t("settings.providerUsageTest")}
1880+
</Button>
1881+
<span className="text-xs text-muted-foreground">
1882+
{t("settings.providerUsageAutoRefreshFixedHint")}
1883+
</span>
18921884
</div>
18931885
{!persistedUsageQueryProviderId ? (
18941886
<p className="mt-2 text-xs text-muted-foreground">
18951887
{t("settings.providerUsageTestSavedHint")}
18961888
</p>
18971889
) : null}
1890+
{usageQueryTest.status !== "idle" ? (
1891+
<div
1892+
className="mt-3 rounded-xl border bg-card px-4 py-3 text-xs"
1893+
role="status"
1894+
aria-live="polite"
1895+
>
1896+
{usageQueryTest.status === "running" ? (
1897+
<span className="text-muted-foreground">
1898+
{t("settings.providerUsageTestRunning")}
1899+
</span>
1900+
) : null}
1901+
{usageQueryTest.status === "error" ? (
1902+
<span className="text-destructive">
1903+
{t("settings.providerUsageTestFailed")}
1904+
{usageQueryTest.error ? `: ${usageQueryTest.error}` : ""}
1905+
</span>
1906+
) : null}
1907+
{usageQueryTest.status === "success" ? (
1908+
usageQueryTest.entries.length > 0 ? (
1909+
<div className="flex flex-wrap gap-x-4 gap-y-1">
1910+
{usageQueryTest.entries.map((entry) => (
1911+
<span key={entry.label} className="truncate">
1912+
{entry.label}: {entry.value}
1913+
{entry.unit ? ` ${entry.unit}` : ""}
1914+
</span>
1915+
))}
1916+
</div>
1917+
) : (
1918+
<span className="text-muted-foreground">
1919+
{t("settings.providerUsageTestEmpty")}
1920+
</span>
1921+
)
1922+
) : null}
1923+
</div>
1924+
) : null}
18981925
</section>
18991926
)}
19001927
</div>
@@ -2287,10 +2314,8 @@ export function ProvidersSection(props: SettingsSectionProps) {
22872314
const [modalOpen, setModalOpen] = useState(false);
22882315
const [customSettingsOpen, setCustomSettingsOpen] = useState(false);
22892316
const [editingProvider, setEditingProvider] = useState<CustomProvider | null>(null);
2290-
const { usageByProvider, refreshingProviderIds, refreshProvider } = useProviderUsage(
2291-
settings.customProviders,
2292-
settings.selectedModel,
2293-
);
2317+
const { usageByProvider, refreshingProviderIds, refreshProvider, hydrateProvider } =
2318+
useProviderUsage(settings.customProviders, settings.selectedModel);
22942319

22952320
function openAdd() {
22962321
setEditingProvider(null);
@@ -2419,7 +2444,7 @@ export function ProvidersSection(props: SettingsSectionProps) {
24192444
providerType={activeTab}
24202445
initialData={editingProvider ?? undefined}
24212446
onSave={handleSave}
2422-
onTestUsage={(providerId) => void refreshProvider(providerId)}
2447+
onTestUsage={(providerId) => void hydrateProvider(providerId)}
24232448
onClose={closeModal}
24242449
/>
24252450
) : null}

0 commit comments

Comments
 (0)