sync: bring over remaining release/custom-0.1.115 changes

- Extract PublicSettingsInjectionPayload named struct with drift test
- Add channel_monitor_default_interval_seconds to SSR injection
- Add image_output_price to SupportedModelChip
- Simplify AppSidebar buildSelfNavItems (admins see available channels)
- Add gateway WARN logs for 503 no-available-accounts branches
- Wire ChannelMonitorRunner into provideCleanup for graceful shutdown
- Add migrations 130/131 (CC template userid fix + mimicry field cleanup)
- Clean up fork-only features (sora, claude max simulation, client affinity)
- Remove ~320 obsolete i18n keys
- Add codexUsage utility, WechatServiceButton, BulkEditAccountModal
- Tidy go.sum
This commit is contained in:
erio
2026-04-23 20:55:18 +08:00
parent d5dac84e12
commit 748a84d871
76 changed files with 1380 additions and 1699 deletions

View File

@@ -73,42 +73,9 @@
<!-- Config fields -->
<div class="border-t border-gray-200 pt-4 dark:border-dark-700">
<div class="mb-3 flex items-center gap-2">
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">
{{ t('admin.settings.payment.providerConfig') }}
</h4>
<HelpTooltip v-if="paymentGuide" trigger="click" width-class="w-80">
<template #trigger>
<button
type="button"
class="inline-flex h-5 w-5 items-center justify-center rounded-full border border-gray-300 text-[11px] font-semibold text-gray-400 transition-colors hover:border-primary-500 hover:text-primary-600 dark:border-dark-500 dark:text-gray-500 dark:hover:border-primary-400 dark:hover:text-primary-400"
:aria-label="t('admin.settings.payment.paymentGuideTrigger')"
:title="t('admin.settings.payment.paymentGuideTrigger')"
>
?
</button>
</template>
<div class="space-y-3">
<p class="font-medium text-white">{{ paymentGuide.summary }}</p>
<div
v-for="item in paymentGuide.items"
:key="item.title"
class="space-y-1.5 border-t border-white/10 pt-2 first:border-t-0 first:pt-0"
>
<p class="font-medium text-white">{{ item.title }}</p>
<p><span class="text-gray-300">{{ t('admin.settings.payment.guideOpenLabel') }}</span>{{ item.open }}</p>
<p><span class="text-gray-300">{{ t('admin.settings.payment.guideCallLabel') }}</span>{{ item.call }}</p>
<p><span class="text-gray-300">{{ t('admin.settings.payment.guideFallbackLabel') }}</span>{{ item.fallback }}</p>
</div>
<p v-if="paymentGuide.note" class="border-t border-white/10 pt-2 text-[11px] text-gray-300">
{{ paymentGuide.note }}
</p>
</div>
</HelpTooltip>
</div>
<p v-if="paymentGuide" class="mb-3 text-xs text-gray-500 dark:text-gray-400">
{{ paymentGuide.summary }}
</p>
<h4 class="mb-3 text-sm font-semibold text-gray-900 dark:text-white">
{{ t('admin.settings.payment.providerConfig') }}
</h4>
<div class="space-y-3">
<div v-for="field in resolvedFields" :key="field.key">
<label class="input-label">
@@ -253,7 +220,6 @@
import { reactive, computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import BaseDialog from '@/components/common/BaseDialog.vue'
import HelpTooltip from '@/components/common/HelpTooltip.vue'
import Select from '@/components/common/Select.vue'
import type { SelectOption } from '@/components/common/Select.vue'
import ToggleSwitch from './ToggleSwitch.vue'
@@ -297,19 +263,6 @@ const emit = defineEmits<{
const { t } = useI18n()
interface PaymentGuideItem {
title: string
open: string
call: string
fallback: string
}
interface PaymentGuide {
summary: string
items: PaymentGuideItem[]
note?: string
}
// --- Form state ---
const form = reactive({
name: '',
@@ -361,63 +314,6 @@ const resolvedFields = computed(() => {
}))
})
const paymentGuide = computed<PaymentGuide | null>(() => {
if (form.provider_key === 'alipay') {
return {
summary: t('admin.settings.payment.alipayGuideSummary'),
items: [
{
title: t('admin.settings.payment.alipayGuideFaceToFaceTitle'),
open: t('admin.settings.payment.alipayGuideFaceToFaceOpen'),
call: t('admin.settings.payment.alipayGuideFaceToFaceCall'),
fallback: t('admin.settings.payment.alipayGuideFaceToFaceFallback'),
},
{
title: t('admin.settings.payment.alipayGuidePagePayTitle'),
open: t('admin.settings.payment.alipayGuidePagePayOpen'),
call: t('admin.settings.payment.alipayGuidePagePayCall'),
fallback: t('admin.settings.payment.alipayGuidePagePayFallback'),
},
{
title: t('admin.settings.payment.alipayGuideWapTitle'),
open: t('admin.settings.payment.alipayGuideWapOpen'),
call: t('admin.settings.payment.alipayGuideWapCall'),
fallback: t('admin.settings.payment.alipayGuideWapFallback'),
},
],
}
}
if (form.provider_key === 'wxpay') {
return {
summary: t('admin.settings.payment.wxpayGuideSummary'),
note: t('admin.settings.payment.wxpayGuideNote'),
items: [
{
title: t('admin.settings.payment.wxpayGuideNativeTitle'),
open: t('admin.settings.payment.wxpayGuideNativeOpen'),
call: t('admin.settings.payment.wxpayGuideNativeCall'),
fallback: t('admin.settings.payment.wxpayGuideNativeFallback'),
},
{
title: t('admin.settings.payment.wxpayGuideJsapiTitle'),
open: t('admin.settings.payment.wxpayGuideJsapiOpen'),
call: t('admin.settings.payment.wxpayGuideJsapiCall'),
fallback: t('admin.settings.payment.wxpayGuideJsapiFallback'),
},
{
title: t('admin.settings.payment.wxpayGuideH5Title'),
open: t('admin.settings.payment.wxpayGuideH5Open'),
call: t('admin.settings.payment.wxpayGuideH5Call'),
fallback: t('admin.settings.payment.wxpayGuideH5Fallback'),
},
],
}
}
return null
})
const limitableTypes = computed(() => {
// Stripe: single "stripe" entry (one set of shared limits)
if (form.provider_key === 'stripe') {

View File

@@ -84,9 +84,6 @@
</div>
</div>
<p v-if="scanHint" class="text-center text-sm text-gray-500 dark:text-gray-400">{{ scanHint }}</p>
<button v-if="payUrl" class="btn btn-secondary text-sm" @click="reopenPopup">
{{ t('payment.qr.openPayWindow') }}
</button>
</div>
</div>
<div class="card p-4 text-center">

View File

@@ -96,36 +96,4 @@ describe('PaymentStatusPanel', () => {
expect(wrapper.text()).toContain('payment.result.success')
expect(wrapper.emitted('success')).toHaveLength(1)
})
it('shows reopen button in QR mode when payUrl is also available', async () => {
const openSpy = vi.spyOn(window, 'open').mockReturnValue({ closed: false } as Window)
const wrapper = mount(PaymentStatusPanel, {
props: {
orderId: 42,
qrCode: 'https://pay.example.com/qr/42',
payUrl: 'https://pay.example.com/session/42',
expiresAt: '2099-01-01T12:30:00Z',
paymentType: 'alipay',
orderType: 'balance',
},
global: {
stubs: {
Icon: true,
},
},
})
await flushPromises()
expect(wrapper.text()).toContain('payment.qr.openPayWindow')
await wrapper.get('button.btn.btn-secondary.text-sm').trigger('click')
expect(openSpy).toHaveBeenCalledWith(
'https://pay.example.com/session/42',
'paymentPopup',
expect.any(String),
)
openSpy.mockRestore()
})
})

View File

@@ -190,14 +190,12 @@ describe('buildCreateOrderPayload', () => {
paymentType: 'alipay_direct',
orderType: 'balance',
origin: 'https://app.example.com/',
isMobile: true,
isWechatBrowser: false,
})).toEqual({
amount: 88,
payment_type: 'alipay',
order_type: 'balance',
return_url: 'https://app.example.com/payment/result',
is_mobile: true,
payment_source: 'hosted_redirect',
})
})
@@ -209,7 +207,6 @@ describe('buildCreateOrderPayload', () => {
orderType: 'subscription',
planId: 7,
origin: 'https://app.example.com',
isMobile: false,
isWechatBrowser: true,
})).toEqual({
amount: 128,
@@ -217,7 +214,6 @@ describe('buildCreateOrderPayload', () => {
order_type: 'subscription',
plan_id: 7,
return_url: 'https://app.example.com/payment/result',
is_mobile: false,
payment_source: 'wechat_in_app_resume',
})
})

