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' 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: [], spcCharts: [], queryHints: [] } } let knowledgeBase = readJson(KNOWLEDGE_FILE, emptyKnowledgeBase()) 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, ''') } function table(headers, rows) { const dataRows = Array.isArray(rows) ? rows : [] let html = '' headers.forEach(header => { html += '' }) html += '' if (!dataRows.length) { html += '' } else { dataRows.forEach(row => { html += '' headers.forEach(header => { const value = row && row[header.key] html += '' }) html += '' }) } html += '
' + escapeHtml(header.label) + '
无匹配数据
' + escapeHtml(Array.isArray(value) ? value.join(', ') : value) + '
' 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 '

' + escapeHtml(title) + '

' + table([{ key: 'message', label: '结果' }], [{ message: '无数据' }]) return '

' + escapeHtml(title) + '

' + table(keys.map(key => ({ key, label: key })), normalized) } 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 return Object.assign({ score }, row) }) .filter(row => row.score > 0) .sort((a, b) => b.score - a.score) .slice(0, limit || 8) } function isNavigationQuestion(question) { return /(打开|跳转|进入|导航|前往|去到|切换到)/.test(String(question || '')) || /查看.*页面/.test(String(question || '')) } function navigationResponse(question) { if (!isNavigationQuestion(question)) return null const candidates = routeCandidates(question, 8) if (!candidates.length) { return { html: table([{ key: '提示', label: '提示' }], [{ 提示: '未找到匹配页面,请换一个页面名或模块名。' }]), actions: [] } } const target = candidates[0] return { html: '

页面导航

' + table([ { key: 'title', label: '页面' }, { key: 'routePath', label: '路由' }, { key: 'view', label: '源码文件' } ], [target]), actions: [{ type: 'navigate', path: target.routePath, title: target.title, view: target.view }] } } function isQualityQuestion(question) { return /质量|SPC|巡检|条码|Andon|安灯|测量|工位|过程能力|趋势|直方|正态|排列|X-R|XR|X-S|XS/i.test(String(question || '')) } function listQualityModules(limit) { return (knowledgeBase.qualityModules || []).slice(0, limit || 50).map(row => ({ title: row.Title, routePath: row.RoutePath, domains: row.Domains, queryable: row.Queryable, view: row.View })) } function qualitySearchResponse(question) { const rows = pick(knowledgeBase.qualityModules, question, ['Title', 'View', 'Domains', 'QueryNames'], 12) return '

质量管理匹配模块

' + table([ { key: 'Title', label: '页面' }, { key: 'RoutePath', label: '路由' }, { key: 'Domains', label: '业务域' }, { key: 'Queryable', 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: '

SPC 查询条件不足

' + 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: '

SPC 接口调用失败

' + 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 = '

SPC 查询条件

' + table([{ key: 'name', label: '条件' }, { key: 'value', label: '值' }], conditionRows) html += '

SPC 统计摘要

' + table([{ key: 'name', label: '指标' }, { key: 'value', label: '值' }], summaryRows) html += htmlForRows('明细数据', rows, 20) html += '

数据来源

' + table([{ key: 'endpoint', label: '接口' }, { key: 'type', label: '类型' }, { key: 'url', label: '调用地址' }], sourceRows) if (!rows.length && typeof raw === 'string') { html += '

原始返回

' + escapeHtml(raw.slice(0, 1000)) + '
' } return { html, charts, data: { params, raw, rows, values, url } } } function postJson(url, payload, headers) { return new Promise((resolve, reject) => { const target = new URL(url) const client = target.protocol === 'https:' ? https : http const body = typeof payload === 'string' ? payload : JSON.stringify(payload) const 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', 'Content-Length': Buffer.byteLength(body) }, headers || {}) }, res => { let text = '' res.setEncoding('utf8') res.on('data', chunk => { text += chunk }) res.on('end', () => { try { resolve(JSON.parse(text)) } catch (error) { resolve({ raw: text, statusCode: res.statusCode }) } }) }) req.on('error', reject) req.write(body) req.end() }) } async function askDeepSeek(question, localHtml) { if (!DEEPSEEK_API_KEY) return localHtml const context = { matchedModules: routeCandidates(question, 8), qualityModules: pick(knowledgeBase.qualityModules, question, ['Title', 'View', 'Domains', 'QueryNames'], 8), spcCharts: pick(knowledgeBase.spcCharts, question, ['chartType', 'title', 'apiFile', 'endpoint', 'functions'], 8) } const system = [ '你是 WC-SPC MES-Manager_View 网页内置的 DeepSeekV4Pro AI 助手。', '使用中文回答,优先输出 HTML 表格。', '只能依据项目知识库和工具结果回答,不要编造数据。', '不要输出可执行 JavaScript;图表由系统结构化渲染。' ].join('\n') const response = await postJson(DEEPSEEK_BASE_URL + '/chat/completions', { model: DEEPSEEK_MODEL, messages: [ { role: 'system', content: system }, { role: 'user', content: '项目知识库匹配上下文:\n' + JSON.stringify(context, null, 2) }, { role: 'user', content: question } ], temperature: 0.2 }, { Authorization: 'Bearer ' + DEEPSEEK_API_KEY }) return response && response.choices && response.choices[0] && response.choices[0].message ? response.choices[0].message.content : localHtml } function localAnswer(question) { if (/项目|功能|模块|有哪些/.test(question)) { return '

项目功能概览

' + table([ { key: 'name', label: '项目' }, { key: 'value', label: '数量' } ], [ { name: '页面模块', value: knowledgeBase.modules.length }, { name: 'API 映射', value: knowledgeBase.apiMappings.length }, { name: '质量相关模块', value: knowledgeBase.qualityModules.length }, { name: 'SPC 图表类型', value: knowledgeBase.spcCharts.length } ]) + '

质量相关模块

' + table([ { key: 'title', label: '页面' }, { key: 'routePath', label: '路由' }, { key: 'domains', label: '业务域' } ], listQualityModules(12)) } if (isQualityQuestion(question)) return qualitySearchResponse(question) const rows = routeCandidates(question, 12) return '

项目知识库匹配结果

' + table([ { key: 'title', label: '页面' }, { key: 'routePath', label: '路由' }, { key: 'view', label: '源码文件' }, { key: 'callCount', label: '调用数' } ], rows) } 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: '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: '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: '

项目概览

' + 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 === '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 === 'list_spc_charts') { const rows = knowledgeBase.spcCharts || [] return { text: JSON.stringify(rows, null, 2), html: table([{ key: 'title', label: '图表' }, { key: 'chartType', label: '类型' }, { key: 'apiFile', label: 'API 文件' }, { key: 'endpoint', label: '后端接口' }], rows), data: rows } } if (name === 'spc_query_chart') { const result = await querySpcChart(args || {}) return { text: JSON.stringify(result.data, null, 2), html: result.html, data: result } } if (name === 'reload_project_index') { knowledgeBase = readJson(KNOWLEDGE_FILE, emptyKnowledgeBase()) return { text: 'reloaded', html: table([{ key: 'name', label: '项目' }, { key: 'value', label: '值' }], [{ name: '状态', value: '知识库已重新加载' }]), data: { ok: true } } } if (name === 'call_mes_backend') { if (!ALLOW_BACKEND_CALLS) { return { text: 'disabled', html: table([{ key: 'name', label: '项目' }, { key: 'value', label: '值' }], [{ name: '状态', value: '已禁用,避免 AI 直接修改业务数据' }]), data: { disabled: true } } } const response = await postJson(MES_BACKEND_URL, args.payload || {}) return { text: JSON.stringify(response, null, 2), html: htmlForRows('MES 后端返回', Array.isArray(response) ? response : [response], 30), data: response } } throw new Error('Unknown tool: ' + name) } async function handleMcp(body) { const id = body.id == null ? null : body.id try { if (body.method === 'initialize') { return { jsonrpc: '2.0', id, result: { protocolVersion: '2025-06-18', serverInfo: { name: 'wc-spc-mcp', version: '1.0.0' }, capabilities: { tools: {} } } } } if (body.method === 'tools/list') return { jsonrpc: '2.0', id, result: { tools } } if (body.method === 'tools/call') { const result = await toolResult(body.params && body.params.name, (body.params && body.params.arguments) || {}) return { jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: result.text }, { type: 'text', text: result.html }], structuredContent: result.data } } } return { jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } } } catch (error) { return { jsonrpc: '2.0', id, error: { code: -32000, message: error.message } } } } function parseBody(req) { return new Promise((resolve, reject) => { const chunks = [] req.on('data', chunk => { chunks.push(Buffer.from(chunk)) }) req.on('end', () => { try { const text = Buffer.concat(chunks).toString('utf8') resolve(text ? JSON.parse(text) : {}) } catch (error) { reject(error) } }) req.on('error', reject) }) } function send(res, status, payload) { const body = typeof payload === 'string' ? payload : JSON.stringify(payload) res.writeHead(status, { 'Content-Type': typeof payload === 'string' ? 'text/html; charset=utf-8' : 'application/json; charset=utf-8', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS' }) res.end(body) } const server = http.createServer(async (req, res) => { if (req.method === 'OPTIONS') return send(res, 204, '') try { if (req.method === 'GET' && req.url === '/health') { return send(res, 200, { ok: true, model: DEEPSEEK_MODEL, apiKeyConfigured: Boolean(DEEPSEEK_API_KEY), knowledgeBase: { loaded: Boolean(knowledgeBase.generatedAt), modules: knowledgeBase.modules.length, qualityModules: knowledgeBase.qualityModules.length, spcCharts: knowledgeBase.spcCharts.length }, endpoints: { mes: MES_BACKEND_URL, spc: SPC_BACKEND_URL } }) } if (req.method === 'GET' && req.url === '/api/tools') return send(res, 200, { tools }) if (req.method === 'POST' && req.url === '/mcp') return send(res, 200, await handleMcp(await parseBody(req))) if (req.method === 'POST' && req.url === '/api/assistant/chat') { const body = await parseBody(req) const question = String(body.question || '').trim() if (!question) return send(res, 400, { error: 'question is required' }) const navigation = navigationResponse(question) if (navigation) return send(res, 200, navigation) if (/图|趋势|直方|正态|过程能力|排列|X-R|XR|X-S|XS/i.test(question)) { return send(res, 200, await querySpcChart({ query: question })) } const localHtml = localAnswer(question) const html = await askDeepSeek(question, localHtml) 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)) })