| rsf-design/src/locales/langs/en.json | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| rsf-design/src/locales/langs/zh.json | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| rsf-design/src/views/orders/wave/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| rsf-design/src/views/orders/wave/wavePage.helpers.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| rsf-design/src/views/system/ai-mcp-mount/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| rsf-design/src/views/system/ai-mcp-mount/modules/ai-mcp-tools-drawer.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
rsf-design/src/locales/langs/en.json
@@ -4164,6 +4164,11 @@ "toolsLoadFailed": "Failed to load tools", "toolsTimeout": "Tool loading timed out and waiting has stopped", "toolTestTimeout": "Tool call timed out and waiting has stopped", "schemaParseFailed": "Failed to parse schema", "structuredInput": "Structured Input", "formattedSchema": "Formatted Schema", "fieldCount": "{count} fields", "required": "Required", "inputSchema": "Input Schema", "inputJson": "Input JSON", "inputJsonPlaceholder": "Enter JSON, e.g. {\"keyword\":\"task\"}", rsf-design/src/locales/langs/zh.json
@@ -4172,6 +4172,11 @@ "toolsLoadFailed": "工具加载失败", "toolsTimeout": "工具加载超时,已停止等待", "toolTestTimeout": "工具调用超时,已停止等待", "schemaParseFailed": "Schema 解析失败", "structuredInput": "结构化入参", "formattedSchema": "格式化 Schema", "fieldCount": "字段数 {count}", "required": "必填", "inputSchema": "输入 Schema", "inputJson": "输入参数(JSON)", "inputJsonPlaceholder": "请输入 JSON 参数,例如 {\"keyword\":\"task\"}", rsf-design/src/views/orders/wave/index.vue
@@ -433,7 +433,7 @@ { timeoutMessage: t('pages.orders.wave.messages.publicTaskTimeout') } ) publicTaskRows.value = Array.isArray(previewResponse?.records) ? previewResponse.records.map((item) => normalizeWaveItemRow(item, t)) ? previewResponse.records.map((item) => normalizeWaveItemRow(item, t, { placeholder: '' })) : [] updatePaginationState( publicTaskPagination, rsf-design/src/views/orders/wave/wavePage.helpers.js
@@ -187,36 +187,37 @@ } } export function normalizeWaveItemRow(record = {}, t) { export function normalizeWaveItemRow(record = {}, t, options = {}) { const placeholder = options.placeholder ?? '-' const statusConfig = getItemStatusConfig(record.exceStatus, record['exceStatus$'], t) return { ...record, id: record.id ?? null, waveId: record.waveId ?? '-', waveCode: record.waveCode || '-', orderCode: record.orderCode || '-', orderItemId: record.orderItemId ?? '-', matnrId: record.matnrId ?? '-', matnrCode: record.matnrCode || '-', maktx: record.maktx || '-', batch: record.batch || '-', splrBatch: record.splrBatch || '-', unit: record.unit || '-', memo: record.memo || '-', trackCode: record.trackCode || '-', fieldsIndex: record.fieldsIndex || '-', waveId: record.waveId ?? placeholder, waveCode: record.waveCode || placeholder, orderCode: record.orderCode || placeholder, orderItemId: record.orderItemId ?? placeholder, matnrId: record.matnrId ?? placeholder, matnrCode: record.matnrCode || placeholder, maktx: record.maktx || placeholder, batch: record.batch || placeholder, splrBatch: record.splrBatch || placeholder, unit: record.unit || placeholder, memo: record.memo || placeholder, trackCode: record.trackCode || placeholder, fieldsIndex: record.fieldsIndex || placeholder, anfme: normalizeNumber(record.anfme), qty: normalizeNumber(record.qty), workQty: normalizeNumber(record.workQty), stockQty: normalizeNumber(record.stockQty), stockLocsText: normalizeStockLocs(record.stockLocs), updateByText: record['updateBy$'] || record.updateBy || '-', createByText: record['createBy$'] || record.createBy || '-', statusLabel: record['status$'] || record.status || '-', updateByText: record['updateBy$'] || record.updateBy || placeholder, createByText: record['createBy$'] || record.createBy || placeholder, statusLabel: record['status$'] || record.status || placeholder, exceStatusText: statusConfig.label, exceStatusTagType: statusConfig.tagType, updateTimeText: record['updateTime$'] || record.updateTime || '-', createTimeText: record['createTime$'] || record.createTime || '-' updateTimeText: record['updateTime$'] || record.updateTime || placeholder, createTimeText: record['createTime$'] || record.createTime || placeholder } } rsf-design/src/views/system/ai-mcp-mount/index.vue
@@ -11,18 +11,30 @@ <ElCard class="art-table-card"> <div class="mb-5 flex flex-wrap items-center justify-between gap-4"> <div> <h3 class="text-lg font-semibold text-[var(--art-gray-900)]">{{ t('pages.system.aiMcpMount.title') }}</h3> <p class="mt-1 text-sm text-[var(--art-gray-500)]">{{ t('pages.system.aiMcpMount.subtitle') }}</p> <h3 class="text-lg font-semibold text-[var(--art-gray-900)]">{{ t('pages.system.aiMcpMount.title') }}</h3> <p class="mt-1 text-sm text-[var(--art-gray-500)]">{{ t('pages.system.aiMcpMount.subtitle') }}</p> </div> <ElSpace wrap> <ElButton v-auth="'save'" @click="openCreateDialog" v-ripple>{{ t('pages.system.aiMcpMount.buttons.add') }}</ElButton> <ElButton :loading="loading" @click="refreshData" v-ripple>{{ t('common.actions.refresh') }}</ElButton> <ElButton @click="openCreateDialog" v-ripple>{{ t('pages.system.aiMcpMount.buttons.add') }}</ElButton> <ElButton :loading="loading" @click="refreshData" v-ripple>{{ t('common.actions.refresh') }}</ElButton> </ElSpace> </div> <div v-loading="loading" class="space-y-6"> <ElEmpty v-if="!groupedRecords.length" :description="t('pages.system.aiMcpMount.empty')" :image-size="110" /> <ElEmpty v-if="!groupedRecords.length" :description="t('pages.system.aiMcpMount.empty')" :image-size="110" /> <section v-for="group in groupedRecords" :key="group.key" class="space-y-4"> <div> @@ -65,7 +77,9 @@ <div class="rounded-2xl bg-[var(--art-main-bg-color)]/70 p-3 ring-1 ring-inset ring-[var(--art-border-color)]" > <p class="text-xs text-[var(--art-gray-500)]">{{ t('pages.system.aiMcpMount.fields.target') }}</p> <p class="text-xs text-[var(--art-gray-500)]">{{ t('pages.system.aiMcpMount.fields.target') }}</p> <p class="mt-2 break-all text-[var(--art-gray-900)]">{{ item.targetLabel || '--' }}</p> @@ -73,7 +87,9 @@ <div class="rounded-2xl bg-[var(--art-main-bg-color)]/70 p-3 ring-1 ring-inset ring-[var(--art-border-color)]" > <p class="text-xs text-[var(--art-gray-500)]">{{ t('pages.system.aiMcpMount.fields.lastTestTime') }}</p> <p class="text-xs text-[var(--art-gray-500)]">{{ t('pages.system.aiMcpMount.fields.lastTestTime') }}</p> <p class="mt-2 text-[var(--art-gray-900)]">{{ item['lastTestTime$'] || t('pages.system.aiMcpMount.health.notTested') }}</p> @@ -82,7 +98,9 @@ <div class="mt-4 grid gap-3 text-sm sm:grid-cols-3"> <div class="rounded-2xl bg-slate-50 px-3 py-2"> <p class="text-xs text-[var(--art-gray-500)]">{{ t('pages.system.aiMcpMount.fields.timeoutMs') }}</p> <p class="text-xs text-[var(--art-gray-500)]">{{ t('pages.system.aiMcpMount.fields.timeoutMs') }}</p> <p class="mt-1 font-medium text-[var(--art-gray-900)]" >{{ item.requestTimeoutMs ?? '--' }} ms</p > @@ -92,7 +110,9 @@ <p class="mt-1 font-medium text-[var(--art-gray-900)]">{{ item.sort ?? '--' }}</p> </div> <div class="rounded-2xl bg-slate-50 px-3 py-2"> <p class="text-xs text-[var(--art-gray-500)]">{{ t('pages.system.aiMcpMount.fields.lastInitElapsedMs') }}</p> <p class="text-xs text-[var(--art-gray-500)]">{{ t('pages.system.aiMcpMount.fields.lastInitElapsedMs') }}</p> <p class="mt-1 font-medium text-[var(--art-gray-900)]">{{ item.lastInitElapsedMs ?? '--' }}</p> @@ -114,8 +134,12 @@ }}</div> <ElSpace wrap> <ElButton text @click="openDetailDialog(item)">{{ t('common.actions.detail') }}</ElButton> <ElButton v-auth="'update'" text @click="openEditDialog(item)">{{ t('common.actions.edit') }}</ElButton> <ElButton text @click="openDetailDialog(item)">{{ t('common.actions.detail') }}</ElButton> <ElButton v-auth="'update'" text @click="openEditDialog(item)">{{ t('common.actions.edit') }}</ElButton> <ElButton v-auth="'update'" text @@ -124,8 +148,12 @@ > {{ t('pages.system.aiMcpMount.actions.connectivityTest') }} </ElButton> <ElButton v-auth="'list'" text @click="openToolsDrawer(item)">{{ t('pages.system.aiMcpMount.actions.toolsPreview') }}</ElButton> <ElButton v-auth="'remove'" text type="danger" @click="handleDelete(item)">{{ t('common.actions.delete') }}</ElButton> <ElButton v-auth="'list'" text @click="openToolsDrawer(item)">{{ t('pages.system.aiMcpMount.actions.toolsPreview') }}</ElButton> <ElButton v-auth="'remove'" text type="danger" @click="handleDelete(item)">{{ t('common.actions.delete') }}</ElButton> </ElSpace> </div> </article> @@ -265,9 +293,21 @@ const groupedRecords = computed(() => { const groups = [ { key: 'BUILTIN', title: t('pages.system.aiMcpMount.groups.builtin.title'), description: t('pages.system.aiMcpMount.groups.builtin.description') }, { key: 'SSE_HTTP', title: t('pages.system.aiMcpMount.groups.sse.title'), description: t('pages.system.aiMcpMount.groups.sse.description') }, { key: 'STDIO', title: t('pages.system.aiMcpMount.groups.stdio.title'), description: t('pages.system.aiMcpMount.groups.stdio.description') } { key: 'BUILTIN', title: t('pages.system.aiMcpMount.groups.builtin.title'), description: t('pages.system.aiMcpMount.groups.builtin.description') }, { key: 'SSE_HTTP', title: t('pages.system.aiMcpMount.groups.sse.title'), description: t('pages.system.aiMcpMount.groups.sse.description') }, { key: 'STDIO', title: t('pages.system.aiMcpMount.groups.stdio.title'), description: t('pages.system.aiMcpMount.groups.stdio.description') } ] return groups .map((group) => ({ @@ -342,10 +382,11 @@ }), t('crud.confirm.deleteTitle'), { confirmButtonText: t('common.confirm'), cancelButtonText: t('common.cancel'), type: 'warning' }) confirmButtonText: t('common.confirm'), cancelButtonText: t('common.cancel'), type: 'warning' } ) await fetchDeleteAiMcpMount(record.id) ElMessage.success(t('crud.messages.deleteSuccess')) await refreshRemove() @@ -362,7 +403,9 @@ const result = await guardRequestWithMessage(fetchTestAiMcpConnectivity(record.id), null, { timeoutMessage: t('pages.system.aiMcpMount.messages.connectivityTimeout') }) ElMessage.success(result?.message || t('pages.system.aiMcpMount.messages.connectivitySuccess')) ElMessage.success( result?.message || t('pages.system.aiMcpMount.messages.connectivitySuccess') ) await refreshUpdate() } catch (error) { ElMessage.error(error?.message || t('pages.system.aiMcpMount.messages.connectivityFailed')) rsf-design/src/views/system/ai-mcp-mount/modules/ai-mcp-tools-drawer.vue
@@ -65,12 +65,80 @@ > <div class="flex flex-wrap items-start justify-between gap-3"> <div> <div class="text-base font-semibold text-[var(--art-gray-900)]">{{ tool.name }}</div> <div class="mt-1 text-sm text-[var(--art-gray-500)]">{{ tool.description || emptyText }}</div> <div class="text-base font-semibold text-[var(--art-gray-900)]">{{ tool.name }}</div> <div class="mt-1 text-sm text-[var(--art-gray-500)]">{{ tool.description || emptyText }}</div> </div> <ElButton :loading="testingToolName === tool.name" @click="handleToolTest(tool.name)"> {{ t('pages.system.aiMcpMount.toolsDrawer.toolTest') }} </ElButton> <div class="flex flex-wrap items-center gap-2"> <span class="text-xs text-[var(--art-gray-500)]"> {{ t('pages.system.aiMcpMount.toolsDrawer.fieldCount', { count: schemaInfoMap[tool.name]?.fields?.length || 0 }) }} </span> <ElButton :loading="testingToolName === tool.name" @click="handleToolTest(tool.name)" > {{ t('pages.system.aiMcpMount.toolsDrawer.toolTest') }} </ElButton> </div> </div> <ElAlert v-if="schemaInfoMap[tool.name]?.error" class="mt-4" type="warning" :closable="false" > {{ schemaInfoMap[tool.name].error }} </ElAlert> <div v-if="schemaInfoMap[tool.name]?.fields?.length" class="mt-4 rounded-xl bg-[var(--art-main-bg-color)] p-3" > <div class="text-xs text-[var(--art-gray-500)]"> {{ t('pages.system.aiMcpMount.toolsDrawer.structuredInput') }} </div> <div class="mt-3 grid gap-4 md:grid-cols-2"> <div v-for="field in schemaInfoMap[tool.name].fields" :key="`${tool.name}-${field.name}`" > <ElSelect v-if="field.type === 'boolean' || field.enumValues.length" :model-value="structuredInputs[tool.name]?.[field.name] ?? ''" clearable filterable class="w-full" :placeholder="field.title" @update:model-value="handleStructuredFieldChange(tool.name, field.name, $event)" > <ElOption v-for="option in getFieldOptions(field)" :key="`${field.name}-${option.value}`" :label="option.label" :value="option.value" /> </ElSelect> <ElInput v-else :model-value="structuredInputs[tool.name]?.[field.name] ?? ''" clearable :type="field.type === 'integer' || field.type === 'number' ? 'number' : 'text'" :placeholder="field.title" @update:model-value="handleStructuredFieldChange(tool.name, field.name, $event)" /> <div class="mt-1 text-xs text-[var(--art-gray-500)]"> {{ buildFieldHelperText(field, schemaInfoMap[tool.name].required) }} </div> </div> </div> </div> <div class="mt-4 grid gap-4 md:grid-cols-2"> @@ -79,10 +147,11 @@ {{ t('pages.system.aiMcpMount.toolsDrawer.inputJson') }} </div> <ElInput v-model="toolInputs[tool.name]" :model-value="toolInputs[tool.name] || ''" type="textarea" :rows="8" :placeholder="t('pages.system.aiMcpMount.toolsDrawer.inputJsonPlaceholder')" @update:model-value="handleInputChange(tool.name, $event)" /> </div> <div class="space-y-2"> @@ -101,11 +170,12 @@ <div v-if="tool.inputSchema" class="mt-4 rounded-xl bg-[var(--art-main-bg-color)] p-3"> <div class="text-xs text-[var(--art-gray-500)]"> {{ t('pages.system.aiMcpMount.toolsDrawer.inputSchema') }} {{ t('pages.system.aiMcpMount.toolsDrawer.formattedSchema') }} </div> <pre class="mt-2 whitespace-pre-wrap break-all text-xs leading-6 text-[var(--art-gray-900)]">{{ formatSchema(tool.inputSchema) }}</pre> <pre class="mt-2 whitespace-pre-wrap break-all text-xs leading-6 text-[var(--art-gray-900)]" >{{ schemaInfoMap[tool.name]?.pretty || formatSchema(tool.inputSchema) }}</pre > </div> </div> </div> @@ -118,7 +188,11 @@ import { useI18n } from 'vue-i18n' import { ElMessage } from 'element-plus' import { guardRequestWithMessage } from '@/utils/sys/requestGuard' import { fetchPreviewAiMcpTools, fetchTestAiMcpConnectivity, fetchTestAiMcpTool } from '@/api/ai-config' import { fetchPreviewAiMcpTools, fetchTestAiMcpConnectivity, fetchTestAiMcpTool } from '@/api/ai-config' const props = defineProps({ visible: { type: Boolean, default: false }, @@ -133,16 +207,24 @@ const connectivityLoading = ref(false) const connectivityResult = ref(null) const toolInputs = reactive({}) const structuredInputs = reactive({}) const toolOutputs = reactive({}) const testingToolName = ref('') const emptyText = computed(() => t('common.placeholder.empty')) const schemaInfoMap = computed(() => tools.value.reduce((result, tool) => { result[tool.name] = parseInputSchema(tool.inputSchema) return result }, {}) ) function resetState() { tools.value = [] connectivityResult.value = null testingToolName.value = '' Object.keys(toolInputs).forEach((key) => delete toolInputs[key]) Object.keys(structuredInputs).forEach((key) => delete structuredInputs[key]) Object.keys(toolOutputs).forEach((key) => delete toolOutputs[key]) } @@ -154,6 +236,136 @@ } } function parseInputSchema(inputSchema) { if (!inputSchema) { return { pretty: '', fields: [], required: [], error: '' } } try { const schema = JSON.parse(inputSchema) const properties = schema?.properties || {} const required = Array.isArray(schema?.required) ? schema.required : [] return { pretty: JSON.stringify(schema, null, 2), required, error: '', fields: Object.entries(properties).map(([name, definition]) => ({ name, title: definition?.title || name, description: definition?.description || '', type: definition?.type || 'string', enumValues: Array.isArray(definition?.enum) ? definition.enum : [] })) } } catch (error) { return { pretty: inputSchema, fields: [], required: [], error: error?.message || t('pages.system.aiMcpMount.toolsDrawer.schemaParseFailed') } } } function normalizeFieldValue(field, rawValue) { if (rawValue === '' || rawValue === undefined || rawValue === null) { return undefined } if (field.type === 'integer') { const parsed = Number.parseInt(rawValue, 10) return Number.isNaN(parsed) ? rawValue : parsed } if (field.type === 'number') { const parsed = Number(rawValue) return Number.isNaN(parsed) ? rawValue : parsed } if (field.type === 'boolean') { return rawValue === true || rawValue === 'true' } return rawValue } function buildInputJson(schemaInfo, fieldValues) { if (!schemaInfo?.fields?.length) { return '' } const payload = {} schemaInfo.fields.forEach((field) => { const normalized = normalizeFieldValue(field, fieldValues?.[field.name]) if (normalized !== undefined) { payload[field.name] = normalized } }) return JSON.stringify(payload, null, 2) } function readStructuredValues(schemaInfo, inputJson) { if (!schemaInfo?.fields?.length || !inputJson || !inputJson.trim()) { return {} } try { const parsed = JSON.parse(inputJson) if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { return {} } const values = {} schemaInfo.fields.forEach((field) => { const value = parsed[field.name] if (value === undefined || value === null) { return } values[field.name] = typeof value === 'boolean' ? String(value) : String(value) }) return values } catch { return {} } } function getFieldOptions(field) { if (field.type === 'boolean') { return [ { label: 'true', value: 'true' }, { label: 'false', value: 'false' } ] } return field.enumValues.map((value) => ({ label: String(value), value: String(value) })) } function buildFieldHelperText(field, requiredFields = []) { const tokens = [] if (requiredFields.includes(field.name)) { tokens.push(t('pages.system.aiMcpMount.toolsDrawer.required')) } if (field.type) { tokens.push(field.type) } if (field.description) { tokens.push(field.description) } return tokens.join(' · ') || emptyText.value } function handleInputChange(toolName, value) { toolInputs[toolName] = value structuredInputs[toolName] = readStructuredValues(schemaInfoMap.value[toolName], value) } function handleStructuredFieldChange(toolName, fieldName, value) { const schemaInfo = schemaInfoMap.value[toolName] const nextToolValues = { ...(structuredInputs[toolName] || {}), [fieldName]: value } if (value === '' || value === undefined || value === null) { delete nextToolValues[fieldName] } structuredInputs[toolName] = nextToolValues toolInputs[toolName] = buildInputJson(schemaInfo, nextToolValues) } async function loadTools() { if (!props.mountId) return toolsLoading.value = true @@ -162,6 +374,15 @@ timeoutMessage: t('pages.system.aiMcpMount.messages.toolsTimeout') }) tools.value = Array.isArray(response) ? response : [] tools.value.forEach((tool) => { if (!(tool.name in toolInputs)) { toolInputs[tool.name] = '' } structuredInputs[tool.name] = readStructuredValues( schemaInfoMap.value[tool.name], toolInputs[tool.name] ) }) } catch (error) { tools.value = [] ElMessage.error(error?.message || t('pages.system.aiMcpMount.toolsDrawer.toolsLoadFailed')) @@ -174,11 +395,16 @@ if (!props.mountId) return connectivityLoading.value = true try { connectivityResult.value = await guardRequestWithMessage(fetchTestAiMcpConnectivity(props.mountId), null, { timeoutMessage: t('pages.system.aiMcpMount.messages.connectivityTimeout') }) connectivityResult.value = await guardRequestWithMessage( fetchTestAiMcpConnectivity(props.mountId), null, { timeoutMessage: t('pages.system.aiMcpMount.messages.connectivityTimeout') } ) ElMessage.success( connectivityResult.value?.message || t('pages.system.aiMcpMount.messages.connectivitySuccess') connectivityResult.value?.message || t('pages.system.aiMcpMount.messages.connectivitySuccess') ) } catch (error) { ElMessage.error(error?.message || t('pages.system.aiMcpMount.messages.connectivityFailed')) @@ -209,7 +435,8 @@ toolOutputs[toolName] = result?.output || JSON.stringify(result || {}, null, 2) ElMessage.success(t('pages.system.aiMcpMount.toolsDrawer.toolTestSuccess')) } catch (error) { toolOutputs[toolName] = error?.message || t('pages.system.aiMcpMount.toolsDrawer.toolTestFailed') toolOutputs[toolName] = error?.message || t('pages.system.aiMcpMount.toolsDrawer.toolTestFailed') ElMessage.error(error?.message || t('pages.system.aiMcpMount.toolsDrawer.toolTestFailed')) } finally { testingToolName.value = ''