Files
WC-SPC/MES-Manager_View/mcp-server/server.js
2026-05-27 20:57:27 +08:00

1700 lines
78 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const fs = require('fs')
const path = require('path')
const http = require('http')
const https = require('https')
const ROOT = path.resolve(__dirname, '..')
const DOCS_DIR = path.join(ROOT, 'docs')
const ENV_FILE = path.join(__dirname, '.env.local')
const KNOWLEDGE_FILE = path.join(DOCS_DIR, 'wc-spc-knowledge-base.json')
function loadLocalEnv() {
if (!fs.existsSync(ENV_FILE)) return
fs.readFileSync(ENV_FILE, 'utf8').split(/\r?\n/).forEach(line => {
const text = line.trim()
if (!text || text[0] === '#') return
const index = text.indexOf('=')
if (index <= 0) return
const key = text.slice(0, index).trim()
const value = text.slice(index + 1).trim().replace(/^["']|["']$/g, '')
if (key && process.env[key] == null) process.env[key] = value
})
}
loadLocalEnv()
const PORT = Number(process.env.AI_MCP_PORT || 3100)
const DEEPSEEK_API_KEY = process.env.DEEPSEEK_API_KEY || ''
const DEEPSEEK_BASE_URL = (process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com').replace(/\/$/, '')
const DEEPSEEK_MODEL = process.env.DEEPSEEK_MODEL || 'deepseek-v4-pro'
const MES_BACKEND_URL = process.env.MES_BACKEND_URL || 'http://127.0.0.1:10050/submit/MESCommonBase.ashx'
const SPC_BACKEND_URL = (process.env.SPC_BACKEND_URL || 'http://localhost:57966').replace(/\/$/, '')
const ALLOW_BACKEND_CALLS = process.env.AI_MCP_ALLOW_BACKEND_CALLS === 'true'
const HOME_PAGE_URL = process.env.AI_HOME_PAGE_URL || 'http://127.0.0.1:1997/#/dashboard'
const HOME_PAGE_PATH = '/dashboard'
const QUALITY_DATA_DEFAULT_LIMIT = 50
function readJson(file, fallback) {
try {
return JSON.parse(fs.readFileSync(file, 'utf8'))
} catch (error) {
return fallback
}
}
function emptyKnowledgeBase() {
return {
generatedAt: null,
modules: [],
apiMappings: [],
sqlTables: [],
qualityModules: [],
qualityDataQueries: [],
spcCharts: [],
queryHints: []
}
}
let knowledgeBase = readJson(KNOWLEDGE_FILE, emptyKnowledgeBase())
const QUALITY_DATA_QUERY_PROFILE = {
key: 'assembly-quality-data',
title: '质量数据查询',
routePath: '/QualityAssurance/AssemblyQualitydataQuery',
view: 'src\\views\\QualityAssurance\\AssemblyQualitydataQuery\\index.vue',
module: 'QualityAssurance',
method: 'selectDate',
type: '11',
nameOrSql: '质量数据_发动机质量数据_各个工位_视图_发动机型号_综合查询',
exportType: '2001',
exportNameOrSql: '质量数据查询_综合查询新',
description: '按时间、工位、工件编号、订货号、订货号ID、机型号、工单ID、工单号、是否合格、测量位置、测量项目查询发动机各工位质量数据无筛选条件时按全时间范围返回最新 50 条。',
defaultStrategy: '无查询条件时使用 1900-01-01 00:00:00 至 2099-12-31 23:59:59、全部筛选_ischeck=0、PageCurrent=1、PageSize=50并按生产日期等时间字段倒序展示最新 50 条。',
latestSortFields: ['生产日期', '操作时间', '完成时间', '到达时间', '离开时间', '时间'],
parameters: [
{ name: '开始时间', arg: 'startTime', defaultValue: '1900-01-01 00:00:00', required: true },
{ name: '结束时间', arg: 'endTime', defaultValue: '2099-12-31 23:59:59', required: true },
{ name: '工位号_ischeck', arg: 'stationNumber', checkFor: '工位号' },
{ name: '工位号', arg: 'stationNumber' },
{ name: 'PageCurrent', arg: 'pageCurrent', defaultValue: 1 },
{ name: 'PageSize', arg: 'pageSize', defaultValue: 50 },
{ name: 'PageCount', defaultValue: '1111', type: 'int', output: '1' },
{ name: 'ItemCount', defaultValue: '1111', type: 'int', output: '1' },
{ name: '工件编号_ischeck', arg: 'engineId', checkFor: '工件编号' },
{ name: '工件编号', arg: 'engineId' },
{ name: '订货号_ischeck', arg: 'orderCode', checkFor: '订货号' },
{ name: '订货号', arg: 'orderCode' },
{ name: '订货号ID_ischeck', arg: 'orderCodeId', checkFor: '订货号ID' },
{ name: '订货号ID', arg: 'orderCodeId' },
{ name: '机型号_ischeck', arg: 'engineType', checkFor: '机型号' },
{ name: '机型号', arg: 'engineType' },
{ name: '工单ID_ischeck', arg: 'workOrderId', checkFor: '工单ID' },
{ name: '工单ID', arg: 'workOrderId' },
{ name: '工单号_ischeck', arg: 'workOrderCode', checkFor: '工单号' },
{ name: '工单号', arg: 'workOrderCode' },
{ name: '是否合格', arg: 'isOk', defaultValue: '0' },
{ name: '测量位置_ischeck', arg: 'testPosition', checkFor: '测量位置' },
{ name: '测量位置', arg: 'testPosition' },
{ name: '测量项目_ischeck', arg: 'testItem', checkFor: '测量项目' },
{ name: '测量项目', arg: 'testItem' }
],
tableFields: [
{ label: '工位名称', prop: '工序名称' },
{ label: '工件编号', prop: '发动机号' },
{ label: '机型号', prop: '机型号' },
{ label: '订货号', prop: '订货号' },
{ label: '订货号ID', prop: '订货号ID' },
{ label: '工单ID', prop: '工单ID' },
{ label: '工单号', prop: '工单号' },
{ label: '测量位置', prop: '测量位置' },
{ label: '测量项目', prop: '测量项目' },
{ label: '测量值', prop: '测量值' },
{ label: '理论值', prop: '理论值' },
{ label: '上限值', prop: '上限值' },
{ label: '下限值', prop: '下限值' },
{ label: '测量单位', prop: '测量单位' },
{ label: '生产日期', prop: '生产日期' },
{ label: '合格标志', prop: '合格标志' }
]
}
function lower(value) {
return String(value || '').toLowerCase()
}
function unique(values) {
return Array.from(new Set(values.filter(Boolean)))
}
function escapeHtml(value) {
return String(value == null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
}
function table(headers, rows) {
const dataRows = Array.isArray(rows) ? rows : []
let html = '<table class="ai-result-table"><thead><tr>'
headers.forEach(header => {
html += '<th>' + escapeHtml(header.label) + '</th>'
})
html += '</tr></thead><tbody>'
if (!dataRows.length) {
html += '<tr><td colspan="' + headers.length + '">无匹配数据</td></tr>'
} else {
dataRows.forEach(row => {
html += '<tr>'
headers.forEach(header => {
const value = row && row[header.key]
html += '<td>' + escapeHtml(Array.isArray(value) ? value.join(', ') : value) + '</td>'
})
html += '</tr>'
})
}
html += '</tbody></table>'
return html
}
function isNgValue(value) {
const text = String(value == null ? '' : value).trim()
return /不合格|NG|false/i.test(text) || text === '0'
}
function firstValue(row, keys) {
for (let i = 0; i < keys.length; i++) {
const value = row && row[keys[i]]
if (value != null && value !== '') return value
}
return ''
}
function detailTable(headers, rows) {
const dataRows = Array.isArray(rows) ? rows : []
let html = '<div class="ai-detail-table-wrap"><table class="ai-detail-table"><thead><tr>'
headers.forEach(header => {
html += '<th>' + escapeHtml(header.label) + '</th>'
})
html += '</tr></thead><tbody>'
if (!dataRows.length) {
html += '<tr><td colspan="' + headers.length + '">无数据</td></tr>'
} else {
dataRows.forEach(row => {
html += '<tr>'
headers.forEach(header => {
const value = row && row[header.key]
const className = header.status ? (isNgValue(value) ? ' class="ai-status-ng"' : '') : ''
html += '<td' + className + '>' + escapeHtml(Array.isArray(value) ? value.join(', ') : value) + '</td>'
})
html += '</tr>'
})
}
html += '</tbody></table></div>'
return html
}
function normalizeRows(rows, limit) {
if (!Array.isArray(rows)) return []
return rows.slice(0, limit || 30).map(row => {
if (!row || typeof row !== 'object') return { value: row }
return row
})
}
function htmlForRows(title, rows, limit) {
const normalized = normalizeRows(rows, limit || 30)
const keys = unique(normalized.flatMap(row => Object.keys(row))).slice(0, 12)
if (!keys.length) return '<h4>' + escapeHtml(title) + '</h4>' + table([{ key: 'message', label: '结果' }], [{ message: '无数据' }])
return '<h4>' + escapeHtml(title) + '</h4>' + table(keys.map(key => ({ key, label: key })), normalized)
}
function qualityDetailRows(rows) {
return normalizeRows(rows, 50).map(row => ({
productionDate: firstValue(row, ['生产日期', '操作时间', '完成时间', '到达时间', '离开时间', '时间']),
stationName: firstValue(row, ['工序名称', '工位名称', '工位号', '指南工位号']),
workpieceNo: firstValue(row, ['发动机号', '工件编号', '工件号', '总成号']),
measureItem: firstValue(row, ['测量项目', '测量内容', '项目']),
measurePosition: firstValue(row, ['测量位置', '位置']),
measureValue: firstValue(row, ['测量值', '实际值', '值']),
upperLimit: firstValue(row, ['上限值', '上限']),
lowerLimit: firstValue(row, ['下限值', '下限']),
passFlag: firstValue(row, ['合格标志', '是否合格', '合格'])
}))
}
function htmlForQualityDetailRows(rows) {
return '<h4>质量数据明细部分按生产日期倒序最多50条</h4>' + detailTable([
{ key: 'productionDate', label: '生产日期' },
{ key: 'stationName', label: '工位名称' },
{ key: 'workpieceNo', label: '工件编号' },
{ key: 'measureItem', label: '测量项目' },
{ key: 'measurePosition', label: '测量位置' },
{ key: 'measureValue', label: '测量值' },
{ key: 'upperLimit', label: '上限值' },
{ key: 'lowerLimit', label: '下限值' },
{ key: 'passFlag', label: '合格标志', status: true }
], qualityDetailRows(rows))
}
function tokens(query) {
const source = lower(query)
const parts = source
.replace(/[^\w\u4e00-\u9fa5]+/g, ' ')
.split(/\s+/)
.map(item => item.trim())
.filter(item => item && item.length > 1)
const grams = []
parts.forEach(part => {
if (/[\u4e00-\u9fa5]/.test(part) && part.length > 2) {
for (let size = 2; size <= Math.min(4, part.length); size++) {
for (let index = 0; index <= part.length - size; index++) {
grams.push(part.substr(index, size))
}
}
}
})
return unique(parts.concat(grams)).filter(item => ['页面', '接口', '数据', '查询', '功能', '模块'].indexOf(item) === -1)
}
function scoreText(text, query) {
const haystack = lower(text)
const compactHaystack = haystack.replace(/\s+/g, '')
const q = lower(query)
const compactQuery = q.replace(/\s+/g, '')
if (!q) return 1
let score = compactQuery && compactHaystack.indexOf(compactQuery) > -1 ? 50 : 0
tokens(query).forEach(part => {
if (haystack.indexOf(part) > -1) score += part.length
})
return score
}
function pick(rows, query, fields, limit) {
return (rows || [])
.map(row => {
const text = fields.map(field => {
const value = row[field]
return Array.isArray(value) ? value.join(' ') : value
}).join(' ')
return { row, score: scoreText(text, query) }
})
.filter(item => item.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, limit || 20)
.map(item => item.row)
}
function routeCandidates(question, limit) {
const query = String(question || '').replace(/打开|跳转|进入|导航|前往|去到|查看|页面|模块/g, ' ')
return (knowledgeBase.modules || [])
.filter(row => row.routePath)
.map(row => {
const text = [row.module, row.title, row.view, row.routePath, row.domains, row.functionNames, row.keywords].join(' ')
let score = scoreText(text, query || question)
const compactQuestion = String(question || '').replace(/\s+/g, '').toLowerCase()
const compactPath = String(row.routePath || '').replace(/[\/_\s]+/g, '').toLowerCase()
const compactTitle = String(row.title || '').replace(/[\/_\s]+/g, '').toLowerCase()
if (compactQuestion.indexOf('spc分析') > -1 && compactPath.indexOf('searchdataspcanalysis') > -1) score += 100
if (compactQuestion.indexOf('spc') > -1 && compactTitle.indexOf('spc') > -1) score += 30
if (compactQuestion.indexOf('质量andon') > -1 && compactPath.indexOf('qualityandon') > -1) score += 40
if (compactQuestion.indexOf('统计') === -1 && compactQuestion.indexOf('质量andon') > -1 && compactPath.indexOf('qualityandonstatistics') > -1) score -= 25
if (compactQuestion.indexOf('统计') > -1 && compactPath.indexOf('statistics') > -1) score += 40
return Object.assign({ score }, row)
})
.filter(row => row.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, limit || 8)
}
function moduleFeatureRows() {
if (Array.isArray(knowledgeBase.moduleFeatureProfiles) && knowledgeBase.moduleFeatureProfiles.length) {
return knowledgeBase.moduleFeatureProfiles
}
return (knowledgeBase.modules || []).map(row => Object.assign({
view: row.view,
module: row.module,
title: row.title,
routePath: row.routePath,
filters: [],
buttons: [],
tableFields: [],
lifecycle: [],
methods: [],
backendCalls: row.calls || [],
actions: [],
riskLevel: row.queryable ? '包含查询入口' : '未识别到数据调用',
description: row.title || row.view,
keywords: row.keywords || []
}, row.featureProfile || {}))
}
function moduleFeatureCandidates(question, limit) {
const q = String(question || '')
return moduleFeatureRows()
.map(row => {
const text = [
row.module,
row.title,
row.view,
row.routePath,
row.description,
row.actions,
(row.filters || []).map(item => [item.label, item.model, item.control].join(' ')).join(' '),
(row.buttons || []).map(item => [item.label, item.method, item.action].join(' ')).join(' '),
(row.tableFields || []).map(item => [item.label, item.prop].join(' ')).join(' '),
(row.backendCalls || []).map(item => item.nameOrSql || item.NameOrSql).join(' '),
row.keywords
].join(' ')
let score = scoreText(text, question)
if (/统计/.test(q) && /Statistics/i.test(row.view + ' ' + row.routePath + ' ' + row.title)) score += 40
if (!/统计/.test(q) && /(页面|查询页面|模块|功能|有什么)/.test(q) && /Statistics/i.test(row.view + ' ' + row.routePath + ' ' + row.title)) score -= 25
if (/质量.*Andon|Andon.*质量/i.test(q) && /qualityAndon\\index\.vue/i.test(row.view)) score += 40
return Object.assign({ score }, row)
})
.filter(row => row.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, limit || 8)
}
function isFeatureQuestion(question) {
const text = String(question || '')
return /(功能|有什么|有哪些|介绍|说明|页面|模块|按钮|字段|表格|筛选|条件|接口|入口|怎么用|如何使用|对应.*代码|功能代码|详细解析)/i.test(text) &&
!/(查数据|查询数据|数据库|明细数据|最近\s*\d+\s*天.*数据|按.*查询.*数据|统计数据|实际数据|返回数据)/i.test(text)
}
function isNavigationQuestion(question) {
return /(打开|跳转|进入|导航|前往|去到|切换到)/.test(String(question || '')) || /查看.*页面/.test(String(question || ''))
}
function isHomeNavigationQuestion(question) {
const text = String(question || '').replace(/\s+/g, '')
return /(打开|跳转|进入|导航|前往|去到|切换到|返回|回到).*(主页|首页|主页面|仪表盘|dashboard)/i.test(text) ||
/(主页|首页|主页面|仪表盘|dashboard).*(打开|跳转|进入|导航|前往|去到|切换到)/i.test(text)
}
function navigationResponse(question) {
if (!isNavigationQuestion(question)) return null
if (isHomeNavigationQuestion(question)) {
const target = {
title: '主页',
routePath: HOME_PAGE_PATH,
url: HOME_PAGE_URL,
view: 'dashboard'
}
return {
html: '<h4>页面导航</h4>' + table([
{ key: 'title', label: '页面' },
{ key: 'routePath', label: '路由' },
{ key: 'url', label: '目标地址' }
], [target]),
actions: [{ type: 'navigate', path: HOME_PAGE_PATH, url: HOME_PAGE_URL, title: target.title, view: target.view }]
}
}
const candidates = routeCandidates(question, 8)
if (!candidates.length) {
return { html: table([{ key: '提示', label: '提示' }], [{ 提示: '未找到匹配页面,请换一个页面名或模块名。' }]), actions: [] }
}
const target = candidates[0]
return {
html: '<h4>页面导航</h4>' + table([
{ key: 'title', label: '页面' },
{ key: 'routePath', label: '路由' },
{ key: 'view', label: '源码文件' }
], [target]),
actions: [{ type: 'navigate', path: target.routePath, title: target.title, view: target.view }]
}
}
function isQualityQuestion(question) {
return /质量|SPC|巡检|条码|Andon|安灯|测量|工位|过程能力|趋势|直方|正态|排列|X-R|XR|X-S|XS/i.test(String(question || ''))
}
function listQualityModules(limit) {
return (knowledgeBase.qualityModules || []).slice(0, limit || 50).map(row => ({
title: row.Title,
routePath: row.RoutePath,
domains: row.Domains,
queryable: row.Queryable,
view: row.View
}))
}
function qualitySearchResponse(question) {
const rows = pick(knowledgeBase.qualityModules, question, ['Title', 'View', 'Domains', 'QueryNames'], 12)
return '<h4>质量管理匹配模块</h4>' + table([
{ key: 'Title', label: '页面' },
{ key: 'RoutePath', label: '路由' },
{ key: 'Domains', label: '业务域' },
{ key: 'Queryable', label: '可查询' }
], rows)
}
function isSimpleLookupQuestion(question) {
const text = String(question || '')
return /^(项目|功能|模块|页面|查询质量数据|质量数据查询|质量Andon|Andon)/i.test(text) || /查看.*页面/.test(text)
}
function featureSummaryRows(profile) {
return [
{ name: '页面', value: profile.title || '' },
{ name: '路由', value: profile.routePath || '' },
{ name: '源码', value: profile.view || '' },
{ name: '一级模块', value: profile.module || '' },
{ name: '功能概要', value: profile.description || '' },
{ name: '读写风险', value: profile.riskLevel || '' }
]
}
function qualityDataProfileRows(profile) {
return [
{ name: '功能名称', value: profile.title || '' },
{ name: '页面路由', value: profile.routePath || '' },
{ name: '源码文件', value: profile.view || '' },
{ name: '默认查询入口', value: profile.nameOrSql || '' },
{ name: '导出入口', value: profile.exportNameOrSql || '' },
{ name: '说明', value: profile.description || '' }
]
}
function qualityDataParameterRows(profile, args) {
const inputs = buildStructuredQualityDataParams(args || {}, profile)
return inputs.map(item => ({
name: item.name,
value: item.value,
output: item.output || '',
type: item.type || ''
}))
}
function describeQualityDataQuery(profile, args) {
let html = '<h4>质量数据查询功能</h4>' + table([
{ key: 'name', label: '项目' },
{ key: 'value', label: '内容' }
], qualityDataProfileRows(profile))
html += '<h4>查询参数</h4>' + table([
{ key: 'name', label: '参数' },
{ key: 'value', label: '值' },
{ key: 'type', label: '类型' },
{ key: 'output', label: '输出' }
], qualityDataParameterRows(profile, args))
html += '<h4>结果字段</h4>' + table([
{ key: 'label', label: '字段' },
{ key: 'prop', label: '数据字段' },
{ key: 'width', label: '宽度' }
], profile.tableFields || [])
html += '<h4>查询入口</h4>' + table([
{ key: 'type', label: '类型' },
{ key: 'nameOrSql', label: '入口名称' },
{ key: 'role', label: '角色' }
], [{
type: profile.type,
nameOrSql: profile.nameOrSql,
role: '页面已有查询方法 selectDate() 的主查询入口'
}])
html += '<h4>导出入口</h4>' + table([
{ key: 'exportType', label: '类型' },
{ key: 'exportNameOrSql', label: '导出入口' }
], [{
exportType: profile.exportType,
exportNameOrSql: profile.exportNameOrSql
}])
return html
}
function describeModuleFeatures(question, limit) {
const candidates = moduleFeatureCandidates(question || '', limit || 5)
if (!candidates.length) {
return '<h4>模块功能说明</h4>' + table([{ key: 'message', label: '提示' }], [{ message: '未找到匹配模块,请换一个页面名、路由或业务关键词。' }])
}
const target = candidates[0]
let html = '<h4>模块功能说明</h4>' + table([
{ key: 'name', label: '项目' },
{ key: 'value', label: '内容' }
], featureSummaryRows(target))
html += '<h4>筛选条件</h4>' + table([
{ key: 'label', label: '条件' },
{ key: 'model', label: '绑定字段' },
{ key: 'control', label: '控件' },
{ key: 'type', label: '类型' }
], target.filters || [])
html += '<h4>页面操作</h4>' + table([
{ key: 'label', label: '按钮/操作' },
{ key: 'action', label: '动作' },
{ key: 'method', label: '方法' },
{ key: 'icon', label: '图标' }
], target.buttons || [])
html += '<h4>表格字段</h4>' + table([
{ key: 'label', label: '显示字段' },
{ key: 'prop', label: '数据字段' },
{ key: 'width', label: '宽度' }
], target.tableFields || [])
html += '<h4>自动加载</h4>' + table([
{ key: 'hook', label: '生命周期' },
{ key: 'calls', label: '调用方法' }
], target.lifecycle || [])
html += '<h4>后端数据入口</h4>' + table([
{ key: 'source', label: '来源' },
{ key: 'type', label: '类型' },
{ key: 'action', label: '动作' },
{ key: 'domain', label: '业务域' },
{ key: 'nameOrSql', label: '查询入口/SQL' },
{ key: 'role', label: '说明' }
], target.backendCalls || [])
if (candidates.length > 1) {
html += '<h4>其他匹配模块</h4>' + table([
{ key: 'title', label: '页面' },
{ key: 'routePath', label: '路由' },
{ key: 'riskLevel', label: '读写风险' }
], candidates.slice(1, 6))
}
return html
}
function searchModuleFeatures(question, limit) {
const rows = moduleFeatureCandidates(question || '', limit || 20).map(row => ({
title: row.title,
routePath: row.routePath,
module: row.module,
actions: (row.actions || []).join('、'),
filters: (row.filters || []).map(item => item.label).slice(0, 6).join('、'),
buttons: (row.buttons || []).map(item => item.label).slice(0, 6).join('、'),
riskLevel: row.riskLevel
}))
return '<h4>模块功能匹配结果</h4>' + table([
{ key: 'title', label: '页面' },
{ key: 'routePath', label: '路由' },
{ key: 'module', label: '一级模块' },
{ key: 'actions', label: '动作' },
{ key: 'filters', label: '筛选条件' },
{ key: 'buttons', label: '页面按钮' },
{ key: 'riskLevel', label: '读写风险' }
], rows)
}
function spcChartType(question) {
const text = String(question || '')
const rules = [
{ type: 'xr', re: /X-R|XR|极差|均值.*极差/i },
{ type: 'xs', re: /X-S|XS|标准差|均值.*标准差/i },
{ type: 'pareto', re: /排列|帕累托|pareto/i },
{ type: 'processCapability', re: /过程能力|CPK|CP|能力/i },
{ type: 'normalDistribution', re: /正态|分布|normal/i },
{ type: 'histogram', re: /直方|频数|histogram/i },
{ type: 'sampleTrend', re: /样本趋势|样本/i },
{ type: 'basicTrend', re: /基本趋势|趋势|折线/i }
]
const match = rules.find(rule => rule.re.test(text))
return match ? match.type : ''
}
function parseDateRange(question) {
const text = String(question || '')
const explicit = text.match(/(\d{4}[-/]\d{1,2}[-/]\d{1,2}).{0,8}(\d{4}[-/]\d{1,2}[-/]\d{1,2})/)
if (explicit) return { startTime: explicit[1].replace(/\//g, '-'), endTime: explicit[2].replace(/\//g, '-') }
const today = new Date()
const end = today.toISOString().slice(0, 10)
const daysMatch = text.match(/最近\s*(\d+)\s*天/)
const days = daysMatch ? Math.min(Math.max(Number(daysMatch[1]), 1), 90) : 7
const startDate = new Date(today.getTime() - (days - 1) * 24 * 60 * 60 * 1000)
return { startTime: startDate.toISOString().slice(0, 10), endTime: end }
}
function parseQuotedOrNamed(question, names) {
const text = String(question || '')
const quoted = text.match(/[“"']([^“”"']+)[”"']/)
if (quoted) return quoted[1]
for (let i = 0; i < names.length; i++) {
const name = names[i]
const re = new RegExp(name + '\\s*[:]?\\s*([^,。\\s]+)')
const match = text.match(re)
if (match) return match[1]
}
return ''
}
function parseSpcParams(question) {
const dateRange = parseDateRange(question)
const sampleSize = (String(question).match(/样本容量\s*[:]?\s*(\d+)/) || [])[1] || '5'
const sampleNumber = (String(question).match(/样本(?:个数|数量)\s*[:]?\s*(\d+)/) || [])[1] || '20'
const usl = (String(question).match(/USL|上限|上规格限/i) || {}).index != null ? ((String(question).match(/(?:USL|上限|上规格限)\s*[:]?\s*(-?\d+(?:\.\d+)?)/i) || [])[1] || '') : ''
const lsl = (String(question).match(/LSL|下限|下规格限/i) || {}).index != null ? ((String(question).match(/(?:LSL|下限|下规格限)\s*[:]?\s*(-?\d+(?:\.\d+)?)/i) || [])[1] || '') : ''
return Object.assign({
opName: parseQuotedOrNamed(question, ['工位', '工位号', '工位名称']),
model: parseQuotedOrNamed(question, ['机型', '型号', '零件号']),
measureName: parseQuotedOrNamed(question, ['测量位置', '位置']),
measureContent: parseQuotedOrNamed(question, ['测量项目', '测量内容', '项目']),
sampleSize,
sampleNumber,
usl,
lsl,
pageSize: 20,
pageCurrent: 1
}, dateRange)
}
function missingSpcParams(params) {
const missing = []
if (!params.opName) missing.push('工位')
if (!params.measureName) missing.push('测量位置')
if (!params.measureContent) missing.push('测量项目')
return missing
}
function buildUrl(endpoint, type, params) {
const query = {
type,
OPNameCheck: params.opName ? 'true' : 'false',
OPName: params.opName || '',
shaftNameCheck: params.measureName ? 'true' : 'false',
shaftName: params.measureName || '',
EngineTypeIDCheck: params.model ? 'true' : 'false',
EngineTypeID: params.model || '',
itemNameCheck: params.measureContent ? 'true' : 'false',
itemName: params.measureContent || '',
startTime: params.startTime,
endTime: params.endTime,
Yangben: params.sampleSize || '5',
Num: params.sampleNumber || '20'
}
if (params.usl) query.Usl = params.usl
if (params.lsl) query.Lsl = params.lsl
if (type === 'GetDataTableByConditent') {
query.page = params.pageCurrent || 1
query.rows = params.pageSize || 20
}
const qs = Object.keys(query).map(key => encodeURIComponent(key) + '=' + encodeURIComponent(query[key])).join('&')
return SPC_BACKEND_URL + '/submit/' + endpoint + '?' + qs
}
function getJson(url) {
return new Promise((resolve, reject) => {
const target = new URL(url)
const client = target.protocol === 'https:' ? https : http
const req = client.request({
method: 'POST',
hostname: target.hostname,
port: target.port || (target.protocol === 'https:' ? 443 : 80),
path: target.pathname + target.search
}, res => {
let text = ''
res.setEncoding('utf8')
res.on('data', chunk => { text += chunk })
res.on('end', () => {
try {
resolve(JSON.parse(text))
} catch (error) {
resolve(text)
}
})
})
req.on('error', reject)
req.end()
})
}
function numericValuesFromRows(rows) {
return normalizeRows(rows, 1000).map(row => {
const keys = Object.keys(row)
const key = keys.find(item => /测量值|value|Value|数值/i.test(item)) || keys.find(item => !Number.isNaN(Number(row[item])))
return key ? Number(row[key]) : NaN
}).filter(value => !Number.isNaN(value))
}
function summaryForValues(values) {
if (!values.length) return []
const sum = values.reduce((acc, item) => acc + item, 0)
const avg = sum / values.length
const variance = values.reduce((acc, item) => acc + Math.pow(item - avg, 2), 0) / values.length
return [
{ name: '样本数', value: values.length },
{ name: '最小值', value: Math.min.apply(null, values).toFixed(4) },
{ name: '最大值', value: Math.max.apply(null, values).toFixed(4) },
{ name: '平均值', value: avg.toFixed(4) },
{ name: '标准差', value: Math.sqrt(variance).toFixed(4) }
]
}
function simpleLineChart(title, values) {
return {
id: 'chart_' + Date.now() + '_' + Math.floor(Math.random() * 10000),
title,
option: {
title: { text: title, left: 'center' },
tooltip: { trigger: 'axis' },
grid: { left: 45, right: 20, top: 55, bottom: 35 },
xAxis: { type: 'category', data: values.map((_, index) => String(index + 1)) },
yAxis: { type: 'value', scale: true },
series: [{ name: '测量值', type: 'line', smooth: true, data: values }]
}
}
}
function simpleBarChart(title, values) {
const bins = {}
values.forEach(value => {
const key = value.toFixed(2)
bins[key] = (bins[key] || 0) + 1
})
const keys = Object.keys(bins).sort((a, b) => Number(a) - Number(b))
return {
id: 'chart_' + Date.now() + '_' + Math.floor(Math.random() * 10000),
title,
option: {
title: { text: title, left: 'center' },
tooltip: { trigger: 'axis' },
grid: { left: 45, right: 20, top: 55, bottom: 45 },
xAxis: { type: 'category', data: keys },
yAxis: { type: 'value' },
series: [{ name: '频数', type: 'bar', data: keys.map(key => bins[key]) }]
}
}
}
async function querySpcChart(args) {
const chartType = args.chartType || spcChartType(args.query || '') || 'basicTrend'
const params = Object.assign(parseSpcParams(args.query || ''), args)
const missing = missingSpcParams(params)
if (missing.length) {
return {
html: '<h4>SPC 查询条件不足</h4>' + table([
{ key: 'field', label: '缺少条件' },
{ key: 'example', label: '示例' }
], missing.map(field => ({ field, example: field + ':请在问题中明确指定' }))),
charts: [],
data: { missing, params }
}
}
const chartMap = {
basicTrend: { endpoint: 'QualityData_TrendPictureBasic.ashx', type: 'GetBaseTrendData', title: '基本趋势图' },
sampleTrend: { endpoint: 'QualityData_TrendPicture.ashx', type: 'GetBaseTrendData', title: '样本趋势图' },
histogram: { endpoint: 'QualityData_Histogram.ashx', type: 'GetDataTableByConditent', title: '直方图' },
normalDistribution: { endpoint: 'QualityData_NormalDistribution.ashx', type: 'GetBaseTrendData', title: '正态分布图' },
processCapability: { endpoint: 'QualityData_NormalDistribution.ashx', type: 'GetBaseTrendData', title: '过程能力分析' },
pareto: { endpoint: 'QualityData_Pareto.ashx', type: 'GetChart', title: '排列图' },
xr: { endpoint: 'QualityData_XR.ashx', type: 'GetBaseTrendData', title: 'X-R 控制图' },
xs: { endpoint: 'QualityData_XS.ashx', type: 'GetBaseTrendData', title: 'X-S 控制图' }
}
const meta = chartMap[chartType] || chartMap.basicTrend
const url = buildUrl(meta.endpoint, meta.type, params)
let raw
try {
raw = await getJson(url)
} catch (error) {
return {
html: '<h4>SPC 接口调用失败</h4>' + table([
{ key: 'name', label: '项目' },
{ key: 'value', label: '内容' }
], [
{ name: '错误', value: error.message },
{ name: '接口', value: url }
]),
charts: [],
data: { error: error.message, params, url }
}
}
const rows = Array.isArray(raw) ? raw : (raw && Array.isArray(raw.rows) ? raw.rows : [])
const values = numericValuesFromRows(rows)
const charts = values.length ? [
/histogram|normalDistribution|processCapability|pareto/.test(chartType) ? simpleBarChart(meta.title, values) : simpleLineChart(meta.title, values)
] : []
const conditionRows = [
{ name: '图表类型', value: meta.title },
{ name: '开始时间', value: params.startTime },
{ name: '结束时间', value: params.endTime },
{ name: '工位', value: params.opName },
{ name: '测量位置', value: params.measureName },
{ name: '测量项目', value: params.measureContent },
{ name: '样本容量', value: params.sampleSize },
{ name: '样本个数', value: params.sampleNumber }
]
const summaryRows = summaryForValues(values)
const sourceRows = [{ endpoint: meta.endpoint, type: meta.type, url }]
let html = '<h4>SPC 查询条件</h4>' + table([{ key: 'name', label: '条件' }, { key: 'value', label: '值' }], conditionRows)
html += '<h4>SPC 统计摘要</h4>' + table([{ key: 'name', label: '指标' }, { key: 'value', label: '值' }], summaryRows)
html += htmlForRows('明细数据', rows, 20)
html += '<h4>数据来源</h4>' + table([{ key: 'endpoint', label: '接口' }, { key: 'type', label: '类型' }, { key: 'url', label: '调用地址' }], sourceRows)
if (!rows.length && typeof raw === 'string') {
html += '<h4>原始返回</h4><pre>' + escapeHtml(raw.slice(0, 1000)) + '</pre>'
}
return { html, charts, data: { params, raw, rows, values, url } }
}
function postJson(url, payload, headers) {
return new Promise((resolve, reject) => {
const target = new URL(url)
const client = target.protocol === 'https:' ? https : http
const body = typeof payload === 'string' ? payload : JSON.stringify(payload)
const bodyBuffer = Buffer.from(body, 'utf8')
const req = client.request({
method: 'POST',
hostname: target.hostname,
port: target.port || (target.protocol === 'https:' ? 443 : 80),
path: target.pathname + target.search,
headers: Object.assign({ 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': bodyBuffer.length }, headers || {})
}, res => {
let text = ''
res.setEncoding('utf8')
res.on('data', chunk => { text += chunk })
res.on('end', () => {
try {
resolve(JSON.parse(text))
} catch (error) {
resolve({ raw: text, statusCode: res.statusCode })
}
})
})
req.on('error', reject)
req.write(bodyBuffer)
req.end()
})
}
function isQualityDataQuestion(question) {
const text = String(question || '')
if (isFeatureQuestion(text)) return false
if (/质量数据/.test(text) && /(查|查询|查找|获取|显示|展示|列出|返回|最近|最新|明细|数据)/.test(text)) return true
return /(查询质量数据|质量数据查询|发动机质量数据|工件质量数据|测量值查询)/i.test(text) ||
/(查数据|查询数据|明细数据|实际数据|返回数据|最近\s*\d+\s*天.*数据|按.*查询.*数据|统计数据)/i.test(text) ||
/(查询|查看|统计|列出).*(质量|Andon|安灯|巡检|条码|测量|工位).*(数据|明细|记录|最近|今天|昨天)/i.test(text) ||
/ANDON_ANDON信息_备份_质量Andon综合查询/i.test(text)
}
function qualityDataQueryProfiles() {
return [QUALITY_DATA_QUERY_PROFILE]
}
function findStructuredQualityDataProfile(args) {
const queryText = [args.query, args.module, args.nameOrSql].filter(Boolean).join(' ')
const exactName = String(args.nameOrSql || '').trim()
return qualityDataQueryProfiles()
.map(profile => {
let score = 0
if (exactName && profile.nameOrSql === exactName) score += 1000
const text = [profile.title, profile.routePath, profile.view, profile.nameOrSql, profile.description, profile.defaultUseWhen, profile.keywords].join(' ')
score += scoreText(text, queryText)
if (/(查询质量数据|质量数据查询|发动机质量数据|工件质量数据|测量值查询)/i.test(queryText)) score += 300
return Object.assign({ score }, profile)
})
.filter(profile => profile.score > 0)
.sort((a, b) => b.score - a.score)[0]
}
function createDataQueries() {
return (knowledgeBase.modules || []).flatMap(module => {
return (module.primaryQueries || [])
.filter(query => query && query.kind === 'create-data' && query.nameOrSql)
.map(query => Object.assign({
moduleTitle: module.title,
routePath: module.routePath,
moduleName: module.module,
qualityRelated: module.qualityRelated
}, query))
})
}
function findQualityDataQuery(args) {
const exactName = String(args.nameOrSql || '').trim()
const structured = exactName && exactName !== QUALITY_DATA_QUERY_PROFILE.nameOrSql
? null
: QUALITY_DATA_QUERY_PROFILE
if (structured) {
return Object.assign({
view: structured.view,
moduleTitle: structured.title,
routePath: structured.routePath,
moduleName: structured.module,
queryTables: [],
backendQueryName: structured.nameOrSql,
structuredProfile: structured
}, structured)
}
const queryText = [args.query, args.module, args.nameOrSql].filter(Boolean).join(' ')
const rows = createDataQueries().filter(row => {
if (exactName) return row.nameOrSql === exactName
const text = [row.moduleTitle, row.routePath, row.view, row.domain, row.nameOrSql].join(' ')
return row.qualityRelated || scoreText(text, queryText) > 0
})
return rows
.map(row => {
const text = [row.moduleTitle, row.routePath, row.view, row.domain, row.nameOrSql].join(' ')
let score = exactName && row.nameOrSql === exactName ? 1000 : scoreText(text, queryText)
if (/质量.*Andon|Andon.*质量|ANDON_ANDON信息/i.test(queryText) && /ANDON_ANDON信息_备份_质量Andon综合查询_分页/i.test(row.nameOrSql)) score += 300
if (String(row.type) === '11') score += 30
if (String(row.type) === '2001' || /导出|下载|excel/i.test(row.nameOrSql)) score -= 80
return Object.assign({ score }, row)
})
.filter(row => row.score > 0)
.sort((a, b) => b.score - a.score)[0]
}
function normalizeMesParams(params) {
if (Array.isArray(params)) {
return params.map(item => {
if (Array.isArray(item)) return { name: item[0], value: item[1], type: item[2], output: item[3] }
return item
}).filter(item => item && item.name)
}
if (params && typeof params === 'object') {
return Object.keys(params).map(key => ({ name: key, value: params[key] }))
}
return []
}
function buildQualityAndonParams(args) {
const range = Object.assign(parseDateRange(args.query || ''), {
startTime: args.startTime,
endTime: args.endTime
})
const stationNumber = args.stationNumber || args.opName || parseQuotedOrNamed(args.query || '', ['工位号', '工位', '工位名称'])
const pageCurrent = Math.max(Number(args.pageCurrent || 1), 1)
const pageSize = Math.min(Math.max(Number(args.pageSize || args.limit || 30), 1), 100)
return [
{ name: '开始时间', value: range.startTime },
{ name: '结束时间', value: range.endTime },
{ name: '工位号_ischeck', value: stationNumber ? 1 : 0 },
{ name: '工位号', value: stationNumber || '' },
{ name: 'PageCurrent', value: pageCurrent },
{ name: 'PageSize', value: pageSize },
{ name: 'PageCount', value: '1111', type: 'int', output: '1' },
{ name: 'ItemCount', value: '1111', type: 'int', output: '1' }
]
}
function withDateTime(value, endOfDay) {
if (!value) return ''
const text = String(value)
if (/\d{1,2}:\d{2}/.test(text)) return text
return text + (endOfDay ? ' 23:59:59' : ' 00:00:00')
}
function normalizePassFlag(value) {
const text = String(value == null ? '' : value).trim()
if (!text || text === '全部') return '0'
if (/不合格|NG|false/i.test(text)) return '2'
if (/合格|OK|true/i.test(text)) return '1'
return text
}
function parsePassFlagFromQuery(query) {
const text = String(query || '')
if (/不合格|NG/i.test(text)) return '2'
if (/全部/.test(text)) return '0'
if (/合格|OK/i.test(text)) return '1'
return ''
}
function pickArg(args, names) {
for (let i = 0; i < names.length; i++) {
const value = args[names[i]]
if (value != null && value !== '') return value
}
return ''
}
function hasExplicitDateRange(args) {
const text = String(args.query || '')
return Boolean(args.startTime || args.endTime || /(\d{4}[-/]\d{1,2}[-/]\d{1,2})|今天|昨日|昨天|最近\s*\d+\s*天/.test(text))
}
function buildStructuredQualityDataParams(args, profile) {
const range = hasExplicitDateRange(args)
? Object.assign(parseDateRange(args.query || ''), { startTime: args.startTime, endTime: args.endTime })
: { startTime: '1900-01-01', endTime: '2099-12-31' }
const values = {
startTime: withDateTime(range.startTime, false),
endTime: withDateTime(range.endTime, true),
stationNumber: pickArg(args, ['stationNumber', 'opName']) || parseQuotedOrNamed(args.query || '', ['工位号', '工位']),
engineId: pickArg(args, ['engineId', 'workpieceNo']) || parseQuotedOrNamed(args.query || '', ['工件编号', '工件号', '发动机号', '总成号']),
orderCode: pickArg(args, ['orderCode']) || parseQuotedOrNamed(args.query || '', ['订货号']),
orderCodeId: pickArg(args, ['orderCodeId']) || parseQuotedOrNamed(args.query || '', ['订货号ID']),
engineType: pickArg(args, ['engineType', 'model']) || parseQuotedOrNamed(args.query || '', ['机型号', '机型', '型号']),
workOrderId: pickArg(args, ['workOrderId']) || parseQuotedOrNamed(args.query || '', ['工单ID']),
workOrderCode: pickArg(args, ['workOrderCode']) || parseQuotedOrNamed(args.query || '', ['工单号']),
isOk: normalizePassFlag(pickArg(args, ['isOk', 'qualified', 'passFlag']) || parseQuotedOrNamed(args.query || '', ['是否合格', '合格标志']) || parsePassFlagFromQuery(args.query || '')),
testPosition: pickArg(args, ['testPosition', 'measurePosition', 'measureName']) || parseQuotedOrNamed(args.query || '', ['测量位置', '位置']),
testItem: pickArg(args, ['testItem', 'measureItem', 'measureContent']) || parseQuotedOrNamed(args.query || '', ['测量项目', '测量内容', '项目']),
pageCurrent: Math.max(Number(args.pageCurrent || 1), 1),
pageSize: Math.min(Math.max(Number(args.pageSize || args.limit || 50), 1), 50)
}
return (profile.parameters || []).map(param => {
if (param.output) return { name: param.name, value: param.defaultValue, type: param.type, output: param.output }
if (param.checkFor) {
const value = values[param.arg]
return { name: param.name, value: value ? 1 : 0 }
}
return { name: param.name, value: values[param.arg] != null && values[param.arg] !== '' ? values[param.arg] : (param.defaultValue == null ? '' : param.defaultValue) }
})
}
function buildMesPayload(query, args) {
const providedParams = normalizeMesParams(args.params)
const params = providedParams.length
? providedParams
: (query.structuredProfile ? buildStructuredQualityDataParams(args, query.structuredProfile)
: (/ANDON_ANDON信息_备份_质量Andon综合查询/i.test(query.nameOrSql) ? buildQualityAndonParams(args) : []))
return {
type: String(args.type || query.type || '11'),
name: query.nameOrSql,
param: JSON.stringify(params),
pageSize: args.pageSize,
pageList: args.pageList,
UserID: args.userId || 'AI',
ModularID: query.routePath || query.view || '/AI'
}
}
function parsePossibleJson(value) {
if (typeof value !== 'string') return value
try {
return JSON.parse(value)
} catch (error) {
return value
}
}
function unwrapMesResponse(response) {
let body = response
if (body && typeof body === 'object' && typeof body.raw === 'string') body = parsePossibleJson(body.raw)
if (typeof body === 'string') body = parsePossibleJson(body)
if (body && typeof body === 'object' && typeof body.d === 'string') body = parsePossibleJson(body.d)
if (body && typeof body === 'object' && typeof body.data === 'string') body.data = parsePossibleJson(body.data)
const data = body && body.data && typeof body.data === 'object' ? body.data : body
const rows = Array.isArray(data) ? data
: (data && Array.isArray(data.result)) ? data.result
: (data && Array.isArray(data.rows)) ? data.rows
: (data && Array.isArray(data.data)) ? data.data
: []
return {
body,
data,
rows,
output: data && Array.isArray(data.output) ? data.output : []
}
}
function parseDateValue(value) {
if (value == null || value === '') return 0
const text = String(value).replace(/\//g, '-')
const time = Date.parse(text)
return Number.isNaN(time) ? 0 : time
}
function isQualityChartQuestion(question) {
const text = String(question || '')
return isQualityDataQuestion(text) && qualityChartTypes(text).length > 0
}
function qualityChartTypes(question) {
const text = String(question || '')
const types = []
const has = words => words.some(word => text.toLowerCase().indexOf(word.toLowerCase()) > -1)
if (has(['\u6837\u672c\u8d8b\u52bf\u56fe'])) types.push('sampleTrend')
if (has(['\u76f4\u65b9\u56fe'])) types.push('histogram')
if (has(['\u5de5\u5e8f\u80fd\u529b\u5206\u6790\u56fe'])) types.push('processCapability')
if (has(['\u6392\u5217\u56fe', 'Pareto'])) types.push('pareto')
if (has(['\u5747\u503c\u6781\u5dee\u56fe'])) types.push('xr')
if (has(['\u5747\u503c\u6807\u51c6\u5dee\u56fe'])) types.push('xs')
if (has(['SPC\u57fa\u672c\u8d8b\u52bf\u56fe', '\u57fa\u672c\u8d8b\u52bf\u56fe'])) types.push('basicTrend')
return unique(types)
}
function numericQualityValue(row) {
const value = firstValue(row, ['测量值', '实际值', 'value', 'Value', '数值'])
const number = Number(value)
return Number.isNaN(number) ? NaN : number
}
function constantLineValue(rows, keys) {
for (let i = 0; i < rows.length; i++) {
const value = firstValue(rows[i], keys)
const number = Number(value)
if (!Number.isNaN(number)) return number
}
return NaN
}
function uniqueRowValues(rows, keys) {
return unique((rows || []).map(row => firstValue(row, keys)).filter(Boolean))
}
function qualityChartPoints(rows) {
const sortedRows = normalizeRows(rows, QUALITY_DATA_DEFAULT_LIMIT).slice().sort((a, b) => {
const diff = parseDateValue(firstValue(a, ['生产日期', '操作时间', '完成时间', '到达时间', '离开时间', '时间'])) -
parseDateValue(firstValue(b, ['生产日期', '操作时间', '完成时间', '到达时间', '离开时间', '时间']))
return diff || 0
})
return sortedRows.map((row, index) => ({
row,
value: numericQualityValue(row),
label: firstValue(row, ['生产日期', '操作时间', '完成时间', '到达时间', '离开时间', '时间']) || String(index + 1)
})).filter(point => !Number.isNaN(point.value))
}
function qualityChartTitle(baseTitle, points) {
const rows = points.map(point => point.row)
const stationNames = uniqueRowValues(rows, ['工序名称', '工位名称', '工位号', '指南工位号'])
const positions = uniqueRowValues(rows, ['测量位置', '位置'])
return baseTitle + ' - ' +
(stationNames.length === 1 ? stationNames[0] : '多工位') +
' / ' +
(positions.length === 1 ? positions[0] : '多测量位置')
}
function mixedMeasurePosition(points) {
return uniqueRowValues(points.map(point => point.row), ['测量位置', '位置']).length > 1
}
function qualitySpecMarkLines(points) {
const rows = points.map(point => point.row)
const markLineData = []
const upper = constantLineValue(rows, ['上限值', '上限'])
const lower = constantLineValue(rows, ['下限值', '下限'])
const theory = constantLineValue(rows, ['理论值', '目标值'])
if (!Number.isNaN(upper)) markLineData.push({ name: '上限值', yAxis: upper })
if (!Number.isNaN(lower)) markLineData.push({ name: '下限值', yAxis: lower })
if (!Number.isNaN(theory)) markLineData.push({ name: '理论值', yAxis: theory })
return markLineData
}
function polishChart(chart, palette) {
if (!chart || !chart.option) return chart
const colors = palette || ['#2563eb', '#16a34a', '#f97316', '#dc2626', '#7c3aed']
chart.option.color = colors
chart.option.backgroundColor = '#ffffff'
chart.option.title = Object.assign({
left: 'center',
top: 10,
textStyle: { color: '#1f2937', fontSize: 14, fontWeight: 600 }
}, chart.option.title || {})
chart.option.tooltip = Object.assign({
trigger: 'axis',
backgroundColor: 'rgba(17, 24, 39, 0.9)',
borderWidth: 0,
textStyle: { color: '#fff', fontSize: 12 },
axisPointer: { type: 'line', lineStyle: { color: '#94a3b8', width: 1, type: 'dashed' } }
}, chart.option.tooltip || {})
chart.option.grid = Object.assign({ left: 54, right: 28, top: 72, bottom: 48, containLabel: true }, chart.option.grid || {})
const axisStyle = {
axisLine: { lineStyle: { color: '#cbd5e1' } },
axisTick: { lineStyle: { color: '#cbd5e1' } },
axisLabel: { color: '#475569', fontSize: 11 },
splitLine: { lineStyle: { color: '#eef2f7', type: 'dashed' } }
}
if (Array.isArray(chart.option.xAxis)) {
chart.option.xAxis = chart.option.xAxis.map(axis => Object.assign({}, axisStyle, axis))
} else {
chart.option.xAxis = Object.assign({}, axisStyle, chart.option.xAxis || {})
}
if (Array.isArray(chart.option.yAxis)) {
chart.option.yAxis = chart.option.yAxis.map(axis => Object.assign({}, axisStyle, axis))
} else {
chart.option.yAxis = Object.assign({}, axisStyle, chart.option.yAxis || {})
}
chart.option.legend = Object.assign({
top: 36,
textStyle: { color: '#475569', fontSize: 11 },
itemWidth: 12,
itemHeight: 8
}, chart.option.legend || {})
if (Array.isArray(chart.option.series)) {
chart.option.series = chart.option.series.map((series, index) => {
const next = Object.assign({
symbol: series.type === 'line' ? 'circle' : undefined,
symbolSize: series.type === 'line' ? 5 : undefined,
lineStyle: series.type === 'line' ? { width: 2 } : undefined,
itemStyle: { borderRadius: series.type === 'bar' ? [4, 4, 0, 0] : 0 }
}, series)
if (next.markLine) {
next.markLine = Object.assign({
symbol: 'none',
label: { color: '#64748b', fontSize: 11 },
lineStyle: { color: colors[(index + 2) % colors.length], width: 1.5, type: 'dashed' }
}, next.markLine)
}
return next
})
}
return chart
}
function lineQualityChart(type, title, points, data, seriesName) {
const markLineData = qualitySpecMarkLines(points)
return polishChart({
id: 'chart_' + type + '_' + Date.now() + '_' + Math.floor(Math.random() * 10000),
title,
mixedMeasurePosition: mixedMeasurePosition(points),
option: {
title: { text: title, left: 'center' },
tooltip: { trigger: 'axis' },
grid: { left: 50, right: 24, top: 62, bottom: 48 },
xAxis: { type: 'category', data: points.map(point => point.label), axisLabel: { rotate: 35 } },
yAxis: { type: 'value', scale: true },
series: [{ name: seriesName || '测量值', type: 'line', smooth: true, data, markLine: markLineData.length ? { symbol: 'none', data: markLineData } : undefined }]
}
})
}
function histogramQualityChart(points) {
const values = points.map(point => point.value)
const min = Math.min.apply(null, values)
const max = Math.max.apply(null, values)
const binCount = Math.min(10, Math.max(4, Math.ceil(Math.sqrt(values.length))))
const width = max === min ? 1 : (max - min) / binCount
const bins = Array.from({ length: binCount }, (_, index) => ({ label: (min + index * width).toFixed(2) + '-' + (min + (index + 1) * width).toFixed(2), count: 0 }))
values.forEach(value => {
const index = max === min ? 0 : Math.min(Math.floor((value - min) / width), binCount - 1)
bins[index].count += 1
})
const title = qualityChartTitle('直方图', points)
return polishChart({ id: 'chart_histogram_' + Date.now() + '_' + Math.floor(Math.random() * 10000), title, mixedMeasurePosition: mixedMeasurePosition(points), option: { title: { text: title, left: 'center' }, tooltip: { trigger: 'axis' }, grid: { left: 48, right: 24, top: 62, bottom: 58 }, xAxis: { type: 'category', data: bins.map(bin => bin.label), axisLabel: { rotate: 35 } }, yAxis: { type: 'value' }, series: [{ name: '频数', type: 'bar', data: bins.map(bin => bin.count) }] } }, ['#2563eb'])
}
function processCapabilityQualityChart(points) {
const values = points.map(point => point.value)
const summary = summaryForValues(values)
const upper = constantLineValue(points.map(point => point.row), ['上限值', '上限'])
const lower = constantLineValue(points.map(point => point.row), ['下限值', '下限'])
const avg = values.reduce((acc, item) => acc + item, 0) / values.length
const variance = values.reduce((acc, item) => acc + Math.pow(item - avg, 2), 0) / values.length
const sigma = Math.sqrt(variance)
const cp = !Number.isNaN(upper) && !Number.isNaN(lower) && sigma > 0 ? (upper - lower) / (6 * sigma) : NaN
const cpk = !Number.isNaN(upper) && !Number.isNaN(lower) && sigma > 0 ? Math.min((upper - avg) / (3 * sigma), (avg - lower) / (3 * sigma)) : NaN
const title = qualityChartTitle('工序能力分析图', points)
return polishChart({ id: 'chart_processCapability_' + Date.now() + '_' + Math.floor(Math.random() * 10000), title, mixedMeasurePosition: mixedMeasurePosition(points), option: { title: { text: title, left: 'center' }, tooltip: { trigger: 'axis' }, grid: { left: 60, right: 24, top: 72, bottom: 42 }, xAxis: { type: 'category', data: summary.map(item => item.name).concat(['CP', 'CPK']) }, yAxis: { type: 'value', scale: true }, series: [{ name: '能力指标', type: 'bar', data: summary.map(item => Number(item.value)).concat([Number.isNaN(cp) ? 0 : Number(cp.toFixed(4)), Number.isNaN(cpk) ? 0 : Number(cpk.toFixed(4))]) }] } }, ['#16a34a'])
}
function paretoQualityChart(points) {
const counts = {}
points.forEach(point => {
const key = firstValue(point.row, ['合格标志', '是否合格', '测量项目', '测量位置']) || '数据项'
counts[key] = (counts[key] || 0) + 1
})
const items = Object.keys(counts).map(name => ({ name, count: counts[name] })).sort((a, b) => b.count - a.count)
const total = items.reduce((acc, item) => acc + item.count, 0) || 1
let cumulative = 0
const line = items.map(item => { cumulative += item.count; return Number((cumulative / total * 100).toFixed(2)) })
const title = qualityChartTitle('排列图', points)
return polishChart({ id: 'chart_pareto_' + Date.now() + '_' + Math.floor(Math.random() * 10000), title, mixedMeasurePosition: mixedMeasurePosition(points), option: { title: { text: title, left: 'center' }, tooltip: { trigger: 'axis' }, legend: { top: 30 }, grid: { left: 48, right: 48, top: 76, bottom: 50 }, xAxis: { type: 'category', data: items.map(item => item.name), axisLabel: { rotate: 25 } }, yAxis: [{ type: 'value' }, { type: 'value', min: 0, max: 100, axisLabel: { formatter: '{value}%' } }], series: [{ name: '数量', type: 'bar', data: items.map(item => item.count) }, { name: '累计占比', type: 'line', yAxisIndex: 1, data: line }] } }, ['#2563eb', '#f97316'])
}
function groupedValues(values, size, mapper) {
const result = []
for (let i = 0; i < values.length; i += size) {
const group = values.slice(i, i + size)
if (group.length) result.push(mapper(group))
}
return result
}
function controlQualityChart(type, points) {
const values = points.map(point => point.value)
const size = 5
const labels = groupedValues(values, size, (_, index) => index).map((_, index) => '组' + (index + 1))
const averages = groupedValues(values, size, group => group.reduce((acc, item) => acc + item, 0) / group.length)
const secondary = groupedValues(values, size, group => {
if (type === 'xr') return Math.max.apply(null, group) - Math.min.apply(null, group)
const avg = group.reduce((acc, item) => acc + item, 0) / group.length
return Math.sqrt(group.reduce((acc, item) => acc + Math.pow(item - avg, 2), 0) / group.length)
})
const title = qualityChartTitle(type === 'xr' ? '均值极差图' : '均值标准差图', points)
return polishChart({ id: 'chart_' + type + '_' + Date.now() + '_' + Math.floor(Math.random() * 10000), title, mixedMeasurePosition: mixedMeasurePosition(points), option: { title: { text: title, left: 'center' }, tooltip: { trigger: 'axis' }, legend: { top: 30 }, grid: { left: 50, right: 44, top: 76, bottom: 42 }, xAxis: { type: 'category', data: labels }, yAxis: [{ type: 'value', scale: true }, { type: 'value', scale: true }], series: [{ name: '均值', type: 'line', smooth: true, data: averages }, { name: type === 'xr' ? '极差' : '标准差', type: 'line', smooth: true, yAxisIndex: 1, data: secondary }] } }, ['#2563eb', '#dc2626'])
}
function qualityDataCharts(rows, question) {
const points = qualityChartPoints(rows)
if (!points.length) return []
return qualityChartTypes(question).map(type => {
if (type === 'histogram') return histogramQualityChart(points)
if (type === 'processCapability') return processCapabilityQualityChart(points)
if (type === 'pareto') return paretoQualityChart(points)
if (type === 'xr') return controlQualityChart('xr', points)
if (type === 'xs') return controlQualityChart('xs', points)
if (type === 'sampleTrend') return lineQualityChart('sampleTrend', qualityChartTitle('样本趋势图', points), points, points.map(point => point.value), '测量值')
return lineQualityChart('basicTrend', qualityChartTitle('SPC基本趋势图', points), points, points.map(point => point.value), '测量值')
}).filter(Boolean)
}
function latestQualityRows(rows, profile, limit) {
const source = Array.isArray(rows) ? rows.slice() : []
const maxRows = Math.min(Math.max(Number(limit || QUALITY_DATA_DEFAULT_LIMIT), 1), QUALITY_DATA_DEFAULT_LIMIT)
const fields = (profile && profile.latestSortFields && profile.latestSortFields.length)
? profile.latestSortFields
: ['生产日期', '操作时间', '完成时间', '到达时间', '离开时间', '时间']
const sortField = fields.find(field => source.some(row => row && row[field]))
if (sortField) {
source.sort((a, b) => parseDateValue(b && b[sortField]) - parseDateValue(a && a[sortField]))
}
return source.slice(0, maxRows)
}
async function queryQualityData(args) {
const queryLimit = Math.min(Math.max(Number((args && (args.limit || args.pageSize)) || QUALITY_DATA_DEFAULT_LIMIT), 1), QUALITY_DATA_DEFAULT_LIMIT)
const selected = findQualityDataQuery(args || {})
if (!selected) {
const rows = pick(knowledgeBase.qualityModules, args.query || '', ['Title', 'View', 'Domains', 'QueryNames'], 10)
return {
html: '<h4>未定位到质量查询入口</h4>' + table([
{ key: 'Title', label: '页面' },
{ key: 'RoutePath', label: '路由' },
{ key: 'QueryNames', label: '可用查询入口' }
], rows),
data: { matched: false, candidates: rows }
}
}
const payload = buildMesPayload(selected, args || {})
const sourceRows = [{
module: selected.moduleTitle,
routePath: selected.routePath,
type: payload.type,
name: payload.name,
note: 'MESCommonBase 查询入口,复用页面已有查询能力'
}]
let response
try {
response = await postJson(MES_BACKEND_URL, JSON.stringify(payload))
} catch (error) {
return {
html: '<h4>质量数据查询入口已定位</h4>' + table([
{ key: 'module', label: '页面' },
{ key: 'routePath', label: '路由' },
{ key: 'type', label: '类型' },
{ key: 'name', label: '查询入口' },
{ key: 'note', label: '说明' }
], sourceRows) + '<h4>接口调用失败</h4>' + table([
{ key: 'name', label: '项目' },
{ key: 'value', label: '内容' }
], [
{ name: '错误', value: error.message },
{ name: '接口', value: MES_BACKEND_URL }
]),
data: { matched: true, error: error.message, payload, query: selected }
}
}
const parsed = unwrapMesResponse(response)
const displayRows = selected.structuredProfile
? latestQualityRows(parsed.rows, selected.structuredProfile, queryLimit)
: normalizeRows(parsed.rows, queryLimit)
const paramRows = parsePossibleJson(payload.param)
let html = '<h4>质量数据查询入口</h4>' + table([
{ key: 'module', label: '页面' },
{ key: 'routePath', label: '路由' },
{ key: 'type', label: '类型' },
{ key: 'name', label: '查询入口' },
{ key: 'note', label: '说明' }
], sourceRows)
html += '<h4>查询条件</h4>' + table([
{ key: 'name', label: '参数' },
{ key: 'value', label: '值' },
{ key: 'output', label: '输出参数' }
], paramRows)
html += htmlForQualityDetailRows(displayRows)
if (parsed.output.length) html += '<h4>输出参数</h4>' + htmlForRows('分页信息', parsed.output, 5)
if (!parsed.rows.length && typeof (response && response.raw) === 'string') {
html += '<h4>原始返回</h4><pre>' + escapeHtml(response.raw.slice(0, 1000)) + '</pre>'
}
return { html, data: { matched: true, query: selected, payload, response, rows: displayRows, rawRows: parsed.rows, output: parsed.output } }
}
async function askDeepSeek(question, toolHtml, context, options) {
const fallback = toolHtml || '<p>DeepSeek 未配置或暂不可用。</p>'
if (!DEEPSEEK_API_KEY) return fallback
const opts = options || {}
const mergedContext = opts.useProjectContext
? Object.assign({
matchedModules: routeCandidates(question, 8),
qualityModules: pick(knowledgeBase.qualityModules, question, ['Title', 'View', 'Domains', 'QueryNames'], 8),
moduleFeatures: moduleFeatureCandidates(question, 8),
qualityDataQueries: qualityDataQueryProfiles(),
spcCharts: pick(knowledgeBase.spcCharts, question, ['chartType', 'title', 'apiFile', 'endpoint', 'functions'], 8)
}, context || {})
: (context || {})
const system = [
'你是 WC-SPC MES-Manager_View 网页内置的 DeepSeekV4Pro AI 助手,回答以 DeepSeek 推理和表达为主。',
'使用中文回答,优先输出规范 HTML 表格;必要时可补充简短段落。',
'如果提供了工具调用结果,必须以工具结果为事实依据组织回答,不要编造工具结果中没有的数据。',
'如果没有工具调用结果,就直接回答用户问题;涉及项目实时数据时,说明需要调用项目已有 HMI 查询入口。',
'数据查询必须依赖项目人机界面中已有的函数、CreateData 查询入口、MESCommonBase.ashx 或已有 SPC 接口;禁止建议或执行数据库直连,回答中不要出现数据库直连方案。',
'当上下文包含页面功能画像、质量数据查询画像或查询结果时,要说明如何通过页面已有查询入口完成查询,并用 HTML 表格组织结果。',
'不要输出可执行 JavaScript图表由系统结构化渲染。'
].join('\n')
const messages = [{ role: 'system', content: system }]
if (Object.keys(mergedContext).length) {
messages.push({ role: 'user', content: '工具/项目上下文:\n' + JSON.stringify(mergedContext, null, 2) })
}
if (toolHtml) messages.push({ role: 'user', content: '工具调用结果 HTML\n' + toolHtml })
messages.push({ role: 'user', content: question })
try {
const response = await postJson(DEEPSEEK_BASE_URL + '/chat/completions', {
model: DEEPSEEK_MODEL,
messages,
temperature: 0.2
}, { Authorization: 'Bearer ' + DEEPSEEK_API_KEY })
return response && response.choices && response.choices[0] && response.choices[0].message
? response.choices[0].message.content
: fallback
} catch (error) {
return fallback + '<p>DeepSeek 调用失败:' + escapeHtml(error.message) + '</p>'
}
}
function localAnswer(question) {
if (isQualityDataQuestion(question)) {
const profile = findStructuredQualityDataProfile({ query: question }) || qualityDataQueryProfiles()[0]
if (profile) return describeQualityDataQuery(profile, { query: question })
}
if (isFeatureQuestion(question)) return describeModuleFeatures(question, 8)
if (/项目|功能|模块|有哪些/.test(question)) {
return '<h4>项目功能概览</h4>' + table([
{ key: 'name', label: '项目' },
{ key: 'value', label: '数量' }
], [
{ name: '页面模块', value: knowledgeBase.modules.length },
{ name: 'API 映射', value: knowledgeBase.apiMappings.length },
{ name: '质量相关模块', value: knowledgeBase.qualityModules.length },
{ name: '质量数据查询画像', value: qualityDataQueryProfiles().length },
{ name: '页面功能画像', value: moduleFeatureRows().length },
{ name: 'SPC 图表类型', value: knowledgeBase.spcCharts.length }
]) + '<h4>质量相关模块</h4>' + table([
{ key: 'title', label: '页面' },
{ key: 'routePath', label: '路由' },
{ key: 'domains', label: '业务域' }
], listQualityModules(12))
}
if (isQualityQuestion(question)) return qualitySearchResponse(question)
const rows = routeCandidates(question, 12)
return '<h4>项目知识库匹配结果</h4>' + table([
{ key: 'title', label: '页面' },
{ key: 'routePath', label: '路由' },
{ key: 'view', label: '源码文件' },
{ key: 'callCount', label: '调用数' }
], rows)
}
function buildAssistantContext(question) {
return {
matchedModules: routeCandidates(question, 8),
moduleFeatures: moduleFeatureCandidates(question, 8),
qualityModules: pick(knowledgeBase.qualityModules, question, ['Title', 'View', 'Domains', 'QueryNames'], 8),
qualityDataQueries: qualityDataQueryProfiles(),
spcCharts: pick(knowledgeBase.spcCharts, question, ['chartType', 'title', 'apiFile', 'endpoint', 'functions'], 8)
}
}
function localResponse(question) {
if (isQualityDataQuestion(question)) {
const profile = findStructuredQualityDataProfile({ query: question }) || qualityDataQueryProfiles()[0]
if (profile) return describeQualityDataQuery(profile, { query: question })
}
if (isFeatureQuestion(question)) return describeModuleFeatures(question, 8)
if (isQualityQuestion(question)) return qualitySearchResponse(question)
return localAnswer(question)
}
const tools = [
{ name: 'project_summary', description: '返回 WC-SPC 项目结构、知识库、质量模块和 SPC 图表概览。', inputSchema: { type: 'object', properties: {} } },
{ name: 'search_project_functions', description: '按关键字搜索项目页面、接口和功能。', inputSchema: { type: 'object', properties: { query: { type: 'string' }, limit: { type: 'number' }}, required: ['query'] } },
{ name: 'describe_module_features', description: '按模块名、页面名、路由或业务关键词返回页面功能画像,说明筛选条件、按钮、表格字段和后端入口,不调用数据库。', inputSchema: { type: 'object', properties: { query: { type: 'string' }, limit: { type: 'number' }}, required: ['query'] } },
{ name: 'search_module_features', description: '搜索所有页面功能画像,返回匹配页面功能摘要,不调用数据库。', inputSchema: { type: 'object', properties: { query: { type: 'string' }, limit: { type: 'number' }}, required: ['query'] } },
{ name: 'list_quality_modules', description: '列出质量管理相关页面。', inputSchema: { type: 'object', properties: { limit: { type: 'number' } } } },
{ name: 'search_quality_functions', description: '搜索质量管理相关页面和查询入口。', inputSchema: { type: 'object', properties: { query: { type: 'string' }, limit: { type: 'number' }}, required: ['query'] } },
{ name: 'describe_quality_data_query', description: '按查询质量数据相关问题返回页面已有查询入口、参数映射和结果字段,不调用数据库。', inputSchema: { type: 'object', properties: { query: { type: 'string' }, limit: { type: 'number' }}, required: ['query'] } },
{ name: 'query_quality_data', description: '按项目 CreateData 查询入口调用质量管理数据,返回 HTML 表格结果。', inputSchema: { type: 'object', properties: { query: { type: 'string' }, nameOrSql: { type: 'string' }, params: { type: 'object' }, startTime: { type: 'string' }, endTime: { type: 'string' }, stationNumber: { type: 'string' }, pageCurrent: { type: 'number' }, pageSize: { type: 'number' }, limit: { type: 'number' } } } },
{ name: 'list_spc_charts', description: '列出 SPC 图表类型与对应接口。', inputSchema: { type: 'object', properties: {} } },
{ name: 'spc_query_chart', description: '根据条件调用 SPC 接口并返回图表、摘要和明细。', inputSchema: { type: 'object', properties: { query: { type: 'string' }, chartType: { type: 'string' }, opName: { type: 'string' }, measureName: { type: 'string' }, measureContent: { type: 'string' }, startTime: { type: 'string' }, endTime: { type: 'string' } } } },
{ name: 'reload_project_index', description: '重新加载知识库。', inputSchema: { type: 'object', properties: {} } },
{ name: 'call_mes_backend', description: '调用 MESCommonBase.ashx默认禁用。', inputSchema: { type: 'object', properties: { payload: { type: 'object' }}, required: ['payload'] } }
]
async function toolResult(name, args) {
const limit = Math.min(Number(args.limit || 30), 100)
if (name === 'project_summary') {
const rows = [
{ name: '知识库生成时间', value: knowledgeBase.generatedAt || '未生成' },
{ name: '页面模块', value: knowledgeBase.modules.length },
{ name: 'API 映射', value: knowledgeBase.apiMappings.length },
{ name: '质量相关模块', value: knowledgeBase.qualityModules.length },
{ name: 'SPC 图表类型', value: knowledgeBase.spcCharts.length },
{ name: 'DeepSeek 已配置', value: Boolean(DEEPSEEK_API_KEY) },
{ name: 'SPC 后端地址', value: SPC_BACKEND_URL }
]
return { text: JSON.stringify(rows, null, 2), html: '<h4>项目概览</h4>' + table([{ key: 'name', label: '项目' }, { key: 'value', label: '值' }], rows), data: rows }
}
if (name === 'search_project_functions') {
const rows = routeCandidates(args.query, limit)
return { text: JSON.stringify(rows, null, 2), html: table([{ key: 'title', label: '页面' }, { key: 'routePath', label: '路由' }, { key: 'view', label: '源码' }, { key: 'callCount', label: '调用数' }], rows), data: rows }
}
if (name === 'describe_module_features') {
const rows = moduleFeatureCandidates(args.query, limit)
return { text: JSON.stringify(rows, null, 2), html: describeModuleFeatures(args.query, limit), data: rows }
}
if (name === 'search_module_features') {
const rows = moduleFeatureCandidates(args.query, limit)
return { text: JSON.stringify(rows, null, 2), html: searchModuleFeatures(args.query, limit), data: rows }
}
if (name === 'list_quality_modules') {
const rows = listQualityModules(limit)
return { text: JSON.stringify(rows, null, 2), html: table([{ key: 'title', label: '页面' }, { key: 'routePath', label: '路由' }, { key: 'domains', label: '业务域' }, { key: 'queryable', label: '可查询' }], rows), data: rows }
}
if (name === 'search_quality_functions') {
const rows = pick(knowledgeBase.qualityModules, args.query, ['Title', 'View', 'Domains', 'QueryNames'], limit)
return { text: JSON.stringify(rows, null, 2), html: table([{ key: 'Title', label: '页面' }, { key: 'RoutePath', label: '路由' }, { key: 'Domains', label: '业务域' }, { key: 'QueryNames', label: '查询入口' }], rows), data: rows }
}
if (name === 'describe_quality_data_query') {
const profile = findStructuredQualityDataProfile(args || {}) || qualityDataQueryProfiles()[0]
const rows = profile ? qualityDataParameterRows(profile, args) : []
const html = profile
? describeQualityDataQuery(profile, args || {})
: '<h4>质量数据查询功能</h4>' + table([{ key: 'message', label: '提示' }], [{ message: '未找到质量数据查询画像' }])
return { text: JSON.stringify(profile || {}, null, 2), html, data: { profile, rows } }
}
if (name === 'query_quality_data') {
const result = await queryQualityData(args || {})
return { text: JSON.stringify(result.data, null, 2), html: result.html, data: result.data }
}
if (name === 'list_spc_charts') {
const rows = knowledgeBase.spcCharts || []
return { text: JSON.stringify(rows, null, 2), html: table([{ key: 'title', label: '图表' }, { key: 'chartType', label: '类型' }, { key: 'apiFile', label: 'API 文件' }, { key: 'endpoint', label: '后端接口' }], rows), data: rows }
}
if (name === 'spc_query_chart') {
const result = await querySpcChart(args || {})
return { text: JSON.stringify(result.data, null, 2), html: result.html, data: result }
}
if (name === 'reload_project_index') {
knowledgeBase = readJson(KNOWLEDGE_FILE, emptyKnowledgeBase())
return { text: 'reloaded', html: table([{ key: 'name', label: '项目' }, { key: 'value', label: '值' }], [{ name: '状态', value: '知识库已重新加载' }]), data: { ok: true } }
}
if (name === 'call_mes_backend') {
if (!ALLOW_BACKEND_CALLS) {
return { text: 'disabled', html: table([{ key: 'name', label: '项目' }, { key: 'value', label: '值' }], [{ name: '状态', value: '已禁用,避免 AI 直接修改业务数据' }]), data: { disabled: true } }
}
const response = await postJson(MES_BACKEND_URL, args.payload || {})
return { text: JSON.stringify(response, null, 2), html: htmlForRows('MES 后端返回', Array.isArray(response) ? response : [response], 30), data: response }
}
throw new Error('Unknown tool: ' + name)
}
async function handleMcp(body) {
const id = body.id == null ? null : body.id
try {
if (body.method === 'initialize') {
return { jsonrpc: '2.0', id, result: { protocolVersion: '2025-06-18', serverInfo: { name: 'wc-spc-mcp', version: '1.0.0' }, capabilities: { tools: {} } } }
}
if (body.method === 'tools/list') return { jsonrpc: '2.0', id, result: { tools } }
if (body.method === 'tools/call') {
const result = await toolResult(body.params && body.params.name, (body.params && body.params.arguments) || {})
return { jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: result.text }, { type: 'text', text: result.html }], structuredContent: result.data } }
}
return { jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } }
} catch (error) {
return { jsonrpc: '2.0', id, error: { code: -32000, message: error.message } }
}
}
function parseBody(req) {
return new Promise((resolve, reject) => {
const chunks = []
req.on('data', chunk => { chunks.push(Buffer.from(chunk)) })
req.on('end', () => {
try {
const text = Buffer.concat(chunks).toString('utf8')
resolve(text ? JSON.parse(text) : {})
} catch (error) {
reject(error)
}
})
req.on('error', reject)
})
}
function send(res, status, payload) {
const body = typeof payload === 'string' ? payload : JSON.stringify(payload)
res.writeHead(status, {
'Content-Type': typeof payload === 'string' ? 'text/html; charset=utf-8' : 'application/json; charset=utf-8',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS'
})
res.end(body)
}
const server = http.createServer(async (req, res) => {
if (req.method === 'OPTIONS') return send(res, 204, '')
try {
if (req.method === 'GET' && req.url === '/health') {
return send(res, 200, {
ok: true,
model: DEEPSEEK_MODEL,
apiKeyConfigured: Boolean(DEEPSEEK_API_KEY),
knowledgeBase: { loaded: Boolean(knowledgeBase.generatedAt), modules: knowledgeBase.modules.length, qualityModules: knowledgeBase.qualityModules.length, spcCharts: knowledgeBase.spcCharts.length },
endpoints: { mes: MES_BACKEND_URL, spc: SPC_BACKEND_URL }
})
}
if (req.method === 'GET' && req.url === '/api/tools') return send(res, 200, { tools })
if (req.method === 'POST' && req.url === '/mcp') return send(res, 200, await handleMcp(await parseBody(req)))
if (req.method === 'POST' && req.url === '/api/assistant/chat') {
const body = await parseBody(req)
const question = String(body.question || '').trim()
if (!question) return send(res, 400, { error: 'question is required' })
const navigation = navigationResponse(question)
if (navigation) return send(res, 200, navigation)
if (!isFeatureQuestion(question) && isQualityChartQuestion(question)) {
const profile = QUALITY_DATA_QUERY_PROFILE
const queryResult = await queryQualityData({ query: question, nameOrSql: profile && profile.nameOrSql, limit: 50, pageSize: 50 })
const rows = queryResult.data && Array.isArray(queryResult.data.rows) ? queryResult.data.rows : []
const charts = qualityDataCharts(rows, question)
let html = queryResult.html
if (charts.some(chart => chart && chart.mixedMeasurePosition)) {
html = '<h4>图表提示</h4>' + table([{ key: 'name', label: '项目' }, { key: 'value', label: '内容' }], [
{ name: '提示', value: '当前结果包含多个测量位置,图表可能混合多个测点;建议指定单一测量位置。' }
]) + html
}
if (!charts.length) {
html += '<h4>SPC图表</h4>' + table([{ key: 'name', label: '项目' }, { key: 'value', label: '内容' }], [
{ name: '状态', value: '未绘制' },
{ name: '原因', value: '质量数据查询结果中没有可识别的数值型测量值。' }
])
}
return send(res, 200, Object.assign({}, queryResult, {
html,
charts
}))
}
if (/图|趋势|直方|正态|过程能力|排列|X-R|XR|X-S|XS/i.test(question)) {
const chartResult = await querySpcChart({ query: question })
const html = await askDeepSeek(question, chartResult.html, {
tool: 'spc_query_chart',
data: chartResult.data || chartResult
}, { useProjectContext: false })
return send(res, 200, Object.assign({}, chartResult, { html }))
}
if (!isFeatureQuestion(question) && isQualityDataQuestion(question)) {
const profile = QUALITY_DATA_QUERY_PROFILE
const queryResult = await queryQualityData({ query: question, nameOrSql: profile && profile.nameOrSql, limit: 50, pageSize: 50 })
const rows = queryResult.data && Array.isArray(queryResult.data.rows) ? queryResult.data.rows : []
if (rows.length > 20) return send(res, 200, queryResult)
const html = await askDeepSeek(question, queryResult.html, {
tool: 'query_quality_data',
query: queryResult.data && queryResult.data.query,
payload: queryResult.data && queryResult.data.payload,
rows,
output: queryResult.data && queryResult.data.output
}, { useProjectContext: false })
return send(res, 200, Object.assign({}, queryResult, { html }))
}
const html = await askDeepSeek(question, '', {}, { useProjectContext: false })
return send(res, 200, { html })
}
return send(res, 404, { error: 'Not found' })
} catch (error) {
return send(res, 500, { error: error.message })
}
})
server.listen(PORT, () => {
console.log('WC-SPC AI MCP server listening on http://127.0.0.1:' + PORT)
console.log('DeepSeek model: ' + DEEPSEEK_MODEL + ', API key configured: ' + Boolean(DEEPSEEK_API_KEY))
console.log('Knowledge base loaded: ' + Boolean(knowledgeBase.generatedAt))
})