Skip to content

个人功能开发 - #5297

Closed
kavoj wants to merge 1 commit into
QuantumNous:mainfrom
kavoj:newpay
Closed

个人功能开发#5297
kavoj wants to merge 1 commit into
QuantumNous:mainfrom
kavoj:newpay

Conversation

@kavoj

@kavoj kavoj commented Jun 3, 2026

Copy link
Copy Markdown

📝 变更描述 / Description
本次变更围绕支付能力、模型类型管理、渠道测试稳定性和本地运行体验做了系统增强:

新增微信支付和支付宝支付配置能力,管理员可在系统支付设置中配置支付网关信息,并支持用户在钱包充值和订阅购买场景中使用微信/支付宝完成支付。
新增支付配置表与敏感字段加密/脱敏机制,避免支付密钥明文返回前端,同时支持配置更新时保留已脱敏字段。
为模型元数据增加模型类型标签,支持 text、embedding、image、file、audio、video 分类;渠道创建/编辑时可选择模型类型,/models/metadata 页面可显示并按类型筛选模型。
优化渠道测试逻辑,使图片模型、向量模型等可根据模型类型或模型特征构造正确的测试请求,修复 Seedream 图片模型被识别为图片端点但仍构造聊天请求导致的测试失败问题。
在 VolcEngine 渠道中增加 Doubao Agent Plan Base URL 快捷选项,并在后端适配 Agent Plan 的 URL 拼接,避免出现 /api/plan/v3/api/v3/... 这类错误路径。
新增本地快速启停脚本,支持构建 35sz-api 二进制并以 9588 端口后台运行,方便本地验证和部署前检查。
这些改动通过新增配置表、路由、支付服务封装、前端配置 UI、模型类型字段和渠道测试请求构造逻辑,使支付流程和多类型模型渠道管理更加稳定、清晰。

🚀 变更类型 / Type of change
🐛 Bug 修复 (Bug fix) - 请关联对应 Issue,避免将设计取舍、理解偏差或预期不一致直接归类为 bug
✨ 新功能 (New feature) - 重大特性建议先通过 Issue 沟通
⚡ 性能优化 / 重构 (Refactor)
📝 文档更新 (Documentation)

本次新增/调整的支付配置、模型类型、VolcEngine Agent Plan URL、前端类型检查与构建均已通过对应验证。

2、增加支付功能、模型类型标签、启动脚本
@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR introduces comprehensive payment gateway integration for Alipay and WeChat with model type metadata tracking. It adds encrypted payment configuration management, top-up and subscription payment flows with webhook handlers, VolcEngine Agent Plan URL routing, and corresponding frontend UI for payment settings and model type filtering, plus operational startup/shutdown scripts.

Changes

Payment Infrastructure and Configuration

Layer / File(s) Summary
Crypto utilities and PaymentConfig model
common/crypto.go, common/payment_crypto_test.go, model/payment_config.go, model/payment_config_test.go, go.mod
Adds AES-GCM encryption/decryption for payment secrets, masking helpers, PaymentConfig GORM model with CRUD operations (create with Unix timestamps, fetch by ID/provider/enabled, list, update, delete), in-memory SQLite tests for persistence and ordering, payment SDK dependencies (Alipay v3, WeChat v3).
Payment config HTTP layer
controller/payment_config.go, controller/payment_config_gateway.go, controller/payment_config_test.go
Exports HTTP handlers (GetPaymentConfigs, GetPaymentConfig, GetPaymentConfigByProvider, CreatePaymentConfig, UpdatePaymentConfig, DeletePaymentConfig) that mask secrets in responses, preserve masked secrets during updates, and validate providers. Gateway helpers decrypt configs and convert Yuan to Fen amounts. Integration tests validate encryption/masking roundtrips and update preservation.
Alipay and WeChat service clients
service/alipay_pay.go, service/wechat_pay.go
Wraps Alipay v3 and WeChat Pay v3 SDKs. AlipayPayClient creates page/WAP pay orders with configurable notify/return URLs and verifies signatures. WechatPayClient creates native/H5 orders with merchant/app IDs and parses webhooks. Both load credentials from PaymentConfig with fallbacks.

Payment Endpoints and Top-Up/Subscription Integration

Layer / File(s) Summary
Alipay top-up and subscription
controller/topup_alipay.go, controller/subscription_payment_alipay.go, controller/payment_webhook_availability.go
RequestAlipayPay validates compliance/enablement, computes group-based pricing, creates pending TopUp, generates PC/H5 URLs. AlipayNotify verifies signatures, marks failed on TRADE_CLOSED, completes recharge on success. RequestAlipayAmount computes payable totals. Subscription flow mirrors with plan validation and per-user limits. Availability helpers check isPaymentComplianceConfirmed() and GetEnabledPaymentConfigByProvider().
WeChat top-up and subscription
controller/topup_wechat.go, controller/subscription_payment_wechat.go
RequestWechatPay validates compliance, creates TopUp, initializes WeChat client, returns code_url (native) or h5_url (H5). WechatNotify verifies, marks failed for non-success states, locks and completes on SUCCESS. RequestWechatAmount computes group-based amounts. Subscription endpoints validate plans and use order locking around completion.
Top-up model extensions
model/topup.go, controller/topup.go
Adds PaymentMethod constants (alipay_pc/h5, wxpay_native/h5) and PaymentProvider (alipay, wxpay). Introduces rechargePaymentConfigTopUp helper with decimal arithmetic and locking. Exports RechargeAlipay and RechargeWechat functions. GetTopUpInfo appends Alipay/WeChat payment methods when enabled, retrieving display names from configs, adds enable_alipay_topup/enable_wechat_topup flags to response.

Model Type System

Layer / File(s) Summary
Model type schema and filtering
model/model_meta.go, model/main.go, model/model_type_test.go, model/model_type_search_test.go
Adds ModelTypeText/Embedding/Image/File/Audio/Video constants, NormalizeModelType function, ModelType field to Model struct with GORM defaults. Insert() and Update() normalize the field. SearchModels() accepts modelType filter parameter. Database migrations ensure model_type column exists. Tests validate persistence across insert/update cycles and filtering behavior.
Channel model type sync and test routing
controller/channel.go, controller/channel-test.go, controller/model_meta.go, controller/channel_test_model_type_test.go
syncChannelModelMetadata derives model_type from channel OtherSettings and upserts Model records. Channel create/update trigger sync. testChannel resolves requestPath via model type lookup, falling back to substring heuristics. buildTestRequest includes automatic mode that detects model type and returns appropriate request (embedding, image, responses). Tests validate endpoint type mapping and Seedream image sizing (2K).

VolcEngine Agent Plan Support

Layer / File(s) Summary
Agent Plan URL construction
relay/channel/volcengine/adaptor.go, docs/superpowers/specs/2026-06-03-doubao-agent-plan-base-url-design.md, relay/channel/volcengine/adaptor_agent_plan_test.go
Adds URL normalization and agent-plan detection helpers. GetRequestURL routes all relay modes (chat, embed, image, rerank, responses) through buildVolcengineURL, which selects between regular /api/v3/... or agent-plan /api/plan/v3/... paths. Tests validate regular Ark URLs, agent-plan mapping across modes, bot routes, Claude format, and special base handling.

Frontend Payment Configuration

Layer / File(s) Summary
Payment config management UI
web/default/src/features/system-settings/types.ts, web/default/src/features/system-settings/api.ts, web/default/src/features/system-settings/integrations/payment-config-dialog.tsx, web/default/src/features/system-settings/integrations/payment-settings-section.tsx
Introduces PaymentConfigProvider and PaymentConfig types, API wrappers for CRUD. PaymentConfigDialog form manages provider/name/enabled/sort/URLs and conditional Alipay/WeChat credential inputs. PaymentSettingsSection loads configs via useQuery, create/update mutations with toasts, provider-driven UI showing enabled status and configure buttons.

Frontend Model Type and Wallet Payments

