906 Commits

Author SHA1 Message Date
shaw
53acde1efd style: fix lint errors in response.failed SSE writer
Errcheck flagged three unchecked strings.Builder.WriteString calls and
gofmt rejected over-aligned trailing comment in the route table.

Rewrite writeResponsesFailedSSE with json.Marshal on typed structs
instead of Builder+strconv.Quote. Same wire format, but:
- no unchecked Write returns to silence
- strict JSON escaping (strconv.Quote emits \a and \v which are not
  valid JSON; Marshal handles all runes correctly)
- omitempty model field via struct tag instead of conditional Builder
- consistent with the json.Marshal style used elsewhere in handler/

Collapse trailing comment whitespace in stream_error_event_test.go to
satisfy gofmt.

All 30+ subtests in the package still pass.
2026-05-25 18:16:46 +08:00
Jamie Wong
b34cc71bee fix(openai): also emit response.failed in ensureForwardErrorResponse after Writer.Written
Case B: when a slot wait flushes SSE ping comments first (Writer.Written
becomes true), the previous ensureForwardErrorResponse short-circuited
on `c.Writer.Written()` and returned false without notifying the client.
Subsequent upstream errors (http2 timeout, stream INTERNAL_ERROR, etc.)
produced silent EOF; Codex CLI reported "stream closed before
response.completed" just like the user-slot timeout case.

Remove the Written() early return; coerce streamStarted to true when
Writer has already been written to, and let handleStreamingAwareError
walk the existing logic — which now (thanks to the previous commits)
emits a protocol-compliant response.failed for /responses paths and the
legacy `event: error` for others.

