R123/A1-A6 收敛移动发票反馈并补部署检查
这个提交包含在:
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env node
|
||||
import process from "node:process";
|
||||
|
||||
const baseUrl = normalizeBaseUrl(process.argv[2] ?? process.env.PUBLIC_BASE_URL ?? process.env.LIVE_SMOKE_BASE_URL ?? "https://oa.hyinda.com");
|
||||
const focusedAssetPattern = /(MobileReimbursementDetailPage|ReimbursementDetailPage|page-mobile|page-assets|page-travel)/;
|
||||
const forbiddenTerms = ["绑定整单", "不绑定明细", "识别完成后可在", "发票池", "OCR"];
|
||||
|
||||
function normalizeBaseUrl(value) {
|
||||
return String(value).replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
function toAssetPath(value) {
|
||||
if (value.startsWith("/")) {
|
||||
return value;
|
||||
}
|
||||
return `/assets/${value.replace(/^\.\//, "")}`;
|
||||
}
|
||||
|
||||
async function fetchText(path) {
|
||||
const response = await fetch(`${baseUrl}${path}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`GET ${path} failed: ${response.status}`);
|
||||
}
|
||||
return response.text();
|
||||
}
|
||||
|
||||
async function collectAssets() {
|
||||
const seen = new Set();
|
||||
const queue = [];
|
||||
const html = await fetchText("/");
|
||||
|
||||
for (const match of html.matchAll(/(?:src|href)=["'](\/assets\/[^"']+\.js)["']/g)) {
|
||||
const assetPath = toAssetPath(match[1]);
|
||||
if (!seen.has(assetPath)) {
|
||||
queue.push(assetPath);
|
||||
seen.add(assetPath);
|
||||
}
|
||||
}
|
||||
|
||||
for (let index = 0; index < queue.length; index += 1) {
|
||||
const assetPath = queue[index];
|
||||
const source = await fetchText(assetPath);
|
||||
for (const match of source.matchAll(/\.\/([A-Za-z0-9_./-]+\.js)/g)) {
|
||||
const nextPath = toAssetPath(match[1]);
|
||||
if (!seen.has(nextPath)) {
|
||||
queue.push(nextPath);
|
||||
seen.add(nextPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(seen).sort();
|
||||
}
|
||||
|
||||
const assets = await collectAssets();
|
||||
const findings = [];
|
||||
|
||||
for (const assetPath of assets) {
|
||||
if (!focusedAssetPattern.test(assetPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const source = await fetchText(assetPath);
|
||||
for (const term of forbiddenTerms) {
|
||||
if (source.includes(term)) {
|
||||
findings.push({ assetPath, term });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[public-bundle-copy] base=${baseUrl} scanned=${assets.length} focused=${assets.filter((item) => focusedAssetPattern.test(item)).length} findings=${findings.length}`);
|
||||
for (const finding of findings) {
|
||||
console.log(`ERROR ${finding.assetPath} contains ${finding.term}`);
|
||||
}
|
||||
|
||||
if (findings.length) {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
@@ -15,6 +15,12 @@ const scanRoots = [
|
||||
];
|
||||
const sourceExtensions = new Set([".js", ".jsx", ".ts", ".tsx"]);
|
||||
const forbiddenCopy = ["先处理", "清爽"];
|
||||
const userFacingBusinessTermFiles = new Set([
|
||||
"client/src/pages/MobileReimbursementDetailPage.tsx",
|
||||
"client/src/pages/ReimbursementDetailPage.tsx",
|
||||
"client/src/components/travel/TravelPaymentTrackCard.tsx",
|
||||
]);
|
||||
const forbiddenUserFacingBusinessTerms = ["绑定整单", "不绑定明细", "发票池", "OCR"];
|
||||
const findings = [];
|
||||
|
||||
function toPosix(value) {
|
||||
@@ -104,7 +110,7 @@ function scanNativeFileInputs(file, source) {
|
||||
const fileInputPattern = /<input\b[^>]*\btype=["']file["'][^>]*>/g;
|
||||
let match = fileInputPattern.exec(source);
|
||||
while (match) {
|
||||
if (/\bhidden\b/.test(match[0])) {
|
||||
if (/\bhidden\b/.test(match[0]) || /\bsr-only\b/.test(match[0])) {
|
||||
match = fileInputPattern.exec(source);
|
||||
continue;
|
||||
}
|
||||
@@ -121,6 +127,28 @@ function scanNativeFileInputs(file, source) {
|
||||
}
|
||||
}
|
||||
|
||||
function scanUserFacingBusinessTerms(file, source) {
|
||||
const normalized = toPosix(path.relative(rootDir, file));
|
||||
if (!userFacingBusinessTermFiles.has(normalized)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const term of forbiddenUserFacingBusinessTerms) {
|
||||
let start = source.indexOf(term);
|
||||
while (start !== -1) {
|
||||
addFinding({
|
||||
rule: "business.no_internal_invoice_terms",
|
||||
severity: "error",
|
||||
file,
|
||||
line: lineNumber(source, start),
|
||||
message: `普通用户主流程不应暴露内部发票术语:${term}`,
|
||||
evidence: term,
|
||||
});
|
||||
start = source.indexOf(term, start + term.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scanRawBusinessIdentifiers(file, source) {
|
||||
const normalized = toPosix(file);
|
||||
if (!/Reimbursement|Invoice|Leave|Purchase|Travel/.test(path.basename(normalized))) {
|
||||
@@ -195,6 +223,7 @@ function scanNavigationTitleCoverage(file, source) {
|
||||
function scanFile(file) {
|
||||
const source = readFileSync(file, "utf8");
|
||||
scanForbiddenCopy(file, source);
|
||||
scanUserFacingBusinessTerms(file, source);
|
||||
scanMobileDarkControls(file, source);
|
||||
scanNativeFileInputs(file, source);
|
||||
scanRawBusinessIdentifiers(file, source);
|
||||
|
||||
在新工单中引用
屏蔽一个用户