Update AI assistant
This commit is contained in:
@@ -411,6 +411,43 @@ 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,
|
||||
@@ -821,7 +858,7 @@ async function querySpcChart(args) {
|
||||
return { html, charts, data: { params, raw, rows, values, url } }
|
||||
}
|
||||
|
||||
function postJson(url, payload, headers) {
|
||||
function postJson(url, payload, headers, timeoutMs) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const target = new URL(url)
|
||||
const client = target.protocol === 'https:' ? https : http
|
||||
@@ -846,12 +883,25 @@ function postJson(url, payload, headers) {
|
||||
})
|
||||
})
|
||||
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
|
||||
@@ -1002,6 +1052,9 @@ 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),
|
||||
@@ -1018,6 +1071,9 @@ function buildStructuredQualityDataParams(args, profile) {
|
||||
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) {
|
||||
@@ -1316,6 +1372,737 @@ function qualityDataCharts(rows, question) {
|
||||
}).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)
|
||||
@@ -1353,7 +2140,7 @@ async function queryQualityData(args) {
|
||||
}]
|
||||
let response
|
||||
try {
|
||||
response = await postJson(MES_BACKEND_URL, JSON.stringify(payload))
|
||||
response = await postJson(MES_BACKEND_URL, JSON.stringify(payload), null, args && args.timeoutMs)
|
||||
} catch (error) {
|
||||
return {
|
||||
html: '<h4>质量数据查询入口已定位</h4>' + table([
|
||||
@@ -1500,7 +2287,9 @@ const tools = [
|
||||
{ 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: {} } },
|
||||
@@ -1549,10 +2338,18 @@ async function toolResult(name, 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 }
|
||||
@@ -1637,8 +2434,73 @@ const server = http.createServer(async (req, res) => {
|
||||
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 })
|
||||
@@ -1669,6 +2531,11 @@ const server = http.createServer(async (req, res) => {
|
||||
}, { 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 })
|
||||
|
||||
Reference in New Issue
Block a user