diff --git a/apps/ui/src/App.tsx b/apps/ui/src/App.tsx index 8f2b7c4..14f363a 100644 --- a/apps/ui/src/App.tsx +++ b/apps/ui/src/App.tsx @@ -507,14 +507,25 @@ const App = () => { const handleSiteTest = useCallback( async (id: string) => { try { - const result = await apiFetch<{ models: Array<{ id: string }> }>( - `/api/channels/${id}/test`, - { - method: "POST", - }, - ); + const result = await apiFetch<{ + models: Array<{ id: string }>; + token_summary?: { + total: number; + success: number; + failed: number; + }; + }>(`/api/channels/${id}/test`, { + method: "POST", + }); await loadSites(); - setNotice(`连通测试完成,模型数 ${result.models?.length ?? 0}`); + const modelCount = result.models?.length ?? 0; + const summary = result.token_summary; + const detail = summary + ? `,令牌成功 ${summary.success}/${summary.total}${ + summary.failed > 0 ? `,失败 ${summary.failed}` : "" + }` + : ""; + setNotice(`连通测试完成,模型数 ${modelCount}${detail}`); } catch (error) { setNotice((error as Error).message); } diff --git a/apps/ui/src/core/constants.ts b/apps/ui/src/core/constants.ts index 427ba2b..2bedc9f 100644 --- a/apps/ui/src/core/constants.ts +++ b/apps/ui/src/core/constants.ts @@ -29,7 +29,7 @@ export const initialSiteForm: SiteForm = { checkin_url: "", system_token: "", system_userid: "", - checkin_enabled: true, + checkin_enabled: false, call_tokens: [ { name: "主调用令牌", diff --git a/apps/ui/src/features/DashboardView.tsx b/apps/ui/src/features/DashboardView.tsx index 173efec..614619e 100644 --- a/apps/ui/src/features/DashboardView.tsx +++ b/apps/ui/src/features/DashboardView.tsx @@ -1,7 +1,7 @@ import type { DashboardData } from "../core/types"; type DashboardViewProps = { - dashboard: DashboardData | null; + dashboard: DashboardData | null; }; /** @@ -14,220 +14,226 @@ type DashboardViewProps = { * Dashboard JSX element. */ export const DashboardView = ({ dashboard }: DashboardViewProps) => { - if (!dashboard) { - return ( -
- 暂无数据 -
- ); - } - const errorRate = dashboard.summary.total_requests - ? Math.round( - (dashboard.summary.total_errors / dashboard.summary.total_requests) * - 100, - ) - : 0; - return ( -
-
-
- - 总请求 - -
- {dashboard.summary.total_requests} -
- - 最近窗口 - -
-
- - 总 Tokens - -
- {dashboard.summary.total_tokens} -
- - 累计消耗 - -
-
- - 错误率 - -
{errorRate}%
- - 平均延迟 {Math.round(dashboard.summary.avg_latency)}ms - -
-
-
-
-
-

- 按日趋势 -

-
-
- - - - - - - - - - {dashboard.byDay.map((row) => ( - - - - - - ))} - -
- 日期 - - 请求 - - Tokens -
- {row.day} - - {row.requests} - - {row.tokens} -
-
-
-
-
-

- 模型排行 -

-
-
- - - - - - - - - - {dashboard.byModel.map((row) => ( - - - - - - ))} - -
- 模型 - - 请求 - - Tokens -
- {row.model ?? "-"} - - {row.requests} - - {row.tokens} -
-
-
-
-
-
-
-

- 渠道贡献 -

-
-
- - - - - - - - - - {dashboard.byChannel.map((row) => ( - - - - - - ))} - -
- 渠道 - - 请求 - - Tokens -
- {row.channel_name ?? "-"} - - {row.requests} - - {row.tokens} -
-
-
-
-
-

- 令牌贡献 -

-
-
- - - - - - - - - - {dashboard.byToken.map((row) => ( - - - - - - ))} - -
- 令牌 - - 请求 - - Tokens -
- {row.token_name ?? "-"} - - {row.requests} - - {row.tokens} -
-
-
-
-
- ); + if (!dashboard) { + return ( +
+ 暂无数据 +
+ ); + } + const errorRate = dashboard.summary.total_requests + ? Math.round( + (dashboard.summary.total_errors / dashboard.summary.total_requests) * + 100, + ) + : 0; + return ( +
+
+
+ + 总请求 + +
+ {dashboard.summary.total_requests} +
+ + 最近窗口 + +
+
+ + 总 Tokens + +
+ {dashboard.summary.total_tokens} +
+ + 累计消耗 + +
+
+ + 错误率 + +
{errorRate}%
+ + 平均延迟 {Math.round(dashboard.summary.avg_latency)}ms + +
+
+
+
+
+

+ 按日趋势 +

+
+
+ + + + + + + + + + {dashboard.byDay.map((row) => ( + + + + + + ))} + +
+ 日期 + + 请求 + + Tokens +
+ {row.day} + + {row.requests} + + {row.tokens} +
+
+
+
+
+

+ 模型排行 +

+
+
+ + + + + + + + + + {dashboard.byModel.map((row) => ( + + + + + + ))} + +
+ 模型 + + 请求 + + Tokens +
+ {row.model ?? "-"} + + {row.requests} + + {row.tokens} +
+
+
+
+
+
+
+

+ 渠道贡献 +

+
+
+ + + + + + + + + + {dashboard.byChannel.map((row) => ( + + + + + + ))} + +
+ 渠道 + + 请求 + + Tokens +
+ {row.channel_name ?? "-"} + + {row.requests} + + {row.tokens} +
+
+
+
+
+

+ 令牌贡献 +

+
+
+ + + + + + + + + + {dashboard.byToken.map((row) => ( + + + + + + ))} + +
+ 令牌 + + 请求 + + Tokens +
+ {row.token_name ?? "-"} + + {row.requests} + + {row.tokens} +
+
+
+
+
+ ); }; diff --git a/apps/ui/src/features/ModelsView.tsx b/apps/ui/src/features/ModelsView.tsx index c961566..9c626c6 100644 --- a/apps/ui/src/features/ModelsView.tsx +++ b/apps/ui/src/features/ModelsView.tsx @@ -1,7 +1,7 @@ import type { ModelItem } from "../core/types"; type ModelsViewProps = { - models: ModelItem[]; + models: ModelItem[]; }; /** @@ -14,40 +14,43 @@ type ModelsViewProps = { * Models JSX element. */ export const ModelsView = ({ models }: ModelsViewProps) => ( -
-
-

- 模型广场 -

- - {models.length} 个模型 - -
-
- - - - - - - - - {models.map((model) => ( - - - - - ))} - -
- 模型 - - 渠道 -
- {model.id} - - {model.channels.map((channel) => channel.name).join(" / ")} -
-
-
+
+
+

+ 模型广场 +

+ + {models.length} 个模型 + +
+
+ + + + + + + + + {models.map((model) => ( + + + + + ))} + +
+ 模型 + + 渠道 +
+ {model.id} + + {model.channels.map((channel) => channel.name).join(" / ")} +
+
+
); diff --git a/apps/worker/src/routes/channels.ts b/apps/worker/src/routes/channels.ts index 5bb0174..395a0ec 100644 --- a/apps/worker/src/routes/channels.ts +++ b/apps/worker/src/routes/channels.ts @@ -8,8 +8,10 @@ import { listChannels, updateChannel, } from "../services/channel-repo"; +import { listCallTokens } from "../services/channel-call-token-repo"; +import { modelsToJson } from "../services/channel-models"; import { - fetchChannelModels, + testChannelTokens, updateChannelTestResult, } from "../services/channel-testing"; import { generateToken } from "../utils/crypto"; @@ -164,28 +166,49 @@ channels.post("/:id/test", async (c) => { return jsonError(c, 404, "channel_not_found", "channel_not_found"); } - const result = await fetchChannelModels( - String(channel.base_url), - String(channel.api_key), - ); + const callTokenRows = await listCallTokens(c.env.DB, { + channelIds: [id], + }); + const tokens = + callTokenRows.length > 0 + ? callTokenRows.map((row) => ({ + id: row.id, + name: row.name, + api_key: row.api_key, + })) + : [ + { + id: "primary", + name: "主调用令牌", + api_key: String(channel.api_key), + }, + ]; - if (!result.ok) { + const summary = await testChannelTokens(String(channel.base_url), tokens); + if (!summary.ok) { await updateChannelTestResult(c.env.DB, id, { ok: false, - elapsed: result.elapsed, + elapsed: summary.elapsed, }); return jsonError(c, 502, "channel_unreachable", "channel_unreachable"); } - const payload = result.payload ?? { data: [] }; - const models = Array.isArray(payload) ? payload : (payload?.data ?? []); + const models = summary.models.map((id) => ({ id })); await updateChannelTestResult(c.env.DB, id, { ok: true, - elapsed: result.elapsed, - modelsJson: JSON.stringify(payload), + elapsed: summary.elapsed, + modelsJson: modelsToJson(summary.models), }); - return c.json({ ok: true, models }); + return c.json({ + ok: true, + models, + token_summary: { + total: summary.total, + success: summary.success, + failed: summary.failed, + }, + }); }); export default channels; diff --git a/apps/worker/src/services/channel-models.ts b/apps/worker/src/services/channel-models.ts index 6edaf97..5d73804 100644 --- a/apps/worker/src/services/channel-models.ts +++ b/apps/worker/src/services/channel-models.ts @@ -26,7 +26,7 @@ export function normalizeModelsInput(input: unknown): string[] { return []; } if (Array.isArray(input)) { - return input.map((item) => String(item)).filter((item) => item.length > 0); + return input.map((item) => toModelId(item)).filter((item) => item.length > 0); } if (typeof input === "string") { return input diff --git a/apps/worker/src/services/channel-testing.ts b/apps/worker/src/services/channel-testing.ts index 343adf0..b811243 100644 --- a/apps/worker/src/services/channel-testing.ts +++ b/apps/worker/src/services/channel-testing.ts @@ -10,6 +10,30 @@ export type ChannelTestResult = { payload?: unknown[] | { data?: unknown[] }; }; +export type ChannelToken = { + id?: string; + name?: string; + api_key: string; +}; + +export type ChannelTokenTestItem = { + tokenId?: string; + tokenName?: string; + ok: boolean; + elapsed: number; + models: string[]; +}; + +export type ChannelTokenTestSummary = { + ok: boolean; + total: number; + success: number; + failed: number; + elapsed: number; + models: string[]; + items: ChannelTokenTestItem[]; +}; + export async function fetchChannelModels( baseUrl: string, apiKey: string, @@ -39,6 +63,75 @@ export async function fetchChannelModels( return { ok: true, elapsed, models, payload }; } +/** + * Tests channel models with multiple API keys and aggregates results. + * + * Args: + * baseUrl: Upstream base URL. + * tokens: List of call tokens to test. + * fetcher: Optional fetcher override for tests. + * + * Returns: + * Summary of token test results and aggregated model IDs. + */ +export async function testChannelTokens( + baseUrl: string, + tokens: ChannelToken[], + fetcher: ( + baseUrl: string, + apiKey: string, + ) => Promise = fetchChannelModels, +): Promise { + if (tokens.length === 0) { + return { + ok: false, + total: 0, + success: 0, + failed: 0, + elapsed: 0, + models: [], + items: [], + }; + } + + const items: ChannelTokenTestItem[] = []; + const modelSet = new Set(); + let success = 0; + let totalElapsed = 0; + + for (const token of tokens) { + const result = await fetcher(baseUrl, token.api_key); + totalElapsed += result.elapsed; + if (result.ok) { + success += 1; + for (const model of result.models) { + modelSet.add(model); + } + } + items.push({ + tokenId: token.id, + tokenName: token.name, + ok: result.ok, + elapsed: result.elapsed, + models: result.models, + }); + } + + const total = tokens.length; + const failed = total - success; + const elapsed = Math.round(totalElapsed / total); + + return { + ok: success > 0, + total, + success, + failed, + elapsed, + models: Array.from(modelSet), + items, + }; +} + export async function updateChannelTestResult( db: D1Database, id: string, diff --git a/helloagents/CHANGELOG.md b/helloagents/CHANGELOG.md index c9dd760..b14e615 100644 --- a/helloagents/CHANGELOG.md +++ b/helloagents/CHANGELOG.md @@ -89,6 +89,16 @@ - 类型: 微调(无方案包) - 文件: apps/worker/src/routes/proxy.ts +## [0.8.0] - 2026-02-22 + +### 变更 +- **[channels]**: 连通测试改为遍历调用令牌并汇总模型与结果统计 + - 方案: [202602222335_multi-token-test-checkin-default-off](archive/2026-02/202602222335_multi-token-test-checkin-default-off/) +- **[admin-ui]**: 新增站点默认关闭自动签到,连通测试提示令牌统计 + - 方案: [202602222335_multi-token-test-checkin-default-off](archive/2026-02/202602222335_multi-token-test-checkin-default-off/) +- **[tests]**: 新增多令牌连通测试聚合单测 + - 方案: [202602222335_multi-token-test-checkin-default-off](archive/2026-02/202602222335_multi-token-test-checkin-default-off/) + ### 修复 - **[checkin]**: 签到结果增加二次校验与非 JSON 响应判定 diff --git a/helloagents/modules/admin-ui.md b/helloagents/modules/admin-ui.md index ed788aa..76bf6db 100644 --- a/helloagents/modules/admin-ui.md +++ b/helloagents/modules/admin-ui.md @@ -18,10 +18,11 @@ - 前端状态集中在单页 - 站点管理采用列表视图,支持分页(默认 10 条),新增/编辑使用弹窗,保留启禁/连通测试操作 - 站点类型支持 new-api / done-hub / subapi / chatgpt / claude / gemini,类型驱动表单字段 -- 站点表单提供系统令牌、userid、自动签到开关与外部签到地址(new-api 默认开启) +- 站点表单提供系统令牌、userid、自动签到开关与外部签到地址(new-api 默认关闭) - 站点支持多调用令牌(备注 + 令牌值) - chatgpt/claude/gemini 站点支持 base_url 为空时自动填充官方地址 - done-hub 站点仅使用 base_url,不再维护多地址输入 +- 站点连通测试提示令牌成功/失败统计 - 使用日志支持本地分页(默认 50 条)与固定高度滚动,并展示令牌、输入/输出 tokens、首 token 延迟、流式与推理强度 - 令牌管理采用列表视图(默认每页 10 条),生成令牌使用弹窗表单,并展示创建时间与前缀 - 站点管理内置一键签到入口,展示今日签到结果(基于后台写回的 `last_checkin_*`) diff --git a/helloagents/modules/channels.md b/helloagents/modules/channels.md index d145bc1..5f7dc74 100644 --- a/helloagents/modules/channels.md +++ b/helloagents/modules/channels.md @@ -27,8 +27,8 @@ ## 行为规范 - `base_url` 保存为无尾斜杠格式 - 创建渠道时可传入自定义 `id`,未提供则自动生成 -- 连通性测试调用 `/v1/models` -- 测试成功后更新 `models_json` +- 连通性测试会对调用令牌逐个调用 `/v1/models` 并汇总模型 +- 至少一枚令牌测试成功时更新 `models_json` 并返回成功/失败统计 - New API 兼容层支持 `type`/`group`/`priority` 等字段映射,并保留扩展字段到 `metadata_json` - New API 标签(tag)存储在 `metadata_json.tag`,标签接口按该字段批量更新 - New API 分组列表从 `group_name` 字段解析,空时返回 `default` diff --git a/helloagents/modules/models.md b/helloagents/modules/models.md index 73d3709..b3a1c3a 100644 --- a/helloagents/modules/models.md +++ b/helloagents/modules/models.md @@ -11,6 +11,7 @@ ## 行为规范 - 仅聚合启用状态的渠道 - 优先使用渠道连通性测试返回的模型数据 +- 模型归一化支持字符串数组与对象数组(对象取 `id` 字段) ## 依赖关系 - `channels` 表