Layer / File(s) Summary
Model type form and filtering
web/default/src/features/channels/lib/model-types.ts, web/default/src/features/channels/lib/channel-form.ts, web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx, web/default/src/features/models/...
Exports MODEL_TYPE_OPTIONS, ModelType union, inferModelType (guesses from name). Channel form adds model_type enum field, derives initial type from first model name, persists to/from settings JSON. Drawer UI adds Model Type select in Models section and Doubao Agent Plan endpoint option for Type 45. Models table/columns/filters support model_type with toolbar and column filtering.
Alipay/WeChat wallet payment methods
web/default/src/features/wallet/constants.ts, web/default/src/features/wallet/lib/payment.ts, web/default/src/features/wallet/lib/ui.tsx, web/default/src/features/wallet/api.ts, web/default/src/features/wallet/hooks/use-payment.ts
Updates PAYMENT_TYPES to ALIPAY_PC/H5 and WECHAT_NATIVE/H5 variants with distinct color mappings. Adds isAlipayPayment/isWechatPayment predicates and icon rendering logic. API wrappers for Alipay/WeChat amount calculation and payment requests. usePayment hook routes to provider-specific calculators and payment handlers, opens returned URLs (pay_url, h5_url, code_url) on success.
Subscription dialog payment providers
web/default/src/features/subscriptions/api.ts, web/default/src/features/subscriptions/types.ts, web/default/src/features/subscriptions/components/dialogs/subscription-purchase-dialog.tsx
Exports paySubscriptionAlipay/Wechat API functions. Dialog detects Alipay/WeChat availability via method type prefixes, renders payment buttons, adds async handlers that open returned URLs and close dialog. Filters Epay dropdown to exclude Alipay/WeChat methods. SubscriptionPayResponse extends with optional pay_url, code_url, h5_url, trade_no fields.

Routing and Operations

Layer / File(s) Summary
Route registration and scripts
router/api-router.go, start.sh, stop.sh, restart.sh, .gitignore
Registers /user/alipay

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • seefs001
  • Calcium-Ion
  • creamlike1024

Poem

🐰 A rabbit hops through payment gates,
Encrypting secrets, securing fates,
Alipay, WeChat—now the system sings,
Model types gleaming on database wings,
From scripts that start and scripts that stop,
The payments flow and never stop! 🌙

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
controller/model_meta.go (1)

4-4: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use common.Marshal() for mm.Endpoints serialization in controller/model_meta.go.

  • controller/model_meta.go imports encoding/json and uses json.Marshal(eps) to build mm.Endpoints; replace with common.Marshal(eps) and remove the encoding/json import.
  • Also apply to the other json.Marshal(eps) occurrences at lines 207-209 and 297-299.
