sub2api/frontend/src/api/admin/settings.ts
DaydreamCoding 30f55a1f72 feat(openai): OpenAI Fast/Flex Policy 完整实现(HTTP + WebSocket + Admin)
对称参照 Claude BetaPolicy 的 fast-mode 过滤实现,新增针对 OpenAI 上游
service_tier 字段(priority / flex,含客户端 "fast" → "priority" 归一化)的
pass / filter / block 三态策略,覆盖全部 OpenAI 入口 + admin 配置入口。

后端核心
- 新增 SettingKeyOpenAIFastPolicySettings、OpenAIFastPolicyRule、
  OpenAIFastPolicySettings 配置模型,含规则的 service_tier × action × scope
  × 模型白名单 × fallback action 维度。
- SettingService.Get/SetOpenAIFastPolicySettings;缺失时返回内置默认策略
  (所有模型的 priority 走 filter,whitelist 为空,fallback=pass)。设计
  依据:service_tier=fast 是用户级开关,与 model 字段正交,默认锁定特定
  model slug 会留下"用 gpt-4 + fast 透传 priority 上游"的绕过路径。JSON
  解析失败不再静默 fallback,slog.Warn 记录脏数据,便于运维定位。
- service_tier 归一化(trim + ToLower + fast→priority + 白名单 priority/flex)
  与策略评估(evaluateOpenAIFastPolicy)作为唯一真实来源,HTTP / WS 共用。
  抽出纯函数 evaluateOpenAIFastPolicyWithSettings,配合 ctx-bound settings
  快照(withOpenAIFastPolicyContext / openAIFastPolicySettingsFromContext),
  WS 长会话入口预取一次后所有帧复用,避免每帧打到 settingService。

HTTP 入口(4 个)
- Chat Completions、Anthropic 兼容(Messages,含 BetaFastMode→priority 二次
  命中)、原生 Responses、Passthrough Responses 全部接入
  applyOpenAIFastPolicyToBody,filter 走 sjson 顶层删除 service_tier,block
  返回 403 forbidden_error JSON。
- 4 入口统一使用 upstream 视角的 model(GetMappedModel +
  normalizeOpenAIModelForUpstream + Codex OAuth normalize 后的 slug),
  避免 chat/messages/native /responses/passthrough 因为 model 维度不同
  造成 whitelist 命中差异。
- 在 pass 路径也把客户端 "fast" 别名归一化为 "priority" 写回 body,
  否则 native /responses 与 passthrough 入口会把 "fast" 原样透传给上游
  导致 400/拒绝(chat-completions 入口的 normalizeResponsesBodyServiceTier
  此前已具备同等行为)。

WebSocket 入口
- 新增 applyOpenAIFastPolicyToWSResponseCreate:严格匹配
  type="response.create",仅处理顶层 service_tier;filter 用 sjson 删字段,
  block 返回 typed *OpenAIFastBlockedError。
- ingress 路径在 parseClientPayload 内调用,block 命中先 Write Realtime
  风格 error event 再返回 OpenAIWSClientCloseError(StatusPolicyViolation
  =1008),依赖底层 WebSocket Conn.Write 的同步 flush 保证 error 先于
  close。
- passthrough 路径在 RunEntry 前对 firstClientMessage 应用策略,并通过
  openAIWSPolicyEnforcingFrameConn 包装 ReadFrame 对每个 client→upstream
  帧执行策略;后续帧无 model 字段时回退到 capturedSessionModel。
  filter 闭包内同时侦测 session.update / session.created 帧的 session.model
  字段刷新 capturedSessionModel,封堵"首帧 model=gpt-4o(pass)→
  session.update 改为 gpt-5.5 → 不带 model 的 response.create fallback
  到 gpt-4o"的 mid-session 绕过路径。
- passthrough billing:requestServiceTier 在策略 filter 之后再从
  firstClientMessage 提取,filter 命中时 OpenAIForwardResult.ServiceTier
  上报 nil(default tier),与 HTTP 入口(reqBody 来自 post-filter map)
  / WS ingress(payload 来自 post-filter bytes)的语义一致。
- 错误事件 schema:{event_id: "evt_<32hex>", type: "error",
  error: {type: "forbidden_error", code: "policy_violation", message}},
  与 OpenAI codex 客户端 error event 解析兼容。

Admin / Frontend
- dto.SystemSettings / UpdateSettingsRequest 新增
  openai_fast_policy_settings 字段(omitempty),bulk GET/PUT 接入。
- Settings 页 Gateway 页签新增 Fast/Flex Policy 表单卡片:
  service_tier × action × scope × 模型白名单 × fallback action 全字段配置。
- 前端守门:openaiFastPolicyLoaded 标志仅在 GET 真带回字段时才允许回写,
  避免 rollout/错误把默认规则覆盖成空;saveSettings 回写循环 skip 该字段,
  由专用刷新逻辑处理;仅 action=block 时发送 error_message,匹配后端
  omitempty 行为。

