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

487 lines
18 KiB
JavaScript

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 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 modules = viewSummary.map(row => {
const calls = viewCalls.filter(call => call.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) : [inferObjectFromName(call.NameOrSql)].filter(Boolean),
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('|') : [],
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 => {
;(query.queryTables || []).forEach(table => {
if (!table) return
sqlTables.push({
table,
sourceFile: module.view,
line: query.line,
sql: query.kind === 'raw-sql' ? query.nameOrSql : '',
source: query.kind === 'raw-sql' ? 'raw-sql' : 'inferred-object',
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,
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)
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 + ' |',
'| 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('spcCharts=' + spcMappings.length)
}
main()