From 30a69b69c7629a4da96e205b8e61f61294836367 Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Wed, 29 Jul 2026 12:47:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20MCP/AI=20Agent=20=E6=8E=A5=E5=85=A5?= =?UTF-8?q?=E9=A1=BE=E9=97=AE=E4=B8=8E=E6=9F=A5=E4=BB=B7=E9=97=AE=20AI?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E8=A1=A5=E9=BD=90=20/ai=20/mcp=20=E5=8F=8D?= =?UTF-8?q?=E4=BB=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .eslintrc-auto-import.json | 13 + nginx-default.conf | 29 + nginx-docker.conf | 23 + src/api/mcp/index.ts | 129 +++++ src/auto-imports.d.ts | 22 + .../NanaAssistant/AiAdvisorShell.vue | 318 +++++++++-- src/components/NanaAssistant/ai-advisor.css | 88 +++ src/composables/assistantMcpGuide.ts | 147 +++++ src/composables/priceAskAdvisor.ts | 99 ++++ src/constants/pageButtonCatalog.ts | 2 + src/locale/en-US.ts | 103 +++- src/locale/zh-CN.ts | 100 +++- .../routes/newModules/system-management.ts | 10 + src/utils/menu-i18n.ts | 1 + src/utils/menu-icon.ts | 1 + src/utils/parcelCreateFailGuide.ts | 26 +- .../components/H5PlaceOrderSheet.vue | 121 ++-- src/views/h5/customer-center/locale/en-US.ts | 14 +- src/views/h5/customer-center/locale/zh-CN.ts | 12 +- src/views/h5/customer-center/price/index.vue | 62 ++- .../myOrders/components/OrderDetailDrawer.vue | 74 ++- src/views/order/placeOrder/index.vue | 201 ++++--- src/views/order/priceCheck/index.vue | 43 +- .../components/OrderDetailDrawer.vue | 74 ++- src/views/system/mcpTools/index.vue | 527 ++++++++++++++++++ vite.config.ts | 14 + 26 files changed, 1962 insertions(+), 291 deletions(-) create mode 100644 src/api/mcp/index.ts create mode 100644 src/composables/assistantMcpGuide.ts create mode 100644 src/composables/priceAskAdvisor.ts create mode 100644 src/views/system/mcpTools/index.vue diff --git a/.eslintrc-auto-import.json b/.eslintrc-auto-import.json index 62ca345..e5e0a2e 100644 --- a/.eslintrc-auto-import.json +++ b/.eslintrc-auto-import.json @@ -10,8 +10,14 @@ "ExtractPropTypes": true, "ExtractPublicPropTypes": true, "InjectionKey": true, + "MCP_CAPABILITY_GUIDES": true, "MaybeRef": true, "MaybeRefOrGetter": true, + "McpCapabilityGuide": true, + "McpCapabilityId": true, + "McpFieldHint": true, + "PriceAskContext": true, + "PriceAskIntent": true, "PropType": true, "Ref": true, "ScanFeedbackType": true, @@ -27,6 +33,7 @@ "asyncComputed": true, "autoResetRef": true, "buildAskAdvisorQuery": true, + "buildPriceAskPrefill": true, "buildWaybillContextFromRow": true, "computed": true, "computedAsync": true, @@ -49,18 +56,23 @@ "defineAsyncComponent": true, "defineComponent": true, "defineStore": true, + "detectMissingFields": true, "eagerComputed": true, "effectScope": true, "extendRef": true, + "extractOrderNo": true, "getActivePinia": true, + "getCapability": true, "getCurrentInstance": true, "getCurrentScope": true, + "getPriceAskLocation": true, "getWaybillContextKey": true, "getWaybillDisplayNo": true, "getWaybillPrefillText": true, "getWaybillReturnPath": true, "h": true, "ignorableWatch": true, + "inferCapabilityFromText": true, "inject": true, "isDefined": true, "isProxy": true, @@ -96,6 +108,7 @@ "onUpdated": true, "onWatcherCleanup": true, "openAskAdvisor": true, + "openPriceAskAdvisor": true, "parseWaybillContextFromQuery": true, "pausableWatch": true, "provide": true, diff --git a/nginx-default.conf b/nginx-default.conf index c60f04b..66c2bf1 100644 --- a/nginx-default.conf +++ b/nginx-default.conf @@ -193,6 +193,9 @@ server { location ^~ /system/xhsAiAssistant { try_files /index.html =200; } + location ^~ /system/mcpTools { + try_files /index.html =200; + } location ^~ /system/dataPerm/ { try_files /index.html =200; } @@ -302,6 +305,32 @@ server { proxy_read_timeout 300s; } + location ^~ /mcp/ { + rewrite ^/(.*)$ /admin/$1 break; + proxy_pass http://api.nanafa.com; + proxy_http_version 1.1; + proxy_set_header Host api.nanafa.com; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 30s; + proxy_send_timeout 30s; + proxy_read_timeout 300s; + } + + location ^~ /ai/ { + rewrite ^/(.*)$ /admin/$1 break; + proxy_pass http://api.nanafa.com; + proxy_http_version 1.1; + proxy_set_header Host api.nanafa.com; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 30s; + proxy_send_timeout 30s; + proxy_read_timeout 300s; + } + # HTML 不缓存 location = /index.html { add_header Cache-Control "no-cache, no-store, must-revalidate"; diff --git a/nginx-docker.conf b/nginx-docker.conf index de694e1..03666a9 100644 --- a/nginx-docker.conf +++ b/nginx-docker.conf @@ -93,6 +93,7 @@ server { location ^~ /system/batchEmail { try_files /index.html =200; } location ^~ /system/otherConfig { try_files /index.html =200; } location ^~ /system/xhsAiAssistant { try_files /index.html =200; } + location ^~ /system/mcpTools { try_files /index.html =200; } location ^~ /system/dataPerm/ { try_files /index.html =200; } location ^~ /api/apiwechat/ { @@ -167,6 +168,28 @@ server { proxy_read_timeout 300s; } + location ^~ /mcp/ { + rewrite ^/(.*)$ /admin/$1 break; + proxy_pass http://api.nanafa.com; + proxy_http_version 1.1; + proxy_set_header Host api.nanafa.com; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 300s; + } + + location ^~ /ai/ { + rewrite ^/(.*)$ /admin/$1 break; + proxy_pass http://api.nanafa.com; + proxy_http_version 1.1; + proxy_set_header Host api.nanafa.com; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 300s; + } + location = /index.html { add_header Cache-Control "no-cache, no-store, must-revalidate"; add_header Pragma "no-cache"; diff --git a/src/api/mcp/index.ts b/src/api/mcp/index.ts new file mode 100644 index 0000000..459ce64 --- /dev/null +++ b/src/api/mcp/index.ts @@ -0,0 +1,129 @@ +/** + * 轻量 MCP 工具管理 / AI Agent + */ +import { axiosInstance } from '@/api/rbac'; +import { resolveAdminApiPath } from '@/utils/adminApi'; + +function mcpUrl(path: string): string { + return resolveAdminApiPath(path); +} + +function assertBusinessOk(body: any) { + if (body == null) throw new Error('空响应'); + const code = body.code ?? body.Code; + const ok = + code === undefined || + code === null || + code === 0 || + code === '0' || + code === 200 || + code === '200'; + if (!ok) { + throw new Error(body.message ?? body.Message ?? body.msg ?? '请求失败'); + } + return body.data ?? body.Data ?? body; +} + +export interface McpToolItem { + id?: number; + toolKey: string; + displayName: string; + description: string; + enabled: boolean; + sortOrder: number; + inputSchemaJson: string; + builtin?: boolean; +} + +export interface McpToolCallResult { + toolKey: string; + success: boolean; + content: string; + elapsedMs: number; +} + +export interface AiAgentToolTrace { + toolKey: string; + argumentsJson: string; + resultPreview: string; + success: boolean; +} + +export interface AiAgentChatResult { + content: string; + toolTraces: AiAgentToolTrace[]; +} + +export async function listMcpTools(): Promise { + const res = await axiosInstance.post(mcpUrl('/mcp/tool/list'), {}); + const data = assertBusinessOk(res.data); + const items = data?.items ?? data?.Items ?? []; + return (items as any[]).map((x) => ({ + id: x.id ?? x.Id, + toolKey: String(x.toolKey ?? x.ToolKey ?? ''), + displayName: String(x.displayName ?? x.DisplayName ?? ''), + description: String(x.description ?? x.Description ?? ''), + enabled: Boolean(x.enabled ?? x.Enabled ?? true), + sortOrder: Number(x.sortOrder ?? x.SortOrder ?? 0), + inputSchemaJson: String(x.inputSchemaJson ?? x.InputSchemaJson ?? ''), + builtin: Boolean(x.builtin ?? x.Builtin ?? true), + })); +} + +export async function saveMcpTool(payload: { + toolKey: string; + displayName: string; + description: string; + enabled: boolean; + sortOrder: number; +}): Promise { + const res = await axiosInstance.post(mcpUrl('/mcp/tool/save'), payload); + const x = assertBusinessOk(res.data) as any; + return { + id: x.id ?? x.Id, + toolKey: String(x.toolKey ?? x.ToolKey ?? ''), + displayName: String(x.displayName ?? x.DisplayName ?? ''), + description: String(x.description ?? x.Description ?? ''), + enabled: Boolean(x.enabled ?? x.Enabled ?? true), + sortOrder: Number(x.sortOrder ?? x.SortOrder ?? 0), + inputSchemaJson: String(x.inputSchemaJson ?? x.InputSchemaJson ?? ''), + builtin: true, + }; +} + +export async function callMcpTool(toolKey: string, argumentsJson: string): Promise { + const res = await axiosInstance.post(mcpUrl('/mcp/tool/call'), { + toolKey, + argumentsJson, + }); + const x = assertBusinessOk(res.data) as any; + return { + toolKey: String(x.toolKey ?? x.ToolKey ?? toolKey), + success: Boolean(x.success ?? x.Success), + content: String(x.content ?? x.Content ?? ''), + elapsedMs: Number(x.elapsedMs ?? x.ElapsedMs ?? 0), + }; +} + +export async function chatAiAgent(payload: { + providerKey: string; + model?: string; + messages: { role: string; content: string }[]; + toolKeys?: string[]; + maxToolRounds?: number; +}): Promise { + // 走 /ai/agent/chat:线上已有;OMS nginx / Vite 须反代 /ai/(见 nginx-*.conf) + // 后端另注册 /mcp/agent/chat,部署后可再切 + const res = await axiosInstance.post(mcpUrl('/ai/agent/chat'), payload); + const x = assertBusinessOk(res.data) as any; + const traces = (x.toolTraces ?? x.ToolTraces ?? []) as any[]; + return { + content: String(x.content ?? x.Content ?? ''), + toolTraces: traces.map((t) => ({ + toolKey: String(t.toolKey ?? t.ToolKey ?? ''), + argumentsJson: String(t.argumentsJson ?? t.ArgumentsJson ?? ''), + resultPreview: String(t.resultPreview ?? t.ResultPreview ?? ''), + success: Boolean(t.success ?? t.Success), + })), + }; +} diff --git a/src/auto-imports.d.ts b/src/auto-imports.d.ts index b8a99e9..577de4d 100644 --- a/src/auto-imports.d.ts +++ b/src/auto-imports.d.ts @@ -8,10 +8,12 @@ export {} declare global { const $baseConfirm: typeof import('./composables/useMessage')['$baseConfirm'] const EffectScope: typeof import('vue')['EffectScope'] + const MCP_CAPABILITY_GUIDES: typeof import('./composables/assistantMcpGuide')['MCP_CAPABILITY_GUIDES'] const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] const asyncComputed: typeof import('@vueuse/core')['asyncComputed'] const autoResetRef: typeof import('@vueuse/core')['autoResetRef'] const buildAskAdvisorQuery: typeof import('./composables/useAskAdvisor')['buildAskAdvisorQuery'] + const buildPriceAskPrefill: typeof import('./composables/priceAskAdvisor')['buildPriceAskPrefill'] const buildWaybillContextFromRow: typeof import('./composables/useAskAdvisor')['buildWaybillContextFromRow'] const computed: typeof import('vue')['computed'] const computedAsync: typeof import('@vueuse/core')['computedAsync'] @@ -34,18 +36,23 @@ declare global { const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] const defineComponent: typeof import('vue')['defineComponent'] const defineStore: typeof import('pinia')['defineStore'] + const detectMissingFields: typeof import('./composables/assistantMcpGuide')['detectMissingFields'] const eagerComputed: typeof import('@vueuse/core')['eagerComputed'] const effectScope: typeof import('vue')['effectScope'] const extendRef: typeof import('@vueuse/core')['extendRef'] + const extractOrderNo: typeof import('./composables/assistantMcpGuide')['extractOrderNo'] const getActivePinia: typeof import('pinia')['getActivePinia'] + const getCapability: typeof import('./composables/assistantMcpGuide')['getCapability'] const getCurrentInstance: typeof import('vue')['getCurrentInstance'] const getCurrentScope: typeof import('vue')['getCurrentScope'] + const getPriceAskLocation: typeof import('./composables/priceAskAdvisor')['getPriceAskLocation'] const getWaybillContextKey: typeof import('./composables/useAskAdvisor')['getWaybillContextKey'] const getWaybillDisplayNo: typeof import('./composables/useAskAdvisor')['getWaybillDisplayNo'] const getWaybillPrefillText: typeof import('./composables/useAskAdvisor')['getWaybillPrefillText'] const getWaybillReturnPath: typeof import('./composables/useAskAdvisor')['getWaybillReturnPath'] const h: typeof import('vue')['h'] const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch'] + const inferCapabilityFromText: typeof import('./composables/assistantMcpGuide')['inferCapabilityFromText'] const inject: typeof import('vue')['inject'] const isDefined: typeof import('@vueuse/core')['isDefined'] const isProxy: typeof import('vue')['isProxy'] @@ -81,6 +88,7 @@ declare global { const onUpdated: typeof import('vue')['onUpdated'] const onWatcherCleanup: typeof import('vue')['onWatcherCleanup'] const openAskAdvisor: typeof import('./composables/useAskAdvisor')['openAskAdvisor'] + const openPriceAskAdvisor: typeof import('./composables/priceAskAdvisor')['openPriceAskAdvisor'] const parseWaybillContextFromQuery: typeof import('./composables/useAskAdvisor')['parseWaybillContextFromQuery'] const pausableWatch: typeof import('@vueuse/core')['pausableWatch'] const provide: typeof import('vue')['provide'] @@ -323,6 +331,12 @@ declare global { export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' import('vue') // @ts-ignore + export type { McpCapabilityId, McpFieldHint, McpCapabilityGuide } from './composables/assistantMcpGuide' + import('./composables/assistantMcpGuide') + // @ts-ignore + export type { PriceAskIntent, PriceAskContext } from './composables/priceAskAdvisor' + import('./composables/priceAskAdvisor') + // @ts-ignore export type { WaybillSource, WaybillContext } from './composables/useAskAdvisor' import('./composables/useAskAdvisor') // @ts-ignore @@ -343,10 +357,12 @@ declare module 'vue' { interface ComponentCustomProperties { readonly $baseConfirm: UnwrapRef readonly EffectScope: UnwrapRef + readonly MCP_CAPABILITY_GUIDES: UnwrapRef readonly acceptHMRUpdate: UnwrapRef readonly asyncComputed: UnwrapRef readonly autoResetRef: UnwrapRef readonly buildAskAdvisorQuery: UnwrapRef + readonly buildPriceAskPrefill: UnwrapRef readonly buildWaybillContextFromRow: UnwrapRef readonly computed: UnwrapRef readonly computedAsync: UnwrapRef @@ -369,18 +385,23 @@ declare module 'vue' { readonly defineAsyncComponent: UnwrapRef readonly defineComponent: UnwrapRef readonly defineStore: UnwrapRef + readonly detectMissingFields: UnwrapRef readonly eagerComputed: UnwrapRef readonly effectScope: UnwrapRef readonly extendRef: UnwrapRef + readonly extractOrderNo: UnwrapRef readonly getActivePinia: UnwrapRef + readonly getCapability: UnwrapRef readonly getCurrentInstance: UnwrapRef readonly getCurrentScope: UnwrapRef + readonly getPriceAskLocation: UnwrapRef readonly getWaybillContextKey: UnwrapRef readonly getWaybillDisplayNo: UnwrapRef readonly getWaybillPrefillText: UnwrapRef readonly getWaybillReturnPath: UnwrapRef readonly h: UnwrapRef readonly ignorableWatch: UnwrapRef + readonly inferCapabilityFromText: UnwrapRef readonly inject: UnwrapRef readonly isDefined: UnwrapRef readonly isProxy: UnwrapRef @@ -416,6 +437,7 @@ declare module 'vue' { readonly onUpdated: UnwrapRef readonly onWatcherCleanup: UnwrapRef readonly openAskAdvisor: UnwrapRef + readonly openPriceAskAdvisor: UnwrapRef readonly parseWaybillContextFromQuery: UnwrapRef readonly pausableWatch: UnwrapRef readonly provide: UnwrapRef diff --git a/src/components/NanaAssistant/AiAdvisorShell.vue b/src/components/NanaAssistant/AiAdvisorShell.vue index c1df5b4..303cafe 100644 --- a/src/components/NanaAssistant/AiAdvisorShell.vue +++ b/src/components/NanaAssistant/AiAdvisorShell.vue @@ -127,6 +127,33 @@

{{ t('nanaAssistant.welcomeTitle') }}

{{ t('nanaAssistant.welcomeSub') }}

+ + +
+ +
+
- + + + + @@ -337,7 +347,17 @@
-
- {{ $t('order.myOrders.drawer.failGuideTitle') }} -

{{ predeclareFailGuide.reason }}

-

- {{ $t('order.myOrders.drawer.failGuideRaw') }}:{{ predeclareFailGuide.rawReason }} +

+ + {{ $t('order.myOrders.drawer.failGuideTip') }} + + {{ predeclareFailGuide.tips[0] }}

-
    -
  • {{ tip }}
  • -
+
+ {{ $t('order.myOrders.drawer.failGuideRaw') }} +

{{ predeclareFailGuide.rawReason }}

+
{{ $t('order.myOrders.drawer.failGuideAskSupport') }} @@ -1923,38 +1924,51 @@ defineExpose({ margin-top: 12px; padding: 12px 14px; border-radius: 8px; - background: #fff7e8; - border: 1px solid #ffe4ba; - - &__title { - font-size: 13px; - font-weight: 600; - color: #1d2129; - margin-bottom: 6px; - } + background: #fff7f7; + border-left: 3px solid #f53f3f; + border-top: 1px solid #ffd8d2; + border-right: 1px solid #ffd8d2; + border-bottom: 1px solid #ffd8d2; &__reason { margin: 0; + font-size: 15px; + font-weight: 600; + line-height: 1.45; + color: #1d2129; + } + + &__tip { + margin: 8px 0 0; font-size: 13px; - line-height: 1.55; + line-height: 1.5; color: #4e5969; } + &__tip-label { + margin-right: 4px; + color: var(--app-primary-color, #2d8c8c); + font-weight: 500; + } + + &__details { + margin-top: 8px; + font-size: 12px; + color: #86909c; + + summary { + cursor: pointer; + user-select: none; + } + } + &__raw { - margin: 4px 0 0; + margin: 6px 0 0; font-size: 12px; color: #86909c; word-break: break-word; } - &__tips { - margin: 8px 0 0; - padding-left: 18px; - font-size: 12px; - line-height: 1.6; - color: #4e5969; - } - &__actions { display: flex; justify-content: flex-end; diff --git a/src/views/order/placeOrder/index.vue b/src/views/order/placeOrder/index.vue index f90e4c3..7c78e70 100644 --- a/src/views/order/placeOrder/index.vue +++ b/src/views/order/placeOrder/index.vue @@ -2904,7 +2904,10 @@