const fs = require('fs') const path = require('path') const ROOT = path.resolve(__dirname, '..') const DOCS_DIR = path.join(ROOT, 'docs') const OUT_FILE = path.join(DOCS_DIR, 'wc-spc-knowledge-base.json') function ensureDir(dir) { if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }) } function walk(dir, predicate, result) { result = result || [] if (!fs.existsSync(dir)) return result fs.readdirSync(dir, { withFileTypes: true }).forEach(entry => { const fullPath = path.join(dir, entry.name) if (entry.isDirectory()) { if (entry.name === 'node_modules' || entry.name === 'dist') return walk(fullPath, predicate, result) } else if (!predicate || predicate(fullPath)) { result.push(fullPath) } }) return result } function readText(file) { try { return fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '') } catch (error) { return '' } } function rel(file) { return path.relative(ROOT, file).replace(/\//g, '\\') } function lineNumber(text, index) { return text.slice(0, index).split(/\r?\n/).length } function csvEscape(value) { const text = String(value == null ? '' : value) return '"' + text.replace(/"/g, '""') + '"' } function writeCsv(file, headers, rows) { const body = [headers.map(csvEscape).join(',')] rows.forEach(row => { body.push(headers.map(header => csvEscape(row[header])).join(',')) }) fs.writeFileSync(path.join(DOCS_DIR, file), body.join('\n'), 'utf8') } function unique(values) { return Array.from(new Set(values.filter(Boolean))) } function normalizeSlash(value) { return String(value || '').replace(/\//g, '\\') } function moduleFromView(view) { const match = normalizeSlash(view).match(/^src\\views\\([^\\]+)/) return match ? match[1] : '' } function viewToRoutePath(view) { const normalized = normalizeSlash(view) const match = normalized.match(/^src\\views\\(.+)\\index\.vue$/i) if (!match) return '' return '/' + match[1].replace(/\\/g, '/') } function displayTitleFromView(view) { const normalized = normalizeSlash(view) const match = normalized.match(/^src\\views\\(.+?)\\index\.vue$/i) if (!match) return normalized.replace(/^src\\views\\/, '').replace(/\\/g, ' / ') return match[1].replace(/\\/g, ' / ') } function keywordsFromText(text) { const raw = String(text || '') .replace(/[^\w\u4e00-\u9fa5]+/g, ' ') .split(/\s+/) .map(item => item.trim()) .filter(item => item && item.length > 1) const grams = [] raw.forEach(item => { if (/[\u4e00-\u9fa5]/.test(item) && item.length > 2) { for (let size = 2; size <= Math.min(4, item.length); size++) { for (let index = 0; index <= item.length - size; index++) { grams.push(item.substr(index, size)) } } } }) return unique(raw.concat(grams)).slice(0, 120) } function inferAction(name) { const text = String(name || '') if (/删除|delete/i.test(text)) return '删除' if (/修改|编辑|更新|update/i.test(text)) return '修改' if (/增加|新增|添加|insert|add/i.test(text)) return '增加' if (/导入|上传|upload/i.test(text)) return '导入' if (/导出|下载|download|excel/i.test(text)) return '导出' if (/select|查询|获取|列表|综合|分页|统计|明细|检索/i.test(text)) return '查询' return '' } function isQueryAction(action, type, name) { if (/删除|修改|增加|导入|上传|提交|审核|执行|启用|禁用/.test(action)) return false if (/^\s*select\b/i.test(String(name || ''))) return true return action === '查询' || type === '11' || type === '2001' || type === '3' } function inferDomain(name, fallback) { const text = String(name || '') if (text.indexOf('_') > -1) return text.split('_')[0] if (text.indexOf('SPC') > -1) return 'SPC分析' if (text.indexOf('质量') > -1) return '质量管理' return fallback || '' } function extractTables(sql) { const tables = [] const re = /\b(?:from|join)\s+((?:\[?[A-Za-z0-9_\u4e00-\u9fa5]+\]?\.)?\[?[A-Za-z0-9_\u4e00-\u9fa5]+\]?)/gi let match while ((match = re.exec(String(sql || '')))) { tables.push(match[1].replace(/\[/g, '').replace(/\]/g, '')) } return unique(tables) } function inferObjectFromName(name) { const value = String(name || '').trim() if (!value || /^\s*select\b/i.test(value)) return '' return value .replace(/_查询数据.*$/, '') .replace(/_查询.*$/, '') .replace(/_分页.*$/, '') .replace(/_列表.*$/, '') .replace(/_综合.*$/, '') .replace(/_统计.*$/, '') .replace(/_增加.*$/, '') .replace(/_修改.*$/, '') .replace(/_删除.*$/, '') } function getAttr(attrs, name) { const re = new RegExp('(?:^|\\s)(?::?' + name + '|v-bind:' + name + ')\\s*=\\s*([\'"])([\\s\\S]*?)\\1', 'i') const match = String(attrs || '').match(re) return match ? match[2].replace(/\s+/g, ' ').trim() : '' } function textFromTemplate(value) { return String(value || '') .replace(//g, '') .replace(//gi, '') .replace(//gi, '') .replace(/<[^>]+>/g, ' ') .replace(/\{\{|\}\}/g, ' ') .replace(/\s+/g, ' ') .trim() } function extractClickMethod(value) { const methods = [] const re = /([A-Za-z_$\u4e00-\u9fa5][\w$\u4e00-\u9fa5]*)\s*\(/g let match while ((match = re.exec(String(value || '')))) methods.push(match[1]) return methods.filter(item => !/^(if|for|while|switch|confirm|parseInt|Number|String)$/.test(item)).pop() || '' } function findBalancedBlock(text, openBraceIndex) { if (openBraceIndex < 0) return '' let depth = 0 let quote = '' let escaped = false for (let i = openBraceIndex; i < text.length; i++) { const ch = text[i] if (quote) { if (escaped) { escaped = false } else if (ch === '\\') { escaped = true } else if (ch === quote) { quote = '' } continue } if (ch === '"' || ch === "'" || ch === '`') { quote = ch continue } if (ch === '{') depth++ if (ch === '}') { depth-- if (depth === 0) return text.slice(openBraceIndex + 1, i) } } return '' } function extractOptionsBlock(text, name) { const re = new RegExp('\\b' + name + '\\s*:\\s*{', 'g') const match = re.exec(text) if (!match) return '' return findBalancedBlock(text, match.index + match[0].lastIndexOf('{')) } function extractLifecycleBlock(text, name) { const patterns = [ new RegExp('\\b' + name + '\\s*\\([^)]*\\)\\s*{', 'g'), new RegExp('\\b' + name + '\\s*:\\s*function\\s*\\([^)]*\\)\\s*{', 'g') ] for (let i = 0; i < patterns.length; i++) { const match = patterns[i].exec(text) if (match) return findBalancedBlock(text, match.index + match[0].lastIndexOf('{')) } return '' } function extractButtons(text) { const rows = [] const re = /]*)>([\s\S]*?)<\/el-button>/gi let match while ((match = re.exec(text))) { const attrs = match[1] const label = textFromTemplate(match[2]) || getAttr(attrs, 'title') || getAttr(attrs, 'icon') || '按钮' const click = getAttr(attrs, '@click') || getAttr(attrs, 'click') || getAttr(attrs, '@click.native.prevent') || getAttr(attrs, '@click.native') rows.push({ label, action: inferAction(label + ' ' + click) || '操作', click, method: extractClickMethod(click), icon: getAttr(attrs, 'icon'), type: getAttr(attrs, 'type') }) } return unique(rows.map(row => JSON.stringify(row))).map(row => JSON.parse(row)).slice(0, 40) } function extractFilterControls(text) { const rows = [] const re = /<(el-date-picker|el-select|el-input|el-cascader|el-checkbox|el-radio-group|el-switch|el-input-number)\b([^>]*)/gi let match while ((match = re.exec(text))) { const attrs = match[2] const model = getAttr(attrs, 'v-model') || getAttr(attrs, 'model') const label = getAttr(attrs, 'placeholder') || getAttr(attrs, 'start-placeholder') || getAttr(attrs, 'end-placeholder') || model || match[1] rows.push({ control: match[1], label, model, type: getAttr(attrs, 'type'), clearable: /\sclearable(\s|>|$)/i.test(attrs), filterable: /\sfilterable(\s|>|$)/i.test(attrs) }) } return unique(rows.map(row => JSON.stringify(row))).map(row => JSON.parse(row)).slice(0, 40) } function extractTableFields(text) { const rows = [] const re = /]*)(?:\/>|>([\s\S]*?)<\/el-table-column>)/gi let match while ((match = re.exec(text))) { const attrs = match[1] const body = match[2] || '' const rowRefs = [] const refRe = /scope\.row\.([A-Za-z0-9_$\u4e00-\u9fa5]+)/g let refMatch while ((refMatch = refRe.exec(body))) rowRefs.push(refMatch[1]) const prop = getAttr(attrs, 'prop') || unique(rowRefs).join('|') const label = getAttr(attrs, 'label') || prop || textFromTemplate(body).slice(0, 30) if (!label && !prop) continue rows.push({ label, prop, width: getAttr(attrs, 'width') || getAttr(attrs, 'min-width'), fixed: getAttr(attrs, 'fixed') }) } return unique(rows.map(row => JSON.stringify(row))).map(row => JSON.parse(row)).slice(0, 80) } function extractMethodProfiles(text) { const block = extractOptionsBlock(text, 'methods') if (!block) return [] const rows = [] const skip = /^(if|for|while|switch|catch|then|map|filter|forEach|setTimeout|setInterval)$/ const re = /(?:^|\n)\s*([A-Za-z_$\u4e00-\u9fa5][\w$\u4e00-\u9fa5]*)\s*(?:\([^)]*\)\s*{|:\s*function\s*\([^)]*\)\s*{)/g let match while ((match = re.exec(block))) { const name = match[1] if (skip.test(name)) continue rows.push({ name, action: inferAction(name), line: lineNumber(text, text.indexOf(match[0].trim())) }) } return unique(rows.map(row => JSON.stringify(row))).map(row => JSON.parse(row)).slice(0, 80) } function extractLifecycleProfiles(text) { return ['created', 'mounted'].map(name => { const block = extractLifecycleBlock(text, name) const calls = [] const re = /this\.([A-Za-z_$\u4e00-\u9fa5][\w$\u4e00-\u9fa5]*)\s*\(/g let match while ((match = re.exec(block))) calls.push(match[1]) return { hook: name, calls: unique(calls) } }).filter(item => item.calls.length) } function riskLevelFromCalls(calls, buttons) { const text = calls.concat(buttons || []).map(item => [item.Action, item.Type, item.NameOrSql, item.action, item.label].join(' ')).join(' ') if (/删除|delete/i.test(text)) return '包含删除操作' if (/修改|编辑|更新|增加|新增|导入|上传|保存|type.?12/i.test(text)) return '包含写入操作' if (/导出|Excel|下载/i.test(text)) return '包含导出操作' return '只读查询为主' } function buildModuleFeatureProfiles(viewCalls, viewImports, apiMappings) { const views = walk(path.join(ROOT, 'src', 'views'), file => /\.vue$/i.test(file)).map(rel).sort() return views.map(view => { const fullPath = path.join(ROOT, view) const text = readText(fullPath) const calls = viewCalls.filter(row => row.View === view) const imports = viewImports.filter(row => row.View === view) const apiFiles = unique(imports.map(row => row.ApiFile)) const apiFunctions = unique(apiFiles.flatMap(file => apiMappings .filter(row => normalizeSlash(row.ApiFile) === normalizeSlash(file)) .map(row => row.FunctionName))) .filter(name => new RegExp('\\b' + name + '\\s*\\(').test(text)) const buttons = extractButtons(text) const filters = extractFilterControls(text) const tableFields = extractTableFields(text) const methods = extractMethodProfiles(text) const lifecycle = extractLifecycleProfiles(text) const actions = unique(buttons.map(row => row.action).concat(calls.map(row => row.Action))).filter(Boolean) const backendCalls = calls.map(row => ({ source: row.Source, type: row.Type, action: row.Action, domain: row.Domain, nameOrSql: row.NameOrSql, line: Number(row.Line || 0), role: row.Source === 'RawSQL' ? 'Raw SQL 查询' : 'MESCommonBase 查询入口' })) const title = displayTitleFromView(view) const description = title + ' 页面支持 ' + (actions.length ? actions.join('、') : '页面查看') + ';主要控件 ' + (filters.length ? filters.map(row => row.label).slice(0, 8).join('、') : '无显式筛选控件') + ';表格字段 ' + (tableFields.length ? tableFields.map(row => row.label).slice(0, 10).join('、') : '未识别到表格字段') + '。' return { view, module: moduleFromView(view), title, routePath: viewToRoutePath(view), filters, buttons, tableFields, lifecycle, methods, backendCalls, apiFiles, apiFunctions, actions, riskLevel: riskLevelFromCalls(calls, buttons), description, keywords: keywordsFromText([view, title, actions.join(' '), filters.map(row => row.label).join(' '), buttons.map(row => row.label).join(' '), tableFields.map(row => row.label).join(' '), backendCalls.map(row => row.nameOrSql).join(' ')].join(' ')) } }) } function buildQualityDataQueryProfiles(moduleFeatureProfiles) { const assembly = moduleFeatureProfiles.find(profile => normalizeSlash(profile.view) === 'src\\views\\QualityAssurance\\AssemblyQualitydataQuery\\index.vue') if (!assembly) return [] return [{ key: 'assembly-quality-data', title: '质量数据查询', routePath: assembly.routePath, view: assembly.view, module: assembly.module, method: 'selectDate', type: '11', nameOrSql: '质量数据_发动机质量数据_各个工位_视图_发动机型号_综合查询', exportType: '2001', exportNameOrSql: '质量数据查询_综合查询新', description: '按时间、工位、工件编号、订货号、订货号ID、机型号、工单ID、工单号、是否合格、测量位置、测量项目查询发动机各工位质量数据;无筛选条件时按全时间范围返回最新 200 条。', defaultStrategy: '无查询条件时使用 1900-01-01 00:00:00 至 2099-12-31 23:59:59、全部筛选_ischeck=0、PageCurrent=1、PageSize=200,并按生产日期等时间字段倒序展示最新 200 条。', latestSortFields: ['生产日期', '操作时间', '完成时间', '到达时间', '离开时间', '时间'], defaultUseWhen: ['查询质量数据', '质量数据查询', '发动机质量数据', '工件质量数据', '测量值查询'], parameters: [ { name: '开始时间', source: 'dateTime[0]', arg: 'startTime', defaultValue: '1900-01-01 00:00:00', required: true }, { name: '结束时间', source: 'dateTime[1]', arg: 'endTime', defaultValue: '2099-12-31 23:59:59', required: true }, { name: '工位号_ischeck', source: 'stationNumberValue', arg: 'stationNumber', checkFor: '工位号' }, { name: '工位号', source: 'stationNumberValue', arg: 'stationNumber' }, { name: 'PageCurrent', source: 'pageCurrent', arg: 'pageCurrent', defaultValue: 1 }, { name: 'PageSize', source: 'pageSize', arg: 'pageSize', defaultValue: 200 }, { name: 'PageCount', source: 'output', defaultValue: '1111', type: 'int', output: '1' }, { name: 'ItemCount', source: 'output', defaultValue: '1111', type: 'int', output: '1' }, { name: '工件编号_ischeck', source: 'engineId', arg: 'engineId', checkFor: '工件编号' }, { name: '工件编号', source: 'engineId', arg: 'engineId' }, { name: '订货号_ischeck', source: 'orderCode', arg: 'orderCode', checkFor: '订货号' }, { name: '订货号', source: 'orderCode', arg: 'orderCode' }, { name: '订货号ID_ischeck', source: 'orderCodeId', arg: 'orderCodeId', checkFor: '订货号ID' }, { name: '订货号ID', source: 'orderCodeId', arg: 'orderCodeId' }, { name: '机型号_ischeck', source: 'engineType', arg: 'engineType', checkFor: '机型号' }, { name: '机型号', source: 'engineType', arg: 'engineType' }, { name: '工单ID_ischeck', source: 'workOrderId', arg: 'workOrderId', checkFor: '工单ID' }, { name: '工单ID', source: 'workOrderId', arg: 'workOrderId' }, { name: '工单号_ischeck', source: 'workOrderCode', arg: 'workOrderCode', checkFor: '工单号' }, { name: '工单号', source: 'workOrderCode', arg: 'workOrderCode' }, { name: '是否合格', source: 'isOk', arg: 'isOk', defaultValue: '0' }, { name: '测量位置_ischeck', source: 'testPosition', arg: 'testPosition', checkFor: '测量位置' }, { name: '测量位置', source: 'testPosition', arg: 'testPosition' }, { name: '测量项目_ischeck', source: 'testItem', arg: 'testItem', checkFor: '测量项目' }, { name: '测量项目', source: 'testItem', arg: 'testItem' } ], tableFields: assembly.tableFields, filters: assembly.filters, backendCalls: assembly.backendCalls, keywords: keywordsFromText([assembly.title, assembly.routePath, assembly.description, '查询质量数据 质量数据查询 发动机质量数据 工件质量数据 测量值查询'].join(' ')) }] } function extractApiMappings() { const apiRoots = [ path.join(ROOT, 'src', 'api'), path.join(ROOT, 'src', 'assets', 'img', 'api') ] const rows = [] apiRoots.forEach(root => { walk(root, file => /\.js$/i.test(file)).forEach(file => { const text = readText(file) const relative = rel(file) const endpointMatches = [] const endpointRe = /['"`]([^'"`]*\/submit\/([^'"`?]+\.ashx)\?type=([^'"`&]+)[^'"`]*)['"`]/gi let endpointMatch while ((endpointMatch = endpointRe.exec(text))) { endpointMatches.push({ line: lineNumber(text, endpointMatch.index), endpoint: endpointMatch[2], type: endpointMatch[3], url: endpointMatch[1] }) } const functionRe = /export\s+function\s+([A-Za-z0-9_$\u4e00-\u9fa5]+)\s*\(([^)]*)\)/g let fnMatch while ((fnMatch = functionRe.exec(text))) { const line = lineNumber(text, fnMatch.index) const nearest = endpointMatches .map(item => Object.assign({ distance: Math.abs(item.line - line) }, item)) .sort((a, b) => a.distance - b.distance)[0] const action = inferAction((nearest && nearest.type) || fnMatch[1]) rows.push({ ApiFile: relative, Line: line, FunctionName: fnMatch[1], Params: fnMatch[2].replace(/\s+/g, ' '), Endpoint: nearest ? nearest.endpoint : '', Type: nearest ? nearest.type : '', Action: action, Domain: inferDomain((nearest && nearest.endpoint) || relative, moduleFromView(relative)), ApiNameOrSql: nearest ? nearest.type : fnMatch[1], InferredObject: inferObjectFromName((nearest && nearest.type) || fnMatch[1]), ParamExpression: fnMatch[2].replace(/\s+/g, ' ') }) } }) }) return rows } function extractViewCalls() { const rows = [] const imports = [] walk(path.join(ROOT, 'src', 'views'), file => /\.vue$/i.test(file)).forEach(file => { const text = readText(file) const relative = rel(file) const moduleName = moduleFromView(relative) const importRe = /from\s+['"]@\/(api\/[^'"]+)['"]/g let importMatch while ((importMatch = importRe.exec(text))) { imports.push({ View: relative, ApiFile: ('src\\' + importMatch[1]).replace(/\//g, '\\'), Line: lineNumber(text, importMatch.index) }) } const createDataRe = /CreateData\s*\(\s*['"]([^'"]*)['"]\s*,\s*(['"`])([\s\S]*?)\2/g let match while ((match = createDataRe.exec(text))) { const name = match[3].replace(/\s+/g, ' ').trim() const action = inferAction(name) rows.push({ View: relative, ViewModule: moduleName, RoutePath: viewToRoutePath(relative), Line: lineNumber(text, match.index), Source: 'CreateData', Type: match[1], Action: action, Domain: inferDomain(name, moduleName), NameOrSql: name, Queryable: isQueryAction(action, match[1], name) }) } const rawSqlRe = /(['"`])\s*(select\s+[\s\S]{10,}?)\1/gi while ((match = rawSqlRe.exec(text))) { const sql = match[2].replace(/\s+/g, ' ').trim() if (!/\bfrom\b/i.test(sql)) continue rows.push({ View: relative, ViewModule: moduleName, RoutePath: viewToRoutePath(relative), Line: lineNumber(text, match.index), Source: 'RawSQL', Type: '3', Action: '查询', Domain: 'RawSQL', NameOrSql: sql, Queryable: true }) } }) return { rows, imports } } function buildSpcMappings(apiMappings, viewCalls) { const known = [ { chartType: 'basicTrend', title: '基本趋势图', apiFile: 'src\\api\\SPCanalysis\\basicTrendTap.js', endpoint: 'QualityData_TrendPictureBasic.ashx' }, { chartType: 'sampleTrend', title: '样本趋势图', apiFile: 'src\\api\\SPCanalysis\\sampleTrendChart.js', endpoint: 'QualityData_TrendPicture.ashx' }, { chartType: 'histogram', title: '直方图', apiFile: 'src\\api\\SPCanalysis\\histogram.js', endpoint: 'QualityData_Histogram.ashx' }, { chartType: 'normalDistribution', title: '正态分布图', apiFile: 'src\\api\\SPCanalysis\\normal_distribution.js', endpoint: 'QualityData_NormalDistribution.ashx' }, { chartType: 'processCapability', title: '过程能力分析', apiFile: 'src\\api\\SPCanalysis\\processCapabilityAnalysis.js', endpoint: 'QualityData_NormalDistribution.ashx' }, { chartType: 'pareto', title: '排列图', apiFile: 'src\\api\\SPCanalysis\\arrangeChart.js', endpoint: 'QualityData_Pareto.ashx' }, { chartType: 'xr', title: 'X-R 控制图', apiFile: 'src\\api\\SPCanalysis\\QualityData_XR.js', endpoint: 'QualityData_XR.ashx' }, { chartType: 'xs', title: 'X-S 控制图', apiFile: 'src\\api\\SPCanalysis\\QualityData_XS.js', endpoint: 'QualityData_XS.ashx' } ] return known.map(item => { const apiRows = apiMappings.filter(row => normalizeSlash(row.ApiFile) === item.apiFile) const views = unique(viewCalls.filter(row => normalizeSlash(row.View).indexOf('SearchData') > -1 && row.NameOrSql.indexOf('SPC') > -1).map(row => row.View)) return Object.assign({}, item, { functions: unique(apiRows.map(row => row.FunctionName)).join('|'), types: unique(apiRows.map(row => row.Type)).join('|'), views: views.join('|') }) }) } function buildQualityModules(viewCalls) { const map = {} viewCalls.forEach(row => { const text = [row.View, row.Domain, row.NameOrSql].join(' ') const quality = /Quality|质量|SPC|Andon|巡检|条码|LineQuality|QualityData/i.test(text) if (!quality) return if (!map[row.View]) { map[row.View] = { View: row.View, Module: row.ViewModule, RoutePath: row.RoutePath, Title: displayTitleFromView(row.View), Domains: [], QueryNames: [], Queryable: false } } map[row.View].Domains.push(row.Domain) if (row.Queryable) { map[row.View].Queryable = true map[row.View].QueryNames.push(row.NameOrSql) } }) return Object.keys(map).sort().map(key => { const item = map[key] item.Domains = unique(item.Domains).join('|') item.QueryNames = unique(item.QueryNames).join('|') return item }) } function main() { ensureDir(DOCS_DIR) const apiMappings = extractApiMappings() const viewData = extractViewCalls() const viewCalls = viewData.rows const viewImports = viewData.imports const summaryMap = {} viewCalls.forEach(row => { if (!summaryMap[row.View]) { summaryMap[row.View] = { View: row.View, ViewModule: row.ViewModule, RoutePath: row.RoutePath, Count: 0, Domains: [], Names: [] } } summaryMap[row.View].Count++ summaryMap[row.View].Domains.push(row.Domain) summaryMap[row.View].Names.push(row.NameOrSql) }) const viewSummary = Object.keys(summaryMap).sort().map(key => { const item = summaryMap[key] item.Domains = unique(item.Domains).join('|') item.Names = unique(item.Names).join('|') return item }) const rawSqlRows = [] viewCalls.filter(row => row.Source === 'RawSQL').forEach(row => { rawSqlRows.push({ ApiFile: row.View, Line: row.Line, Tables: extractTables(row.NameOrSql).join('|'), Sql: row.NameOrSql }) }) const spcMappings = buildSpcMappings(apiMappings, viewCalls) const qualityModules = buildQualityModules(viewCalls) const moduleFeatureProfiles = buildModuleFeatureProfiles(viewCalls, viewImports, apiMappings) const qualityDataQueries = buildQualityDataQueryProfiles(moduleFeatureProfiles) const modules = viewSummary.map(row => { const calls = viewCalls.filter(call => call.View === row.View) const featureProfile = moduleFeatureProfiles.find(profile => profile.view === row.View) const primaryQueries = calls.filter(call => call.Queryable).map(call => ({ kind: call.Source === 'RawSQL' ? 'raw-sql' : 'create-data', view: call.View, line: Number(call.Line || 0), source: call.Source, type: call.Type, action: call.Action, domain: call.Domain, nameOrSql: call.NameOrSql, queryTables: call.Source === 'RawSQL' ? extractTables(call.NameOrSql) : [], backendQueryName: call.Source === 'RawSQL' ? '' : call.NameOrSql, inferredObject: call.Source === 'RawSQL' ? '' : inferObjectFromName(call.NameOrSql), keywords: keywordsFromText([call.View, call.Domain, call.NameOrSql].join(' ')) })) return { module: row.ViewModule, view: row.View, title: displayTitleFromView(row.View), routePath: row.RoutePath, callCount: Number(row.Count || calls.length || 0), domains: row.Domains ? row.Domains.split('|') : [], functionNames: row.Names ? row.Names.split('|') : [], featureProfile, calls, primaryQueries, queryable: primaryQueries.length > 0, queryTables: unique(primaryQueries.flatMap(query => query.queryTables || [])), qualityRelated: qualityModules.some(item => item.View === row.View), keywords: keywordsFromText([row.ViewModule, row.View, row.Domains, row.Names].join(' ')) } }) const sqlTables = [] rawSqlRows.forEach(row => { row.Tables.split('|').filter(Boolean).forEach(table => { sqlTables.push({ table, sourceFile: row.ApiFile, line: row.Line, sql: row.Sql, source: 'raw-sql', queryable: true, keywords: keywordsFromText([table, row.ApiFile, row.Sql].join(' ')) }) }) }) modules.forEach(module => { module.primaryQueries.forEach(query => { if (query.kind !== 'raw-sql') return ;(query.queryTables || []).forEach(table => { if (!table) return sqlTables.push({ table, sourceFile: module.view, line: query.line, sql: query.nameOrSql, source: 'raw-sql', queryable: true, keywords: keywordsFromText([table, module.view, query.nameOrSql].join(' ')) }) }) }) }) const tableMap = {} sqlTables.forEach(item => { if (!tableMap[item.table]) tableMap[item.table] = Object.assign({}, item, { references: [] }) tableMap[item.table].references.push({ sourceFile: item.sourceFile, line: item.line, source: item.source, sql: item.sql }) }) const dedupedTables = Object.keys(tableMap).sort().map(key => tableMap[key]) const knowledgeBase = { generatedAt: new Date().toISOString(), project: 'WC-SPC MES-Manager_View', modules, apiMappings: apiMappings.map(row => ({ apiFile: row.ApiFile, line: Number(row.Line || 0), functionName: row.FunctionName, params: row.Params, endpoint: row.Endpoint, type: row.Type, action: row.Action, domain: row.Domain, apiNameOrSql: row.ApiNameOrSql, inferredObject: row.InferredObject, queryable: isQueryAction(row.Action, row.Type, row.ApiNameOrSql), keywords: keywordsFromText([row.ApiFile, row.FunctionName, row.Endpoint, row.Type].join(' ')) })), sqlTables: dedupedTables, qualityModules, moduleFeatureProfiles, qualityDataQueries, spcCharts: spcMappings, queryHints: modules.map(item => ({ kind: 'module', key: item.view, title: item.title, keywords: item.keywords, tableCandidates: item.queryTables })) } writeCsv('api-name-mapping.csv', ['ApiFile', 'Line', 'FunctionName', 'Params', 'Endpoint', 'Type', 'Action', 'Domain', 'ApiNameOrSql', 'InferredObject', 'ParamExpression'], apiMappings) writeCsv('view-api-imports.csv', ['View', 'ApiFile', 'Line'], viewImports) writeCsv('view-database-calls.csv', ['View', 'ViewModule', 'RoutePath', 'Line', 'Source', 'Type', 'Action', 'Domain', 'NameOrSql', 'Queryable'], viewCalls) writeCsv('view-database-calls-summary.csv', ['View', 'ViewModule', 'RoutePath', 'Count', 'Domains', 'Names'], viewSummary) writeCsv('raw-sql-table-mapping.csv', ['ApiFile', 'Line', 'Tables', 'Sql'], rawSqlRows) writeCsv('spc-chart-mapping.csv', ['chartType', 'title', 'apiFile', 'endpoint', 'functions', 'types', 'views'], spcMappings) writeCsv('quality-module-mapping.csv', ['View', 'Module', 'RoutePath', 'Title', 'Domains', 'QueryNames', 'Queryable'], qualityModules) writeCsv('quality-data-query-profiles.csv', [ 'Key', 'Title', 'RoutePath', 'View', 'Type', 'NameOrSql', 'ExportType', 'ExportNameOrSql', 'Description', 'Parameters', 'TableFields' ], qualityDataQueries.map(profile => ({ Key: profile.key, Title: profile.title, RoutePath: profile.routePath, View: profile.view, Type: profile.type, NameOrSql: profile.nameOrSql, ExportType: profile.exportType, ExportNameOrSql: profile.exportNameOrSql, Description: profile.description, Parameters: profile.parameters.map(row => [row.name, row.arg, row.defaultValue, row.output].filter(value => value != null && value !== '').join(':')).join('|'), TableFields: profile.tableFields.map(row => [row.label, row.prop].filter(Boolean).join(':')).join('|') }))) writeCsv('module-feature-profiles.csv', [ 'View', 'Module', 'RoutePath', 'Title', 'Description', 'Actions', 'Filters', 'Buttons', 'TableFields', 'Lifecycle', 'Methods', 'BackendCalls', 'ApiFiles', 'ApiFunctions', 'RiskLevel' ], moduleFeatureProfiles.map(profile => ({ View: profile.view, Module: profile.module, RoutePath: profile.routePath, Title: profile.title, Description: profile.description, Actions: profile.actions.join('|'), Filters: profile.filters.map(row => [row.label, row.model, row.control].filter(Boolean).join(':')).join('|'), Buttons: profile.buttons.map(row => [row.label, row.method, row.action].filter(Boolean).join(':')).join('|'), TableFields: profile.tableFields.map(row => [row.label, row.prop].filter(Boolean).join(':')).join('|'), Lifecycle: profile.lifecycle.map(row => row.hook + ':' + row.calls.join('/')).join('|'), Methods: profile.methods.map(row => [row.name, row.action].filter(Boolean).join(':')).join('|'), BackendCalls: profile.backendCalls.map(row => [row.type, row.action, row.nameOrSql].filter(Boolean).join(':')).join('|'), ApiFiles: profile.apiFiles.join('|'), ApiFunctions: profile.apiFunctions.join('|'), RiskLevel: profile.riskLevel }))) fs.writeFileSync(OUT_FILE, JSON.stringify(knowledgeBase, null, 2), 'utf8') const report = [ '# WC-SPC 项目静态分析', '', '生成时间:' + new Date().toLocaleString('zh-CN', { hour12: false }), '', '## 结论', '', '- 项目为 Vue 2 + Element UI 前端和 ASP.NET Web Site 后端组合。', '- 前端通用业务接口通过 `src/utils/request.js` 调用 `MESCommonBase.ashx`。', '- SPC 专项接口位于 `src/api/SPCanalysis`,后端对应 `QualityData_*.ashx`。', '- 本文件由 `mcp-server/build-knowledge-base.js` 自动生成,用于 AI MCP 服务。', '', '## 统计', '', '| 项目 | 数量 |', '| --- | ---: |', '| 页面查询调用 | ' + viewCalls.length + ' |', '| 页面汇总 | ' + viewSummary.length + ' |', '| API 映射 | ' + apiMappings.length + ' |', '| Raw SQL | ' + rawSqlRows.length + ' |', '| 质量相关模块 | ' + qualityModules.length + ' |', '| 质量数据查询画像 | ' + qualityDataQueries.length + ' |', '| 页面功能画像 | ' + moduleFeatureProfiles.length + ' |', '| SPC 图表类型 | ' + spcMappings.length + ' |' ].join('\n') fs.writeFileSync(path.join(DOCS_DIR, 'wc-spc-static-analysis.md'), report, 'utf8') console.log('Knowledge base generated: ' + OUT_FILE) console.log('modules=' + modules.length) console.log('apiMappings=' + apiMappings.length) console.log('qualityModules=' + qualityModules.length) console.log('qualityDataQueries=' + qualityDataQueries.length) console.log('moduleFeatureProfiles=' + moduleFeatureProfiles.length) console.log('spcCharts=' + spcMappings.length) } main()