Add HL MES manager with AI assistant service

This commit is contained in:
2026-05-25 18:28:10 +08:00
commit 35f11c095d
881 changed files with 172675 additions and 0 deletions

View File

@@ -0,0 +1,661 @@
const fs = require('fs')
const path = require('path')
const childProcess = require('child_process')
const serviceDir = path.resolve(__dirname, '..')
const repoRoot = path.resolve(serviceDir, '..')
const frontendDir = path.join(repoRoot, '前端源码', 'HL_MES_manager')
const backendDir = path.join(repoRoot, 'MES_Manage_standard')
const projectDir = path.join(repoRoot, 'MesProject20250822')
const kbDir = path.join(serviceDir, 'knowledge')
const generatedDir = path.join(kbDir, 'generated')
ensureDir(kbDir)
ensureDir(generatedDir)
const capabilities = readJson(path.join(serviceDir, 'catalog', 'mes-capabilities.json'))
const frontendTaskMap = readJson(path.join(serviceDir, 'catalog', 'frontend-task-map.json'))
const apiCalls = extractApiCalls(frontendDir)
const vuePages = listFiles(path.join(frontendDir, 'src', 'views'), file => file.endsWith('.vue'))
const pageCatalog = extractPageCatalog(frontendDir, vuePages, apiCalls)
const apiFiles = listFiles(path.join(frontendDir, 'src', 'api'), file => file.endsWith('.js'))
const backendHandlers = listFiles(path.join(backendDir, 'submit'), file => file.endsWith('.ashx'))
const excelInventory = inspectExcelFiles([
path.join(projectDir, '数据库结构.xlsx'),
path.join(projectDir, 'MES基础数据.xlsx'),
path.join(projectDir, '基本变量.xlsx')
])
const docs = [
buildOverviewDoc(capabilities, vuePages, apiFiles, backendHandlers),
buildFeatureDoc(capabilities),
buildPageCatalogDoc(pageCatalog),
buildExportCatalogDoc(pageCatalog),
buildInterfaceDoc(apiCalls, backendHandlers),
buildFrontendTaskDoc(frontendTaskMap),
buildDatabaseDoc(excelInventory, apiCalls),
buildIntegrationDoc(capabilities),
buildOperationsDoc()
]
for (const doc of docs) {
fs.writeFileSync(path.join(kbDir, `${doc.id}.md`), doc.content, 'utf8')
}
const index = docs.map(doc => ({
id: doc.id,
title: doc.title,
category: doc.category,
tags: doc.tags,
file: `${doc.id}.md`,
summary: doc.summary
}))
fs.writeFileSync(path.join(kbDir, 'index.json'), JSON.stringify(index, null, 2), 'utf8')
fs.writeFileSync(path.join(generatedDir, 'api-calls.json'), JSON.stringify(apiCalls.map(item => ({
module: item.module,
operation: item.operation,
type: item.type,
name: item.name,
file: relative(repoRoot, item.file),
line: item.line
})), null, 2), 'utf8')
fs.writeFileSync(path.join(generatedDir, 'page-catalog.json'), JSON.stringify(pageCatalog.map(item => Object.assign({}, item, {
file: relative(repoRoot, item.file)
})), null, 2), 'utf8')
fs.writeFileSync(path.join(generatedDir, 'database-inventory.json'), JSON.stringify(excelInventory.map(item => Object.assign({}, item, {
path: relative(repoRoot, item.path)
})), null, 2), 'utf8')
console.log(`Knowledge base generated: ${kbDir}`)
console.log(`Documents: ${docs.length}`)
console.log(`API calls extracted: ${apiCalls.length}`)
function buildOverviewDoc(catalog, vuePages, apiFiles, handlers) {
return {
id: 'overview',
title: '系统总体知识',
category: 'architecture',
tags: ['系统架构', '前端', '后端', '模块'],
summary: 'HL_MES_manager 的技术架构、目录、模块数量和核心入口。',
content: [
'# 系统总体知识',
'',
'HL_MES_manager 是合力差速器智能产线 MES 管理端,前端为 Vue 2 + Element UI后端为 ASP.NET ASHX数据库为 SQL Server。',
'',
'## 关键目录',
'',
`- 前端源码:${catalog.frontend}`,
`- 后端发布目录:${catalog.backend}`,
`- 主接口:${catalog.mainEndpoint}`,
'- AI/MCP 服务ai-mcp-service',
'',
'## 规模',
'',
`- Vue 页面数量:${vuePages.length}`,
`- 前端 API 文件数量:${apiFiles.length}`,
`- 后端 ASHX 处理器数量:${handlers.length}`,
`- 功能模块数量:${catalog.modules.length}`,
'',
'## 模块清单',
'',
...catalog.modules.map(item => `- ${item.name}${item.features.join('、')}`),
''
].join('\n')
}
}
function buildFeatureDoc(catalog) {
const lines = ['# 软件功能知识库', '']
for (const module of catalog.modules) {
lines.push(`## ${module.name}`)
lines.push('')
lines.push(`路径:${module.paths.join('、')}`)
lines.push('')
lines.push('功能:')
for (const feature of module.features) {
lines.push(`- ${feature}`)
}
lines.push('')
}
return {
id: 'software-features',
title: '软件功能知识库',
category: 'feature',
tags: ['功能', '页面', '菜单', '业务模块'],
summary: '按业务模块整理前端页面和 MES 功能。',
content: lines.join('\n')
}
}
function buildPageCatalogDoc(pages) {
const grouped = groupBy(pages, item => item.module)
const lines = [
'# 页面级操作知识库',
'',
'该文档从 Vue 页面源码中抽取页面路径、按钮文本、接口调用、导出能力和表格字段线索,用于回答“在哪里操作、怎么查询、怎么导出”。',
''
]
Object.keys(grouped).sort().forEach(module => {
lines.push(`## ${module}`)
lines.push('')
grouped[module].forEach(page => {
lines.push(`### ${page.title || page.name}`)
lines.push('')
lines.push(`源码:${relative(repoRoot, page.file)}`)
lines.push(`路由推断:${page.route}`)
if (page.buttons.length) lines.push(`按钮:${page.buttons.join('、')}`)
if (page.exports.length) {
lines.push('导出能力:')
page.exports.forEach(item => lines.push(`- ${item.name}type=${item.type}line=${item.line}`))
}
if (page.calls.length) {
lines.push('主要接口/过程:')
page.calls.slice(0, 20).forEach(item => lines.push(`- ${item.name}type=${item.type || ''}line=${item.line}`))
}
if (page.tableColumns.length) lines.push(`表格字段线索:${page.tableColumns.slice(0, 30).join('、')}`)
lines.push('')
})
})
return {
id: 'page-operation-catalog',
title: '页面级操作知识库',
category: 'frontend',
tags: ['页面', '按钮', '导出', '查询', '操作'],
summary: `${pages.length} 个 Vue 页面抽取页面操作、按钮和接口线索。`,
content: lines.join('\n')
}
}
function buildExportCatalogDoc(pages) {
const exportPages = pages.filter(page => page.exports.length || page.buttons.some(button => /导出|下载|Excel|EXCEL/i.test(button)))
const lines = [
'# 导出能力知识库',
'',
'该文档整理前端页面中可导出 Excel/文件/报表的页面、按钮和后端过程。用户提出导出请求时优先参考此文档。',
''
]
exportPages.forEach(page => {
lines.push(`## ${page.title || page.name}`)
lines.push('')
lines.push(`模块:${page.module}`)
lines.push(`源码:${relative(repoRoot, page.file)}`)
lines.push(`路由推断:${page.route}`)
if (page.buttons.length) lines.push(`相关按钮:${page.buttons.filter(button => /导出|下载|Excel|EXCEL|报表/i.test(button)).join('、') || page.buttons.join('、')}`)
if (page.exports.length) {
lines.push('导出接口/过程:')
page.exports.forEach(item => lines.push(`- ${item.name}type=${item.type}line=${item.line}`))
}
lines.push('')
})
return {
id: 'export-capabilities',
title: '导出能力知识库',
category: 'frontend',
tags: ['导出', 'Excel', '报表', '下载'],
summary: `整理 ${exportPages.length} 个具备导出/下载能力的页面。`,
content: lines.join('\n')
}
}
function buildInterfaceDoc(apiCalls, handlers) {
const grouped = groupBy(apiCalls, item => item.module)
const lines = [
'# 接口与存储过程知识库',
'',
'前端主要通过 request.js 请求 MESCommonBase.ashx业务参数为 type、name、param。旧页面也大量使用 CreateData(type, name, param, pageSize, pageList)。',
'',
'## 后端 ASHX 入口',
'',
...handlers.map(file => `- ${relative(repoRoot, file)}`),
'',
'## 前端调用索引',
''
]
Object.keys(grouped).sort().forEach(module => {
lines.push(`### ${module}`)
lines.push('')
grouped[module].slice(0, 180).forEach(item => {
lines.push(`- ${item.operation || 'unknown'} | type=${item.type || ''} | ${item.name} | ${relative(repoRoot, item.file)}:${item.line}`)
})
if (grouped[module].length > 180) {
lines.push(`- ... 还有 ${grouped[module].length - 180} 条调用未在文档中展开,可检索 knowledge/index.json 或重新生成索引。`)
}
lines.push('')
})
return {
id: 'interfaces-and-procedures',
title: '接口与存储过程知识库',
category: 'interface',
tags: ['接口', '存储过程', 'ASHX', 'MESCommonBase', 'CreateData'],
summary: `从源码抽取 ${apiCalls.length} 条接口/存储过程调用。`,
content: lines.join('\n')
}
}
function buildFrontendTaskDoc(taskMap) {
const lines = [
'# 前端操作任务知识库',
'',
'该文档把用户常见说法映射到 HL_MES_manager 的真实前端模块、页面和导出接口AI 助手回答时应优先按这些页面操作路径给建议。',
''
]
taskMap.forEach(item => {
lines.push(`## ${item.page}`)
lines.push('')
lines.push(`关键词:${item.keywords.join('、')}`)
lines.push(`模块:${item.module}`)
lines.push(`页面:${item.page}`)
lines.push(`操作:${item.actions.join('、')}`)
lines.push(`过程/查询:${item.procedures.join('、')}`)
lines.push('源码路径:')
item.paths.forEach(file => lines.push(`- ${file}`))
lines.push('')
lines.push(item.answer)
lines.push('')
})
return {
id: 'frontend-task-map',
title: '前端操作任务知识库',
category: 'frontend',
tags: ['前端操作', '页面跳转', '导出', '系统日志', '报警日志'],
summary: '把用户常见请求映射到当前 MES 前端页面、源码路径和导出接口。',
content: lines.join('\n')
}
}
function buildDatabaseDoc(excelInventory, apiCalls) {
const objectNames = new Set()
apiCalls.forEach(call => {
extractDbTokens(call.name).forEach(name => objectNames.add(name))
})
const lines = [
'# 数据库知识库',
'',
'数据库主库配置为 MESBasicDB_HL。当前知识库从项目 Excel 资料和前端接口调用名中整理数据库对象线索。',
'',
'## Excel 资料',
''
]
excelInventory.forEach(item => {
lines.push(`### ${item.name}`)
lines.push('')
lines.push(`路径:${relative(repoRoot, item.path)}`)
lines.push(`状态:${item.status}`)
if (item.sheets && item.sheets.length) {
lines.push('工作表:')
item.sheets.forEach(sheet => {
lines.push(`- ${sheet.name}${sheet.rows || '未知'} 行,${sheet.columns || '未知'} 列;字段示例:${(sheet.headers || []).join('、')}`)
})
}
lines.push('')
})
lines.push('## 从接口调用推断的数据库/过程对象关键词')
lines.push('')
Array.from(objectNames).sort().slice(0, 300).forEach(name => {
lines.push(`- ${name}`)
})
return {
id: 'database-knowledge',
title: '数据库知识库',
category: 'database',
tags: ['数据库', 'SQL Server', '表', '存储过程', 'Excel'],
summary: '数据库资料、Excel 工作簿结构和从接口名推断的对象线索。',
content: lines.join('\n')
}
}
function buildIntegrationDoc(catalog) {
const integration = catalog.integration
return {
id: 'plc-mqtt-integration',
title: 'PLC 与 MQTT 集成知识库',
category: 'integration',
tags: ['PLC', 'MQTT', 'RFID', 'OP010', '质量保存'],
summary: '现场 PLC、RFID、MQTT、OP010、自动工位和 OPA 上料的集成要点。',
content: [
'# PLC 与 MQTT 集成知识库',
'',
'## MQTT',
'',
`- 订阅主题:${integration.mqtt.subscriptionTopic}`,
`- 发布主题:${integration.mqtt.publishTopic}`,
`- 用途:${integration.mqtt.usage.join('、')}`,
'',
'## PLC/RFID',
'',
`用途:${integration.plc.usage.join('、')}`,
'',
'关键点位:',
...integration.plc.signals.map(signal => `- ${signal}`),
'',
'## 常见排查',
'',
'- MES 不保存质量数据:检查 DBX587.0 是否置位、DBB750 是否有质量数据、MES 后端是否可用。',
'- PLC 等待保存完成:检查 MES 是否写 DBX11.3。',
'- OP010 不允许上线检查二维码、订单机型匹配、DBX500.3、DBX1.1。',
'- 物料数量异常:检查 OPA 上料完成信号和 MES 扣减逻辑。',
''
].join('\n')
}
}
function buildOperationsDoc() {
return {
id: 'operations-and-risk',
title: '运维与风险知识库',
category: 'operations',
tags: ['运维', '部署', '安全', '故障排查', '风险'],
summary: '前端、后端、AI 服务的部署检查和已知安全风险。',
content: [
'# 运维与风险知识库',
'',
'## AI/MCP 服务',
'',
'- 目录ai-mcp-service',
'- 后台启动:.\\start.ps1',
'- 状态检查:.\\status.ps1',
'- 停止服务:.\\stop.ps1',
'- 健康检查http://127.0.0.1:8787/health',
'- DeepSeek Key 配置ai-mcp-service/.env 中的 DEEPSEEK_API_KEY',
'',
'## 前端配置',
'',
'- 开发配置:前端源码/HL_MES_manager/static/config.js',
'- 发布配置:前端源码/HL_MES_manager/dist/static/config.js',
'- 后端地址baseURL',
'- AI 服务地址aiMcpURL',
'',
'## 后端配置',
'',
'- 主配置MES_Manage_standard/Web.config',
'- 主库MESBasicDB_HL',
'- 主入口submit/MESCommonBase.ashx',
'',
'## 安全风险',
'',
'- Web.config 存在明文数据库账号密码。',
'- CORS 当前允许任意来源。',
'- 通用接口支持 name/param 动态调用,应限制 AI 写入权限。',
'- AI 服务默认禁止写入类操作,只有 MES_AI_ALLOW_WRITE=1 才放开。',
'- 前端登录态主要依赖 Cookiesession 校验逻辑未启用。',
''
].join('\n')
}
}
function extractApiCalls(root) {
const files = listFiles(path.join(root, 'src'), file => file.endsWith('.js') || file.endsWith('.vue'))
const calls = []
files.forEach(file => {
const text = safeRead(file)
const lines = text.split(/\r?\n/)
lines.forEach((line, index) => {
const nameMatches = [
...line.matchAll(/name\s*:\s*['"`]([^'"`]+)['"`]/g),
...line.matchAll(/CreateData\(\s*['"`]([^'"`]+)['"`]\s*,\s*['"`]([^'"`]+)['"`]/g)
]
nameMatches.forEach(match => {
if (match[2]) {
calls.push({
file,
line: index + 1,
module: inferModule(root, file),
operation: 'CreateData',
type: match[1],
name: cleanup(match[2])
})
} else {
calls.push({
file,
line: index + 1,
module: inferModule(root, file),
operation: 'request',
type: '',
name: cleanup(match[1])
})
}
})
})
})
return calls.filter(item => item.name && item.name.length > 1 && item.name !== 'Dashboard')
}
function extractPageCatalog(root, vuePages, apiCalls) {
return vuePages.map(file => {
const text = safeRead(file)
const rel = relative(path.join(root, 'src', 'views'), file).replace(/\\/g, '/')
const parts = rel.split('/')
const module = parts[0] || 'root'
const name = parts.slice(0, -1).join('/') || path.basename(file, '.vue')
const title = inferPageTitle(text, name)
const calls = apiCalls
.filter(call => call.file === file)
.map(call => ({
operation: call.operation,
type: call.type,
name: call.name,
line: call.line
}))
const buttons = extractButtonTexts(text)
const tableColumns = extractTableColumns(text)
const exports = calls.filter(call => call.type === '2001' || /导出|报表|download|excel/i.test(call.name) || /导出|下载|Excel|EXCEL/.test(buttons.join(' ')))
return {
module,
name,
title,
file,
route: '/' + name.replace(/\/index$/, ''),
buttons,
tableColumns,
calls,
exports
}
})
}
function inferPageTitle(text, fallback) {
const titleMatch = text.match(/<h[1-6][^>]*>([^<]{2,40})<\/h[1-6]>/i)
if (titleMatch) return cleanup(titleMatch[1])
const nameMatch = text.match(/name\s*:\s*['"`]([^'"`]+)['"`]/)
if (nameMatch) return cleanup(nameMatch[1])
return fallback
}
function extractButtonTexts(text) {
const result = []
const buttonRegex = /<el-button[\s\S]*?>([\s\S]*?)<\/el-button>/g
let match
while ((match = buttonRegex.exec(text))) {
const raw = match[1]
.replace(/<[^>]+>/g, '')
.replace(/\{\{[\s\S]*?\}\}/g, '')
const value = cleanup(raw)
if (value && value.length <= 30) result.push(value)
}
return Array.from(new Set(result)).slice(0, 30)
}
function extractTableColumns(text) {
const result = []
const labelRegex = /<el-table-column[\s\S]*?label\s*=\s*['"]([^'"]+)['"][\s\S]*?>/g
let match
while ((match = labelRegex.exec(text))) {
const value = cleanup(match[1])
if (value) result.push(value)
}
return Array.from(new Set(result)).slice(0, 80)
}
function inspectExcelFiles(files) {
const xlsx = loadXlsx()
return files.map(file => {
if (!fs.existsSync(file)) {
return { name: path.basename(file), path: file, status: '文件不存在' }
}
if (xlsx) {
try {
const workbook = xlsx.readFile(file, { cellDates: false })
const sheets = workbook.SheetNames.map(name => {
const sheet = workbook.Sheets[name]
const ref = sheet['!ref'] ? xlsx.utils.decode_range(sheet['!ref']) : null
const rows = ref ? ref.e.r + 1 : 0
const columns = ref ? ref.e.c + 1 : 0
const matrix = xlsx.utils.sheet_to_json(sheet, { header: 1, blankrows: false })
const headers = []
matrix.slice(0, 5).forEach(row => {
row.forEach(value => {
if (value !== undefined && value !== null && String(value).trim() && headers.length < 12) {
headers.push(String(value).trim())
}
})
})
return { name, rows, columns, headers }
})
return { name: path.basename(file), path: file, status: 'ok', sheets }
} catch (error) {
return {
name: path.basename(file),
path: file,
status: 'xlsx 读取失败',
error: error.message
}
}
}
const script = [
'import json, sys',
'try:',
' import openpyxl',
'except Exception as e:',
' print(json.dumps({"status":"openpyxl unavailable","error":str(e)}, ensure_ascii=False)); sys.exit(0)',
'path=sys.argv[1]',
'wb=openpyxl.load_workbook(path, read_only=True, data_only=True)',
'sheets=[]',
'for ws in wb.worksheets:',
' rows=ws.max_row',
' cols=ws.max_column',
' headers=[]',
' for row in ws.iter_rows(min_row=1, max_row=min(rows, 5), values_only=True):',
' for val in row:',
' if val is not None and str(val).strip() and len(headers)<12:',
' headers.append(str(val).strip())',
' sheets.append({"name":ws.title,"rows":rows,"columns":cols,"headers":headers})',
'print(json.dumps({"status":"ok","sheets":sheets}, ensure_ascii=False))'
].join('\n')
try {
const output = childProcess.execFileSync('python', ['-c', script, file], {
encoding: 'utf8',
windowsHide: true,
timeout: 20000
})
const result = JSON.parse(output)
return Object.assign({ name: path.basename(file), path: file }, result)
} catch (error) {
return {
name: path.basename(file),
path: file,
status: '读取失败',
error: error.message
}
}
})
}
function loadXlsx() {
const candidates = [
path.join(frontendDir, 'node_modules', 'xlsx'),
'xlsx'
]
for (const candidate of candidates) {
try {
return require(candidate)
} catch (error) {
// try next candidate
}
}
return null
}
function extractDbTokens(text) {
const tokens = []
String(text || '').split(/[\s,()\[\]{};]+/).forEach(part => {
const value = part.trim()
if (!value) return
if (/[\u4e00-\u9fa5]/.test(value) && value.length >= 3) {
tokens.push(value)
}
})
return tokens
}
function inferModule(root, file) {
const rel = relative(root, file).replace(/\\/g, '/')
const parts = rel.split('/')
const viewsIndex = parts.indexOf('views')
if (viewsIndex >= 0 && parts[viewsIndex + 1]) return parts[viewsIndex + 1]
const apiIndex = parts.indexOf('api')
if (apiIndex >= 0 && parts[apiIndex + 1]) return parts[apiIndex + 1]
return parts[0] || 'root'
}
function listFiles(dir, predicate) {
if (!fs.existsSync(dir)) return []
const result = []
const stack = [dir]
while (stack.length) {
const current = stack.pop()
const entries = fs.readdirSync(current, { withFileTypes: true })
entries.forEach(entry => {
const fullPath = path.join(current, entry.name)
if (entry.isDirectory()) {
if (entry.name !== 'node_modules' && entry.name !== 'dist') stack.push(fullPath)
} else if (!predicate || predicate(fullPath)) {
result.push(fullPath)
}
})
}
return result.sort()
}
function groupBy(items, getKey) {
return items.reduce((result, item) => {
const key = getKey(item)
if (!result[key]) result[key] = []
result[key].push(item)
return result
}, {})
}
function readJson(file) {
return JSON.parse(fs.readFileSync(file, 'utf8'))
}
function safeRead(file) {
try {
return fs.readFileSync(file, 'utf8')
} catch (error) {
return ''
}
}
function cleanup(value) {
return String(value || '').replace(/\s+/g, ' ').trim()
}
function ensureDir(dir) {
fs.mkdirSync(dir, { recursive: true })
}
function relative(from, file) {
return path.relative(from, file)
}