Files
HL_MES_manager_ai/ai-mcp-service/scripts/build-knowledge-base.js

662 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const fs = require('fs')
const path = require('path')
const 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)
}