View File

@@ -12,9 +12,9 @@ describe('PROVIDER_CONFIG_FIELDS.wxpay', () => {
expect(findField('certSerial')?.optional).toBeFalsy()
})
it('only keeps the simplified visible credential set in the admin form', () => {
expect(findField('mpAppId')).toBeUndefined()
expect(findField('h5AppName')).toBeUndefined()
expect(findField('h5AppUrl')).toBeUndefined()
it('exposes optional mp and H5 metadata fields for WeChat-specific flows', () => {
expect(findField('mpAppId')?.optional).toBe(true)
expect(findField('h5AppName')?.optional).toBe(true)
expect(findField('h5AppUrl')?.optional).toBe(true)
})
})

View File

@@ -68,7 +68,6 @@ export interface BuildCreateOrderPayloadInput {
orderType: OrderType
planId?: number
origin?: string
isMobile: boolean
isWechatBrowser: boolean
}
@@ -107,7 +106,6 @@ export function buildCreateOrderPayload(input: BuildCreateOrderPayloadInput): Cr
amount: input.amount,
payment_type: visibleMethod,
order_type: input.orderType,
is_mobile: input.isMobile,
payment_source: visibleMethod === 'wxpay' && input.isWechatBrowser
? 'wechat_in_app_resume'
: 'hosted_redirect',

View File

@@ -96,12 +96,15 @@ export const PROVIDER_CONFIG_FIELDS: Record<string, ConfigFieldDef[]> = {
],
wxpay: [
{ key: 'appId', label: 'App ID', sensitive: false },
{ key: 'mpAppId', label: '', sensitive: false, optional: true },
{ key: 'mchId', label: '', sensitive: false },
{ key: 'privateKey', label: '', sensitive: true },
{ key: 'apiV3Key', label: '', sensitive: true },
{ key: 'certSerial', label: '', sensitive: false },
{ key: 'publicKey', label: '', sensitive: true },
{ key: 'publicKeyId', label: '', sensitive: false },
{ key: 'h5AppName', label: '', sensitive: false, optional: true },
{ key: 'h5AppUrl', label: '', sensitive: false, optional: true },
],
stripe: [
{ key: 'secretKey', label: '', sensitive: true },