Files
MES_Manage_View_V20/work/system-manual/verify-revised-manual.js

243 lines
12 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 JSZip = require('jszip')
const { DOMParser } = require('@xmldom/xmldom')
const { PNG } = require('pngjs')
const root = path.resolve(__dirname, '..', '..')
const docxPath = process.env.MES_MANUAL_DOCX
? path.resolve(process.env.MES_MANUAL_DOCX)
: path.resolve(root, 'doc', '大连元利流体技术有限公司MES系统用户操作手册_V1.1_2026-07-23.docx')
const catalog = JSON.parse(fs.readFileSync(path.resolve(__dirname, 'full-page-catalog.json'), 'utf8'))
const dialogAudit = JSON.parse(fs.readFileSync(path.resolve(__dirname, 'manual-dialog-audit.json'), 'utf8'))
const screenshotsDir = path.resolve(__dirname, 'full-screenshots')
function localName(node) {
return node && (node.localName || String(node.nodeName || '').split(':').pop())
}
function elementChildren(node, name) {
if (!node || !node.childNodes) return []
return [...node.childNodes].filter(child => child.nodeType === 1 && (!name || localName(child) === name))
}
function descendants(node, name, output = []) {
for (const child of elementChildren(node)) {
if (!name || localName(child) === name) output.push(child)
descendants(child, name, output)
}
return output
}
function textOf(node) {
return descendants(node, 't').map(item => item.textContent || '').join('').trim()
}
function firstDescendant(node, name) {
return descendants(node, name, [])[0] || null
}
function fail(message) {
throw new Error(message)
}
function imagesMatch(actual, expected) {
if (actual.equals(expected)) return true
const actualPng = PNG.sync.read(actual)
const expectedPng = PNG.sync.read(expected)
if (actualPng.width === expectedPng.width && actualPng.height === expectedPng.height) {
return actualPng.data.equals(expectedPng.data)
}
const actualRatio = actualPng.width / actualPng.height
const expectedRatio = expectedPng.width / expectedPng.height
if (Math.abs(actualRatio - expectedRatio) > 0.002) return false
// WPS downsamples embedded PNGs to their displayed size when saving. Compare
// sampled RGB values against a bilinear resize of the captured source image.
let difference = 0
let samples = 0
const stride = 2
for (let y = 0; y < actualPng.height; y += stride) {
const sourceY = (y + 0.5) * expectedPng.height / actualPng.height - 0.5
const y0 = Math.max(0, Math.floor(sourceY))
const y1 = Math.min(expectedPng.height - 1, y0 + 1)
const fractionY = sourceY - y0
for (let x = 0; x < actualPng.width; x += stride) {
const sourceX = (x + 0.5) * expectedPng.width / actualPng.width - 0.5
const x0 = Math.max(0, Math.floor(sourceX))
const x1 = Math.min(expectedPng.width - 1, x0 + 1)
const fractionX = sourceX - x0
for (let channel = 0; channel < 3; channel++) {
const topLeft = expectedPng.data[(y0 * expectedPng.width + x0) * 4 + channel]
const topRight = expectedPng.data[(y0 * expectedPng.width + x1) * 4 + channel]
const bottomLeft = expectedPng.data[(y1 * expectedPng.width + x0) * 4 + channel]
const bottomRight = expectedPng.data[(y1 * expectedPng.width + x1) * 4 + channel]
const expectedValue = (topLeft * (1 - fractionX) + topRight * fractionX) * (1 - fractionY) +
(bottomLeft * (1 - fractionX) + bottomRight * fractionX) * fractionY
difference += Math.abs(actualPng.data[(y * actualPng.width + x) * 4 + channel] - expectedValue)
samples++
}
}
}
return difference / samples < 2
}
function main() {
const zip = new JSZip(fs.readFileSync(docxPath))
const parser = new DOMParser()
const document = parser.parseFromString(zip.file('word/document.xml').asText(), 'application/xml')
const relationships = parser.parseFromString(zip.file('word/_rels/document.xml.rels').asText(), 'application/xml')
const body = firstDescendant(document.documentElement, 'body')
const bodyChildren = elementChildren(body)
const fullText = textOf(document.documentElement)
const relationshipTargets = new Map()
for (const relationship of elementChildren(relationships.documentElement, 'Relationship')) {
relationshipTargets.set(relationship.getAttribute('Id'), relationship.getAttribute('Target').replace(/^\.\//, ''))
}
const chapterByModule = {
'设备管理': 7,
'研发管理': 8,
'工艺管理': 9,
'计划排产': 10,
'生产管理': 11,
'异常提醒': 12,
'质量管理': 13,
'销售订单管理': 14
}
let verifiedScreenshots = 0
let verifiedActionTables = 0
for (const page of catalog) {
const section = `${chapterByModule[page.module]}.${page.pageIndex}`
const pageStart = bodyChildren.findIndex(node => localName(node) === 'p' && textOf(node) === `${section} ${page.title}`)
if (pageStart < 0) fail(`页面标题缺失:${section} ${page.title}`)
let pageEnd = bodyChildren.findIndex((node, index) => index > pageStart && localName(node) === 'p' && /^\d+\.\d+ /.test(textOf(node)))
if (pageEnd < 0) pageEnd = bodyChildren.length
const imageParagraph = bodyChildren.slice(pageStart, pageEnd).find(node => localName(node) === 'p' && firstDescendant(node, 'blip'))
if (!imageParagraph) fail(`页面截图缺失:${section} ${page.title}`)
const blip = firstDescendant(imageParagraph, 'blip')
const relationshipId = blip.getAttribute('r:embed') || blip.getAttributeNS('http://schemas.openxmlformats.org/officeDocument/2006/relationships', 'embed')
const target = relationshipTargets.get(relationshipId)
if (!target || !zip.file(`word/${target}`)) fail(`页面截图关系缺失:${section} ${page.title}`)
const embedded = zip.file(`word/${target}`).asNodeBuffer()
const expected = fs.readFileSync(path.resolve(screenshotsDir, page.screenshot))
if (!imagesMatch(embedded, expected)) fail(`页面截图未更新:${section} ${page.title}${target}`)
verifiedScreenshots++
const actionHeadingIndex = bodyChildren.findIndex((node, index) =>
index > pageStart && index < pageEnd && localName(node) === 'p' && textOf(node) === `${section}.3 功能按钮与操作`
)
const actionTable = actionHeadingIndex >= 0
? bodyChildren.slice(actionHeadingIndex + 1, pageEnd).find(node => localName(node) === 'tbl')
: null
const actionHeaders = actionTable ? elementChildren(elementChildren(actionTable, 'tr')[0], 'tc').map(textOf) : []
if (actionHeaders.join('|') !== '操作入口|执行结果|业务影响与注意事项') {
const subsectionHeadings = bodyChildren.slice(pageStart, pageEnd)
.filter(node => localName(node) === 'p' && textOf(node).startsWith(`${section}.`))
.map(textOf)
fail(`操作结果与影响表缺失:${section} ${page.title};子节:${subsectionHeadings.join(' | ')}`)
}
verifiedActionTables++
}
let verifiedDialogPages = 0
for (const item of dialogAudit.filter(page => page.dialogs.length)) {
const page = catalog.find(entry => entry.module === item.module && entry.title === item.title)
if (!page) fail(`弹窗审计页未纳入目录:${item.module}/${item.title}`)
const section = `${chapterByModule[item.module]}.${page.pageIndex}`
const pageStart = bodyChildren.findIndex(node => localName(node) === 'p' && textOf(node) === `${section} ${item.title}`)
let pageEnd = bodyChildren.findIndex((node, index) => index > pageStart && localName(node) === 'p' && /^\d+\.\d+ /.test(textOf(node)))
if (pageEnd < 0) pageEnd = bodyChildren.length
const pageText = bodyChildren.slice(pageStart, pageEnd).map(textOf).join('\n')
const hasDialogCoverage = pageText.includes('弹窗') || (section === '9.3' && pageText.includes('编辑生产工艺与保存时机'))
if (!hasDialogCoverage) fail(`弹窗说明缺失:${section} ${item.title}`)
verifiedDialogPages++
}
const requiredPhrases = [
'9.3.4 编辑生产工艺与保存时机',
'左侧工序列表',
'上移/下移',
'保存为标准工艺',
'替换该物料原有的标准工艺路线',
'不会自动生成生产任务',
'下发任务(单工序行)',
'操作入口',
'执行结果',
'业务影响与注意事项',
'10.3.4 可编辑字段与编辑弹窗',
'计划开始',
'计划完成',
'外协分组',
'修改后立即保存',
'编辑备注信息弹窗',
'上游业务数据',
'本页维护结果',
'下游业务影响'
]
for (const phrase of requiredPhrases) if (!fullText.includes(phrase)) fail(`关键内容缺失:${phrase}`)
const forbiddenPhrases = [
'页面路由',
'只读查询过程/接口',
'写入过程/接口',
'技术核对说明',
'本文档编制未实际执行',
'未产生数据',
'不产生数据',
'正式环境只读截图',
'后台过程',
'存储过程',
'核对内容后使用弹窗中的',
'保存提交'
]
const presentForbiddenPhrases = forbiddenPhrases.filter(phrase => fullText.includes(phrase))
if (presentForbiddenPhrases.length) fail(`仍含不应出现的措辞:${presentForbiddenPhrases.join('、')}`)
const implementationNames = [...new Set(catalog.flatMap(page => [
page.route,
...(page.readProcedures || []),
...(page.writeProcedures || [])
]).filter(Boolean))]
const leakedNames = implementationNames.filter(name => fullText.includes(name))
if (leakedNames.length) fail(`仍含内部路由或过程名称:${leakedNames.join('、')}`)
const processPopupCaptionIndex = bodyChildren.findIndex(node => localName(node) === 'p' && /^图 9\.3-2/.test(textOf(node)))
const processPopupImageParagraph = bodyChildren.slice(0, processPopupCaptionIndex).reverse()
.find(node => localName(node) === 'p' && firstDescendant(node, 'blip'))
if (processPopupCaptionIndex < 0 || !processPopupImageParagraph) fail('9.3 编辑生产工艺弹窗截图未嵌入文档')
const processPopupBlip = firstDescendant(processPopupImageParagraph, 'blip')
const processPopupRelationshipId = processPopupBlip.getAttribute('r:embed') || processPopupBlip.getAttributeNS('http://schemas.openxmlformats.org/officeDocument/2006/relationships', 'embed')
const processPopupTarget = relationshipTargets.get(processPopupRelationshipId)
const processPopupEntry = processPopupTarget && zip.file(`word/${processPopupTarget}`)
const processPopupExpected = fs.readFileSync(path.resolve(screenshotsDir, '03-03-process-edit-dialog.png'))
if (!processPopupEntry || !imagesMatch(processPopupEntry.asNodeBuffer(), processPopupExpected)) fail('9.3 编辑生产工艺弹窗截图内容不正确')
const popupCaptionIndex = bodyChildren.findIndex(node => localName(node) === 'p' && /^图 10\.3-2/.test(textOf(node)))
const popupImageParagraph = bodyChildren.slice(0, popupCaptionIndex).reverse().find(node => localName(node) === 'p' && firstDescendant(node, 'blip'))
if (popupCaptionIndex < 0 || !popupImageParagraph) fail('10.3 编辑弹窗截图未嵌入文档')
const popupBlip = firstDescendant(popupImageParagraph, 'blip')
const popupRelationshipId = popupBlip.getAttribute('r:embed') || popupBlip.getAttributeNS('http://schemas.openxmlformats.org/officeDocument/2006/relationships', 'embed')
const popupTarget = relationshipTargets.get(popupRelationshipId)
const popupEntry = popupTarget && zip.file(`word/${popupTarget}`)
const popupExpected = fs.readFileSync(path.resolve(screenshotsDir, '04-03-plan-edit-dialog.png'))
if (!popupEntry || !imagesMatch(popupEntry.asNodeBuffer(), popupExpected)) fail('10.3 编辑弹窗截图内容不正确')
console.log(`Verified screenshots: ${verifiedScreenshots}`)
console.log(`Verified action tables: ${verifiedActionTables}`)
console.log(`Verified dialog pages: ${verifiedDialogPages}`)
console.log('Forbidden wording: 0')
console.log('Internal routes/procedures: 0')
console.log('Section 9.3: complete')
console.log('Section 10.3: complete')
}
try {
main()
} catch (error) {
console.error(error.stack || error.message)
process.exitCode = 1
}