♻️ Suggested change
-import (
-	"encoding/json"
+import (
 	"sort"
 	"strconv"
 	"strings"
@@
-				if b, err := json.Marshal(eps); err == nil {
+				if b, err := common.Marshal(eps); err == nil {
 					mm.Endpoints = string(b)
 				}
@@
-			if b, err := json.Marshal(eps); err == nil {
+			if b, err := common.Marshal(eps); err == nil {
 				mm.Endpoints = string(b)
 			}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/model_meta.go` at line 4, Replace direct uses of json.Marshal for
serializing endpoint lists with the project's helper: call common.Marshal(eps)
wherever json.Marshal(eps) is used to build mm.Endpoints (including the
occurrences that set mm.Endpoints around the variables named eps and mm). Remove
the unused "encoding/json" import after switching to common.Marshal. Ensure
mm.Endpoints is assigned the result of common.Marshal(eps) (and handle any
returned error the same way the existing code expects).
🧹 Nitpick comments (7)
controller/payment_config_gateway.go (1)

53-55: 💤 Low value

Consider using math.Round for more robust currency conversion.

The +0.5 truncation trick can have edge cases with floating-point representation. Using math.Round(amount*100) is the idiomatic Go approach and handles negative values and edge cases correctly.

♻️ Suggested improvement
+import "math"
+
 func yuanToFen(amount float64) int64 {
-	return int64(amount*100 + 0.5)
+	return int64(math.Round(amount * 100))
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/payment_config_gateway.go` around lines 53 - 55, The yuanToFen
function uses a +0.5 truncation trick which is fragile for floating-point and
negative values; replace the conversion with int64(math.Round(amount*100)) in
yuanToFen and add/ensure the math package is imported so rounding is performed
correctly and edge cases (including negatives) are handled idiomatically.
service/wechat_pay.go (1)

28-63: ⚖️ Poor tradeoff

Global downloader manager may conflict with multiple WeChat Pay configurations.

The downloader.MgrInstance() is a global singleton. If multiple payment configs with different WeChat credentials are registered, subsequent registrations may overwrite or conflict with earlier ones. This is a known SDK limitation, but worth noting if multi-tenant scenarios are planned.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/wechat_pay.go` around lines 28 - 63, NewWechatPayClient currently
uses the global downloader.MgrInstance() which can cause cross-tenant conflicts
when multiple WeChat configs are registered; change the registration logic to
avoid clobbering global state by either (A) checking
downloader.MgrInstance().GetCertificateVisitor(config.WechatMchID) before
calling RegisterDownloaderWithPrivateKey and only register if no existing
visitor (if an existing visitor is found, verify it matches the current
serial/key and skip or return an error), or (B) create and use a dedicated
downloader instance per client (instead of MgrInstance()) that you pass to
verifiers.NewSHA256WithRSAVerifier and retain on the returned WechatPayClient;
update NewWechatPayClient to implement one of these strategies (use symbols:
NewWechatPayClient, downloader.MgrInstance(), RegisterDownloaderWithPrivateKey,
GetCertificateVisitor, verifiers.NewSHA256WithRSAVerifier) so multiple merchant
configs do not overwrite each other.
controller/payment_config_test.go (1)

96-140: ⚡ Quick win

Add WeChat secret-preservation coverage.

This file only proves masked-secret preservation for Alipay. The new settings dialog also edits masked WeChat secrets, so a regression in wechat_api_key / wechat_private_key handling would still pass this suite and break existing gateway configs at runtime.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/payment_config_test.go` around lines 96 - 140, Add a parallel test
(or extend TestUpdatePaymentConfigKeepsMaskedSecrets) that verifies
masked-secret preservation for WeChat: create a model.PaymentConfig for
model.PaymentProviderWeChat with encrypted wechat_api_key and wechat_private_key
using common.EncryptPaymentSecret, save it via model.CreatePaymentConfig, then
issue an update request via UpdatePaymentConfig with the same masked values
(e.g. "old-****") for wechat_api_key and wechat_private_key and an updated
non-secret field (e.g. Name), and finally fetch the stored config with
model.GetPaymentConfigByProvider and assert that decrypting stored
wechat_api_key/wechat_private_key (common.DecryptPaymentSecret) returns the
original secrets and that non-secret fields were updated. Ensure you reference
the same handler UpdatePaymentConfig and methods
CreatePaymentConfig/GetPaymentConfigByProvider used in the Alipay test.
web/default/src/features/subscriptions/components/dialogs/subscription-purchase-dialog.tsx (1)

502-502: 💤 Low value

Consider extracting the duplicated filter logic.

The filter (m) => !m.type?.startsWith('alipay_') && !m.type?.startsWith('wxpay_') is repeated in two places. Consider extracting it to a variable for clarity and maintainability.

♻️ Suggested refactor
+ const filteredEpayMethods = (props.epayMethods || []).filter(
+   (m) => !m.type?.startsWith('alipay_') && !m.type?.startsWith('wxpay_')
+ )
  // Then use filteredEpayMethods in both the Select items and SelectContent

Also applies to: 518-518

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@web/default/src/features/subscriptions/components/dialogs/subscription-purchase-dialog.tsx`
at line 502, Extract the duplicated filter predicate used on props.epayMethods
((m) => !m.type?.startsWith('alipay_') && !m.type?.startsWith('wxpay_')) into a
named constant (e.g., nonChinaPayFilter or isNonAlipayWxpay) and reuse it in
both map calls where the current inline predicate appears; update the two
occurrences that call .filter(...) before .map(...) to reference that constant
so the logic is centralized and clearer (search for uses around
props.epayMethods and the inline arrow predicate).
web/default/src/features/wallet/hooks/use-payment.ts (2)

101-113: ⚡ Quick win

Refactor deeply nested ternary in payment request logic.

Same issue as above - 4-level nested ternary violates coding guidelines.

♻️ Suggested refactor
+const getPaymentRequester = (paymentType: string) => {
+  if (isStripePayment(paymentType)) {
+    return (amount: number) => requestStripePayment({ amount, payment_method: 'stripe' })
+  }
+  if (isAlipayPayment(paymentType)) {
+    return (amount: number) => requestAlipayPayment({ amount, payment_method: paymentType })
+  }
+  if (isWechatPayment(paymentType)) {
+    return (amount: number) => requestWechatPayment({ amount, payment_method: paymentType })
+  }
+  return (amount: number) => requestPayment({ amount, payment_method: paymentType })
+}

 const processPayment = useCallback(
   async (topupAmount: number, paymentType: string) => {
     try {
       setProcessing(true)

       const isStripe = isStripePayment(paymentType)
       const isAlipay = isAlipayPayment(paymentType)
       const isWechat = isWechatPayment(paymentType)
       const amount = Math.floor(topupAmount)

-      const response = isStripe
-        ? await requestStripePayment({
-            amount,
-            payment_method: 'stripe',
-          })
-        : isAlipay
-          ? await requestAlipayPayment({ amount, payment_method: paymentType })
-          : isWechat
-            ? await requestWechatPayment({ amount, payment_method: paymentType })
-            : await requestPayment({
-                amount,
-                payment_method: paymentType,
-              })
+      const requester = getPaymentRequester(paymentType)
+      const response = await requester(amount)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/wallet/hooks/use-payment.ts` around lines 101 - 113,
Replace the 4-level nested ternary used to choose the payment request with a
clear conditional/dispatch structure: inspect the booleans isStripe, isAlipay,
isWechat and call the corresponding functions (requestStripePayment,
requestAlipayPayment, requestWechatPayment) otherwise fall back to
requestPayment; implement this as an if/else-if chain or a small switch/map that
builds the payload (amount, payment_method) and invokes the selected function so
the logic is readable and maintainable.

61-69: ⚡ Quick win

Refactor deeply nested ternary to improve readability.

The 4-level nested ternary violates coding guidelines. As per coding guidelines: "Prohibit nested ternary expressions with 2 or more levels; use if-else, early returns, or extract functions instead."

♻️ Suggested refactor using a helper function
+const getAmountCalculator = (paymentType: string) => {
+  if (isStripePayment(paymentType)) return calculateStripeAmount
+  if (isWaffoPancakePayment(paymentType)) return calculateWaffoPancakeAmount
+  if (isAlipayPayment(paymentType)) return calculateAlipayAmount
+  if (isWechatPayment(paymentType)) return calculateWechatAmount
+  return calculateAmount
+}

 const calculatePaymentAmount = useCallback(
   async (topupAmount: number, paymentType: string) => {
     try {
       setCalculating(true)

-      const isStripe = isStripePayment(paymentType)
-      const isPancake = isWaffoPancakePayment(paymentType)
-      const isAlipay = isAlipayPayment(paymentType)
-      const isWechat = isWechatPayment(paymentType)
-      const response = isStripe
-        ? await calculateStripeAmount({ amount: topupAmount })
-        : isPancake
-          ? await calculateWaffoPancakeAmount({ amount: topupAmount })
-          : isAlipay
-            ? await calculateAlipayAmount({ amount: topupAmount })
-            : isWechat
-              ? await calculateWechatAmount({ amount: topupAmount })
-              : await calculateAmount({ amount: topupAmount })
+      const calculator = getAmountCalculator(paymentType)
+      const response = await calculator({ amount: topupAmount })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/wallet/hooks/use-payment.ts` around lines 61 - 69,
The nested ternary that sets response is hard to read; replace it with a small
helper or an if/else chain that selects the correct calculation function based
on the flags (isStripe, isPancake, isAlipay, isWechat) and then awaits that
function (calculateStripeAmount, calculateWaffoPancakeAmount,
calculateAlipayAmount, calculateWechatAmount, or calculateAmount) to assign
response; e.g., create a selectAmountCalculator helper or use a simple if/else
block before assigning response so each branch is explicit and readable.
web/default/src/features/models/types.ts (1)

97-108: ⚡ Quick win

Narrow model_type to ModelType.

This file introduces the ModelType union on Line 25, but both request interfaces still accept any string. Using ModelType here lets the compiler catch typos in the new filter/query path before they ship.

Suggested fix
 export interface GetModelsParams {
@@
-  model_type?: string // filter by model type
+  model_type?: ModelType // filter by model type
 }
@@
 export interface SearchModelsParams {
@@
-  model_type?: string // filter by model type
+  model_type?: ModelType // filter by model type
   p?: number
   page_size?: number
 }

Please rerun bun run typecheck after tightening these signatures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/models/types.ts` around lines 97 - 108, Change the
loose string-typed model_type fields to the strict ModelType union: update
SearchModelsParams.model_type (and the earlier model list interface's
model_type) to use ModelType instead of string so the compiler can validate
allowed values; ensure ModelType is in scope (it's declared earlier in this
file) and then rerun the typecheck command (bun run typecheck) to confirm no
remaining type errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.gitignore:
- Line 40: Remove the self-referential ignore entry that lists ".gitignore" so
the repository will track changes to the ignore rules; locate the literal
".gitignore" entry in the .gitignore file and delete that line entirely (keep
all other ignore patterns intact).

In `@common/crypto.go`:
- Around line 90-98: MaskSecret and IsMaskedSecret disagree for short secrets;
update MaskSecret (function MaskSecret) so that for secrets with length <= 4 it
returns the entire secret concatenated with "****" (e.g., secret + "****")
instead of just "****", while keeping the existing behavior for longer secrets
(secret[:4] + "****"); this makes IsMaskedSecret (which checks suffix == "****")
correctly detect masked short secrets without changing IsMaskedSecret logic.
- Around line 41-46: getPaymentEncryptionKey currently silently falls back to
sha256(CryptoSecret) when PAYMENT_CONFIG_ENCRYPTION_KEY is the wrong length;
change it to fail fast: if os.Getenv("PAYMENT_CONFIG_ENCRYPTION_KEY") returns a
non-empty value and len != 32, immediately log.Fatal/ panic with a clear message
(including the env var name and expected length) so startup fails rather than
continuing with an unintended key; otherwise if len == 32 return []byte(key),
and only compute sha256(CryptoSecret) when the env var is unset/empty.

In `@controller/channel-test.go`:
- Around line 146-149: The branch detecting Seedream is currently case-sensitive
and should match model names like "Doubao-Seedream-..."; in
controller/channel-test.go update the check that sets requestPath (the if using
channel.Type, constant.ChannelTypeVolcEngine, and testModel) to perform a
case-insensitive match (e.g. compare a lowercased testModel to "seedream") so it
aligns with buildImageTestRequest() and the later fallback logic and avoids
relay/request-type mismatches.

In `@controller/channel.go`:
- Around line 45-50: The code currently ignores errors from existing.Update()
and the Insert() call, so AddChannel/UpdateChannel can succeed while model
metadata writes fail; modify the block that finds or creates model.Model (the
model.DB.Where(...).First(&existing).Error branch) to check the returned error
from existing.Update() and from (&model.Model{ModelName: modelName, ModelType:
modelType, Status: 1}).Insert(), and propagate or return those errors (or log
and fail the channel operation) instead of discarding them; ensure callers
AddChannel/UpdateChannel receive/handle the error so model_type stays in sync
with saved channel state.

In `@controller/model_meta.go`:
- Around line 20-30: Normalize the incoming query model_type before using it to
filter: call model.NormalizeModelType on the retrieved modelType (the local
variable) and use the normalized value when calling model.SearchModels; do the
same normalization in the other read path around the code that calls
model.SearchModels/GetAllModels (also applied to the block at lines handling the
second read path around the 56-61 area) so searches match normalized values
stored by Create/Update.

In `@controller/payment_webhook_availability.go`:
- Around line 113-136: The helpers isAlipayTopUpEnabled, isAlipayWebhookEnabled,
isWechatTopUpEnabled and isWechatWebhookEnabled currently return true if an
enabled config row exists; instead fetch the config via
model.GetEnabledPaymentConfigByProvider and validate that provider-specific
required fields are present/non-empty (e.g. Alipay: app_id, private_key,
cert_serial/certs as needed; WeChat: appid, mch_id, api_key/certificates,
serials) and only return true when those credentials required for top-up vs
webhook are populated; keep the existing isPaymentComplianceConfirmed() guard
for top-up checks and add explicit field checks (or call a new
config.IsValidForTopUp()/IsValidForWebhook() helper you add) before returning
true.

In `@model/main.go`:
- Around line 386-393: The migration adds the ModelType column but doesn't
backfill existing rows; update ensureModelTypeColumn to perform a backfill after
DB.Migrator().AddColumn(&Model{}, "ModelType") by running updates on the models
table to populate ModelType for legacy rows: infer "image" or "video" where
possible from existing Model fields (e.g., media-related counters/flags on the
Model struct) and then set any remaining NULL/empty values to "text"; use the
Model struct (DB.Model(&Model{})) and safe bulk updates (or raw SQL) targeting
rows where model_type IS NULL or empty to avoid touching already-migrated
records.

In `@model/model_meta.go`:
- Around line 28-43: NormalizeModelType currently coerces any unknown non-empty
input to ModelTypeText; change its signature to return (string, bool) so it
returns the normalized model type and a success flag: for empty input return
(ModelTypeText, true), for known values return (thatValue, true), and for
unknown non-empty input return ("", false). Then update callers (Insert, Update,
SearchModels) to check the bool and explicitly reject/return a validation error
when ok == false instead of silently rewriting the value to text.

In `@model/model_type_test.go`:
- Around line 13-20: The test helper currently sets DB = nil and toggles
common.UsingSQLite but does not clear or restore other backend flags; update the
helper to capture and restore the previous values of common.UsingPostgreSQL and
common.UsingMySQL (in addition to common.UsingSQLite and DB) so the harness
explicitly clears non-SQLite flags before the test (set common.UsingPostgreSQL =
false and common.UsingMySQL = false) and then restore all saved values in the
t.Cleanup closure; reference DB, common.UsingSQLite, common.UsingPostgreSQL,
common.UsingMySQL and the existing t.Cleanup to locate where to add the
saves/clears and restore logic.

In `@model/payment_config_test.go`:
- Around line 13-19: The test helper only snapshots common.UsingSQLite but must
snapshot and restore all DB backend flags plus DB to avoid misrouting
backend-specific branches; capture old values of common.UsingSQLite,
common.UsingMySQL, and common.UsingPostgreSQL (and DB), then set
common.UsingSQLite = true and the other two = false for the SQLite test, and
restore all four saved values in the t.Cleanup callback so the test leaves
global state unchanged.

In `@model/payment_config.go`:
- Around line 88-90: UpdatePaymentConfig currently uses DB.Save which can INSERT
if the row is missing; change it to perform a scoped update and fail when no row
was updated: set config.UpdatedAt = time.Now().Unix(), then call a scoped update
such as DB.Model(&PaymentConfig{}).Where("id = ?", config.ID).Updates(...) or
DB.Where("id = ?", config.ID).Updates(config) and check the returned
result.RowsAffected; if RowsAffected == 0 return a not-found/error so the
function does not recreate a deleted PaymentConfig. Ensure you still return any
DB.Error.

In `@relay/channel/volcengine/adaptor.go`:
- Around line 39-50: The code treats the bare "/api/plan" base as an agent-plan
base and thus builds incorrect non-v3 endpoints; change the detection to only
consider the v3 path as an agent plan base. Update isVolcengineAgentPlanBase to
return true only for the normalized
"https://ark.cn-beijing.volces.com/api/plan/v3" (remove the bare "/api/plan"
case) and ensure normalizeVolcengineBaseURL still normalizes trailing slashes so
buildVolcengineURL concatenation remains correct; run/update tests for
buildVolcengineURL and isVolcengineAgentPlanBase to cover both "/api/plan" and
"/api/plan/v3" inputs.

In `@web/default/src/features/channels/lib/channel-form.ts`:
- Around line 383-385: transformChannelToFormDefaults currently trusts
parsed.model_type and assigns it via "modelType = parsed.model_type as
ModelType"; instead validate and normalize that value against the allowed enum
values before using it (e.g., check parsed.model_type is one of
['text','embedding','image','file','audio','video']), and if not, set modelType
to a safe default like 'text'. Also update buildSettingsJSON to normalize
formData.model_type before writing settingsObj.model_type (so legacy
capitalizations or invalid strings don't get re-saved). Locate the logic in
transformChannelToFormDefaults and buildSettingsJSON and add a small
validation/normalization step around parsed.model_type and formData.model_type
to enforce the z.enum values.

In `@web/default/src/features/channels/lib/model-types.ts`:
- Around line 1-7: MODEL_TYPE_OPTIONS currently contains hard-coded English
labels; change it to store i18n keys (e.g., add a labelKey property or use the
existing value as the key) instead of user-facing strings, and update the
consumer in channel-mutate-drawer.tsx to render translated labels via the i18n
function (e.g., replace usages of option.label with t(option.labelKey) or
t(option.label)). Locate MODEL_TYPE_OPTIONS and add a labelKey for each entry
(or repurpose the value as the key), then modify the dropdown rendering in
channel-mutate-drawer.tsx to call t(...) for each option label so the UI is
translatable.

In `@web/default/src/features/models/components/models-columns.tsx`:
- Around line 156-163: The model type strings are rendered and used in the
filter without i18n; update the rendering and options to use localized labels by
mapping model_type keys to t(...) values (e.g. create a shared constant or
helper like MODEL_TYPE_LABELS or getModelTypeLabel(key) and use it in
models-columns.tsx where the cell currently returns <StatusBadge
label={modelType} ...> and in models-table.tsx where modelTypeOptions is
defined), replace verbatim labels with the mapped t(...) labels so both the
StatusBadge and the filter dropdown are localized and keep keys unchanged for
filtering logic.

In `@web/default/src/features/models/components/models-table.tsx`:
- Around line 225-233: The modelTypeOptions array in models-table.tsx currently
hardcodes English labels (Text, Embedding, Image, File, Audio, Video) so they
won't localize; update the modelTypeOptions definition to wrap each label with
the translation function t(), e.g., t('Text'), and consider centralizing this
mapping (e.g., export a getModelTypeOptions or MODEL_TYPE_LABELS constant) for
reuse across components like ModelsTable and any filters to avoid duplication.

In
`@web/default/src/features/system-settings/integrations/payment-config-dialog.tsx`:
- Around line 97-281: The Label elements lack htmlFor/id pairs which breaks
accessibility; add stable unique id attributes to each form control (Input,
Textarea, Switch) and set the corresponding Label's htmlFor to that id. For
example, for fields referenced by form.display_name, form.sort_order,
form.icon_url, form.app_id, form.app_private_key, form.alipay_public_key,
form.alipay_app_public_cert, form.alipay_public_cert, form.alipay_root_cert,
form.wechat_app_id, form.wechat_mch_id, form.wechat_api_key,
form.wechat_serial_no, form.wechat_private_key, form.gateway_url,
form.notify_url, form.return_url and the Switch bound to form.enabled, generate
stable ids (e.g. payment-display_name, payment-sort_order, payment-icon_url,
etc.), add id="<that-id>" to the Input/Textarea/Switch components and set each
Label htmlFor="<that-id>" so screen readers correctly associate labels with
controls.

In
`@web/default/src/features/system-settings/integrations/payment-settings-section.tsx`:
- Around line 1302-1312: When clicking a provider, don't open the payment-config
dialog when the fetch fails: call setCurrentPaymentProvider(item.provider) as
before, then await getPaymentConfigByProvider(item.provider) and on success call
setEditingPaymentConfig(response.data || null) and
setPaymentConfigDialogOpen(true); on catch, call handleServerError(error) and do
NOT call setEditingPaymentConfig(null) or setPaymentConfigDialogOpen(true) so
the empty editor isn't shown (if you want an explicit "create new" flow, surface
that as a separate action). Reference getPaymentConfigByProvider,
setEditingPaymentConfig, setPaymentConfigDialogOpen, setCurrentPaymentProvider,
and handleServerError when making this change.
- Around line 262-268: The query currently masks failures by returning [] when
getPaymentConfigs() returns success: false; update the useQuery queryFn (the
block using getPaymentConfigs within payment-configs) to call the shared error
handler and propagate the error instead of returning an empty array — e.g., call
handleServerError(response) (or pass the response/error into the shared handler)
and then throw so React Query routes to the global server-error path; ensure the
change references getPaymentConfigs, the payment-configs useQuery, and
handleServerError so failed loads are not treated as "no gateways."

---

Outside diff comments:
In `@controller/model_meta.go`:
- Line 4: Replace direct uses of json.Marshal for serializing endpoint lists
with the project's helper: call common.Marshal(eps) wherever json.Marshal(eps)
is used to build mm.Endpoints (including the occurrences that set mm.Endpoints
around the variables named eps and mm). Remove the unused "encoding/json" import
after switching to common.Marshal. Ensure mm.Endpoints is assigned the result of
common.Marshal(eps) (and handle any returned error the same way the existing
code expects).

---

Nitpick comments:
In `@controller/payment_config_gateway.go`:
- Around line 53-55: The yuanToFen function uses a +0.5 truncation trick which
is fragile for floating-point and negative values; replace the conversion with
int64(math.Round(amount*100)) in yuanToFen and add/ensure the math package is
imported so rounding is performed correctly and edge cases (including negatives)
are handled idiomatically.

In `@controller/payment_config_test.go`:
- Around line 96-140: Add a parallel test (or extend
TestUpdatePaymentConfigKeepsMaskedSecrets) that verifies masked-secret
preservation for WeChat: create a model.PaymentConfig for
model.PaymentProviderWeChat with encrypted wechat_api_key and wechat_private_key
using common.EncryptPaymentSecret, save it via model.CreatePaymentConfig, then
issue an update request via UpdatePaymentConfig with the same masked values
(e.g. "old-****") for wechat_api_key and wechat_private_key and an updated
non-secret field (e.g. Name), and finally fetch the stored config with
model.GetPaymentConfigByProvider and assert that decrypting stored
wechat_api_key/wechat_private_key (common.DecryptPaymentSecret) returns the
original secrets and that non-secret fields were updated. Ensure you reference
the same handler UpdatePaymentConfig and methods
CreatePaymentConfig/GetPaymentConfigByProvider used in the Alipay test.

In `@service/wechat_pay.go`:
- Around line 28-63: NewWechatPayClient currently uses the global
downloader.MgrInstance() which can cause cross-tenant conflicts when multiple
WeChat configs are registered; change the registration logic to avoid clobbering
global state by either (A) checking
downloader.MgrInstance().GetCertificateVisitor(config.WechatMchID) before
calling RegisterDownloaderWithPrivateKey and only register if no existing
visitor (if an existing visitor is found, verify it matches the current
serial/key and skip or return an error), or (B) create and use a dedicated
downloader instance per client (instead of MgrInstance()) that you pass to
verifiers.NewSHA256WithRSAVerifier and retain on the returned WechatPayClient;
update NewWechatPayClient to implement one of these strategies (use symbols:
NewWechatPayClient, downloader.MgrInstance(), RegisterDownloaderWithPrivateKey,
GetCertificateVisitor, verifiers.NewSHA256WithRSAVerifier) so multiple merchant
configs do not overwrite each other.

In `@web/default/src/features/models/types.ts`:
- Around line 97-108: Change the loose string-typed model_type fields to the
strict ModelType union: update SearchModelsParams.model_type (and the earlier
model list interface's model_type) to use ModelType instead of string so the
compiler can validate allowed values; ensure ModelType is in scope (it's
declared earlier in this file) and then rerun the typecheck command (bun run
typecheck) to confirm no remaining type errors.

In
`@web/default/src/features/subscriptions/components/dialogs/subscription-purchase-dialog.tsx`:
- Line 502: Extract the duplicated filter predicate used on props.epayMethods
((m) => !m.type?.startsWith('alipay_') && !m.type?.startsWith('wxpay_')) into a
named constant (e.g., nonChinaPayFilter or isNonAlipayWxpay) and reuse it in
both map calls where the current inline predicate appears; update the two
occurrences that call .filter(...) before .map(...) to reference that constant
so the logic is centralized and clearer (search for uses around
props.epayMethods and the inline arrow predicate).

In `@web/default/src/features/wallet/hooks/use-payment.ts`:
- Around line 101-113: Replace the 4-level nested ternary used to choose the
payment request with a clear conditional/dispatch structure: inspect the
booleans isStripe, isAlipay, isWechat and call the corresponding functions
(requestStripePayment, requestAlipayPayment, requestWechatPayment) otherwise
fall back to requestPayment; implement this as an if/else-if chain or a small
switch/map that builds the payload (amount, payment_method) and invokes the
selected function so the logic is readable and maintainable.
- Around line 61-69: The nested ternary that sets response is hard to read;
replace it with a small helper or an if/else chain that selects the correct
calculation function based on the flags (isStripe, isPancake, isAlipay,
isWechat) and then awaits that function (calculateStripeAmount,
calculateWaffoPancakeAmount, calculateAlipayAmount, calculateWechatAmount, or
calculateAmount) to assign response; e.g., create a selectAmountCalculator
helper or use a simple if/else block before assigning response so each branch is
explicit and readable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2f59bd40-576a-4aab-bc1d-7659437418ac

📥 Commits

Reviewing files that changed from the base of the PR and between b0ac042 and 97b18e1.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (53)
  • .gitignore
  • common/crypto.go
  • common/payment_crypto_test.go
  • controller/channel-test.go
  • controller/channel.go
  • controller/channel_test_model_type_test.go
  • controller/model_meta.go
  • controller/payment_config.go
  • controller/payment_config_gateway.go
  • controller/payment_config_test.go
  • controller/payment_webhook_availability.go
  • controller/subscription_payment_alipay.go
  • controller/subscription_payment_wechat.go
  • controller/topup.go
  • controller/topup_alipay.go
  • controller/topup_wechat.go
  • docs/superpowers/specs/2026-06-03-doubao-agent-plan-base-url-design.md
  • go.mod
  • model/main.go
  • model/model_meta.go
  • model/model_type_search_test.go
  • model/model_type_test.go
  • model/payment_config.go
  • model/payment_config_test.go
  • model/topup.go
  • relay/channel/volcengine/adaptor.go
  • relay/channel/volcengine/adaptor_agent_plan_test.go
  • restart.sh
  • router/api-router.go
  • service/alipay_pay.go
  • service/wechat_pay.go
  • start.sh
  • stop.sh
  • web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx
  • web/default/src/features/channels/lib/channel-form.ts
  • web/default/src/features/channels/lib/index.ts
  • web/default/src/features/channels/lib/model-types.ts
  • web/default/src/features/models/components/models-columns.tsx
  • web/default/src/features/models/components/models-table.tsx
  • web/default/src/features/models/types.ts
  • web/default/src/features/subscriptions/api.ts
  • web/default/src/features/subscriptions/components/dialogs/subscription-purchase-dialog.tsx
  • web/default/src/features/subscriptions/types.ts
  • web/default/src/features/system-settings/api.ts
  • web/default/src/features/system-settings/integrations/payment-config-dialog.tsx
  • web/default/src/features/system-settings/integrations/payment-settings-section.tsx
  • web/default/src/features/system-settings/types.ts
  • web/default/src/features/wallet/api.ts
  • web/default/src/features/wallet/constants.ts
  • web/default/src/features/wallet/hooks/use-payment.ts
  • web/default/src/features/wallet/lib/payment.ts
  • web/default/src/features/wallet/lib/ui.tsx
  • web/default/src/features/wallet/types.ts

Comment thread .gitignore
skills-lock.json
.playwright-mcp
35sz-api
.gitignore

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Remove the self-referential .gitignore entry.

Line 40 tells git to ignore the .gitignore file itself, which would prevent version control from tracking changes to the ignore rules. This line should be removed entirely.

🐛 Proposed fix
 35sz-api
-.gitignore
 35sz-api.pid
 35sz-api.log
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.gitignore at line 40, Remove the self-referential ignore entry that lists
".gitignore" so the repository will track changes to the ignore rules; locate
the literal ".gitignore" entry in the .gitignore file and delete that line
entirely (keep all other ignore patterns intact).

Comment thread common/crypto.go
Comment on lines +41 to +46
func getPaymentEncryptionKey() []byte {
if key := os.Getenv("PAYMENT_CONFIG_ENCRYPTION_KEY"); len(key) == 32 {
return []byte(key)
}
h := sha256.Sum256([]byte(CryptoSecret))
return h[:]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail fast on an invalid PAYMENT_CONFIG_ENCRYPTION_KEY.

A mis-sized env value currently falls back to sha256(CryptoSecret) silently. That means secrets can be encrypted with an unintended key, and once the operator fixes the env var later, previously stored ciphertext becomes undecryptable.

💡 Proposed fix
-func getPaymentEncryptionKey() []byte {
-	if key := os.Getenv("PAYMENT_CONFIG_ENCRYPTION_KEY"); len(key) == 32 {
-		return []byte(key)
-	}
-	h := sha256.Sum256([]byte(CryptoSecret))
-	return h[:]
+func getPaymentEncryptionKey() ([]byte, error) {
+	if key, ok := os.LookupEnv("PAYMENT_CONFIG_ENCRYPTION_KEY"); ok {
+		if len(key) != 32 {
+			return nil, fmt.Errorf("PAYMENT_CONFIG_ENCRYPTION_KEY must be exactly 32 bytes")
+		}
+		return []byte(key), nil
+	}
+	h := sha256.Sum256([]byte(CryptoSecret))
+	return h[:], nil
 }
-	block, err := aes.NewCipher(getPaymentEncryptionKey())
+	key, err := getPaymentEncryptionKey()
+	if err != nil {
+		return "", err
+	}
+	block, err := aes.NewCipher(key)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@common/crypto.go` around lines 41 - 46, getPaymentEncryptionKey currently
silently falls back to sha256(CryptoSecret) when PAYMENT_CONFIG_ENCRYPTION_KEY
is the wrong length; change it to fail fast: if
os.Getenv("PAYMENT_CONFIG_ENCRYPTION_KEY") returns a non-empty value and len !=
32, immediately log.Fatal/ panic with a clear message (including the env var
name and expected length) so startup fails rather than continuing with an
unintended key; otherwise if len == 32 return []byte(key), and only compute
sha256(CryptoSecret) when the env var is unset/empty.

Comment thread common/crypto.go
Comment on lines +90 to +98
func MaskSecret(secret string) string {
if len(secret) <= 4 {
return "****"
}
return secret[:4] + "****"
}

func IsMaskedSecret(secret string) bool {
return len(secret) > 4 && secret[len(secret)-4:] == "****"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

MaskSecret and IsMaskedSecret disagree for short secrets.

MaskSecret("abcd") returns "****", but IsMaskedSecret("****") is false. Any update flow that preserves masked values will treat a 4-character secret as a brand-new literal and overwrite the stored secret.

💡 Proposed fix
 func IsMaskedSecret(secret string) bool {
-	return len(secret) > 4 && secret[len(secret)-4:] == "****"
+	return secret == "****" || (len(secret) > 4 && secret[len(secret)-4:] == "****")
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func MaskSecret(secret string) string {
if len(secret) <= 4 {
return "****"
}
return secret[:4] + "****"
}
func IsMaskedSecret(secret string) bool {
return len(secret) > 4 && secret[len(secret)-4:] == "****"
func MaskSecret(secret string) string {
if len(secret) <= 4 {
return "****"
}
return secret[:4] + "****"
}
func IsMaskedSecret(secret string) bool {
return secret == "****" || (len(secret) > 4 && secret[len(secret)-4:] == "****")
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@common/crypto.go` around lines 90 - 98, MaskSecret and IsMaskedSecret
disagree for short secrets; update MaskSecret (function MaskSecret) so that for
secrets with length <= 4 it returns the entire secret concatenated with "****"
(e.g., secret + "****") instead of just "****", while keeping the existing
behavior for longer secrets (secret[:4] + "****"); this makes IsMaskedSecret
(which checks suffix == "****") correctly detect masked short secrets without
changing IsMaskedSecret logic.

Comment on lines +146 to +149
// VolcEngine 图像生成模型
if channel.Type == constant.ChannelTypeVolcEngine && strings.Contains(testModel, "seedream") {
requestPath = "/v1/images/generations"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make Seedream path detection case-insensitive.

This branch is currently case-sensitive, while buildImageTestRequest() and the later automatic request fallback both lowercase the model name. A model like Doubao-Seedream-... can therefore keep /v1/chat/completions here but still build a *dto.ImageRequest, which breaks channel tests with a relay/request-type mismatch.

🐛 Suggested change
-			if channel.Type == constant.ChannelTypeVolcEngine && strings.Contains(testModel, "seedream") {
+			if channel.Type == constant.ChannelTypeVolcEngine && strings.Contains(strings.ToLower(testModel), "seedream") {
 				requestPath = "/v1/images/generations"
 			}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/channel-test.go` around lines 146 - 149, The branch detecting
Seedream is currently case-sensitive and should match model names like
"Doubao-Seedream-..."; in controller/channel-test.go update the check that sets
requestPath (the if using channel.Type, constant.ChannelTypeVolcEngine, and
testModel) to perform a case-insensitive match (e.g. compare a lowercased
testModel to "seedream") so it aligns with buildImageTestRequest() and the later
fallback logic and avoids relay/request-type mismatches.

Comment thread controller/channel.go
Comment on lines +45 to +50
if err := model.DB.Where("model_name = ?", modelName).First(&existing).Error; err == nil {
existing.ModelType = modelType
_ = existing.Update()
continue
}
_ = (&model.Model{ModelName: modelName, ModelType: modelType, Status: 1}).Insert()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t swallow model-metadata write failures.

existing.Update() and Insert() are both ignored here, so AddChannel/UpdateChannel can return success while the new model_type never reaches model.Model. That leaves the new model-type filters and badges out of sync with the saved channel state.

Suggested fix
-func syncChannelModelMetadata(channel *model.Channel) {
+func syncChannelModelMetadata(channel *model.Channel) error {
 	if channel == nil {
-		return
+		return nil
 	}
 	var settings map[string]any
 	if strings.TrimSpace(channel.OtherSettings) != "" {
-		_ = common.UnmarshalJsonStr(channel.OtherSettings, &settings)
+		if err := common.UnmarshalJsonStr(channel.OtherSettings, &settings); err != nil {
+			return err
+		}
 	}
 	modelType := model.ModelTypeText
 	if settings != nil {
 		if raw, ok := settings["model_type"].(string); ok {
 			modelType = model.NormalizeModelType(raw)
@@
 		var existing model.Model
 		if err := model.DB.Where("model_name = ?", modelName).First(&existing).Error; err == nil {
 			existing.ModelType = modelType
-			_ = existing.Update()
-			continue
+			if err := existing.Update(); err != nil {
+				return err
+			}
+		} else {
+			if err := (&model.Model{ModelName: modelName, ModelType: modelType, Status: 1}).Insert(); err != nil {
+				return err
+			}
 		}
-		_ = (&model.Model{ModelName: modelName, ModelType: modelType, Status: 1}).Insert()
 	}
+	return nil
 }
-	for i := range channels {
-		syncChannelModelMetadata(&channels[i])
-	}
+	for i := range channels {
+		if err := syncChannelModelMetadata(&channels[i]); err != nil {
+			common.ApiError(c, err)
+			return
+		}
+	}
-	syncChannelModelMetadata(&channel.Channel)
+	if err := syncChannelModelMetadata(&channel.Channel); err != nil {
+		common.ApiError(c, err)
+		return
+	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if err := model.DB.Where("model_name = ?", modelName).First(&existing).Error; err == nil {
existing.ModelType = modelType
_ = existing.Update()
continue
}
_ = (&model.Model{ModelName: modelName, ModelType: modelType, Status: 1}).Insert()
if err := model.DB.Where("model_name = ?", modelName).First(&existing).Error; err == nil {
existing.ModelType = modelType
if err := existing.Update(); err != nil {
return err
}
} else {
if err := (&model.Model{ModelName: modelName, ModelType: modelType, Status: 1}).Insert(); err != nil {
return err
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/channel.go` around lines 45 - 50, The code currently ignores
errors from existing.Update() and the Insert() call, so AddChannel/UpdateChannel
can succeed while model metadata writes fail; modify the block that finds or
creates model.Model (the model.DB.Where(...).First(&existing).Error branch) to
check the returned error from existing.Update() and from
(&model.Model{ModelName: modelName, ModelType: modelType, Status: 1}).Insert(),
and propagate or return those errors (or log and fail the channel operation)
instead of discarding them; ensure callers AddChannel/UpdateChannel
receive/handle the error so model_type stays in sync with saved channel state.

Comment on lines +156 to +163
// Model Type column
{
accessorKey: 'model_type',
meta: { label: t('Model Type'), mobileBadge: true },
header: t('Model Type'),
cell: ({ row }) => {
const modelType = row.original.model_type || 'text'
return <StatusBadge label={modelType} variant='info' size='sm' />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and print the relevant TSX lines for the reviewed snippet
file="web/default/src/features/models/components/models-columns.tsx"
if [ -f "$file" ]; then
  echo "=== File: $file ==="
  nl -ba "$file" | sed -n '130,220p'
else
  echo "Missing file: $file"
fi

# Search for how model_type is labeled/translated elsewhere in this feature
rg -n "model_type" web/default/src/features/models -S || true
rg -n "Model Type|model type|ModelType" web/default/src/features/models -S || true

# Check if StatusBadge translates its label prop internally
rg -n "function StatusBadge|const StatusBadge|StatusBadge" web/default/src -S || true
rg -n "StatusBadge\\s*\\(" web/default/src -S || true

# Check for a shared mapping/constants for model_type labels
fd -t f web/default/src/features/models -e ts -e tsx -e constants.ts -e types.ts | head -n 50
rg -n "labelKey|t\\(" web/default/src/features/models -S || true
rg -n "model.*type.*label|label.*model.*type|modelType" web/default/src/features/models -S || true

Repository: QuantumNous/new-api

Length of output: 200


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="web/default/src/features/models/components/models-columns.tsx"
echo "=== Exists? $file"
test -f "$file" && echo "yes" || { echo "no"; exit 0; }

echo "=== Snippet: $file (around 150-180)"
# Print with line numbers using cat -n (nl may be missing)
cat -n "$file" | sed -n '140,190p'

echo "=== Searching for model_type labeling/mapping in models feature"
rg -n "model_type" web/default/src/features/models -S || true
rg -n "Model Type|model type|ModelType" web/default/src/features/models -S || true
rg -n "labelKey|t\\(" web/default/src/features/models -S || true

echo "=== Locating StatusBadge implementation"
rg -n "function StatusBadge|const StatusBadge|StatusBadge\\s*=" web/default/src -S || true
rg -n "StatusBadge\\s*\\(" web/default/src -S || true

echo "=== Locating StatusBadge prop usage for label vs labelKey"
rg -n "StatusBadge\\s*[^\\n]*labelKey|labelKey\\s*=|label\\s*=" web/default/src -S || true

Repository: QuantumNous/new-api

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Targeted line-range prints to avoid huge output
echo "=== models-table.tsx: find model_type filter option construction ==="
file="web/default/src/features/models/components/models-table.tsx"
test -f "$file" && echo "exists: $file" || { echo "missing $file"; exit 0; }

# Show only the areas referencing 'model_type' and 'title: t('Model Type')'
rg -n "model_type" "$file" -S
echo "--- context around model_type references ---"
rg -n "model_type" "$file" -S -C 3 --no-heading | head -n 200

echo "=== models-table.tsx: inspect where model_type filter options are built (search for 'all types' etc.) ==="
rg -n "All Types|Model Type|type.*filter|filter.*type" "$file" -S || true
rg -n "columnId: 'model_type'" "$file" -S -C 3 --no-heading || true

echo "=== StatusBadge implementation (label vs labelKey) ==="
# Narrow search
rg -n "StatusBadge" web/default/src -S --glob='*.tsx' --glob='*.ts' | head -n 50

# If found, inspect its props interface quickly
badge_files=$(rg -l "StatusBadge\\b" web/default/src --glob='*.tsx' --glob='*.ts' | tr '\n' ' ')
echo "badge_files(first up to 10):"
echo "$badge_files" | cut -c1-500

# Print the first likely definition region (limit output)
for f in $(rg -l "export (function|const) StatusBadge|function StatusBadge|const StatusBadge" web/default/src --glob='*.tsx' --glob='*.ts' | head -n 10); do
  echo "=== Inspecting StatusBadge candidate: $f ==="
  rg -n "StatusBadge" "$f" -S -C 2 --no-heading | head -n 60
done

echo "=== model type label mapping candidates in models feature ==="
# Look for explicit literal model types
rg -n "'text'|'embedding'|'image'|'file'|'audio'|'video'" web/default/src/features/models -S || true

# Look for any mapping keyed by ModelType
rg -n "Record<.*ModelType|modelType.*label|MODEL_.*TYPE|ModelType.*(label|t\\()" web/default/src/features/models -S || true

Repository: QuantumNous/new-api

Length of output: 12542


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Print relevant segments with line numbers
echo "=== models-table.tsx: modelTypeOptions definition ==="
file="web/default/src/features/models/components/models-table.tsx"
cat -n "$file" | sed -n '200,260p'

echo "=== status-badge.tsx: label / labelKey behavior ==="
file2="web/default/src/components/status-badge.tsx"
cat -n "$file2" | sed -n '60,190p'

Repository: QuantumNous/new-api

Length of output: 6516


🏁 Script executed:

#!/bin/bash
set -euo pipefail
file="web/default/src/features/models/constants.ts"
test -f "$file" && echo "exists" || echo "missing"
rg -n "'text'|'embedding'|'image'|'file'|'audio'|'video'|ModelType|model_type" "$file" -S || true

Repository: QuantumNous/new-api

Length of output: 70


Translate model-type badge (and filter options) labels

  • models-columns.tsx: StatusBadge renders label verbatim, so row.original.model_type (text, embedding, ...) is not i18n-localized—map model types to t(...) labels (shared with the filter).
  • models-table.tsx: modelTypeOptions hardcodes 'Text'|'Embedding'|'Image'|'File'|'Audio'|'Video' without t(), so the model-type filter dropdown won’t be localized either.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/models/components/models-columns.tsx` around lines
156 - 163, The model type strings are rendered and used in the filter without
i18n; update the rendering and options to use localized labels by mapping
model_type keys to t(...) values (e.g. create a shared constant or helper like
MODEL_TYPE_LABELS or getModelTypeLabel(key) and use it in models-columns.tsx
where the cell currently returns <StatusBadge label={modelType} ...> and in
models-table.tsx where modelTypeOptions is defined), replace verbatim labels
with the mapped t(...) labels so both the StatusBadge and the filter dropdown
are localized and keep keys unchanged for filtering logic.

Comment on lines +225 to +233
const modelTypeOptions = [
{ label: t('All Types'), value: 'all' },
{ label: 'Text', value: 'text' },
{ label: 'Embedding', value: 'embedding' },
{ label: 'Image', value: 'image' },
{ label: 'File', value: 'file' },
{ label: 'Audio', value: 'audio' },
{ label: 'Video', value: 'video' },
]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="web/default/src/features/models/components/models-table.tsx"

# Show surrounding code for the modelTypeOptions block
sed -n '190,270p' "$FILE" | cat -n

# Search for where model type strings are rendered in the table/cells
rg -n --fixed-string "'text'" "$FILE" || true
rg -n --fixed-string "'embedding'" "$FILE" || true
rg -n --fixed-string "'image'" "$FILE" || true
rg -n --fixed-string "'file'" "$FILE" || true
rg -n --fixed-string "'audio'" "$FILE" || true
rg -n --fixed-string "'video'" "$FILE" || true

# Show any model type label map / render function
rg -n "modelType|model type|typeOptions|embedding|audio|video|Image|File|Text|All Types" "$FILE" || true

Repository: QuantumNous/new-api

Length of output: 6391


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="web/default/src/features/models/components/models-table.tsx"

# Search for raw English model-type labels anywhere else in the file
rg -n "label:\s*'Text'|label:\s*'Embedding'|label:\s*'Image'|label:\s*'File'|label:\s*'Audio'|label:\s*'Video'" "$FILE" || true
rg -n "'text'|'embedding'|'image'|'file'|'audio'|'video'" "$FILE" || true

# Show the model type filter configuration area
sed -n '260,320p' "$FILE" | cat -n

# If there are helper functions nearby for model type display, capture them too
sed -n '1,120p' "$FILE" | cat -n
sed -n '120,220p' "$FILE" | cat -n

Repository: QuantumNous/new-api

Length of output: 10376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Focus on the models feature folder
FEATURE_DIR="web/default/src/features/models/components"

# Find where model type labels are displayed (not just filter options)
rg -n "model_type" "$FEATURE_DIR" || true
rg -n "Text|Embedding|Image|File|Audio|Video" "$FEATURE_DIR" || true

# Inspect the columns implementation used by this table
COLS_FILE="$FEATURE_DIR/models-columns.tsx"
if [ -f "$COLS_FILE" ]; then
  sed -n '1,220p' "$COLS_FILE" | cat -n
  sed -n '220,520p' "$COLS_FILE" | cat -n
fi

# Check nearby constants for any i18n helpers for model types
CONST_DIR="web/default/src/features/models"
rg -n "model type|model_type|embedding|audio|video|file|image|text" "$CONST_DIR" -S || true

Repository: QuantumNous/new-api

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for any model-type localization/config helpers in the models feature
FEATURE_DIR="web/default/src/features/models"

rg -n "MODEL_TYPE|getModelType|modelTypeOptions|ModelType|model type options|model_type.*label" "$FEATURE_DIR" || true

# Search for translation usages of the canonical model type strings
rg -n "t\('(?:Text|Embedding|Image|File|Audio|Video)'\)|t\('text'\)|t\('embedding'\)|t\('image'\)|t\('file'\)|t\('audio'\)|t\('video'\)" "$FEATURE_DIR" || true

# Confirm the exact model type cell rendering logic
sed -n '130,190p' web/default/src/features/models/components/models-columns.tsx | cat -n

Repository: QuantumNous/new-api

Length of output: 2862


Localize the new model type filter options.

Only All Types is wrapped with t(). The Text/Embedding/Image/File/Audio/Video option labels are hard-coded English strings, so they won’t translate—wrap each label in t() (and ideally centralize the mapping for reuse).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/models/components/models-table.tsx` around lines 225
- 233, The modelTypeOptions array in models-table.tsx currently hardcodes
English labels (Text, Embedding, Image, File, Audio, Video) so they won't
localize; update the modelTypeOptions definition to wrap each label with the
translation function t(), e.g., t('Text'), and consider centralizing this
mapping (e.g., export a getModelTypeOptions or MODEL_TYPE_LABELS constant) for
reuse across components like ModelsTable and any filters to avoid duplication.

Comment on lines +97 to +281
<div className='grid gap-4 py-2'>
<div className='grid gap-2 sm:grid-cols-2'>
<div className='space-y-2'>
<Label>{t('Display name')}</Label>
<Input
value={form.display_name || ''}
onChange={(event) => setValue('display_name', event.target.value)}
/>
</div>
<div className='space-y-2'>
<Label>{t('Sort order')}</Label>
<Input
type='number'
value={form.sort_order ?? 0}
onChange={(event) =>
setValue('sort_order', Number(event.target.value) || 0)
}
/>
</div>
</div>

<div className='space-y-2'>
<Label>{t('Icon URL')}</Label>
<Input
value={form.icon_url || ''}
onChange={(event) => setValue('icon_url', event.target.value)}
placeholder='https://example.com/payment-icon.png'
/>
</div>

{isAlipay ? (
<>
<div className='space-y-2'>
<Label>{t('App ID')}</Label>
<Input
value={form.app_id || ''}
onChange={(event) => setValue('app_id', event.target.value)}
/>
</div>
<div className='space-y-2'>
<Label>{t('App private key')}</Label>
<Textarea
rows={4}
value={form.app_private_key || ''}
onChange={(event) =>
setValue('app_private_key', event.target.value)
}
placeholder={t('Enter new key to update')}
/>
</div>
<div className='space-y-2'>
<Label>{t('Alipay public key')}</Label>
<Textarea
rows={4}
value={form.alipay_public_key || ''}
onChange={(event) =>
setValue('alipay_public_key', event.target.value)
}
/>
</div>
<div className='grid gap-2 sm:grid-cols-3'>
<div className='space-y-2'>
<Label>{t('App public cert')}</Label>
<Textarea
rows={3}
value={form.alipay_app_public_cert || ''}
onChange={(event) =>
setValue('alipay_app_public_cert', event.target.value)
}
/>
</div>
<div className='space-y-2'>
<Label>{t('Alipay public cert')}</Label>
<Textarea
rows={3}
value={form.alipay_public_cert || ''}
onChange={(event) =>
setValue('alipay_public_cert', event.target.value)
}
/>
</div>
<div className='space-y-2'>
<Label>{t('Alipay root cert')}</Label>
<Textarea
rows={3}
value={form.alipay_root_cert || ''}
onChange={(event) =>
setValue('alipay_root_cert', event.target.value)
}
/>
</div>
</div>
</>
) : (
<>
<div className='grid gap-2 sm:grid-cols-2'>
<div className='space-y-2'>
<Label>{t('App ID')}</Label>
<Input
value={form.wechat_app_id || ''}
onChange={(event) =>
setValue('wechat_app_id', event.target.value)
}
/>
</div>
<div className='space-y-2'>
<Label>{t('Merchant ID')}</Label>
<Input
value={form.wechat_mch_id || ''}
onChange={(event) =>
setValue('wechat_mch_id', event.target.value)
}
/>
</div>
</div>
<div className='grid gap-2 sm:grid-cols-2'>
<div className='space-y-2'>
<Label>{t('APIv3 Key')}</Label>
<Input
type='password'
value={form.wechat_api_key || ''}
onChange={(event) =>
setValue('wechat_api_key', event.target.value)
}
/>
</div>
<div className='space-y-2'>
<Label>{t('Certificate Serial No')}</Label>
<Input
value={form.wechat_serial_no || ''}
onChange={(event) =>
setValue('wechat_serial_no', event.target.value)
}
/>
</div>
</div>
<div className='space-y-2'>
<Label>{t('Merchant private key')}</Label>
<Textarea
rows={4}
value={form.wechat_private_key || ''}
onChange={(event) =>
setValue('wechat_private_key', event.target.value)
}
/>
</div>
</>
)}

<div className='grid gap-2 sm:grid-cols-3'>
<div className='space-y-2'>
<Label>{t('Gateway URL')}</Label>
<Input
value={form.gateway_url || ''}
onChange={(event) => setValue('gateway_url', event.target.value)}
/>
</div>
<div className='space-y-2'>
<Label>{t('Notify URL')}</Label>
<Input
value={form.notify_url || ''}
onChange={(event) => setValue('notify_url', event.target.value)}
/>
</div>
<div className='space-y-2'>
<Label>{t('Return URL')}</Label>
<Input
value={form.return_url || ''}
onChange={(event) => setValue('return_url', event.target.value)}
/>
</div>
</div>

<div className='flex items-center justify-between rounded-lg border p-3'>
<div>
<Label>{t('Enable payment gateway')}</Label>
<p className='text-muted-foreground text-xs'>
{t('Enabled gateways are shown to users on wallet and subscription pages.')}
</p>
</div>
<Switch
checked={form.enabled}
onCheckedChange={(checked) => setValue('enabled', checked)}
/>
</div>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Bind each Label to its control.

These labels are rendered without matching htmlFor/id pairs, so screen readers won't reliably announce field names across the dialog. Add stable ids for each Input/Textarea/Switch and point the corresponding Label at them. As per coding guidelines, associate form inputs with label elements.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@web/default/src/features/system-settings/integrations/payment-config-dialog.tsx`
around lines 97 - 281, The Label elements lack htmlFor/id pairs which breaks
accessibility; add stable unique id attributes to each form control (Input,
Textarea, Switch) and set the corresponding Label's htmlFor to that id. For
example, for fields referenced by form.display_name, form.sort_order,
form.icon_url, form.app_id, form.app_private_key, form.alipay_public_key,
form.alipay_app_public_cert, form.alipay_public_cert, form.alipay_root_cert,
form.wechat_app_id, form.wechat_mch_id, form.wechat_api_key,
form.wechat_serial_no, form.wechat_private_key, form.gateway_url,
form.notify_url, form.return_url and the Switch bound to form.enabled, generate
stable ids (e.g. payment-display_name, payment-sort_order, payment-icon_url,
etc.), add id="<that-id>" to the Input/Textarea/Switch components and set each
Label htmlFor="<that-id>" so screen readers correctly associate labels with
controls.

Comment on lines +262 to +268
const { data: paymentConfigs = [], refetch: refetchPaymentConfigs } = useQuery({
queryKey: ['payment-configs'],
queryFn: async () => {
const response = await getPaymentConfigs()
return response.success ? response.data || [] : []
},
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't turn failed config loads into an empty list.

When getPaymentConfigs() returns success: false, this query renders the section as if no gateways exist. That hides backend/auth failures and can push admins into duplicate-create flows from a false "Not configured" state. Throw here and route the failure through the shared server-error path instead of returning []. Based on learnings: handle server errors uniformly via handleServerError.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@web/default/src/features/system-settings/integrations/payment-settings-section.tsx`
around lines 262 - 268, The query currently masks failures by returning [] when
getPaymentConfigs() returns success: false; update the useQuery queryFn (the
block using getPaymentConfigs within payment-configs) to call the shared error
handler and propagate the error instead of returning an empty array — e.g., call
handleServerError(response) (or pass the response/error into the shared handler)
and then throw so React Query routes to the global server-error path; ensure the
change references getPaymentConfigs, the payment-configs useQuery, and
handleServerError so failed loads are not treated as "no gateways."

Comment on lines +1302 to +1312
onClick={async () => {
setCurrentPaymentProvider(item.provider)
try {
const response = await getPaymentConfigByProvider(
item.provider
)
setEditingPaymentConfig(response.data || null)
} catch {
setEditingPaymentConfig(null)
}
setPaymentConfigDialogOpen(true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid opening a blank editor after a fetch failure.

The catch path clears editingPaymentConfig and still opens the dialog. If loading an existing provider fails, the user gets a create-form fallback, and the save path flips from update to create even though a config already exists. Surface the error and keep the dialog closed unless the fetch succeeds or you explicitly want a create flow. Based on learnings: handle server errors uniformly via handleServerError.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@web/default/src/features/system-settings/integrations/payment-settings-section.tsx`
around lines 1302 - 1312, When clicking a provider, don't open the
payment-config dialog when the fetch fails: call
setCurrentPaymentProvider(item.provider) as before, then await
getPaymentConfigByProvider(item.provider) and on success call
setEditingPaymentConfig(response.data || null) and
setPaymentConfigDialogOpen(true); on catch, call handleServerError(error) and do
NOT call setEditingPaymentConfig(null) or setPaymentConfigDialogOpen(true) so
the empty editor isn't shown (if you want an explicit "create new" flow, surface
that as a separate action). Reference getPaymentConfigByProvider,
setEditingPaymentConfig, setPaymentConfigDialogOpen, setCurrentPaymentProvider,
and handleServerError when making this change.

@seefs001

seefs001 commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

不接受支付PR

@seefs001 seefs001 closed this Jun 4, 2026
@kavoj kavoj changed the title 1、修复系统不支持image和video的问题 个人功能开发 Jun 4, 2026
@kavoj

kavoj commented Jun 4, 2026

Copy link
Copy Markdown
Author

我以为提交在我自己的仓库里,提交错了,这个pull requests怎么删除

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants