Commit Graph
4026 Commits
Author SHA1 Message Date
erio 44d521e2d2 chore: bump version to 0.1.114.18 2026-04-20 18:52:33 +08:00
erio 6dd578fe80 feat(payment): localize config-field names in error messages
之前错误提示里直接显示原始字段名(certSerial / apiV3Key),用户看不懂。

在 extractI18nErrorMessage 内加 localizeMetadata:自动把 metadata.key 和 metadata.keys(斜杠分隔)对应到 admin.settings.payment.field_<key> 的 i18n 标签,查不到才回退原文。

- 「微信支付配置缺少必填项:certSerial」 → 「微信支付配置缺少必填项:证书序列号」
- 「publicKey/publicKeyId must be provided together」 → 「公钥 / 公钥 ID 必须同时配置」
2026-04-20 18:52:20 +08:00
erio 89f1fe0b4e chore: bump version to 0.1.114.17 2026-04-20 18:45:55 +08:00
erio f2c6687cbf feat(payment): structured error codes + frontend i18n mapping
后端返回结构化错误码(reason + metadata 关键词),前端按 reason 查 i18n 文案并用 metadata 填充占位符,用户看到本地化提示而不是英文原始错误。

Backend:
- wxpay.go: 校验错误从 fmt.Errorf 改为 infraerrors.BadRequest 结构化,新增 reason 码:
  - WXPAY_CONFIG_MISSING_KEY (metadata: key)
  - WXPAY_CONFIG_INVALID_KEY_LENGTH (metadata: key, expected, actual)
  - WXPAY_CONFIG_PAIR_VIOLATION (metadata: keys)
- payment_order.go: invokeProvider 用 errors.As 识别 ApplicationError 透传(保留 wxpay 的 reason/metadata),只有非结构化 err 才 fallback 到 PAYMENT_PROVIDER_MISCONFIGURED;其他几个用户可见错误(TOO_MANY_PENDING / DAILY_LIMIT_EXCEEDED / PAYMENT_GATEWAY_ERROR / NO_AVAILABLE_INSTANCE)message 简化为关键词,参数放 metadata
- wxpay_test.go: 更新断言匹配新的 reason code

Frontend:
- apiError.ts: extractApiErrorCode 优先返回 reason(字符串错误码)而非 HTTP 数字;新增 extractI18nErrorMessage 按 namespace.reason 查 i18n 并以 metadata 作模板变量
- 12 个支付相关 Vue 文件(用户/管理/组件)统一改用 extractI18nErrorMessage('payment.errors', ...)
- i18n zh/en locales 下的 payment.errors 添加完整错误码文案(PAYMENT_DISABLED / INVALID_AMOUNT / TOO_MANY_PENDING / DAILY_LIMIT_EXCEEDED / WXPAY_CONFIG_* 等共 20+ 条,支持 {max}/{remaining}/{key} 等占位符)
- 移除 SettingsView 中已失效的 paymentErrorMap computed
2026-04-20 18:45:30 +08:00
erio 14bb922a92 chore: bump version to 0.1.114.16 2026-04-20 18:20:31 +08:00
erio d635b66c29 feat(payment): support wxpay legacy platform-certificate verifier mode
之前的改动只保留了微信支付"新公钥验签"路径,对仍在用平台证书的老商户不可用。

- wxpay.go: publicKeyId 从必填移除;buildVerifier 根据是否配置 publicKeyId 自动切换:
  - 有 → 新公钥方式(NewSHA256WithRSAPubkeyVerifier + 配置的 publicKey)
  - 无 → 平台证书方式(downloader 自动下载平台证书 + NewSHA256WithRSAVerifier)
  publicKey/publicKeyId 必须成对提供,单独一个视为配置错误
