Files
WC-SPC/MES-Manager_View/mcp-server/build-knowledge-base.js
2026-05-27 20:57:27 +08:00

838 lines
33 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 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(/<!--[\s\S]*?-->/g, '')
.replace(/<script[\s\S]*?<\/script>/gi, '')
.replace(/<style[\s\S]*?<\/style>/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 = /<el-button\b([^>]*)>([\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 = /<el-table-column\b([^>]*)(?:\/>|>([\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()