Commit Graph
234 Commits
Author SHA1 Message Date
DaydreamCodingandClaude Sonnet 5 bdf7ead157 feat(spark-shadow): OpenAI Spark 链接型影子账号
背景:gpt-5.3-codex-spark 使用独立于 codex 全局(5h/7d)的配额窗口(数据源是
/wham/usage 响应体的 codex_bengalfox,而非 codex 全局用的 x-codex-* 响应头),且
只能挂在已完成 OAuth 授权的 OpenAI 账号下复用其登录态,不能作为独立账号单独接入。
为此新增“链接型影子账号”(spark shadow account):影子账号本身不持有任何凭据,
通过 parent_account_id 指向母账号,凭据/token/代理透传自母账号并共享母账号的刷新
周期,仅在配额维度(quota_dimension=spark)和用量窗口上与母账号完全独立调度、互不
连坐。

实现:
- 数据模型:migration 154(+154a)给 accounts 表加 parent_account_id /
  quota_dimension 列 + 4 条约束(维度合法 / parent⟺非 global 维度一致 / 禁自指 /
  FK)+ 2 个 CONCURRENTLY 索引(母账号索引 + 每母账号至多一个影子的唯一索引)。
- 创建:POST /api/v1/admin/accounts/:id/shadow(CreateShadow)—— 一母一影(唯一
  索引兜底并发竞态),继承母账号 proxy/分组/并发/优先级(显式传参可覆盖),默认
  model_mapping 恒等映射到 spark(拒绝非 spark 模型),母账号必须是真实的 OpenAI
  OAuth 账号(非影子)。
- 凭据透传:resolveCredentialAccount 把影子解析回母账号,GetAccessToken / 请求头
  / WS 三条路径统一走此函数;影子自身 Credentials 恒为空(仅允许写 model_mapping),
  凭据写入的汇聚点 persistAccountCredentials 对影子早返 no-op,防止误写。
- 调度:parentHealthyForShadow 只看母账号是否仍是 OpenAI OAuth + 凭据/传输是否
  可用(active、token 未过期、未处于 401/刷新失败/传输故障导致的临时不可调度冷却),
  刻意不看母账号的 global 限流窗口——两条 429 道互不连坐。
- 用量:影子的 codex_5h/7d 走 OpenAIQuotaService.QueryUsage(/wham/usage 的
  codex_bengalfox),与母账号走的 WSv2 探测(/responses 头)完全独立的数据源、
  刷新节流与 staleness 判定。
- 备份:ExportData 显式排除影子账号(影子不持凭据,通用凭据型导入强制
  credentials 非空、无法表达父子链接),按 skipped_shadows 计数提示前端。
- 前端:账号操作菜单新增“创建 Spark 影子”入口,影子行展示回填的母账号信息
  (邮箱 / plan / 隐私模式 / 订阅到期 / chatgpt_account_id),批量操作自动跳过
  影子账号。

说明:migrations 目录用完整文件名(而非纯数字前缀)标识迁移,故本次新增的
154_account_spark_shadow.sql / 154a_..._notx.sql 与已有的
154_add_ops_system_logs_api_key_id.sql 按序号共存,与目录里 145/151 已有的
先例一致。

测试:新增约 20 个测试文件,覆盖 handler(CreateShadow 校验 / 母账号信息回填)、
repository(影子 round-trip / 一母一影唯一索引 / 迁移 schema)、service(凭据
透传三路径 / 调度母健康门 / 用量窗口来源与刷新节流 / CRS 母账号不变量 / 各类
早返与 fail-closed 场景)及前端组件(账号列表 / 操作菜单 / 用量重置)。

验证(镜像 CI;golangci-lint 首次全量分析耗时过长被跳过,其余全部实测):
- gofmt -l:干净
- go build ./... / go vet ./...:通过
- go test ./... -count=1:全绿(全部包 ok,含 internal/service、
  internal/repository、migrations)
- go test -tags integration ./internal/repository/... ./internal/service/...
  (真实 Postgres,testcontainers):全绿,含迁移幂等性
  (TestMigrationsRunner_IsIdempotent_AndSchemaIsUpToDate)与影子相关全部用例
- pnpm lint:check / pnpm typecheck / pnpm build(真实 vite 构建)/
  pnpm vitest run:全绿(124 文件 760 用例)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-01 12:21:45 +08:00
Heatherm Huang 4a7148e203 fix: support grok cli compatibility routes 2026-06-29 17:53:19 +08:00
Heatherm Huang 44f502bab8 fix: address grok review feedback 2026-06-26 14:26:43 +08:00
Heatherm Huang 0d28642181 feat: add grok quota probe parity 2026-06-26 10:37:37 +08:00
Heatherm Huang 39be1ec97f feat: add grok subscription support 2026-06-26 10:36:09 +08:00
Wesley Liddick 16765bde69 Merge pull request #3230 from DaydreamCoding/feat/openai-cyber-policy-passthrough
feat(openai): cyber_policy 硬阻断全链路透传、审计与计费
2026-06-16 16:55:51 +08:00
shaw b816949291 feat(openai-quota): query + reset rate-limit credits for OpenAI accounts
Adds an admin-side action that mirrors the Codex Desktop "rate-limit reset"
flow against chatgpt.com upstream for OpenAI OAuth accounts.