- providerConfig.ts: publicKey/publicKeyId 标记为 optional,certSerial 保留必填
- wxpay_test.go: 补 legacy 模式成功用例和成对校验错误用例
2026-04-20 18:19:56 +08:00
erio 7207c5553b chore: bump version to 0.1.114.15 2026-04-20 18:07:46 +08:00
erio 0130ee2422 fix(payment): mark wxpay publicKeyId/certSerial required, surface provider misconfig error
前端把 wxpay 的 publicKeyId 和 certSerial 标成 optional,但后端 NewWxpay 校验时两者都是必填,导致缺配置的实例在创建订单时落入 CreateProvider 失败分支,对外只抛出模糊的 "payment method is temporarily unavailable"。

- providerConfig.ts: 去掉两个字段的 optional: true,表单必填校验生效
- payment_order.go: CreateProvider 失败时改用 PAYMENT_PROVIDER_MISCONFIGURED 错误码,带底层原因和 provider/instance_id metadata,并加 slog.Error 便于定位
2026-04-20 18:07:37 +08:00
erio 2be60ac63a fix: suppress errcheck on recover() in harvester Read path 2026-04-20 01:29:23 +08:00
erio 089d14a2e3 fix(signature): isolate pool buckets by platform
OAuth/setup-token accounts now use per-platform buckets
(oauth:anthropic, oauth:antigravity) instead of a single shared
"oauth" bucket. Prevents cross-contamination between platforms
whose thinking signatures are incompatible.

BucketFor() now takes a platform parameter. All call sites
(harvester, rectifier factory, gateway service) updated.
2026-04-20 00:47:03 +08:00
erio 53b88213f0 feat(signature): add structured logging for pool signature replacement
Log when signatures are replaced from the pool during rectification,
including account_id, bucket, pool_size, and replacement count.
Helps operators verify the pool-replace strategy is working.
2026-04-20 00:24:27 +08:00
erio ade6684077 chore: remove temporary harvester diagnostic logging 2026-04-20 00:08:55 +08:00
erio 0f9c9e8b75 debug: add harvester diagnostic logging 2026-04-19 23:58:15 +08:00
erio 40976d5f08 fix(signature): async harvester with signature_delta support
Rewrite harvester to be fully decoupled from the main read path:
- Read() copies chunks to a buffered channel (non-blocking)
- Background goroutine parses for signatures independently
- sync.Once prevents double-close panic
- defer recover() in Read() guards send-on-closed-channel
- ctx.Done() arm prevents goroutine leaks on abandoned responses
- Panic recovery with slog.Warn logging

Fix signature extraction: the Anthropic API sends signatures via
content_block_delta with delta.type="signature_delta", not in
content_block_start (which has an empty signature field). Support
both shapes for compatibility.

Add 17 comprehensive tests including signature_delta, panic
isolation, double-close, context cancellation, text containing
"signature" word, and no-thinking streams.
2026-04-19 23:25:31 +08:00
erio 0ff3bfe5fa fix(signature): harvest from signature_delta events, not content_block_start
The Anthropic API sends thinking signatures via content_block_delta
with delta.type="signature_delta", not in the content_block_start
event (which has an empty signature field). The harvester was only
checking content_block_start, so it never captured any signatures
and the pool remained permanently empty.

Now handles both content_block_start (legacy compat) and
content_block_delta/signature_delta (current API behavior).
2026-04-19 23:05:10 +08:00
erio fb9c1323d0 debug: add temporary harvester/factory logging to diagnose empty signature pool 2026-04-19 22:56:30 +08:00
erio 26e7d969e8 fix(tlsfingerprint): realign TLS+headers to Claude Code 2.1.114 baseline
Merge hai/snapshot fingerprint update into release branch, preserving
our architectural optimizations (SOCKS5 ContextDialer, identity_service
refactoring, slog migration, capture_fingerprint multi-file structure).

TLS defaults: 52→17 ciphers, 5→3 curves (drop MLKEM768/P521),
2→1 key share (X25519 only), 3→1 point format, 26→9 sig algs.
New probabilistic ECH GREASE + padding (~50% per handshake).
Extension order realigned: server_name first, encrypt_then_mac removed,
status_request/SCT added.

