mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
fix(openai): map new ChatGPT plan tiers to friendly labels
prolite -> Pro 5x (violet, same as Pro), pro/chatgptpro -> Pro 20x, self_serve_business_prolite -> Business Premium (indigo, same as Team), team -> Business Standard. The tier naming stays OpenAI-only so Antigravity Pro and Grok pro keep their own labels; planTypeDisplayLabel now shares openAIPlanTypeLabel with PlatformTypeBadge instead of duplicating the mapping, and the manual plan_type dropdown gains the Pro 5x / Business Premium presets.
This commit is contained in:
@@ -3419,7 +3419,8 @@ const openaiPassthroughEnabled = ref(false)
|
||||
// OpenAI Codex namespace 工具摊平兼容开关(仅 OAuth),缺省关闭即原样保留
|
||||
const openaiFlattenNamespacesEnabled = ref(false)
|
||||
const openAILongContextBillingEnabled = ref(false)
|
||||
// OpenAI 订阅档位(Plus/Pro/Free)手动覆盖值,存于 credentials.plan_type;'' 表示清空/自动识别
|
||||
// OpenAI 订阅档位(Plus / Pro 20x / Pro 5x / Business Standard / Business Premium / Free)手动覆盖值,
|
||||
// 存于 credentials.plan_type;'' 表示清空/自动识别
|
||||
const editPlanType = ref<string>('')
|
||||
const openAICompactMode = ref<OpenAICompactMode>('auto')
|
||||
const openAIResponsesMode = ref<OpenAIResponsesMode>('auto')
|
||||
@@ -5210,7 +5211,8 @@ const handleSubmit = async () => {
|
||||
updatePayload.extra = newExtra
|
||||
}
|
||||
|
||||
// OpenAI: 手动覆盖订阅档位 plan_type(Plus/Pro/Free)。仅 OAuth 非影子账号:
|
||||
// OpenAI: 手动覆盖订阅档位 plan_type(Plus / Pro 20x / Pro 5x / Business Standard / Business Premium / Free)。
|
||||
// 仅 OAuth 非影子账号:
|
||||
// 影子账号凭据由母账号管理(且后端会 sanitize),setup-token 无订阅调度语义。
|
||||
if (props.account.platform === 'openai' && props.account.type === 'oauth' && !isSparkShadow.value) {
|
||||
const currentCredentials = (updatePayload.credentials as Record<string, unknown>) ||
|
||||
|
||||
@@ -396,11 +396,18 @@ describe('plan_type helpers', () => {
|
||||
describe('planTypeDisplayLabel', () => {
|
||||
it('maps canonical + alias values to friendly labels', () => {
|
||||
expect(planTypeDisplayLabel('plus')).toBe('Plus')
|
||||
expect(planTypeDisplayLabel('pro')).toBe('Pro')
|
||||
expect(planTypeDisplayLabel('chatgptpro')).toBe('Pro')
|
||||
expect(planTypeDisplayLabel('pro')).toBe('Pro 20x')
|
||||
expect(planTypeDisplayLabel('chatgptpro')).toBe('Pro 20x')
|
||||
expect(planTypeDisplayLabel('prolite')).toBe('Pro 5x')
|
||||
expect(planTypeDisplayLabel('free')).toBe('Free')
|
||||
expect(planTypeDisplayLabel('team')).toBe('Team')
|
||||
expect(planTypeDisplayLabel('CHATGPTPRO')).toBe('Pro')
|
||||
expect(planTypeDisplayLabel('team')).toBe('Business Standard')
|
||||
expect(planTypeDisplayLabel('self_serve_business_prolite')).toBe('Business Premium')
|
||||
})
|
||||
it('normalizes case, separators and surrounding blanks', () => {
|
||||
expect(planTypeDisplayLabel('CHATGPTPRO')).toBe('Pro 20x')
|
||||
expect(planTypeDisplayLabel('PROLITE')).toBe('Pro 5x')
|
||||
expect(planTypeDisplayLabel(' Pro Lite ')).toBe('Pro 5x')
|
||||
expect(planTypeDisplayLabel('self-serve-business-pro-lite')).toBe('Business Premium')
|
||||
})
|
||||
it('returns unknown values verbatim', () => {
|
||||
expect(planTypeDisplayLabel('self_serve_business')).toBe('self_serve_business')
|
||||
@@ -426,22 +433,39 @@ describe('plan_type helpers', () => {
|
||||
expect(buildPlanTypeOptions('', clear)).toEqual([
|
||||
{ value: '', label: clear },
|
||||
{ value: 'plus', label: 'Plus' },
|
||||
{ value: 'pro', label: 'Pro' },
|
||||
{ value: 'pro', label: 'Pro 20x' },
|
||||
{ value: 'prolite', label: 'Pro 5x' },
|
||||
{ value: 'self_serve_business_prolite', label: 'Business Premium' },
|
||||
{ value: 'free', label: 'Free' }
|
||||
])
|
||||
})
|
||||
it('keeps canonical chatgptpro under a single friendly "Pro" option (no duplicate)', () => {
|
||||
it('keeps canonical chatgptpro under a single friendly "Pro 20x" option (no duplicate)', () => {
|
||||
const opts = buildPlanTypeOptions('chatgptpro', clear)
|
||||
const pros = opts.filter(o => o.label === 'Pro')
|
||||
const pros = opts.filter(o => o.label === 'Pro 20x')
|
||||
expect(pros).toHaveLength(1)
|
||||
expect(pros[0].value).toBe('chatgptpro')
|
||||
expect(opts.map(o => o.value)).toEqual(['', 'plus', 'chatgptpro', 'free'])
|
||||
expect(opts.map(o => o.value)).toEqual([
|
||||
'',
|
||||
'plus',
|
||||
'chatgptpro',
|
||||
'prolite',
|
||||
'self_serve_business_prolite',
|
||||
'free'
|
||||
])
|
||||
})
|
||||
it('appends an unknown-but-labeled value (team) as its own option', () => {
|
||||
const opts = buildPlanTypeOptions('team', clear)
|
||||
expect(opts.find(o => o.value === 'team')).toEqual({ value: 'team', label: 'Team' })
|
||||
expect(opts.find(o => o.value === 'team')).toEqual({ value: 'team', label: 'Business Standard' })
|
||||
// presets untouched
|
||||
expect(opts.map(o => o.value)).toEqual(['', 'plus', 'pro', 'free', 'team'])
|
||||
expect(opts.map(o => o.value)).toEqual([
|
||||
'',
|
||||
'plus',
|
||||
'pro',
|
||||
'prolite',
|
||||
'self_serve_business_prolite',
|
||||
'free',
|
||||
'team'
|
||||
])
|
||||
})
|
||||
it('appends a fully custom value with a raw label', () => {
|
||||
const opts = buildPlanTypeOptions('weird_x', clear)
|
||||
@@ -450,7 +474,27 @@ describe('plan_type helpers', () => {
|
||||
it('does not duplicate an exact preset value', () => {
|
||||
const opts = buildPlanTypeOptions('pro', clear)
|
||||
expect(opts.filter(o => o.value === 'pro')).toHaveLength(1)
|
||||
expect(opts.map(o => o.value)).toEqual(['', 'plus', 'pro', 'free'])
|
||||
expect(opts.map(o => o.value)).toEqual([
|
||||
'',
|
||||
'plus',
|
||||
'pro',
|
||||
'prolite',
|
||||
'self_serve_business_prolite',
|
||||
'free'
|
||||
])
|
||||
})
|
||||
it('does not duplicate a preset value that is the current one', () => {
|
||||
for (const preset of ['prolite', 'self_serve_business_prolite']) {
|
||||
const opts = buildPlanTypeOptions(preset, clear)
|
||||
expect(opts.filter(o => o.value === preset)).toHaveLength(1)
|
||||
}
|
||||
})
|
||||
it('keeps a separator-free variant of a preset as its own value', () => {
|
||||
const opts = buildPlanTypeOptions('selfservebusinessprolite', clear)
|
||||
expect(opts.find(o => o.value === 'selfservebusinessprolite')).toEqual({
|
||||
value: 'selfservebusinessprolite',
|
||||
label: 'Business Premium'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { openAIPlanTypeLabel } from '@/utils/planType'
|
||||
|
||||
export function applyInterceptWarmup(
|
||||
credentials: Record<string, unknown>,
|
||||
enabled: boolean,
|
||||
@@ -406,23 +408,12 @@ export interface PlanTypeOption {
|
||||
}
|
||||
|
||||
/**
|
||||
* plan_type 值的友好显示标签,镜像 PlatformTypeBadge 的映射
|
||||
* (canonical 值 chatgptpro 显示为 Pro,team 显示为 Team)。未知值原样返回。
|
||||
* plan_type 值的友好显示标签(ChatGPT 档位命名)。
|
||||
* 与 PlatformTypeBadge 共用 openAIPlanTypeLabel,避免两处映射漂移;
|
||||
* canonical 值 chatgptpro 显示为 Pro 20x,team 显示为 Business Standard。未知值原样返回。
|
||||
*/
|
||||
export function planTypeDisplayLabel(value: string): string {
|
||||
switch (value.trim().toLowerCase()) {
|
||||
case 'plus':
|
||||
return 'Plus'
|
||||
case 'pro':
|
||||
case 'chatgptpro':
|
||||
return 'Pro'
|
||||
case 'free':
|
||||
return 'Free'
|
||||
case 'team':
|
||||
return 'Team'
|
||||
default:
|
||||
return value
|
||||
}
|
||||
return openAIPlanTypeLabel(value) || value
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -435,8 +426,8 @@ export function readPlanType(credentials: Record<string, unknown> | undefined |
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 plan_type 下拉选项:清空 + Plus/Pro/Free 预设。
|
||||
* 若当前值是某预设的别名(如 chatgptpro↔Pro),用当前的 canonical 值占据该
|
||||
* 构建 plan_type 下拉选项:清空 + Plus/Pro 20x/Pro 5x/Business Premium/Free 预设。
|
||||
* 若当前值是某预设的别名(如 chatgptpro↔Pro 20x),用当前的 canonical 值占据该
|
||||
* 标签位(保留 canonical,显示友好标签,避免重复项);若是完全预设外的值
|
||||
* (如 team 或异常值),追加为一项,避免编辑时下拉丢失原值。
|
||||
*/
|
||||
@@ -445,7 +436,9 @@ export function buildPlanTypeOptions(current: string, clearLabel: string): PlanT
|
||||
const curLabel = cur ? planTypeDisplayLabel(cur) : ''
|
||||
const presets: PlanTypeOption[] = [
|
||||
{ value: 'plus', label: 'Plus' },
|
||||
{ value: 'pro', label: 'Pro' },
|
||||
{ value: 'pro', label: 'Pro 20x' },
|
||||
{ value: 'prolite', label: 'Pro 5x' },
|
||||
{ value: 'self_serve_business_prolite', label: 'Business Premium' },
|
||||
{ value: 'free', label: 'Free' }
|
||||
]
|
||||
const opts: PlanTypeOption[] = [{ value: '', label: clearLabel }]
|
||||
|
||||
@@ -69,6 +69,7 @@ import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { AccountPlatform, AccountType } from '@/types'
|
||||
import { platformLabel as sharedPlatformLabel } from '@/utils/platformColors'
|
||||
import { normalizePlanType, openAIPlanTypeLabel } from '@/utils/planType'
|
||||
import GrokFreeIcon from './GrokFreeIcon.vue'
|
||||
import PlatformIcon from './PlatformIcon.vue'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
@@ -113,12 +114,16 @@ const typeLabel = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const normalizedPlanType = computed(() =>
|
||||
(props.planType || '').trim().toLowerCase().replace(/[\s_-]+/g, '')
|
||||
)
|
||||
const normalizedPlanType = computed(() => normalizePlanType(props.planType))
|
||||
|
||||
const planLabel = computed(() => {
|
||||
if (!normalizedPlanType.value) return ''
|
||||
// ChatGPT 档位命名(Pro 5x / Pro 20x、Business Standard / Business Premium)只适用于
|
||||
// OpenAI:Antigravity 与 Grok 各自的 pro/team 沿用下面的通用标签。
|
||||
if (props.platform === 'openai') {
|
||||
const label = openAIPlanTypeLabel(props.planType)
|
||||
if (label) return label
|
||||
}
|
||||
switch (normalizedPlanType.value) {
|
||||
case 'plus':
|
||||
return 'Plus'
|
||||
@@ -254,10 +259,14 @@ const planBadgeClass = computed(() => {
|
||||
if (normalizedPlanType.value === 'plus') {
|
||||
return 'bg-sky-100 text-sky-700 dark:bg-sky-900/30 dark:text-sky-300'
|
||||
}
|
||||
if (normalizedPlanType.value === 'team') {
|
||||
if (normalizedPlanType.value === 'team' || normalizedPlanType.value === 'selfservebusinessprolite') {
|
||||
return 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-300'
|
||||
}
|
||||
if (normalizedPlanType.value === 'pro' || normalizedPlanType.value === 'chatgptpro') {
|
||||
if (
|
||||
normalizedPlanType.value === 'pro' ||
|
||||
normalizedPlanType.value === 'chatgptpro' ||
|
||||
normalizedPlanType.value === 'prolite'
|
||||
) {
|
||||
return 'bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-300'
|
||||
}
|
||||
return typeClass.value
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { AccountPlatform } from '@/types'
|
||||
import PlatformTypeBadge from '../PlatformTypeBadge.vue'
|
||||
|
||||
vi.mock('vue-i18n', async () => {
|
||||
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
|
||||
return {
|
||||
...actual,
|
||||
useI18n: () => ({ t: (key: string) => key }),
|
||||
}
|
||||
})
|
||||
|
||||
function mountPlan(platform: AccountPlatform, planType: string) {
|
||||
return mount(PlatformTypeBadge, {
|
||||
props: { platform, type: 'oauth', planType },
|
||||
})
|
||||
}
|
||||
|
||||
describe('PlatformTypeBadge ChatGPT plan tiers', () => {
|
||||
it('labels pro / chatgptpro as Pro 20x with the Pro color', () => {
|
||||
for (const planType of ['pro', 'chatgptpro', 'PRO']) {
|
||||
const wrapper = mountPlan('openai', planType)
|
||||
|
||||
expect(wrapper.text()).toContain('Pro 20x')
|
||||
expect(wrapper.html()).toContain('bg-violet-100')
|
||||
}
|
||||
})
|
||||
|
||||
it('labels prolite as Pro 5x sharing the Pro color', () => {
|
||||
for (const planType of ['prolite', 'PROLITE', 'pro_lite']) {
|
||||
const wrapper = mountPlan('openai', planType)
|
||||
|
||||
expect(wrapper.text()).toContain('Pro 5x')
|
||||
expect(wrapper.html()).toContain('bg-violet-100')
|
||||
expect(wrapper.text()).not.toContain('Pro 20x')
|
||||
}
|
||||
})
|
||||
|
||||
it('labels team as Business Standard with the Team color', () => {
|
||||
const wrapper = mountPlan('openai', 'team')
|
||||
|
||||
expect(wrapper.text()).toContain('Business Standard')
|
||||
expect(wrapper.html()).toContain('bg-indigo-100')
|
||||
})
|
||||
|
||||
it('labels self_serve_business_prolite as Business Premium sharing the Team color', () => {
|
||||
for (const planType of ['self_serve_business_prolite', 'selfservebusinessprolite']) {
|
||||
const wrapper = mountPlan('openai', planType)
|
||||
|
||||
expect(wrapper.text()).toContain('Business Premium')
|
||||
expect(wrapper.html()).toContain('bg-indigo-100')
|
||||
expect(wrapper.text()).not.toContain('self_serve_business_prolite')
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps plus, free and abnormal labels unchanged', () => {
|
||||
const plus = mountPlan('openai', 'plus')
|
||||
expect(plus.text()).toContain('Plus')
|
||||
expect(plus.html()).toContain('bg-sky-100')
|
||||
|
||||
const free = mountPlan('openai', 'free')
|
||||
expect(free.text()).toContain('Free')
|
||||
expect(free.html()).toContain('bg-gray-100')
|
||||
|
||||
const abnormal = mountPlan('openai', 'abnormal')
|
||||
expect(abnormal.text()).toContain('admin.accounts.subscriptionAbnormal')
|
||||
expect(abnormal.html()).toContain('bg-red-100')
|
||||
})
|
||||
|
||||
it('falls back to the raw value for unknown plans', () => {
|
||||
const wrapper = mountPlan('openai', 'enterprise')
|
||||
|
||||
expect(wrapper.text()).toContain('enterprise')
|
||||
expect(wrapper.html()).not.toContain('bg-violet-100')
|
||||
expect(wrapper.html()).not.toContain('bg-indigo-100')
|
||||
})
|
||||
|
||||
it('does not apply the ChatGPT tier naming to other platforms', () => {
|
||||
// Antigravity 的 Pro 与 Grok 的 pro 是各自产品线的档位,不能显示成 Pro 20x。
|
||||
for (const platform of ['antigravity', 'grok'] as AccountPlatform[]) {
|
||||
const wrapper = mountPlan(platform, 'pro')
|
||||
|
||||
expect(wrapper.text()).toContain('Pro')
|
||||
expect(wrapper.text()).not.toContain('Pro 20x')
|
||||
}
|
||||
|
||||
const antigravityTeam = mountPlan('antigravity', 'team')
|
||||
expect(antigravityTeam.text()).toContain('Team')
|
||||
expect(antigravityTeam.text()).not.toContain('Business Standard')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* ChatGPT(OpenAI)订阅档位 plan_type 的解析与展示映射。
|
||||
*
|
||||
* plan_type 由上游原样透传,同一档位会出现 `chatgpt_pro`、`self_serve_business_prolite`
|
||||
* 这类下划线/别名写法,因此匹配前一律先归一化。
|
||||
*
|
||||
* 这里的档位命名只对 OpenAI 平台成立:Antigravity 的 `Pro`、Grok 的 `pro` 是各自产品线的
|
||||
* 档位,套用 ChatGPT 的倍率命名(Pro 5x / Pro 20x)会显示错误。
|
||||
*/
|
||||
|
||||
/**
|
||||
* plan_type 归一化:去首尾空白、转小写,并去掉空格/下划线/连字符。
|
||||
* 例:`self_serve_business_prolite` → `selfservebusinessprolite`。
|
||||
*/
|
||||
export function normalizePlanType(value?: string | null): string {
|
||||
return (value || '').trim().toLowerCase().replace(/[\s_-]+/g, '')
|
||||
}
|
||||
|
||||
/**
|
||||
* ChatGPT 档位 → 展示标签;未知档位返回空串,由调用方决定是否回退为原始值。
|
||||
*
|
||||
* Pro 的倍率命名:`pro`/`chatgptpro` 为 Pro 20x,`prolite` 为 Pro 5x;
|
||||
* Team/Business:`team` 为 Business Standard,`self_serve_business_prolite` 为 Business Premium。
|
||||
*/
|
||||
export function openAIPlanTypeLabel(value?: string | null): string {
|
||||
switch (normalizePlanType(value)) {
|
||||
case 'plus':
|
||||
return 'Plus'
|
||||
case 'chatgptpro':
|
||||
case 'pro':
|
||||
return 'Pro 20x'
|
||||
case 'prolite':
|
||||
return 'Pro 5x'
|
||||
case 'selfservebusinessprolite':
|
||||
return 'Business Premium'
|
||||
case 'team':
|
||||
return 'Business Standard'
|
||||
case 'free':
|
||||
return 'Free'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user