Backend
- OpenAIQuotaService.QueryUsage / ResetCredit hit /wham/usage and
  /wham/rate-limit-reset-credits/consume with the Codex Desktop header set,
  reusing OpenAITokenProvider for refreshed tokens and PrivacyClientFactory
  for the impersonated Chrome TLS fingerprint.
- Honors the account's configured proxy by reading the eager-loaded
  account.Proxy directly (falls back to proxyRepo only when missing).
- GET /api/v1/admin/openai/accounts/:id/quota
  POST /api/v1/admin/openai/accounts/:id/reset-quota
- Wire DI for the new service + handler dependency.

Frontend
- OpenAIQuotaResetCell renders a single action row in AccountUsageCell's
  OpenAI section: the existing local "查询" (active sampling) is injected
  via #pre-actions, alongside a "次数 N" button that doubles as the
  upstream query trigger and the available-credit indicator, and a "重置"
  button that consumes one credit.
- No duplicate 5h/7d window display; the local UsageProgressBar owns those
  bars to avoid confusion.
2026-06-16 16:55:07 +08:00
DaydreamCodingandClaude Opus 4.8 b62b573f7f feat(openai): cyber_policy 硬阻断全链路透传、审计与计费
上游对单次请求下发 error.code=cyber_policy 硬阻断时,网关在所有端点
(/v1/responses、/v1/chat/completions、/v1/messages、WebSocket)及流式/
非流式路径下,将该结果原样透传给客户端,绝不 failover、换号或同步拦截;
命中后异步完成审计与计费:

- 风控中心记录 cyber_policy 留痕并发送通知邮件,落库先于发信,SMTP 阻塞
  不影响留痕
- ops 错误请求记录,状态码对齐客户端实际接收(流式 200 / 非流式 400)
- 用量明细标记 request_type=cyber,按上游真实 token 计费,HTTP 与
  WebSocket 计费口径统一,零 token 命中不误扣
- 会话级自动屏蔽(管理员开关,默认关):命中的会话在可配 TTL 内本地拦截
  不再发往上游,仅屏蔽该会话不影响同 Key 其他会话
- 封号计数排除开关:可选让 cyber 命中不计入自动封号,命中当次不判定且
  历史行在违规计数中一并排除

WebSocket 多轮连接下 cyber 标记按 turn 生命周期管理,逐轮独立检测与记录;
透传的错误响应不被兜底逻辑追加内容污染。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 01:47:01 +08:00
shaw 0acf00c4a1 Add admin compliance acknowledgement gate 2026-06-10 14:16:51 +08:00
DaydreamCoding af19d44327 feat(proxies): 代理有效期与失败回退
- schema/迁移: 代理有效期、提醒天数、失败回退配置 + 账号 fallback 来源字段
- service/repo/DTO/handler: CRUD 透传新字段 + 校验
- fallback 目标解析纯函数(链式解析 + 环检测 + 兜底)
- SweepExpiredProxies 到期改投账号 + outbox 失效
- ProxyExpiryService 后台到期扫描任务 + wire 注册
- 账号侧手动回切原代理 + fallback 来源徽章/按钮
- 前端: 创建/编辑表单、列表到期徽章、类型/API/i18n
- ops 告警: proxy_expired_count / proxy_expiring_soon_count 指标
- 导入导出携带有效期/回退字段(备用按 name 映射)
- 补全测试 stub + 集成测试 + review 问题修复
2026-06-08 00:01:30 +08:00
visa2andClaude Opus 4.8 362f9e77bf fix(service): gate periodic background jobs with a leader lock for multi-instance
Three periodic background jobs ran on every instance with no cross-instance
coordination, multiplying their cost (and side effects) by the replica count:

- DashboardAggregationService.runScheduledAggregation: N× heavy GROUP BY
  aggregation queries every minute plus watermark write races.
- PaymentOrderExpiryService.runOnce: N× upstream payment-provider reconcile/
  expiry API calls per pending order.
- SubscriptionExpiryService.sendExpiryReminders: N× full active-subscription
  scans every minute and potential duplicate reminder emails.

Add a LeaderLockCache abstraction so only one instance runs each job per cycle:

- The interface lives in the service layer; the Redis-backed implementation
  (SetNX + compare-and-delete release) lives in the repository layer, so the
  service package keeps its depguard "must not import redis" boundary intact.
- tryAcquireSingletonLeaderLock prefers the cache and falls back to a Postgres
  advisory lock when Redis errors, mirroring the Ops background services. When
  neither backend is configured the job runs ungated, preserving single-instance
  and test behavior (no self-lockout: the lock is released every cycle).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 22:47:24 +08:00
DaydreamCodingandClaude Opus 4.8 cfb195c7b2 feat(usage): 记录并展示失败请求(用户端+管理端)
- 记录失败请求并在用户端/管理端展示;分类下拉改用统一 Select 组件
- 模型过滤改后端 ILIKE 模糊匹配;新增「Key 名称」列(含已删除标记)与按 Key 过滤;时间列移至末列
- 用户可见「已删除 key 失败请求」:OpsErrorLogFilter 加 MatchDeletedKeyOwner,用户侧归属
  放宽为 (user_id OR deleted_key_owner_user_id),让 key 原所有者能看到删除 key 后继续请求
  导致的认证失败记录(他人仍 NotFound,不泄露存在性)