Headers: UA 2.1.114, Runtime v24.3.0 (bundled Node), Timeout 600.
New BetaStructuredOutputs20251215 for Haiku title-sidecar requests.
2026-04-19 22:21:51 +08:00
erio e20a57f53e refactor(scheduled-test): switch to minimal PR 1753 approach
Replace the 4-file service-layer cleanup with the upstream PR's simpler
transaction-level DELETE in accountRepository.Delete.

Bump version to 0.1.114.13.
2026-04-19 20:44:19 +08:00
erio 91db3aabfe Merge remote-tracking branch 'origin/feat/fix-orphaned-scheduled-tests' into release/custom-0.1.114 2026-04-19 20:30:31 +08:00
erio f68894191b chore: bump version to 0.1.114.12 2026-04-19 20:30:01 +08:00
erio b51f20ea85 fix: add missing ScheduledTestPlanRepository arg in test 2026-04-19 20:29:01 +08:00
erio edf20829ba Merge remote-tracking branch 'origin/feat/fix-orphaned-scheduled-tests' into release/custom-0.1.114 2026-04-19 20:26:49 +08:00
erio 79e100a1cf fix: delete scheduled test plans when account is deleted
Accounts use soft-delete (setting deleted_at), so PostgreSQL's
ON DELETE CASCADE on scheduled_test_plans.account_id never fires.
This leaves orphaned plans that continue executing and cannot be
managed from the frontend since the account no longer exists.

Closes Wei-Shaw/sub2api#1728
2026-04-19 20:22:18 +08:00
erio cae8ed99fa test(tlsfingerprint): update expectations to Claude Code CLI default (X25519MLKEM768 + X25519) 2026-04-19 19:41:44 +08:00
erio c1d95da06a fix(ci): add missing signaturePool arg + gofmt alignment 2026-04-19 19:28:10 +08:00
erio a4209ab4f8 chore: bump version to 0.1.114.11 2026-04-19 19:14:59 +08:00
erio 44666b1282 Merge remote-tracking branch 'origin/fix/quota-exceeded-scheduling' into release/custom-0.1.114 2026-04-19 19:14:10 +08:00
erio 258fd145ff fix(account): prevent quota-exceeded API key/Bedrock accounts from being scheduled
Add quota exceeded check to IsSchedulable() and refactor
shouldClearStickySession to delegate to IsSchedulable(), eliminating
duplicated logic and fixing missed overload/rate-limit/expired checks.
Frontend displays quota exceeded status independently via quota fields.
2026-04-19 18:45:04 +08:00
erio 39d41e921b Merge remote-tracking branch 'origin/fix/xhigh-reasoning-effort' into release/custom-0.1.114
# Conflicts:
#	backend/cmd/server/VERSION
2026-04-19 18:32:30 +08:00
erio 6530776a62 fix: support xhigh reasoning effort in usage records for Claude Messages API
Closes #1732
2026-04-19 18:05:25 +08:00
erio 5e9cbd26b9 fix(signature): address code review findings + expand test coverage
Review fixes (P0/P1/P2):
- P0: move nil receiver guards before stage check in PoolClaudeRectifier
  and PoolAntigravityRectifier to prevent panic on nil receiver
- P1: add explicit bedrock/upstream cases to BucketFor for self-documenting
  intent (return empty bucket = no pool participation)
- P2: replace harvester emit de-dupe from O(N²) [][]byte scan to O(1) map
- P2: add lineBufCap (256KB) to SSE line accumulator to prevent unbounded
  growth from malformed upstream streams without newlines

New test cases (29 → 39 total in signature package):
- MultipleAssistantMessages: verify replacement across user/assistant mix
- MalformedContentSkipped: partial JSON doesn't panic or corrupt counter
- SSE_SignatureSplitAcrossReads: line split across multiple Read calls
- SSE_LineBufCapTruncatesHugeLine: lineBuf cap prevents OOM
- NilRequestNoop: PoolAntigravityRectifier with nil Request
- NilReceiverNoPanic: PoolClaudeRectifier nil receiver
- StripAntigravityRectifier_BothStages: verify both stages call strip func
2026-04-19 17:22:43 +08:00
erio 0e6d5817ba test(signature): add Redis pool unit tests with miniredis
Covers the Lua script logic (ZADD + lazy TTL expiry + capacity trim)
via in-process miniredis — no Docker required, runs under `go test
-tags unit`.

