fix:优化控制面板ui
This commit is contained in:
1 parent
54baba28a3
commit
dc74a5087a
11 files changed
+731
-633
No files matched your search
@@ -9,6 +9,12 @@
|
||||
|
||||
### 变更
|
||||
|
||||
- **[settings/admin-ui]**: 设置页分区重排并内联队列状态与使用数量 — by lsy
|
||||
- 方案: [202603181300_settings-panel-sections](archive/2026-03/202603181300_settings-panel-sections/)
|
||||
- 决策: settings-panel-sections#D001(只读项内联展示)
|
||||
- **[settings/admin-ui]**: 运行时设置改为仅数据库生效,移除环境变量回退 — by lsy
|
||||
- 方案: [202603181221_settings-db-only](archive/2026-03/202603181221_settings-db-only/)
|
||||
- 决策: settings-db-only#D001(停用环境变量回退)
|
||||
- **[settings/proxy/usage/admin-ui]**: 运行时设置可在后台配置,新增队列日限额与直写比例并接入 UsageLimiter — by lsy
|
||||
- 方案: [202603180031_usage-queue-simplified](archive/2026-03/202603180031_usage-queue-simplified/)
|
||||
- **[cache/settings/proxy/usage/admin-ui]**: 引入分组缓存与自动失效,面板/日志/模型短期缓存可配置,清理节流并补齐索引 — by lsy
|
||||
@@ -221,6 +227,12 @@
|
||||
|
||||
### 快速修改
|
||||
|
||||
- **[admin-ui/settings]**: 缓存 TTL 与版本成对展示为卡片 — by lsy
|
||||
- 类型: 快速修改(无方案包)
|
||||
- 文件: apps/ui/src/features/SettingsView.tsx:417-632
|
||||
- **[admin-ui/settings]**: 缓存版本调整为小标题展示 — by lsy
|
||||
- 类型: 快速修改(无方案包)
|
||||
- 文件: apps/ui/src/features/SettingsView.tsx:566-584
|
||||
- **[ci]**: 队列创建失败时识别“名称已占用(11009)”并视为已存在 — by lsy
|
||||
- 类型: 快速修改(无方案包)
|
||||
- 文件: .github/workflows/deploy.yml:263-320,416-473
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
- 系统设置增加定时签到开关与时间(中国时间)
|
||||
- 系统设置提供模型失败冷却分钟数配置
|
||||
- 系统设置提供运行时配置可编辑(上游超时、流式解析模式/上限、队列开关/日限额/直写比例)
|
||||
- 系统设置展示运行时配置(只读),以卡片显示名称 + 环境变量名 + 当前值,并提示设置优先生效
|
||||
- 系统设置将运行时配置拆分为“解析与请求 / 队列设置”,队列绑定/实际生效/使用数量以内联只读项展示
|
||||
- 系统设置提供缓存开关与 TTL(面板/日志/模型/鉴权/渠道/调用令牌/设置)配置
|
||||
- 系统设置展示缓存版本号并提供刷新入口
|
||||
- 列表分页页脚统一使用页码导航 + 每页条数选择,并显示总页数(无数据时显示 0 页)
|
||||
|
||||
@@ -12,15 +12,16 @@
|
||||
|
||||
## 行为规范
|
||||
- 配置写入后即时生效
|
||||
- 未配置时使用环境变量/默认值回退
|
||||
- 未配置时使用默认值回退
|
||||
- 定时签到时间使用 `HH:mm` 格式(默认 `00:10`,中国时间)
|
||||
- 更新签到时间后会触发 `CheckinScheduler` 重新调度
|
||||
- 若更新后当前时间已超过新签到时间,当日可再次触发自动签到
|
||||
- 模型失败冷却以分钟计(默认 10 分钟),用于跳过最近失败的渠道/模型
|
||||
- 运行时配置可写入 settings(环境变量作为回退)
|
||||
- 运行时配置可写入 settings(不再使用环境变量回退)
|
||||
- `GET /api/settings` 返回:
|
||||
- `runtime_settings`(可写配置值)
|
||||
- `runtime_config`(生效值 + 队列绑定/生效状态)
|
||||
- `usage_queue_status`(队列使用数量与状态:count/date/limit/enabled/bound/active)
|
||||
- 运行时配置字段:
|
||||
- `proxy_upstream_timeout_ms`
|
||||
- `proxy_stream_usage_mode`
|
||||
@@ -30,10 +31,11 @@
|
||||
- `usage_queue_daily_limit`
|
||||
- `usage_queue_direct_write_ratio`
|
||||
- 运行时配置默认值:
|
||||
- `PROXY_STREAM_USAGE_MODE=full`
|
||||
- `PROXY_STREAM_USAGE_MAX_BYTES=0`
|
||||
- `PROXY_STREAM_USAGE_MAX_PARSERS=0`
|
||||
- `PROXY_UPSTREAM_TIMEOUT_MS=30000`
|
||||
- `proxy_stream_usage_mode=full`
|
||||
- `proxy_stream_usage_max_bytes=0`
|
||||
- `proxy_stream_usage_max_parsers=0`
|
||||
- `proxy_upstream_timeout_ms=30000`
|
||||
- `proxy_usage_queue_enabled=true`
|
||||
- `usage_queue_daily_limit=10000`
|
||||
- `usage_queue_direct_write_ratio=0.5`
|
||||
- `PROXY_STREAM_USAGE_MAX_BYTES/PROXY_STREAM_USAGE_MAX_PARSERS` 设为 `0` 表示无限制
|
||||
@@ -44,6 +46,7 @@
|
||||
- `cache_ttl_settings_seconds`
|
||||
- `cache_config` 中包含分组版本号(dashboard/usage/models/tokens/channels/call_tokens/settings),用于精准失效
|
||||
- `POST /api/settings/cache/refresh` 会 bump 所有缓存版本(用于紧急刷新)
|
||||
- 队列使用数量来自 UsageLimiter DO 的 `/status`,未绑定时返回空值
|
||||
|
||||
## 依赖关系
|
||||
- `settings` 表
|
||||
@@ -86,7 +86,7 @@ bun run dev:worker
|
||||
- 日志保留天数(默认 30)
|
||||
- 会话时长(小时,默认 12)
|
||||
- 管理员密码(首次登录在登录页输入密码将自动设置,可在系统设置中修改)
|
||||
- 运行时配置(只读,来自环境变量):`PROXY_STREAM_USAGE_MODE`、`PROXY_STREAM_USAGE_MAX_BYTES`、`PROXY_STREAM_USAGE_MAX_PARSERS`、`PROXY_USAGE_QUEUE_ENABLED`(需在部署环境中修改)
|
||||
- 运行时配置(以数据库为准,可在系统设置中修改)
|
||||
|
||||
### 3) 启动管理台 UI
|
||||
|
||||
|
||||
@@ -1902,6 +1902,7 @@ const App = () => {
|
||||
buildActionKey("settings:cache:refresh"),
|
||||
)}
|
||||
runtimeConfig={data.settings?.runtime_config ?? null}
|
||||
usageQueueStatus={data.settings?.usage_queue_status ?? null}
|
||||
isSaving={isActionPending(buildActionKey("settings:submit"))}
|
||||
onSubmit={handleSettingsSubmit}
|
||||
onFormChange={handleSettingsFormChange}
|
||||
|
||||
@@ -125,6 +125,7 @@ export type Settings = {
|
||||
runtime_settings?: RuntimeProxySettings;
|
||||
runtime_config?: RuntimeProxyConfig;
|
||||
cache_config?: CacheConfig;
|
||||
usage_queue_status?: UsageQueueStatus | null;
|
||||
};
|
||||
|
||||
export type RuntimeProxySettings = {
|
||||
@@ -160,6 +161,15 @@ export type CacheConfig = {
|
||||
version_settings: number;
|
||||
};
|
||||
|
||||
export type UsageQueueStatus = {
|
||||
count: number | null;
|
||||
date: string | null;
|
||||
limit: number;
|
||||
enabled: boolean;
|
||||
bound: boolean;
|
||||
active: boolean;
|
||||
};
|
||||
|
||||
export type ModelChannel = {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -457,7 +457,7 @@ proxy.all("/*", tokenAuth, async (c) => {
|
||||
const requestStart = Date.now();
|
||||
const [cacheConfig, runtimeSettings] = await Promise.all([
|
||||
getCacheConfig(c.env.DB),
|
||||
getProxyRuntimeSettings(c.env.DB, c.env),
|
||||
getProxyRuntimeSettings(c.env.DB),
|
||||
]);
|
||||
const scheduleUsageEvent = createUsageEventScheduler(c, runtimeSettings);
|
||||
let requestText = await c.req.text();
|
||||
|
||||
@@ -22,6 +22,10 @@ import {
|
||||
setModelFailureCooldownMinutes,
|
||||
setProxyRuntimeSettings,
|
||||
} from "../services/settings";
|
||||
import {
|
||||
getUsageLimiterStub,
|
||||
getUsageQueueStatus,
|
||||
} from "../services/usage-limiter";
|
||||
import { sha256Hex } from "../utils/crypto";
|
||||
import { jsonError } from "../utils/http";
|
||||
|
||||
@@ -38,9 +42,45 @@ settings.get("/", async (c) => {
|
||||
const modelFailureCooldownMinutes = await getModelFailureCooldownMinutes(
|
||||
c.env.DB,
|
||||
);
|
||||
const runtimeSettings = await getProxyRuntimeSettings(c.env.DB, c.env);
|
||||
const runtimeSettings = await getProxyRuntimeSettings(c.env.DB);
|
||||
const runtimeConfig = getRuntimeProxyConfig(c.env, runtimeSettings);
|
||||
const cacheConfig = await getCacheConfig(c.env.DB);
|
||||
let usageQueueStatus: {
|
||||
count: number | null;
|
||||
date: string | null;
|
||||
limit: number;
|
||||
enabled: boolean;
|
||||
bound: boolean;
|
||||
active: boolean;
|
||||
} | null = null;
|
||||
if (c.env.USAGE_LIMITER) {
|
||||
try {
|
||||
const status = await getUsageQueueStatus(
|
||||
getUsageLimiterStub(c.env.USAGE_LIMITER),
|
||||
);
|
||||
usageQueueStatus = {
|
||||
count: status.count,
|
||||
date: status.date,
|
||||
limit: runtimeSettings.usage_queue_daily_limit,
|
||||
enabled: runtimeSettings.usage_queue_enabled,
|
||||
bound: runtimeConfig.usage_queue_bound,
|
||||
active: runtimeConfig.usage_queue_active,
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn("[settings:usage_queue_status_failed]", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
usageQueueStatus = {
|
||||
count: null,
|
||||
date: null,
|
||||
limit: runtimeSettings.usage_queue_daily_limit,
|
||||
enabled: runtimeSettings.usage_queue_enabled,
|
||||
bound: runtimeConfig.usage_queue_bound,
|
||||
active: runtimeConfig.usage_queue_active,
|
||||
};
|
||||
}
|
||||
return c.json({
|
||||
log_retention_days: retention,
|
||||
session_ttl_hours: sessionTtlHours,
|
||||
@@ -50,6 +90,7 @@ settings.get("/", async (c) => {
|
||||
runtime_config: runtimeConfig,
|
||||
runtime_settings: runtimeSettings,
|
||||
cache_config: cacheConfig,
|
||||
usage_queue_status: usageQueueStatus,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ const DEFAULT_CACHE_CHANNELS_TTL_SECONDS = 15;
|
||||
const DEFAULT_CACHE_CALL_TOKENS_TTL_SECONDS = 15;
|
||||
const DEFAULT_CACHE_SETTINGS_TTL_SECONDS = 30;
|
||||
const DEFAULT_PROXY_UPSTREAM_TIMEOUT_MS = 30000;
|
||||
const DEFAULT_PROXY_USAGE_QUEUE_ENABLED = true;
|
||||
const DEFAULT_USAGE_QUEUE_DAILY_LIMIT = 10000;
|
||||
const DEFAULT_USAGE_QUEUE_DIRECT_WRITE_RATIO = 0.5;
|
||||
const CACHE_CONFIG_TTL_MS = 1000;
|
||||
@@ -184,35 +185,6 @@ function parsePositiveNumber(value: string | null, fallback: number): number {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function parseBooleanEnv(value: string | undefined, fallback: boolean): boolean {
|
||||
if (value === undefined || value === null) {
|
||||
return fallback;
|
||||
}
|
||||
return parseBooleanSetting(value, fallback);
|
||||
}
|
||||
|
||||
function parsePositiveEnv(value: string | undefined, fallback: number): number {
|
||||
if (value === undefined || value === null || value === "") {
|
||||
return fallback;
|
||||
}
|
||||
const parsed = Number(value);
|
||||
if (!Number.isNaN(parsed) && parsed > 0) {
|
||||
return Math.floor(parsed);
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function parseNonNegativeInt(value: string | undefined, fallback: number): number {
|
||||
if (value === undefined || value === null || value === "") {
|
||||
return fallback;
|
||||
}
|
||||
const parsed = Number(value);
|
||||
if (!Number.isNaN(parsed) && parsed >= 0) {
|
||||
return Math.floor(parsed);
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function parseNonNegativeSetting(value: string | null, fallback: number): number {
|
||||
if (!value) {
|
||||
return fallback;
|
||||
@@ -381,33 +353,26 @@ export async function getSettingsSnapshot(
|
||||
|
||||
export async function getProxyRuntimeSettings(
|
||||
db: D1Database,
|
||||
env: Bindings,
|
||||
): Promise<ProxyRuntimeSettings> {
|
||||
const settings = await getSettingsSnapshot(db);
|
||||
const upstreamTimeout = parsePositiveSetting(
|
||||
settings[PROXY_UPSTREAM_TIMEOUT_KEY] ?? null,
|
||||
parsePositiveEnv(env.PROXY_UPSTREAM_TIMEOUT_MS, DEFAULT_PROXY_UPSTREAM_TIMEOUT_MS),
|
||||
DEFAULT_PROXY_UPSTREAM_TIMEOUT_MS,
|
||||
);
|
||||
const streamUsageMode = normalizeStreamUsageMode(
|
||||
settings[PROXY_STREAM_USAGE_MODE_KEY] ?? env.PROXY_STREAM_USAGE_MODE,
|
||||
settings[PROXY_STREAM_USAGE_MODE_KEY],
|
||||
);
|
||||
const streamUsageMaxBytes = parseNonNegativeSetting(
|
||||
settings[PROXY_STREAM_USAGE_MAX_BYTES_KEY] ?? null,
|
||||
parseNonNegativeInt(
|
||||
env.PROXY_STREAM_USAGE_MAX_BYTES,
|
||||
DEFAULT_PROXY_STREAM_USAGE_MAX_BYTES,
|
||||
),
|
||||
DEFAULT_PROXY_STREAM_USAGE_MAX_BYTES,
|
||||
);
|
||||
const streamUsageMaxParsers = parseNonNegativeSetting(
|
||||
settings[PROXY_STREAM_USAGE_MAX_PARSERS_KEY] ?? null,
|
||||
parseNonNegativeInt(
|
||||
env.PROXY_STREAM_USAGE_MAX_PARSERS,
|
||||
DEFAULT_PROXY_STREAM_USAGE_MAX_PARSERS,
|
||||
),
|
||||
DEFAULT_PROXY_STREAM_USAGE_MAX_PARSERS,
|
||||
);
|
||||
const usageQueueEnabled = parseBooleanSetting(
|
||||
settings[PROXY_USAGE_QUEUE_ENABLED_KEY] ?? null,
|
||||
parseBooleanEnv(env.PROXY_USAGE_QUEUE_ENABLED, true),
|
||||
DEFAULT_PROXY_USAGE_QUEUE_ENABLED,
|
||||
);
|
||||
const usageQueueDailyLimit = parseNonNegativeSetting(
|
||||
settings[USAGE_QUEUE_DAILY_LIMIT_KEY] ?? null,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
DurableObjectNamespace,
|
||||
DurableObjectState,
|
||||
DurableObjectStub,
|
||||
} from "@cloudflare/workers-types";
|
||||
import { beijingDateString } from "../utils/time";
|
||||
|
||||
@@ -16,11 +17,17 @@ export type UsageLimiterReserveResult = {
|
||||
date: string;
|
||||
};
|
||||
|
||||
export type UsageLimiterStatus = {
|
||||
ok: boolean;
|
||||
date: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
export const getUsageLimiterStub = (namespace: DurableObjectNamespace) =>
|
||||
namespace.get(namespace.idFromName(LIMITER_NAME));
|
||||
|
||||
export async function reserveUsageQueue(
|
||||
stub: { fetch: (input: RequestInfo, init?: RequestInit) => Promise<Response> },
|
||||
stub: DurableObjectStub,
|
||||
options: { limit: number; amount?: number },
|
||||
): Promise<UsageLimiterReserveResult> {
|
||||
const limit = Math.max(0, Math.floor(options.limit));
|
||||
@@ -36,6 +43,17 @@ export async function reserveUsageQueue(
|
||||
return payload;
|
||||
}
|
||||
|
||||
export async function getUsageQueueStatus(
|
||||
stub: DurableObjectStub,
|
||||
): Promise<UsageLimiterStatus> {
|
||||
const response = await stub.fetch("https://usage-limiter/status");
|
||||
if (!response.ok) {
|
||||
throw new Error(`usage_limiter_status_failed:${response.status}`);
|
||||
}
|
||||
const payload = (await response.json()) as UsageLimiterStatus;
|
||||
return payload;
|
||||
}
|
||||
|
||||
export class UsageLimiter {
|
||||
private state: DurableObjectState;
|
||||
|
||||
|
||||
Reference in new issue
Block a user