格式化
This commit is contained in:
1 parent
1495666ea6
commit
d49487d5fc
13 files changed
+334
-326
No files matched your search
+6
-6
@@ -15,10 +15,10 @@ import {
|
||||
} from "./core/constants";
|
||||
import {
|
||||
filterSites,
|
||||
sortSites,
|
||||
summarizeSiteTests,
|
||||
type SiteSortState,
|
||||
type SiteTestResult,
|
||||
sortSites,
|
||||
summarizeSiteTests,
|
||||
} from "./core/sites";
|
||||
import type {
|
||||
AdminData,
|
||||
@@ -464,10 +464,10 @@ const App = () => {
|
||||
event.preventDefault();
|
||||
const form = event.currentTarget as HTMLFormElement;
|
||||
const formData = new FormData(form);
|
||||
const payload = Object.fromEntries(formData.entries()) as Record<
|
||||
string,
|
||||
FormDataEntryValue
|
||||
>;
|
||||
const payload: Record<string, FormDataEntryValue> = {};
|
||||
formData.forEach((value, key) => {
|
||||
payload[key] = value;
|
||||
});
|
||||
try {
|
||||
const result = await apiFetch<{ token: string }>("/api/tokens", {
|
||||
method: "POST",
|
||||
|
||||
@@ -117,7 +117,11 @@ const getSortValue = (site: Site, key: SiteSortKey, today: string) => {
|
||||
case "tokens":
|
||||
return Number(site.call_tokens?.length ?? 0);
|
||||
case "checkin_enabled":
|
||||
return site.site_type === "new-api" ? (site.checkin_enabled ? "已开启" : "已关闭") : "-";
|
||||
return site.site_type === "new-api"
|
||||
? site.checkin_enabled
|
||||
? "已开启"
|
||||
: "已关闭"
|
||||
: "-";
|
||||
case "checkin":
|
||||
return getSiteCheckinLabel(site, today);
|
||||
default:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { DashboardData } from "../core/types";
|
||||
|
||||
type DashboardViewProps = {
|
||||
dashboard: DashboardData | null;
|
||||
dashboard: DashboardData | null;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -14,226 +14,220 @@ 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-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>
|
||||
);
|
||||
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,43 +14,40 @@ 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-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>
|
||||
<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>
|
||||
);
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { CheckinSummary, Site, SiteForm } from "../core/types";
|
||||
import {
|
||||
getSiteCheckinLabel,
|
||||
getSiteTypeLabel,
|
||||
type SiteSortKey,
|
||||
type SiteSortState,
|
||||
} from "../core/sites";
|
||||
import type { CheckinSummary, Site, SiteForm } from "../core/types";
|
||||
import {
|
||||
buildPageItems,
|
||||
formatDateTime,
|
||||
@@ -182,9 +182,7 @@ export const SitesView = ({
|
||||
placeholder="站点名称或 URL"
|
||||
value={siteSearch}
|
||||
onInput={(event) =>
|
||||
onSearchChange(
|
||||
(event.currentTarget as HTMLInputElement).value,
|
||||
)
|
||||
onSearchChange((event.currentTarget as HTMLInputElement).value)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
@@ -219,10 +217,6 @@ export const SitesView = ({
|
||||
const systemReady = Boolean(
|
||||
site.system_token && site.system_userid,
|
||||
);
|
||||
const checkinEnabled =
|
||||
site.site_type === "new-api" && Boolean(site.checkin_enabled);
|
||||
const showCheckin =
|
||||
site.site_type === "new-api" && checkinEnabled;
|
||||
const callTokenCount = site.call_tokens?.length ?? 0;
|
||||
return (
|
||||
<div
|
||||
@@ -252,12 +246,12 @@ export const SitesView = ({
|
||||
{isActive ? "启用" : "禁用"}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-3 flex items-center justify-between text-xs text-stone-500">
|
||||
<span>类型</span>
|
||||
<span class="font-semibold text-stone-700">
|
||||
{getSiteTypeLabel(site.site_type)}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-3 flex items-center justify-between text-xs text-stone-500">
|
||||
<span>类型</span>
|
||||
<span class="font-semibold text-stone-700">
|
||||
{getSiteTypeLabel(site.site_type)}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-3 flex items-center justify-between text-xs text-stone-500">
|
||||
<span>权重</span>
|
||||
<span class="font-semibold text-stone-700">
|
||||
@@ -290,11 +284,13 @@ export const SitesView = ({
|
||||
<p class="mt-1 font-semibold text-stone-700">
|
||||
{getSiteCheckinLabel(site, today)}
|
||||
</p>
|
||||
{message && showCheckin && (
|
||||
<p class="mt-1 truncate text-[11px] text-stone-500">
|
||||
{message}
|
||||
</p>
|
||||
)}
|
||||
{message &&
|
||||
site.site_type === "new-api" &&
|
||||
site.checkin_enabled && (
|
||||
<p class="mt-1 truncate text-[11px] text-stone-500">
|
||||
{message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 grid grid-cols-2 gap-2">
|
||||
@@ -342,9 +338,7 @@ export const SitesView = ({
|
||||
onClick={() => toggleSort(column.key)}
|
||||
>
|
||||
{column.label}
|
||||
<span class="text-[10px]">
|
||||
{sortIndicator(column.key)}
|
||||
</span>
|
||||
<span class="text-[10px]">{sortIndicator(column.key)}</span>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
@@ -358,12 +352,6 @@ export const SitesView = ({
|
||||
<div class="divide-y divide-stone-100">
|
||||
{pagedSites.map((site) => {
|
||||
const isActive = site.status === "active";
|
||||
const isToday = site.last_checkin_date === today;
|
||||
const checkinEnabled =
|
||||
site.site_type === "new-api" &&
|
||||
Boolean(site.checkin_enabled);
|
||||
const showCheckin =
|
||||
site.site_type === "new-api" && checkinEnabled;
|
||||
const callTokenCount = site.call_tokens?.length ?? 0;
|
||||
return (
|
||||
<div
|
||||
@@ -406,7 +394,11 @@ export const SitesView = ({
|
||||
{callTokenCount > 0 ? `${callTokenCount} 个` : "-"}
|
||||
</div>
|
||||
<div class="text-xs text-stone-600">
|
||||
{site.site_type === "new-api" ? (site.checkin_enabled ? "已开启" : "已关闭") : "-"}
|
||||
{site.site_type === "new-api"
|
||||
? site.checkin_enabled
|
||||
? "已开启"
|
||||
: "已关闭"
|
||||
: "-"}
|
||||
</div>
|
||||
<div class="text-xs text-stone-600">
|
||||
{getSiteCheckinLabel(site, today)}
|
||||
@@ -802,8 +794,18 @@ export const SitesView = ({
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="disabled" selected={!siteForm.checkin_enabled}>自动签到关闭</option>
|
||||
<option value="enabled" selected={siteForm.checkin_enabled}>自动签到开启</option>
|
||||
<option
|
||||
value="disabled"
|
||||
selected={!siteForm.checkin_enabled}
|
||||
>
|
||||
自动签到关闭
|
||||
</option>
|
||||
<option
|
||||
value="enabled"
|
||||
selected={siteForm.checkin_enabled}
|
||||
>
|
||||
自动签到开启
|
||||
</option>
|
||||
</select>
|
||||
)}
|
||||
<input
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Hono } from "hono";
|
||||
import type { AppEnv } from "../env";
|
||||
import { listCallTokens } from "../services/channel-call-token-repo";
|
||||
import { modelsToJson } from "../services/channel-models";
|
||||
import {
|
||||
channelExists,
|
||||
deleteChannel,
|
||||
@@ -8,8 +10,6 @@ import {
|
||||
listChannels,
|
||||
updateChannel,
|
||||
} from "../services/channel-repo";
|
||||
import { listCallTokens } from "../services/channel-call-token-repo";
|
||||
import { modelsToJson } from "../services/channel-models";
|
||||
import {
|
||||
testChannelTokens,
|
||||
updateChannelTestResult,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { Hono } from "hono";
|
||||
import type { AppEnv } from "../env";
|
||||
import {
|
||||
getCheckinSchedulerStub,
|
||||
shouldResetLastRun,
|
||||
} from "../services/checkin-scheduler";
|
||||
import {
|
||||
getCheckinScheduleTime,
|
||||
getRetentionDays,
|
||||
@@ -10,10 +14,6 @@ import {
|
||||
setRetentionDays,
|
||||
setSessionTtlHours,
|
||||
} from "../services/settings";
|
||||
import {
|
||||
getCheckinSchedulerStub,
|
||||
shouldResetLastRun,
|
||||
} from "../services/checkin-scheduler";
|
||||
import { sha256Hex } from "../utils/crypto";
|
||||
import { jsonError } from "../utils/http";
|
||||
|
||||
|
||||
@@ -26,7 +26,9 @@ export function normalizeModelsInput(input: unknown): string[] {
|
||||
return [];
|
||||
}
|
||||
if (Array.isArray(input)) {
|
||||
return input.map((item) => toModelId(item)).filter((item) => item.length > 0);
|
||||
return input
|
||||
.map((item) => toModelId(item))
|
||||
.filter((item) => item.length > 0);
|
||||
}
|
||||
if (typeof input === "string") {
|
||||
return input
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { D1Database } from "@cloudflare/workers-types";
|
||||
import { beijingDateString } from "../utils/time";
|
||||
import { listChannels, updateChannelCheckinResult } from "./channel-repo";
|
||||
import {
|
||||
runCheckin,
|
||||
summarizeCheckin,
|
||||
type CheckinResultItem,
|
||||
type CheckinSummary,
|
||||
runCheckin,
|
||||
summarizeCheckin,
|
||||
} from "./checkin";
|
||||
import { parseSiteMetadata } from "./site-metadata";
|
||||
import { beijingDateString } from "../utils/time";
|
||||
|
||||
export type CheckinRunResult = {
|
||||
results: CheckinResultItem[];
|
||||
|
||||
@@ -3,13 +3,13 @@ import type {
|
||||
DurableObjectState,
|
||||
} from "@cloudflare/workers-types";
|
||||
import type { Bindings } from "../env";
|
||||
import { getCheckinScheduleTime } from "./settings";
|
||||
import { runCheckinAll } from "./checkin-runner";
|
||||
import {
|
||||
beijingDateString,
|
||||
computeBeijingScheduleTime,
|
||||
computeNextBeijingRun,
|
||||
} from "../utils/time";
|
||||
import { runCheckinAll } from "./checkin-runner";
|
||||
import { getCheckinScheduleTime } from "./settings";
|
||||
|
||||
const SCHEDULER_NAME = "checkin-scheduler";
|
||||
const LAST_RUN_DATE_KEY = "last_run_date";
|
||||
@@ -30,10 +30,8 @@ export const shouldRunCheckin = (
|
||||
return now.getTime() >= scheduledAt.getTime();
|
||||
};
|
||||
|
||||
export const shouldResetLastRun = (
|
||||
currentTime: string,
|
||||
nextTime: string,
|
||||
) => currentTime !== nextTime;
|
||||
export const shouldResetLastRun = (currentTime: string, nextTime: string) =>
|
||||
currentTime !== nextTime;
|
||||
|
||||
export const computeNextAlarmAt = (
|
||||
now: Date,
|
||||
|
||||
@@ -112,9 +112,7 @@ export async function isAdminPasswordSet(db: D1Database): Promise<boolean> {
|
||||
return Boolean(hash);
|
||||
}
|
||||
|
||||
export async function getCheckinScheduleTime(
|
||||
db: D1Database,
|
||||
): Promise<string> {
|
||||
export async function getCheckinScheduleTime(db: D1Database): Promise<string> {
|
||||
const timeRaw = await readSetting(db, CHECKIN_SCHEDULE_TIME_KEY);
|
||||
return timeRaw && timeRaw.length > 0
|
||||
? timeRaw
|
||||
|
||||
@@ -28,7 +28,9 @@ export function beijingDateString(date: Date = new Date()): string {
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
export function parseScheduleTime(value: string): { hour: number; minute: number } | null {
|
||||
export function parseScheduleTime(
|
||||
value: string,
|
||||
): { hour: number; minute: number } | null {
|
||||
if (!/^\d{2}:\d{2}$/.test(value)) {
|
||||
return null;
|
||||
}
|
||||
@@ -57,8 +59,13 @@ export function getBeijingDateParts(date: Date = new Date()) {
|
||||
};
|
||||
}
|
||||
|
||||
const DEFAULT_SCHEDULE_PARTS = parseScheduleTime(DEFAULT_SCHEDULE_TIME) ?? {
|
||||
hour: 0,
|
||||
minute: 10,
|
||||
};
|
||||
|
||||
const resolveScheduleParts = (value: string) =>
|
||||
parseScheduleTime(value) ?? parseScheduleTime(DEFAULT_SCHEDULE_TIME)!;
|
||||
parseScheduleTime(value) ?? DEFAULT_SCHEDULE_PARTS;
|
||||
|
||||
export function computeBeijingScheduleTime(
|
||||
now: Date,
|
||||
@@ -71,10 +78,7 @@ export function computeBeijingScheduleTime(
|
||||
return new Date(utcMs);
|
||||
}
|
||||
|
||||
export function computeNextBeijingRun(
|
||||
now: Date,
|
||||
scheduleTime: string,
|
||||
): Date {
|
||||
export function computeNextBeijingRun(now: Date, scheduleTime: string): Date {
|
||||
const scheduled = computeBeijingScheduleTime(now, scheduleTime);
|
||||
if (now.getTime() < scheduled.getTime()) {
|
||||
return scheduled;
|
||||
|
||||
@@ -4,6 +4,15 @@
|
||||
|
||||
### 微调
|
||||
|
||||
- **[admin-ui]**: 令牌创建表单使用 FormData.forEach 以兼容缺少 entries 的类型定义
|
||||
- 类型: 微调(无方案包)
|
||||
- 文件: apps/ui/src/App.tsx
|
||||
- **[admin-ui]**: 清理站点列表未使用变量以通过 lint
|
||||
- 类型: 微调(无方案包)
|
||||
- 文件: apps/ui/src/features/SitesView.tsx
|
||||
- **[worker]**: 时间工具函数移除非空断言以满足 lint
|
||||
- 类型: 微调(无方案包)
|
||||
- 文件: apps/worker/src/utils/time.ts
|
||||
- **[admin-ui]**: 站点管理新增一键测试并汇总结果提示
|
||||
- 类型: 微调(无方案包)
|
||||
- 文件: apps/ui/src/App.tsx, apps/ui/src/features/SitesView.tsx, apps/ui/src/core/sites.ts, tests/ui/sites.test.ts
|
||||
|
||||
Reference in new issue
Block a user