10 cases: basic add/topN, fewer-than-N, empty bucket, capacity
trim to newest 3, lazy expiry on Add, TopN-does-not-filter-by-TTL
(design rule: stale sigs survive until next Add), duplicate score
update, empty-input noop, zero-N guard, Size accuracy.

Also keeps the integration test file (build tag `integration`) for
environments with Docker + testcontainers.
2026-04-19 17:10:24 +08:00
erio 35bdd1400e chore: bump version to 0.1.114.10 2026-04-19 15:18:14 +08:00
erio 91ead361b7 test(signature): add unit tests + fix gjson ForEach indexing bug
Covers:
  - ReplaceThinkingSignaturesInBody / InClaudeRequest:
    M>N cycling, empty pool, no thinking blocks, empty signature
    preservation, string-content messages, nil pool guard
  - Strip / Pool rectifier strategies:
    Strip stage-1 unconditional, stage-2 gated on tool error;
    Pool empty→proceed=false (rule A), no replacements→abort,
    one-shot semantics (stage-2 always declines), pool error
    treated as empty
  - BucketFor: oauth/setup-token share, apikey per-id, unknown empty
  - Harvester SSE: content_block_start extraction, per-response
    dedupe, Skip callback, bucket/capacity guards
  - Harvester non-streaming: buffers until Close then parses once

Along the way caught and fixed a path-construction bug in
ReplaceThinkingSignaturesInBody: it used gjson.ForEach's key.Raw
which is empty for array indices, producing malformed sjson paths
like "messages..content..signature". Switched to manual index
counters so paths are always well-formed integers.
2026-04-19 14:00:01 +08:00
erio c8e4753ada feat(signature): frontend UI + i18n for signature pool size
Adds a Signature Pool Size numeric input to the Rectifier settings
section with a hint explaining the 0-vs-positive semantics. When
pool size > 0, the Thinking Signature and API Key Signature toggle
hints dynamically switch from "strip signatures and retry" to
"replace with pooled signatures (pass through when pool is empty)",
matching the runtime strategy swap.

Includes both zh and en locale keys (poolSize, poolSizeHint,
poolSizeUnit, thinkingSignatureHintPool, apikeySignatureHintPool)
plus the signature_pool_size field in the TypeScript RectifierSettings
interface and save payload.
2026-04-19 13:54:24 +08:00
erio 78de54b693 feat(signature): activate PoolRectifier + harvester hookup
Introduces the pool-replace retry strategy end-to-end. When
RectifierSettings has SignaturePoolSize>0 and the account's per-type
sub-switch is on, the factory returns a PoolClaudeRectifier /
PoolAntigravityRectifier that fetches the freshest signatures from the
Redis pool and cycles them through the request's thinking blocks
(M>N cycling). Rule A: an empty pool transparently passes the
original upstream error back — no fallback to strip.

Key pieces:
  - PoolClaudeRectifier / PoolAntigravityRectifier in internal/service/signature
  - signatureRectifierFactory in the service package selects strategy per
    request via shouldUsePool(ctx, account) which implements the agreed
    decision table (Enabled + SignaturePoolSize>0 + per-type sub-switch)
  - WrapResponseBody helper on the factory is invoked at each Claude-native
    DoWithTLS entry point (main Forward, Anthropic passthrough, Bedrock) to
    run the Harvester over the upstream body; no-op when pool disabled
  - ctxkey.IsSignatureRectifyRetry is set on all retry contexts (Claude
    two-stage, count_tokens, Antigravity signature retries) so the harvester
    skips ingesting signatures from retry responses and does not pollute the
    pool with values we ourselves injected
  - NewGatewayService / NewAntigravityGatewayService now accept
    signature.SignaturePool; wire_gen.go updated accordingly

