Add HL MES manager with AI assistant service
This commit is contained in:
661
ai-mcp-service/scripts/build-knowledge-base.js
Normal file
661
ai-mcp-service/scripts/build-knowledge-base.js
Normal 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 才放开。',
|
||||
'- 前端登录态主要依赖 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(/<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)
|
||||
}
|
||||
Reference in New Issue
Block a user