diff --git a/src/components/NanaAssistant/AiAdvisorShell.vue b/src/components/NanaAssistant/AiAdvisorShell.vue index 4517835..2896912 100644 --- a/src/components/NanaAssistant/AiAdvisorShell.vue +++ b/src/components/NanaAssistant/AiAdvisorShell.vue @@ -364,9 +364,7 @@ import { } from '@/composables/assistantMcpGuide'; import { isMobileUserAgent, isWeChatBrowser } from '@/utils/device'; import { openWecomKf } from '@/utils/wecomKf'; -import { - providerModelTags, -} from '@/views/system/aiAssistant/providerCatalog'; +import { resolveTextModelId } from '@/views/system/aiAssistant/providerCatalog'; import NanaAvatar from './NanaAvatar.vue'; import logoImg from '@/assets/logo2.png'; @@ -903,10 +901,10 @@ async function loadProvider() { const def = enabled.find((p: any) => p.isDefault) ?? enabled[0]; providerKey.value = def?.providerKey ?? ''; if (def) { - const tags = providerModelTags(String(def.providerType || '')); - const secret = String(def.apiSecret || '').trim(); - providerModel.value = - tags[0]?.modelId || (secret && secret !== '******' ? secret : '') || ''; + providerModel.value = resolveTextModelId({ + providerType: String(def.providerType || ''), + apiSecret: def.apiSecret, + }); } else { providerModel.value = ''; } diff --git a/src/views/api/upstreamInterface/components/AddParameter.vue b/src/views/api/upstreamInterface/components/AddParameter.vue index 4ffe558..9009e7e 100644 --- a/src/views/api/upstreamInterface/components/AddParameter.vue +++ b/src/views/api/upstreamInterface/components/AddParameter.vue @@ -6,7 +6,7 @@ import CodeMirror from '@/components/CodeMirror/index.vue'; import { listAiProviders } from '@/api/ai'; import { - providerModelTags, + resolveProviderModelTags, type AiProviderItem, } from '@/views/system/aiAssistant/providerCatalog'; import { @@ -64,12 +64,7 @@ const modelOptions = computed(() => { const p = selectedProvider.value; if (!p) return []; - const tags = providerModelTags(p.providerType); - const extra = String(p.apiSecret || '').trim(); - if (extra && extra !== '******' && !tags.some((m) => m.modelId === extra)) { - return [...tags, { modelId: extra, role: '自定义' }]; - } - return tags; + return resolveProviderModelTags(p); }); function loadPrefs() { diff --git a/src/views/api/upstreamInterface/components/MapAiAssistModal.vue b/src/views/api/upstreamInterface/components/MapAiAssistModal.vue index d455ceb..df7d226 100644 --- a/src/views/api/upstreamInterface/components/MapAiAssistModal.vue +++ b/src/views/api/upstreamInterface/components/MapAiAssistModal.vue @@ -4,7 +4,7 @@ import { Message } from '@arco-design/web-vue' import { listAiProviders, type AiSkillItem } from '@/api/ai' import { - providerModelTags, + resolveProviderModelTags, type AiProviderItem, } from '@/views/system/aiAssistant/providerCatalog' import { @@ -62,12 +62,7 @@ const modelOptions = computed(() => { const p = selectedProvider.value if (!p) return [] - const tags = providerModelTags(p.providerType) - const extra = String(p.apiSecret || '').trim() - if (extra && extra !== '******' && !tags.some((m) => m.modelId === extra)) { - return [...tags, { modelId: extra, role: '自定义' }] - } - return tags + return resolveProviderModelTags(p) }) const previewRows = computed(() => { diff --git a/src/views/channel/autoTrigger/components/FormulaAiAssistDrawer.vue b/src/views/channel/autoTrigger/components/FormulaAiAssistDrawer.vue index a72e263..169ac4f 100644 --- a/src/views/channel/autoTrigger/components/FormulaAiAssistDrawer.vue +++ b/src/views/channel/autoTrigger/components/FormulaAiAssistDrawer.vue @@ -152,7 +152,7 @@ import { useI18n } from 'vue-i18n' import { useRouter } from 'vue-router' import { chatAi, listAiProviders, listAiSkills, type AiSkillItem } from '@/api/ai' import { - providerModelTags, + resolveProviderModelTags, type AiProviderItem, } from '@/views/system/aiAssistant/providerCatalog' import { @@ -237,12 +237,7 @@ const selectedProvider = computed( const modelOptions = computed(() => { const p = selectedProvider.value if (!p) return [] - const tags = providerModelTags(p.providerType) - const extra = String(p.apiSecret || '').trim() - if (extra && extra !== '******' && !tags.some((m) => m.modelId === extra)) { - return [...tags, { modelId: extra, role: '自定义' }] - } - return tags + return resolveProviderModelTags(p) }) const canSend = computed( diff --git a/src/views/channel/salesProducts/components/PricePublishAiAssistDrawer.vue b/src/views/channel/salesProducts/components/PricePublishAiAssistDrawer.vue index 814e2e2..3a59bab 100644 --- a/src/views/channel/salesProducts/components/PricePublishAiAssistDrawer.vue +++ b/src/views/channel/salesProducts/components/PricePublishAiAssistDrawer.vue @@ -158,7 +158,7 @@ import { useRouter } from 'vue-router' import { chatAi, listAiProviders, listAiSkills, type AiSkillItem } from '@/api/ai' import type { SnapshotDiffRow } from '@/utils/publishSnapshotDiff' import { - providerModelTags, + resolveProviderModelTags, type AiProviderItem, } from '@/views/system/aiAssistant/providerCatalog' import { @@ -232,12 +232,7 @@ const selectedProvider = computed( const modelOptions = computed(() => { const p = selectedProvider.value if (!p) return [] - const tags = providerModelTags(p.providerType) - const extra = String(p.apiSecret || '').trim() - if (extra && extra !== '******' && !tags.some((m) => m.modelId === extra)) { - return [...tags, { modelId: extra, role: '自定义' }] - } - return tags + return resolveProviderModelTags(p) }) const canSend = computed( diff --git a/src/views/h5/customer-center/components/H5PlaceOrderSheet.vue b/src/views/h5/customer-center/components/H5PlaceOrderSheet.vue index d97fd7f..ee57da0 100644 --- a/src/views/h5/customer-center/components/H5PlaceOrderSheet.vue +++ b/src/views/h5/customer-center/components/H5PlaceOrderSheet.vue @@ -1262,7 +1262,7 @@ import { type DomesticPickupProductOption, } from '@/utils/domesticPickupProducts'; import { - providerModelTags, + resolveProviderModelTags, type AiProviderItem, } from '@/views/system/aiAssistant/providerCatalog'; import H5SelectDeclarationSkuSheet from './H5SelectDeclarationSkuSheet.vue'; @@ -1362,12 +1362,7 @@ const receiverAiSelectedProvider = computed( const receiverAiModelOptions = computed(() => { const p = receiverAiSelectedProvider.value; if (!p) return []; - const tags = providerModelTags(p.providerType); - const extra = String(p.apiSecret || '').trim(); - if (extra && extra !== '******' && !tags.some((m) => m.modelId === extra)) { - return [...tags, { modelId: extra, role: '自定义' }]; - } - return tags; + return resolveProviderModelTags(p); }); /** 申报品「更多」展开状态,按 item._key */ const openItemMoreKeys = ref>(new Set()); diff --git a/src/views/h5/customer-center/locale/en-US.ts b/src/views/h5/customer-center/locale/en-US.ts index 95ecdc9..5a16968 100644 --- a/src/views/h5/customer-center/locale/en-US.ts +++ b/src/views/h5/customer-center/locale/en-US.ts @@ -919,7 +919,7 @@ export default { 'h5.invite.tab.share': 'Share invite', 'h5.invite.tab.referees': 'Invited', 'h5.invite.lead': - 'Earn freight credit when friends’ deliveries succeed: 5% on 1st order (max ¥50), 3% on orders 2–6 (max ¥30), plus a ¥20 coupon. Friends get ¥5 on sign-up.', + 'Earn freight credit when friends’ deliveries succeed: 5% on 1st order (max ¥50), 3% on orders 2–6 (max ¥30), plus a ¥10 coupon. Friends get ¥5 on sign-up.', 'h5.invite.locked': 'Upgrade to V1 to invite friends', 'h5.invite.code': 'Invite code', 'h5.invite.link': 'Invite link (open in WeChat)', diff --git a/src/views/h5/customer-center/locale/zh-CN.ts b/src/views/h5/customer-center/locale/zh-CN.ts index 932a562..2cc62b9 100644 --- a/src/views/h5/customer-center/locale/zh-CN.ts +++ b/src/views/h5/customer-center/locale/zh-CN.ts @@ -905,7 +905,7 @@ export default { 'h5.invite.tab.share': '分享邀请', 'h5.invite.tab.referees': '已邀请好友', 'h5.invite.lead': - '邀请有奖:好友派送成功后,您得运费余额(首单 5% 最高 50 元,第 2–6 单 3% 最高 30 元),另得 20 元券;好友注册即送 5 元券', + '邀请有奖:好友派送成功后,您得运费余额(首单 5% 最高 50 元,第 2–6 单 3% 最高 30 元),另得 10 元券;好友注册即送 5 元券', 'h5.invite.locked': '升级到 V1 后可邀请好友', 'h5.invite.code': '邀请码', 'h5.invite.link': '邀请链接(微信内打开)', diff --git a/src/views/h5/customer-center/price/index.vue b/src/views/h5/customer-center/price/index.vue index 72754a5..a356431 100644 --- a/src/views/h5/customer-center/price/index.vue +++ b/src/views/h5/customer-center/price/index.vue @@ -9,6 +9,9 @@ v-if="formCollapsed" type="button" class="quote-collapsed" + :class="{ 'is-loading': queryLoading }" + :disabled="queryLoading" + :aria-busy="queryLoading" @click="expandQuoteForm" >
@@ -16,8 +19,14 @@ {{ collapsedMetaText }}
- {{ t('h5.price.editQuery') }} - + + @@ -205,8 +214,12 @@ -
-
+
+

{{ t('h5.price.resultCount', { n: sortedResults.length }) }} @@ -228,8 +241,42 @@

- -
+ +
+
+
+
+ + +
+ +
+ + +
+
+
+ + + +
+
+
+ +
+
+ -
+
@@ -23,6 +23,7 @@ :provider="p" @edit="(item) => emit('edit', item)" @disable="(item) => emit('disable', item)" + @saved="emit('refresh')" />
@@ -75,8 +76,8 @@ const emit = defineEmits<{ .providers-grid { display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 14px; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; } .empty-hint { @@ -87,13 +88,7 @@ const emit = defineEmits<{ line-height: 1.6; } -@media (max-width: 1100px) { - .providers-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } -} - -@media (max-width: 720px) { +@media (max-width: 900px) { .providers-grid { grid-template-columns: 1fr; } diff --git a/src/views/system/aiAssistant/providerCatalog.ts b/src/views/system/aiAssistant/providerCatalog.ts index 1b7300c..b7fa835 100644 --- a/src/views/system/aiAssistant/providerCatalog.ts +++ b/src/views/system/aiAssistant/providerCatalog.ts @@ -61,17 +61,174 @@ export function usesZhipuSdk(typeId: string): boolean { return ZHIPU_SDK_TYPES.has(typeId as ProviderTypeId); } -export function providerModelTags(typeId: string): Array<{ modelId: string; role: string }> { - if (typeId === 'zhipu') { - return [ - { modelId: ZHIPU_CHAT_MODEL, role: '文字' }, - { modelId: ZHIPU_IMAGE_MODEL, role: '图片' }, - { modelId: ZHIPU_VISION_MODEL, role: '图文' }, - ]; +export type ModelRole = '文字' | '图片' | '图文'; + +export interface ProviderModelTag { + modelId: string; + role: ModelRole; + /** 是否为对话默认模型 */ + isChatDefault?: boolean; + /** 是否相对目录默认有改动(可删除) */ + custom?: boolean; + editable?: boolean; +} + +/** apiSecret 多模型存储结构(兼容旧版纯模型名字符串) */ +export interface ProviderModelsConfig { + defaultModelId: string; + models: Array<{ modelId: string; role: ModelRole }>; +} + +export const MODEL_ROLES: ModelRole[] = ['文字', '图片', '图文']; + +export function defaultModel(typeId: string): string { + return PROVIDER_TYPES.find((p) => p.id === typeId)?.defaultModel ?? ''; +} + +export function isMaskedSecret(value?: string | null): boolean { + const text = (value || '').trim(); + return !text || text === '******'; +} + +function isModelRole(value: unknown): value is ModelRole { + return value === '文字' || value === '图片' || value === '图文'; +} + +/** 解析 apiSecret:纯字符串 / JSON 多模型 */ +export function parseProviderModelsConfig( + apiSecret: string | null | undefined, + providerType: string, +): ProviderModelsConfig { + const catalogDefault = defaultModel(providerType); + const secret = (apiSecret || '').trim(); + if (isMaskedSecret(secret)) { + return { + defaultModelId: catalogDefault, + models: catalogDefault ? [{ modelId: catalogDefault, role: '文字' }] : [], + }; } - const def = PROVIDER_TYPES.find((p) => p.id === typeId); - if (!def) return []; - return [{ modelId: def.defaultModel, role: '文字' }]; + + if (secret.startsWith('{')) { + try { + const raw = JSON.parse(secret) as { + defaultModelId?: string; + models?: Array<{ modelId?: string; role?: string }>; + }; + const models: Array<{ modelId: string; role: ModelRole }> = (raw.models ?? []) + .map((m) => ({ + modelId: String(m.modelId || '').trim(), + role: (isModelRole(m.role) ? m.role : '文字') as ModelRole, + })) + .filter((m) => m.modelId); + let defaultModelId = String(raw.defaultModelId || '').trim(); + if (!defaultModelId) { + defaultModelId = + models.find((m) => m.role === '文字')?.modelId || models[0]?.modelId || catalogDefault; + } + if (!models.length && defaultModelId) { + models.push({ modelId: defaultModelId, role: '文字' }); + } + if (defaultModelId && !models.some((m) => m.modelId === defaultModelId)) { + models.unshift({ modelId: defaultModelId, role: '文字' }); + } + return { defaultModelId: defaultModelId || catalogDefault, models }; + } catch { + /* fallthrough as plain model id */ + } + } + + return { + defaultModelId: secret, + models: [{ modelId: secret, role: '文字' }], + }; +} + +/** 序列化到 apiSecret:单模型仍写纯字符串,多模型写 JSON */ +export function serializeProviderModelsConfig(config: ProviderModelsConfig): string { + const models = config.models + .map((m) => ({ + modelId: m.modelId.trim(), + role: isModelRole(m.role) ? m.role : ('文字' as ModelRole), + })) + .filter((m) => m.modelId); + const defaultModelId = + (config.defaultModelId || '').trim() || + models.find((m) => m.role === '文字')?.modelId || + models[0]?.modelId || + ''; + + if (!models.length) return defaultModelId; + + const unique: Array<{ modelId: string; role: ModelRole }> = []; + const seen = new Set(); + for (const m of models) { + const key = `${m.role}::${m.modelId}`; + if (seen.has(key)) continue; + seen.add(key); + unique.push(m); + } + + if (unique.length === 1 && unique[0].role === '文字' && unique[0].modelId === defaultModelId) { + return defaultModelId; + } + + return JSON.stringify({ + defaultModelId: defaultModelId || unique[0].modelId, + models: unique, + }); +} + +/** 目录预设 chips(仅文字主模型;不含用户覆盖) */ +export function providerModelTags(typeId: string): ProviderModelTag[] { + const modelId = defaultModel(typeId); + if (!modelId) return []; + return [{ modelId, role: '文字', editable: true, isChatDefault: true }]; +} + +/** 卡片/选择器:展示已配置的全部模型 chips */ +export function resolveProviderModelTags(item: { + providerType: string; + apiSecret?: string | null; +}): ProviderModelTag[] { + const catalogDefault = defaultModel(item.providerType); + const config = parseProviderModelsConfig(item.apiSecret, item.providerType); + return config.models.map((m) => ({ + modelId: m.modelId, + role: m.role, + editable: true, + isChatDefault: m.modelId === config.defaultModelId && m.role === '文字' + ? true + : m.modelId === config.defaultModelId, + custom: !(m.role === '文字' && m.modelId === catalogDefault && config.models.length === 1), + })); +} + +/** 对话默认文字模型;空则回落目录默认 */ +export function resolveTextModelId(item: { + providerType: string; + apiSecret?: string | null; +}): string { + const config = parseProviderModelsConfig(item.apiSecret, item.providerType); + return ( + config.defaultModelId || + config.models.find((m) => m.role === '文字')?.modelId || + defaultModel(item.providerType) + ); +} + +/** 在已有配置上设置/确保对话默认文字模型 */ +export function upsertDefaultTextModel( + apiSecret: string | null | undefined, + providerType: string, + modelId: string, +): string { + const config = parseProviderModelsConfig(apiSecret, providerType); + const id = modelId.trim(); + if (!id) return serializeProviderModelsConfig(config); + const idx = config.models.findIndex((m) => m.modelId === id && m.role === '文字'); + if (idx < 0) config.models.unshift({ modelId: id, role: '文字' }); + config.defaultModelId = id; + return serializeProviderModelsConfig(config); } export function maskSecret(value?: string | null, visibleTail = 4): string { @@ -81,12 +238,16 @@ export function maskSecret(value?: string | null, visibleTail = 4): string { return '*'.repeat(text.length - visibleTail) + text.slice(-visibleTail); } -export function sortProviders( - items: T[], -): T[] { +export function sortProviders< + T extends { providerKey: string; providerType: string; isDefault?: boolean }, +>(items: T[]): T[] { const order = DEFAULT_PROVIDER_KEYS.map((p) => p.key); const typeOrder = DEFAULT_PROVIDER_KEYS.map((p) => p.type); return [...items].sort((a, b) => { + const aDef = a.isDefault ? 0 : 1; + const bDef = b.isDefault ? 0 : 1; + if (aDef !== bDef) return aDef - bDef; + const ai = order.indexOf(a.providerKey); const bi = order.indexOf(b.providerKey); if (ai >= 0 || bi >= 0) return (ai < 0 ? 99 : ai) - (bi < 0 ? 99 : bi); diff --git a/src/views/system/mcpTools/index.vue b/src/views/system/mcpTools/index.vue index 20c55bb..d4901b0 100644 --- a/src/views/system/mcpTools/index.vue +++ b/src/views/system/mcpTools/index.vue @@ -143,7 +143,7 @@ import { } from '@/api/mcp'; import { listAiProviders } from '@/api/ai'; import { - providerModelTags, + resolveTextModelId, type AiProviderItem, } from '@/views/system/aiAssistant/providerCatalog'; @@ -287,8 +287,7 @@ async function loadProviders() { const def = providers.value.find((p) => p.isDefault) ?? providers.value[0]; if (def) { providerKey.value = def.providerKey; - const tags = providerModelTags(def.providerType); - modelId.value = tags[0]?.modelId || String(def.apiSecret || '').trim() || ''; + modelId.value = resolveTextModelId(def); } } catch { providers.value = [];