Antigravity responses are raw Gemini format so WrapResponseBody is not
called there — harvesting stays Claude-only as designed. Antigravity can
still *read* from the shared OAuth pool on retry; whether cross-ecosystem
signatures verify upstream is the question the future PoC will answer.
2026-04-19 13:51:22 +08:00
erio 2df77c1604 feat(signature): add SignaturePool infrastructure (not yet wired)
Introduces the building blocks for the thinking-signature pool feature
without activating any runtime behavior. Nothing uses these new types
yet — Phase 3 will wire them into the retry loops.

New package internal/service/signature adds:
  - SignaturePool interface + Bucket helpers (oauth shared / apikey per-account)
  - ReplaceThinkingSignaturesInBody / ReplaceThinkingSignaturesInClaudeRequest
    pure functions that cycle through pool entries for M>N replacements
  - Harvester io.ReadCloser decorator for SSE + non-streaming JSON that
    extracts content_block.signature fields best-effort into the pool
  - 1h soft TTL constant for lazy expiry

New repository adapter internal/repository/signature_pool_cache.go
implements Redis ZSET storage with a single Lua script handling atomic
add + lazy expiry cleanup + capacity trim. Registered via
ProvideSignaturePool in the wire ProviderSet.

Settings extension: RectifierSettings gains a SignaturePoolSize int
field (0 = pool disabled / sticks with strip behavior; >0 = pool replace
is active). Threaded through service view, DTO, and handler GET/PUT
paths with bounds validation (max 1000).

ctxkey.IsSignatureRectifyRetry added so the harvester can later skip
ingesting signatures from retry requests we ourselves injected.
2026-04-19 12:43:11 +08:00
erio 84adf1d6de refactor(signature): extract retry body-transform into Rectifier strategy
Introduce internal/service/signature package with ClaudeRectifier and
AntigravityRectifier interfaces plus StripClaudeRectifier /
StripAntigravityRectifier implementations that wrap the legacy
FilterThinkingBlocksForRetry / FilterSignatureSensitiveBlocksForRetry
and stripThinkingFromClaudeRequest / stripSignatureSensitiveBlocksFromClaudeRequest
functions. Refactor the Claude and Antigravity retry loops
(including count_tokens) to delegate body transformation to the
rectifier, keeping all surrounding scaffolding (ops events, logging,
time budget checks, HTTP plumbing) unchanged.

Also extracts the looksLikeToolSignatureError predicate previously
inlined as an anonymous closure.

Zero behavior change: all existing unit tests pass unchanged. This
prepares for Phase 2 where a Pool strategy will be plugged in via
the same interface.
2026-04-19 12:33:56 +08:00
Wesley Liddick 51af8df31d Merge pull request #1731 from touwaeriol/fix/rate-billing-autofill-response-limit
fix: subscription billing, alipay redirect + H5, payment secrets, 128MB response limit
2026-04-19 09:43:24 +08:00
erio 235f710853 feat(payment): redact provider secrets in admin config API
Admin GET /api/v1/admin/payment/providers previously returned every
config value — including privateKey / apiV3Key / secretKey etc. —
verbatim. Any future XSS on the admin UI would hand attackers the
full set of production payment credentials, and the plaintext values
sat unnecessarily in browser memory for every operator.

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

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

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

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

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

Changes:

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

Frontend
- types/payment.ts, PaymentView.vue: declare is_mobile in
  CreateOrderRequest and pass the computed isMobileDevice() value.
