diff --git a/.helloagents/CHANGELOG.md b/.helloagents/CHANGELOG.md index 9f37272..ad55e58 100644 --- a/.helloagents/CHANGELOG.md +++ b/.helloagents/CHANGELOG.md @@ -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 diff --git a/.helloagents/modules/admin-ui.md b/.helloagents/modules/admin-ui.md index 74d7c56..b0b136c 100644 --- a/.helloagents/modules/admin-ui.md +++ b/.helloagents/modules/admin-ui.md @@ -39,7 +39,7 @@ - 系统设置增加定时签到开关与时间(中国时间) - 系统设置提供模型失败冷却分钟数配置 - 系统设置提供运行时配置可编辑(上游超时、流式解析模式/上限、队列开关/日限额/直写比例) -- 系统设置展示运行时配置(只读),以卡片显示名称 + 环境变量名 + 当前值,并提示设置优先生效 +- 系统设置将运行时配置拆分为“解析与请求 / 队列设置”,队列绑定/实际生效/使用数量以内联只读项展示 - 系统设置提供缓存开关与 TTL(面板/日志/模型/鉴权/渠道/调用令牌/设置)配置 - 系统设置展示缓存版本号并提供刷新入口 - 列表分页页脚统一使用页码导航 + 每页条数选择,并显示总页数(无数据时显示 0 页) diff --git a/.helloagents/modules/settings.md b/.helloagents/modules/settings.md index 991d939..907dbf1 100644 --- a/.helloagents/modules/settings.md +++ b/.helloagents/modules/settings.md @@ -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` 表 diff --git a/README.md b/README.md index d8282e8..2de2142 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/apps/ui/src/App.tsx b/apps/ui/src/App.tsx index 22ab7f8..cb85f4b 100644 --- a/apps/ui/src/App.tsx +++ b/apps/ui/src/App.tsx @@ -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} diff --git a/apps/ui/src/core/types.ts b/apps/ui/src/core/types.ts index 5e49404..b8e992d 100644 --- a/apps/ui/src/core/types.ts +++ b/apps/ui/src/core/types.ts @@ -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; diff --git a/apps/ui/src/features/SettingsView.tsx b/apps/ui/src/features/SettingsView.tsx index 1d3619a..9656403 100644 --- a/apps/ui/src/features/SettingsView.tsx +++ b/apps/ui/src/features/SettingsView.tsx @@ -1,16 +1,22 @@ import { Button, Card, Input, Select, Switch } from "../components/ui"; -import type { CacheConfig, RuntimeProxyConfig, SettingsForm } from "../core/types"; +import type { + CacheConfig, + RuntimeProxyConfig, + SettingsForm, + UsageQueueStatus, +} from "../core/types"; type SettingsViewProps = { - settingsForm: SettingsForm; - adminPasswordSet: boolean; - isSaving: boolean; - cacheConfig?: CacheConfig | null; - isRefreshingCache?: boolean; - runtimeConfig?: RuntimeProxyConfig | null; - onSubmit: (event: Event) => void; - onFormChange: (patch: Partial) => void; - onRefreshCache: () => void; + settingsForm: SettingsForm; + adminPasswordSet: boolean; + isSaving: boolean; + cacheConfig?: CacheConfig | null; + isRefreshingCache?: boolean; + runtimeConfig?: RuntimeProxyConfig | null; + usageQueueStatus?: UsageQueueStatus | null; + onSubmit: (event: Event) => void; + onFormChange: (patch: Partial) => void; + onRefreshCache: () => void; }; /** @@ -23,577 +29,619 @@ type SettingsViewProps = { * Settings JSX element. */ export const SettingsView = ({ - settingsForm, - adminPasswordSet, - isSaving, - cacheConfig, - isRefreshingCache, - runtimeConfig, - onSubmit, - onFormChange, - onRefreshCache, + settingsForm, + adminPasswordSet, + isSaving, + cacheConfig, + isRefreshingCache, + runtimeConfig, + usageQueueStatus, + onSubmit, + onFormChange, + onRefreshCache, }: SettingsViewProps) => { - const runtimeItems = [ - { - label: "上游超时(毫秒)", - value: runtimeConfig?.upstream_timeout_ms ?? 30000, - env: "PROXY_UPSTREAM_TIMEOUT_MS", - }, - { - label: "流式 usage 解析模式", - value: runtimeConfig?.stream_usage_mode ?? "full", - env: "PROXY_STREAM_USAGE_MODE", - }, - { - label: "流式解析最大字节", - value: runtimeConfig?.stream_usage_max_bytes ?? 0, - env: "PROXY_STREAM_USAGE_MAX_BYTES", - }, - { - label: "流式解析并发上限", - value: runtimeConfig?.stream_usage_max_parsers ?? 0, - env: "PROXY_STREAM_USAGE_MAX_PARSERS", - }, - { - label: "队列开关", - value: runtimeConfig?.usage_queue_enabled ? "启用" : "关闭", - env: "PROXY_USAGE_QUEUE_ENABLED", - }, - { - label: "队列日限额", - value: runtimeConfig?.usage_queue_daily_limit ?? 10000, - env: "usage_queue_daily_limit", - }, - { - label: "直写比例", - value: - runtimeConfig?.usage_queue_direct_write_ratio !== undefined - ? `${Math.round(runtimeConfig.usage_queue_direct_write_ratio * 100)}%` - : "50%", - env: "usage_queue_direct_write_ratio", - }, - { - label: "队列绑定", - value: runtimeConfig?.usage_queue_bound ? "已绑定" : "未绑定", - env: "USAGE_QUEUE 绑定", - }, - { - label: "队列实际生效", - value: runtimeConfig?.usage_queue_active ? "是" : "否", - env: "USAGE_QUEUE 运行时", - }, - ]; + const queueBoundValue = + runtimeConfig === null || runtimeConfig === undefined + ? "-" + : runtimeConfig.usage_queue_bound + ? "已绑定" + : "未绑定"; + const queueActiveValue = + runtimeConfig === null || runtimeConfig === undefined + ? "-" + : runtimeConfig.usage_queue_active + ? "是" + : "否"; + const queueUsageValue = usageQueueStatus + ? usageQueueStatus.count === null + ? "未绑定" + : usageQueueStatus.limit > 0 + ? `${usageQueueStatus.count} / ${usageQueueStatus.limit}` + : String(usageQueueStatus.count) + : "-"; - return ( -
-
-

系统设置

-
- -
-
- - { - const target = event.currentTarget as HTMLInputElement | null; - onFormChange({ - log_retention_days: target?.value ?? "", - }); - }} - /> -
-
- - { - const target = event.currentTarget as HTMLInputElement | null; - onFormChange({ - session_ttl_hours: target?.value ?? "", - }); - }} - /> -
-
- - { - const target = event.currentTarget as HTMLInputElement | null; - onFormChange({ - checkin_schedule_time: target?.value ?? "", - }); - }} - /> -
-
- - { - const target = event.currentTarget as HTMLInputElement | null; - onFormChange({ - model_failure_cooldown_minutes: target?.value ?? "", - }); - }} - /> -

- 同一模型失败后在该时间内跳过对应渠道。 -

-
-
- - { - const target = event.currentTarget as HTMLInputElement | null; - onFormChange({ - admin_password: target?.value ?? "", - }); - }} - /> -

- 密码状态:{adminPasswordSet ? "已设置" : "未设置"} -

-
-
-
运行时配置
-

- 以下配置可在后台修改,环境变量仅作为回退值。 -

-
-
- - { - const target = event.currentTarget as HTMLInputElement | null; - onFormChange({ - proxy_upstream_timeout_ms: target?.value ?? "", - }); - }} - /> -

- 设置为 0 表示不限制超时。 -

-
-
- - -
-
- - { - const target = event.currentTarget as HTMLInputElement | null; - onFormChange({ - proxy_stream_usage_max_bytes: target?.value ?? "", - }); - }} - /> -

- 0 表示不限制。 -

-
-
- - { - const target = event.currentTarget as HTMLInputElement | null; - onFormChange({ - proxy_stream_usage_max_parsers: target?.value ?? "", - }); - }} - /> -

- 0 表示不限制。 -

-
-
-
-
-
- 启用用量队列 -
-

- 启用后将按比例把 usage 写入队列。 -

-
- { - onFormChange({ proxy_usage_queue_enabled: next }); - }} - /> -
-
-
- - { - const target = event.currentTarget as HTMLInputElement | null; - onFormChange({ - usage_queue_daily_limit: target?.value ?? "", - }); - }} - /> -

- 达到上限后自动切回 Worker 直写。 -

-
-
- - { - const target = event.currentTarget as HTMLInputElement | null; - onFormChange({ - usage_queue_direct_write_ratio: target?.value ?? "", - }); - }} - /> -

- 示例:0.5 表示 50% 直写。 -

-
-
-
-
-
- 启用面板缓存 -
-

- 对数据面板、日志列表与模型列表启用短时缓存,降低 D1 读取量。 -

-
- { - onFormChange({ cache_enabled: next }); - }} - /> -
-
-
- - { - const target = event.currentTarget as HTMLInputElement | null; - onFormChange({ - cache_ttl_dashboard_seconds: target?.value ?? "", - }); - }} - /> -

- 0 表示关闭该接口缓存。 -

-
-
- - { - const target = event.currentTarget as HTMLInputElement | null; - onFormChange({ - cache_ttl_usage_seconds: target?.value ?? "", - }); - }} - /> -

- 日志查询建议设置较短 TTL。 -

-
-
- - { - const target = event.currentTarget as HTMLInputElement | null; - onFormChange({ - cache_ttl_models_seconds: target?.value ?? "", - }); - }} - /> -
-
- - { - const target = event.currentTarget as HTMLInputElement | null; - onFormChange({ - cache_ttl_tokens_seconds: target?.value ?? "", - }); - }} - /> -

- 禁用令牌会自动失效,不影响即时生效。 -

-
-
- - { - const target = event.currentTarget as HTMLInputElement | null; - onFormChange({ - cache_ttl_channels_seconds: target?.value ?? "", - }); - }} - /> -
-
- - { - const target = event.currentTarget as HTMLInputElement | null; - onFormChange({ - cache_ttl_call_tokens_seconds: target?.value ?? "", - }); - }} - /> -
-
- - { - const target = event.currentTarget as HTMLInputElement | null; - onFormChange({ - cache_ttl_settings_seconds: target?.value ?? "", - }); - }} - /> -
-
-
- 缓存版本: - {cacheConfig - ? `tokens ${cacheConfig.version_tokens}, channels ${cacheConfig.version_channels}, call_tokens ${cacheConfig.version_call_tokens}, models ${cacheConfig.version_models}, dashboard ${cacheConfig.version_dashboard}, usage ${cacheConfig.version_usage}, settings ${cacheConfig.version_settings}` - : "-"} -
- -
-
- -
-
-
- -

运行时配置(只读)

-

- 此处展示实际生效值(设置优先,环境变量作为回退)。 - 环境变量名已在每项卡片内标注。 -

-
- {runtimeItems.map((item) => ( -
-
- {item.label} -
-
- {item.env} -
-
- {item.value} -
-
- ))} -
-
-
- ); + return ( +
+
+

系统设置

+
+ +
+
+
基础设置
+
+
+ + { + const target = event.currentTarget as HTMLInputElement | null; + onFormChange({ + log_retention_days: target?.value ?? "", + }); + }} + /> +
+
+ + { + const target = event.currentTarget as HTMLInputElement | null; + onFormChange({ + session_ttl_hours: target?.value ?? "", + }); + }} + /> +
+
+ + { + const target = event.currentTarget as HTMLInputElement | null; + onFormChange({ + checkin_schedule_time: target?.value ?? "", + }); + }} + /> +
+
+ + { + const target = event.currentTarget as HTMLInputElement | null; + onFormChange({ + model_failure_cooldown_minutes: target?.value ?? "", + }); + }} + /> +

+ 同一模型失败后在该时间内跳过对应渠道。 +

+
+
+ + { + const target = event.currentTarget as HTMLInputElement | null; + onFormChange({ + admin_password: target?.value ?? "", + }); + }} + /> +

+ 密码状态:{adminPasswordSet ? "已设置" : "未设置"} +

+
+
+
解析与请求
+
+
+ + { + const target = event.currentTarget as HTMLInputElement | null; + onFormChange({ + proxy_upstream_timeout_ms: target?.value ?? "", + }); + }} + /> +

+ 设置为 0 表示不限制超时。 +

+
+
+ + +
+
+ + { + const target = event.currentTarget as HTMLInputElement | null; + onFormChange({ + proxy_stream_usage_max_bytes: target?.value ?? "", + }); + }} + /> +

+ 0 表示不限制。 +

+
+
+ + { + const target = event.currentTarget as HTMLInputElement | null; + onFormChange({ + proxy_stream_usage_max_parsers: target?.value ?? "", + }); + }} + /> +

+ 0 表示不限制。 +

+
+
+
队列设置
+
+
+
+
+
+ 启用用量队列 +
+

+ 启用后将按比例把 usage 写入队列。 +

+
+ { + onFormChange({ proxy_usage_queue_enabled: next }); + }} + /> +
+
+
+ + { + const target = event.currentTarget as HTMLInputElement | null; + onFormChange({ + usage_queue_daily_limit: target?.value ?? "", + }); + }} + /> +

+ 达到上限后自动切回 Worker 直写。 +

+
+
+ + { + const target = event.currentTarget as HTMLInputElement | null; + onFormChange({ + usage_queue_direct_write_ratio: target?.value ?? "", + }); + }} + /> +

+ 示例:0.5 表示 50% 直写。 +

+
+
+
+
+
+ 队列绑定 +
+
+ {queueBoundValue} +
+
+
+
+ 队列实际生效 +
+
+ {queueActiveValue} +
+
+
+
+ 队列使用数量 +
+
+ {queueUsageValue} +
+
+ {usageQueueStatus?.date + ? `统计日期:${usageQueueStatus.date}` + : "统计日期:-"} +
+
+
+
+
+
缓存设置
+
+
+
+
+
+ 启用缓存(总开关) +
+

+ 影响面板、日志、模型、鉴权、渠道、调用令牌与设置缓存。 +

+
+ { + onFormChange({ cache_enabled: next }); + }} + /> +
+
+
+
+
+
+ 面板缓存 +
+ + { + const target = event.currentTarget as HTMLInputElement | null; + onFormChange({ + cache_ttl_dashboard_seconds: target?.value ?? "", + }); + }} + /> +

+ 0 表示关闭该接口缓存。 +

+
+ 版本: + {cacheConfig ? cacheConfig.version_dashboard : "-"} +
+
+
+
+ 日志缓存 +
+ + { + const target = event.currentTarget as HTMLInputElement | null; + onFormChange({ + cache_ttl_usage_seconds: target?.value ?? "", + }); + }} + /> +

+ 日志查询建议设置较短 TTL。 +

+
+ 版本: + {cacheConfig ? cacheConfig.version_usage : "-"} +
+
+
+
+ 模型缓存 +
+ + { + const target = event.currentTarget as HTMLInputElement | null; + onFormChange({ + cache_ttl_models_seconds: target?.value ?? "", + }); + }} + /> +
+ 版本: + {cacheConfig ? cacheConfig.version_models : "-"} +
+
+
+
+ Token 鉴权缓存 +
+ + { + const target = event.currentTarget as HTMLInputElement | null; + onFormChange({ + cache_ttl_tokens_seconds: target?.value ?? "", + }); + }} + /> +

+ 禁用令牌会自动失效,不影响即时生效。 +

+
+ 版本: + {cacheConfig ? cacheConfig.version_tokens : "-"} +
+
+
+
+ 渠道缓存 +
+ + { + const target = event.currentTarget as HTMLInputElement | null; + onFormChange({ + cache_ttl_channels_seconds: target?.value ?? "", + }); + }} + /> +
+ 版本: + {cacheConfig ? cacheConfig.version_channels : "-"} +
+
+
+
+ 调用令牌缓存 +
+ + { + const target = event.currentTarget as HTMLInputElement | null; + onFormChange({ + cache_ttl_call_tokens_seconds: target?.value ?? "", + }); + }} + /> +
+ 版本: + {cacheConfig ? cacheConfig.version_call_tokens : "-"} +
+
+
+
+ 设置缓存 +
+ + { + const target = event.currentTarget as HTMLInputElement | null; + onFormChange({ + cache_ttl_settings_seconds: target?.value ?? "", + }); + }} + /> +
+ 版本: + {cacheConfig ? cacheConfig.version_settings : "-"} +
+
+
+
+
+ +
+
+ +
+
+
+
+ ); }; diff --git a/apps/worker/src/routes/proxy.ts b/apps/worker/src/routes/proxy.ts index 359acbf..66c2df6 100644 --- a/apps/worker/src/routes/proxy.ts +++ b/apps/worker/src/routes/proxy.ts @@ -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(); diff --git a/apps/worker/src/routes/settings.ts b/apps/worker/src/routes/settings.ts index c71d048..d7953a4 100644 --- a/apps/worker/src/routes/settings.ts +++ b/apps/worker/src/routes/settings.ts @@ -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, }); }); diff --git a/apps/worker/src/services/settings.ts b/apps/worker/src/services/settings.ts index 33b6f1c..e056c06 100644 --- a/apps/worker/src/services/settings.ts +++ b/apps/worker/src/services/settings.ts @@ -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 { 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, diff --git a/apps/worker/src/services/usage-limiter.ts b/apps/worker/src/services/usage-limiter.ts index 594855e..de44401 100644 --- a/apps/worker/src/services/usage-limiter.ts +++ b/apps/worker/src/services/usage-limiter.ts @@ -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 }, + stub: DurableObjectStub, options: { limit: number; amount?: number }, ): Promise { 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 { + 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;