- 迁移 148:ops_error_logs 用户+时间索引

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 14:00:57 +08:00
Wesley Liddick 418d09be56 Merge pull request #2881 from xiaoyiluck666/fix/openai-oauth-refresh-enrichment
修复 OpenAI OAuth 刷新未补全账号信息
2026-06-01 10:04:16 +08:00
DaydreamCodingandClaude Opus 4.8 f7f5e33830 feat(quota): user×platform 配额 DB 写聚合 flusher
Redis 同步权威 + DB 镜像,不在进程内维护 delta:
- 写入点 HasUserPlatformQuotaLimit 守卫:无 limit 跳过 Redis 写与持久化
- 累加 usage 的 Lua 在 flusher_enabled 时 SADD 脏集 billing:upq:dirty
- UserPlatformQuotaUsageFlusher 定时 SPOP 脏集 → 批量 HGETALL 读当前窗口 usage 快照
  → BatchSnapshotUsage 绝对值 UPSERT 覆盖 DB(去 SELECT FOR UPDATE 行锁)
  → 失败 SADD 回 / FK(23503)整批丢弃
- flusher 单批 clamp 到 ≤6000,保证一次 flush 只生成一条 UPSERT(单事务原子)
- flusher_enabled 默认 false(降级=旧异步直写 DB)

效果:DB 写连接从 O(QPS) 收敛到 O(副本)。
循环依赖:service 层独立 Snapshot/FK 类型,repository adapter 转换 + %w 映射 FK error。
admin reset/upsert 后失效 cache(脏残留被 flusher 当 MISS 跳过)。

健壮性与可观测性:
- flusher_enabled=false 时 Start 不注册定时器;flush_interval_ms 非法回退 2s
- Readd 回填失败单独计 dirty_lost(不再误记 dirty_readd)并 ALERT;脏集 Readd 补兜底 TTL
- 单 tick 达 max batches 上限仍有积压时记 log
- admin 失效 cache 失败升级为 ALERT(提示 enforcement 可能延迟至 sentinel TTL)
- BatchGet 单条命令失败 / usage 字段损坏均记 log,避免静默以 0 覆写 DB

三态 go vet + 单测/集成测全绿。
已知取舍(默认 flusher_enabled=false 不触发):
- FK 整批丢弃牵连同批正常 key(活跃 key 靠下次 SADD+绝对值快照自愈;Redis 仍权威)
- admin reset/upsert 直写 DB 与 flusher 异步刷存在覆盖竞态:flusher 持旧快照在途时可能覆盖
  admin 刚写值(limit 列不受影响;usage 有 preflight windowExpired 兜底;低频)。彻底消除需 version OCC。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 17:19:15 +08:00
wucm667andClaude Opus 4.7 c9caadb378 fix(account): address second-round review on quota auto-pause
- TopK initial filter now drops quota-paused accounts: fold the quota check
  into isAccountRequestCompatible so session-hash, TopK pool, and per-candidate
  rechecks all skip paused accounts. Previously the candidate pool was built
  without the quota check, so paused accounts could fill TopK and leave the
  scheduler returning "no available accounts" even with healthy ones available.
- Add per-account explicit disable flags auto_pause_5h_disabled /
  auto_pause_7d_disabled with toggles in EditAccountModal. Without these,
  leaving the account threshold blank silently falls back to the global default,
  so admins could not exempt a single account once a global default existed.
  Disable is per-window: an account can opt out of 5h auto-pause while still
  honoring 7d. Schedule snapshot whitelist includes the new fields, i18n EN/ZH
  updated, threshold-hint text revised to explain "blank = global default".
- Move quota auto-pause settings off the request hot path: replace the per-repo
  TTL+singleflight sync DB read with a per-SettingService stale-while-revalidate
  in-memory snapshot. Get is non-blocking (atomic.Pointer load + async refresh
  on staleness); writes via UpdateOpsAdvancedSettings push directly into the
  cache through an injected sink; wire warms the cache at startup. Adds Warm
  (sync) for tests/init and SetOpenAIQuotaAutoPauseSettings (sink target).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 14:32:45 +08:00
xiaoyiluck666 eba2046320 fix: enrich OpenAI OAuth token refresh 2026-05-29 14:01:39 +08:00
DaydreamCodingandClaude Opus 4.7 6b39b344d8 feat(quota): 用户 × 平台 USD 配额
为用户在 anthropic/openai/gemini/antigravity 四个平台上提供日/周/月
三个窗口的 USD 配额管控。配额语义:未设置=不限制,0=禁用,>0=美元上限。

两层模型:
- 配置层:系统默认配额,以及 email/linuxdo/oidc/wechat/github/google/
  dingtalk 七个鉴权来源的默认配额,存于 settings,以嵌套 JSON 整体读写
  (系统 1 个 key + 每个来源 1 个 key),整体替换语义。
- 运行时层:user_platform_quota 表按用户记录实际配额,与配置层解耦。

