格式化

This commit is contained in:
lsy committed 2026-02-25 19:02:53 +08:00
1 parent 1495666ea6
commit d49487d5fc
13 files changed
+334 -326

No files matched your search

+6 -6
View File
@@ -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",
+5 -1
View File
@@ -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:
+217 -223
View File
@@ -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>
);
};
+37 -40
View File
@@ -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>
);
+33 -31
View File
@@ -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
+2 -2
View File
@@ -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,
+4 -4
View File
@@ -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";
+3 -1
View File
@@ -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
+3 -3
View File
@@ -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,
+1 -3
View File
@@ -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
+10 -6
View File
@@ -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;
+9
View File
@@ -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