- providerConfig.ts: replace the two fixed POPUP_WINDOW_FEATURES
  constants with getPaymentPopupFeatures(), which prefers 1250×900
  (Alipay's checkout footprint), clamps to window.screen.avail* and
  centers the popup so it never overflows on smaller laptops.
- PaymentQRDialog.vue, PaymentStatusPanel.vue, StripePaymentInline.vue,
  PaymentView.vue: use the new helper at all popup call sites.
2026-04-19 02:22:41 +08:00
erio 2a7272429f fix(payment): size popup to fit alipay checkout without any scrolling
Alipay's page is ~1200x900; a fixed 1250x780 still clipped vertically.
Replace the constant with getPaymentPopupFeatures(): it prefers
1250x900, clamps to window.screen.avail*, and centers the popup so
it never overflows the visible work area on smaller laptops.

Drop POPUP_WINDOW_FEATURES and STRIPE_POPUP_WINDOW_FEATURES in favor of
the helper — all five call sites migrated.

Bump version to 0.1.114.9
2026-04-19 01:32:17 +08:00
erio 4b948e3917 fix(payment): widen popup to 1250x780 so alipay checkout fits without scrolling
Alipay's standard checkout (QR + account login panel) needs ~1200px
width. The default popup was 1000x750, forcing a horizontal scrollbar.
Unify to the wider size that the Stripe-alipay popup already used.

Bump version to 0.1.114.8
2026-04-19 01:24:53 +08:00
erio b3897940b8 fix(payment): stripe publishableKey must not be masked in admin GET
Previous pattern-based isSensitiveConfigField treated any key containing
"key" as a secret and stripped it from the admin response, which wrongly
hid Stripe's publishableKey (a public value). Edit dialogs then failed
its required-field validation because the backend no longer returned it.

Replace the pattern matcher with an explicit per-provider registry that
mirrors the frontend PROVIDER_CONFIG_FIELDS, so only true secrets are
masked/preserved:

- alipay: privateKey, publicKey, alipayPublicKey
- wxpay: privateKey, apiV3Key, publicKey
- stripe: secretKey, webhookSecret (publishableKey stays plaintext)
- easypay: pkey

Bump version to 0.1.114.7
2026-04-19 01:10:43 +08:00
erio 2599b9f278 feat(payment): hide secret config fields in admin GET and preserve on empty PUT
- Strip sensitive provider config keys (privateKey/publicKey/apiV3Key/
  secretKey/webhookSecret/pkey/password/etc.) from admin list/detail API
- mergeConfig: empty value for a sensitive key preserves the stored secret
- Admin dialog: sensitive inputs add autocomplete="new-password" +
  data-*ignore + spellcheck="false" to block browser password managers;
  edit-mode placeholder shows "leave empty to keep"; skip required
  validation for sensitive fields when editing

Bump version to 0.1.114.6
2026-04-19 01:01:19 +08:00
erio dcf56340be chore: bump version to 0.1.114.5 2026-04-18 23:46:28 +08:00
erio 5877cc9e6f fix(payment): frontend declares is_mobile in create-order request
Backend's UA heuristic (mobile|android|iphone|ipad|ipod) misidentifies
iPadOS 13+ (reports as Mac) and certain embedded browsers that strip
the "Mobile" keyword, so H5 users got a PC alipay.trade.page.pay URL.
Frontend then tried to window.location.href into it — navigation went
somewhere unexpected or the popup fallback left the user staring at
the paying-state frontend.

Let the frontend — which has navigator.userAgentData.mobile and better
context — declare is_mobile on the create-order request. Backend uses
the explicit value when present, falling back to UA detection only
when the client didn't send one (preserves old clients / direct API
callers).
2026-04-18 23:46:17 +08:00
erio ad754c905c chore: bump version to 0.1.114.4 2026-04-18 23:28:29 +08:00
erio 5347fd1eac fix(payment): switch PC alipay to redirect-only (trade.page.pay)
The previous change used TradePreCreate (FACE_TO_FACE_PAYMENT), which
requires the merchant to sign "当面付". Our merchant only has "电脑网站
支付" so Alipay returns ACQ.ACCESS_FORBIDDEN.

Go back to TradePagePay but fix the original bug differently: fill
only PayURL (do NOT fill QRCode). Frontend:
  - PC hits the pay_url branch → openWindow() popup (redirect to
    Alipay's own page, which shows login/QR natively)
  - H5 hits the mobile+pay_url branch → window.location.href jump

This matches Alipay's "电脑网站支付" UX and works with the current
merchant signing scope. No client-side QR encoding of the gateway URL
(which was never a valid scannable payload).
2026-04-18 23:28:21 +08:00
erio 73c87fe3c5 chore: bump version to 0.1.114.3 2026-04-18 23:16:09 +08:00