Commit Graph

2280 Commits

Author SHA1 Message Date
IanShaw027
97c9b992cb fix: require wechat unionid for oauth identity 2026-04-20 20:27:15 +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
IanShaw027
b51bc7ee24 feat: wire payment return url payloads 2026-04-20 20:19:23 +08:00
IanShaw027
fb6204ea8b feat: apply oauth first-bind defaults and pending bind 2fa 2026-04-20 19:53:22 +08:00
erio
79192cf65b feat(payment): harden wxpay config validation with structured errors
Motivation: platform-certificate mode is being phased out by WeChat (2024-10+,
newly-provisioned merchants already cannot download platform certificates at
all), and wxpay config errors currently surface only when an order is being
created — admins have no feedback at save time. Also, errors were returned as
natural-language strings, leaving the frontend no way to localize them.

Changes:

- backend/internal/payment/provider/wxpay.go
  - Replace fmt.Errorf with structured infraerrors.BadRequest errors:
    - WXPAY_CONFIG_MISSING_KEY    (metadata: key)
    - WXPAY_CONFIG_INVALID_KEY_LENGTH  (metadata: key, expected, actual)
    - WXPAY_CONFIG_INVALID_KEY    (metadata: key) for malformed PEMs
  - Parse privateKey and publicKey PEMs in NewWxpay so malformed keys fail
    at save time instead of at order creation.
  - Keep the pubkey verifier (NewSHA256WithRSAPubkeyVerifier) as the single
    supported verifier; no more loadKeyPair helper.

- backend/internal/service/payment_order.go invokeProvider
  - If CreateProvider or CreatePayment returns a structured ApplicationError,
    pass it through (optionally enriching metadata with provider/instance_id)
    instead of wrapping it as generic PAYMENT_GATEWAY_ERROR — so clients see
    the actual reason code (e.g. WXPAY_CONFIG_MISSING_KEY) and can localize.
  - Simplify a few messages (TOO_MANY_PENDING, DAILY_LIMIT_EXCEEDED,
    PAYMENT_GATEWAY_ERROR, NO_AVAILABLE_INSTANCE) to keyword form with
    metadata for template variables.

- backend/internal/service/payment_config_providers.go
  - New helper validateProviderConfig calls provider.CreateProvider at save
    time. Enabled instances are validated on both Create and Update so admins
    see config errors immediately in the dialog, not later at order creation.
  - Disabled instances are not validated (half-filled drafts are allowed).

- backend/internal/payment/provider/wxpay_test.go
  - Add generateTestKeyPair helper that produces valid RSA-2048 PKCS8/PKIX
    PEMs per test, used by the valid-config baseline (prior fake strings no
    longer pass the eager PEM check).
  - Cover each structured-error branch (missing/invalid-length/malformed PEM).
2026-04-20 19:49:45 +08:00
IanShaw027
6a75bd77e3 feat: add pending oauth email onboarding flow 2026-04-20 19:30:09 +08:00
IanShaw027
d47580a144 test: pin email signup defaults in register tests 2026-04-20 18:42:28 +08:00
IanShaw027
0353c3870f test: update user service stubs for identity summaries 2026-04-20 18:40:34 +08:00
IanShaw027
4e0e691546 feat: apply auth source signup defaults 2026-04-20 18:39:53 +08:00
IanShaw027
c6d8592484 feat: add profile auth identity binding flow 2026-04-20 18:28:44 +08:00
IanShaw027
13d9780df4 feat: expose user activity timestamps in admin list 2026-04-20 17:48:30 +08:00
IanShaw027
e9de839d87 feat: rebuild auth identity foundation flow 2026-04-20 17:39:57 +08:00
IanShaw027
fbd0a2e3c4 feat: carry suggested third-party profile through pending oauth 2026-04-20 16:27:23 +08:00
IanShaw027
d3d4267731 fix: harden oidc callback security 2026-04-20 16:23:42 +08:00
Wesley Liddick
f5ee93796d Merge pull request #1753 from touwaeriol/feat/fix-orphaned-scheduled-tests
fix: delete scheduled test plans when account is deleted
2026-04-19 21:14:23 +08:00
Wesley Liddick
e8be434498 Merge pull request #1752 from touwaeriol/fix/quota-exceeded-scheduling
fix(account): prevent quota-exceeded API key/Bedrock accounts from being scheduled
2026-04-19 21:14:06 +08:00
erio
6579f28b64 fix: delete scheduled test plans when account is deleted
Accounts use soft-delete (setting deleted_at), so PostgreSQL's
ON DELETE CASCADE on scheduled_test_plans.account_id never fires.
Add plan deletion to the existing account deletion transaction
to ensure atomicity.

