fix:自动签到默认改为关闭;测试模型只测试一个令牌

This commit is contained in:
lsy committed 2026-02-22 23:56:15 +08:00
1 parent 0dd3b72edd
commit f3bb4d435c
11 files changed
+426 -278

No files matched your search

+18 -7
View File
@@ -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);
}
+1 -1
View File
@@ -29,7 +29,7 @@ export const initialSiteForm: SiteForm = {
checkin_url: "",
system_token: "",
system_userid: "",
checkin_enabled: true,
checkin_enabled: false,
call_tokens: [
{
name: "主调用令牌",
+223 -217
View File
@@ -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 (
<div class="rounded-2xl border border-stone-200 bg-white p-5 shadow-lg">
暂无数据
</div>
);
}
const errorRate = dashboard.summary.total_requests
? Math.round(
(dashboard.summary.total_errors / dashboard.summary.total_requests) *
100,
)
: 0;
return (
<div class="space-y-5">
<div class="grid grid-cols-1 gap-5 lg:grid-cols-3">
<div class="flex flex-col gap-1.5 rounded-2xl border border-stone-200 bg-white p-5 shadow-lg">
<span class="rounded-full bg-stone-100 px-2.5 py-1 text-xs text-stone-500">
总请求
</span>
<div class="text-2xl font-semibold text-stone-900">
{dashboard.summary.total_requests}
</div>
<span class="font-['Space_Grotesk'] text-xs text-stone-500">
最近窗口
</span>
</div>
<div class="flex flex-col gap-1.5 rounded-2xl border border-stone-200 bg-white p-5 shadow-lg">
<span class="rounded-full bg-stone-100 px-2.5 py-1 text-xs text-stone-500">
总 Tokens
</span>
<div class="text-2xl font-semibold text-stone-900">
{dashboard.summary.total_tokens}
</div>
<span class="font-['Space_Grotesk'] text-xs text-stone-500">
累计消耗
</span>
</div>
<div class="flex flex-col gap-1.5 rounded-2xl border border-stone-200 bg-white p-5 shadow-lg">
<span class="rounded-full bg-stone-100 px-2.5 py-1 text-xs text-stone-500">
错误率
</span>
<div class="text-2xl font-semibold text-stone-900">{errorRate}%</div>
<span class="font-['Space_Grotesk'] text-xs text-stone-500">
平均延迟 {Math.round(dashboard.summary.avg_latency)}ms
</span>
</div>
</div>
<div class="grid grid-cols-1 gap-5 lg:grid-cols-2">
<div class="rounded-2xl border border-stone-200 bg-white p-5 shadow-lg">
<div class="mb-4 flex items-center justify-between">
<h3 class="mb-0 font-['Space_Grotesk'] text-lg tracking-tight text-stone-900">
按日趋势
</h3>
</div>
<div class="overflow-x-auto">
<table class="min-w-[520px] w-full border-collapse text-xs sm:text-sm">
<thead>
<tr>
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
日期
</th>
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
请求
</th>
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
Tokens
</th>
</tr>
</thead>
<tbody>
{dashboard.byDay.map((row) => (
<tr class="hover:bg-stone-50" key={row.day}>
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
{row.day}
</td>
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
{row.requests}
</td>
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
{row.tokens}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<div class="rounded-2xl border border-stone-200 bg-white p-5 shadow-lg">
<div class="mb-4 flex items-center justify-between">
<h3 class="mb-0 font-['Space_Grotesk'] text-lg tracking-tight text-stone-900">
模型排行
</h3>
</div>
<div class="overflow-x-auto">
<table class="min-w-[520px] w-full border-collapse text-xs sm:text-sm">
<thead>
<tr>
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
模型
</th>
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
请求
</th>
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
Tokens
</th>
</tr>
</thead>
<tbody>
{dashboard.byModel.map((row) => (
<tr class="hover:bg-stone-50" key={row.model}>
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
{row.model ?? "-"}
</td>
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
{row.requests}
</td>
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
{row.tokens}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
<div class="grid grid-cols-1 gap-5 lg:grid-cols-2">
<div class="rounded-2xl border border-stone-200 bg-white p-5 shadow-lg">
<div class="mb-4 flex items-center justify-between">
<h3 class="mb-0 font-['Space_Grotesk'] text-lg tracking-tight text-stone-900">
渠道贡献
</h3>
</div>
<div class="overflow-x-auto">
<table class="min-w-[520px] w-full border-collapse text-xs sm:text-sm">
<thead>
<tr>
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
渠道
</th>
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
请求
</th>
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
Tokens
</th>
</tr>
</thead>
<tbody>
{dashboard.byChannel.map((row) => (
<tr
class="hover:bg-stone-50"
key={row.channel_name ?? "unknown"}
>
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
{row.channel_name ?? "-"}
</td>
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
{row.requests}
</td>
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
{row.tokens}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<div class="rounded-2xl border border-stone-200 bg-white p-5 shadow-lg">
<div class="mb-4 flex items-center justify-between">
<h3 class="mb-0 font-['Space_Grotesk'] text-lg tracking-tight text-stone-900">
令牌贡献
</h3>
</div>
<div class="overflow-x-auto">
<table class="min-w-[520px] w-full border-collapse text-xs sm:text-sm">
<thead>
<tr>
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
令牌
</th>
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
请求
</th>
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
Tokens
</th>
</tr>
</thead>
<tbody>
{dashboard.byToken.map((row) => (
<tr
class="hover:bg-stone-50"
key={row.token_name ?? "unknown"}
>
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
{row.token_name ?? "-"}
</td>
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
{row.requests}
</td>
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
{row.tokens}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
);
if (!dashboard) {
return (
<div class="rounded-2xl border border-stone-200 bg-white p-5 shadow-lg">
暂无数据
</div>
);
}
const errorRate = dashboard.summary.total_requests
? Math.round(
(dashboard.summary.total_errors / dashboard.summary.total_requests) *
100,
)
: 0;
return (
<div class="space-y-5">
<div class="grid grid-cols-1 gap-5 lg:grid-cols-3">
<div class="flex flex-col gap-1.5 rounded-2xl border border-stone-200 bg-white p-5 shadow-lg">
<span class="rounded-full bg-stone-100 px-2.5 py-1 text-xs text-stone-500">
总请求
</span>
<div class="text-2xl font-semibold text-stone-900">
{dashboard.summary.total_requests}
</div>
<span class="font-['Space_Grotesk'] text-xs text-stone-500">
最近窗口
</span>
</div>
<div class="flex flex-col gap-1.5 rounded-2xl border border-stone-200 bg-white p-5 shadow-lg">
<span class="rounded-full bg-stone-100 px-2.5 py-1 text-xs text-stone-500">
总 Tokens
</span>
<div class="text-2xl font-semibold text-stone-900">
{dashboard.summary.total_tokens}
</div>
<span class="font-['Space_Grotesk'] text-xs text-stone-500">
累计消耗
</span>
</div>
<div class="flex flex-col gap-1.5 rounded-2xl border border-stone-200 bg-white p-5 shadow-lg">
<span class="rounded-full bg-stone-100 px-2.5 py-1 text-xs text-stone-500">
错误率
</span>
<div class="text-2xl font-semibold text-stone-900">{errorRate}%</div>
<span class="font-['Space_Grotesk'] text-xs text-stone-500">
平均延迟 {Math.round(dashboard.summary.avg_latency)}ms
</span>
</div>
</div>
<div class="grid grid-cols-1 gap-5 lg:grid-cols-2">
<div class="rounded-2xl border border-stone-200 bg-white p-5 shadow-lg">
<div class="mb-4 flex items-center justify-between">
<h3 class="mb-0 font-['Space_Grotesk'] text-lg tracking-tight text-stone-900">
按日趋势
</h3>
</div>
<div class="overflow-x-auto">
<table class="min-w-130 w-full border-collapse text-xs sm:text-sm">
<thead>
<tr>
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
日期
</th>
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
请求
</th>
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
Tokens
</th>
</tr>
</thead>
<tbody>
{dashboard.byDay.map((row) => (
<tr
class="hover:bg-stone-50"
key={row.day}
>
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
{row.day}
</td>
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
{row.requests}
</td>
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
{row.tokens}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<div class="rounded-2xl border border-stone-200 bg-white p-5 shadow-lg">
<div class="mb-4 flex items-center justify-between">
<h3 class="mb-0 font-['Space_Grotesk'] text-lg tracking-tight text-stone-900">
模型排行
</h3>
</div>
<div class="overflow-x-auto">
<table class="min-w-130 w-full border-collapse text-xs sm:text-sm">
<thead>
<tr>
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
模型
</th>
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
请求
</th>
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
Tokens
</th>
</tr>
</thead>
<tbody>
{dashboard.byModel.map((row) => (
<tr
class="hover:bg-stone-50"
key={row.model}
>
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
{row.model ?? "-"}
</td>
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
{row.requests}
</td>
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
{row.tokens}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
<div class="grid grid-cols-1 gap-5 lg:grid-cols-2">
<div class="rounded-2xl border border-stone-200 bg-white p-5 shadow-lg">
<div class="mb-4 flex items-center justify-between">
<h3 class="mb-0 font-['Space_Grotesk'] text-lg tracking-tight text-stone-900">
渠道贡献
</h3>
</div>
<div class="overflow-x-auto">
<table class="min-w-130 w-full border-collapse text-xs sm:text-sm">
<thead>
<tr>
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
渠道
</th>
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
请求
</th>
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
Tokens
</th>
</tr>
</thead>
<tbody>
{dashboard.byChannel.map((row) => (
<tr
class="hover:bg-stone-50"
key={row.channel_name ?? "unknown"}
>
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
{row.channel_name ?? "-"}
</td>
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
{row.requests}
</td>
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
{row.tokens}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<div class="rounded-2xl border border-stone-200 bg-white p-5 shadow-lg">
<div class="mb-4 flex items-center justify-between">
<h3 class="mb-0 font-['Space_Grotesk'] text-lg tracking-tight text-stone-900">
令牌贡献
</h3>
</div>
<div class="overflow-x-auto">
<table class="min-w-130 w-full border-collapse text-xs sm:text-sm">
<thead>
<tr>
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
令牌
</th>
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
请求
</th>
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
Tokens
</th>
</tr>
</thead>
<tbody>
{dashboard.byToken.map((row) => (
<tr
class="hover:bg-stone-50"
key={row.token_name ?? "unknown"}
>
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
{row.token_name ?? "-"}
</td>
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
{row.requests}
</td>
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
{row.tokens}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
);
};
+40 -37
View File
@@ -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) => (
<div class="rounded-2xl border border-stone-200 bg-white p-5 shadow-lg">
<div class="mb-4 flex items-center justify-between">
<h3 class="mb-0 font-['Space_Grotesk'] text-lg tracking-tight text-stone-900">
模型广场
</h3>
<span class="rounded-full bg-stone-100 px-2.5 py-1 text-xs text-stone-500">
{models.length} 个模型
</span>
</div>
<div class="overflow-x-auto">
<table class="min-w-[420px] w-full border-collapse text-xs sm:text-sm">
<thead>
<tr>
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
模型
</th>
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
渠道
</th>
</tr>
</thead>
<tbody>
{models.map((model) => (
<tr class="hover:bg-stone-50" key={model.id}>
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
{model.id}
</td>
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
{model.channels.map((channel) => channel.name).join(" / ")}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<div class="rounded-2xl border border-stone-200 bg-white p-5 shadow-lg">
<div class="mb-4 flex items-center justify-between">
<h3 class="mb-0 font-['Space_Grotesk'] text-lg tracking-tight text-stone-900">
模型广场
</h3>
<span class="rounded-full bg-stone-100 px-2.5 py-1 text-xs text-stone-500">
{models.length} 个模型
</span>
</div>
<div class="overflow-x-auto">
<table class="min-w-105 w-full border-collapse text-xs sm:text-sm">
<thead>
<tr>
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
模型
</th>
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
渠道
</th>
</tr>
</thead>
<tbody>
{models.map((model) => (
<tr
class="hover:bg-stone-50"
key={model.id}
>
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
{model.id}
</td>
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
{model.channels.map((channel) => channel.name).join(" / ")}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
+35 -12
View File
@@ -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;
+1 -1
View File
@@ -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
@@ -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<ChannelTestResult> = fetchChannelModels,
): Promise<ChannelTokenTestSummary> {
if (tokens.length === 0) {
return {
ok: false,
total: 0,
success: 0,
failed: 0,
elapsed: 0,
models: [],
items: [],
};
}
const items: ChannelTokenTestItem[] = [];
const modelSet = new Set<string>();
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,
+10
View File
@@ -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 响应判定
+2 -1
View File
@@ -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_*`)
+2 -2
View File
@@ -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`
+1
View File
@@ -11,6 +11,7 @@
## 行为规范
- 仅聚合启用状态的渠道
- 优先使用渠道连通性测试返回的模型数据
- 模型归一化支持字符串数组与对象数组(对象取 `id` 字段)
## 依赖关系
- `channels` 表