个人功能开发 - #5297
Conversation
2、增加支付功能、模型类型标签、启动脚本
WalkthroughThis 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. ChangesPayment Infrastructure and Configuration
Payment Endpoints and Top-Up/Subscription Integration
Model Type System
VolcEngine Agent Plan Support
Frontend Payment Configuration
Frontend Model Type and Wallet Payments
Routing and Operations
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
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 winUse
common.Marshal()formm.Endpointsserialization incontroller/model_meta.go.
controller/model_meta.goimportsencoding/jsonand usesjson.Marshal(eps)to buildmm.Endpoints; replace withcommon.Marshal(eps)and remove theencoding/jsonimport.- 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 valueConsider using
math.Roundfor more robust currency conversion.The
+0.5truncation trick can have edge cases with floating-point representation. Usingmath.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 tradeoffGlobal 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 winAdd 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_keyhandling 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 valueConsider 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 SelectContentAlso 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 winRefactor 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 winRefactor 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 winNarrow
model_typetoModelType.This file introduces the
ModelTypeunion on Line 25, but both request interfaces still accept anystring. UsingModelTypehere 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 typecheckafter 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
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (53)
.gitignorecommon/crypto.gocommon/payment_crypto_test.gocontroller/channel-test.gocontroller/channel.gocontroller/channel_test_model_type_test.gocontroller/model_meta.gocontroller/payment_config.gocontroller/payment_config_gateway.gocontroller/payment_config_test.gocontroller/payment_webhook_availability.gocontroller/subscription_payment_alipay.gocontroller/subscription_payment_wechat.gocontroller/topup.gocontroller/topup_alipay.gocontroller/topup_wechat.godocs/superpowers/specs/2026-06-03-doubao-agent-plan-base-url-design.mdgo.modmodel/main.gomodel/model_meta.gomodel/model_type_search_test.gomodel/model_type_test.gomodel/payment_config.gomodel/payment_config_test.gomodel/topup.gorelay/channel/volcengine/adaptor.gorelay/channel/volcengine/adaptor_agent_plan_test.gorestart.shrouter/api-router.goservice/alipay_pay.goservice/wechat_pay.gostart.shstop.shweb/default/src/features/channels/components/drawers/channel-mutate-drawer.tsxweb/default/src/features/channels/lib/channel-form.tsweb/default/src/features/channels/lib/index.tsweb/default/src/features/channels/lib/model-types.tsweb/default/src/features/models/components/models-columns.tsxweb/default/src/features/models/components/models-table.tsxweb/default/src/features/models/types.tsweb/default/src/features/subscriptions/api.tsweb/default/src/features/subscriptions/components/dialogs/subscription-purchase-dialog.tsxweb/default/src/features/subscriptions/types.tsweb/default/src/features/system-settings/api.tsweb/default/src/features/system-settings/integrations/payment-config-dialog.tsxweb/default/src/features/system-settings/integrations/payment-settings-section.tsxweb/default/src/features/system-settings/types.tsweb/default/src/features/wallet/api.tsweb/default/src/features/wallet/constants.tsweb/default/src/features/wallet/hooks/use-payment.tsweb/default/src/features/wallet/lib/payment.tsweb/default/src/features/wallet/lib/ui.tsxweb/default/src/features/wallet/types.ts
| skills-lock.json | ||
| .playwright-mcp | ||
| 35sz-api | ||
| .gitignore |
There was a problem hiding this comment.
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).
| func getPaymentEncryptionKey() []byte { | ||
| if key := os.Getenv("PAYMENT_CONFIG_ENCRYPTION_KEY"); len(key) == 32 { | ||
| return []byte(key) | ||
| } | ||
| h := sha256.Sum256([]byte(CryptoSecret)) | ||
| return h[:] |
There was a problem hiding this comment.
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.
| 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:] == "****" |
There was a problem hiding this comment.
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.
| 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.
| // VolcEngine 图像生成模型 | ||
| if channel.Type == constant.ChannelTypeVolcEngine && strings.Contains(testModel, "seedream") { | ||
| requestPath = "/v1/images/generations" | ||
| } |
There was a problem hiding this comment.
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.
| 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() |
There was a problem hiding this comment.
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.
| 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.
| // 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' /> |
There was a problem hiding this comment.
🧩 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 || trueRepository: 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 || trueRepository: 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 || trueRepository: 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 || trueRepository: QuantumNous/new-api
Length of output: 70
Translate model-type badge (and filter options) labels
models-columns.tsx:StatusBadgerenderslabelverbatim, sorow.original.model_type(text,embedding, ...) is not i18n-localized—map model types tot(...)labels (shared with the filter).models-table.tsx:modelTypeOptionshardcodes'Text'|'Embedding'|'Image'|'File'|'Audio'|'Video'withoutt(), 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.
| 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' }, | ||
| ] |
There was a problem hiding this comment.
🧩 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" || trueRepository: 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 -nRepository: 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 || trueRepository: 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 -nRepository: 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.
| <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> |
There was a problem hiding this comment.
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.
| const { data: paymentConfigs = [], refetch: refetchPaymentConfigs } = useQuery({ | ||
| queryKey: ['payment-configs'], | ||
| queryFn: async () => { | ||
| const response = await getPaymentConfigs() | ||
| return response.success ? response.data || [] : [] | ||
| }, | ||
| }) |
There was a problem hiding this comment.
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."
| onClick={async () => { | ||
| setCurrentPaymentProvider(item.provider) | ||
| try { | ||
| const response = await getPaymentConfigByProvider( | ||
| item.provider | ||
| ) | ||
| setEditingPaymentConfig(response.data || null) | ||
| } catch { | ||
| setEditingPaymentConfig(null) | ||
| } | ||
| setPaymentConfigDialogOpen(true) |
There was a problem hiding this comment.
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.
|
不接受支付PR |
|
我以为提交在我自己的仓库里,提交错了,这个pull requests怎么删除 |
📝 变更描述 / 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、前端类型检查与构建均已通过对应验证。