Closes Wei-Shaw/sub2api#1728
2026-04-19 20:38:57 +08:00
erio
258fd145ff fix(account): prevent quota-exceeded API key/Bedrock accounts from being scheduled
Add quota exceeded check to IsSchedulable() and refactor
shouldClearStickySession to delegate to IsSchedulable(), eliminating
duplicated logic and fixing missed overload/rate-limit/expired checks.
Frontend displays quota exceeded status independently via quota fields.
2026-04-19 18:45:04 +08:00
erio
6530776a62 fix: support xhigh reasoning effort in usage records for Claude Messages API
Closes #1732
2026-04-19 18:05:25 +08:00
erio
235f710853 feat(payment): redact provider secrets in admin config API
Admin GET /api/v1/admin/payment/providers previously returned every
config value — including privateKey / apiV3Key / secretKey etc. —
verbatim. Any future XSS on the admin UI would hand attackers the
full set of production payment credentials, and the plaintext values
sat unnecessarily in browser memory for every operator.

Treat those fields as write-only from the admin surface:

- decryptAndMaskConfig() strips sensitive keys from the GET response.
  The authoritative list is an explicit per-provider registry that
  mirrors the frontend's PROVIDER_CONFIG_FIELDS sensitive flag:
    alipay   → privateKey, publicKey, alipayPublicKey
    wxpay    → privateKey, apiV3Key, publicKey
    stripe   → secretKey, webhookSecret (publishableKey stays plain)
    easypay  → pkey
  Payment runtime still reads the full config via decryptConfig, so
  nothing at the gateway changes.

- mergeConfig() treats an empty value for a sensitive key as "leave
  unchanged" — the admin UI omits unchanged secrets so operators can
  tweak non-sensitive settings without re-entering credentials.

- Admin dialog (PaymentProviderDialog.vue):
  * secret inputs get autocomplete="new-password", data-1p-ignore,
    data-lpignore and data-bwignore so password managers do not
    offer to save provider credentials
  * in edit mode the required-field check skips sensitive fields
    (empty is the "keep existing" signal) and the placeholder shows
    "leave empty to keep" instead of the default example value
  * create mode still requires every non-optional field, including
    secrets, since there is nothing to preserve

- Unit test renamed to TestIsSensitiveProviderConfigField, covers
  the per-provider registry and specifically asserts that Stripe's
  publishableKey is NOT treated as a secret.
2026-04-19 02:22:53 +08:00
erio
c3cb0280ef fix(payment): alipay redirect-only flow, H5 detection and popup sizing
The native Alipay provider previously tried to embed the payment page
URL into a QR code on the client — the URL is not a scannable payload
so the QR never worked. Merchants also hit a H5 detection mismatch
whenever the backend UA sniffer missed iPadOS 13+ or embedded browsers,
and the popup window was too small for Alipay's standard checkout
layout (QR + account-login panel on the right), forcing the user to
scroll horizontally and vertically.

Changes:

Backend
- alipay.go: drop QR-on-URL path. Use redirect-only flow —
  alipay.trade.page.pay for PC (returns a gateway URL the browser
  opens in a new window) and alipay.trade.wap.pay for H5 (returns a
  URL the browser jumps to). Both flows produce pages on
  openapi.alipaydev.com / excashier.alipay.com; the client never
  renders a QR itself.
- payment_handler.go: add optional is_mobile bool to
  CreateOrderRequest so the frontend can declare the device
  explicitly. Server still falls back to UA sniffing when absent.

Frontend
- types/payment.ts, PaymentView.vue: declare is_mobile in
  CreateOrderRequest and pass the computed isMobileDevice() value.
- providerConfig.ts: replace the two fixed POPUP_WINDOW_FEATURES
  constants with getPaymentPopupFeatures(), which prefers 1250×900
  (Alipay's checkout footprint), clamps to window.screen.avail* and
  centers the popup so it never overflows on smaller laptops.
- PaymentQRDialog.vue, PaymentStatusPanel.vue, StripePaymentInline.vue,
  PaymentView.vue: use the new helper at all popup call sites.
2026-04-19 02:22:41 +08:00
erio
61a008f7e4 chore(payment): mark legacy AES ciphertext fallback as deprecated
明文 JSON 已经是新写入的默认格式;保留 AES 密文读取仅为兼容迁移期间的旧
记录,一旦所有部署通过管理后台重存过一次即可删除。标记为 deprecated 并加
TODO,几个版本后统一清理掉:payment.Encrypt / payment.Decrypt、两处
decryptConfig 的 AES 分支、PaymentConfigService.encryptionKey 和
DefaultLoadBalancer.encryptionKey 字段。
2026-04-17 23:24:27 +08:00
erio
bf0bbe0be7 feat(gateway): raise upstream response read limit 8MB -> 128MB (configurable)
图片生成 API 返回的 base64 内联图响应经常超过 8MB 单次读取上限,被
ReadUpstreamResponseBody 拦截成 502 upstream_error。

单张 4K PNG base64 最坏约 67MB,多张候选图或 imageSize=4K 的 image_generation
一次请求能轻松到 30MB+。把默认上限提到 128MB 能覆盖 2-3 张 4K 图,相对
请求体上限 256MB 仍有缓冲;同时抽出 config.DefaultUpstreamResponseReadMaxBytes
共享常量,viper 默认值和 service 层兜底共用,消除两处同步魔法数字。

仍可通过 gateway.upstream_response_read_max_bytes 配置项覆盖。
2026-04-17 22:07:15 +08:00
erio
df57d2776b fix(billing): reject rate_multiplier <= 0 on save; clamp negatives to 0 in compute
分组倍率和用户专属倍率在保存时没有校验,0 会触发计费层的 `<=0 → 1.0`
防御条款,结果订阅/余额分组按标准价扣费;完全是沉默地绕过了业务规则。

- 保存校验(admin_service):CreateGroup / UpdateGroup / BatchSetGroupRateMultipliers /
  UpdateUser.SyncUserGroupRates 全部要求 > 0
- 计算层(billing_service):三处 `<=0 → 1.0` 改为 `<0 → 0`;负数按 0 结算,
  避免配置异常被静默按 1x 收费
- 前端:分组倍率 / 用户专属倍率输入 min 统一到 0.001
- 删除未使用的 IsFreeSubscription 方法

测试:新增 billing_service_rate_multiplier_test.go 端到端验证;更新原有锁定
旧 `<=0 → 1.0` 行为的测试。
2026-04-17 22:06:32 +08:00
erio
44cdef7934 fix(usage): subscription billing honours group rate multiplier
Subscription-mode billing was consuming quota at TotalCost (raw) instead of
ActualCost (TotalCost * RateMultiplier), so per-group rate multipliers —
including free subscriptions (multiplier = 0) — were silently ignored.
Switch the three subscription cost writes in buildUsageBillingCommand,
finalizePostUsageBilling, and the legacy postUsageBilling fallback to
ActualCost, and add a table-driven test covering 2x / 0.5x / free multipliers
plus a balance-mode regression check.
2026-04-17 22:06:32 +08:00
erio
fd0c9a1305 fix(payment): store provider config as plaintext JSON with legacy ciphertext fallback
Without TOTP_ENCRYPTION_KEY, saved payment configs were lost on restart because
the AES round-trip failed silently. Write new records as plaintext JSON; read
path tries JSON first, falls back to legacy AES decrypt when a key is present,
and treats unreadable values as empty so admins can re-enter them via the UI.
2026-04-17 22:06:32 +08:00
github-actions[bot]
6cfdf4ec05 chore: sync VERSION to 0.1.114 [skip ci] 2026-04-17 02:51:18 +00:00
Wesley Liddick
358ff6a608 Merge pull request #1683 from FjlI5/dev-main
fix:修复上游账号为OpenAI API key时Claude Code调用缓存率低的问题
2026-04-17 10:28:12 +08:00
Wesley Liddick
41fbdba104 Merge pull request #1687 from touwaeriol/refactor/upstream-response-limit-dedup
refactor: extract ReadUpstreamResponseBody to deduplicate response read + too-large handling
2026-04-17 10:19:14 +08:00
Wesley Liddick
c22d11cedd Merge pull request #1702 from StarryKira/fix/outbox-watermark-context-dedup-1691
fix: fix outbox watermark context expiry and add in-batch group rebuild dedup
2026-04-17 10:18:56 +08:00
shaw
5d586a9f3a fix: 上游返回 KYC 身份验证要求时停止账号调度 2026-04-17 10:17:50 +08:00
shaw
a789c8c4c7 feat: 支持opus-4.7 2026-04-17 09:37:25 +08:00
Elysia
697c41a3f6 fix: create fresh context per watermark write retry attempt
Each retry in the SetOutboxWatermark loop now gets its own 5s context.
Previously a shared context could already be expired when the second or
third attempt ran, making the retries pointless.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 20:41:40 +08:00
Elysia
e44baa1094 fix: fix outbox watermark context expiry and add in-batch group rebuild dedup
Fixes #1691

- pollOutbox() reused a 10s context for SetOutboxWatermark after event
  processing could take much longer, causing "outbox watermark write
  failed: context deadline exceeded". The watermark never advanced so
  the same 200 events were reprocessed every poll cycle, spiking CPU.
  Now uses an independent 5s context with up to 3 retries (200ms apart).

- When multiple Codex accounts sharing the same 21-22 groups are all
  rate-limited in quick succession, each account_changed event triggered
  redundant bucket rebuild attempts for the same groups. Introduce
  batchSeenKey{groupID, platform} and thread a seen map through the
  handler chain; rebuildBucketsForPlatform skips (group, platform) pairs
  already rebuilt within the same poll batch (~80% fewer rebuild calls in
  the 5-accounts-same-groups scenario).

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 19:09:40 +08:00
Wesley Liddick
e6e73b4f52 Merge pull request #1690 from KnowSky404/fix/ws-codex-scheduler-cache-1662
fix: preserve openai ws flags in scheduler cache
2026-04-16 17:21:32 +08:00
KnowSky404
3944b3d216 fix: preserve openai ws flags in scheduler cache 2026-04-16 02:01:50 +00:00
erio
10699eeb34 refactor: extract ReadUpstreamResponseBody to deduplicate upstream response read + too-large error handling
Consolidates 9 call sites of resolveUpstreamResponseReadLimit + readUpstreamResponseBodyLimited + ErrUpstreamResponseBodyTooLarge error handling into a single ReadUpstreamResponseBody function with TooLargeWriter callback for API-format-specific error responses (Anthropic, OpenAI, countTokens).
2026-04-16 01:53:22 +08:00
fjl5
6c89d8d35c add prompt_cache_key injection for messages→responses 2026-04-15 23:56:56 +08:00
github-actions[bot]
be7551b9f4 chore: sync VERSION to 0.1.113 [skip ci] 2026-04-15 09:34:24 +00:00
Wesley Liddick
70d0569f08 Merge pull request #1668 from tyqy12/main
修复 OpenAI 账号限流回流误判:7d 窗口可用时不因 5h 窗口为 0 回写 429
2026-04-15 16:48:48 +08:00
Wesley Liddick
1db32d692b Merge pull request #1666 from touwaeriol/feat/account-cost-display
feat(usage): add account cost display to admin dashboard and usage pages
2026-04-15 16:43:07 +08:00
erio
db27e8f000 feat(usage): add account cost to breakdown sub-table and admin usage log
- UserBreakdownItem: add AccountCost field + SQL aggregation
- UserBreakdownSubTable: add orange account cost column
- Admin usage table: add account_cost column (after cost, default visible)
- Column settings: add account_cost toggle option
2026-04-15 15:40:40 +08:00
Wesley Liddick
7451b6f9ae 修复 OpenAI 账号限流回流误判:7d 窗口可用时不因 5h 窗口为 0 回写 429 2026-04-15 15:29:52 +08:00
erio
22680dc602 test(usage): add unit tests for account_cost and fix gofmt
- Fix mock for GetModelStatsWithFilters: add account_cost column
- Add assertion: GetStatsWithFilters always returns TotalAccountCost
- New test: GetModelStatsAccountCostColumn verifies scan of AccountCost
- New test: GetGroupStatsAccountCostColumn verifies scan of AccountCost
- New test: GetStatsWithFiltersAlwaysReturnsAccountCost (no AccountID filter)
- Integration test: add TotalAccountCost/TodayAccountCost assertions
- Fix gofmt alignment in usage_log_types.go
2026-04-15 15:02:21 +08:00
erio
6ade6d30a8 feat(usage): add account cost display to admin dashboard and usage pages
- Add account_cost column to dashboard aggregation tables (migration 107)
- DashboardStats: add TotalAccountCost/TodayAccountCost fields
- ModelStat/GroupStat: add AccountCost field with SQL aggregation
- GetStatsWithFilters: always return TotalAccountCost (remove accountID filter)
- Dashboard Token cards: show user(green)/cost(orange)/standard(gray)
- Usage stats card: show account cost and standard below main value
- Model/Group distribution tables: add orange cost column
2026-04-15 15:02:21 +08:00
erio
c2108421c2 fix: gofmt payment_service.go and payment_order.go 2026-04-15 01:50:19 +08:00
erio
342dbd2e19 fix(payment): use original recharge amount in product name, not pay_amount
Product name (e.g. "快代码科技工作室 100 元") should show the user's
original recharge amount (limitAmount), not the fee-inclusive pay amount.
The gateway receives payAmount separately for actual charging.
2026-04-15 01:43:56 +08:00
erio
60614e6f74 fix: gofmt formatting and update API contract test for new fields
- Fix gofmt alignment in setting_handler.go, settings.go, payment_config_service.go
- Add payment_balance_recharge_multiplier and payment_recharge_fee_rate
  to API contract test expected JSON
2026-04-15 01:39:00 +08:00
erio
d149dbc91f fix(payment): enhance fee rate input validation and UI
Backend:
- Validate recharge_fee_rate: 0 ≤ rate ≤ 100, max 2 decimal places

Frontend settings:
- Add % suffix icon to fee rate input
- Enforce max=100, min=0, step=0.01 with 2 decimal precision
2026-04-15 01:27:24 +08:00
erio
e761d38fd1 fix(payment): integrate recharge fee rate in order flow and fix UI display
Backend:
- Use cfg.RechargeFeeRate in order creation instead of hardcoded 0
- Remove dead getFeeRate stub method
- All amounts computed server-side: order_amount, pay_amount, fee_rate

Frontend - PaymentView:
- Read recharge_fee_rate from checkout-info API (not per-method)
- Show fee breakdown only when fee_rate > 0
- Show credited amount only when multiplier ≠ 1

Frontend - Order display (user + admin):
- Fix fee_rate * 100 bug (fee_rate is already a percentage)
- OrderTable: show pay_amount as primary, fee/credited as sub-lines
- AdminOrderDetail: full breakdown (base/fee/paid/credited)
- AdminRefundDialog: label "到账金额" for clarity
- PaymentResultView: show pay_amount with fee info

Types + i18n:
- Add recharge_fee_rate to CheckoutInfoResponse
- Add fee_rate to CreateOrderResult
- Add translations: creditedAmount, fee, baseAmount, includedInPayAmount
2026-04-15 01:27:24 +08:00