测试
- HTTP 路径:openai_fast_policy_test.go 覆盖默认配置(whitelist=[],所有
  模型 priority filter)/ block 自定义错误 / scope 区分 / filter 删字段 /
  block 不改 body / block 短路上游 / Anthropic BetaFastMode 触发 OpenAI
  fast policy 等场景。
- WebSocket 路径:openai_fast_policy_ws_test.go 覆盖
    helper 单元(filter / fast→priority 归一化 / flex 透传 / block typed
    error / 无 service_tier 字节不变 / 非 response.create 帧不动 / 空 type
    帧不动 / event_id+code 字段断言 / 非字符串 service_tier 容错)+
    pass 路径 fast 别名归一化回归 +
    ingress 端到端(filter 后上游不含 service_tier / block 后客户端先收
    error event 再收 close 1008 且上游 0 写)+
    passthrough capturedSessionModel fallback 用例(whitelist 策略下首帧
    建立、缺 model 命中 fallback、缺少 fallback 时的 leak 文档化)+
    passthrough session.update / session.created 旋转 capturedSessionModel
    的 mid-session 绕过回归 +
    passthrough billing post-filter ServiceTier 与 idempotent filter 回归。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 11:15:09 +08:00

1038 lines
30 KiB
TypeScript

/**
* Admin Settings API endpoints
* Handles system settings management for administrators
*/
import { apiClient } from "../client";
import type { CustomMenuItem, CustomEndpoint, NotifyEmailEntry } from "@/types";
export interface DefaultSubscriptionSetting {
group_id: number;
validity_days: number;
}
export type AuthSourceType = "email" | "linuxdo" | "oidc" | "wechat";
export interface AuthSourceDefaultsValue {
balance: number;
concurrency: number;
subscriptions: DefaultSubscriptionSetting[];
grant_on_signup: boolean;
grant_on_first_bind: boolean;
}
export type AuthSourceDefaultsState = Record<
AuthSourceType,
AuthSourceDefaultsValue
>;
export type PaymentVisibleMethod = "alipay" | "wxpay";
export type PaymentVisibleMethodSource =
| ""
| "official_alipay"
| "easypay_alipay"
| "official_wxpay"
| "easypay_wxpay";
export type WeChatConnectMode = "open" | "mp" | "mobile";
export interface PaymentVisibleMethodSourceOption {
value: PaymentVisibleMethodSource;
labelZh: string;
labelEn: string;
}
export interface WeChatConnectModeOption {
value: WeChatConnectMode;
labelZh: string;
labelEn: string;
}
const AUTH_SOURCE_TYPES: AuthSourceType[] = [
"email",
"linuxdo",
"oidc",
"wechat",
];
const AUTH_SOURCE_DEFAULT_BALANCE = 0;
const AUTH_SOURCE_DEFAULT_CONCURRENCY = 5;
const PAYMENT_VISIBLE_METHOD_SOURCE_OPTIONS: Record<
PaymentVisibleMethod,
PaymentVisibleMethodSourceOption[]
> = {
alipay: [
{ value: "", labelZh: "未配置", labelEn: "Not configured" },
{
value: "official_alipay",
labelZh: "支付宝官方",
labelEn: "Official Alipay",
},
{
value: "easypay_alipay",
labelZh: "易支付支付宝",
labelEn: "EasyPay Alipay",
},
],
wxpay: [
{ value: "", labelZh: "未配置", labelEn: "Not configured" },
{
value: "official_wxpay",
labelZh: "微信官方",
labelEn: "Official WeChat Pay",
},
{
value: "easypay_wxpay",
labelZh: "易支付微信",
labelEn: "EasyPay WeChat Pay",
},
],
};
const PAYMENT_VISIBLE_METHOD_SOURCE_ALIASES: Record<
PaymentVisibleMethod,
Record<string, PaymentVisibleMethodSource>
> = {
alipay: {
official_alipay: "official_alipay",
alipay: "official_alipay",
alipay_direct: "official_alipay",
official: "official_alipay",
easypay_alipay: "easypay_alipay",
easypay: "easypay_alipay",
},
wxpay: {
official_wxpay: "official_wxpay",
wxpay: "official_wxpay",
wxpay_direct: "official_wxpay",
wechat: "official_wxpay",
official: "official_wxpay",
easypay_wxpay: "easypay_wxpay",
easypay: "easypay_wxpay",
},
};
const WECHAT_CONNECT_MODE_OPTIONS: WeChatConnectModeOption[] = [
{ value: "open", labelZh: "PC 应用", labelEn: "PC App" },
{
value: "mp",
labelZh: "公众号",
labelEn: "Official Account",
},
{
value: "mobile",
labelZh: "移动应用",
labelEn: "Mobile App",
},
];
const WECHAT_CONNECT_MODE_ALIASES: Record<string, WeChatConnectMode> = {
open: "open",
open_platform: "open",
official: "open",
wx_open: "open",
mp: "mp",
official_account: "mp",
wechat_mp: "mp",
mini_program: "mp",
mobile: "mobile",
mobile_app: "mobile",
native_app: "mobile",
};
export function normalizeDefaultSubscriptionSettings(
subscriptions: DefaultSubscriptionSetting[] | null | undefined,
): DefaultSubscriptionSetting[] {
if (!Array.isArray(subscriptions)) return [];
return subscriptions
.filter((item) => item.group_id > 0 && item.validity_days > 0)
.map((item) => ({
group_id: Math.floor(item.group_id),
validity_days: Math.min(
36500,
Math.max(1, Math.floor(item.validity_days)),
),
}));
}
export function buildAuthSourceDefaultsState(
settings: Partial<SystemSettings>,
): AuthSourceDefaultsState {
const raw = settings as Record<string, unknown>;
return AUTH_SOURCE_TYPES.reduce((acc, source) => {
const subscriptions = raw[`auth_source_default_${source}_subscriptions`];
acc[source] = {
balance: Number(
raw[`auth_source_default_${source}_balance`] ??
AUTH_SOURCE_DEFAULT_BALANCE,
),
concurrency: Math.max(
1,
Number(
raw[`auth_source_default_${source}_concurrency`] ??
AUTH_SOURCE_DEFAULT_CONCURRENCY,
),
),
subscriptions: normalizeDefaultSubscriptionSettings(
Array.isArray(subscriptions)
? (subscriptions as DefaultSubscriptionSetting[])
: [],
),
grant_on_signup:
raw[`auth_source_default_${source}_grant_on_signup`] === true,
grant_on_first_bind:
raw[`auth_source_default_${source}_grant_on_first_bind`] === true,
};
return acc;
}, {} as AuthSourceDefaultsState);
}
export function appendAuthSourceDefaultsToUpdateRequest(
payload: UpdateSettingsRequest,
authSourceDefaults: AuthSourceDefaultsState,
): UpdateSettingsRequest {
const target = payload as Record<string, unknown>;
for (const source of AUTH_SOURCE_TYPES) {
const current = authSourceDefaults[source];
target[`auth_source_default_${source}_balance`] =
Number(current.balance) || 0;
target[`auth_source_default_${source}_concurrency`] = Math.max(
1,
Math.floor(
Number(current.concurrency) || AUTH_SOURCE_DEFAULT_CONCURRENCY,
),
);
target[`auth_source_default_${source}_subscriptions`] =
normalizeDefaultSubscriptionSettings(current.subscriptions);
target[`auth_source_default_${source}_grant_on_signup`] =
current.grant_on_signup;
target[`auth_source_default_${source}_grant_on_first_bind`] =
current.grant_on_first_bind;
}
return payload;
}
export function getPaymentVisibleMethodSourceOptions(
method: PaymentVisibleMethod,
): PaymentVisibleMethodSourceOption[] {
return PAYMENT_VISIBLE_METHOD_SOURCE_OPTIONS[method];
}
export function normalizePaymentVisibleMethodSource(
method: PaymentVisibleMethod,
source: unknown,
): PaymentVisibleMethodSource {
if (typeof source !== "string") return "";
const normalized = source.trim().toLowerCase();
if (!normalized) return "";
return PAYMENT_VISIBLE_METHOD_SOURCE_ALIASES[method][normalized] ?? "";
}
export function getWeChatConnectModeOptions(): WeChatConnectModeOption[] {
return WECHAT_CONNECT_MODE_OPTIONS;
}
export function normalizeWeChatConnectMode(source: unknown): WeChatConnectMode {
if (typeof source !== "string") return "open";
const normalized = source.trim().toLowerCase();
if (!normalized) return "open";
return WECHAT_CONNECT_MODE_ALIASES[normalized] ?? "open";
}
export function defaultWeChatConnectScopesForMode(mode: unknown): string {
switch (normalizeWeChatConnectMode(mode)) {
case "mp":
return "snsapi_userinfo";
case "mobile":
return "";
default:
return "snsapi_login";
}
}
export function resolveWeChatConnectModeCapabilities(
openEnabled: unknown,
mpEnabled: unknown,
mobileEnabled: unknown,
legacyMode: unknown,
): { openEnabled: boolean; mpEnabled: boolean; mobileEnabled: boolean } {
if (
typeof openEnabled === "boolean" ||
typeof mpEnabled === "boolean" ||
typeof mobileEnabled === "boolean"
) {
return {
openEnabled: openEnabled === true,
mpEnabled: mpEnabled === true,
mobileEnabled: mobileEnabled === true,
};
}
switch (normalizeWeChatConnectMode(legacyMode)) {
case "mp":
return { openEnabled: false, mpEnabled: true, mobileEnabled: false };
case "mobile":
return { openEnabled: false, mpEnabled: false, mobileEnabled: true };
default:
return { openEnabled: true, mpEnabled: false, mobileEnabled: false };
}
}
export function deriveWeChatConnectStoredMode(
openEnabled: boolean,
mpEnabled: boolean,
mobileEnabled: boolean,
legacyMode: unknown,
): WeChatConnectMode {
if (mpEnabled) return "mp";
if (mobileEnabled) return "mobile";
if (openEnabled) return "open";
return normalizeWeChatConnectMode(legacyMode);
}
/**
* System settings interface
*/
export interface SystemSettings {
// Registration settings
registration_enabled: boolean;
email_verify_enabled: boolean;
registration_email_suffix_whitelist: string[];
promo_code_enabled: boolean;
password_reset_enabled: boolean;
frontend_url: string;
invitation_code_enabled: boolean;
totp_enabled: boolean; // TOTP 双因素认证
totp_encryption_key_configured: boolean; // TOTP 加密密钥是否已配置
// Default settings
default_balance: number;
affiliate_rebate_rate: number;
affiliate_rebate_freeze_hours: number;
affiliate_rebate_duration_days: number;
affiliate_rebate_per_invitee_cap: number;
default_concurrency: number;
default_user_rpm_limit: number;
default_subscriptions: DefaultSubscriptionSetting[];
auth_source_default_email_balance?: number;
auth_source_default_email_concurrency?: number;
auth_source_default_email_subscriptions?: DefaultSubscriptionSetting[];
auth_source_default_email_grant_on_signup?: boolean;
auth_source_default_email_grant_on_first_bind?: boolean;
auth_source_default_linuxdo_balance?: number;
auth_source_default_linuxdo_concurrency?: number;
auth_source_default_linuxdo_subscriptions?: DefaultSubscriptionSetting[];
auth_source_default_linuxdo_grant_on_signup?: boolean;
auth_source_default_linuxdo_grant_on_first_bind?: boolean;
auth_source_default_oidc_balance?: number;
auth_source_default_oidc_concurrency?: number;
auth_source_default_oidc_subscriptions?: DefaultSubscriptionSetting[];
auth_source_default_oidc_grant_on_signup?: boolean;
auth_source_default_oidc_grant_on_first_bind?: boolean;
auth_source_default_wechat_balance?: number;
auth_source_default_wechat_concurrency?: number;
auth_source_default_wechat_subscriptions?: DefaultSubscriptionSetting[];
auth_source_default_wechat_grant_on_signup?: boolean;
auth_source_default_wechat_grant_on_first_bind?: boolean;
force_email_on_third_party_signup?: boolean;
// OEM settings
site_name: string;
site_logo: string;
site_subtitle: string;
api_base_url: string;
contact_info: string;
doc_url: string;
home_content: string;
hide_ccs_import_button: boolean;
table_default_page_size: number;
table_page_size_options: number[];
backend_mode_enabled: boolean;
custom_menu_items: CustomMenuItem[];
custom_endpoints: CustomEndpoint[];
// SMTP settings
smtp_host: string;
smtp_port: number;
smtp_username: string;
smtp_password_configured: boolean;
smtp_from_email: string;
smtp_from_name: string;
smtp_use_tls: boolean;
// Cloudflare Turnstile settings
turnstile_enabled: boolean;
turnstile_site_key: string;
turnstile_secret_key_configured: boolean;
// LinuxDo Connect OAuth settings
linuxdo_connect_enabled: boolean;
linuxdo_connect_client_id: string;
linuxdo_connect_client_secret_configured: boolean;
linuxdo_connect_redirect_url: string;
// WeChat Connect OAuth settings
wechat_connect_enabled: boolean;
wechat_connect_app_id: string;
wechat_connect_app_secret_configured: boolean;
wechat_connect_open_app_id?: string;
wechat_connect_open_app_secret_configured?: boolean;
wechat_connect_mp_app_id?: string;
wechat_connect_mp_app_secret_configured?: boolean;
wechat_connect_mobile_app_id?: string;
wechat_connect_mobile_app_secret_configured?: boolean;
wechat_connect_open_enabled?: boolean;
wechat_connect_mp_enabled?: boolean;
wechat_connect_mobile_enabled?: boolean;
wechat_connect_mode: string;
wechat_connect_scopes: string;
wechat_connect_redirect_url: string;
wechat_connect_frontend_redirect_url: string;
// Generic OIDC OAuth settings
oidc_connect_enabled: boolean;
oidc_connect_provider_name: string;
oidc_connect_client_id: string;
oidc_connect_client_secret_configured: boolean;
oidc_connect_issuer_url: string;
oidc_connect_discovery_url: string;
oidc_connect_authorize_url: string;
oidc_connect_token_url: string;
oidc_connect_userinfo_url: string;
oidc_connect_jwks_url: string;
oidc_connect_scopes: string;
oidc_connect_redirect_url: string;
oidc_connect_frontend_redirect_url: string;
oidc_connect_token_auth_method: string;
oidc_connect_use_pkce: boolean;
oidc_connect_validate_id_token: boolean;
oidc_connect_allowed_signing_algs: string;
oidc_connect_clock_skew_seconds: number;
oidc_connect_require_email_verified: boolean;
oidc_connect_userinfo_email_path: string;
oidc_connect_userinfo_id_path: string;
oidc_connect_userinfo_username_path: string;
// Model fallback configuration
enable_model_fallback: boolean;
fallback_model_anthropic: string;
fallback_model_openai: string;
fallback_model_gemini: string;
fallback_model_antigravity: string;
// Identity patch configuration (Claude -> Gemini)
enable_identity_patch: boolean;
identity_patch_prompt: string;
// Ops Monitoring (vNext)
ops_monitoring_enabled: boolean;
ops_realtime_monitoring_enabled: boolean;
ops_query_mode_default: "auto" | "raw" | "preagg" | string;
ops_metrics_interval_seconds: number;
// Claude Code version check
min_claude_code_version: string;
max_claude_code_version: string;
// 分组隔离
allow_ungrouped_key_scheduling: boolean;
// Gateway forwarding behavior
enable_fingerprint_unification: boolean;
enable_metadata_passthrough: boolean;
enable_cch_signing: boolean;
web_search_emulation_enabled?: boolean;
// Payment configuration
payment_enabled: boolean;
payment_min_amount: number;
payment_max_amount: number;
payment_daily_limit: number;
payment_order_timeout_minutes: number;
payment_max_pending_orders: number;
payment_enabled_types: string[];
payment_balance_disabled: boolean;
payment_balance_recharge_multiplier: number;
payment_recharge_fee_rate: number;
payment_load_balance_strategy: string;
payment_product_name_prefix: string;
payment_product_name_suffix: string;
payment_help_image_url: string;
payment_help_text: string;
payment_cancel_rate_limit_enabled: boolean;
payment_cancel_rate_limit_max: number;
payment_cancel_rate_limit_window: number;
payment_cancel_rate_limit_unit: string;
payment_cancel_rate_limit_window_mode: string;
payment_visible_method_alipay_source?: string;
payment_visible_method_wxpay_source?: string;
payment_visible_method_alipay_enabled?: boolean;
payment_visible_method_wxpay_enabled?: boolean;
openai_advanced_scheduler_enabled?: boolean;
// Balance & quota notification
balance_low_notify_enabled: boolean;
balance_low_notify_threshold: number;
balance_low_notify_recharge_url: string;
account_quota_notify_enabled: boolean;
account_quota_notify_emails: NotifyEmailEntry[];
// Channel Monitor feature switch
channel_monitor_enabled: boolean;
channel_monitor_default_interval_seconds: number;
// Available Channels feature switch
available_channels_enabled: boolean;
// Affiliate (邀请返利) feature switch
affiliate_enabled: boolean;
// OpenAI fast/flex policy
openai_fast_policy_settings?: OpenAIFastPolicySettings;
}
export interface UpdateSettingsRequest {
registration_enabled?: boolean;
email_verify_enabled?: boolean;
registration_email_suffix_whitelist?: string[];
promo_code_enabled?: boolean;
password_reset_enabled?: boolean;
frontend_url?: string;
invitation_code_enabled?: boolean;
totp_enabled?: boolean; // TOTP 双因素认证
default_balance?: number;
affiliate_rebate_rate?: number;
affiliate_rebate_freeze_hours?: number;
affiliate_rebate_duration_days?: number;
affiliate_rebate_per_invitee_cap?: number;
default_concurrency?: number;
default_user_rpm_limit?: number;
default_subscriptions?: DefaultSubscriptionSetting[];
auth_source_default_email_balance?: number;
auth_source_default_email_concurrency?: number;
auth_source_default_email_subscriptions?: DefaultSubscriptionSetting[];
auth_source_default_email_grant_on_signup?: boolean;
auth_source_default_email_grant_on_first_bind?: boolean;
auth_source_default_linuxdo_balance?: number;
auth_source_default_linuxdo_concurrency?: number;
auth_source_default_linuxdo_subscriptions?: DefaultSubscriptionSetting[];
auth_source_default_linuxdo_grant_on_signup?: boolean;
auth_source_default_linuxdo_grant_on_first_bind?: boolean;
auth_source_default_oidc_balance?: number;
auth_source_default_oidc_concurrency?: number;
auth_source_default_oidc_subscriptions?: DefaultSubscriptionSetting[];
auth_source_default_oidc_grant_on_signup?: boolean;
auth_source_default_oidc_grant_on_first_bind?: boolean;
auth_source_default_wechat_balance?: number;
auth_source_default_wechat_concurrency?: number;
auth_source_default_wechat_subscriptions?: DefaultSubscriptionSetting[];
auth_source_default_wechat_grant_on_signup?: boolean;
auth_source_default_wechat_grant_on_first_bind?: boolean;
force_email_on_third_party_signup?: boolean;
site_name?: string;
site_logo?: string;
site_subtitle?: string;
api_base_url?: string;
contact_info?: string;
doc_url?: string;
home_content?: string;
hide_ccs_import_button?: boolean;
table_default_page_size?: number;
table_page_size_options?: number[];
backend_mode_enabled?: boolean;
custom_menu_items?: CustomMenuItem[];
custom_endpoints?: CustomEndpoint[];
smtp_host?: string;
smtp_port?: number;
smtp_username?: string;
smtp_password?: string;
smtp_from_email?: string;
smtp_from_name?: string;
smtp_use_tls?: boolean;
turnstile_enabled?: boolean;
turnstile_site_key?: string;
turnstile_secret_key?: string;
linuxdo_connect_enabled?: boolean;
linuxdo_connect_client_id?: string;
linuxdo_connect_client_secret?: string;
linuxdo_connect_redirect_url?: string;
wechat_connect_enabled?: boolean;
wechat_connect_app_id?: string;
wechat_connect_app_secret?: string;
wechat_connect_open_app_id?: string;
wechat_connect_open_app_secret?: string;
wechat_connect_mp_app_id?: string;
wechat_connect_mp_app_secret?: string;
wechat_connect_mobile_app_id?: string;
wechat_connect_mobile_app_secret?: string;
wechat_connect_open_enabled?: boolean;
wechat_connect_mp_enabled?: boolean;
wechat_connect_mobile_enabled?: boolean;
wechat_connect_mode?: string;
wechat_connect_scopes?: string;
wechat_connect_redirect_url?: string;
wechat_connect_frontend_redirect_url?: string;
oidc_connect_enabled?: boolean;
oidc_connect_provider_name?: string;
oidc_connect_client_id?: string;
oidc_connect_client_secret?: string;
oidc_connect_issuer_url?: string;
oidc_connect_discovery_url?: string;
oidc_connect_authorize_url?: string;
oidc_connect_token_url?: string;
oidc_connect_userinfo_url?: string;
oidc_connect_jwks_url?: string;
oidc_connect_scopes?: string;
oidc_connect_redirect_url?: string;
oidc_connect_frontend_redirect_url?: string;
oidc_connect_token_auth_method?: string;
oidc_connect_use_pkce?: boolean;
oidc_connect_validate_id_token?: boolean;
oidc_connect_allowed_signing_algs?: string;
oidc_connect_clock_skew_seconds?: number;
oidc_connect_require_email_verified?: boolean;
oidc_connect_userinfo_email_path?: string;
oidc_connect_userinfo_id_path?: string;
oidc_connect_userinfo_username_path?: string;
enable_model_fallback?: boolean;
fallback_model_anthropic?: string;
fallback_model_openai?: string;
fallback_model_gemini?: string;
fallback_model_antigravity?: string;
enable_identity_patch?: boolean;
identity_patch_prompt?: string;
ops_monitoring_enabled?: boolean;
ops_realtime_monitoring_enabled?: boolean;
ops_query_mode_default?: "auto" | "raw" | "preagg" | string;
ops_metrics_interval_seconds?: number;
min_claude_code_version?: string;
max_claude_code_version?: string;
allow_ungrouped_key_scheduling?: boolean;
enable_fingerprint_unification?: boolean;
enable_metadata_passthrough?: boolean;
enable_cch_signing?: boolean;
// Payment configuration
payment_enabled?: boolean;
payment_min_amount?: number;
payment_max_amount?: number;
payment_daily_limit?: number;
payment_order_timeout_minutes?: number;
payment_max_pending_orders?: number;
payment_enabled_types?: string[];
payment_balance_disabled?: boolean;
payment_balance_recharge_multiplier?: number;
payment_recharge_fee_rate?: number;
payment_load_balance_strategy?: string;
payment_product_name_prefix?: string;
payment_product_name_suffix?: string;
payment_help_image_url?: string;
payment_help_text?: string;
payment_cancel_rate_limit_enabled?: boolean;
payment_cancel_rate_limit_max?: number;
payment_cancel_rate_limit_window?: number;
payment_cancel_rate_limit_unit?: string;
payment_cancel_rate_limit_window_mode?: string;
payment_visible_method_alipay_source?: string;
payment_visible_method_wxpay_source?: string;
payment_visible_method_alipay_enabled?: boolean;
payment_visible_method_wxpay_enabled?: boolean;
openai_advanced_scheduler_enabled?: boolean;
// Balance & quota notification
balance_low_notify_enabled?: boolean;
balance_low_notify_threshold?: number;
balance_low_notify_recharge_url?: string;
account_quota_notify_enabled?: boolean;
account_quota_notify_emails?: NotifyEmailEntry[];
// Channel Monitor feature switch
channel_monitor_enabled?: boolean;
channel_monitor_default_interval_seconds?: number;
// Available Channels feature switch
available_channels_enabled?: boolean;
// Affiliate (邀请返利) feature switch
affiliate_enabled?: boolean;
// OpenAI fast/flex policy
openai_fast_policy_settings?: OpenAIFastPolicySettings;
}
/**
* Get all system settings
* @returns System settings
*/
export async function getSettings(): Promise<SystemSettings> {
const { data } = await apiClient.get<SystemSettings>("/admin/settings");
return data;
}
/**
* Update system settings
* @param settings - Partial settings to update
* @returns Updated settings
*/
export async function updateSettings(
settings: UpdateSettingsRequest,
): Promise<SystemSettings> {
const { data } = await apiClient.put<SystemSettings>(
"/admin/settings",
settings,
);
return data;
}
/**
* Test SMTP connection request
*/
export interface TestSmtpRequest {
smtp_host: string;
smtp_port: number;
smtp_username: string;
smtp_password: string;
smtp_use_tls: boolean;
}
/**
* Test SMTP connection with provided config
* @param config - SMTP configuration to test
* @returns Test result message
*/
export async function testSmtpConnection(
config: TestSmtpRequest,
): Promise<{ message: string }> {
const { data } = await apiClient.post<{ message: string }>(
"/admin/settings/test-smtp",
config,
);
return data;
}
/**
* Send test email request
*/
export interface SendTestEmailRequest {
email: string;
smtp_host: string;
smtp_port: number;
smtp_username: string;
smtp_password: string;
smtp_from_email: string;
smtp_from_name: string;
smtp_use_tls: boolean;
}
/**
* Send test email with provided SMTP config
* @param request - Email address and SMTP config
* @returns Test result message
*/
export async function sendTestEmail(
request: SendTestEmailRequest,
): Promise<{ message: string }> {
const { data } = await apiClient.post<{ message: string }>(
"/admin/settings/send-test-email",
request,
);
return data;
}
/**
* Admin API Key status response
*/
export interface AdminApiKeyStatus {
exists: boolean;
masked_key: string;
}
/**
* Get admin API key status
* @returns Status indicating if key exists and masked version
*/
export async function getAdminApiKey(): Promise<AdminApiKeyStatus> {
const { data } = await apiClient.get<AdminApiKeyStatus>(
"/admin/settings/admin-api-key",
);
return data;
}
/**
* Regenerate admin API key
* @returns The new full API key (only shown once)
*/
export async function regenerateAdminApiKey(): Promise<{ key: string }> {
const { data } = await apiClient.post<{ key: string }>(
"/admin/settings/admin-api-key/regenerate",
);
return data;
}
/**
* Delete admin API key
* @returns Success message
*/
export async function deleteAdminApiKey(): Promise<{ message: string }> {
const { data } = await apiClient.delete<{ message: string }>(
"/admin/settings/admin-api-key",
);
return data;
}
// ==================== Overload Cooldown Settings ====================
/**
* Overload cooldown settings interface (529 handling)
*/
export interface OverloadCooldownSettings {
enabled: boolean;
cooldown_minutes: number;
}
export async function getOverloadCooldownSettings(): Promise<OverloadCooldownSettings> {
const { data } = await apiClient.get<OverloadCooldownSettings>(
"/admin/settings/overload-cooldown",
);
return data;
}
export async function updateOverloadCooldownSettings(
settings: OverloadCooldownSettings,
): Promise<OverloadCooldownSettings> {
const { data } = await apiClient.put<OverloadCooldownSettings>(
"/admin/settings/overload-cooldown",
settings,
);
return data;
}
// ==================== Stream Timeout Settings ====================
/**
* Stream timeout settings interface
*/
export interface StreamTimeoutSettings {
enabled: boolean;
action: "temp_unsched" | "error" | "none";
temp_unsched_minutes: number;
threshold_count: number;
threshold_window_minutes: number;
}
/**
* Get stream timeout settings
* @returns Stream timeout settings
*/
export async function getStreamTimeoutSettings(): Promise<StreamTimeoutSettings> {
const { data } = await apiClient.get<StreamTimeoutSettings>(
"/admin/settings/stream-timeout",
);
return data;
}
/**
* Update stream timeout settings
* @param settings - Stream timeout settings to update
* @returns Updated settings
*/
export async function updateStreamTimeoutSettings(
settings: StreamTimeoutSettings,
): Promise<StreamTimeoutSettings> {
const { data } = await apiClient.put<StreamTimeoutSettings>(
"/admin/settings/stream-timeout",
settings,
);
return data;
}
// ==================== Rectifier Settings ====================
/**
* Rectifier settings interface
*/
export interface RectifierSettings {
enabled: boolean;
thinking_signature_enabled: boolean;
thinking_budget_enabled: boolean;
apikey_signature_enabled: boolean;
apikey_signature_patterns: string[];
}
/**
* Get rectifier settings
* @returns Rectifier settings
*/
export async function getRectifierSettings(): Promise<RectifierSettings> {
const { data } = await apiClient.get<RectifierSettings>(
"/admin/settings/rectifier",
);
return data;
}
/**
* Update rectifier settings
* @param settings - Rectifier settings to update
* @returns Updated settings
*/
export async function updateRectifierSettings(
settings: RectifierSettings,
): Promise<RectifierSettings> {
const { data } = await apiClient.put<RectifierSettings>(
"/admin/settings/rectifier",
settings,
);
return data;
}
// ==================== OpenAI Fast Policy Settings ====================
/**
* OpenAI fast/flex policy rule interface.
* Matches backend dto.OpenAIFastPolicyRule.
*/
export interface OpenAIFastPolicyRule {
service_tier: "all" | "priority" | "flex";
action: "pass" | "filter" | "block";
scope: "all" | "oauth" | "apikey" | "bedrock";
error_message?: string;
model_whitelist?: string[];
fallback_action?: "pass" | "filter" | "block";
fallback_error_message?: string;
}
/**
* OpenAI fast/flex policy settings interface.
*/
export interface OpenAIFastPolicySettings {
rules: OpenAIFastPolicyRule[];
}
// ==================== Beta Policy Settings ====================
/**
* Beta policy rule interface
*/
export interface BetaPolicyRule {
beta_token: string;
action: "pass" | "filter" | "block";
scope: "all" | "oauth" | "apikey" | "bedrock";
error_message?: string;
model_whitelist?: string[];
fallback_action?: "pass" | "filter" | "block";
fallback_error_message?: string;
}
/**
* Beta policy settings interface
*/
export interface BetaPolicySettings {
rules: BetaPolicyRule[];
}
/**
* Get beta policy settings
* @returns Beta policy settings
*/
export async function getBetaPolicySettings(): Promise<BetaPolicySettings> {
const { data } = await apiClient.get<BetaPolicySettings>(
"/admin/settings/beta-policy",
);
return data;
}
/**
* Update beta policy settings
* @param settings - Beta policy settings to update
* @returns Updated settings
*/
export async function updateBetaPolicySettings(
settings: BetaPolicySettings,
): Promise<BetaPolicySettings> {
const { data } = await apiClient.put<BetaPolicySettings>(
"/admin/settings/beta-policy",
settings,
);
return data;
}
// --- Web Search Emulation Config ---
export interface WebSearchProviderConfig {
type: "brave" | "tavily";
api_key: string;
api_key_configured: boolean;
quota_limit: number | null;
subscribed_at: number | null;
quota_used?: number;
proxy_id: number | null;
expires_at: number | null;
}
export interface WebSearchEmulationConfig {
enabled: boolean;
providers: WebSearchProviderConfig[];
}
export interface WebSearchTestResult {
provider: string;
results: { url: string; title: string; snippet: string; page_age?: string }[];
query: string;
}
export async function getWebSearchEmulationConfig(): Promise<WebSearchEmulationConfig> {
const { data } = await apiClient.get<WebSearchEmulationConfig>(
"/admin/settings/web-search-emulation",
);
return data;
}
export async function updateWebSearchEmulationConfig(
config: WebSearchEmulationConfig,
): Promise<WebSearchEmulationConfig> {
const { data } = await apiClient.put<WebSearchEmulationConfig>(
"/admin/settings/web-search-emulation",
config,
);
return data;
}
export async function testWebSearchEmulation(
query: string,
): Promise<WebSearchTestResult> {
const { data } = await apiClient.post<WebSearchTestResult>(
"/admin/settings/web-search-emulation/test",
{ query },
);
return data;
}
export async function resetWebSearchUsage(payload: {
provider_type: string;
}): Promise<void> {
await apiClient.post(
"/admin/settings/web-search-emulation/reset-usage",
payload,
);
}
export const settingsAPI = {
getSettings,
updateSettings,
testSmtpConnection,
sendTestEmail,
getAdminApiKey,
regenerateAdminApiKey,
deleteAdminApiKey,
getOverloadCooldownSettings,
updateOverloadCooldownSettings,
getStreamTimeoutSettings,
updateStreamTimeoutSettings,
getRectifierSettings,
updateRectifierSettings,
getBetaPolicySettings,
updateBetaPolicySettings,
getWebSearchEmulationConfig,
updateWebSearchEmulationConfig,
testWebSearchEmulation,
resetWebSearchUsage,
};
export default settingsAPI;