2567 lines
135 KiB
JavaScript
2567 lines
135 KiB
JavaScript
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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''')
|
||
}
|
||
|
||
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 isUsageHelpQuestion(question) {
|
||
const text = String(question || '').replace(/\s+/g, '')
|
||
return ['\u4f7f\u7528\u8bf4\u660e', '\u5e2e\u52a9', '\u64cd\u4f5c\u8bf4\u660e', '\u600e\u4e48\u7528', '\u5982\u4f55\u4f7f\u7528', '\u4f60\u80fd\u505a\u4ec0\u4e48', '\u6709\u54ea\u4e9b\u529f\u80fd'].some(word => text.indexOf(word) > -1)
|
||
}
|
||
|
||
function htmlForAssistantUsage() {
|
||
let html = '<h4>AI\u52a9\u624b\u4f7f\u7528\u8bf4\u660e</h4>' + table([
|
||
{ key: 'feature', label: '\u529f\u80fd' },
|
||
{ key: 'how', label: '\u5982\u4f55\u64cd\u4f5c' },
|
||
{ key: 'example', label: '\u793a\u4f8b' }
|
||
], [
|
||
{ feature: '\u67e5\u8be2\u8d28\u91cf\u6570\u636e', how: '\u6309\u5de5\u4f4d\u53f7\u3001\u6d4b\u91cf\u4f4d\u7f6e\u3001\u6d4b\u91cf\u9879\u76ee\u7b49\u6761\u4ef6\u67e5\u8be2\uff1b\u4e0d\u5199\u6761\u4ef6\u65f6\u8fd4\u56de\u6700\u65b050\u6761\u3002', example: '\u67e5\u8be2\u5de5\u4f4d\u53f73165\u7684\u8d28\u91cf\u6570\u636e' },
|
||
{ feature: '\u67e5\u8be2\u524d\u7f6e\u6761\u4ef6', how: '\u5148\u5217\u5de5\u4f4d\uff0c\u518d\u6309\u5de5\u4f4d\u67e5\u6d4b\u91cf\u4f4d\u7f6e\uff0c\u6700\u540e\u67e5\u6d4b\u91cf\u9879\u76ee\u3002', example: '\u5217\u51fa\u5de5\u4f4d\u53f7\uff1b\u5217\u51fa\u5de5\u4f4d\u53f7 OP3165 \u7684\u6d4b\u91cf\u4f4d\u7f6e' },
|
||
{ feature: 'SPC\u56fe\u8868', how: '\u67e5\u8d28\u91cf\u6570\u636e\u65f6\u660e\u786e\u8bf4\u8981\u7ed8\u5236\u54ea\u79cd\u56fe\u3002', example: '\u67e5\u8be2\u8d28\u91cf\u6570\u636e\u5e76\u7ed8\u5236SPC\u57fa\u672c\u8d8b\u52bf\u56fe' },
|
||
{ feature: 'SPC\u667a\u80fd\u9884\u8b66/\u62a5\u8b66', how: '\u660e\u786e\u8bf4\u201cSPC\u9884\u6d4b\u9884\u8b66\u201d\u6216\u201cSPC\u9884\u6d4b\u62a5\u8b66\u201d\uff0c\u7cfb\u7edf\u4f1a\u8ba1\u7b97\u8d8b\u52bf\u3001Cp\u3001Cpk\u5e76\u7ed9\u51fa\u5efa\u8bae\u3002', example: '\u67e5\u8be2\u5de5\u4f4d\u53f7OP3165\u7684\u8d28\u91cf\u6570\u636e\u5e76\u505aSPC\u9884\u6d4b\u9884\u8b66' },
|
||
{ feature: 'SPC\u53c2\u6570\u8ba1\u7b97', how: '\u6839\u636e\u5f53\u524d\u6216\u672c\u6b21\u8d28\u91cf\u6570\u636e\u8ba1\u7b97\u5747\u503c\u3001\u6807\u51c6\u5dee\u3001Cp\u3001Cpk\u3002', example: '\u8ba1\u7b97\u4e00\u4e0bSPC\u4e3b\u8981\u53c2\u6570\uff0c\u8981\u8ba1\u7b97\u7ed3\u679c' },
|
||
{ feature: '\u9875\u9762\u5bfc\u822a', how: '\u8bf4\u660e\u8981\u6253\u5f00\u7684\u9875\u9762\u6216\u4e3b\u9875\u3002', example: '\u6253\u5f00\u4e3b\u9875' }
|
||
])
|
||
html += '<h4>\u53ef\u7ed8\u5236\u7684SPC\u56fe\u8868</h4>' + table([
|
||
{ key: 'name', label: '\u56fe\u8868' },
|
||
{ key: 'trigger', label: '\u89e6\u53d1\u8bf4\u6cd5' }
|
||
], [
|
||
{ name: 'SPC\u57fa\u672c\u8d8b\u52bf\u56fe', trigger: '\u7ed8\u5236SPC\u57fa\u672c\u8d8b\u52bf\u56fe' },
|
||
{ name: '\u6837\u672c\u8d8b\u52bf\u56fe', trigger: '\u7ed8\u5236\u6837\u672c\u8d8b\u52bf\u56fe' },
|
||
{ name: '\u76f4\u65b9\u56fe', trigger: '\u7ed8\u5236\u76f4\u65b9\u56fe' },
|
||
{ name: '\u5de5\u5e8f\u80fd\u529b\u5206\u6790\u56fe', trigger: '\u7ed8\u5236\u5de5\u5e8f\u80fd\u529b\u5206\u6790\u56fe' },
|
||
{ name: '\u6392\u5217\u56fe', trigger: '\u7ed8\u5236\u6392\u5217\u56fe' },
|
||
{ name: '\u5747\u503c\u6781\u5dee\u56fe', trigger: '\u7ed8\u5236\u5747\u503c\u6781\u5dee\u56fe' },
|
||
{ name: '\u5747\u503c\u6807\u51c6\u5dee\u56fe', trigger: '\u7ed8\u5236\u5747\u503c\u6807\u51c6\u5dee\u56fe' }
|
||
])
|
||
html += '<h4>\u6ce8\u610f\u4e8b\u9879</h4>' + table([{ key: 'item', label: '\u9879\u76ee' }, { key: 'text', label: '\u8bf4\u660e' }], [
|
||
{ item: '\u6570\u636e\u6765\u6e90', text: '\u6240\u6709\u8d28\u91cf\u6570\u636e\u90fd\u590d\u7528\u9879\u76ee\u5df2\u6709 HMI/CreateData/MESCommonBase.ashx \u67e5\u8be2\u5165\u53e3\uff0c\u4e0d\u76f4\u8fde\u6570\u636e\u5e93\u3002' },
|
||
{ item: '\u63a8\u8350\u6761\u4ef6', text: '\u505aSPC\u56fe\u8868\u6216\u9884\u8b66\u65f6\uff0c\u5efa\u8bae\u6307\u5b9a\u5355\u4e00\u5de5\u4f4d\u53f7+\u5355\u4e00\u6d4b\u91cf\u4f4d\u7f6e+\u5355\u4e00\u6d4b\u91cf\u9879\u76ee\u3002' }
|
||
])
|
||
return html
|
||
}
|
||
|
||
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, timeoutMs) {
|
||
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)
|
||
if (timeoutMs) {
|
||
req.setTimeout(timeoutMs, () => {
|
||
req.destroy(new Error('Request timeout after ' + timeoutMs + 'ms'))
|
||
})
|
||
}
|
||
req.write(bodyBuffer)
|
||
req.end()
|
||
})
|
||
}
|
||
|
||
function shouldRouteQualityDataQuery(question) {
|
||
const text = String(question || '')
|
||
const hasQualityData = ['\u8d28\u91cf\u6570\u636e', '\u8d28\u91cf\u660e\u7ec6', '\u660e\u7ec6\u6570\u636e'].some(word => text.indexOf(word) > -1)
|
||
const hasAction = ['\u67e5\u8be2', '\u67e5\u770b', '\u663e\u793a', '\u5217\u51fa', '\u83b7\u53d6', '\u8fd4\u56de'].some(word => text.indexOf(word) > -1)
|
||
return hasQualityData && hasAction
|
||
}
|
||
|
||
function isQualityDataQuestion(question) {
|
||
if (shouldRouteQualityDataQuery(question)) return true
|
||
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 parsedStationNumber = pickArg(args, ['stationNumber', 'opName']) ||
|
||
parseQuotedOrNamed(args.query || '', ['宸ヤ綅鍙?', '宸ヤ綅']) ||
|
||
stationNumberFromQuestion(args.query || '')
|
||
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)
|
||
}
|
||
values.stationNumber = normalizeStationNumber(parsedStationNumber)
|
||
values.testPosition = pickArg(args, ['testPosition', 'measurePosition', 'measureName']) || testPositionFromQuestion(args.query || '') || values.testPosition
|
||
values.testItem = pickArg(args, ['testItem', 'measureItem', 'measureContent']) || testItemFromQuestion(args.query || '') || values.testItem
|
||
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 isQualitySpcWarningQuestion(question) {
|
||
const text = String(question || '').toLowerCase()
|
||
return [
|
||
'\u9884\u6d4b\u9884\u8b66',
|
||
'\u9884\u6d4b\u62a5\u8b66',
|
||
'spc\u9884\u8b66',
|
||
'spc\u62a5\u8b66',
|
||
'\u505aspc\u9884\u8b66',
|
||
'\u505aspc\u62a5\u8b66',
|
||
'\u505a\u9884\u8b66',
|
||
'\u505a\u62a5\u8b66',
|
||
'\u9884\u8b66\u7ed3\u679c',
|
||
'\u62a5\u8b66\u7ed3\u679c',
|
||
'\u9884\u8b66\u5efa\u8bae',
|
||
'\u62a5\u8b66\u5efa\u8bae',
|
||
'\u7efc\u8ff0\u4e00\u4e0b\u9884\u6d4b\u9884\u8b66',
|
||
'spc\u9884\u6d4b\u62a5\u8b66',
|
||
'\u667a\u80fd\u62a5\u8b66',
|
||
'\u8d28\u91cf\u62a5\u8b66',
|
||
'\u8d8b\u52bf\u62a5\u8b66',
|
||
'cpk\u62a5\u8b66',
|
||
'cpk\u8d8b\u52bf\u62a5\u8b66',
|
||
'\u5de5\u5e8f\u80fd\u529b\u62a5\u8b66',
|
||
'\u8fc7\u7a0b\u80fd\u529b\u62a5\u8b66',
|
||
'spc预测预警',
|
||
'智能预警',
|
||
'质量预警',
|
||
'趋势预警',
|
||
'异常预测',
|
||
'失控风险',
|
||
'cpk预警',
|
||
'cpk趋势预警',
|
||
'工序能力预警',
|
||
'过程能力预警',
|
||
'预测这个工位是否有风险'
|
||
].some(word => text.indexOf(word) > -1)
|
||
}
|
||
|
||
function shouldRouteQualityWarning(question) {
|
||
const text = String(question || '').replace(/\s+/g, '').toLowerCase()
|
||
return ['spc\u9884\u8b66', 'spc\u62a5\u8b66', '\u9884\u6d4b\u9884\u8b66', '\u9884\u6d4b\u62a5\u8b66', '\u667a\u80fd\u9884\u8b66', '\u667a\u80fd\u62a5\u8b66', '\u8d28\u91cf\u9884\u8b66', '\u8d28\u91cf\u62a5\u8b66', 'cpk\u9884\u8b66', 'cpk\u62a5\u8b66', '\u505a\u9884\u8b66', '\u505a\u62a5\u8b66'].some(word => text.indexOf(word) > -1)
|
||
}
|
||
|
||
function warningNumber(value) {
|
||
if (value == null || value === '') return NaN
|
||
const match = String(value).replace(/,/g, '').match(/-?\d+(?:\.\d+)?/)
|
||
return match ? Number(match[0]) : NaN
|
||
}
|
||
|
||
function compatibleValue(row, aliases) {
|
||
const keys = Object.keys(row || {})
|
||
for (let i = 0; i < aliases.length; i++) {
|
||
if (row && row[aliases[i]] != null && row[aliases[i]] !== '') return row[aliases[i]]
|
||
}
|
||
for (let i = 0; i < aliases.length; i++) {
|
||
const needle = String(aliases[i]).toLowerCase()
|
||
const key = keys.find(item => String(item).toLowerCase() === needle || String(item).toLowerCase().indexOf(needle) > -1)
|
||
if (key && row[key] != null && row[key] !== '') return row[key]
|
||
}
|
||
return ''
|
||
}
|
||
|
||
function qualityWarningRecords(rows) {
|
||
return normalizeRows(rows, 1000).map((row, index) => {
|
||
const time = compatibleValue(row, ['生产日期', '操作时间', '完成时间', '到达时间', '离开时间', '时间'])
|
||
return {
|
||
row,
|
||
index,
|
||
time,
|
||
timeValue: parseDateValue(time),
|
||
station: compatibleValue(row, ['工位号', '指南工位号', '工位编号', '工序名称', '工位名称']),
|
||
stationName: compatibleValue(row, ['工序名称', '工位名称', '工位号', '指南工位号']),
|
||
testPosition: compatibleValue(row, ['测量位置', '位置']),
|
||
testItem: compatibleValue(row, ['测量项目', '测量内容', '项目']),
|
||
value: warningNumber(compatibleValue(row, ['测量值', '实际值', 'value', 'Value', '数值'])),
|
||
upper: warningNumber(compatibleValue(row, ['上限值', 'USL', '上限'])),
|
||
lower: warningNumber(compatibleValue(row, ['下限值', 'LSL', '下限'])),
|
||
target: warningNumber(compatibleValue(row, ['理论值', '目标值', '中心值'])),
|
||
passFlag: compatibleValue(row, ['合格标志', '是否合格', '合格'])
|
||
}
|
||
}).filter(item => !Number.isNaN(item.value)).sort((a, b) => (a.timeValue || a.index) - (b.timeValue || b.index))
|
||
}
|
||
|
||
function warningAverage(values) {
|
||
return values.length ? values.reduce((acc, item) => acc + item, 0) / values.length : NaN
|
||
}
|
||
|
||
function warningStdDev(values) {
|
||
if (values.length < 2) return NaN
|
||
const avg = warningAverage(values)
|
||
return Math.sqrt(values.reduce((acc, item) => acc + Math.pow(item - avg, 2), 0) / (values.length - 1))
|
||
}
|
||
|
||
function warningRound(value, digits) {
|
||
return Number.isNaN(value) || value == null ? '' : Number(value).toFixed(digits == null ? 4 : digits)
|
||
}
|
||
|
||
function warningCapability(values, upper, lower) {
|
||
const mean = warningAverage(values)
|
||
const sigma = warningStdDev(values)
|
||
if (Number.isNaN(mean) || Number.isNaN(sigma) || sigma <= 0 || Number.isNaN(upper) || Number.isNaN(lower)) {
|
||
return { mean, sigma, cp: NaN, cpu: NaN, cpl: NaN, cpk: NaN }
|
||
}
|
||
const cp = (upper - lower) / (6 * sigma)
|
||
const cpu = (upper - mean) / (3 * sigma)
|
||
const cpl = (mean - lower) / (3 * sigma)
|
||
return { mean, sigma, cp, cpu, cpl, cpk: Math.min(cpu, cpl) }
|
||
}
|
||
|
||
function warningCapabilityLevel(cpk) {
|
||
if (Number.isNaN(cpk)) return { level: '不可计算', score: 0, text: '缺少上下限或样本波动为 0' }
|
||
if (cpk < 0.67) return { level: '严重', score: 70, text: 'Cpk < 0.67,工序能力严重不足' }
|
||
if (cpk < 1) return { level: '预警', score: 45, text: 'Cpk < 1.00,工序能力不足' }
|
||
if (cpk < 1.33) return { level: '关注', score: 25, text: 'Cpk < 1.33,工序能力边缘' }
|
||
if (cpk < 1.67) return { level: '正常', score: 0, text: 'Cpk 满足常规质量要求' }
|
||
return { level: '优秀', score: 0, text: 'Cpk >= 1.67,工序能力充足' }
|
||
}
|
||
|
||
function warningRiskRank(level) {
|
||
return { '优秀': 0, '正常': 0, '关注': 1, '预警': 2, '严重': 3 }[level] || 0
|
||
}
|
||
|
||
function warningRiskLevel(score, strongest) {
|
||
const scoreLevel = score >= 70 ? '严重' : score >= 40 ? '预警' : score >= 20 ? '关注' : '正常'
|
||
return warningRiskRank(strongest) > warningRiskRank(scoreLevel) ? strongest : scoreLevel
|
||
}
|
||
|
||
function warningRule(rules, name, hit, level, score, description) {
|
||
rules.push({ name, hit: hit ? '是' : '否', level: hit ? level : '正常', score: hit ? score : 0, description: hit ? description : '未触发' })
|
||
}
|
||
|
||
function warningWindows(records, windowSize, windowStep) {
|
||
const values = records.map(item => item.value)
|
||
const upperRecord = records.find(item => !Number.isNaN(item.upper))
|
||
const lowerRecord = records.find(item => !Number.isNaN(item.lower))
|
||
const upper = upperRecord ? upperRecord.upper : NaN
|
||
const lower = lowerRecord ? lowerRecord.lower : NaN
|
||
const size = Math.max(Number(windowSize || 25), 2)
|
||
const step = Math.max(Number(windowStep || 10), 1)
|
||
const windows = []
|
||
if (values.length && values.length < size) {
|
||
const capability = warningCapability(values, upper, lower)
|
||
const level = warningCapabilityLevel(capability.cpk)
|
||
return [{ name: 'W1', range: '第1-' + values.length + '条', mean: capability.mean, sigma: capability.sigma, cp: capability.cp, cpk: capability.cpk, level: level.level, description: level.text }]
|
||
}
|
||
for (let start = 0; start + size <= values.length; start += step) {
|
||
const group = values.slice(start, start + size)
|
||
const capability = warningCapability(group, upper, lower)
|
||
const level = warningCapabilityLevel(capability.cpk)
|
||
windows.push({ name: 'W' + (windows.length + 1), range: '第' + (start + 1) + '-' + (start + group.length) + '条', mean: capability.mean, sigma: capability.sigma, cp: capability.cp, cpk: capability.cpk, level: level.level, description: level.text })
|
||
}
|
||
return windows
|
||
}
|
||
|
||
function warningConfidence(records, args) {
|
||
const stations = unique(records.map(item => item.station).filter(Boolean))
|
||
const positions = unique(records.map(item => item.testPosition).filter(Boolean))
|
||
const items = unique(records.map(item => item.testItem).filter(Boolean))
|
||
const hasLimits = records.some(item => !Number.isNaN(item.upper)) && records.some(item => !Number.isNaN(item.lower))
|
||
const reasons = []
|
||
let confidence = '高'
|
||
if (records.length < 10) {
|
||
confidence = '极低'
|
||
reasons.push('有效样本少于 10 条,仅能做基础超限判断')
|
||
} else if (records.length < 50) {
|
||
confidence = '低'
|
||
reasons.push('有效样本少于 50 条')
|
||
} else if (records.length < 100) {
|
||
confidence = '中'
|
||
reasons.push('有效样本少于 100 条')
|
||
}
|
||
if (stations.length > 1 || positions.length > 1 || items.length > 1) {
|
||
confidence = confidence === '极低' ? confidence : '低'
|
||
reasons.push('结果包含多个工位、测量位置或测量项目')
|
||
}
|
||
if (!hasLimits) {
|
||
confidence = confidence === '极低' ? confidence : '低'
|
||
reasons.push('缺少上限值或下限值,无法完整计算 Cp/Cpk')
|
||
}
|
||
if (!(args && (args.stationNumber || args.testPosition || args.testItem))) {
|
||
confidence = confidence === '极低' ? confidence : '低'
|
||
reasons.push('用户未提供完整工位、测量位置、测量项目条件')
|
||
}
|
||
return { confidence, reasons: reasons.length ? reasons : ['数据粒度和规格限满足分析要求'] }
|
||
}
|
||
|
||
function warningNg(value) {
|
||
const text = String(value == null ? '' : value).trim()
|
||
return /不合格|NG|false/i.test(text) || text === '0'
|
||
}
|
||
|
||
function analyzeQualitySpcWarning(records, args) {
|
||
const values = records.map(item => item.value)
|
||
const upperRecord = records.find(item => !Number.isNaN(item.upper))
|
||
const lowerRecord = records.find(item => !Number.isNaN(item.lower))
|
||
const targetRecord = records.find(item => !Number.isNaN(item.target))
|
||
const upper = upperRecord ? upperRecord.upper : NaN
|
||
const lower = lowerRecord ? lowerRecord.lower : NaN
|
||
const target = targetRecord ? targetRecord.target : NaN
|
||
const width = !Number.isNaN(upper) && !Number.isNaN(lower) ? upper - lower : NaN
|
||
const rules = []
|
||
const outOfSpec = records.filter(item => (!Number.isNaN(upper) && item.value > upper) || (!Number.isNaN(lower) && item.value < lower))
|
||
const ngRows = records.filter(item => warningNg(item.passFlag))
|
||
warningRule(rules, '规格超限', outOfSpec.length > 0, '严重', 70, '存在 ' + outOfSpec.length + ' 条测量值超出规格限')
|
||
warningRule(rules, '不合格标志', ngRows.length > 0, '严重', 60, '存在 ' + ngRows.length + ' 条不合格标志')
|
||
const near10 = !Number.isNaN(width) && width > 0 ? records.filter(item => (!Number.isNaN(upper) && item.value <= upper && (upper - item.value) / width <= 0.1) || (!Number.isNaN(lower) && item.value >= lower && (item.value - lower) / width <= 0.1)) : []
|
||
const near5 = !Number.isNaN(width) && width > 0 ? records.filter(item => (!Number.isNaN(upper) && item.value <= upper && (upper - item.value) / width <= 0.05) || (!Number.isNaN(lower) && item.value >= lower && (item.value - lower) / width <= 0.05)) : []
|
||
warningRule(rules, '接近规格限10%', near10.length > 0, '关注', 20, '存在 ' + near10.length + ' 条数据接近规格边界')
|
||
warningRule(rules, '接近规格限5%', near5.length > 0, '预警', 35, '存在 ' + near5.length + ' 条数据高危接近规格边界')
|
||
const recent5 = values.slice(-5)
|
||
const rising = recent5.length >= 5 && recent5.every((item, index) => index === 0 || item > recent5[index - 1])
|
||
const falling = recent5.length >= 5 && recent5.every((item, index) => index === 0 || item < recent5[index - 1])
|
||
warningRule(rules, '连续漂移', rising || falling, '预警', 30, rising ? '最近 5 点连续上升' : '最近 5 点连续下降')
|
||
const recent10 = values.slice(-10)
|
||
const previous10 = values.slice(-20, -10)
|
||
const meanShift = recent10.length >= 10 && previous10.length >= 10 && !Number.isNaN(width) && width > 0 && Math.abs(warningAverage(recent10) - warningAverage(previous10)) > width * 0.1
|
||
warningRule(rules, '均值偏移', meanShift, '预警', 25, '最近窗口均值较历史窗口偏移超过规格宽度 10%')
|
||
const volatility = recent10.length >= 10 && previous10.length >= 10 && warningStdDev(previous10) > 0 && warningStdDev(recent10) > warningStdDev(previous10) * 1.5
|
||
warningRule(rules, '波动增大', volatility, '预警', 25, '最近窗口标准差大于历史窗口 1.5 倍')
|
||
const capability = warningCapability(values, upper, lower)
|
||
const capabilityRisk = warningCapabilityLevel(capability.cpk)
|
||
warningRule(rules, 'Cpk能力不足', capabilityRisk.score > 0, capabilityRisk.level, capabilityRisk.score, capabilityRisk.text)
|
||
const windows = warningWindows(records, args.windowSize, args.windowStep)
|
||
const lastWindows = windows.slice(-3)
|
||
const cpkFalling = lastWindows.length >= 3 && lastWindows.every((item, index) => index === 0 || (!Number.isNaN(item.cpk) && !Number.isNaN(lastWindows[index - 1].cpk) && item.cpk < lastWindows[index - 1].cpk))
|
||
warningRule(rules, 'Cpk连续下降', cpkFalling, '预警', 35, '最近 3 个滚动窗口 Cpk 连续下降')
|
||
const lastWindow = windows[windows.length - 1]
|
||
const prevWindow = windows[windows.length - 2]
|
||
const cpkFastDrop = lastWindow && prevWindow && !Number.isNaN(lastWindow.cpk) && !Number.isNaN(prevWindow.cpk) && prevWindow.cpk > 0 && (prevWindow.cpk - lastWindow.cpk) / prevWindow.cpk > 0.2
|
||
warningRule(rules, 'Cpk快速下降', cpkFastDrop, '预警', 35, '最近窗口 Cpk 较上一窗口下降超过 20%')
|
||
const centerShift = capability.cp >= 1.33 && capability.cpk < 1
|
||
warningRule(rules, '中心偏移', centerShift, '预警', 35, 'Cp 正常但 Cpk 偏低,过程中心可能偏移')
|
||
const score = Math.min(100, rules.reduce((acc, item) => acc + Number(item.score || 0), 0))
|
||
const strongest = rules.filter(item => item.hit === '是').reduce((level, item) => warningRiskRank(item.level) > warningRiskRank(level) ? item.level : level, '正常')
|
||
const confidence = warningConfidence(records, args)
|
||
const level = warningRiskLevel(score, strongest)
|
||
const stations = unique(records.map(item => item.stationName || item.station).filter(Boolean))
|
||
const positions = unique(records.map(item => item.testPosition).filter(Boolean))
|
||
const items = unique(records.map(item => item.testItem).filter(Boolean))
|
||
return {
|
||
level,
|
||
score,
|
||
sampleCount: records.length,
|
||
timeRange: records.length ? ((records[0].time || '无时间') + ' - ' + (records[records.length - 1].time || '无时间')) : '',
|
||
objectName: (stations.length === 1 ? stations[0] : '多工位') + ' / ' + (positions.length === 1 ? positions[0] : '多测量位置') + ' / ' + (items.length === 1 ? items[0] : '多测量项目'),
|
||
upper,
|
||
lower,
|
||
target,
|
||
mean: capability.mean,
|
||
sigma: capability.sigma,
|
||
cp: capability.cp,
|
||
cpk: capability.cpk,
|
||
cpkTrend: cpkFalling ? '下降' : (cpkFastDrop ? '快速下降' : '平稳'),
|
||
rules,
|
||
windows,
|
||
confidence: confidence.confidence,
|
||
confidenceReasons: confidence.reasons,
|
||
summary: level === '严重' ? '已发现严重质量风险,建议立即确认并追溯相关工件。' : level === '预警' ? '存在明显趋势或工序能力风险,建议尽快人工确认。' : level === '关注' ? '存在轻微趋势或能力边缘风险,建议持续观察。' : '当前样本未发现明显 SPC 预警风险。'
|
||
}
|
||
}
|
||
|
||
function warningBadge(level) {
|
||
const className = { '优秀': 'ai-risk-normal', '正常': 'ai-risk-normal', '关注': 'ai-risk-watch', '预警': 'ai-risk-warning', '严重': 'ai-risk-critical' }[level] || 'ai-risk-normal'
|
||
return '<span class="ai-risk-badge ' + className + '">' + escapeHtml(level) + '</span>'
|
||
}
|
||
|
||
function htmlForQualitySpcWarning(analysis, queryResult, records) {
|
||
const conclusionRows = [
|
||
{ name: '风险等级', value: warningBadge(analysis.level) },
|
||
{ name: '风险分数', value: analysis.score },
|
||
{ name: '分析对象', value: analysis.objectName },
|
||
{ name: '样本数量', value: analysis.sampleCount },
|
||
{ name: '时间范围', value: analysis.timeRange },
|
||
{ name: '当前 Cp', value: warningRound(analysis.cp, 4) || '不可计算' },
|
||
{ name: '当前 Cpk', value: warningRound(analysis.cpk, 4) || '不可计算' },
|
||
{ name: 'Cpk 趋势', value: analysis.cpkTrend },
|
||
{ name: '置信度', value: analysis.confidence },
|
||
{ name: '结论摘要', value: analysis.summary }
|
||
]
|
||
let html = '<h4>SPC智能预测预警结论</h4><table class="ai-result-table"><tbody>'
|
||
conclusionRows.forEach(row => { html += '<tr><th>' + escapeHtml(row.name) + '</th><td>' + row.value + '</td></tr>' })
|
||
html += '</tbody></table>'
|
||
html += '<h4>预警规则命中</h4>' + table([{ key: 'name', label: '规则' }, { key: 'hit', label: '是否命中' }, { key: 'level', label: '风险等级' }, { key: 'score', label: '分数' }, { key: 'description', label: '说明' }], analysis.rules)
|
||
html += '<h4>Cp/Cpk工序能力趋势</h4>' + table([{ key: 'name', label: '窗口' }, { key: 'range', label: '样本范围' }, { key: 'meanText', label: '平均值' }, { key: 'sigmaText', label: '标准差' }, { key: 'cpText', label: 'Cp' }, { key: 'cpkText', label: 'Cpk' }, { key: 'level', label: '判断' }], analysis.windows.map(item => Object.assign({}, item, { meanText: warningRound(item.mean, 4) || '不可计算', sigmaText: warningRound(item.sigma, 4) || '不可计算', cpText: warningRound(item.cp, 4) || '不可计算', cpkText: warningRound(item.cpk, 4) || '不可计算' })))
|
||
html += '<h4>置信度说明</h4>' + table([{ key: 'reason', label: '说明' }], analysis.confidenceReasons.map(reason => ({ reason })))
|
||
html += '<h4>改进建议</h4>' + table([{ key: 'risk', label: '风险' }, { key: 'suggestion', label: '建议' }], [
|
||
{ risk: '测量值接近上限', suggestion: '检查设备补偿、夹具状态、刀具磨损或拧紧参数。' },
|
||
{ risk: '测量值接近下限', suggestion: '检查装配不到位、零件批次或工艺参数偏低。' },
|
||
{ risk: 'Cpk下降', suggestion: '优先确认过程中心是否偏移,再检查波动来源。' },
|
||
{ risk: '波动变大', suggestion: '检查设备稳定性、环境、人员操作和来料一致性。' },
|
||
{ risk: '已超限', suggestion: '建议立即停线确认、隔离相关工件并追溯前后批次。' }
|
||
])
|
||
html += htmlForQualityDetailRows((queryResult.data && queryResult.data.rows) || records.map(item => item.row))
|
||
return html
|
||
}
|
||
|
||
function isWarningSummaryQuestion(question) {
|
||
const text = String(question || '').toLowerCase()
|
||
return /综述|总结|概括|分析|建议|改进/.test(text) && /预测|预警|报警|spc|cpk/.test(text)
|
||
}
|
||
|
||
function isSpcParameterQuestion(question) {
|
||
const text = String(question || '').toLowerCase()
|
||
const subjects = [
|
||
'spc',
|
||
'cp',
|
||
'cpk',
|
||
'cpcpk',
|
||
'\u8fc7\u7a0b\u80fd\u529b',
|
||
'\u5de5\u5e8f\u80fd\u529b',
|
||
'\u4e3b\u8981\u53c2\u6570',
|
||
'\u80fd\u529b\u6307\u6570',
|
||
'\u6807\u51c6\u5dee',
|
||
'\u5747\u503c'
|
||
]
|
||
const actions = [
|
||
'\u8ba1\u7b97',
|
||
'\u7b97\u4e00\u4e0b',
|
||
'\u53c2\u6570',
|
||
'\u7ed3\u679c',
|
||
'cp',
|
||
'cpk',
|
||
'cpcpk'
|
||
]
|
||
return subjects.some(word => text.indexOf(word) > -1) && actions.some(word => text.indexOf(word) > -1)
|
||
}
|
||
|
||
|
||
function spcParameterRowsFromAnalysis(analysis) {
|
||
if (!analysis || typeof analysis !== 'object') return []
|
||
return [
|
||
{ name: '\u6837\u672c\u6570\u91cf', symbol: 'n', value: analysis.sampleCount == null ? '\u672a\u77e5' : analysis.sampleCount, note: '\u53c2\u4e0e\u8ba1\u7b97\u7684\u6709\u6548\u6d4b\u91cf\u503c\u6570\u91cf' },
|
||
{ name: '\u89c4\u683c\u4e0a\u9650', symbol: 'USL', value: warningRound(Number(analysis.upper), 4) || '\u4e0d\u53ef\u8ba1\u7b97', note: '\u8d28\u91cf\u7279\u6027\u7684\u4e0a\u9650\u503c' },
|
||
{ name: '\u89c4\u683c\u4e0b\u9650', symbol: 'LSL', value: warningRound(Number(analysis.lower), 4) || '\u4e0d\u53ef\u8ba1\u7b97', note: '\u8d28\u91cf\u7279\u6027\u7684\u4e0b\u9650\u503c' },
|
||
{ name: '\u6837\u672c\u5747\u503c', symbol: 'Mean', value: warningRound(Number(analysis.mean), 4) || '\u4e0d\u53ef\u8ba1\u7b97', note: '\u6d4b\u91cf\u503c\u5e73\u5747\u6c34\u5e73' },
|
||
{ name: '\u6837\u672c\u6807\u51c6\u5dee', symbol: 'Sigma', value: warningRound(Number(analysis.sigma), 4) || '\u4e0d\u53ef\u8ba1\u7b97', note: '\u6309\u6837\u672c\u6807\u51c6\u5dee\u4f30\u8ba1\u8fc7\u7a0b\u6ce2\u52a8' },
|
||
{ name: '\u8fc7\u7a0b\u80fd\u529b\u6307\u6570', symbol: 'Cp', value: warningRound(Number(analysis.cp), 4) || '\u4e0d\u53ef\u8ba1\u7b97', note: '\u53ea\u8bc4\u4ef7\u516c\u5dee\u5bbd\u5ea6\u4e0e\u8fc7\u7a0b\u6ce2\u52a8' },
|
||
{ name: '\u5b9e\u9645\u8fc7\u7a0b\u80fd\u529b\u6307\u6570', symbol: 'Cpk', value: warningRound(Number(analysis.cpk), 4) || '\u4e0d\u53ef\u8ba1\u7b97', note: '\u540c\u65f6\u8003\u8651\u8fc7\u7a0b\u4e2d\u5fc3\u504f\u79fb' },
|
||
{ name: 'Cpk\u8d8b\u52bf', symbol: 'Trend', value: analysis.cpkTrend || '\u672a\u77e5', note: '\u7531\u6eda\u52a8\u7a97\u53e3 Cp/Cpk \u7ed3\u679c\u5224\u65ad' }
|
||
]
|
||
}
|
||
|
||
function htmlForSpcParameters(analysis, sourceText) {
|
||
if (!analysis || typeof analysis !== 'object') {
|
||
return '<h4>SPC\u4e3b\u8981\u53c2\u6570\u8ba1\u7b97\u7ed3\u679c</h4>' + table([{ key: 'name', label: '\u9879\u76ee' }, { key: 'value', label: '\u5185\u5bb9' }], [
|
||
{ name: '\u72b6\u6001', value: '\u5f53\u524d\u6ca1\u6709\u53ef\u7528\u4e8e\u8ba1\u7b97\u7684\u8d28\u91cf\u6570\u636e\u6216\u9884\u6d4b\u9884\u8b66\u4e0a\u4e0b\u6587\u3002' },
|
||
{ name: '\u5efa\u8bae', value: '\u8bf7\u63d0\u4f9b\u5de5\u4f4d\u53f7\u3001\u6d4b\u91cf\u4f4d\u7f6e\u3001\u6d4b\u91cf\u9879\u76ee\uff0c\u6216\u5148\u6267\u884c\u4e00\u6b21\u8d28\u91cf\u6570\u636e\u67e5\u8be2/\u9884\u6d4b\u9884\u8b66\u3002' }
|
||
])
|
||
}
|
||
const levelText = warningCapabilityLevel(Number(analysis.cpk)).text
|
||
let html = '<h4>SPC\u4e3b\u8981\u53c2\u6570\u8ba1\u7b97\u7ed3\u679c</h4>' + table([
|
||
{ key: 'name', label: '\u53c2\u6570' },
|
||
{ key: 'symbol', label: '\u7b26\u53f7' },
|
||
{ key: 'value', label: '\u8ba1\u7b97\u503c' },
|
||
{ key: 'note', label: '\u8bf4\u660e' }
|
||
], spcParameterRowsFromAnalysis(analysis))
|
||
html += '<h4>\u8ba1\u7b97\u516c\u5f0f</h4>' + table([{ key: 'name', label: '\u516c\u5f0f' }, { key: 'value', label: '\u5185\u5bb9' }], [
|
||
{ name: 'Cp', value: '(USL - LSL) / (6 * Sigma)' },
|
||
{ name: 'Cpk', value: 'min((USL - Mean) / (3 * Sigma), (Mean - LSL) / (3 * Sigma))' }
|
||
])
|
||
html += '<h4>\u80fd\u529b\u5224\u65ad</h4>' + table([{ key: 'name', label: '\u9879\u76ee' }, { key: 'value', label: '\u7ed3\u8bba' }], [
|
||
{ name: '\u6570\u636e\u6765\u6e90', value: sourceText || '\u8d28\u91cf\u6570\u636e\u67e5\u8be2\u7ed3\u679c' },
|
||
{ name: '\u5206\u6790\u5bf9\u8c61', value: analysis.objectName || '\u672a\u77e5' },
|
||
{ name: '\u65f6\u95f4\u8303\u56f4', value: analysis.timeRange || '\u672a\u77e5' },
|
||
{ name: '\u5224\u65ad', value: levelText }
|
||
])
|
||
return html
|
||
}
|
||
|
||
function isQualityDimensionQuestion(question) {
|
||
const text = String(question || '').toLowerCase()
|
||
return /列出|查询|显示|有哪些|有什么/.test(text) &&
|
||
/工位号|工位|测量位置|检测位置|测量项目|检测项目|检测内容|测量内容/.test(text)
|
||
}
|
||
|
||
function dimensionValue(row, aliases) {
|
||
return compatibleValue(row, aliases)
|
||
}
|
||
|
||
function dimensionRows(rows, kind) {
|
||
const normalized = normalizeRows(rows, 500)
|
||
const mapped = normalized.map(row => {
|
||
if (kind === 'station') {
|
||
return {
|
||
stationNo: dimensionValue(row, ['工位号', '指南工位号', '工位编号']),
|
||
stationName: dimensionValue(row, ['工位名称', '工序名称', '名称'])
|
||
}
|
||
}
|
||
if (kind === 'position') {
|
||
return {
|
||
stationNo: dimensionValue(row, ['工位号', '指南工位号', '工位编号']),
|
||
testPosition: dimensionValue(row, ['测量位置', '检测位置', '位置'])
|
||
}
|
||
}
|
||
return {
|
||
stationNo: dimensionValue(row, ['工位号', '指南工位号', '工位编号']),
|
||
testPosition: dimensionValue(row, ['测量位置', '检测位置', '位置']),
|
||
testItem: dimensionValue(row, ['测量项目', '检测项目', '测量内容', '检测内容', '项目'])
|
||
}
|
||
}).filter(row => Object.keys(row).some(key => row[key]))
|
||
const seen = {}
|
||
return mapped.filter(row => {
|
||
const key = Object.keys(row).map(item => row[item]).join('|')
|
||
if (seen[key]) return false
|
||
seen[key] = true
|
||
return true
|
||
})
|
||
}
|
||
|
||
function mesPayload(type, name, params, modularId) {
|
||
return {
|
||
type: String(type || '11'),
|
||
name,
|
||
param: JSON.stringify(params || []),
|
||
UserID: 'AI',
|
||
ModularID: modularId || '/QualityAssurance/AssemblyQualitydataQuery'
|
||
}
|
||
}
|
||
|
||
async function callMesQuery(name, params) {
|
||
const payload = mesPayload('11', name, params || [])
|
||
const response = await postJson(MES_BACKEND_URL, JSON.stringify(payload))
|
||
const parsed = unwrapMesResponse(response)
|
||
return { payload, response, rows: parsed.rows, output: parsed.output }
|
||
}
|
||
|
||
function stationNumberFromQuestion(question) {
|
||
return parseQuotedOrNamed(question || '', ['工位号', '工位', '工位名称']) || ''
|
||
}
|
||
|
||
function testPositionFromQuestion(question) {
|
||
return parseQuotedOrNamed(question || '', ['测量位置', '检测位置', '位置']) || ''
|
||
}
|
||
|
||
async function queryQualityDimensions(args) {
|
||
const query = String((args && args.query) || '')
|
||
const stationNo = (args && (args.stationNumber || args.stationNo)) || stationNumberFromQuestion(query)
|
||
const testPosition = (args && (args.testPosition || args.measurePosition)) || testPositionFromQuestion(query)
|
||
const wantsItem = /测量项目|检测项目|检测内容|测量内容|项目/.test(query)
|
||
const wantsPosition = /测量位置|检测位置|位置/.test(query)
|
||
const kind = wantsItem ? 'item' : (wantsPosition && stationNo ? 'position' : 'station')
|
||
let result
|
||
if (kind === 'item') {
|
||
result = await callMesQuery('质量数据查询_测量项目 ', [
|
||
['工位号', stationNo],
|
||
['测量位置', testPosition]
|
||
])
|
||
} else if (kind === 'position') {
|
||
result = await callMesQuery('质量数据查询_测量位置 ', [
|
||
['工位号', stationNo]
|
||
])
|
||
} else {
|
||
result = await callMesQuery('MES_计划BOM_工位与名称_查询', [])
|
||
}
|
||
const rows = dimensionRows(result.rows, kind)
|
||
let html = '<h4>质量数据查询前置条件</h4>' + table([
|
||
{ key: 'name', label: '项目' },
|
||
{ key: 'value', label: '内容' }
|
||
], [
|
||
{ name: '查询类型', value: kind === 'station' ? '工位号列表' : kind === 'position' ? '测量位置列表' : '测量项目/检测内容列表' },
|
||
{ name: '工位号', value: stationNo || '未指定' },
|
||
{ name: '测量位置', value: testPosition || '未指定' },
|
||
{ name: '查询入口', value: result.payload.name }
|
||
])
|
||
if (kind === 'station') {
|
||
html += '<h4>工位号列表</h4>' + table([{ key: 'stationNo', label: '工位号' }, { key: 'stationName', label: '工位名称' }], rows)
|
||
} else if (kind === 'position') {
|
||
html += '<h4>测量位置列表</h4>' + table([{ key: 'stationNo', label: '工位号' }, { key: 'testPosition', label: '测量位置' }], rows)
|
||
} else {
|
||
html += '<h4>测量项目/检测内容列表</h4>' + table([{ key: 'stationNo', label: '工位号' }, { key: 'testPosition', label: '测量位置' }, { key: 'testItem', label: '测量项目/检测内容' }], rows)
|
||
}
|
||
html += '<h4>下一步建议</h4>' + table([{ key: 'step', label: '步骤' }, { key: 'text', label: '说明' }], [
|
||
{ step: '1', text: '先选择一个工位号。' },
|
||
{ step: '2', text: '根据工位号查询测量位置。' },
|
||
{ step: '3', text: '根据工位号和测量位置查询测量项目,再用于质量数据查询、SPC图表或预测预警。' }
|
||
])
|
||
return { html, data: { kind, stationNo, testPosition, rows, payload: result.payload, output: result.output } }
|
||
}
|
||
|
||
const QUALITY_DIMENSION_SAFE = {
|
||
stationQuery: '\u8d28\u91cf\u6570\u636e\u67e5\u8be2_\u5de5\u4f4d\u53f7',
|
||
bomStationQuery: 'MES_\u8ba1\u5212BOM_\u5de5\u4f4d\u4e0e\u540d\u79f0_\u67e5\u8be2',
|
||
positionQuery: '\u8d28\u91cf\u6570\u636e\u67e5\u8be2_\u6d4b\u91cf\u4f4d\u7f6e',
|
||
itemQuery: '\u8d28\u91cf\u6570\u636e\u67e5\u8be2_\u6d4b\u91cf\u9879\u76ee',
|
||
stationParam: '\u5de5\u4f4d\u53f7',
|
||
positionParam: '\u6d4b\u91cf\u4f4d\u7f6e',
|
||
stationAliases: ['\u5de5\u4f4d\u53f7', '\u6307\u5357\u5de5\u4f4d\u53f7', '\u5de5\u4f4d\u7f16\u53f7', 'opName', 'stationNumber'],
|
||
stationNameAliases: ['\u5de5\u4f4d\u540d\u79f0', '\u5de5\u5e8f\u540d\u79f0', '\u540d\u79f0', 'stationName'],
|
||
positionAliases: ['\u6d4b\u91cf\u4f4d\u7f6e', '\u68c0\u6d4b\u4f4d\u7f6e', '\u4f4d\u7f6e', 'measureName'],
|
||
itemAliases: ['\u6d4b\u91cf\u9879\u76ee', '\u68c0\u6d4b\u9879\u76ee', '\u6d4b\u91cf\u5185\u5bb9', '\u68c0\u6d4b\u5185\u5bb9', '\u9879\u76ee', 'measureContent']
|
||
}
|
||
|
||
function isQualityDimensionQuestion(question) {
|
||
const text = String(question || '').toLowerCase()
|
||
const hasAction = ['\u5217\u51fa', '\u67e5\u8be2', '\u663e\u793a', '\u6709\u54ea\u4e9b', '\u6709\u4ec0\u4e48', '\u83b7\u53d6'].some(word => text.indexOf(word) > -1)
|
||
const hasDimension = ['\u5de5\u4f4d\u53f7', '\u5de5\u4f4d', '\u6d4b\u91cf\u4f4d\u7f6e', '\u68c0\u6d4b\u4f4d\u7f6e', '\u6d4b\u91cf\u9879\u76ee', '\u68c0\u6d4b\u9879\u76ee', '\u68c0\u6d4b\u5185\u5bb9', '\u6d4b\u91cf\u5185\u5bb9'].some(word => text.indexOf(word) > -1)
|
||
return hasAction && hasDimension
|
||
}
|
||
|
||
function dimensionRows(rows, kind) {
|
||
const normalized = normalizeRows(rows, 500)
|
||
const mapped = normalized.map(row => {
|
||
if (kind === 'station') {
|
||
return {
|
||
stationNo: compatibleValue(row, QUALITY_DIMENSION_SAFE.stationAliases),
|
||
stationName: compatibleValue(row, QUALITY_DIMENSION_SAFE.stationNameAliases)
|
||
}
|
||
}
|
||
if (kind === 'position') {
|
||
return {
|
||
stationNo: compatibleValue(row, QUALITY_DIMENSION_SAFE.stationAliases),
|
||
testPosition: compatibleValue(row, QUALITY_DIMENSION_SAFE.positionAliases)
|
||
}
|
||
}
|
||
return {
|
||
stationNo: compatibleValue(row, QUALITY_DIMENSION_SAFE.stationAliases),
|
||
testPosition: compatibleValue(row, QUALITY_DIMENSION_SAFE.positionAliases),
|
||
testItem: compatibleValue(row, QUALITY_DIMENSION_SAFE.itemAliases)
|
||
}
|
||
}).filter(row => Object.keys(row).some(key => row[key]))
|
||
const seen = {}
|
||
return mapped.filter(row => {
|
||
const key = Object.keys(row).map(item => row[item]).join('|')
|
||
if (seen[key]) return false
|
||
seen[key] = true
|
||
return true
|
||
})
|
||
}
|
||
|
||
async function callMesQuery(name, params, options) {
|
||
const payload = mesPayload('11', name, params || [])
|
||
const response = await postJson(MES_BACKEND_URL, JSON.stringify(payload), null, options && options.timeoutMs)
|
||
const parsed = unwrapMesResponse(response)
|
||
return { payload, response, rows: parsed.rows, output: parsed.output }
|
||
}
|
||
|
||
function stationNumberFromQuestion(question) {
|
||
const text = String(question || '')
|
||
const exact = text.match(/(?:\u5de5\u4f4d\u53f7|\u5de5\u4f4d)\s*[:\uff1a=]?\s*([A-Za-z0-9_-]+)/i)
|
||
if (exact) return exact[1]
|
||
const op = text.match(/\b[A-Z]{1,4}\d{2,}[A-Z0-9_-]*\b/i)
|
||
if (op) return op[0]
|
||
return parseQuotedOrNamed(question || '', ['\u5de5\u4f4d\u53f7', '\u5de5\u4f4d', '\u5de5\u4f4d\u540d\u79f0']) || ''
|
||
}
|
||
|
||
function testPositionFromQuestion(question) {
|
||
const text = String(question || '')
|
||
const match = text.match(/(?:\u6d4b\u91cf\u4f4d\u7f6e|\u68c0\u6d4b\u4f4d\u7f6e|\u4f4d\u7f6e)\s*[:\uff1a=]?\s*([\s\S]+?)(?:\s*(?:\u7684)?(?:\u6d4b\u91cf\u9879\u76ee|\u68c0\u6d4b\u9879\u76ee|\u6d4b\u91cf\u5185\u5bb9|\u68c0\u6d4b\u5185\u5bb9|\u9879\u76ee)|[,\uff0c;\uff1b]|\s+\u5e76|\s+\u505a|$)/)
|
||
if (match) return match[1].trim()
|
||
return parseQuotedOrNamed(question || '', ['\u6d4b\u91cf\u4f4d\u7f6e', '\u68c0\u6d4b\u4f4d\u7f6e', '\u4f4d\u7f6e']) || ''
|
||
}
|
||
|
||
function testItemFromQuestion(question) {
|
||
const text = String(question || '')
|
||
const match = text.match(/(?:\u6d4b\u91cf\u9879\u76ee|\u68c0\u6d4b\u9879\u76ee|\u6d4b\u91cf\u5185\u5bb9|\u68c0\u6d4b\u5185\u5bb9|\u9879\u76ee)\s*[:\uff1a=]?\s*([\s\S]+?)(?:\u7684?SPC|\u7684?\u9884\u8b66|\u7684?\u62a5\u8b66|[,\uff0c;\uff1b]|\s+\u5e76|\s+\u505a|\s+\u7ed8\u5236|\s+spc|$)/i)
|
||
if (match) return match[1].replace(/\u7684$/, '').trim()
|
||
return parseQuotedOrNamed(question || '', ['\u6d4b\u91cf\u9879\u76ee', '\u68c0\u6d4b\u9879\u76ee', '\u6d4b\u91cf\u5185\u5bb9', '\u68c0\u6d4b\u5185\u5bb9', '\u9879\u76ee']) || ''
|
||
}
|
||
|
||
function shouldRouteQualityDimension(question) {
|
||
const text = String(question || '')
|
||
const asksQualityData = ['\u8d28\u91cf\u6570\u636e', '\u660e\u7ec6\u6570\u636e', '\u6700\u65b0\u6570\u636e', '\u67e5\u6570\u636e'].some(word => text.indexOf(word) > -1)
|
||
if (asksQualityData) return false
|
||
const hasAction = ['\u5217\u51fa', '\u67e5\u8be2', '\u663e\u793a', '\u6709\u54ea\u4e9b', '\u6709\u4ec0\u4e48', '\u83b7\u53d6'].some(word => text.indexOf(word) > -1)
|
||
const asksPositionOrItem = ['\u6d4b\u91cf\u4f4d\u7f6e', '\u68c0\u6d4b\u4f4d\u7f6e', '\u6d4b\u91cf\u9879\u76ee', '\u68c0\u6d4b\u9879\u76ee', '\u68c0\u6d4b\u5185\u5bb9', '\u6d4b\u91cf\u5185\u5bb9'].some(word => text.indexOf(word) > -1)
|
||
const compactText = text.replace(/\s+/g, '')
|
||
const asksStationList = ['\u5217\u51fa\u5de5\u4f4d\u53f7', '\u663e\u793a\u5de5\u4f4d\u53f7', '\u83b7\u53d6\u5de5\u4f4d\u53f7', '\u5de5\u4f4d\u53f7\u5217\u8868', '\u5de5\u4f4d\u5217\u8868'].some(word => compactText.indexOf(word) > -1) &&
|
||
['\u5217\u51fa', '\u663e\u793a', '\u6709\u54ea\u4e9b', '\u6709\u4ec0\u4e48', '\u83b7\u53d6'].some(word => text.indexOf(word) > -1)
|
||
return hasAction && (asksPositionOrItem || asksStationList)
|
||
}
|
||
|
||
function normalizeStationNumber(value) {
|
||
const text = String(value || '').trim()
|
||
if (!text) return ''
|
||
if (/^op/i.test(text)) return text.toUpperCase()
|
||
if (/^\d{3,5}$/.test(text)) {
|
||
const candidate = 'OP' + text
|
||
if (quickQualityStationRows().some(row => row.stationNo === candidate)) return candidate
|
||
}
|
||
return text
|
||
}
|
||
|
||
function quickQualityStationRows() {
|
||
return [
|
||
'OP3025', 'OP3030', 'OP3067', 'OP3076', 'OP3077', 'OP3090', 'OP3130', 'OP3150', 'OP3165', 'OP3170',
|
||
'OP3175', 'OP3192', 'OP3195', 'OP3220', 'OP3225', 'OP3230', 'OP3240', 'OP3267', 'OP3270', 'OP3272',
|
||
'OP3290', 'OP3291', 'OP3328', 'OP3387', 'OP3388', 'OP3395', 'OP3400', 'OP3410', 'OP3420', 'OP3421',
|
||
'OP4000', 'OP5002', 'OP5003', 'OP5004', 'OP5005', 'OP5006', 'OP5010', 'OP5011'
|
||
].map(stationNo => ({ stationNo, stationName: '' }))
|
||
}
|
||
|
||
async function queryQualityDimensions(args) {
|
||
const query = String((args && args.query) || '')
|
||
const stationNo = (args && (args.stationNumber || args.stationNo)) || stationNumberFromQuestion(query)
|
||
const testPosition = (args && (args.testPosition || args.measurePosition)) || testPositionFromQuestion(query)
|
||
const wantsItem = ['\u6d4b\u91cf\u9879\u76ee', '\u68c0\u6d4b\u9879\u76ee', '\u68c0\u6d4b\u5185\u5bb9', '\u6d4b\u91cf\u5185\u5bb9'].some(word => query.indexOf(word) > -1)
|
||
const wantsPosition = ['\u6d4b\u91cf\u4f4d\u7f6e', '\u68c0\u6d4b\u4f4d\u7f6e', '\u4f4d\u7f6e'].some(word => query.indexOf(word) > -1)
|
||
const kind = wantsItem ? 'item' : (wantsPosition && stationNo ? 'position' : 'station')
|
||
if (kind === 'station' && !(args && args.live === true)) {
|
||
const rows = quickQualityStationRows()
|
||
let html = '<h4>\u8d28\u91cf\u6570\u636e\u67e5\u8be2\u524d\u7f6e\u6761\u4ef6</h4>' + table([
|
||
{ key: 'name', label: '\u9879\u76ee' },
|
||
{ key: 'value', label: '\u5185\u5bb9' }
|
||
], [
|
||
{ name: '\u67e5\u8be2\u7c7b\u578b', value: '\u5de5\u4f4d\u53f7\u5217\u8868' },
|
||
{ name: '\u67e5\u8be2\u5165\u53e3', value: QUALITY_DIMENSION_SAFE.stationQuery },
|
||
{ name: '\u8bf4\u660e', value: '\u5217\u8868\u6765\u81ea\u9879\u76ee\u5df2\u6709\u8d28\u91cf\u6570\u636e\u5de5\u4f4d\u67e5\u8be2\u5165\u53e3\u9a8c\u8bc1\u7ed3\u679c\uff0c\u7528\u4e8e\u540e\u7eed\u67e5\u6d4b\u91cf\u4f4d\u7f6e\u548c\u6d4b\u91cf\u9879\u76ee\u3002' }
|
||
])
|
||
html += '<h4>\u5de5\u4f4d\u53f7\u5217\u8868</h4>' + table([{ key: 'stationNo', label: '\u5de5\u4f4d\u53f7' }, { key: 'stationName', label: '\u5de5\u4f4d\u540d\u79f0' }], rows)
|
||
html += '<h4>\u4e0b\u4e00\u6b65\u5efa\u8bae</h4>' + table([{ key: 'step', label: '\u6b65\u9aa4' }, { key: 'text', label: '\u8bf4\u660e' }], [
|
||
{ step: '1', text: '\u8f93\u5165\u201c\u5217\u51fa\u5de5\u4f4d\u53f7 OP3077 \u7684\u6d4b\u91cf\u4f4d\u7f6e\u201d\u67e5\u8be2\u8be5\u5de5\u4f4d\u7684\u68c0\u6d4b\u4f4d\u7f6e\u3002' },
|
||
{ step: '2', text: '\u518d\u8f93\u5165\u201c\u5217\u51fa\u5de5\u4f4d\u53f7 OP3077 \u6d4b\u91cf\u4f4d\u7f6e XXX \u7684\u6d4b\u91cf\u9879\u76ee\u201d\u67e5\u68c0\u6d4b\u5185\u5bb9\u3002' }
|
||
])
|
||
return { html, data: { kind, stationNo: '', testPosition: '', rows, rowCount: rows.length, payload: { name: QUALITY_DIMENSION_SAFE.stationQuery, param: '[]' }, output: [], cached: true } }
|
||
}
|
||
let result
|
||
let note = ''
|
||
try {
|
||
if (kind === 'item') {
|
||
result = await callMesQuery(QUALITY_DIMENSION_SAFE.itemQuery, [
|
||
[QUALITY_DIMENSION_SAFE.stationParam, stationNo],
|
||
[QUALITY_DIMENSION_SAFE.positionParam, testPosition]
|
||
], { timeoutMs: 5000 })
|
||
} else if (kind === 'position') {
|
||
result = await callMesQuery(QUALITY_DIMENSION_SAFE.positionQuery, [
|
||
[QUALITY_DIMENSION_SAFE.stationParam, stationNo]
|
||
], { timeoutMs: 5000 })
|
||
} else {
|
||
result = await callMesQuery(QUALITY_DIMENSION_SAFE.stationQuery, [], { timeoutMs: 5000 })
|
||
if (!Array.isArray(result.rows) || !result.rows.length) {
|
||
note = '\u4e13\u7528\u8d28\u91cf\u5de5\u4f4d\u53f7\u5165\u53e3\u672a\u8fd4\u56de\u6570\u636e\uff0c\u5df2\u81ea\u52a8\u56de\u9000\u5230 BOM \u5de5\u4f4d\u67e5\u8be2\u5165\u53e3\u3002'
|
||
result = await callMesQuery(QUALITY_DIMENSION_SAFE.bomStationQuery, [], { timeoutMs: 5000 })
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
html: '<h4>\u8d28\u91cf\u6570\u636e\u67e5\u8be2\u524d\u7f6e\u6761\u4ef6</h4>' + table([
|
||
{ key: 'name', label: '\u9879\u76ee' },
|
||
{ key: 'value', label: '\u5185\u5bb9' }
|
||
], [
|
||
{ name: '\u72b6\u6001', value: '\u67e5\u8be2\u5165\u53e3\u5df2\u5b9a\u4f4d\uff0c\u4f46 MESCommonBase.ashx \u8c03\u7528\u5931\u8d25\u3002' },
|
||
{ name: '\u67e5\u8be2\u7c7b\u578b', value: kind === 'station' ? '\u5de5\u4f4d\u53f7\u5217\u8868' : kind === 'position' ? '\u6d4b\u91cf\u4f4d\u7f6e\u5217\u8868' : '\u6d4b\u91cf\u9879\u76ee/\u68c0\u6d4b\u5185\u5bb9\u5217\u8868' },
|
||
{ name: '\u5de5\u4f4d\u53f7', value: stationNo || '\u672a\u6307\u5b9a' },
|
||
{ name: '\u6d4b\u91cf\u4f4d\u7f6e', value: testPosition || '\u672a\u6307\u5b9a' },
|
||
{ name: '\u9519\u8bef', value: error.message }
|
||
]),
|
||
data: { kind, stationNo, testPosition, rows: [], error: error.message }
|
||
}
|
||
}
|
||
const rows = dimensionRows(result.rows, kind)
|
||
let html = '<h4>\u8d28\u91cf\u6570\u636e\u67e5\u8be2\u524d\u7f6e\u6761\u4ef6</h4>' + table([
|
||
{ key: 'name', label: '\u9879\u76ee' },
|
||
{ key: 'value', label: '\u5185\u5bb9' }
|
||
], [
|
||
{ name: '\u67e5\u8be2\u7c7b\u578b', value: kind === 'station' ? '\u5de5\u4f4d\u53f7\u5217\u8868' : kind === 'position' ? '\u6d4b\u91cf\u4f4d\u7f6e\u5217\u8868' : '\u6d4b\u91cf\u9879\u76ee/\u68c0\u6d4b\u5185\u5bb9\u5217\u8868' },
|
||
{ name: '\u5de5\u4f4d\u53f7', value: stationNo || '\u672a\u6307\u5b9a' },
|
||
{ name: '\u6d4b\u91cf\u4f4d\u7f6e', value: testPosition || '\u672a\u6307\u5b9a' },
|
||
{ name: '\u67e5\u8be2\u5165\u53e3', value: result.payload.name },
|
||
{ name: '\u8bf4\u660e', value: note || '\u590d\u7528\u9879\u76ee\u5df2\u6709 CreateData -> MESCommonBase.ashx \u67e5\u8be2\u5165\u53e3\uff0c\u4e0d\u76f4\u8fde\u6570\u636e\u5e93\u3002' }
|
||
])
|
||
if (kind === 'station') {
|
||
html += '<h4>\u5de5\u4f4d\u53f7\u5217\u8868</h4>' + table([{ key: 'stationNo', label: '\u5de5\u4f4d\u53f7' }, { key: 'stationName', label: '\u5de5\u4f4d\u540d\u79f0' }], rows)
|
||
} else if (kind === 'position') {
|
||
html += '<h4>\u6d4b\u91cf\u4f4d\u7f6e\u5217\u8868</h4>' + table([{ key: 'stationNo', label: '\u5de5\u4f4d\u53f7' }, { key: 'testPosition', label: '\u6d4b\u91cf\u4f4d\u7f6e' }], rows)
|
||
} else {
|
||
html += '<h4>\u6d4b\u91cf\u9879\u76ee/\u68c0\u6d4b\u5185\u5bb9\u5217\u8868</h4>' + table([{ key: 'stationNo', label: '\u5de5\u4f4d\u53f7' }, { key: 'testPosition', label: '\u6d4b\u91cf\u4f4d\u7f6e' }, { key: 'testItem', label: '\u6d4b\u91cf\u9879\u76ee/\u68c0\u6d4b\u5185\u5bb9' }], rows)
|
||
}
|
||
html += '<h4>\u4e0b\u4e00\u6b65\u5efa\u8bae</h4>' + table([{ key: 'step', label: '\u6b65\u9aa4' }, { key: 'text', label: '\u8bf4\u660e' }], [
|
||
{ step: '1', text: '\u5148\u4f7f\u7528\u201c\u5217\u51fa\u5de5\u4f4d\u53f7\u201d\u83b7\u53d6\u53ef\u7528\u5de5\u4f4d\u3002' },
|
||
{ step: '2', text: '\u518d\u8f93\u5165\u201c\u5217\u51fa\u5de5\u4f4d\u53f7 XXX \u7684\u6d4b\u91cf\u4f4d\u7f6e\u201d\u83b7\u53d6\u8be5\u5de5\u4f4d\u53ef\u68c0\u6d4b\u7684\u4f4d\u7f6e\u3002' },
|
||
{ step: '3', text: '\u6700\u540e\u8f93\u5165\u201c\u5217\u51fa\u5de5\u4f4d\u53f7 XXX \u6d4b\u91cf\u4f4d\u7f6e YYY \u7684\u6d4b\u91cf\u9879\u76ee\u201d\uff0c\u518d\u7528\u8fd9\u4e9b\u6761\u4ef6\u67e5\u8be2\u8d28\u91cf\u6570\u636e\u6216\u7ed8\u5236 SPC \u56fe\u8868\u3002' }
|
||
])
|
||
return { html, data: { kind, stationNo, testPosition, rows, rowCount: rows.length, payload: result.payload, output: result.output } }
|
||
}
|
||
|
||
function htmlForWarningSummary(analysis) {
|
||
if (!analysis || typeof analysis !== 'object') {
|
||
return '<h4>预测预警结果综述</h4>' + table([{ key: 'name', label: '项目' }, { key: 'value', label: '内容' }], [
|
||
{ name: '状态', value: '当前对话中没有可用的 SPC 预测预警结果。' },
|
||
{ name: '建议', value: '请先输入“查询质量数据 工位号 ... 测量位置 ... 测量项目 ... 做SPC预测预警”,生成预警结果后再要求综述。' }
|
||
])
|
||
}
|
||
const summaryRows = [
|
||
{ name: '风险等级', value: analysis.level || '未知' },
|
||
{ name: '风险分数', value: analysis.score == null ? '未知' : analysis.score },
|
||
{ name: '分析对象', value: analysis.objectName || '未知' },
|
||
{ name: '样本数量', value: analysis.sampleCount == null ? '未知' : analysis.sampleCount },
|
||
{ name: '时间范围', value: analysis.timeRange || '未知' },
|
||
{ name: '当前 Cp', value: warningRound(Number(analysis.cp), 4) || '不可计算' },
|
||
{ name: '当前 Cpk', value: warningRound(Number(analysis.cpk), 4) || '不可计算' },
|
||
{ name: 'Cpk趋势', value: analysis.cpkTrend || '未知' },
|
||
{ name: '置信度', value: analysis.confidence || '未知' },
|
||
{ name: '综述', value: analysis.summary || '已根据当前质量数据完成 SPC 预测预警分析。' }
|
||
]
|
||
const hitRules = Array.isArray(analysis.rules) ? analysis.rules.filter(rule => rule && rule.hit === '是') : []
|
||
const suggestionRows = [
|
||
{ risk: '优先处理', suggestion: (analysis.level === '严重' || analysis.level === '预警') ? '优先复核命中的预警规则和最近异常工件,必要时进行隔离、复测和工艺确认。' : '当前未见高等级风险,建议保持监控并关注后续趋势变化。' },
|
||
{ risk: '过程能力', suggestion: Number(analysis.cpk) < 1.33 ? 'Cpk 低于 1.33 时应检查过程中心偏移、设备稳定性、夹具状态和关键工艺参数。' : '过程能力暂满足常规要求,可继续按现有频率监控。' },
|
||
{ risk: '数据粒度', suggestion: '建议按单一工位、单一测量位置、单一测量项目重新分析,减少混合数据导致的误判。' },
|
||
{ risk: '响应闭环', suggestion: '将命中规则、对应工件编号和时间范围记录到质量处置流程,形成复核、调整、验证的闭环。' }
|
||
]
|
||
let html = '<h4>预测预警结果综述</h4>' + table([{ key: 'name', label: '项目' }, { key: 'value', label: '内容' }], summaryRows)
|
||
html += '<h4>已触发规则</h4>' + table([{ key: 'name', label: '规则' }, { key: 'level', label: '等级' }, { key: 'score', label: '分数' }, { key: 'description', label: '说明' }], hitRules.length ? hitRules : [{ name: '无高风险规则', level: '正常', score: 0, description: '当前结果未命中高风险规则。' }])
|
||
html += '<h4>改进建议</h4>' + table([{ key: 'risk', label: '方向' }, { key: 'suggestion', label: '建议' }], suggestionRows)
|
||
return html
|
||
}
|
||
|
||
function qualityWarningCharts(records, analysis) {
|
||
if (!records.length) return []
|
||
const labels = records.map((item, index) => item.time || String(index + 1))
|
||
const values = records.map(item => item.value)
|
||
const markLine = []
|
||
if (!Number.isNaN(analysis.upper)) markLine.push({ name: 'USL', yAxis: analysis.upper })
|
||
if (!Number.isNaN(analysis.lower)) markLine.push({ name: 'LSL', yAxis: analysis.lower })
|
||
if (!Number.isNaN(analysis.target)) markLine.push({ name: '理论值', yAxis: analysis.target })
|
||
if (!Number.isNaN(analysis.mean)) markLine.push({ name: '平均值', yAxis: Number(analysis.mean.toFixed(4)) })
|
||
const trend = polishChart({ id: 'chart_spc_warning_trend_' + Date.now() + '_' + Math.floor(Math.random() * 10000), title: 'SPC智能预测预警-测量值趋势', option: { title: { text: 'SPC智能预测预警-测量值趋势', left: 'center' }, tooltip: { trigger: 'axis' }, grid: { left: 58, right: 28, top: 74, bottom: 58 }, xAxis: { type: 'category', data: labels, axisLabel: { rotate: 35 } }, yAxis: { type: 'value', scale: true }, series: [{ name: '测量值', type: 'line', smooth: true, data: values.map((value, index) => { const item = records[index]; const out = (!Number.isNaN(item.upper) && value > item.upper) || (!Number.isNaN(item.lower) && value < item.lower); return { value, itemStyle: { color: out ? '#dc2626' : '#2563eb' } } }), markLine: markLine.length ? { symbol: 'none', data: markLine } : undefined }] } }, ['#2563eb', '#dc2626', '#f97316'])
|
||
const capability = polishChart({ id: 'chart_spc_warning_capability_' + Date.now() + '_' + Math.floor(Math.random() * 10000), title: 'SPC智能预测预警-Cp/Cpk趋势', option: { title: { text: 'SPC智能预测预警-Cp/Cpk趋势', left: 'center' }, tooltip: { trigger: 'axis' }, legend: { top: 36 }, grid: { left: 58, right: 28, top: 78, bottom: 48 }, xAxis: { type: 'category', data: analysis.windows.map(item => item.name) }, yAxis: { type: 'value', scale: true }, series: [{ name: 'Cp', type: 'line', smooth: true, data: analysis.windows.map(item => Number.isNaN(item.cp) ? null : Number(item.cp.toFixed(4))) }, { name: 'Cpk', type: 'line', smooth: true, data: analysis.windows.map(item => Number.isNaN(item.cpk) ? null : { value: Number(item.cpk.toFixed(4)), itemStyle: { color: item.cpk < 1 ? '#dc2626' : '#16a34a' } }), markLine: { symbol: 'none', data: [{ name: '优秀线1.67', yAxis: 1.67 }, { name: '合格线1.33', yAxis: 1.33 }, { name: '预警线1.00', yAxis: 1 }, { name: '严重线0.67', yAxis: 0.67 }] } }] } }, ['#2563eb', '#16a34a', '#f97316', '#dc2626'])
|
||
return [trend, capability]
|
||
}
|
||
|
||
async function queryQualityWarningData(args) {
|
||
const displaySize = Math.min(Math.max(Number(args.displaySize || 50), 1), 50)
|
||
const analysisSize = Math.min(Math.max(Number(args.analysisSize || 150), displaySize), 150)
|
||
const queryResult = await queryQualityData(Object.assign({}, args, { limit: analysisSize, pageSize: analysisSize, timeoutMs: 15000 }))
|
||
const rawRows = queryResult.data && Array.isArray(queryResult.data.rawRows) && queryResult.data.rawRows.length ? queryResult.data.rawRows : ((queryResult.data && queryResult.data.rows) || [])
|
||
const records = qualityWarningRecords(rawRows).slice(-analysisSize)
|
||
const analysis = analyzeQualitySpcWarning(records, Object.assign({}, args, { displaySize, analysisSize }))
|
||
return { html: htmlForQualitySpcWarning(analysis, queryResult, records), charts: qualityWarningCharts(records, analysis), data: Object.assign({}, queryResult.data || {}, { rawRows, analysis, capabilityWindows: analysis.windows, confidence: analysis.confidence }) }
|
||
}
|
||
|
||
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), null, args && args.timeoutMs)
|
||
} 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_dimensions', description: 'List quality query dimensions: stations, measurement positions, and measurement items using existing MESCommonBase query entries.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, stationNumber: { type: 'string' }, stationNo: { type: 'string' }, testPosition: { type: 'string' }, measurePosition: { type: 'string' } } } },
|
||
{ 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: 'predict_quality_spc_warning', description: 'Run SPC warning prediction from existing quality data query results; analyze measurement trends and Cp/Cpk capability trends without direct database access.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, stationNumber: { type: 'string' }, testPosition: { type: 'string' }, testItem: { type: 'string' }, engineId: { type: 'string' }, startTime: { type: 'string' }, endTime: { type: 'string' }, analysisSize: { type: 'number' }, displaySize: { type: 'number' }, windowSize: { type: 'number' }, windowStep: { 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_dimensions') {
|
||
const result = await queryQualityDimensions(args || {})
|
||
return { text: JSON.stringify(result.data, null, 2), html: result.html, data: result.data }
|
||
}
|
||
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 === 'predict_quality_spc_warning') {
|
||
const result = await queryQualityWarningData(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 clientContext = body.context && typeof body.context === 'object' ? body.context : {}
|
||
if (isUsageHelpQuestion(question)) {
|
||
return send(res, 200, { html: htmlForAssistantUsage(), data: { type: 'usage-help' }, charts: [] })
|
||
}
|
||
if (shouldRouteQualityDimension(question)) {
|
||
const dimensionResult = await queryQualityDimensions({ query: question })
|
||
return send(res, 200, dimensionResult)
|
||
}
|
||
const navigation = navigationResponse(question)
|
||
if (navigation) return send(res, 200, navigation)
|
||
if (isWarningSummaryQuestion(question) && clientContext.lastSpcWarning && clientContext.lastSpcWarning.analysis) {
|
||
return send(res, 200, {
|
||
html: htmlForWarningSummary(clientContext.lastSpcWarning.analysis),
|
||
data: { contextUsed: 'lastSpcWarning', analysis: clientContext.lastSpcWarning.analysis }
|
||
})
|
||
}
|
||
if (isWarningSummaryQuestion(question)) {
|
||
return send(res, 200, {
|
||
html: htmlForWarningSummary(null),
|
||
data: { contextUsed: 'none' }
|
||
})
|
||
}
|
||
if (isSpcParameterQuestion(question) && clientContext.lastSpcWarning && clientContext.lastSpcWarning.analysis) {
|
||
return send(res, 200, {
|
||
html: htmlForSpcParameters(clientContext.lastSpcWarning.analysis, '上一轮SPC预测预警结果'),
|
||
data: { contextUsed: 'lastSpcWarning', analysis: clientContext.lastSpcWarning.analysis }
|
||
})
|
||
}
|
||
if (isSpcParameterQuestion(question)) {
|
||
const profile = QUALITY_DATA_QUERY_PROFILE
|
||
const warningResult = await queryQualityWarningData({ query: question, nameOrSql: profile && profile.nameOrSql, analysisSize: 150, displaySize: 50, windowSize: 25, windowStep: 10 })
|
||
const analysis = warningResult.data && warningResult.data.analysis
|
||
return send(res, 200, Object.assign({}, warningResult, {
|
||
html: htmlForSpcParameters(analysis, '本次质量数据查询结果') + htmlForQualityDetailRows((warningResult.data && warningResult.data.rows) || []),
|
||
charts: []
|
||
}))
|
||
}
|
||
if (!isFeatureQuestion(question) && shouldRouteQualityDimension(question)) {
|
||
const dimensionResult = await queryQualityDimensions({ query: question })
|
||
return send(res, 200, dimensionResult)
|
||
}
|
||
if (!isFeatureQuestion(question) && (shouldRouteQualityWarning(question) || isQualitySpcWarningQuestion(question))) {
|
||
const profile = QUALITY_DATA_QUERY_PROFILE
|
||
const warningResult = await queryQualityWarningData({ query: question, nameOrSql: profile && profile.nameOrSql, analysisSize: 150, displaySize: 50, windowSize: 25, windowStep: 10 })
|
||
const requestedCharts = qualityChartTypes(question)
|
||
if (requestedCharts.length) {
|
||
const chartRows = warningResult.data && Array.isArray(warningResult.data.rawRows) && warningResult.data.rawRows.length
|
||
? warningResult.data.rawRows
|
||
: ((warningResult.data && warningResult.data.rows) || [])
|
||
const normalCharts = qualityDataCharts(chartRows, question)
|
||
const warningCharts = Array.isArray(warningResult.charts) ? warningResult.charts : []
|
||
const chartRowsForHtml = normalCharts.concat(warningCharts).map(chart => ({
|
||
name: chart.title || (chart.option && chart.option.title && chart.option.title.text) || chart.id,
|
||
type: chart.id || '',
|
||
source: '同一批质量数据查询结果'
|
||
}))
|
||
const chartHtml = '<h4>图表输出</h4>' + table([
|
||
{ key: 'name', label: '图表' },
|
||
{ key: 'source', label: '数据来源' }
|
||
], chartRowsForHtml.length ? chartRowsForHtml : [{ name: '未绘制图表', source: '质量数据中没有可识别的数值型测量值' }])
|
||
return send(res, 200, Object.assign({}, warningResult, {
|
||
html: chartHtml + warningResult.html,
|
||
charts: normalCharts.concat(warningCharts)
|
||
}))
|
||
}
|
||
return send(res, 200, warningResult)
|
||
}
|
||
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) && shouldRouteQualityDataQuery(question)) {
|
||
const profile = QUALITY_DATA_QUERY_PROFILE
|
||
const queryResult = await queryQualityData({ query: question, nameOrSql: profile && profile.nameOrSql, limit: 50, pageSize: 50 })
|
||
return send(res, 200, queryResult)
|
||
}
|
||
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))
|
||
})
|