const fs = require('fs') const path = require('path') const JSZip = require('jszip') const { DOMParser, XMLSerializer } = require('@xmldom/xmldom') const root = path.resolve(__dirname, '..', '..') const docxPath = path.resolve(root, 'doc', '大连元利流体技术有限公司MES系统用户操作手册_V1.1_2026-07-23.docx') const backupDir = path.resolve(__dirname, 'backups') const backupPath = path.resolve(backupDir, '大连元利流体技术有限公司MES系统用户操作手册_V1.1_2026-07-23_修改前备份.docx') const stagingPath = path.resolve(__dirname, 'revised-manual.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 actionAudit = JSON.parse(fs.readFileSync(path.resolve(__dirname, 'manual-action-audit.json'), 'utf8')) const screenshotsDir = path.resolve(__dirname, 'full-screenshots') const NS = { w: 'http://schemas.openxmlformats.org/wordprocessingml/2006/main', r: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships', a: 'http://schemas.openxmlformats.org/drawingml/2006/main', rel: 'http://schemas.openxmlformats.org/package/2006/relationships', xml: 'http://www.w3.org/XML/1998/namespace' } 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 createElement(document, name) { return document.createElementNS(NS.w, `w:${name}`) } function setParagraphText(document, paragraph, text) { const pPr = elementChildren(paragraph, 'pPr')[0] || null const sourceRPr = firstDescendant(paragraph, 'rPr') for (const child of [...paragraph.childNodes]) { if (child !== pPr) paragraph.removeChild(child) } const run = createElement(document, 'r') if (sourceRPr) run.appendChild(sourceRPr.cloneNode(true)) const textNode = createElement(document, 't') if (/^\s|\s$/.test(text)) textNode.setAttributeNS(NS.xml, 'xml:space', 'preserve') textNode.appendChild(document.createTextNode(text)) run.appendChild(textNode) paragraph.appendChild(run) return paragraph } function setCellText(document, cell, text) { let paragraph = elementChildren(cell, 'p')[0] if (!paragraph) { paragraph = createElement(document, 'p') cell.appendChild(paragraph) } setParagraphText(document, paragraph, text) for (const extra of elementChildren(cell, 'p').slice(1)) cell.removeChild(extra) } function cellTexts(row) { return elementChildren(row, 'tc').map(cell => textOf(cell)) } function setRowValues(document, row, values) { const cells = elementChildren(row, 'tc') for (let index = 0; index < cells.length; index++) { setCellText(document, cells[index], values[index] || '') } } function makeParagraph(document, template, text) { return setParagraphText(document, template.cloneNode(true), text) } function makeTable(document, template, rows) { const table = template.cloneNode(true) const sourceRows = elementChildren(table, 'tr') if (!sourceRows.length) throw new Error('Table template has no rows') const headerTemplate = sourceRows[0].cloneNode(true) const dataTemplate = (sourceRows[1] || sourceRows[0]).cloneNode(true) for (const row of sourceRows) table.removeChild(row) rows.forEach((values, index) => { const row = (index === 0 ? headerTemplate : dataTemplate).cloneNode(true) setRowValues(document, row, values) table.appendChild(row) }) return table } function findDirectParagraphIndex(bodyChildren, exactText) { return bodyChildren.findIndex(node => localName(node) === 'p' && textOf(node) === exactText) } function replaceAllTextNodes(document, replacements) { for (const textNode of descendants(document.documentElement, 't')) { let value = textNode.textContent || '' for (const [search, replacement] of replacements) value = value.split(search).join(replacement) textNode.textContent = value } } function summarize(values, limit = 18) { const unique = [...new Set(values.map(value => String(value || '').trim()).filter(Boolean))] if (!unique.length) return '按弹窗提示查看或填写相关业务内容' return unique.slice(0, limit).join('、') + (unique.length > limit ? `等${unique.length}项` : '') } function normalizeLabel(value) { return String(value || '').replace(/\s+/g, '').replace(/[::]/g, '') } function dialogInfo(page) { const item = dialogAudit.find(entry => entry.module === page.module && entry.title === page.title) return item && Array.isArray(item.dialogs) ? item.dialogs : [] } function dialogTitleForAction(page, action) { const method = String(action.method || '').toLowerCase() const expression = String(action.expression || '').toLowerCase() const title = String(page.title || '') const rules = [ [/submitreceivecheckdata/, '收检'], [/sendinspection/, '送检'], [/abnormalform|addworktimerecord/, '异常工时'], [/submitaddrecord/, '新增工时记录'], [/submiteditform|edittask/, '编辑任务信息'], [/submitreturnware|returnwarehouse/, '退库单'], [/submitmaterialrequisition|materialrequisition/, '领料单'], [/workendform|workendvisible/, '报工结束'], [/confirmovertime|overtimedialog/, '加班确认'], [/submitremark|remarkdialog/, '编辑备注信息'], [/submitcopy|dialogformvisible/, '生产工艺拷贝'], [/processsave|saveprocess|workprocess|downone|upone|insertone|deleteone/, '编辑生产工艺'], [/assistendvisible/, '收检'], [/editdialogvisible/, '编辑任务信息'], [/returnwarehousepop/, '退库单'], [/materialrequisitionpop/, '领料单'], [/misspop/, '缺料单'], [/seevisible/, '图纸'], [/urgentvisible/, '加急明细'] ] for (const [pattern, mappedTitle] of rules) { if (pattern.test(method) || pattern.test(expression)) return mappedTitle } if (title === '工序管理' && (method === 'editprocess' || expression.includes('editprocess'))) return '业务操作' if (title === '机加件已排产' && (method === 'editsum' || method === 'editremark')) return '编辑备注信息' const dialogs = dialogInfo(page) if (dialogs.length === 1) return dialogs[0].title || `${title}操作窗口` return dialogs[0] ? (dialogs[0].title || `${title}操作窗口`) : `${title}确认窗口` } function dialogFieldsForAction(page, action) { const mappedTitle = dialogTitleForAction(page, action) const dialog = dialogInfo(page).find(item => item.title === mappedTitle) if (!dialog) return '' return summarize([...(dialog.fields || []), ...(dialog.columns || [])], 8) } function friendlyActionLabel(page, action) { const raw = normalizeLabel(action.label) const method = String(action.method || '').toLowerCase() const expression = String(action.expression || '').toLowerCase() const dialogTitle = dialogTitleForAction(page, action) if (method === 'receivecheck') { if (page.title === '原材料退库检') return '退库检' if (page.title === '序检收检') return '收检' return '入库检' } if (method === 'finalcheck') return '终检记录' if (method === 'change' && page.title === '加急件管理') return '加急状态调整' if (method === 'betweenprocessprevpage') return '序检自检窗口的上/下条、保存与删除' if (raw.includes('生产工艺') && raw.includes('取消')) return '生产工艺拷贝弹窗【取消】' if (method.includes('search') || method.includes('query') || /查询/.test(raw) || /status-light|\?| value.trim()).filter(Boolean) return relation[1] || relation[0] || `后续业务以${page.title}页面保存的最新结果为准。` } function actionParameters(action) { const ignored = /^(PageCurrent|PageSize|PageCount|ItemCount|分页|每页|返回值|result)$/i const values = [...new Set((action.parameters || []) .map(value => String(value || '').trim()) .filter(value => value && !ignored.test(value)))] return values.slice(0, 6).join('、') + (values.length > 6 ? `等${values.length}项` : '') } function hasWriteProcedure(page, action) { const writeProcedures = new Set(page.writeProcedures || []) return (action.procedures || []).some(name => writeProcedures.has(name)) } function isWriteAction(page, action) { if (hasWriteProcedure(page, action)) return true const label = normalizeLabel(action.label) return /新增|添加|保存|确定|提交|编辑|修改|删除|下发|派工|开工|开始|结束|完成|报工|送检|收检|退库|入库|出库|上传|导入|复制|生成|绑定|调整|插入|上移|下移|审核|审批|关闭订单|恢复/.test(label) } function effectForAction(page, action) { const label = friendlyActionLabel(page, action) const rawLabel = normalizeLabel(action.label) const parameters = actionParameters(action) const writes = isWriteAction(page, action) const downstream = downstreamImpact(page) const dialogTitle = dialogTitleForAction(page, action) const fields = dialogFieldsForAction(page, action) if (/弹窗【取消】/.test(label)) return [`关闭“${dialogTitle}”弹窗,不提交本次${fields || '弹窗'}修改。`, '本次弹窗中尚未提交的内容被放弃;此前已经即时保存的表格修改不受影响。'] if (/窗口【关闭】/.test(label)) return [`关闭“${dialogTitle}”窗口并返回当前页面。`, '仅退出查看窗口,不改变已保存的业务记录。'] if (/弹窗取消或确定/.test(label)) return [`根据选择执行“${dialogTitle}”弹窗的提交或退出。`, '点击【确定】会提交本次弹窗内容;点击【取消】会放弃本次弹窗修改。'] if (/弹窗【确定】/.test(label)) return [`提交“${dialogTitle}”弹窗中的${fields || '已填写内容'},成功后关闭弹窗或刷新列表。`, `本次提交结果成为${page.title}后续处理依据;提交前应核对目标记录和字段。${downstream}`] if (/查询|搜索|筛选|刷新/.test(label)) return ['按当前筛选条件重新读取并显示匹配记录。', '只刷新页面结果,不修改订单、任务或主数据。'] if (/导出|下载/.test(label)) return ['按当前筛选范围生成并下载文件。', '不修改系统业务数据;导出内容以下载时的页面结果为准。'] if (/打印|预览/.test(label)) return ['打开当前记录的预览或打印内容。', '不修改业务数据;打印前应核对记录和版本。'] if (/查看|详情|图纸|记录|日志|进度|缺料/.test(label) && !writes) return ['打开当前记录的明细、附件或关联信息供查看。', '属于只读查看,不改变当前记录状态。'] if (/^当前记录【编辑】|^编辑|^修改/.test(label) && !hasWriteProcedure(page, action)) return ['打开当前记录的编辑窗口,并读取已保存内容。', '仅打开编辑窗口不会修改数据;后续是否生效以窗口内的确认或即时保存说明为准。'] if (/批量生成|生成/.test(label)) return [`根据所选记录和现有基础数据生成${page.title}相关业务记录。`, `生成结果成为后续处理依据;重复生成前应先核对当前状态。${downstream}`] if (/复制|拷贝/.test(label)) return ['将选定来源记录复制到当前目标,形成一份可继续维护的数据。', `复制后目标记录与来源记录分别维护;后续使用目标记录的最新内容。${downstream}`] if (/新增|添加/.test(label)) return [`打开新增入口;确认后创建${parameters || '当前业务'}记录。`, `新记录成功后进入本页业务范围,并可被后续环节使用。${downstream}`] if (/删除/.test(label)) return ['删除当前选定记录,并刷新相关列表或顺序。', `被删除内容不再作为后续处理依据;操作前应确认没有仍需使用的关联任务。${downstream}`] if (/上移/.test(label)) return ['将当前记录与上一条记录交换顺序。', `新的顺序立即用于后续执行;调整后应重新核对完整顺序。${downstream}`] if (/下移/.test(label)) return ['将当前记录与下一条记录交换顺序。', `新的顺序立即用于后续执行;调整后应重新核对完整顺序。${downstream}`] if (/插入/.test(label)) return ['在当前位置增加一条待填写记录或工序位置。', `插入会改变后续记录顺序,完成内容选择后应重新核对并保存当前业务。${downstream}`] if (/下发|派工/.test(label)) return ['按当前记录生成或更新可执行任务,并将任务置为下游岗位可处理的状态。', `下发后生产、质量或执行岗位会据此接收任务;再次下发前应核对是否已存在任务。${downstream}`] if (/开工|开始/.test(label)) return ['记录当前任务的实际开始时间,并将任务转入进行中状态。', `开始时间进入工时和进度计算;开始后应在实际结束时及时完成报工。${downstream}`] if (/结束|完工|完成|报工/.test(label)) return ['记录本次完成数量、时间或结果,并更新任务完成状态。', `结果进入进度、工时、质量或后续流转判断;确认前应核对数量和结果。${downstream}`] if (/送检/.test(label)) return ['将当前完成内容转入检验环节,并形成待检记录。', `质量岗位会收到待检任务,检验结果将影响合格数量和后续入库或流转。${downstream}`] if (/收检|检验|判定/.test(label)) return ['记录检验接收、实测值或判定结果,并更新检验状态。', `合格与不合格结果会影响生产完成、处置和入库数据。${downstream}`] if (/上传|导入/.test(label)) return ['读取所选文件并保存附件或批量业务数据。', `导入或上传后的内容可被本页及关联环节使用;执行前应核对模板、记录范围和文件版本。${downstream}`] if (/保存|确定|提交|编辑|修改|调整|设置|绑定/.test(label) || writes) { return [`保存当前记录的${parameters || '已填写内容'};成功后页面显示最新结果。`, `修改结果立即成为后续业务依据;操作前应核对目标记录和字段。${downstream}`] } return [`执行【${label}】并显示对应结果。`, `该操作针对${page.title}当前记录;如随后出现确认按钮,应核对目标记录和弹窗标题后再决定是否提交。${rawLabel && rawLabel !== label ? '文档已将页面内部展示片段转换为用户可识别的操作名称。' : ''}`] } function pageActions(page) { const audit = actionAudit.find(item => item.module === page.module && item.title === page.title) if (!audit) return [] const preferredLabels = { downOne: '下移工序', upOne: '上移工序', insertOne: '插入工序', deleteOne: '删除工序' } const selected = new Map() for (const action of audit.actions || []) { const label = normalizeLabel(action.label) if (!label || /^小时/.test(label)) continue const combinedMaintenance = label.includes('查询') && label.includes('新增标准') && label.includes('修改') && label.includes('删除') const expanded = combinedMaintenance ? ['查询', '新增标准', '修改', '删除', '确定', '取消'].map(command => ({ ...action, label: command, method: `${action.method || '页面操作'}:${command}`, expression: `${action.expression || '页面操作'}:${command}` })) : [action] for (const expandedAction of expanded) { const expandedLabel = normalizeLabel(expandedAction.label) if (expandedLabel === '点击列表行' && !isWriteAction(page, expandedAction)) continue const key = expandedAction.method || expandedAction.expression || expandedLabel const existing = selected.get(key) const preferred = preferredLabels[expandedAction.method] if (!existing || (preferred && expandedLabel === preferred) || (!preferred && expandedLabel.length > normalizeLabel(existing.label).length)) { selected.set(key, { ...expandedAction, label: preferred || expandedAction.label }) } } } return [...selected.values()] } function actionRows(page) { const actions = pageActions(page) if (!actions.length) return [['操作入口', '执行结果', '业务影响与注意事项'], ['页面操作', `按页面提示完成${page.title}查询或维护。`, downstreamImpact(page)]] return [ ['操作入口', '执行结果', '业务影响与注意事项'], ...actions.map(action => { const label = friendlyActionLabel(page, action) return [actionEntry(page, action, label), ...effectForAction(page, action)] }) ] } function splitDialogCommands(command) { const value = String(command || '').replace(/\s+/g, '') const known = ['上一个', '下一个', '保存当前记录', '删除当前记录', '删除', '保存', '确定', '取消', '取 消', '确 定', '关闭', '新增异常工时', '编辑', '查询', '新增标准', '修改'] const parts = [] let remaining = value while (remaining) { const match = known.sort((a, b) => b.length - a.length).find(item => remaining.startsWith(item)) if (!match) return [command] parts.push(match.replace(/\s+/g, '')) remaining = remaining.slice(match.length) } return parts } function dialogRows(item, page) { const actions = pageActions(page) const merged = new Map() for (const dialog of item.dialogs) { const title = dialog.title || '业务操作弹窗' if (!merged.has(title)) merged.set(title, { fields: [], columns: [], commands: [] }) const target = merged.get(title) target.fields.push(...(dialog.fields || [])) target.columns.push(...(dialog.columns || [])) target.commands.push(...(dialog.commands || [])) } return [...merged.entries()].map(([title, details]) => { const content = summarize([...details.fields, ...details.columns]) const commands = [...new Set(details.commands.filter(Boolean))] const usage = commands.length ? commands.flatMap(command => splitDialogCommands(command)).map(command => { const match = actions.find(action => normalizeLabel(action.label) === normalizeLabel(command)) if (/取消/.test(command)) return `在“${title}”弹窗点击【取消】:关闭弹窗并放弃本次未提交修改。` if (/关闭/.test(command)) return `在“${title}”窗口点击【关闭】:退出查看窗口,不改变已保存内容。` if (match) return `在“${title}”弹窗点击【${command}】:${effectForAction(page, match)[0]}` if (/确定|保存/.test(command)) return `在“${title}”弹窗点击【${command}】:提交本弹窗中与${title}相关的已填写内容。` return `在“${title}”窗口执行【${command}】:完成该窗口对应的操作。` }).join(' ') : '该窗口用于查看当前记录的关联内容,关闭窗口不会修改业务数据。' return [title, content, usage] }) } async function main() { if (!fs.existsSync(docxPath)) throw new Error(`Manual not found: ${docxPath}`) fs.mkdirSync(backupDir, { recursive: true }) if (!fs.existsSync(backupPath)) fs.copyFileSync(docxPath, backupPath) // Always regenerate from the untouched backup so repeated runs stay idempotent. const sourcePath = fs.existsSync(backupPath) ? backupPath : docxPath const zip = new JSZip(fs.readFileSync(sourcePath)) const parser = new DOMParser() const serializer = new XMLSerializer() 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') if (!body) throw new Error('Word document body not found') const relationshipMap = new Map() let maxRelationshipId = 0 for (const relation of elementChildren(relationships.documentElement, 'Relationship')) { const id = relation.getAttribute('Id') relationshipMap.set(id, relation) const numeric = Number(String(id).replace(/^rId/, '')) if (Number.isFinite(numeric)) maxRelationshipId = Math.max(maxRelationshipId, numeric) } const chapterByModule = { '设备管理': 7, '研发管理': 8, '工艺管理': 9, '计划排产': 10, '生产管理': 11, '异常提醒': 12, '质量管理': 13, '销售订单管理': 14 } let replacedScreenshots = 0 for (const page of catalog) { const chapter = chapterByModule[page.module] const heading = `${chapter}.${page.pageIndex} ${page.title}` const bodyChildren = elementChildren(body) const start = findDirectParagraphIndex(bodyChildren, heading) if (start < 0) throw new Error(`Page heading not found: ${heading}`) const nextPagePrefix = `${chapter}.${page.pageIndex + 1} ` let end = bodyChildren.findIndex((node, index) => index > start && localName(node) === 'p' && textOf(node).startsWith(nextPagePrefix)) if (end < 0) end = bodyChildren.findIndex((node, index) => index > start && localName(node) === 'p' && /^\d+ /.test(textOf(node))) if (end < 0) end = bodyChildren.length const imageParagraph = bodyChildren.slice(start, end).find(node => localName(node) === 'p' && firstDescendant(node, 'blip')) if (!imageParagraph) throw new Error(`Screenshot drawing not found: ${heading}`) const blip = firstDescendant(imageParagraph, 'blip') const relationshipId = blip.getAttributeNS(NS.r, 'embed') || blip.getAttribute('r:embed') const relation = relationshipMap.get(relationshipId) if (!relation) throw new Error(`Image relationship not found: ${relationshipId}`) const target = relation.getAttribute('Target').replace(/^\.\//, '') const screenshotPath = path.resolve(screenshotsDir, page.screenshot) zip.file(`word/${target}`, fs.readFileSync(screenshotPath)) replacedScreenshots++ } let bodyChildren = elementChildren(body) const firstFilterHeadingIndex = bodyChildren.findIndex(node => localName(node) === 'p' && /\.1 搜索与过滤$/.test(textOf(node))) const firstFilterTable = bodyChildren.slice(firstFilterHeadingIndex + 1).find(node => localName(node) === 'tbl' && elementChildren(elementChildren(node, 'tr')[0] || {}, 'tc').length === 3) const firstButtonHeadingIndex = bodyChildren.findIndex(node => localName(node) === 'p' && /\.3 功能按钮与操作$/.test(textOf(node))) const firstButtonTable = bodyChildren.slice(firstButtonHeadingIndex + 1).find(node => localName(node) === 'tbl' && elementChildren(elementChildren(node, 'tr')[0] || {}, 'tc').length === 2) const heading3Template = bodyChildren[firstButtonHeadingIndex] const bodyParagraphTemplate = bodyChildren.find(node => localName(node) === 'p' && textOf(node).length > 30 && !firstDescendant(node, 'blip')) if (!firstFilterTable || !firstButtonTable || !heading3Template || !bodyParagraphTemplate) throw new Error('Required document templates not found') // Remove internal implementation rows and notes from all generated business pages. for (const table of descendants(body, 'tbl')) { for (const row of [...elementChildren(table, 'tr')]) { const firstCell = cellTexts(row)[0] || '' if (['页面路由', '只读查询过程/接口', '写入过程/接口'].includes(firstCell)) table.removeChild(row) if (firstCell === '页面输入') setCellText(document, elementChildren(row, 'tc')[0], '操作输入与上游条件') if (firstCell === '页面输出') setCellText(document, elementChildren(row, 'tc')[0], '页面结果与下游影响') } } for (const table of [...descendants(body, 'tbl')]) { const value = textOf(table) if (value.startsWith('技术核对说明:')) table.parentNode.removeChild(table) if (value.includes('截图仅执行登录、页面导航和默认查询')) { const cell = firstDescendant(table, 'tc') if (cell) setCellText(document, cell, '本章范围:本章按正式菜单顺序说明各功能页面,重点介绍查询条件、列表字段、业务操作、弹窗和上下游数据关系。') } } replaceAllTextNodes(document, [ ['正式页面', '系统页面'], ['(正式环境只读截图)', '(系统页面)'], ['点击【取消】不产生数据。', '如不保存,点击【取消】关闭窗口。'], ['点击【取消】不会产生数据。', '如不保存,点击【取消】关闭窗口。'], ['由页面一并传入后台查询', '由系统一并用于查询'], ['以对应页面后台过程为准', '以页面实际查询结果为准'], ['本文档编制未实际执行这些按钮;', ''], ['本文档编制未实际执行这些按钮。', ''] ]) // Replace every page's generic button list with code-backed action results and business impact. let actionTablesRewritten = 0 for (const page of catalog) { const section = `${chapterByModule[page.module]}.${page.pageIndex}` bodyChildren = elementChildren(body) const headingIndex = findDirectParagraphIndex(bodyChildren, `${section}.3 功能按钮与操作`) if (headingIndex < 0) continue const nextHeadingIndex = bodyChildren.findIndex((node, index) => index > headingIndex && localName(node) === 'p' && new RegExp(`^${section.replace('.', '\\.')}\\.\\d+ `).test(textOf(node)) ) const limit = nextHeadingIndex >= 0 ? nextHeadingIndex : bodyChildren.length const oldTable = bodyChildren.slice(headingIndex + 1, limit).find(node => localName(node) === 'tbl') const newTable = makeTable(document, firstFilterTable, actionRows(page)) if (oldTable) body.replaceChild(newTable, oldTable) else body.insertBefore(newTable, bodyChildren[limit] || null) actionTablesRewritten++ } // Add actual dialog/form coverage to every business page that exposes dialogs. let dialogSectionsAdded = 0 const fallbackDialogPages = [] for (const item of dialogAudit) { if (item.title === '机加件已排产' || !item.dialogs.length) continue const catalogPage = catalog.find(page => page.module === item.module && page.title === item.title) if (!catalogPage) continue const section = `${chapterByModule[item.module]}.${catalogPage.pageIndex}` bodyChildren = elementChildren(body) const sectionPattern = new RegExp(`^${section.replace('.', '\\.')}\\.(\\d+) 前后数据关系$`) const relationshipIndex = bodyChildren.findIndex(node => localName(node) === 'p' && sectionPattern.test(textOf(node))) let insertBeforeNode let dialogNumber let relationshipHeading = null if (relationshipIndex >= 0) { relationshipHeading = bodyChildren[relationshipIndex] dialogNumber = Number(textOf(relationshipHeading).match(sectionPattern)[1]) insertBeforeNode = relationshipHeading } else { const pageStart = findDirectParagraphIndex(bodyChildren, `${section} ${item.title}`) const pageEnd = bodyChildren.findIndex((node, index) => index > pageStart && localName(node) === 'p' && /^\d+\.\d+ /.test(textOf(node)) ) if (pageStart < 0) throw new Error(`Dialog page heading not found: ${section} ${item.title}`) const end = pageEnd >= 0 ? pageEnd : bodyChildren.length - 1 const subsectionPattern = new RegExp(`^${section.replace('.', '\\.')}\\.(\\d+) `) const subsectionNumbers = bodyChildren.slice(pageStart + 1, end) .filter(node => localName(node) === 'p' && subsectionPattern.test(textOf(node))) .map(node => Number(textOf(node).match(subsectionPattern)[1])) dialogNumber = Math.max(3, ...subsectionNumbers) + 1 insertBeforeNode = bodyChildren[end] fallbackDialogPages.push(`${section} ${item.title}`) } const rows = [['弹窗/操作', '可编辑或查看内容', '操作结果与影响'], ...dialogRows(item, catalogPage)] const nodes = [ makeParagraph(document, heading3Template, `${section}.${dialogNumber} 弹窗与可编辑内容`), makeParagraph(document, bodyParagraphTemplate, '点击列表中的新增、编辑、查看或业务操作后,系统会打开相应弹窗。下表说明各窗口的用途、确认后的结果以及对后续业务的影响。'), makeTable(document, firstFilterTable, rows) ] for (const node of nodes) body.insertBefore(node, insertBeforeNode) if (relationshipHeading) setParagraphText(document, relationshipHeading, `${section}.${dialogNumber + 1} 前后数据关系`) dialogSectionsAdded++ } // Rewrite section 9.3 from the production-process component and its actual data effects. bodyChildren = elementChildren(body) let processSectionStart = findDirectParagraphIndex(bodyChildren, '9.3 生产工艺') let processSectionEnd = findDirectParagraphIndex(bodyChildren, '9.4 工艺查询') if (processSectionStart < 0 || processSectionEnd < 0) throw new Error('Section 9.3 boundaries not found') setParagraphText(document, bodyChildren[processSectionStart + 1], '按生产订单编制和启用本次生产使用的工艺路线,可由标准工艺批量生成,也可在“编辑生产工艺”窗口中调整工序、工时、工装量具、机床和车间,确认后再下发为可执行的生产任务。') bodyChildren = elementChildren(body) const processButtonHeadingIndex = findDirectParagraphIndex(bodyChildren, '9.3.3 功能按钮与操作') processSectionEnd = findDirectParagraphIndex(bodyChildren, '9.4 工艺查询') const processButtonLimit = bodyChildren.findIndex((node, index) => index > processButtonHeadingIndex && index < processSectionEnd && localName(node) === 'p' && /^9\.3\.\d+ /.test(textOf(node)) ) const oldProcessButtonTable = bodyChildren.slice(processButtonHeadingIndex + 1, processButtonLimit >= 0 ? processButtonLimit : processSectionEnd) .find(node => localName(node) === 'tbl') const processButtonRows = [ ['操作入口', '执行结果', '业务影响与注意事项'], ['查询', '按订单、物料、任务状态、属性、日期和工艺状态刷新列表。', '只刷新页面结果,不修改订单、工艺或任务数据。'], ['批量生成生产工艺', '对已勾选且“已有标准工艺、尚无生产工艺”的记录,将该物料的标准工艺复制为当前订单的生产工艺。', '只生成订单专用工艺,不等于下发生产任务;不符合条件的勾选行不会重复生成。'], ['编辑工艺', '打开当前订单和物料的“编辑生产工艺”窗口,读取原料信息、可选工序和已编制路线。', '打开窗口本身不修改数据;窗口内的行编辑和结构调整有各自的保存时机。'] ] if (oldProcessButtonTable) body.replaceChild(makeTable(document, firstFilterTable, processButtonRows), oldProcessButtonTable) bodyChildren = elementChildren(body) processSectionStart = findDirectParagraphIndex(bodyChildren, '9.3 生产工艺') processSectionEnd = findDirectParagraphIndex(bodyChildren, '9.4 工艺查询') const genericProcessDialogIndex = bodyChildren.findIndex((node, index) => index > processSectionStart && index < processSectionEnd && localName(node) === 'p' && /^9\.3\.\d+ 弹窗与可编辑内容$/.test(textOf(node)) ) if (genericProcessDialogIndex >= 0) { for (const node of bodyChildren.slice(genericProcessDialogIndex, processSectionEnd)) body.removeChild(node) } bodyChildren = elementChildren(body) const processInsertBefore = bodyChildren[findDirectParagraphIndex(bodyChildren, '9.4 工艺查询')] const processEditRows = [ ['区域/字段', '操作方法', '保存时机与实际影响'], ['左侧工序列表', '可先输入工序名称过滤,再单击所需工序。', '没有插入占位行时,所选工序追加到当前路线末尾;存在插入占位行时,所选工序填入该位置。单击后即写入当前订单工艺,并将工艺置为需重新保存的状态。'], ['准备工时、标准工时、工时合计', '在右侧当前工序行直接输入数值。', '输入变化时立即保存到该工序,用于后续工时核算和任务执行;顶部【保存】不是这些字段的统一提交按钮。'], ['工装量具、指定机床、车间、属性', '在右侧当前工序行选择对应项目。', '选择变化后立即保存,作为后续排产、设备/工位匹配和生产执行的工艺条件。'], ['工序说明、备注', '在右侧当前工序行输入,离开输入框时完成更新。', '完成输入后立即保存,用于传递工艺要求和执行注意事项。'], ['上移/下移', '点击当前工序行右侧的上移或下移图标。', '当前工序与相邻工序交换顺序,并立即刷新路线;调整后工艺需重新【保存】启用。'], ['插入', '在目标工序行点击插入图标,再到左侧单击要插入的工序。', '系统先在当前位置建立空占位行并后移原有顺序;必须再从左侧选择工序填入,不要留下空工序。完成后需重新【保存】启用。'], ['删除', '点击当前工序行右侧的删除图标。', '删除该工序并重排后续顺序;已下发任务时应先确认现场执行情况。删除后需重新【保存】启用。'] ] const processEffectRows = [ ['操作', '点击后实际完成的事项', '对后续业务的影响/注意事项'], ['保存', '将当前订单的生产工艺标记为已启用,并更新计划记录的“生产工艺”状态。', '它的作用是确认当前工艺路线可用,不是保存右侧每个字段,也不会自动生成生产任务。新增、移动、插入或删除工序后应点击此按钮。'], ['保存为标准工艺', '将当前订单的整条生产工艺复制为该物料的标准工艺,并替换该物料原有的标准工艺路线。', '以后对同物料使用“批量生成生产工艺”时,会以这份新标准为模板。该操作会覆盖旧标准,应由有权限人员在完整复核后执行。'], ['下发任务(顶部)', '按当前工艺路线的全部工序生成生产任务,并将订单任务状态更新为已下发。', '下发后各工序进入生产排产、派工和执行环节;系统会识别已下发状态,避免重复生成整套任务。应在路线完整且已保存启用后执行。'], ['下发任务(单工序行)', '只为当前选定工序生成一条生产任务,并维护后续任务顺序。', '用于补发特定工序,不是整条路线下发的常规入口;使用前应检查该工序是否已有任务,避免重复。'], ['零件图纸/工艺图纸', '打开当前物料已上传的对应 PDF 图纸。', '只用于核对图纸,不修改工艺或任务数据;页面提示未上传时,应联系图纸维护人员。'] ] const processRelationRows = [ ['数据关系', '业务说明'], ['上游业务数据', '使用生产订单、物料、计划数量、物料原料信息、工序库、设备/车间主数据以及该物料已有的标准工艺。'], ['本页形成的数据', '为指定生产订单形成一条订单专用生产工艺,包括工序顺序、工时、工装量具、指定机床、车间、属性和工艺要求。'], ['下游业务影响', '已启用的生产工艺可进一步下发为生产任务,供排产、派工、加工、工时和质量环节使用;保存为标准工艺后,还会影响同物料以后的工艺批量生成。'] ] bodyChildren = elementChildren(body) processSectionStart = findDirectParagraphIndex(bodyChildren, '9.3 生产工艺') processSectionEnd = findDirectParagraphIndex(bodyChildren, '9.4 工艺查询') const processMainImageParagraph = bodyChildren.slice(processSectionStart, processSectionEnd) .find(node => localName(node) === 'p' && firstDescendant(node, 'blip')) if (!processMainImageParagraph) throw new Error('Section 9.3 main screenshot not found') const processPopupImageParagraph = processMainImageParagraph.cloneNode(true) const processPopupRelationshipId = `rId${++maxRelationshipId}` const processPopupTarget = 'media/mes-9-3-process-edit-dialog.png' const processPopupRelation = relationships.createElementNS(NS.rel, 'Relationship') processPopupRelation.setAttribute('Id', processPopupRelationshipId) processPopupRelation.setAttribute('Type', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image') processPopupRelation.setAttribute('Target', processPopupTarget) relationships.documentElement.appendChild(processPopupRelation) firstDescendant(processPopupImageParagraph, 'blip').setAttributeNS(NS.r, 'r:embed', processPopupRelationshipId) zip.file(`word/${processPopupTarget}`, fs.readFileSync(path.resolve(screenshotsDir, '03-03-process-edit-dialog.png'))) const processMainImageIndex = bodyChildren.indexOf(processMainImageParagraph) const processCaptionTemplate = bodyChildren.find((node, index) => index > processMainImageIndex && index < processSectionEnd && localName(node) === 'p' && /^图 9\.3-1/.test(textOf(node)) ) || bodyParagraphTemplate const processNodes = [ makeParagraph(document, heading3Template, '9.3.4 编辑生产工艺与保存时机'), makeParagraph(document, bodyParagraphTemplate, '“编辑生产工艺”窗口左侧是可选工序库,右侧是当前订单实际使用的工艺路线。右侧行内字段在输入或选择变化时保存;新增、移动、插入或删除工序会改变路线结构,完成后还应点击顶部【保存】将路线重新启用。'), processPopupImageParagraph, makeParagraph(document, processCaptionTemplate, '图 9.3-2 生产工艺-编辑生产工艺弹窗'), makeTable(document, firstFilterTable, processEditRows), makeParagraph(document, heading3Template, '9.3.5 顶部与行内操作结果'), makeTable(document, firstFilterTable, processEffectRows), makeParagraph(document, heading3Template, '9.3.6 前后数据关系'), makeTable(document, firstButtonTable, processRelationRows) ] for (const node of processNodes) body.insertBefore(node, processInsertBefore) // Rewrite section 10.3 from the actual current page behavior. bodyChildren = elementChildren(body) const sectionStart = findDirectParagraphIndex(bodyChildren, '10.3 机加件已排产') const sectionEnd = findDirectParagraphIndex(bodyChildren, '10.4 机加件未排产') if (sectionStart < 0 || sectionEnd < 0) throw new Error('Section 10.3 boundaries not found') setParagraphText(document, bodyChildren[sectionStart + 1], '查询并维护已完成排产的机加任务,可按订单、物料、工序、要求完工日期和计划日期筛选,并在列表中调整工位、数量、计划时间、外协分组及备注信息。') bodyChildren = elementChildren(body) const searchHeadingIndex = findDirectParagraphIndex(bodyChildren, '10.3.1 搜索与过滤') const listHeadingIndex = findDirectParagraphIndex(bodyChildren, '10.3.2 列表字段与页面结果') const oldSearchTable = bodyChildren.slice(searchHeadingIndex + 1, listHeadingIndex).find(node => localName(node) === 'tbl' && elementChildren(elementChildren(node, 'tr')[0] || {}, 'tc').length === 3) const newSearchRows = [ ['条件', '控件形式', '使用说明'], ['生产订单', '文本输入', '输入生产订单号,支持与其他条件组合查询。'], ['销售订单', '文本输入', '输入销售订单号,定位对应合同任务。'], ['物料编码', '文本输入', '输入完整或部分物料编码。'], ['物料名称', '文本输入', '输入完整或部分物料名称。'], ['工序名称', '文本输入', '输入工序名称,定位指定加工环节。'], ['要求完工日期', '日期范围', '选择开始和结束日期,筛选要求在该区间完工的任务。'], ['计划开始', '日期选择', '筛选计划开始日期不早于所选日期的任务。'], ['计划完成', '日期选择', '筛选计划完成日期不晚于所选日期的任务。'], ['工位复选框', '多选', '勾选一个或多个工位后缩小任务范围;未勾选时查询全部工位。'] ] if (oldSearchTable) body.replaceChild(makeTable(document, firstFilterTable, newSearchRows), oldSearchTable) bodyChildren = elementChildren(body) const functionSectionStart = findDirectParagraphIndex(bodyChildren, '10.3 机加件已排产') const functionSectionEnd = findDirectParagraphIndex(bodyChildren, '10.4 机加件未排产') const functionHeadingIndex = findDirectParagraphIndex(bodyChildren, '10.3.3 功能按钮与操作') const relationshipHeadingIndex = bodyChildren.findIndex((node, index) => index > functionHeadingIndex && index < functionSectionEnd && localName(node) === 'p' && /^10\.3\.[45] 前后数据关系$/.test(textOf(node)) ) const functionSectionLimit = relationshipHeadingIndex >= 0 ? relationshipHeadingIndex : functionSectionEnd const oldButtonTable = bodyChildren.slice(functionHeadingIndex + 1, functionSectionLimit).find(node => localName(node) === 'tbl' && elementChildren(elementChildren(node, 'tr')[0] || {}, 'tc').length === 2) const newButtonRows = [ ['按钮/区域', '操作说明'], ['查询', '按当前订单、物料、工序、日期和工位条件刷新列表。'], ['表单', '以表格方式查看任务,并在可编辑列中直接维护排产信息。'], ['日程', '按日历查看计划;拖动或调整日程会修改任务的计划开始和计划完成日期。'], ['原料状态-查看', '打开原料信息弹窗,查看子件编码、数量、库存和材质。'], ['编辑', '打开“编辑备注信息”弹窗,维护优先级、未完成说明和派工特殊备注。'], ['分页与横向滚动', '使用分页切换记录;向右滚动可查看二次派工、最晚开始、编辑和最后编辑信息。'] ] if (oldButtonTable) body.replaceChild(makeTable(document, firstButtonTable, newButtonRows), oldButtonTable) bodyChildren = elementChildren(body) const refreshedSectionStart = findDirectParagraphIndex(bodyChildren, '10.3 机加件已排产') const refreshedSectionEnd = findDirectParagraphIndex(bodyChildren, '10.4 机加件未排产') let relationshipIndex = bodyChildren.findIndex((node, index) => index > refreshedSectionStart && index < refreshedSectionEnd && localName(node) === 'p' && /^10\.3\.[45] 前后数据关系$/.test(textOf(node)) ) let relationshipHeading = bodyChildren[relationshipIndex] if (!relationshipHeading) { const sectionTables = bodyChildren.slice(functionHeadingIndex + 1, refreshedSectionEnd).filter(node => localName(node) === 'tbl') const relationshipTable = sectionTables[sectionTables.length - 1] relationshipHeading = makeParagraph(document, heading3Template, '10.3.4 前后数据关系') body.insertBefore(relationshipHeading, relationshipTable || bodyChildren[refreshedSectionEnd]) bodyChildren = elementChildren(body) relationshipIndex = bodyChildren.indexOf(relationshipHeading) } const editRows = [ ['字段/入口', '操作方法', '功能解释与注意事项'], ['指派对象', '单击当前工位,在下拉列表选择新工位。', '用于调整任务执行工位;选择后立即保存。'], ['指派数量', '单击数量,输入新值后点击行内【确定】。', '数量必须大于等于1且不能超过计划数量。'], ['计划开始', '在当前行日期框选择日期。', '表示任务预计开始加工的日期;修改后立即保存,并影响日程视图及开工及时性判断。'], ['计划完成', '在当前行日期框选择日期。', '表示任务计划完成的日期;应不早于计划开始,修改后立即保存。'], ['外协分组', '在当前行输入分组值,完成输入后触发保存。', '用于给外协任务标记同一协同批次;应按计划部门既定分组规则填写,不要随意使用测试编号。'], ['二次派工', '在多选下拉框中选择参与人员。', '用于补充协作人员;选择变化后立即保存。'], ['最晚开始', '在当前行日期框选择日期。', '表示为满足完工要求允许的最迟开工日期;修改后立即保存。'], ['编辑', '点击【编辑】,在弹窗中维护优先级、未完成说明和派工特殊备注,再点击【确定】。', '只有弹窗中的三个备注类字段由【确定】统一提交;点击【取消】不保存本次弹窗修改。'] ] const intro = '本页有两种修改方式:计划开始、计划完成、外协分组等字段在表格中直接修改;【编辑】按钮只打开备注弹窗。表格内多数控件在选择或输入完成后立即保存,没有统一的【保存】按钮,因此操作前必须先确认当前行。' const specialNodes = [ makeParagraph(document, heading3Template, '10.3.4 可编辑字段与编辑弹窗'), makeParagraph(document, bodyParagraphTemplate, intro), makeTable(document, firstFilterTable, editRows) ] const mainImageParagraph = elementChildren(body).slice(sectionStart, sectionEnd).find(node => localName(node) === 'p' && firstDescendant(node, 'blip')) const popupImageParagraph = mainImageParagraph.cloneNode(true) const popupRelationshipId = `rId${++maxRelationshipId}` const popupTarget = 'media/mes-10-3-edit-dialog.png' const popupRelation = relationships.createElementNS(NS.rel, 'Relationship') popupRelation.setAttribute('Id', popupRelationshipId) popupRelation.setAttribute('Type', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image') popupRelation.setAttribute('Target', popupTarget) relationships.documentElement.appendChild(popupRelation) const popupBlip = firstDescendant(popupImageParagraph, 'blip') popupBlip.setAttributeNS(NS.r, 'r:embed', popupRelationshipId) zip.file(`word/${popupTarget}`, fs.readFileSync(path.resolve(screenshotsDir, '04-03-plan-edit-dialog.png'))) const mainImageIndex = elementChildren(body).indexOf(mainImageParagraph) const captionTemplate = elementChildren(body).find((node, index) => index > mainImageIndex && index < refreshedSectionEnd && localName(node) === 'p' && /^图 10\.3-1/.test(textOf(node)) ) || bodyParagraphTemplate specialNodes.push(popupImageParagraph) specialNodes.push(makeParagraph(document, captionTemplate, '图 10.3-2 机加件已排产-编辑备注信息弹窗')) for (const node of specialNodes) body.insertBefore(node, relationshipHeading) setParagraphText(document, relationshipHeading, '10.3.5 前后数据关系') const relationshipRows = [ ['数据关系', '业务说明'], ['上游业务数据', '使用生产订单、销售订单、物料与工序信息、工位设置以及已形成的排产任务,页面筛选条件不会改变上游原始订单和主数据。'], ['本页维护结果', '可调整任务工位、指派数量、计划开始、计划完成、外协分组、协作人员、最晚开始及备注信息;相应修改保存到当前排产任务。'], ['下游业务影响', '维护结果用于后续派工、日程查看、原料准备、生产执行和计划及时性判断;日期或工位调整后,相关岗位应以页面最新结果为准。'] ] const relationshipTable = makeTable(document, firstButtonTable, relationshipRows) body.insertBefore(relationshipTable, relationshipHeading.nextSibling) bodyChildren = elementChildren(body) const relationshipHeadingPosition = bodyChildren.indexOf(relationshipHeading) const nextPagePosition = findDirectParagraphIndex(bodyChildren, '10.4 机加件未排产') const operationNoticeTable = bodyChildren.slice(relationshipHeadingPosition + 1, nextPagePosition) .find(node => node !== relationshipTable && localName(node) === 'tbl') if (operationNoticeTable) { const noticeCell = firstDescendant(operationNoticeTable, 'tc') if (noticeCell) setCellText(document, noticeCell, '操作注意:计划开始、计划完成、外协分组等表格字段在选择或输入完成后立即保存;【编辑】弹窗中的备注字段点击【确定】后保存。操作前应核对当前任务行和修改内容。') } // Make the section summary state that this page writes immediately. bodyChildren = elementChildren(body) const currentSectionStart = findDirectParagraphIndex(bodyChildren, '10.3 机加件已排产') const currentSectionEnd = findDirectParagraphIndex(bodyChildren, '10.4 机加件未排产') for (const table of bodyChildren.slice(currentSectionStart, currentSectionEnd).filter(node => localName(node) === 'tbl')) { for (const row of elementChildren(table, 'tr')) { const values = cellTexts(row) if (values[0] === '数据性质' && values[1] && values[1].includes('业务按钮可能')) { setCellText(document, elementChildren(row, 'tc')[1], '支持查询及排产信息维护;表格内修改通常即时保存') } } } // Append a revision record entry. const revisionTable = descendants(body, 'tbl').find(table => { const firstRow = elementChildren(table, 'tr')[0] return firstRow && cellTexts(firstRow)[0] === '版本' }) if (revisionTable) { const rows = elementChildren(revisionTable, 'tr') const newRow = rows[rows.length - 1].cloneNode(true) setRowValues(document, newRow, ['V1.1', '2026-07-24', '按用户反馈复核修订', '更新69页系统截图和操作结果/业务影响说明,补充44页弹窗内容,详细重写9.3生产工艺与10.3机加已排产,移除内部实现名称和编制过程描述']) revisionTable.appendChild(newRow) } const coreEntry = zip.file('docProps/core.xml') if (coreEntry) { const core = parser.parseFromString(coreEntry.asText(), 'application/xml') const modified = descendants(core.documentElement, 'modified')[0] if (modified) modified.textContent = new Date().toISOString() zip.file('docProps/core.xml', serializer.serializeToString(core)) } zip.file('word/document.xml', serializer.serializeToString(document)) zip.file('word/_rels/document.xml.rels', serializer.serializeToString(relationships)) const output = zip.generate({ type: 'nodebuffer', compression: 'DEFLATE' }) fs.writeFileSync(stagingPath, output) // Reopen the generated package before replacing the requested document. const validationZip = new JSZip(fs.readFileSync(stagingPath)) const validationDocument = parser.parseFromString(validationZip.file('word/document.xml').asText(), 'application/xml') const validationText = textOf(validationDocument.documentElement) if (!validationText.includes('10.3.4 可编辑字段与编辑弹窗')) throw new Error('Revised section 10.3 was not written') if (!validationText.includes('编辑备注信息弹窗')) throw new Error('Edit dialog caption was not written') if (!validationText.includes('图 9.3-2 生产工艺-编辑生产工艺弹窗')) throw new Error('Production process dialog caption was not written') fs.copyFileSync(stagingPath, docxPath) console.log(`Replaced screenshots: ${replacedScreenshots}`) console.log(`Rewritten action tables: ${actionTablesRewritten}`) console.log(`Added dialog sections: ${dialogSectionsAdded + 1}`) if (fallbackDialogPages.length) console.log(`Fallback dialog pages: ${fallbackDialogPages.join(' | ')}`) console.log(`Backup: ${backupPath}`) console.log(`DOCX: ${docxPath}`) } main().catch(error => { console.error(error.stack || error.message) process.exitCode = 1 })