From b43dee5753853ec8ba29052e2b056895eb946f47 Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Wed, 28 Oct 2026 11:03:28 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20AI=20=E6=8F=90=E4=BE=9B=E5=95=86?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=A4=9A=E6=A8=A1=E5=9E=8B=E5=A2=9E=E5=88=A0?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E4=BC=98=E5=8C=96=E6=8E=A5=E5=85=A5=E9=A1=B5?= =?UTF-8?q?=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 卡片可真正追加模型(JSON 存 apiSecret),默认提供商置顶;同步各助手选模逻辑,并附带查价页与邀新文案调整。 Co-authored-by: Cursor --- .../NanaAssistant/AiAdvisorShell.vue | 12 +- .../components/AddParameter.vue | 9 +- .../components/MapAiAssistModal.vue | 9 +- .../components/FormulaAiAssistDrawer.vue | 9 +- .../components/PricePublishAiAssistDrawer.vue | 9 +- .../components/H5PlaceOrderSheet.vue | 9 +- src/views/h5/customer-center/locale/en-US.ts | 2 +- src/views/h5/customer-center/locale/zh-CN.ts | 2 +- src/views/h5/customer-center/price/index.vue | 333 ++++++++++++- src/views/order/placeOrder/index.vue | 9 +- src/views/site/lib/site-welcome.ts | 4 +- .../system/aiAssistant/AiProviderCard.vue | 448 +++++++++++++++--- .../aiAssistant/AiProviderEditModal.vue | 34 +- .../system/aiAssistant/AiProvidersPanel.vue | 15 +- .../system/aiAssistant/providerCatalog.ts | 187 +++++++- src/views/system/mcpTools/index.vue | 5 +- 16 files changed, 907 insertions(+), 189 deletions(-) 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 = []; From 9fe829c6a973a8433b74af0dd55239b774f22b83 Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Wed, 28 Oct 2026 11:17:37 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E6=99=BA=E8=83=BD=E5=8A=A9?= =?UTF-8?q?=E6=89=8B=E5=93=81=E5=90=8D=E5=92=A8=E8=AF=A2=E4=BC=98=E5=85=88?= =?UTF-8?q?=E8=B0=83=E7=94=A8=20oms.channel=5Fguide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 识别能不能寄/走什么渠道;仅品名即可发问,并加强 Agent 系统提示强制调 MCP。 Co-authored-by: Cursor --- .../NanaAssistant/AiAdvisorShell.vue | 18 ++++++++++++++---- src/composables/assistantMcpGuide.ts | 19 ++++++++++++++----- src/locale/nana/en-US.ts | 6 +++--- src/locale/nana/zh-CN.ts | 6 +++--- 4 files changed, 34 insertions(+), 15 deletions(-) diff --git a/src/components/NanaAssistant/AiAdvisorShell.vue b/src/components/NanaAssistant/AiAdvisorShell.vue index 2896912..0d2fd5a 100644 --- a/src/components/NanaAssistant/AiAdvisorShell.vue +++ b/src/components/NanaAssistant/AiAdvisorShell.vue @@ -1026,10 +1026,20 @@ function buildAgentMessages(userFacingMessages: ChatMessage[]) { if (activeCapability.value) { const cap = getCapability(activeCapability.value); - list.push({ - role: 'system', - content: `用户当前意图偏向「${cap.toolKey}」。必填信息已齐时立刻调工具;不要索要起运地;不要空回复。`, - }); + if (cap.id === 'guide') { + list.push({ + role: 'system', + content: + '用户在问品名能不能寄/走什么渠道。必须调用 oms.channel_guide,参数形如 ' + + '{"productCode":"","productName":"","cargoName":"<从用户话提取的品名>"}。' + + '已知渠道再填 productCode/productName,否则留空。不要空回复。', + }); + } else { + list.push({ + role: 'system', + content: `用户当前意图偏向「${cap.toolKey}」。必填信息已齐时立刻调工具;不要索要起运地;不要空回复。`, + }); + } } // 只带最近几轮,避免历史把 token 打爆 const dialog = userFacingMessages diff --git a/src/composables/assistantMcpGuide.ts b/src/composables/assistantMcpGuide.ts index 6eeff48..02b1c69 100644 --- a/src/composables/assistantMcpGuide.ts +++ b/src/composables/assistantMcpGuide.ts @@ -50,8 +50,8 @@ export const MCP_CAPABILITY_GUIDES: McpCapabilityGuide[] = [ exampleKey: 'nanaAssistant.mcp.guideExample', draftKey: 'nanaAssistant.mcp.guideDraft', fields: [ - { key: 'product', required: true, labelKey: 'nanaAssistant.mcp.field.product' }, { key: 'cargoName', required: true, labelKey: 'nanaAssistant.mcp.field.cargoName' }, + { key: 'product', required: false, labelKey: 'nanaAssistant.mcp.field.product' }, ], }, { @@ -80,7 +80,6 @@ const ORDER_NO_LOOSE = /[A-Za-z][A-Za-z0-9\-_]{5,}/ const ORDER_NO_STRICT = /\b(IDE\d{8,}|(?:YD|SF|YT|NF|LT)[A-Za-z0-9]{6,}|(?:[A-Z]{2,}\d{8,}))\b/i -const PRODUCT_HINT = /产品|渠道|线路|运输方式|[A-Z]{2,}[_-][A-Z0-9_]+/i const CARGO_HINT = /品名|货物|普货|敏感|衣服|服装|鞋|化妆品|lipstick|手机|电池|粉末|液体|食品|寄|发/i @@ -113,8 +112,12 @@ export function detectMissingFields( if (!COUNTRY_HINT.test(t)) missing.push('country') if (!WEIGHT_HINT.test(t)) missing.push('weight') } else if (capability === 'guide') { - if (!PRODUCT_HINT.test(t)) missing.push('product') - if (!CARGO_HINT.test(t) && t.replace(/\s/g, '').length < 4) missing.push('cargoName') + // 仅品名必填;销售产品可选(工具可按品名检索对客说明) + const hasCargo = + CARGO_HINT.test(t) || + /[A-Za-z]{2,}/.test(t) || + (t.replace(/\s/g, '').length >= 2 && !/^(能不能|可以|吗|么|寄|发|走)+$/.test(t.replace(/\s/g, ''))) + if (!hasCargo) missing.push('cargoName') } else if (capability === 'track') { if (!opts?.hasWaybillContext && !extractOrderNo(t)) missing.push('orderNo') } @@ -131,7 +134,13 @@ export function inferCapabilityFromText(text: string): McpCapabilityId | null { if (extractOrderNo(raw) && !COUNTRY_HINT.test(raw)) return 'track' if (ORDER_NO_STRICT.test(raw) && raw.length < 40) return 'track' - if (/品名|能发|禁运|可寄|对客说明|承运|敏感货/.test(lower)) return 'guide' + if ( + /品名|能发|禁运|可寄|对客说明|承运|敏感货|能不能走|可以走|走什么渠道|哪个渠道|什么渠道|能不能寄|可以寄|能寄吗|可发吗/.test( + lower, + ) + ) { + return 'guide' + } if (/price|cost|quote|报价|价格|多少钱|运费|查价|怎么走|走得动/.test(lower)) return 'price' diff --git a/src/locale/nana/en-US.ts b/src/locale/nana/en-US.ts index d5549ea..5cba53f 100644 --- a/src/locale/nana/en-US.ts +++ b/src/locale/nana/en-US.ts @@ -40,9 +40,9 @@ export default { 'nanaAssistant.mcp.guideTitle': 'Can this cargo ship?', 'nanaAssistant.mcp.guideDesc': 'See if this item can go on this service', 'nanaAssistant.mcp.guideCoach': - 'I need the sales product (service) and the cargo name.\nExample: product XX, cargo lipstick — can it ship?', - 'nanaAssistant.mcp.guideExample': 'Can lipstick ship?', - 'nanaAssistant.mcp.guideDraft': 'Sales product code/name:\nCargo name:', + 'Tell me the cargo name and I’ll check which channels allow it.\nExample: Can lipstick ship? / Which channel for cosmetics?', + 'nanaAssistant.mcp.guideExample': 'Can lipstick ship, which channel?', + 'nanaAssistant.mcp.guideDraft': 'Cargo name:\nSales product code/name (optional):', 'nanaAssistant.mcp.trackTitle': 'Where is my parcel?', 'nanaAssistant.mcp.trackDesc': 'See where the parcel is now', 'nanaAssistant.mcp.trackCoach': diff --git a/src/locale/nana/zh-CN.ts b/src/locale/nana/zh-CN.ts index 65d1de9..329cbe5 100644 --- a/src/locale/nana/zh-CN.ts +++ b/src/locale/nana/zh-CN.ts @@ -39,9 +39,9 @@ export default { 'nanaAssistant.mcp.guideTitle': '这个货能不能寄', 'nanaAssistant.mcp.guideDesc': '看这个品名能不能走这条渠道', 'nanaAssistant.mcp.guideCoach': - '判断能不能寄需要:销售产品(运输方式)和品名。\n例如:产品 XX,品名 lipstick,能发吗?', - 'nanaAssistant.mcp.guideExample': 'lipstick能寄吗', - 'nanaAssistant.mcp.guideDraft': '销售产品代码/名称:\n客户品名:', + '告诉我品名即可查能不能寄、走什么渠道。\n例如:lipstick 能寄吗? / 化妆品走什么渠道?', + 'nanaAssistant.mcp.guideExample': 'lipstick能寄吗,走什么渠道', + 'nanaAssistant.mcp.guideDraft': '客户品名:\n销售产品代码/名称(选填):', 'nanaAssistant.mcp.trackTitle': '包裹到哪了', 'nanaAssistant.mcp.trackDesc': '看包裹现在到哪了', 'nanaAssistant.mcp.trackCoach':