后端:新增 ent schema 与 140_user_platform_quotas.sql 迁移、repository
与 service 端口、计费链路集成、管理端与用户端读写接口。
前端:管理端设置页配额编辑、用户配额管理 Modal、用户 Dashboard 展示、
中英文案。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 10:49:20 +08:00
shaw 1e406fed52 fix: optimize OpenAI account cooldown scheduling 2026-05-23 10:18:43 +08:00
shaw a613a587ba feat: add subscription expiry email toggle 2026-05-21 14:27:50 +08:00
Wesley Liddick 378a0a6a61 Merge pull request #2599 from Arron196/feature/email-template-editor
feat: 添加邮件模板编辑器与通知邮件模板化
2026-05-20 15:12:57 +08:00
benjaminandSisyphus 8cef9a7ab1 chore(wire): 注入通知邮件服务
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-20 11:09:18 +08:00
wucm667 92ad68a314 feat(channels): 模型定价支持一键同步最新模型
从 LiteLLM 定价目录中读取指定平台的最新模型列表,
将尚未录入的模型以新定价条目(价格留空)的形式追加,
管理员只需点击同步最新模型按钮即可完成操作。

- backend/service: PricingService 新增 ListModelNamesByProvider
- backend/handler: ChannelHandler 新增 SyncPricingModels (GET /api/v1/admin/channels/pricing/sync-models)
- backend/routes: 注册新路由(在 /:id 通配符之前)
- backend/wire_gen: 手动更新 NewChannelHandler 调用
- frontend/api: channels.ts 新增 syncPricingModels
- frontend/i18n: zh.ts / en.ts 新增 5 个 key
- frontend/view: ChannelsView 定价区域标题行新增「同步最新模型」按钮
- tests: pricing_service_test + channel_handler_test 新增单元测试
2026-05-19 20:32:32 +08:00
DaydreamCodingandClaude Opus 4.7 b19da9c7fe feat(dingtalk): 钉钉 OAuth 登录接入与 internal_only 用户属性同步
⚠️ 应用类型约束:当前实现仅支持「钉钉登录-企业内部应用」(DingTalk 开放平台
internal_app 类型)。第三方个人应用、第三方企业应用类型暂不支持——OAuth 流程
相同但 corp 校验、跨企业行为不同。backend 通过 DingTalkAppKind 校验对非
internal_app 类型 fail-closed(硬约束)。

钉钉 OAuth 登录主链
- 4 步 OAuth 链:ExchangeCodeForUserToken / GetUnionIdByUserToken /
  GetUserIdByUnionId / GetStaffInfoByUserId;app token 缓存
- pending session 机制持久化 OAuth 中间态;cookie-only token 持久化
- 三种分流:bind_login_required / email_completion / choose_account_action
- corp_restriction_policy 支持 none + internal_only;stale "whitelist" 在
  加载层与写入层均静默 coerce 为 none + slog.Warn
- bypass_registration 开关:企业内部模式豁免全局 REGISTRATION_DISABLED
- isReservedEmail / signup_source / canUnbindProvider / OAuth pending flow
  等横切点支持 dingtalk provider
- migration 136:4 表 CHECK 约束加入 'dingtalk' provider 值

internal_only 模式同步企业邮箱/姓名/部门到用户属性
- SyncCorpEmail / SyncDisplayName / SyncDept 三个独立开关 + 对应
  SyncXxxAttrKey 目标属性 key(默认 dingtalk_email / dingtalk_name /
  dingtalk_department);非 internal_only policy 在写入层与加载层均
  coerce 为 false,admin handler 与 setting_service 双层兜底
- 同步语义:首次注册写 users.username(昵称优先 → 企业姓名 fallback),
  之后每次登录刷新 3 个属性;空值也写入以覆盖旧值
- 邮箱三级 fallback:org_email > email > extension["企业邮箱"]
  (钉钉自定义字段 JSON)
- 部门路径递归向上拼接,跳过 dept_id=1 选首个真实子部门,剥离根组织名
- GetUnionIdByUserToken 同时返回 OIDC /contact/users/me 的 nick 字段;
  新增 GetDeptInfo 调用 OAPI /topapi/v2/department/get
- AuthHandler 注入 UserAttributeService;OAuth pending flow 在
  createPendingOAuthAccount / bindPendingOAuthLogin 分别派发到
  AfterRegistration(syncUsername=true)/ AfterLogin
- migration 137 seed dingtalk_email/name/department 三个用户属性定义

附带修复(同集成路径暴露的两个 OAuth 注册回归)
- LoginOrRegisterOAuthWithTokenPair 新建用户分支用 inferLegacySignupSource
  覆写 caller 显式传入的 signupSource,导致 dingtalk/linuxdo/oidc/wechat
  渠道授权按 email 渠道读取;改为只在 caller 未显式传入时回退邮箱推断
- mergeProviderDefaultGrantSettings 把 parse fallback 默认值
  (Concurrency=5 / Balance=0) 当作"未配置"哨兵,admin 显式设 5 时被误判
  退回全局默认(复现:全局默认 1 + 渠道默认并发 5 + grant_on_signup → 新
  用户实际 concurrency=1);去掉哨兵,admin 任何 >=0 值都覆盖 globalDefaults

前端
- DingTalk Login / Callback / EmailCompletion / ChoiceAccount / Error
  视图;router + auth API client
- admin SettingsView:corp policy radio(none / internal_only)+ bypass
  注册开关 + i18n;internal_only 下展示三同步开关 + 目标 attr key 下拉
  (拉取 user attribute definitions),展示 fieldEmail /
  qyapi_get_department_list 钉钉权限申请提示
