fix:自动签到默认改为关闭;测试模型只测试一个令牌
This commit is contained in:
1 parent
0dd3b72edd
commit
f3bb4d435c
11 files changed
+426
-278
No files matched your search
+18
-7
@@ -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);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export const initialSiteForm: SiteForm = {
|
||||
checkin_url: "",
|
||||
system_token: "",
|
||||
system_userid: "",
|
||||
checkin_enabled: true,
|
||||
checkin_enabled: false,
|
||||
call_tokens: [
|
||||
{
|
||||
name: "主调用令牌",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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 响应判定
|
||||
|
||||
@@ -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_*`)
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
## 行为规范
|
||||
- 仅聚合启用状态的渠道
|
||||
- 优先使用渠道连通性测试返回的模型数据
|
||||
- 模型归一化支持字符串数组与对象数组(对象取 `id` 字段)
|
||||
|
||||
## 依赖关系
|
||||
- `channels` 表
|
||||
Reference in new issue
Block a user