import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' const reportDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../reports/model-cover-20260905') const sourcePath = path.join(reportDir, 'evidence.json') const sensitiveKey = /password|secret|token|ticket|authorization|cookie|signature|credential/i const cleanText = value => String(value ?? '') .replace(/\bBearer\s+[^\s"'<>]+/gi, 'Bearer [已隐藏]') .replace(/(https?:\/\/)[^\s/@]+:[^\s/@]+@/gi, '$1[已隐藏]@') .replace(/\b((?:access|refresh)?[_-]?token|password|secret|uploadTicket|signature|api[_-]?key)\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s,;}]+)/gi, '$1=[已隐藏]') function sanitize(value) { if (typeof value === 'string') return cleanText(value) if (Array.isArray(value)) return value.map(sanitize) if (value && typeof value === 'object') { return Object.fromEntries(Object.entries(value) .filter(([key]) => !sensitiveKey.test(key)) .map(([key, item]) => [key, sanitize(item)])) } return value } const data = sanitize(JSON.parse(fs.readFileSync(sourcePath, 'utf8').replace(/^\uFEFF/, ''))) const evidence = Array.isArray(data.evidence) ? data.evidence : [] const projects = Array.isArray(data.projects) ? data.projects : [] const issues = Array.isArray(data.issues) ? data.issues : data.issues ? [data.issues] : [] const commands = Array.isArray(data.commands) ? data.commands : data.commands ? [data.commands] : [] const passCount = evidence.filter(item => item.status === 'PASS').length const nonPassCount = evidence.length - passCount const imageFields = new Set(['screenshot', 'screenshots', 'coverImage', 'image', 'finalListScreenshot']) const imagePaths = new Set() function collectImages(value, key = '') { if (typeof value === 'string' && imageFields.has(key) && value) imagePaths.add(value) else if (Array.isArray(value)) value.forEach(item => collectImages(item, key)) else if (value && typeof value === 'object') { Object.entries(value).forEach(([childKey, item]) => collectImages(item, childKey)) } } collectImages(data) const realReportDir = fs.realpathSync(reportDir) for (const relative of imagePaths) { const normalized = relative.replaceAll('\\', '/') if (path.isAbsolute(relative) || /^[a-z][a-z\d+.-]*:/i.test(relative) || !/\.(png|jpe?g|webp)$/i.test(relative)) { throw new Error(`图片必须是报告目录中的 PNG/JPEG/WebP 相对路径:${relative}`) } const absolute = path.resolve(reportDir, normalized) const underRoot = path.relative(reportDir, absolute) if (underRoot.startsWith('..') || path.isAbsolute(underRoot) || !fs.existsSync(absolute) || !fs.statSync(absolute).isFile() || fs.statSync(absolute).size === 0) { throw new Error(`图片不存在、为空或越出报告目录:${relative}`) } const realRelative = path.relative(realReportDir, fs.realpathSync(absolute)) if (realRelative.startsWith('..') || path.isAbsolute(realRelative)) { throw new Error(`图片链接目标越出报告目录:${relative}`) } } const html = value => cleanText(value).replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', })[char]) const md = value => cleanText(value).replaceAll('\\', '\\\\').replaceAll('|', '\\|').replace(/\r?\n/g, ' ') const url = value => String(value).replaceAll('\\', '/').split('/').map(encodeURIComponent).join('/') const pretty = value => typeof value === 'string' ? value : JSON.stringify(value, null, 2) const status = value => value || '未记录' const statusClass = value => value === 'PASS' ? 'pass' : value === 'FAIL' ? 'fail' : 'neutral' const picturesOf = item => [...new Set([ item.screenshot, ...(Array.isArray(item.screenshots) ? item.screenshots : []), item.coverImage, ].filter(Boolean))] const figure = (src, caption, className = '') => src ? `
${html(caption)}
${html(caption)} · 点击查看原图
` : '

尚未记录图片。

' const mdImage = (src, caption) => src ? `[![${md(caption)}](${url(src)})](${url(src)})` : '尚未记录图片。' const finalList = data.finalListScreenshot || [...evidence].reverse().find(item => item.screenshot && (/最终.*(?:列表|模型|封面)/.test(item.title || '') || /final.*(?:covers|list)/i.test(item.screenshot)))?.screenshot const cleanup = evidence.find(item => String(item.details?.id) === '158' && item.status === 'PASS' && (item.details?.readAfterDelete === 404 || /清理|软删/.test(item.title || ''))) const cleanupText = cleanup ? '首次导入验证使用的临时项目 158 已通过业务软删除,删除后读取返回 404;报告保留操作证据,不宣称物理文件已删除。' : '临时项目 158 的业务软删除结果尚未在 evidence.json 中完整记录,请以补充后的证据为准。' const retainedText = data.retainedData ? pretty(data.retainedData) : projects.length ? `项目 ${projects.map(item => item.id).join('、')} 保留在开发环境,供继续查看模型、封面及历史版本。` : '尚未记录需要保留的项目。' const limitations = [ '仅手动保存和首次导入模型时生成或更新封面,封面取自当前三维视口。', '自动保存只保存工程变更,不重新截图或上传封面。', '截图或封面上传失败时保留旧封面,并提示失败;工程保存仍按其实际请求结果判断。', ] const featureText = '截图封面功能已实施并完成下列真实浏览器验证:视口截图作为受控图片资产与工程关联保存,列表读取真实封面,模型列表按创建时间倒序显示。' const environment = `${data.date || '日期未记录'} · ${data.baseURL || '地址未记录'} · ${data.browser || '浏览器未记录'} · ${data.role || '角色未记录'}` const commandParts = item => typeof item === 'string' ? { command: item, result: '未记录执行结果' } : { command: item.command || item.cmd || item.name || '命令未记录', result: pretty(Object.fromEntries(Object.entries(item).filter(([key]) => !['command', 'cmd', 'name'].includes(key)))) } const outputHtml = ` 模型截图封面验证报告 · ${html(data.date)}
MODEL COVER · IMPLEMENTED

模型截图封面验证报告

${html(featureText)}

${html(environment)}

Markdown 报告
${passCount} / ${evidence.length}证据项通过
${nonPassCount}非 PASS 证据项
${imagePaths.size}图片文件已校验

最终模型封面

${projects.map(project => { const cover = project.cover || {} return `

${html(project.name || `项目 ${project.id}`)}

${figure(cover.image, `项目 ${project.id} 最终封面`, 'cover-image')}
项目 / 最终状态
${html(project.id)} / ${html(status(project.finalStatus))}
工程版本 / 封面资产
${html(project.finalVersionId || cover.versionId || '未记录')} / ${html(cover.coverAssetId || '未记录')}
图片规格
${html(cover.width || '?')} × ${html(cover.height || '?')} · ${html(cover.bytes ?? '?')} 字节
封面持久化证据
${html(pretty(cover))}
` }).join('')}

最终列表现场

${figure(finalList, '两条保留模型的真实封面与创建时间倒序列表')}

验证记录

通过数只统计 status 为 PASS 的记录;故障注入中预期的 503、401 或 404 不等于用例失败。

${evidence.map((item, index) => { const pictures = picturesOf(item) const extra = Object.fromEntries(Object.entries(item).filter(([key]) => !['title', 'status', 'screenshot', 'screenshots', 'coverImage'].includes(key))) return `

${index + 1}. ${html(item.title)}

${html(status(item.status))}
${Object.keys(extra).length ? `
检查细节
${html(pretty(extra))}
` : ''}
${pictures.map(src => figure(src, /\.jpe?g$/i.test(src) ? '实际生成封面' : '浏览器现场')).join('')}
` }).join('')}

数据保留与清理

${html(retainedText)}

${projects.map(project => ``).join('')}
项目初始状态最终状态
${html(project.id)} · ${html(project.name)}${html(status(project.originalStatus))}${html(status(project.finalStatus))}

${html(cleanupText)}

问题记录

${issues.length ? issues.map(issue => `
${html(pretty(issue))}
`).join('') : '

当前证据未登记未解决问题。

'}

实际命令与结果

${commands.length ? commands.map(item => { const entry = commandParts(item); return `

${html(entry.command)}

${html(entry.result)}
` }).join('') : '

本次 evidence.json 尚未记录命令结果;不据此宣称构建或命令行测试通过。

'}

当前行为与限制

` const outputMarkdown = [ '# 模型截图封面验证报告', '', featureText, '', environment, '', `证据通过:**${passCount} / ${evidence.length}**;非 PASS:${nonPassCount};已校验图片:${imagePaths.size}。`, '', '## 最终模型封面', '', ...projects.flatMap(project => [ `### ${project.name || `项目 ${project.id}`}`, '', `项目 ${project.id};最终状态:${status(project.finalStatus)};工程版本 ID:${project.finalVersionId || project.cover?.versionId || '未记录'};封面资产 ID:${project.cover?.coverAssetId || '未记录'}。`, '', mdImage(project.cover?.image, `项目 ${project.id} 最终封面`), '', ]), '## 最终列表现场', '', mdImage(finalList, '最终两条模型封面列表'), '', '## 验证记录', '', '通过统计只认 status 为 PASS;预期故障响应不视为用例失败。', '', '| 序号 | 检查项 | 结果 | 图片 |', '|---|---|---|---|', ...evidence.map((item, index) => `| ${index + 1} | ${md(item.title)} | ${md(status(item.status))} | ${picturesOf(item).map((src, i) => `[图片 ${i + 1}](${url(src)})`).join(' · ') || '—'} |`), '', ...evidence.filter(item => item.details !== undefined).flatMap(item => [ `
${html(item.title)}:检查细节`, '', '
', html(pretty(item.details)), '
', '', '
', '', ]), '## 数据保留与清理', '', retainedText, '', '| 项目 | 初始状态 | 最终状态 |', '|---|---|---|', ...projects.map(project => `| ${md(project.id)} · ${md(project.name)} | ${md(status(project.originalStatus))} | ${md(status(project.finalStatus))} |`), '', cleanupText, '', '## 问题记录', '', ...(issues.length ? issues.flatMap(issue => [pretty(issue), '']) : ['当前证据未登记未解决问题。', '']), '## 实际命令与结果', '', ...(commands.length ? commands.flatMap(item => { const entry = commandParts(item); return [`**${md(entry.command)}**`, '', '
', html(entry.result), '
', ''] }) : ['本次 evidence.json 尚未记录命令结果;不据此宣称构建或命令行测试通过。', '']), '## 当前行为与限制', '', ...limitations.map(item => `- ${item}`), '', '报告由 evidence.json 生成,未修改原始证据;图片均已校验存在且非空,点击图片可查看原图。', '', ].join('\n') fs.writeFileSync(path.join(reportDir, 'index.html'), outputHtml, 'utf8') fs.writeFileSync(path.join(reportDir, 'report.md'), outputMarkdown, 'utf8') console.log(JSON.stringify({ report: 'reports/model-cover-20260905/index.html', passed: passCount, total: evidence.length, nonPass: nonPassCount, imagesValidated: imagePaths.size, commandsRecorded: commands.length }))