Update tests that previously asserted "do not override written response":
the new contract is to *append* an SSE terminal frame so the client sees
a clean close instead of EOF. recoverResponsesPanic inherits this fix.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 22:00:56 +08:00
Jamie Wong
cff2f291be fix(openai): also match bare /responses route in handleStreamingAwareError
The first revision compared GetInboundEndpoint(c) against EndpointResponses
("/v1/responses"). NormalizeInboundEndpoint only recognizes paths that
contain the literal "/v1/responses" substring, but the project actually
registers six /responses routes — three of which (top-level
r.POST("/responses", ...) and codexDirect's "/backend-api/codex/responses")
have FullPath values without the "/v1" prefix and therefore fall through
to the default branch.

Codex CLI users targeting the bare /responses route at the production
deployment (observed 2026-05-24 ~11:05 UTC, user 16) never reached the
new writeResponsesFailedSSE path: the endpoint check was false, the
legacy `event: error` frame fired, and the strict SDK kept reporting
"stream closed before response.completed".

Replace the strict equality check with inboundIsResponses(c), which
uses suffix detection on FullPath (falling back to URL.Path when
FullPath is empty in test fixtures) and covers all six route variants:

  /v1/responses[/...]
  /responses[/...]
  /backend-api/codex/responses[/...]

Add test table covering all routes plus negative cases.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 19:32:08 +08:00
Jamie Wong
5e5c2062bf fix(openai): emit response.failed for /v1/responses after stream started
When /v1/responses streaming hits the user/account concurrency wait, the
wait loop sends SSE ping comments to keep the connection alive, which
flushes HTTP 200 + headers. If the wait then times out (or any other
post-flush error fires), handleStreamingAwareError previously emitted a
generic `event: error` frame. Codex CLI requires the stream to end with
a Responses terminal event (response.completed/failed/incomplete/cancelled),
so it reports "stream closed before response.completed" and the user-facing
rate-limit intent is lost.

This change detects inbound = /v1/responses in both handleStreamingAwareError
implementations and emits a protocol-compliant response.failed event whose
field set mirrors apicompat.makeResponsesCompletedEvent
(id/object/model/status/output/error). The synthetic id reuses
ctxkey.RequestID so client errors can be grepped against server logs.
sequence_number is intentionally omitted to preserve monotonicity on streams
that already emitted real events.

Other inbound endpoints (/v1/chat/completions, /v1/messages) keep their
legacy formats untouched.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 10:58:29 +08:00
shaw
1e406fed52 fix: optimize OpenAI account cooldown scheduling 2026-05-23 10:18:43 +08:00
Wesley Liddick
f59d9a5f8e
Merge pull request #2674 from wucm667/feat/moderation-per-model-toggle
feat(risk-control): 内容审计支持按模型生效
2026-05-22 20:10:38 +08:00
Wesley Liddick
9f91a8af17
Merge pull request #2662 from touwaeriol/feat/bedrock-cc-compat
feat(bedrock): add Claude Code compatibility for AWS Bedrock
2026-05-22 17:32:11 +08:00
wucm667
0d5c6f7cc7 feat(risk-control): 内容审计支持按模型生效 2026-05-21 21:18:43 +08:00
shaw
aae20ef437 fix(oidc): harden verified-email fast path 2026-05-21 15:19:29 +08:00
Wesley Liddick
35901a174b
Merge pull request #2655 from ye4241/feat/oidc-trust-verified-email-fast-path
feat(oidc): 上游邮箱已验证时跳过 choice 页直接登录注册
2026-05-21 14:47:08 +08:00
shaw
a613a587ba feat: add subscription expiry email toggle 2026-05-21 14:27:50 +08:00
ye4241
55554adc18 chore(oidc): 回应 Copilot review
- ProviderType 从 identity.ProviderType 取(不再硬编码)
- fast-path 日志改用 infraerrors.Reason(err) 避免泄露 PII / 降噪
2026-05-21 13:32:20 +08:00
ye4241
39fe7aa0eb feat(oidc): 上游邮箱已验证时跳过 choice 页直接登录注册
当前 OIDC 首次登录无条件创建 choose_account_action_required 的 pending
session,即使 force_email_on_third_party_signup 关闭,前端仍然会强制
弹出"创建账号 / 绑定已有账号"的二选一界面,并展示内部合成邮箱
(oidc-xxx@oidc-connect.invalid),用户体验差。

本次复用已存在的 LoginOrRegisterVerifiedEmailOAuth 路径(原本仅供
github/google 使用),在以下条件全部满足时跳过 choice 页,直接
信任上游身份完成注册/登录:

- force_email_on_third_party_signup = false
- 邀请码模式未启用
- 上游声明 email_verified = true 且 compat_email 非空
- 本地不存在同邮箱已有账号

失败时(如邮箱后缀不在白名单、注册关闭等)自动回退到现有 choice
流程,行为完全向后兼容。

测试覆盖:
- TestTryOIDCVerifiedEmailFastPathCreatesUserAndIdentity
- TestTryOIDCVerifiedEmailFastPathSkippedWhenInvitationCodeRequired
- TestTryOIDCVerifiedEmailFastPathSkippedWhenForceEmailEnabled
2026-05-21 13:32:20 +08:00
erio
fe1c6c958b feat(bedrock): add Claude Code compatibility for AWS Bedrock
- Export ApplyBedrockCCCompat() in GatewayService, called after channel
  model mapping to ensure mapped model ID is used for Opus 4.7+ detection
- Add sanitizeBedrockCCFields(): remove service_tier/interface_geo/
  context_management, inject max_tokens/anthropic_version defaults
- Add sanitizeBedrockCCBetaTokens(): filter anthropic_beta to keep only
  Bedrock-supported tokens, reusing autoInjectBedrockBetaTokens and
  filterBedrockBetaTokens for consistent rules
- Remove unsupported beta tokens (interleaved-thinking, context-management)
  from whitelist based on AWS official docs
- Simplify IsBedrockCCCompatEnabled() to check boolean toggle directly,
  applying CC compat to all accounts regardless of platform
- Add unit tests for IsBedrockCCCompatEnabled (8 cases),
  sanitizeBedrockCCFields (8 cases), sanitizeBedrockCCBetaTokens (7 cases)
- Update bedrock beta policy tests for removed auto-injection
2026-05-21 11:46:24 +08:00
Wesley Liddick
bd3d4d9a24
Merge pull request #2399 from gaoren002/fix/openai-image-upstream-errors
fix(openai): surface image moderation errors
2026-05-21 11:31:22 +08:00
Wesley Liddick
eda04c6129
Merge pull request #2615 from wucm667/feat/redeem-code-batch-update
feat(redeem): 兑换码支持批量修改
2026-05-21 10:39:46 +08:00
Wesley Liddick
d3c4e50753
Merge pull request #2645 from lyen1688/fix/trusted-forwarded-ip-acl
PR:为 API Key IP 白/黑名单增加可配置的反代真实 IP 判断
2026-05-21 10:34:28 +08:00
lyen1688
08c8c67df7 为 API Key ACL 增加反代真实 IP 开关 2026-05-20 22:51:46 +08:00
benjamin
69305a6091 fix(ops): 排除本地客户端限制错误的 SLA 计数
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-20 22:01:33 +08:00
gaoren002
888cd8092d fix(openai): surface image moderation errors 2026-05-20 09:19:20 +00:00
wucm667
3263ca63c7 feat(redeem): add redeem code batch update 2026-05-20 16:08:41 +08:00
wucm667
90b2b2a757 feat(usage): 用户 API Key 用量页支持按日明细 2026-05-20 15:48:38 +08:00
Wesley Liddick
378a0a6a61
Merge pull request #2599 from Arron196/feature/email-template-editor
feat: 添加邮件模板编辑器与通知邮件模板化
2026-05-20 15:12:57 +08:00
shaw
878ad3b569 feat(openai-gateway): Codex OAuth 账号浏览器 UA 自动改写规避 Cloudflare
质询
2026-05-20 14:33:51 +08:00
benjamin
efa1994233 feat(auth): 透传验证码邮件语言偏好
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-20 13:25:45 +08:00
shaw
91da815993 feat(risk-control): 内容审计新增关键词拦截 2026-05-20 11:13:53 +08:00
benjamin
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
benjamin
903ef7b592 feat(payment): 发送支付成功通知邮件
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
benjamin
55b13cd7b4 feat(settings): 添加邮件退订入口
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
benjamin
88346b4d53 feat(admin): 添加邮件模板管理接口
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
Wesley Liddick
03730fbcf3
Merge pull request #2585 from Arron196/feature/channel-monitor-openai-detection
优化渠道监控 OpenAI 检测协议与内置模板
2026-05-20 08:50:44 +08:00
Wesley Liddick
74e35a0150
Merge pull request #2582 from wucm667/feat/channel-pricing-sync-models
feat(channels): 模型定价支持一键同步最新模型
2026-05-20 08:43:10 +08:00
Wesley Liddick
44c13e7a73
Merge pull request #2578 from wucm667/feat/payment-force-qrcode
feat(payment): 支持强制移动端统一使用二维码支付
2026-05-20 08:41:29 +08:00
benjamin
917bd877ae feat(channel-monitor): 暴露 API 模式接口字段
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-19 22:05:43 +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
name
2eb622f2f6 Remove ops retry replay storage 2026-05-19 19:37:41 +08:00
wucm667
e4c7927eff feat(payment): 支持强制移动端统一使用二维码支付 2026-05-19 18:22:12 +08:00
Wesley Liddick
21ae52c01f
Merge pull request #2571 from sherlockwhite/fix/openai-account-email-and-usage-refresh
fix(accounts): show email and fix usage refresh for OpenAI OAuth accounts
2026-05-19 16:56:26 +08:00
Wesley Liddick
2a242aec0f
Merge pull request #2573 from wucm667/feat/redeem-code-expiry
feat(redeem): 兑换码支持设置使用有效期
2026-05-19 16:25:12 +08:00
Wesley Liddick
66fca3d598
Merge pull request #2572 from wucm667/fix/openai-silent-refusal-failover
fix(openai): 识别上游静默拒绝(空流+finish_reason=stop)并触发 failover
2026-05-19 16:15:54 +08:00
Wesley Liddick
a929e285ce
Merge pull request #2271 from StarryKira/fix/redact-account-credentials
fix(security): 屏蔽 admin 账号接口返回的敏感凭证字段
2026-05-19 16:15:36 +08:00
Wesley Liddick
32037cb17b
Merge pull request #2570 from wucm667/fix/ops-sla-exclude-ip-denied
fix(ops): 用户 IP 限制导致的 ACCESS_DENIED 不计入 SLA 错误
2026-05-19 16:03:16 +08:00
wucm667
e4aaf0af29 feat(redeem): 兑换码支持设置使用有效期 2026-05-19 15:53:28 +08:00
wucm667
6381f9e37d fix(openai): 识别上游静默拒绝并触发 failover 2026-05-19 15:48:36 +08:00
chenjian
41e7ae534c fix(accounts): fix OpenAI OAuth usage quota never refreshing on manual refresh
The refresh button had no effect because two independent gates blocked
the Codex probe:

1. isOpenAICodexSnapshotStale returned false for non-WS-v2 accounts even
   when data was stale — this is correct for background auto-refresh (Codex
   quota is only auto-tracked for Codex CLI / WS v2 accounts), so this
   behavior is preserved.

2. shouldProbeOpenAICodexSnapshot had a 10-min in-memory rate limit with
   no bypass for explicit user requests.

Fix: add a force parameter threaded from handler → GetUsage → getOpenAIUsage
→ shouldProbeOpenAICodexSnapshot. When force=true (manual refresh button),
both gates are bypassed and the probe always runs. Background auto-loads
continue to respect the original WS v2 logic and 10-min rate limit.
2026-05-19 15:43:21 +08:00
wucm667
271aba1abe fix(ops): exclude IP-denied access from SLA 2026-05-19 15:41:54 +08:00
Wesley Liddick
1b6ed24c33
Merge pull request #2492 from DaydreamCoding/feat/dingtalk-login
feat(dingtalk): 钉钉 OAuth 登录接入 + internal_only 用户属性同步
2026-05-19 15:36:13 +08:00
DaydreamCoding
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
DaydreamCoding
664e9fdcd4 feat(usage): 用户用量按平台拆分 + UsersView 列设置可配置 + 用量列排序
后端
- BatchUserUsageStats / UserDashboardStats 新增 ByPlatform 字段
  复用 ops 路径 COALESCE(g.platform, a.platform) 语义,不冗余 DB 字段
- 抽出 usageLogEffectivePlatformExpr 常量供管理员与用户两路径共用
- GetBatchUsersUsage cacheKey 加 v=2 + 当日日期,修复跨午夜旧缓存兼容新字段

前端
- 新建 PlatformUsageBreakdown:管理员用量列 hover tooltip 展示各平台 today/total
- 新建 PlatformCostCell:单平台 today/total 紧凑单元格
- UsersView 列设置新增 Claude/OpenAI/Gemini/Antigravity 四个平台子列,默认隐藏可手动启用
- 普通用户 Dashboard 新增 Row 3 平台拆分卡片,受 isSimple 控制
- 平台之和 < 总值时显式展示"其他"行,避免数字对不齐
- last_active_at 从 FORCED_VISIBLE_COLUMNS 移除,允许用户隐藏并持久化
- 列设置加 schema 版本号 + 迁移机制,老用户升级时新增默认隐藏列自动应用
- UsersView 用量列(汇总 + 4 平台子列)加入前端单页排序:列头单按钮 + 弹出菜单
  切换"今日 / 近30天",三态循环 desc → asc → off;菜单底部备注"仅对本页数据排序"
- sortedUsers computed 在 server-side-sort 结果之上叠加本地排序,缺失值按 0 处理;
  usageSort 状态独立 localStorage 持久化,互不干扰后端 sort_by
- i18n 新增 admin.users.sortBy / sortCurrentPageOnly

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 15:25:34 +08:00
Wesley Liddick
8a4ee578cb
Merge pull request #2451 from wucm667/codex/issue-2237-gemini-chat-completions
fix(gateway): 修复 Gemini 组 Chat Completions 路由
2026-05-19 14:47:52 +08:00