- Profile:S1 主动绑定 / S5 解绑钉钉按钮 + 合成邮箱防自锁

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 15:27:47 +08:00
Wesley Liddick d52da45363 Merge pull request #2202 from Michael-Jetson/main
新增三大功能:兑换码邀请返利、批量修改用户并发数、Markdown页面渲染
2026-05-07 09:35:14 +08:00
shaw fff4a300c6 feat(risk-control): add content moderation audit 2026-05-07 09:14:47 +08:00
Michael-JetsonandClaude Opus 4.6 4cbd4932a0 feat: add redeem code affiliate rebate, batch concurrency API, and markdown page rendering
1. Redeem code affiliate rebate: balance-type redeem codes now trigger
   invite rebate for the inviter. Payment fulfillment uses context key
   to prevent double-rebate.

2. Batch concurrency update: new POST /admin/users/batch-concurrency
   endpoint supporting mode=set/add with all=true for all users.

3. Markdown page rendering: new GET /api/v1/pages/:slug API serves local
   .md files. Custom menu items with url="md:slug" render markdown with
   collapsible TOC sidebar, scroll spy, and copy buttons on code blocks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-05 06:44:37 -07:00
erio c4598aa9b6 fix(ops-cleanup): 让 UI 数据保留策略真正生效
UI 上 admin 改的数据保留策略(cron + retention 天数)此前只写入 settings 表的
ops_advanced_settings.data_retention,但 OpsCleanupService 启动时只读
cfg.Ops.Cleanup(config.yaml / 环境变量),从未读取 settings 表,导致 UI 配置
完全不生效——cron 实际仍按默认 0 2 * * * 每日跑、retention 30 天。

改动:
- OpsCleanupService 增加 settingRepo 依赖,新增 effective 配置 + Reload 方法。
  Start/Reload 时从 settings.ops_advanced_settings.data_retention 覆盖
  cfg.Ops.Cleanup(Enabled、Schedule、*RetentionDays),无 settings 时整体
  fallback 到 cfg。runScheduled 顶部刷新一次 effective,让 retention 改动当次
  即生效(schedule/enabled 改动需要 Reload 才换 cron)。
- 用 mu + started/stopped 替换 startOnce/stopOnce 以支持 Reload 重建 cron。
- OpsService 增加 CleanupReloader 接口与 SetCleanupReloader setter;
  UpdateOpsAdvancedSettings 写入后调用 Reload。
- wire 通过 setter 注入 cleanup hook,避免构造期循环依赖。
- 新增单测覆盖 overlay 五种情形 + Update 触发 Reload。
2026-05-04 12:43:15 +08:00
Wesley Liddick 17ced6b73a Merge pull request #2027 from hansnow/codex/fix-api-key-rate-limit-reset
fix(api-key): reset rate limit usage cache
2026-04-29 21:27:52 +08:00
Wesley Liddick 63ef23108c Merge pull request #1977 from sholiverlee/vertex
feat: 支持 Vertex Service Account(Anthropic / Gemini)
2026-04-29 15:48:26 +08:00
DaydreamCodingandClaude Opus 4.7 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
hansnow 53f919f8f0 fix(api-key): reset rate limit usage cache 2026-04-27 16:47:44 +08:00
Oliver 6d11f9ed77 Add Vertex service account support 2026-04-25 20:39:58 -04:00
shaw 4e1bb2b445 feat(affiliate): add feature toggle and per-user custom invite settings
- 在系统设置「功能开关」中新增邀请返利总开关,默认关闭;
  关闭态:菜单隐藏、注册忽略 aff、新充值不返利,但已有 quota 仍可转余额
- 支持管理员为指定用户设置专属邀请码(覆盖随机码,全局唯一)
- 支持管理员为指定用户设置专属返利比例(覆盖全局比例,可单条/批量调整)
- 在系统设置邀请返利卡片内嵌入专属用户管理表格(搜索/编辑/批量/删除),
  删除采用项目通用 ConfirmDialog,会同时清除专属比例并把邀请码重置为系统随机码
- /affiliate 用户页新增「我的返利比例」卡片与动态使用说明,让用户直观看到
  分享后能拿到多少(同源 resolveRebateRatePercent 计算,与实际充值一致)
