fix:将ui改成扁平化ui,优化ui错误

This commit is contained in:
lsy committed 2026-03-18 21:48:50 +08:00
1 parent d87610151c
commit d6aa20e85b
36 files changed
+4023 -2973

No files matched your search

+30 -35
View File
@@ -485,29 +485,29 @@ const App = () => {
const handleDashboardApply = useCallback(
async (override?: DashboardQuery) => {
const actionKey = buildActionKey("dashboard:filter");
if (isActionPending(actionKey)) {
return;
}
const nextQuery = override ?? dashboardQuery;
startAction(actionKey);
try {
await loadDashboard(nextQuery);
pushNotice("success", "筛选已更新");
} catch (error) {
pushNotice("error", (error as Error).message);
} finally {
endAction(actionKey);
}
},
[
dashboardQuery,
endAction,
isActionPending,
loadDashboard,
pushNotice,
startAction,
],
const actionKey = buildActionKey("dashboard:filter");
if (isActionPending(actionKey)) {
return;
}
const nextQuery = override ?? dashboardQuery;
startAction(actionKey);
try {
await loadDashboard(nextQuery);
pushNotice("success", "筛选已更新");
} catch (error) {
pushNotice("error", (error as Error).message);
} finally {
endAction(actionKey);
}
},
[
dashboardQuery,
endAction,
isActionPending,
loadDashboard,
pushNotice,
startAction,
],
);
const loadSites = useCallback(async () => {
@@ -667,16 +667,14 @@ const App = () => {
proxy_upstream_timeout_ms: String(
runtimeSettings?.upstream_timeout_ms ?? 30000,
),
proxy_stream_usage_mode:
runtimeSettings?.stream_usage_mode ?? "full",
proxy_stream_usage_mode: runtimeSettings?.stream_usage_mode ?? "full",
proxy_stream_usage_max_bytes: String(
runtimeSettings?.stream_usage_max_bytes ?? 0,
),
proxy_stream_usage_max_parsers: String(
runtimeSettings?.stream_usage_max_parsers ?? 0,
),
proxy_usage_queue_enabled:
runtimeSettings?.usage_queue_enabled ?? true,
proxy_usage_queue_enabled: runtimeSettings?.usage_queue_enabled ?? true,
usage_queue_daily_limit: String(
runtimeSettings?.usage_queue_daily_limit ?? 10000,
),
@@ -1303,17 +1301,16 @@ const App = () => {
settingsForm.model_failure_cooldown_minutes,
);
const upstreamTimeoutMs = Number(settingsForm.proxy_upstream_timeout_ms);
const streamUsageMode =
settingsForm.proxy_stream_usage_mode.trim().toLowerCase();
const streamUsageMode = settingsForm.proxy_stream_usage_mode
.trim()
.toLowerCase();
const streamUsageMaxBytes = Number(
settingsForm.proxy_stream_usage_max_bytes,
);
const streamUsageMaxParsers = Number(
settingsForm.proxy_stream_usage_max_parsers,
);
const usageQueueDailyLimit = Number(
settingsForm.usage_queue_daily_limit,
);
const usageQueueDailyLimit = Number(settingsForm.usage_queue_daily_limit);
const usageQueueDirectRatio = Number(
settingsForm.usage_queue_direct_write_ratio,
);
@@ -1327,9 +1324,7 @@ const App = () => {
const cacheCallTokensTtl = Number(
settingsForm.cache_ttl_call_tokens_seconds,
);
const cacheSettingsTtl = Number(
settingsForm.cache_ttl_settings_seconds,
);
const cacheSettingsTtl = Number(settingsForm.cache_ttl_settings_seconds);
if (
Number.isNaN(retention) ||
retention < 1 ||
+7 -3
View File
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef, useState } from "hono/jsx/dom";
import { cx, type ClassName } from "./utils";
import { type ClassName, cx } from "./utils";
export type AreaChartPoint = {
label: string;
@@ -111,7 +111,10 @@ export const AreaChart = ({
}
const maxValue = Math.max(...data.map((item) => item.value), 1);
const drawableWidth = Math.max(1, viewBoxWidth - paddingX * 2);
const drawableHeight = Math.max(1, viewBoxHeight - paddingTop - paddingBottom);
const drawableHeight = Math.max(
1,
viewBoxHeight - paddingTop - paddingBottom,
);
return data.map((item, index) => {
const x =
data.length === 1
@@ -143,7 +146,8 @@ export const AreaChart = ({
const horizontalGridLines = useMemo(
() =>
[0.2, 0.4, 0.6, 0.8].map(
(step) => paddingTop + step * (viewBoxHeight - paddingTop - paddingBottom),
(step) =>
paddingTop + step * (viewBoxHeight - paddingTop - paddingBottom),
),
[paddingBottom, paddingTop, viewBoxHeight],
);
+5 -2
View File
@@ -1,7 +1,7 @@
import { useEffect, useMemo, useState } from "hono/jsx/dom";
import { Button } from "./button";
import { Popover, PopoverContent } from "./popover";
import { cx, type ClassName } from "./utils";
import { type ClassName, cx } from "./utils";
export type ColumnOption = {
id: string;
@@ -77,7 +77,10 @@ export const ColumnPicker = ({
{label}
</Button>
<Popover open={open}>
<PopoverContent class="right-0 w-64 p-3 app-popover-content--spaced">
<PopoverContent
class="right-0 p-3 app-popover-content--spaced app-column-picker-popover"
style="width:176px;min-width:176px;max-width:176px;"
>
<div class="text-xs font-semibold uppercase tracking-widest text-[color:var(--app-ink-muted)]">
显示列
</div>
+24 -13
View File
@@ -1,4 +1,5 @@
import type { JSX } from "hono/jsx";
import { createPortal } from "hono/jsx/dom";
import { cx } from "./utils";
export type DialogProps = {
@@ -7,22 +8,32 @@ export type DialogProps = {
children?: unknown;
};
export const Dialog = ({ open, onClose, children }: DialogProps) => {
export const Dialog = ({
open,
onClose,
children,
}: DialogProps): JSX.Element | null => {
if (!open) {
return null;
}
if (typeof document === "undefined") {
return null;
}
return (
<div class="fixed inset-0 z-50">
<button
aria-label="关闭弹窗"
class="absolute inset-0 bg-slate-950/60"
type="button"
onClick={onClose}
/>
<div class="relative z-10 flex min-h-screen items-center justify-center px-4 py-8">
{children}
</div>
</div>
<>
{createPortal(
<div class="app-dialog-root">
<button
aria-label="关闭弹窗"
class="app-dialog-backdrop"
type="button"
onClick={onClose}
/>
<div class="app-dialog-frame">{children}</div>
</div>,
document.body,
)}
</>
);
};
@@ -32,7 +43,7 @@ export const DialogContent = ({
}: JSX.IntrinsicElements["div"]) => (
<div
{...props}
class={cx("app-card w-full max-w-xl p-6", className)}
class={cx("app-card app-dialog-content w-full max-w-xl p-6", className)}
role="dialog"
/>
);
+5 -2
View File
@@ -1,7 +1,7 @@
import { useEffect, useMemo, useState } from "hono/jsx/dom";
import { Input } from "./input";
import { PopoverContent } from "./popover";
import { cx, type ClassName } from "./utils";
import { type ClassName, cx } from "./utils";
export type MultiSelectOption = {
value: string;
@@ -124,7 +124,10 @@ export const MultiSelect = ({
>
<button
aria-expanded={open}
class={cx("app-multi-select app-focus", open && "app-multi-select--open")}
class={cx(
"app-multi-select app-focus",
open && "app-multi-select--open",
)}
type="button"
onClick={(event) => {
event.stopPropagation();
+1 -1
View File
@@ -1,5 +1,5 @@
import { Button } from "./button";
import { cx, type ClassName } from "./utils";
import { type ClassName, cx } from "./utils";
export type PaginationItem = number | "ellipsis";
+6 -1
View File
@@ -12,5 +12,10 @@ export const Select = ({
}: SelectProps) => {
const variantClass =
variant === "pill" ? "app-input app-input--pill" : "app-input";
return <select {...props} class={cx(variantClass, "app-focus", className)} />;
return (
<select
{...props}
class={cx(variantClass, "app-select app-focus", className)}
/>
);
};
+3 -1
View File
@@ -3,5 +3,7 @@ type ClassInput = ClassName | Promise<string>;
export const cx = (...classes: ClassInput[]) =>
classes
.filter((value): value is string => typeof value === "string" && value.length > 0)
.filter(
(value): value is string => typeof value === "string" && value.length > 0,
)
.join(" ");
+12 -10
View File
@@ -67,7 +67,7 @@ export const AppLayout = ({
};
return (
<div class="relative flex min-h-screen flex-col lg:grid lg:grid-cols-[260px_1fr]">
<div class="relative flex min-h-screen flex-col lg:grid lg:grid-cols-[300px_1fr]">
<input class="peer hidden" id="app-nav-toggle" type="checkbox" />
<header class="app-bar flex items-center justify-between px-4 py-4 lg:hidden">
<div class="flex items-center gap-3">
@@ -129,12 +129,14 @@ export const AppLayout = ({
</Button>
</div>
</header>
<aside class="app-sidebar fixed inset-y-0 left-0 z-40 flex w-72 -translate-x-full flex-col overflow-y-auto px-5 py-8 shadow-xl transition-transform duration-300 ease-in-out peer-checked:translate-x-0 lg:sticky lg:top-0 lg:z-auto lg:h-screen lg:w-auto lg:translate-x-0 lg:shadow-none">
<div class="mb-8 flex flex-col gap-1.5">
<h2 class="app-title text-lg">api-workers</h2>
<span class="text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]">
console
</span>
<aside class="app-sidebar fixed inset-y-0 left-0 z-40 flex w-[18.5rem] max-w-[86vw] -translate-x-full flex-col overflow-y-auto rounded-r-[28px] px-5 py-6 shadow-xl transition-transform duration-300 ease-in-out peer-checked:translate-x-0 lg:sticky lg:top-5 lg:z-10 lg:mx-5 lg:my-5 lg:h-[calc(100vh-40px)] lg:w-auto lg:translate-x-0 lg:rounded-[30px] lg:shadow-none">
<div class="mb-8 flex flex-col gap-3">
<div class="flex flex-col gap-1.5">
<h2 class="app-title text-[21px]">api-workers</h2>
<span class="text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]">
control center
</span>
</div>
</div>
<nav class="flex flex-col gap-2.5">
{tabs.map((tab) => (
@@ -152,7 +154,7 @@ export const AppLayout = ({
</button>
))}
</nav>
<div class="mt-auto hidden w-full items-center justify-between gap-2 rounded-2xl border border-[color:var(--app-border)] bg-white/70 px-3 py-2 text-xs text-[color:var(--app-ink-muted)] lg:flex">
<div class="mt-auto hidden w-full items-center justify-between gap-2 rounded-[18px] border border-[color:var(--app-border)] bg-[color:var(--app-surface-muted)] px-3 py-2 text-xs text-[color:var(--app-ink-muted)] lg:flex">
<span>{token ? "已登录" : "未登录"}</span>
<Button
class="h-8 px-3 text-[11px]"
@@ -165,8 +167,8 @@ export const AppLayout = ({
</Button>
</div>
</aside>
<main class="px-4 pt-5 pb-16 sm:px-10 sm:pt-8">
{children}
<main class="px-4 pt-5 pb-16 sm:px-10 sm:pt-8 lg:pt-5 lg:pl-0 lg:pr-8">
<div class="animate-fade-up">{children}</div>
</main>
{notices.length > 0 && (
<ToastViewport aria-live="polite">
File diff suppressed because it is too large. Load diff
+51 -53
View File
@@ -2,9 +2,9 @@ import { Button, Card, Input } from "../components/ui";
import type { NoticeMessage } from "../core/types";
type LoginViewProps = {
notice: NoticeMessage | null;
isSubmitting: boolean;
onSubmit: (event: Event) => void;
notice: NoticeMessage | null;
isSubmitting: boolean;
onSubmit: (event: Event) => void;
};
/**
@@ -17,55 +17,53 @@ type LoginViewProps = {
* Login JSX element.
*/
export const LoginView = ({
notice,
isSubmitting,
onSubmit,
notice,
isSubmitting,
onSubmit,
}: LoginViewProps) => {
const toneStyles: Record<NoticeMessage["tone"], string> = {
success: "app-notice app-notice--success",
warning: "app-notice app-notice--warning",
error: "app-notice app-notice--error",
info: "app-notice app-notice--info",
};
return (
<Card class="animate-fade-up mx-auto mt-24 max-w-md p-8">
<h1 class="app-title mb-2 text-2xl">api-workers</h1>
<p class="text-sm text-[color:var(--app-ink-muted)]">
请输入管理员密码登录管理台。
</p>
<form
class="mt-6 grid gap-4"
onSubmit={onSubmit}
>
<div>
<label
class="mb-1.5 block text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]"
for="password"
>
管理员密码
</label>
<Input
id="password"
name="password"
type="password"
required
/>
</div>
<Button
variant="primary"
size="lg"
type="submit"
disabled={isSubmitting}
>
{isSubmitting ? "登录中..." : "登录"}
</Button>
</form>
<p class="mt-3 text-xs text-[color:var(--app-ink-muted)]">
按回车键可快速提交。
</p>
{notice && (
<div class={`mt-4 ${toneStyles[notice.tone]}`}>{notice.message}</div>
)}
</Card>
);
const toneStyles: Record<NoticeMessage["tone"], string> = {
success: "app-notice app-notice--success",
warning: "app-notice app-notice--warning",
error: "app-notice app-notice--error",
info: "app-notice app-notice--info",
};
return (
<div class="flex min-h-screen items-center justify-center px-4 py-10">
<Card class="app-login-card animate-fade-up p-8 sm:p-10">
<div class="app-login-badge mb-4 w-max">
<span aria-hidden="true" class="app-login-badge__dot" />
SwiftUI Style
</div>
<h1 class="app-title mb-2 text-3xl">api-workers</h1>
<p class="text-sm text-[color:var(--app-ink-muted)]">
欢迎回来,输入管理员密码进入控制台。
</p>
<form class="mt-7 grid gap-4" onSubmit={onSubmit}>
<div>
<label
class="mb-1.5 block text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]"
for="password"
>
管理员密码
</label>
<Input id="password" name="password" type="password" required />
</div>
<Button
variant="primary"
size="lg"
type="submit"
disabled={isSubmitting}
>
{isSubmitting ? "登录中..." : "登录"}
</Button>
</form>
<p class="mt-4 text-xs text-[color:var(--app-ink-muted)]">
支持回车快捷提交。
</p>
{notice && (
<div class={`mt-4 ${toneStyles[notice.tone]}`}>{notice.message}</div>
)}
</Card>
</div>
);
};
+35 -39
View File
@@ -5,7 +5,6 @@ import {
ColumnPicker,
MultiSelect,
Pagination,
Select,
Table,
TableBody,
TableCell,
@@ -87,8 +86,7 @@ export const ModelsView = ({ models }: ModelsViewProps) => {
.sort((a, b) => a.label.localeCompare(b.label));
}, [models]);
const filteredModels = useMemo(() => {
const modelSet =
modelFilters.length > 0 ? new Set(modelFilters) : null;
const modelSet = modelFilters.length > 0 ? new Set(modelFilters) : null;
const channelSet =
channelFilters.length > 0 ? new Set(channelFilters) : null;
return models.filter((model) => {
@@ -134,9 +132,9 @@ export const ModelsView = ({ models }: ModelsViewProps) => {
<Card variant="compact" class="app-layer-raised space-y-3 p-4">
<div class="grid gap-3 sm:grid-cols-2">
<div>
<label class="mb-1.5 block text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]">
<p class="mb-1.5 block text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]">
模型
</label>
</p>
<MultiSelect
class="w-full"
options={modelOptions}
@@ -148,9 +146,9 @@ export const ModelsView = ({ models }: ModelsViewProps) => {
/>
</div>
<div>
<label class="mb-1.5 block text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]">
<p class="mb-1.5 block text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]">
渠道
</label>
</p>
<MultiSelect
class="w-full"
options={channelOptions}
@@ -190,18 +188,18 @@ export const ModelsView = ({ models }: ModelsViewProps) => {
</TableRow>
) : (
pagedModels.map((model) => (
<TableRow key={model.id}>
{visibleColumnSet.has("model") && (
<TableCell>{model.id}</TableCell>
)}
{visibleColumnSet.has("channels") && (
<TableCell>
{model.channels
.map((channel) => channel.name)
.join(" / ")}
</TableCell>
)}
</TableRow>
<TableRow key={model.id}>
{visibleColumnSet.has("model") && (
<TableCell>{model.id}</TableCell>
)}
{visibleColumnSet.has("channels") && (
<TableCell>
{model.channels
.map((channel) => channel.name)
.join(" / ")}
</TableCell>
)}
</TableRow>
))
)}
</TableBody>
@@ -209,7 +207,7 @@ export const ModelsView = ({ models }: ModelsViewProps) => {
</div>
)}
{models.length > 0 && (
<div class="flex flex-col gap-3 text-xs text-[color:var(--app-ink-muted)] sm:flex-row sm:items-center sm:justify-between">
<div class="app-pagination-bar flex flex-col gap-3 text-xs text-[color:var(--app-ink-muted)] sm:flex-row sm:items-center sm:justify-between">
<div class="flex flex-wrap items-center gap-2">
<span class="text-xs text-[color:var(--app-ink-muted)]">
共 {filteredModels.length} 条 · {totalPages} 页
@@ -221,28 +219,26 @@ export const ModelsView = ({ models }: ModelsViewProps) => {
onPageChange={setPage}
/>
</div>
<label class="app-page-size" for="model-page-size">
<span>每页条数</span>
<Select
variant="pill"
class="w-auto text-xs app-page-size__select"
id="model-page-size"
value={String(pageSize)}
onChange={(event) => {
const next = Number(
(event.currentTarget as HTMLSelectElement).value,
);
persistPageSizePref("pageSize:models", next);
setPageSize(next);
}}
>
<div class="app-page-size-control">
<span class="app-page-size-control__label">每页</span>
<div class="app-page-size-control__chips">
{pageSizeOptions.map((size) => (
<option key={size} value={String(size)}>
<button
class={`app-page-size-chip ${
pageSize === size ? "app-page-size-chip--active" : ""
}`}
key={size}
type="button"
onClick={() => {
persistPageSizePref("pageSize:models", size);
setPageSize(size);
}}
>
{size}
</option>
</button>
))}
</Select>
</label>
</div>
</div>
</div>
)}
</div>
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+22 -25
View File
@@ -157,7 +157,7 @@ export const TokensView = ({
}, [isTokenModalOpen, onCloseModal]);
return (
<div class="space-y-5">
<div class="animate-fade-up space-y-4">
<div class="app-panel animate-fade-up space-y-4">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<h3 class="app-title text-lg">令牌列表</h3>
@@ -181,7 +181,7 @@ export const TokensView = ({
</div>
</div>
<div>
<div class="space-y-3 md:hidden">
<div class="app-mobile-stack space-y-3 md:hidden">
{pagedTokens.length === 0 ? (
<Card class="text-center text-sm text-[color:var(--app-ink-muted)]">
<p>暂无令牌,请先创建。</p>
@@ -300,9 +300,9 @@ export const TokensView = ({
})
)}
</div>
<div class="app-surface hidden overflow-hidden md:block">
<div class="app-surface app-list-shell hidden overflow-hidden md:block">
<div
class="grid gap-3 bg-white/60 px-4 py-3 text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]"
class="app-list-header grid gap-3 px-4 py-3 text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]"
style={`grid-template-columns: ${tokenGridTemplate};`}
>
{visibleColumnSet.has("name") && <div>名称</div>}
@@ -315,7 +315,7 @@ export const TokensView = ({
{visibleColumnSet.has("actions") && <div>操作</div>}
</div>
{pagedTokens.length === 0 ? (
<div class="px-4 py-10 text-center text-sm text-[color:var(--app-ink-muted)]">
<div class="app-list-empty px-4 py-10 text-center text-sm text-[color:var(--app-ink-muted)]">
<p>暂无令牌,请先创建。</p>
<Button
class="mt-4 h-9 px-4 text-xs"
@@ -328,7 +328,7 @@ export const TokensView = ({
</Button>
</div>
) : (
<div class="divide-y divide-white/60">
<div class="app-list-body divide-y divide-white/60">
{pagedTokens.map((tokenItem) => {
const isActive = tokenItem.status === "active";
const revealPending = isActionPending(
@@ -342,7 +342,7 @@ export const TokensView = ({
);
return (
<div
class="grid items-center gap-3 px-4 py-4 text-sm"
class="app-list-row grid items-center gap-3 px-4 py-4 text-sm"
key={tokenItem.id}
style={`grid-template-columns: ${tokenGridTemplate};`}
>
@@ -443,7 +443,7 @@ export const TokensView = ({
)}
</div>
</div>
<div class="flex flex-col gap-3 text-xs text-[color:var(--app-ink-muted)] sm:flex-row sm:items-center sm:justify-between">
<div class="app-pagination-bar flex flex-col gap-3 text-xs text-[color:var(--app-ink-muted)] sm:flex-row sm:items-center sm:justify-between">
<div class="flex flex-wrap items-center gap-2">
<span class="text-xs text-[color:var(--app-ink-muted)]">
共 {tokenTotal} 条 · {displayPages} 页
@@ -455,26 +455,23 @@ export const TokensView = ({
onPageChange={onPageChange}
/>
</div>
<label class="app-page-size" for="token-page-size">
<span>每页条数</span>
<Select
variant="pill"
class="w-auto text-xs app-page-size__select"
id="token-page-size"
value={String(tokenPageSize)}
onChange={(event) => {
onPageSizeChange(
Number((event.currentTarget as HTMLSelectElement).value),
);
}}
>
<div class="app-page-size-control">
<span class="app-page-size-control__label">每页</span>
<div class="app-page-size-control__chips">
{pageSizeOptions.map((size) => (
<option key={size} value={String(size)}>
<button
class={`app-page-size-chip ${
tokenPageSize === size ? "app-page-size-chip--active" : ""
}`}
key={size}
type="button"
onClick={() => onPageSizeChange(size)}
>
{size}
</option>
</button>
))}
</Select>
</label>
</div>
</div>
</div>
</div>
{isTokenModalOpen && (
+63 -44
View File
@@ -12,7 +12,6 @@ import {
Input,
MultiSelect,
Pagination,
Select,
Skeleton,
Tooltip,
} from "../components/ui";
@@ -203,7 +202,7 @@ export const UsageView = ({
return (
<div class="space-y-5">
<div class="animate-fade-up space-y-4">
<div class="app-panel animate-fade-up space-y-4">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<h3 class="app-title text-lg">使用日志</h3>
@@ -221,7 +220,10 @@ export const UsageView = ({
</Button>
</div>
</div>
<Card variant="compact" class="app-layer-raised space-y-3 p-4">
<Card
variant="compact"
class="app-layer-raised app-toolbar-card space-y-3 p-4"
>
<div class="flex flex-wrap items-center justify-between gap-3">
<div class="text-xs font-semibold uppercase tracking-widest text-[color:var(--app-ink-muted)]">
筛选模块
@@ -270,9 +272,9 @@ export const UsageView = ({
/>
</div>
<div>
<label class="mb-1.5 block text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]">
<p class="mb-1.5 block text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]">
渠道
</label>
</p>
<MultiSelect
class="w-full"
options={channelOptions}
@@ -286,9 +288,9 @@ export const UsageView = ({
</div>
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
<div>
<label class="mb-1.5 block text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]">
<p class="mb-1.5 block text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]">
令牌
</label>
</p>
<MultiSelect
class="w-full"
options={tokenOptions}
@@ -300,9 +302,9 @@ export const UsageView = ({
/>
</div>
<div>
<label class="mb-1.5 block text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]">
<p class="mb-1.5 block text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]">
模型
</label>
</p>
<MultiSelect
class="w-full"
options={modelOptions}
@@ -314,9 +316,9 @@ export const UsageView = ({
/>
</div>
<div>
<label class="mb-1.5 block text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]">
<p class="mb-1.5 block text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]">
状态
</label>
</p>
<MultiSelect
class="w-full"
options={statusOptions}
@@ -351,51 +353,69 @@ export const UsageView = ({
</div>
</div>
</Card>
<div class="app-surface overflow-hidden">
<div class="app-surface app-data-shell overflow-hidden">
<div class="h-[360px] overflow-auto sm:h-[440px]">
<table class="app-table min-w-[960px] w-full text-xs sm:text-sm">
<thead>
<tr>
{visibleColumnSet.has("time") && (
<th class="sticky top-0 bg-slate-100/90">时间</th>
<th class="sticky top-0 bg-[color:var(--app-surface-strong)]/95">
时间
</th>
)}
{visibleColumnSet.has("model") && (
<th class="sticky top-0 bg-slate-100/90">模型</th>
<th class="sticky top-0 bg-[color:var(--app-surface-strong)]/95">
模型
</th>
)}
{visibleColumnSet.has("channel") && (
<th class="sticky top-0 bg-slate-100/90">渠道</th>
<th class="sticky top-0 bg-[color:var(--app-surface-strong)]/95">
渠道
</th>
)}
{visibleColumnSet.has("token") && (
<th class="sticky top-0 bg-slate-100/90">令牌</th>
<th class="sticky top-0 bg-[color:var(--app-surface-strong)]/95">
令牌
</th>
)}
{visibleColumnSet.has("prompt_tokens") && (
<th class="sticky top-0 bg-slate-100/90">输入 Tokens</th>
<th class="sticky top-0 bg-[color:var(--app-surface-strong)]/95">
输入 Tokens
</th>
)}
{visibleColumnSet.has("completion_tokens") && (
<th class="sticky top-0 bg-slate-100/90">输出 Tokens</th>
<th class="sticky top-0 bg-[color:var(--app-surface-strong)]/95">
输出 Tokens
</th>
)}
{visibleColumnSet.has("latency") && (
<th class="sticky top-0 bg-slate-100/90">用时 (s)</th>
<th class="sticky top-0 bg-[color:var(--app-surface-strong)]/95">
用时 (s)
</th>
)}
{visibleColumnSet.has("first_token") && (
<th class="sticky top-0 bg-slate-100/90">
<th class="sticky top-0 bg-[color:var(--app-surface-strong)]/95">
<Tooltip content="首个 token 返回的等待时间。">
<span>首 token 延迟 (s)</span>
</Tooltip>
</th>
)}
{visibleColumnSet.has("stream") && (
<th class="sticky top-0 bg-slate-100/90">流式</th>
<th class="sticky top-0 bg-[color:var(--app-surface-strong)]/95">
流式
</th>
)}
{visibleColumnSet.has("reasoning") && (
<th class="sticky top-0 bg-slate-100/90">
<th class="sticky top-0 bg-[color:var(--app-surface-strong)]/95">
<Tooltip content="模型推理强度等级。">
<span>推理强度</span>
</Tooltip>
</th>
)}
{visibleColumnSet.has("status") && (
<th class="sticky top-0 bg-slate-100/90">状态码</th>
<th class="sticky top-0 bg-[color:var(--app-surface-strong)]/95">
状态码
</th>
)}
</tr>
</thead>
@@ -489,7 +509,7 @@ export const UsageView = ({
</td>
)}
{visibleColumnSet.has("status") && (
<td class="px-3 py-2.5 text-left text-xs text-[color:var(--app-ink)] sm:text-sm">
<td class="px-3 py-2.5 text-left text-xs text-[color:var(--app-ink)] sm:text-sm">
<button
class="app-focus inline-flex items-center border-0 bg-transparent p-0"
type="button"
@@ -498,7 +518,9 @@ export const UsageView = ({
<Chip
class="text-[10px]"
variant={
log.upstream_status === 200 ? "success" : "danger"
log.upstream_status === 200
? "success"
: "danger"
}
>
{statusDetail.label}
@@ -514,7 +536,7 @@ export const UsageView = ({
</table>
</div>
</div>
<div class="flex flex-col gap-3 text-xs text-[color:var(--app-ink-muted)] sm:flex-row sm:items-center sm:justify-between">
<div class="app-pagination-bar flex flex-col gap-3 text-xs text-[color:var(--app-ink-muted)] sm:flex-row sm:items-center sm:justify-between">
<div class="flex flex-wrap items-center gap-2">
<span class="text-xs text-[color:var(--app-ink-muted)]">
共 {displayPages} 页 · {total} 条
@@ -527,27 +549,24 @@ export const UsageView = ({
disabled={isRefreshing}
/>
</div>
<label class="app-page-size" for="usage-page-size">
<span>每页条数</span>
<Select
variant="pill"
class="w-auto text-xs app-page-size__select"
id="usage-page-size"
value={String(pageSize)}
disabled={isRefreshing}
onChange={(event) =>
onPageSizeChange(
Number((event.currentTarget as HTMLSelectElement).value),
)
}
>
<div class="app-page-size-control">
<span class="app-page-size-control__label">每页</span>
<div class="app-page-size-control__chips">
{pageSizeOptions.map((size) => (
<option key={size} value={String(size)}>
<button
class={`app-page-size-chip ${
pageSize === size ? "app-page-size-chip--active" : ""
}`}
key={size}
type="button"
disabled={isRefreshing}
onClick={() => onPageSizeChange(size)}
>
{size}
</option>
</button>
))}
</Select>
</label>
</div>
</div>
</div>
</div>
{activeErrorLog ? (
+1187 -116
View File
File diff suppressed because it is too large. Load diff
-1
View File
@@ -28,4 +28,3 @@ export type AppEnv = {
Bindings: Bindings;
Variables: Variables;
};
+5 -6
View File
@@ -1,11 +1,11 @@
import { createMiddleware } from "hono/factory";
import type { AppEnv } from "../env";
import { getCacheConfig } from "../services/settings";
import { canConsumeQuota, normalizeQuota } from "../services/quota";
import { getCacheConfig } from "../services/settings";
import { withJsonCache } from "../utils/cache";
import { sha256Hex } from "../utils/crypto";
import { jsonError } from "../utils/http";
import { getBearerToken } from "../utils/request";
import { withJsonCache } from "../utils/cache";
export type TokenRecord = {
id: string;
@@ -37,10 +37,9 @@ export const tokenAuth = createMiddleware<AppEnv>(async (c, next) => {
enabled: cacheConfig.enabled,
},
async () => {
const row = await c.env.DB
.prepare(
"SELECT id, name, quota_total, quota_used, status, allowed_channels, expires_at FROM tokens WHERE key_hash = ?",
)
const row = await c.env.DB.prepare(
"SELECT id, name, quota_total, quota_used, status, allowed_channels, expires_at FROM tokens WHERE key_hash = ?",
)
.bind(tokenHash)
.first<TokenRecord>();
return row ?? null;
+4 -1
View File
@@ -37,7 +37,10 @@ models.get("/", async (c) => {
>();
for (const entry of entries) {
const existing = map.get(entry.id) ?? { id: entry.id, channels: [] };
existing.channels.push({ id: entry.channelId, name: entry.channelName });
existing.channels.push({
id: entry.channelId,
name: entry.channelName,
});
map.set(entry.id, existing);
}
+1 -1
View File
@@ -20,7 +20,6 @@ import {
fetchChannelModels,
updateChannelTestResult,
} from "../services/channel-testing";
import { bumpCacheVersions } from "../services/settings";
import {
mergeMetadata,
modelsToJson,
@@ -29,6 +28,7 @@ import {
toNewApiChannel,
withNewApiDefaults,
} from "../services/newapi";
import { bumpCacheVersions } from "../services/settings";
import { generateToken } from "../utils/crypto";
import { safeJsonParse } from "../utils/json";
import { newApiFailure, newApiSuccess } from "../utils/newapi-response";
+1 -3
View File
@@ -1,9 +1,7 @@
import { Hono } from "hono";
import type { AppEnv } from "../env";
import { newApiAuth } from "../middleware/newApiAuth";
import {
listModelsByChannelWithFallback,
} from "../services/channel-model-capabilities";
import { listModelsByChannelWithFallback } from "../services/channel-model-capabilities";
import { listActiveChannels } from "../services/channel-repo";
import { newApiSuccess } from "../utils/newapi-response";
+53 -48
View File
@@ -1,9 +1,7 @@
import { Hono } from "hono";
import type { AppEnv } from "../env";
import { type TokenRecord, tokenAuth } from "../middleware/tokenAuth";
import {
type CallTokenItem,
} from "../services/call-token-selector";
import type { CallTokenItem } from "../services/call-token-selector";
import { listCallTokens } from "../services/channel-call-token-repo";
import {
type ChannelMetadata,
@@ -11,20 +9,16 @@ import {
resolveMappedModel,
resolveProvider,
} from "../services/channel-metadata";
import {
type ChannelRecord,
createWeightedOrder,
extractModels,
} from "../services/channels";
import {
listCoolingDownChannelsForModel,
listVerifiedModelsByChannel,
} from "../services/channel-model-capabilities";
import {
getCacheConfig,
getModelFailureCooldownMinutes,
getProxyRuntimeSettings,
} from "../services/settings";
type ChannelRecord,
createWeightedOrder,
extractModels,
} from "../services/channels";
import { adaptChatResponse } from "../services/chat-response-adapter";
import {
applyGeminiModelToPath,
buildUpstreamChatRequest,
@@ -42,16 +36,24 @@ import {
parseDownstreamModel,
parseDownstreamStream,
} from "../services/provider-transform";
import { processUsageQueueEvent, type UsageQueueEvent } from "../services/usage-queue";
import {
getCacheConfig,
getModelFailureCooldownMinutes,
getProxyRuntimeSettings,
} from "../services/settings";
import {
getUsageLimiterStub,
reserveUsageQueue,
} from "../services/usage-limiter";
import {
processUsageQueueEvent,
type UsageQueueEvent,
} from "../services/usage-queue";
import { withJsonCache } from "../utils/cache";
import { jsonError } from "../utils/http";
import { safeJsonParse } from "../utils/json";
import { extractReasoningEffort } from "../utils/reasoning";
import { normalizeBaseUrl } from "../utils/url";
import { withJsonCache } from "../utils/cache";
import {
type NormalizedUsage,
parseUsageFromHeaders,
@@ -60,7 +62,6 @@ import {
type StreamUsageMode,
type StreamUsageOptions,
} from "../utils/usage";
import { adaptChatResponse } from "../services/chat-response-adapter";
const proxy = new Hono<AppEnv>();
@@ -100,12 +101,10 @@ function normalizeMessage(value: string | null): string | null {
return trimmed;
}
function getStreamUsageOptions(
settings: {
stream_usage_mode: string;
stream_usage_max_bytes: number;
},
): StreamUsageOptions {
function getStreamUsageOptions(settings: {
stream_usage_mode: string;
stream_usage_max_bytes: number;
}): StreamUsageOptions {
return {
mode: settings.stream_usage_mode as StreamUsageMode,
maxBytes: Math.max(0, Math.floor(settings.stream_usage_max_bytes)),
@@ -173,7 +172,7 @@ function createUsageEventScheduler(
const decideSequential = () => {
const decision = decisionChain.then(decide);
decisionChain = decision.catch(() => undefined);
decisionChain = decision.then(() => undefined).catch(() => undefined);
return decision;
};
@@ -198,7 +197,10 @@ async function extractErrorDetails(
): Promise<{ errorCode: string | null; errorMessage: string | null }> {
const contentType = response.headers.get("content-type") ?? "";
if (contentType.includes("application/json")) {
const payload = await response.clone().json().catch(() => null);
const payload = await response
.clone()
.json()
.catch(() => null);
if (payload && typeof payload === "object") {
const raw = payload as Record<string, unknown>;
const error = (raw.error ?? raw) as Record<string, unknown>;
@@ -220,7 +222,10 @@ async function extractErrorDetails(
};
}
}
const text = await response.clone().text().catch(() => "");
const text = await response
.clone()
.text()
.catch(() => "");
return { errorCode: null, errorMessage: normalizeMessage(text) };
}
@@ -560,8 +565,9 @@ proxy.all("/*", tokenAuth, async (c) => {
enabled: cacheConfig.enabled,
},
async () => {
const result = await c.env.DB
.prepare("SELECT * FROM channels WHERE status = ?")
const result = await c.env.DB.prepare(
"SELECT * FROM channels WHERE status = ?",
)
.bind("active")
.all();
return (result.results ?? []) as ChannelRecord[];
@@ -631,12 +637,7 @@ proxy.all("/*", tokenAuth, async (c) => {
code: "upstream_cooldown",
message: "upstream_cooldown",
});
return jsonError(
c,
503,
"upstream_cooldown",
"upstream_cooldown",
);
return jsonError(c, 503, "upstream_cooldown", "upstream_cooldown");
}
}
}
@@ -782,10 +783,7 @@ proxy.all("/*", tokenAuth, async (c) => {
}
}
}
if (
endpointType === "images" &&
metadata.endpoint_overrides.image_url
) {
if (endpointType === "images" && metadata.endpoint_overrides.image_url) {
if (normalizedImage) {
const request = buildUpstreamImageRequest(
upstreamProvider,
@@ -989,7 +987,8 @@ proxy.all("/*", tokenAuth, async (c) => {
} catch (error) {
const isTimeout =
error instanceof Error &&
(error.name === "AbortError" || error.message.includes("upstream_timeout"));
(error.name === "AbortError" ||
error.message.includes("upstream_timeout"));
console.error("[proxy:upstream_exception]", {
channel_id: channel.id,
upstream_provider: upstreamProvider,
@@ -1085,11 +1084,11 @@ proxy.all("/*", tokenAuth, async (c) => {
const errorDetails =
lastResponse.ok || selectedChannel
? null
: lastErrorDetails ?? {
: (lastErrorDetails ?? {
upstreamStatus: lastResponse.status,
errorCode: null,
errorMessage: null,
};
});
const record = async (
usage: NormalizedUsage | null,
firstTokenLatencyMs?: number | null,
@@ -1105,14 +1104,14 @@ proxy.all("/*", tokenAuth, async (c) => {
const usageMissing = !usage;
const errorCode = lastResponse.ok
? usageMissing
? usageMissingReason ?? "usage_missing"
? (usageMissingReason ?? "usage_missing")
: null
: errorDetails?.errorCode ?? null;
: (errorDetails?.errorCode ?? null);
const errorMessage = lastResponse.ok
? usageMissing
? usageMissingReason ?? "usage_missing"
? (usageMissingReason ?? "usage_missing")
: null
: errorDetails?.errorMessage ?? null;
: (errorDetails?.errorMessage ?? null);
scheduleUsageEvent({
type: "usage",
payload: {
@@ -1131,7 +1130,7 @@ proxy.all("/*", tokenAuth, async (c) => {
status: lastResponse.ok ? "ok" : "error",
upstreamStatus: lastResponse.ok
? lastResponse.status
: errorDetails?.upstreamStatus ?? lastResponse.status,
: (errorDetails?.upstreamStatus ?? lastResponse.status),
errorCode,
errorMessage,
},
@@ -1167,13 +1166,15 @@ proxy.all("/*", tokenAuth, async (c) => {
const task = parseUsageFromSse(lastResponse.clone(), streamUsageOptions)
.then((streamUsage) => {
const usageValue = immediateUsage ?? streamUsage.usage;
const reason =
usageValue ? null : "usage_missing";
const reason = usageValue ? null : "usage_missing";
return record(usageValue, streamUsage.firstTokenLatencyMs, reason);
})
.catch(() => undefined)
.finally(() => {
activeStreamUsageParsers = Math.max(0, activeStreamUsageParsers - 1);
activeStreamUsageParsers = Math.max(
0,
activeStreamUsageParsers - 1,
);
});
if (executionCtx?.waitUntil) {
executionCtx.waitUntil(task);
@@ -1182,7 +1183,11 @@ proxy.all("/*", tokenAuth, async (c) => {
}
}
} else {
await record(immediateUsage, latencyMs, immediateUsage ? null : "usage_missing");
await record(
immediateUsage,
latencyMs,
immediateUsage ? null : "usage_missing",
);
}
}
+5 -5
View File
@@ -8,19 +8,19 @@ import {
bumpCacheVersions,
getCacheConfig,
getCheckinScheduleTime,
getRetentionDays,
getSessionTtlHours,
getModelFailureCooldownMinutes,
getProxyRuntimeSettings,
getRetentionDays,
getRuntimeProxyConfig,
getSessionTtlHours,
isAdminPasswordSet,
setCacheConfig,
setAdminPasswordHash,
setCacheConfig,
setCheckinScheduleTime,
setRetentionDays,
setSessionTtlHours,
setModelFailureCooldownMinutes,
setProxyRuntimeSettings,
setRetentionDays,
setSessionTtlHours,
} from "../services/settings";
import {
getUsageLimiterStub,
+1 -1
View File
@@ -12,12 +12,12 @@ import {
updateChannel,
} from "../services/channel-repo";
import { runCheckinAll, runCheckinSingle } from "../services/checkin-runner";
import { bumpCacheVersions } from "../services/settings";
import {
buildSiteMetadata,
parseSiteMetadata,
type SiteType,
} from "../services/site-metadata";
import { bumpCacheVersions } from "../services/settings";
import { generateToken } from "../utils/crypto";
import { jsonError } from "../utils/http";
import { nowIso } from "../utils/time";
+1 -3
View File
@@ -20,9 +20,7 @@ type TokenRow = {
expires_at?: string | null;
};
const normalizeAllowedChannels = (
raw: string | null,
): string[] | null => {
const normalizeAllowedChannels = (raw: string | null): string[] | null => {
if (!raw) {
return null;
}
+3 -1
View File
@@ -139,7 +139,9 @@ usage.get("/", async (c) => {
}
const rawLimit = Number(query.limit ?? 50);
const normalizedLimit = Number.isNaN(rawLimit) ? 50 : Math.floor(rawLimit);
const normalizedLimit = Number.isNaN(rawLimit)
? 50
: Math.floor(rawLimit);
const limit = Math.min(Math.max(normalizedLimit, 1), 200);
const rawOffset = Number(query.offset ?? 0);
const normalizedOffset = Number.isNaN(rawOffset)
@@ -1,7 +1,7 @@
import type { D1Database } from "@cloudflare/workers-types";
import { nowIso } from "../utils/time";
import { extractModelIds } from "./channel-models";
import type { ModelEntry } from "./channel-models";
import { extractModelIds } from "./channel-models";
export type CapabilityRow = {
channel_id: string;
@@ -51,7 +51,9 @@ export async function listVerifiedModelEntries(
channels: Array<{ id: string; name: string }>,
): Promise<ModelEntry[]> {
const ids = channels.map((channel) => channel.id);
const nameMap = new Map(channels.map((channel) => [channel.id, channel.name]));
const nameMap = new Map(
channels.map((channel) => [channel.id, channel.name]),
);
const map = await listVerifiedModelsByChannel(db, ids);
const entries: ModelEntry[] = [];
for (const [channelId, models] of map.entries()) {
@@ -96,7 +98,12 @@ export async function listModelEntriesWithFallback(
continue;
}
for (const id of models) {
entries.push({ id, label: id, channelId: channel.id, channelName: channel.name });
entries.push({
id,
label: id,
channelId: channel.id,
channelName: channel.name,
});
}
}
return entries;
@@ -153,14 +160,7 @@ export async function recordChannelModelError(
.prepare(
"INSERT INTO channel_model_capabilities (channel_id, model, last_ok_at, last_err_at, last_err_code, last_err_count, created_at, updated_at) VALUES (?, ?, 0, ?, ?, 1, ?, ?) ON CONFLICT(channel_id, model) DO UPDATE SET last_err_at = excluded.last_err_at, last_err_code = excluded.last_err_code, last_err_count = COALESCE(channel_model_capabilities.last_err_count, 0) + 1, updated_at = excluded.updated_at",
)
.bind(
channelId,
model,
nowSeconds,
errorCode,
timestamp,
timestamp,
)
.bind(channelId, model, nowSeconds, errorCode, timestamp, timestamp)
.run();
}
+1 -1
View File
@@ -1,8 +1,8 @@
import type { D1Database } from "@cloudflare/workers-types";
import { nowIso } from "../utils/time";
import { normalizeBaseUrl } from "../utils/url";
import { modelsToJson, normalizeModelsInput } from "./channel-models";
import { upsertChannelModelCapabilities } from "./channel-model-capabilities";
import { modelsToJson, normalizeModelsInput } from "./channel-models";
export type ChannelTestResult = {
ok: boolean;
@@ -260,7 +260,9 @@ function writeGeminiChunk(
controller.enqueue(encoder.encode(`${JSON.stringify(data)}\n`));
}
async function adaptOpenAiJsonToAnthropic(options: AdaptOptions): Promise<Response> {
async function adaptOpenAiJsonToAnthropic(
options: AdaptOptions,
): Promise<Response> {
const payload = (await options.response
.clone()
.json()
@@ -283,7 +285,9 @@ async function adaptOpenAiJsonToAnthropic(options: AdaptOptions): Promise<Respon
: {};
const promptTokens = Number(usage.prompt_tokens ?? 0) || 0;
const completionTokens = Number(usage.completion_tokens ?? 0) || 0;
const stopReason = mapOpenAiFinishReasonToAnthropic(firstChoice.finish_reason);
const stopReason = mapOpenAiFinishReasonToAnthropic(
firstChoice.finish_reason,
);
const text = openAiContentToText(message.content);
const transformed = {
id:
@@ -460,7 +464,9 @@ function adaptOpenAiSseToAnthropic(options: AdaptOptions): Response {
});
}
async function adaptAnthropicJsonToOpenAi(options: AdaptOptions): Promise<Response> {
async function adaptAnthropicJsonToOpenAi(
options: AdaptOptions,
): Promise<Response> {
const payload = (await options.response
.clone()
.json()
@@ -552,7 +558,8 @@ function adaptAnthropicSseToOpenAi(options: AdaptOptions): Response {
newlineIndex = buffer.indexOf("\n");
continue;
}
const eventType = typeof parsed.type === "string" ? parsed.type : "";
const eventType =
typeof parsed.type === "string" ? parsed.type : "";
if (!started && eventType === "message_start") {
started = true;
writeOpenAiSseChunk(controller, encoder, {
@@ -642,7 +649,9 @@ function adaptAnthropicSseToOpenAi(options: AdaptOptions): Response {
});
}
async function adaptGeminiJsonToOpenAi(options: AdaptOptions): Promise<Response> {
async function adaptGeminiJsonToOpenAi(
options: AdaptOptions,
): Promise<Response> {
const payload = (await options.response
.clone()
.json()
@@ -651,7 +660,9 @@ async function adaptGeminiJsonToOpenAi(options: AdaptOptions): Promise<Response>
return options.response;
}
const text = geminiCandidateText(payload);
const finishReason = mapGeminiFinishReasonToOpenAi(geminiFinishReason(payload));
const finishReason = mapGeminiFinishReasonToOpenAi(
geminiFinishReason(payload),
);
const usage = geminiUsageTokens(payload);
const transformed = {
id: `chatcmpl_${Date.now()}`,
@@ -680,7 +691,9 @@ async function adaptGeminiJsonToOpenAi(options: AdaptOptions): Promise<Response>
});
}
async function adaptGeminiJsonToAnthropic(options: AdaptOptions): Promise<Response> {
async function adaptGeminiJsonToAnthropic(
options: AdaptOptions,
): Promise<Response> {
const payload = (await options.response
.clone()
.json()
@@ -712,7 +725,9 @@ async function adaptGeminiJsonToAnthropic(options: AdaptOptions): Promise<Respon
});
}
async function adaptOpenAiJsonToGemini(options: AdaptOptions): Promise<Response> {
async function adaptOpenAiJsonToGemini(
options: AdaptOptions,
): Promise<Response> {
const payload = (await options.response
.clone()
.json()
@@ -757,7 +772,9 @@ async function adaptOpenAiJsonToGemini(options: AdaptOptions): Promise<Response>
});
}
async function adaptAnthropicJsonToGemini(options: AdaptOptions): Promise<Response> {
async function adaptAnthropicJsonToGemini(
options: AdaptOptions,
): Promise<Response> {
const payload = (await options.response
.clone()
.json()
@@ -833,7 +850,11 @@ function adaptGeminiSseToOpenAi(options: AdaptOptions): Response {
created,
model: options.model ?? "",
choices: [
{ index: 0, delta: { role: "assistant" }, finish_reason: null },
{
index: 0,
delta: { role: "assistant" },
finish_reason: null,
},
],
});
}
@@ -952,7 +973,9 @@ function adaptGeminiSseToAnthropic(options: AdaptOptions): Response {
writeSseEvent(controller, encoder, "message_delta", {
type: "message_delta",
delta: { stop_reason: stopReason, stop_sequence: null },
usage: { output_tokens: geminiUsageTokens(parsed).completionTokens },
usage: {
output_tokens: geminiUsageTokens(parsed).completionTokens,
},
});
writeSseEvent(controller, encoder, "message_stop", {
type: "message_stop",
@@ -1051,7 +1074,9 @@ function adaptOpenAiSseToGemini(options: AdaptOptions): Response {
}
if (!lastFinishReason) {
writeGeminiChunk(controller, encoder, {
candidates: [{ content: { role: "model", parts: [] }, finishReason: "STOP" }],
candidates: [
{ content: { role: "model", parts: [] }, finishReason: "STOP" },
],
});
}
controller.close();
@@ -1096,7 +1121,8 @@ function adaptAnthropicSseToGemini(options: AdaptOptions): Response {
newlineIndex = buffer.indexOf("\n");
continue;
}
const eventType = typeof parsed.type === "string" ? parsed.type : "";
const eventType =
typeof parsed.type === "string" ? parsed.type : "";
if (eventType === "content_block_delta") {
const delta =
parsed.delta && typeof parsed.delta === "object"
@@ -1119,7 +1145,8 @@ function adaptAnthropicSseToGemini(options: AdaptOptions): Response {
? (parsed.delta as Record<string, unknown>)
: {};
lastFinishReason =
mapAnthropicStopReasonToGemini(delta.stop_reason) ?? lastFinishReason;
mapAnthropicStopReasonToGemini(delta.stop_reason) ??
lastFinishReason;
}
newlineIndex = buffer.indexOf("\n");
}
@@ -1185,7 +1212,9 @@ const adapters: Record<string, AdapterFn> = {
},
};
export async function adaptChatResponse(options: AdaptOptions): Promise<Response> {
export async function adaptChatResponse(
options: AdaptOptions,
): Promise<Response> {
if (options.upstreamProvider === options.downstreamProvider) {
return options.response;
}
+5 -4
View File
@@ -185,7 +185,10 @@ function parsePositiveNumber(value: string | null, fallback: number): number {
return fallback;
}
function parseNonNegativeSetting(value: string | null, fallback: number): number {
function parseNonNegativeSetting(
value: string | null,
fallback: number,
): number {
if (!value) {
return fallback;
}
@@ -259,9 +262,7 @@ function clearCacheConfigSnapshot(): void {
cacheConfigSnapshot = null;
}
function setSettingsCacheControlSnapshot(
value: SettingsCacheControl,
): void {
function setSettingsCacheControlSnapshot(value: SettingsCacheControl): void {
settingsCacheControlSnapshot = {
value,
expiresAt: Date.now() + CACHE_CONFIG_TTL_MS,
+2 -4
View File
@@ -77,8 +77,7 @@ export class UsageLimiter {
return new Response("Invalid payload", { status: 400 });
}
const nowDate = beijingDateString(new Date());
let storedDate =
(await this.state.storage.get<string>(DATE_KEY)) ?? null;
let storedDate = (await this.state.storage.get<string>(DATE_KEY)) ?? null;
let count = (await this.state.storage.get<number>(COUNT_KEY)) ?? 0;
if (storedDate !== nowDate) {
storedDate = nowDate;
@@ -106,8 +105,7 @@ export class UsageLimiter {
}
if (request.method === "GET" && url.pathname === "/status") {
const nowDate = beijingDateString(new Date());
let storedDate =
(await this.state.storage.get<string>(DATE_KEY)) ?? null;
let storedDate = (await this.state.storage.get<string>(DATE_KEY)) ?? null;
let count = (await this.state.storage.get<number>(COUNT_KEY)) ?? 0;
if (storedDate !== nowDate) {
storedDate = nowDate;
+4 -1
View File
@@ -1,6 +1,9 @@
import type { D1Database, ExecutionContext } from "@cloudflare/workers-types";
import type { Bindings } from "../env";
import { recordChannelModelError, upsertChannelModelCapabilities } from "./channel-model-capabilities";
import {
recordChannelModelError,
upsertChannelModelCapabilities,
} from "./channel-model-capabilities";
import type { UsageInput } from "./usage";
import { recordUsage } from "./usage";
+1 -5
View File
@@ -18,11 +18,7 @@ export type StreamUsageOptions = {
maxBytes?: number;
};
const USAGE_HINTS = [
"\"usage\"",
"\"usageMetadata\"",
"\"usage_metadata\"",
];
const USAGE_HINTS = ['"usage"', '"usageMetadata"', '"usage_metadata"'];
function toNumber(value: unknown): number | null {
if (value === null || value === undefined) {
+19 -7
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env node
import { spawn } from "node:child_process";
import { access } from "node:fs/promises";
import { existsSync } from "node:fs";
import { access } from "node:fs/promises";
import path from "node:path";
import { createInterface } from "node:readline/promises";
import { fileURLToPath } from "node:url";
@@ -13,7 +13,7 @@ const BUN_CMD = (() => {
return process.env.BUN_BIN;
}
const npmExec = process.env.npm_execpath;
if (npmExec && npmExec.toLowerCase().includes("bun")) {
if (npmExec?.toLowerCase().includes("bun")) {
return npmExec;
}
if (process.env.BUN_INSTALL) {
@@ -77,8 +77,12 @@ const parseArgs = () => {
const printHelp = () => {
console.log("Usage:");
console.log(" node scripts/deploy.mjs init [--target both] [--migrate true]");
console.log(" node scripts/deploy.mjs update [--target auto] [--migrate auto]");
console.log(
" node scripts/deploy.mjs init [--target both] [--migrate true]",
);
console.log(
" node scripts/deploy.mjs update [--target auto] [--migrate auto]",
);
console.log("");
console.log("Options:");
console.log(" --action init|update 部署动作(可用位置参数替代)");
@@ -87,7 +91,9 @@ const printHelp = () => {
console.log(" --config <path> wrangler.toml 路径");
console.log(" --skip-install 跳过 bun install");
console.log("");
console.log("说明: 本脚本仅执行本地流程(构建 + 本地迁移),不会触发远程部署。");
console.log(
"说明: 本脚本仅执行本地流程(构建 + 本地迁移),不会触发远程部署。",
);
};
const run = (command, args, options = {}) =>
@@ -161,14 +167,20 @@ const promptChoice = async (rl, label, choices, defaultValue) => {
const choicesText = choices.join("/");
const defaultText = defaultValue ? ` [${defaultValue}]` : "";
while (true) {
const answer = (await rl.question(`${label} (${choicesText})${defaultText}: `))
const answer = (
await rl.question(`${label} (${choicesText})${defaultText}: `)
)
.trim()
.toLowerCase();
if (!answer && defaultValue) {
return defaultValue;
}
const numeric = Number(answer);
if (Number.isInteger(numeric) && numeric >= 1 && numeric <= choices.length) {
if (
Number.isInteger(numeric) &&
numeric >= 1 &&
numeric <= choices.length
) {
return choices[numeric - 1];
}
if (choices.includes(answer)) {
+1 -1
View File
@@ -8,7 +8,7 @@ const BUN_CMD = (() => {
return process.env.BUN_BIN;
}
const npmExec = process.env.npm_execpath;
if (npmExec && npmExec.toLowerCase().includes("bun")) {
if (npmExec?.toLowerCase().includes("bun")) {
return npmExec;
}
if (process.env.BUN_INSTALL) {