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 才放开。', '- 前端登录态主要依赖 Cookie,session 校验逻辑未启用。', '' ].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(/]*>([^<]{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 = /([\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 = //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) }