- 新增数据库迁移 132 添加 aff_rebate_rate_percent 与 aff_code_custom 列
- 新增 admin 路由组 /api/v1/admin/affiliates/users/* 共 5 个端点
- AffiliateService 改为只依赖 *SettingService,去除冗余的 SettingRepository
- 邀请码格式校验放宽到 [A-Z0-9_-]{4,32},兼容旧 12 位系统码与新自定义码
- 补充单元测试与集成测试覆盖新方法、冲突路径与边界值
2026-04-25 20:22:07 +08:00
shaw aa8ee33b0a refactor(affiliate): tighten DI and harden inviter code validation
- Drop SetAffiliateService setters and ProvideAuthService /
  ProvidePaymentService / ProvideUserHandler wrappers in favor of direct
  Wire constructor injection. AffiliateService has no back-edge to
  Auth/Payment/User, so the indirection was never required.
- Change RegisterWithVerification's variadic affiliateCode to a fixed
  parameter; adjust all call sites.
- Validate aff_code length and charset in BindInviterByCode before any
  DB lookup, eliminating timing-side-channel and useless DB roundtrips
  on malformed input.
- Make affiliate cache invalidation synchronous; surface Redis errors
  via the project logger instead of swallowing them in a detached
  goroutine.
- Add an integration test guarding cross-layer tx propagation in
  AccrueQuota and a unit test pinning the aff_code format rules.
2026-04-25 08:44:18 +08:00
VpSanta33 f03de00cb9 feat: add affiliate invite rebate flow and admin rebate-rate setting 2026-04-24 22:22:26 +08:00
erio 5e060b2222 Merge remote-tracking branch 'upstream/main' into feat/channel-insights
# Conflicts:
#	backend/cmd/server/wire_gen.go
2026-04-23 22:30:45 +08:00
erio a3ea8ecac5 fix(wire): add ChannelMonitorRunner.Stop() to cleanup steps in wire_gen.go 2026-04-23 21:06:51 +08:00
erio 748a84d871 sync: bring over remaining release/custom-0.1.115 changes
- Extract PublicSettingsInjectionPayload named struct with drift test
- Add channel_monitor_default_interval_seconds to SSR injection
- Add image_output_price to SupportedModelChip
- Simplify AppSidebar buildSelfNavItems (admins see available channels)
- Add gateway WARN logs for 503 no-available-accounts branches
- Wire ChannelMonitorRunner into provideCleanup for graceful shutdown
- Add migrations 130/131 (CC template userid fix + mimicry field cleanup)
- Clean up fork-only features (sora, claude max simulation, client affinity)
- Remove ~320 obsolete i18n keys
- Add codexUsage utility, WechatServiceButton, BulkEditAccountModal
- Tidy go.sum
2026-04-23 20:55:18 +08:00
james-6-23 dc5d42addc feat(rpm): RPM 限流模块优化
P0:
- rpm_override 嵌入 Auth Cache Snapshot,消除每请求 DB 查询 (snapshot v6→v7)
- 429 RPM 响应返回 Retry-After 头(当前分钟剩余秒数)

P1:
- ClearAll 按钮直连 DELETE API,带 loading 防重复
- 新增 GET /admin/users/:id/rpm-status 管理员 RPM 用量查询端点

优化:
- checkRPM 从级联互斥改为并行取最严,user.rpm_limit 作为全局硬上限始终生效
- Override/Group 变更后自动失效 auth cache
- fail-open 语义不变,Redis 故障不阻塞业务
2026-04-23 16:34:37 +08:00
wx-11 11cf23da7d 修改403逻辑: 先临时冷却,再根据连续次数决定是否判坏号 2026-04-23 12:58:13 +08:00
erio 6cd7c60549 fix(channels): supported models = mapping ∪ pricing with global LiteLLM fallback
Why: channels with model pricing entries but no model mapping (e.g. azcc with
3 priced claude models, no mapping) were rendering as 未配置模型 in the
'Available Channels' page. The algorithm only iterated ModelMapping and
silently dropped any platform without a mapping entry.

Changes:
- channel.go: SupportedModels now unions mapping + pricing entries.
  For exact mapping src → target, pricing is looked up by target (the actually
  billed name), not by src.
- channel_available.go: ListAvailable enriches each entry with nil pricing
  via PricingService.GetModelPricing (global LiteLLM fallback) so the popover
  always shows a price.
- channel_service.go: NewChannelService takes *PricingService as 4th param.
- channel_test.go: rewrote 4 tests that froze the old mapping-only semantics;
  added pricing-only / mapping-target / target-missing coverage.
2026-04-23 00:45:10 +08:00
erio 9ba42aa556 feat(channels): gate available channels behind feature switch (backend)
Add a DB-backed soft switch "available_channels_enabled" controlling
the user-facing /channels/available endpoint and sidebar entry. Default
to false (opt-in) — the feature stays invisible until an admin enables
it under Admin Settings > Features.

- domain_constants: SettingKeyAvailableChannelsEnabled
- settings_view: AllSettings/PublicSettings + AvailableChannelsEnabled
- setting_service: public+all read/write, seed default "false",
  GetAvailableChannelsRuntime helper (fail-closed on read error)
- admin setting_handler: UpdateSettingsRequest *bool + update branch
  + audit diff entry
- public setting_handler: expose via GET /api/v1/settings
- available_channel_handler: featureEnabled() guard — returns empty
  list after auth when disabled (401 precedes the feature check to
  preserve existing behavior)
2026-04-21 17:23:20 +08:00
erio 59290e39f9 chore(channels): drop admin-side available channels view
Remove the admin-side "Available Channels" aggregate view — admins
already see full channel configuration (groups, pricing, model
mappings) in the channel edit dialog, making a read-only admin
aggregate view redundant. The user-side "可用渠道" remains.

Backend:
- Delete handler/admin/available_channel_handler.go (+ test)
- Drop AdminHandlers.AvailableChannel field and wire injection
- Remove /admin/channels/available route

Frontend:
- Delete views/admin/AvailableChannelsView.vue
- Drop /admin/available-channels router entry
- Strip AvailableChannel types + listAvailable from api/admin/channels.ts
2026-04-21 17:18:37 +08:00
IanShaw027 dcd5c43da4 feat: complete email binding and pending oauth verification flows 2026-04-21 10:00:06 +08:00
erio 654cfb6480 feat(channels): add "Available Channels" aggregate view
Add a read-only aggregate view per channel: its linked groups and a
deterministic wildcard-free supported-model list with pricing details.

Backend
- service.Channel.SupportedModels(): combine ModelMapping keys with
  same-platform ModelPricing.Models; trailing "*" keys expand via
  pricing prefix match; platforms without a mapping produce no
  entries (intentional "no mapping = not shown" rule).
- Extract splitWildcardSuffix() shared with toModelEntry.
- Build a per-call pricing lookup map (platform+lowerName -> *pricing)
  to avoid O(N*M) scans in SupportedModels.
- ChannelService.ListAvailable() aggregates channels + active groups;
  filters out group IDs no longer active.
- Admin route GET /api/v1/admin/channels/available returns the full
  DTO (id, status, billing_model_source, restrict_models, groups,
  supported_models).
- User route GET /api/v1/channels/available applies three filters:
  Status==active, visible-group intersection, and platform filter
  on supported_models (prevents cross-platform leak when a channel
  links to both a user-accessible group and an inaccessible one on
  another platform). Response is a plain array (matches the
  /groups/available sibling shape). Field whitelist omits
  billing_model_source, restrict_models, ids, status, sort_order.

Frontend
- New /admin/available-channels and /available-channels views backed
  by a shared AvailableChannelsTable component (admin adds status +
  billing-source columns via slots).
- PricingRow extracted to its own SFC; SupportedModelChip references
  shared billing-mode constants in constants/channel.ts.
- Sidebar: new entry above "渠道管理" for admin; matching entry in
  user nav.
- i18n: zh + en coverage for both namespaces.

Tests
- SupportedModels: wildcard-only pricing skipped, prefix-matches-
  nothing, cross-platform bleed, case-insensitive dedup, empty
  platform mapping.
- ListAvailable: nil groupRepo, inactive-group-ID dropped, stable
  case-insensitive name sort.
- User handler: 401 on unauthenticated, visible-group intersection,
  platform filter on supported_models, JSON whitelist.
- Admin handler: full DTO including default BillingModelSource
  fallback.

Refs: issue #1729
2026-04-21 00:27:10 +08:00
erio a296425994 feat(channel-monitor): request templates with snapshot apply + headers/body override
Problem:
Upstream channels can reject monitor probes based on client fingerprint
(e.g. "only Claude Code clients allowed"). The monitor had no way to
customize the outgoing request to bypass such restrictions.

Solution:
Introduce reusable request templates that carry extra_headers plus an
optional body override; monitors reference a template and receive a
snapshot copy on apply. Template edits do NOT auto-propagate — users
must click "apply to associated monitors" to refresh snapshots, so a
bad template edit cannot instantly break all production monitors.

Data model (migration 112):
- channel_monitor_request_templates: id, name, provider, description,
  extra_headers jsonb, body_override_mode ('off'|'merge'|'replace'),
  body_override jsonb. Unique (provider, name).
- channel_monitors: +template_id (FK, ON DELETE SET NULL), +extra_headers,
  +body_override_mode, +body_override (the three runtime snapshot fields).

Checker (channel_monitor_checker.go):
- callProvider + runCheckForModel accept a CheckOptions carrying the
  snapshot fields. mergeHeaders applies user headers on top of adapter
  defaults (forbidden list: Host / Content-Length / Transfer-Encoding /
  Connection / Content-Encoding).
- buildRequestBody:
    off     -> adapter default body
    merge   -> shallow-merge over default; per-provider deny list
               (model/messages/contents) protects the challenge contract
    replace -> user body verbatim
- Replace mode skips challenge validation; instead HTTP 2xx + non-empty
  extracted response text = operational, empty = failed.
- 4 new unit tests cover all three modes + replace/empty-response case.

Admin API:
- /admin/channel-monitor-templates CRUD + /:id/apply (overwrite snapshot
  on all template_id=id monitors, returns affected count).
- channel_monitor request/response DTOs gain the 4 new fields.

Frontend:
- channelMonitorTemplate.ts API client.
- MonitorAdvancedRequestConfig.vue shared component for headers textarea
  + body mode radio + body JSON editor; used by both template and monitor
  forms.
- MonitorTemplateManagerDialog.vue: provider tabs, list/create/edit/
  delete/apply, live "associated monitors" count per row.
- MonitorFiltersBar: new 模板管理 button next to 新增监控.
- MonitorFormDialog: collapsible 高级 section with template dropdown
  (filtered by form.provider, clears on provider change) + embedded
  AdvancedRequestConfig. Picking a template copies its fields into the
  form (snapshot semantics mirrored on the client).
- i18n zh/en entries for all new copy.

chore: bump version to 0.1.114.32
2026-04-21 14:14:49 +08:00
erio 8cf83c984e feat(channel-monitor): aggregate history to daily rollups + soft delete
明细只保留 1 天,超过 1 天聚合到新表 channel_monitor_daily_rollups(按
monitor_id/model/bucket_date 维度),聚合保留 30 天。两张表都用 SoftDeleteMixin
软删除(DELETE 自动改为 UPDATE deleted_at = NOW())。

聚合 + 清理任务由 OpsCleanupService 的 cron 统一调度,与运维监控的清理共享
schedule(默认 0 2 * * *)和 leader lock。ChannelMonitorRunner 的 cleanupLoop
被移除,只保留 dueCheckLoop。

读取路径 ComputeAvailability* 改为 UNION 明细(今天 deleted_at IS NULL)+
聚合(过去 windowDays 天 deleted_at IS NULL),SUM(ok)/SUM(total) 自然加权
计算可用率,AVG latency 用 SUM(sum_latency_ms)/SUM(count_latency)。

watermark 表 channel_monitor_aggregation_watermark 单行(id=1),记录
last_aggregated_date,重启后从该日期 +1 继续聚合,首次为 nil 则从
today - 30d 开始回填,单次最多 35 天上限避免长事务。

raw SQL 的 ListLatestPerModel / ListLatestForMonitorIDs / ListRecentHistoryForMonitors
都补上 deleted_at IS NULL 过滤(SoftDeleteMixin interceptor 只对 ent query 生效)。

bump version to 0.1.114.28

GroupBadge 在 MonitorKeyPickerDialog 中复用平台主题色 + 倍率/专属倍率
(顺手优化)。
2026-04-21 10:10:56 +08:00
erio 7da5124067 feat(channel-monitor): add feature switch settings + fix extra_models save
Settings:
- New "功能开关" tab between 通用设置 and 安全与认证
- ChannelMonitorEnabled toggle: runner skips scheduling when false,
  user-facing list returns empty
- ChannelMonitorDefaultIntervalSeconds (15-3600): pre-fills interval
  when creating a new monitor; each monitor can still override

Bug fix:
- ModelTagInput now commits pending input on blur, not just Enter/Tab.
  Previously clicking "save" with an un-Enter'd extra model would drop
  the value (DB stored extra_models=[] even when user typed entries).

Backend:
- domain_constants: SettingKeyChannelMonitor{Enabled,DefaultIntervalSeconds}
- SettingService.GetChannelMonitorRuntime: lightweight getter used by
  runner tick + user handler per-request (fail-open on DB error)
- Runner tickDueChecks: bails early when feature disabled
- ChannelMonitorUserHandler: checks feature flag before serving
- Comment on runner doc: scheduler state is implicit (every tick re-reads
  ListEnabled from DB), so CRUD ops on monitors self-maintain the schedule

Bump VERSION to 0.1.114.25
2026-04-21 00:21:29 +08:00
erio 20a4e41872 feat(monitor): admin channel monitor MVP with SSRF protection and batch aggregation
新增 admin「渠道监控」模块(参考 BingZi-233/check-cx),独立于现有 Channel 体系。
admin 配置 + 后台定时调用上游 LLM chat completions 健康检查 + 所有登录用户只读可见。

后端:
- ent: channel_monitor + channel_monitor_history(AES-256-GCM 加密 api_key)
- service 按职责拆分:service/aggregator/validate/checker/runner/ssrf
- provider strategy map 替代 switch(openai/anthropic/gemini)
- repository batch 聚合(ListLatestForMonitorIDs + ComputeAvailabilityForMonitors)消除 N+1
- runner: ticker(5s) + pond worker pool(5) + inFlight 防并发 + TrySubmit 防雪崩
  + 凌晨 3 点 cron 清理 30 天历史
- SSRF 防护:强制 https + 私网/loopback/云元数据 IP 拒绝(127/8、10/8、172.16/12、
  192.168/16、169.254/16、100.64/10、::1、fc00::/7、fe80::/10)+ DialContext
  在 socket 层防 DNS rebinding
- API key sanitize:擦除 url.Error 与上游响应 body 中的 sk-/sk-ant-/AIza/JWT 模式
- APIKeyDecryptFailed 标志位 + 单 monitor 路径检测,避免空 key 调用上游

handler:
- admin: CRUD + 手动触发 + 历史接口(api_key 脱敏)
- user: 只读列表 + 状态详情(去除 api_key/endpoint)
- ParseChannelMonitorID 共用 + dto.ChannelMonitorExtraModelStatus 共用

前端:
- 路由 /admin/channels/{pricing,monitor} + /monitor(用户只读)
- AppSidebar 父项 expandOnly 支持
- ChannelMonitorView 拆为 8 个子组件 + ChannelStatusView 拆出 detail dialog
- composables/useChannelMonitorFormat + constants/channelMonitor 共享
- i18n monitorCommon namespace 消除 admin/user 两 view 重复

合规:所有文件符合 CLAUDE.md(Go ≤ 500 行 / Vue ≤ 300 行 / 函数 ≤ 30 行)
CI: go build / gofmt / golangci-lint(0 issues) / make test-unit / pnpm build 全绿
2026-04-20 20:21:02 +08:00
erio 3d2027227b fix: update wire_gen.go to use ProvideSchedulerCache with config injection
wire_gen.go was calling NewSchedulerCache(redisClient) but wire.go had
been updated to register ProvideSchedulerCache(redisClient, config),
which reads SnapshotMGetChunkSize and SnapshotWriteChunkSize from config.
Without this fix, those config values were silently ignored.
2026-04-14 20:22:45 +08:00