(() => {
const runId = document.body.dataset.runId;
const root = document.getElementById("audit-root");
const runSummary = document.getElementById("run-summary");
const moduleJump = document.getElementById("module-jump");
const onlyUsedFields = document.getElementById("only-used-fields");
const sourceFilter = document.getElementById("source-filter");
const readColumnTitle = document.getElementById("read-column-title");
const contextColumnTitle = document.getElementById("context-column-title");
const detailCache = new Map();
const renderedModules = new Set();
const renderQueue = [];
let renderScheduled = false;
let runMeta = null;
let moduleTotal = 0;
let streamComplete = false;
const jsonText = (value) => {
if (value === null || value === undefined) return "";
if (typeof value === "string") return value;
try { return JSON.stringify(value, null, 2); } catch { return String(value); }
};
const chars = (value) => Array.from(jsonText(value)).length;
const escapeHtml = (value) => String(value ?? "")
.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">")
.replaceAll('"', """).replaceAll("'", "'");
const truncate = (value, length = 240) => {
const text = jsonText(value).replace(/\s+/g, " ").trim();
return Array.from(text).length > length ? `${Array.from(text).slice(0, length).join("")}…` : text;
};
const excerptAround = (value, needle, length = 300) => {
const text = jsonText(value).replace(/\s+/g, " ").trim();
if (!needle || !text.includes(needle)) return truncate(text, length);
const charsBefore = 80;
const index = text.indexOf(needle);
const start = Math.max(0, index - charsBefore);
const end = Math.min(text.length, start + Math.max(length, needle.length + charsBefore));
return `${start > 0 ? "…" : ""}${text.slice(start, end)}${end < text.length ? "…" : ""}`;
};
const correlationText = (value) => {
if (value === null || value === undefined) return "";
if (typeof value === "string") return value.trim();
if (typeof value === "number" || typeof value === "boolean") return String(value);
return jsonText(value).trim();
};
const makeSegments = (scope, items) => items
.filter((item) => item && correlationText(item.value))
.map((item, index) => ({ ...item, id: `${scope}-${index + 1}`, number: index + 1, color: index % 8, text: correlationText(item.value) }));
const matchingSegments = (value, segments) => {
const text = correlationText(value);
if (!text) return [];
return segments.filter((segment) => segment.text.length >= 2 && text.includes(segment.text));
};
const labelForDisposition = (value) => ({ selected: "已采用", fallback: "备用来源", "calculation-input": "参与计算", unconfirmed: "来源未确认", missing: "记录缺失" }[value] || value || "已读取");
const sourceKind = (source = {}) => source.sourceKind || "unknown";
const sourceKindLabel = (kind) => ({ database: "数据库", function: "函数", log: "日志", unknown: "未确认" }[kind] || "未确认");
const relationLabel = (value) => ({ exact: "直接摘取", "exact-record": "完整记录", structured: "结构化字段", projected: "展示处理", combined: "多来源重组", "calculation-input": "参与计算", anchored: "日志定位", missing: "记录缺失" }[value] || value || "未标记");
const completenessLabel = (value) => ({ complete: "完整", "body-missing": "Event Body 缺失", missing: "记录缺失", partial: "部分可用" }[value] || value || "未知");
function sourceName(source = {}) {
const kind = sourceKind(source);
if (kind === "database") return `数据库:${source.table || "表名未记录"}`;
if (kind === "function") return `函数:${source.functionName || "Inspector 详情组装"}`;
if (kind === "log") return `日志:${source.table || "script_build_log"}`;
return "来源未确认";
}
function sourceDetail(source = {}) {
return [
source.recordId !== null && source.recordId !== undefined ? `记录 #${source.recordId}` : null,
source.eventId !== null && source.eventId !== undefined ? `Event ${source.eventId}` : null,
source.eventName || null,
source.technicalGroup ? `处理组 ${source.technicalGroup}` : null,
].filter(Boolean).join(" · ");
}
function buildSourceRegistry(reads, contexts) {
const registry = new Map();
const counters = { database: 0, function: 0, log: 0, unknown: 0 };
[...reads, ...contexts].forEach((item) => {
const key = item.sourceKey || `unknown:${registry.size + 1}`;
if (registry.has(key)) return;
const kind = sourceKind(item.source || {});
counters[kind] = (counters[kind] || 0) + 1;
registry.set(key, { key, kind, label: `${sourceKindLabel(kind)} ${counters[kind]}`, source: item.source || {} });
});
return registry;
}
function groupReads(reads) {
const groups = new Map();
reads.forEach((read, index) => {
const key = read.sourceKey || `unknown:${index}`;
if (!groups.has(key)) groups.set(key, { key, source: read.source || {}, items: [], usageAreas: new Set() });
const group = groups.get(key);
group.items.push(read);
(Array.isArray(read.usageAreas) ? read.usageAreas : []).forEach((area) => group.usageAreas.add(area));
});
return [...groups.values()];
}
function groupsForAreas(groups, areas) {
const wanted = new Set(areas);
return groups.filter((group) => [...group.usageAreas].some((area) => wanted.has(area)));
}
function referencesForGroups(groups, registry) {
return groups.map((group) => registry.get(group.key)).filter(Boolean);
}
function contextsForGroups(contexts, groups) {
const keys = new Set(groups.map((group) => group.key));
return contexts.filter((context) => keys.has(context.sourceKey) || (context.kind === "log-module" && context.relatedSourceKey && keys.has(context.relatedSourceKey)));
}
function activeSourceKind() {
return sourceFilter?.value || "all";
}
function filterGroups(groups) {
const wanted = activeSourceKind();
return wanted === "all" ? groups : groups.filter((group) => sourceKind(group.source) === wanted);
}
function filterContexts(contexts) {
const wanted = activeSourceKind();
return wanted === "all" ? contexts : contexts.filter((context) => sourceKind(context.source) === wanted);
}
function parseStructuredText(value) {
if (typeof value !== "string") return value;
const text = value.trim();
if (!text || !["{", "["].includes(text[0])) return value;
try { return JSON.parse(text); } catch { return value; }
}
function sourceLinks(links = [], usageAreas = []) {
if (!links.length && !usageAreas.length) return "";
const refs = links.map((item) => `${escapeHtml(item.label)}`).join("");
const usages = usageAreas.map((area) => `${escapeHtml(area)}`).join("");
return `${refs}${usages}`;
}
function correlationLinks(segments = []) {
if (!segments.length) return "";
return `${segments.map((segment) => `${String(segment.number).padStart(2, "0")}${escapeHtml(segment.label)}`).join("")}`;
}
function contentBox(title, value, options = {}) {
const count = options.characters ?? chars(value);
const long = count > 800;
const body = options.html ?? `
${escapeHtml(jsonText(value))}`;
const meta = [options.meta, `${count.toLocaleString("zh-CN")} 字`].filter(Boolean).join(" · ");
const sourceKeys = [...new Set([...(options.links || []).map((item) => item.key), ...(options.sourceKeys || [])].filter(Boolean))].join(" ");
return `
${escapeHtml(title)}${escapeHtml(meta)}${sourceLinks(options.links, options.usageAreas)}${correlationLinks(options.correlationSegments)}${long && options.showPreview ? `${escapeHtml(truncate(value))}` : ""}
${body}
`;
}
function cardSegments(card) {
return makeSegments("card", (card?.fields || []).map((field) => ({ label: field?.label || "内容", value: field?.value })));
}
function businessSegments(inspector) {
const items = [];
(inspector?.businessSections || []).forEach((section) => items.push({ label: section.title || "内容", value: section.content ?? section.items ?? section }));
(inspector?.blocks || []).forEach((block) => items.push({ label: block.title || block.type || "决策内容", value: block.value ?? block.items ?? block }));
if (inspector?.changes) items.push({ label: "产出与改动", value: inspector.changes });
return makeSegments("business", items);
}
function technicalSegments(inspector) {
const technical = inspector?.technical;
if (!technical || typeof technical !== "object") return [];
return makeSegments("technical", Object.entries(technical).map(([label, value]) => ({ label, value })));
}
function targetAttrs(segment) {
if (!segment) return "";
return ` role="button" tabindex="0" data-correlation-id="${escapeHtml(segment.id)}"`;
}
function renderCard(card, links, segments) {
if (!card) return contentBox("卡片", "该模块没有卡片展示数据。", { tone: "muted" });
const fields = (card.fields || []).filter((field) => field && field.value !== null && field.value !== undefined && field.value !== "");
const html = `${escapeHtml(card.title || "卡片")}
${fields.map((field, index) => {
const segment = segments[index];
return `
${segment ? `${String(segment.number).padStart(2, "0")}` : ""}${escapeHtml(field.label || "内容")}${renderPlainValue(field.value)}
`;
}).join("") || `
卡片没有可显示字段。
`}
`;
return contentBox("卡片", card, { html, characters: chars(card), meta: "最终显示内容", tone: "used", links, correlationSegments: segments });
}
function businessDetailPayload(inspector) {
return {
businessSections: inspector?.businessSections || [],
blocks: inspector?.blocks || [],
changes: inspector?.changes ?? null,
};
}
function renderBusinessDetail(inspector, notice, links, segments) {
if (!inspector) return contentBox("业务详情", notice || "该模块没有业务详情。", { tone: "muted", links });
const parts = [];
let segmentIndex = 0;
(inspector.businessSections || []).forEach((section) => {
const segment = segments[segmentIndex++];
parts.push(`${segment ? `${String(segment.number).padStart(2, "0")}` : ""}${escapeHtml(section.title || "内容")}
${renderPlainValue(section.content ?? section.items ?? section)}`);
});
(inspector.blocks || []).forEach((block) => {
const segment = segments[segmentIndex++];
parts.push(`${segment ? `${String(segment.number).padStart(2, "0")}` : ""}${escapeHtml(block.title || block.type || "决策内容")}
${renderPlainValue(block.value ?? block.items ?? block)}`);
});
if (inspector.changes) {
const segment = segments[segmentIndex];
parts.push(`${segment ? `${String(segment.number).padStart(2, "0")}` : ""}产出与改动
${renderPlainValue(inspector.changes)}`);
}
const payload = businessDetailPayload(inspector);
return contentBox("业务详情", payload, {
html: parts.length ? `${parts.join("")}
` : `本次运行没有记录业务详情。
`,
characters: chars(payload),
meta: "Inspector 最终显示内容",
tone: "used",
links,
correlationSegments: segments,
});
}
function renderTechnicalDetail(inspector, notice, links, segments) {
if (!inspector || !Object.prototype.hasOwnProperty.call(inspector, "technical")) {
return contentBox("技术详情", notice || "该模块没有技术详情。", { tone: "muted", links });
}
const technical = inspector.technical;
const html = technical && typeof technical === "object"
? `${Object.entries(technical).map(([label, value], index) => {
const segment = segments[index];
return `${segment ? `${String(segment.number).padStart(2, "0")}` : ""}${escapeHtml(label)}
${renderPlainValue(value)}`;
}).join("")}
`
: renderPlainValue(technical);
return contentBox("技术详情", technical, { html, meta: "Inspector 最终显示内容", tone: "read", links, correlationSegments: segments });
}
function renderReadGroup(group, registry, segments) {
const entry = registry.get(group.key);
const usageAreas = [...group.usageAreas];
const count = group.items.reduce((sum, item) => sum + Number(item.characters || 0), 0);
const correlated = segments.filter((segment) => group.items.some((item) => matchingSegments(item.value, [segment]).length));
const html = group.items.map((read) => {
const status = `${escapeHtml(labelForDisposition(read.disposition))}`;
const ratio = read.usageRatio?.label || "不可直接计算";
const field = read.fieldPath && read.fieldPath !== "$" ? `读取字段:${read.fieldPath}` : "读取完整记录";
const exactRef = (read.contextRefs || []).find((ref) => ref.exact);
const locateLabel = exactRef ? "定位原始字段" : "定位原始记录";
return `${status}${escapeHtml(read.transformation || relationLabel(read.relation))}使用比例 ${escapeHtml(ratio)}
${renderContextValue(read.value, "", [], [], correlated)}
`;
}).join("");
const detail = sourceDetail(group.source);
return contentBox(sourceName(group.source), group.items.map((item) => item.value), { html, characters: count, meta: [detail, `${group.items.length} 项读取`].filter(Boolean).join(" · "), tone: sourceKind(group.source), links: entry ? [entry] : [], usageAreas: usageAreas.length ? usageAreas : ["未进入界面"], correlationSegments: correlated });
}
function refsForContext(groups, context) {
return groups.flatMap((group) => group.items).flatMap((read) =>
(read.contextRefs || [])
.filter((ref) => ref.contextId === context.id)
.map((ref) => ({ read, ref }))
);
}
function renderContext(context, groups, registry, segments) {
const source = context.source || {};
const entry = registry.get(context.sourceKey);
const meta = [context.title, sourceDetail(source), relationLabel(context.match), completenessLabel(context.completeness)].filter(Boolean).join(" · ");
const readPaths = Array.isArray(context.readPaths) ? context.readPaths : [];
const usedFragments = Array.isArray(context.usedFragments) ? context.usedFragments : [];
const correlated = matchingSegments(context.content, segments);
const exactFields = refsForContext(groups, context)
.filter(({ ref }) => ref.contextFieldPath)
.map(({ read, ref }) => ({ read, ref, value: resolveFieldPath(context.content, ref.contextFieldPath) }))
.filter(({ value }) => value !== undefined);
const raw = exactFields.length
? `第二列对应的原始字段
${exactFields.map(({ read, ref, value }) => renderRawField(read, value, segments, [], ref)).join("")}`
: "";
const html = renderContextValue(context.content, "", readPaths, usedFragments, correlated);
return contentBox(sourceName(source), context.content, { html: `${raw}${html}
`, characters: context.characters, meta, tone: context.completeness === "complete" ? sourceKind(source) : "warning", links: entry ? [entry] : [], sourceKeys: [context.relatedSourceKey], correlationSegments: correlated });
}
function renderReads(groups, registry, segments) {
return groups.map((group) => renderReadGroup(group, registry, segments)).join("") || contentBox("读取来源", "这一部分没有可确认的读取数据。", { tone: "muted" });
}
function renderContexts(contexts, groups, registry, segments) {
const matched = filterContexts(contextsForGroups(contexts, groups));
return matched.map((context) => renderContext(context, groups, registry, segments)).join("") || contentBox("原始记录与上下文", "这一部分没有可定位的原始记录或日志上下文。", { tone: "muted" });
}
function compactSourceLabel(group) {
const source = group.source || {};
const event = group.items.map((item) => item.value).find((value) => value && typeof value === "object" && value.event_name);
const eventName = source.eventName || event?.event_name;
const record = source.eventId !== null && source.eventId !== undefined ? `Event ${source.eventId}` : source.recordId !== null && source.recordId !== undefined ? `记录 ${source.recordId}` : "";
return [sourceKindLabel(sourceKind(source)), eventName || source.functionName || source.table, record].filter(Boolean).join(" · ");
}
function previewWithMatch(value, needle) {
const preview = excerptAround(value, needle, 300);
if (!needle || !preview.includes(needle)) return escapeHtml(preview);
const index = preview.indexOf(needle);
return `${escapeHtml(preview.slice(0, index))}${escapeHtml(needle)}${escapeHtml(preview.slice(index + needle.length))}`;
}
function renderValueDisclosure(value, label = "实际读取值", needle = "") {
const count = chars(value);
if (count <= 260 && typeof parseStructuredText(value) !== "object") {
return `${escapeHtml(label)}${previewWithMatch(value, needle)}
`;
}
return `${escapeHtml(label)}${count.toLocaleString("zh-CN")} 字 · 展开全文${previewWithMatch(value, needle)}
${renderPlainValue(value)}
`;
}
function renderCompactSource(group, segments) {
const unique = new Map();
group.items.forEach((read) => {
if (read.value === null || read.value === undefined || read.value === "") return;
const key = read.readId || `${read.fieldPath || "$"}::${jsonText(read.value)}`;
if (!unique.has(key)) unique.set(key, read);
});
const items = [...unique.values()];
const visible = items.slice(0, 8);
const hidden = items.length - visible.length;
return `
${visible.map((read) => {
const related = matchingSegments(read.value, segments);
const ids = related.map((segment) => segment.id).join(" ");
const tone = related[0] ? ` correspondenceTarget correlation-${related[0].color}` : "";
const exactRef = (read.contextRefs || []).find((ref) => ref.exact);
return `${escapeHtml(read.label || "读取字段")}${escapeHtml(read.fieldPath || "$" )}${correlationLinks(related)}
${renderValueDisclosure(read.value, "实际读取值", related[0]?.text || "")}${related.length ? "" : `
参与展示处理,但无法逐字定位到左侧单一字段`}
`;
}).join("")}
${hidden > 0 ? `另有 ${hidden} 个读取字段,可在完整记录模式查看。` : ""}
`;
}
function renderCompactReads(groups, contexts, segments) {
if (!groups.length) return `当前来源筛选下没有业务读取字段。
`;
const items = groups.map((group) => renderCompactSource(group, segments)).join("");
return `字段来源
${groups.reduce((sum, group) => sum + group.items.length, 0)} 个实际读取字段${items}
`;
}
function contextDigest(context) {
const content = context.content && typeof context.content === "object" ? context.content : {};
const eventName = content.event_name || context.source?.eventName || context.source?.functionName || context.title;
const eventId = context.source?.eventId;
return { context, eventName, eventId };
}
function resolveFieldPath(value, fieldPath) {
if (!fieldPath || fieldPath === "$" || fieldPath === "event") return value;
const parts = String(fieldPath).replace(/^\$\.?/, "").replaceAll("[]", "").split(".").filter(Boolean);
let current = value;
for (const part of parts) {
current = parseStructuredText(current);
if (!current || typeof current !== "object" || !(part in current)) return undefined;
current = current[part];
}
return parseStructuredText(current);
}
function renderRawField(read, value, segments, derivedSegments, ref = {}) {
const exact = matchingSegments(value, segments);
const related = exact.length ? exact : derivedSegments;
const ids = related.map((segment) => segment.id).join(" ");
const tone = related[0] ? ` correlation-${related[0].color}` : "";
const relation = ref.relation === "record-only" ? "只能定位到记录" : ref.relation === "transformed" ? "函数处理结果" : "精确字段对应";
return `${renderValueDisclosure(value, "原始字段值", exact[0]?.text || "")}
`;
}
function renderCompactContexts(contexts, groups, segments) {
const matched = filterContexts(contextsForGroups(contexts, groups));
if (!matched.length) return `当前来源筛选下没有相关原始记录。
`;
const grouped = new Map();
matched.forEach((context) => {
const kind = sourceKind(context.source);
const key = `${kind}::${context.sourceKey || context.id}`;
if (!grouped.has(key)) grouped.set(key, { context, contexts: [], eventIds: [] });
const bucket = grouped.get(key);
bucket.contexts.push(context);
if (context.source?.eventId !== null && context.source?.eventId !== undefined) bucket.eventIds.push(context.source.eventId);
});
const html = [...grouped.values()].map((bucket) => {
const context = bucket.context;
const kind = sourceKind(context.source);
const digest = contextDigest(context);
const records = bucket.eventIds.length ? `Event ${[...new Set(bucket.eventIds)].join("、")}` : sourceDetail(context.source);
const sourceKey = context.sourceKey || "";
const relatedGroups = groups.filter((group) => group.key === sourceKey || (context.relatedSourceKey && group.key === context.relatedSourceKey));
const reads = relatedGroups.flatMap((group) => group.items);
const uniqueReads = new Map();
reads.forEach((read) => {
(read.contextRefs || []).forEach((ref) => {
const targetContext = bucket.contexts.find((candidate) => candidate.id === ref.contextId);
if (!targetContext) return;
const value = resolveFieldPath(targetContext.content, ref.contextFieldPath);
if (value === undefined) return;
const key = `${read.readId || "read"}::${ref.contextId}::${ref.contextFieldPath}`;
if (!uniqueReads.has(key)) uniqueReads.set(key, { read, value, ref });
});
});
if (kind === "log") return `日志${escapeHtml(digest.eventName)}${escapeHtml(records || `${context.characters || 0} 字`)}${renderValueDisclosure(context.content, "锚定日志上下文")}`;
const fields = [...uniqueReads.values()].slice(0, 6);
const hidden = uniqueReads.size - fields.length;
return `${escapeHtml(sourceKindLabel(kind))}${escapeHtml(digest.eventName)}${escapeHtml(records)}原始记录位置${escapeHtml(context.source?.table || sourceName(context.source))}${context.source?.recordId !== null && context.source?.recordId !== undefined ? `#${escapeHtml(context.source.recordId)}` : ""}${context.source?.eventId !== null && context.source?.eventId !== undefined ? `Event ${escapeHtml(context.source.eventId)}` : ""}
${fields.length ? `${fields.map(({ read, value, ref }) => renderRawField(read, value, segments, [], ref)).join("")}
` : `这条记录参与了处理,但没有可确认的独立读取字段。
`}${hidden > 0 ? `另有 ${hidden} 个字段,可在完整记录模式查看。` : ""}`;
}).join("");
return ``;
}
function correspondenceRow(kind, actualUse, groups, contextGroups, contexts, registry, segments, compact) {
return `
${compact ? "字段来源与读取" : "模块实际读取"}
${compact ? renderCompactReads(groups, contexts, segments) : renderReads(groups, registry, segments)}
${compact ? "原始字段与记录" : "原始记录与上下文"}
${compact ? renderCompactContexts(contexts, contextGroups, segments) : renderContexts(contexts, contextGroups, registry, segments)}
`;
}
function renderPlainValue(value) {
if (value === null || value === undefined || value === "") return `空`;
if (typeof value === "object") return `${escapeHtml(jsonText(value))}`;
return `${escapeHtml(String(value)).replaceAll("\n", "
")}
`;
}
function renderContextValue(value, path, readPaths, snippets, segments = []) {
if (Array.isArray(value)) return `${value.map((item, index) => `- ${renderContextValue(item, `${path}[${index}]`, readPaths, snippets, segments)}
`).join("")}
`;
if (value && typeof value === "object") return `${Object.entries(value).map(([key, nested]) => {
const childPath = path ? `${path}.${key}` : key;
const read = readPaths.some((item) => item === "$" || childPath === item || childPath.endsWith(`.${item}`) || item.startsWith(`${childPath}.`));
const correlated = matchingSegments(nested, segments).length > 0;
return `- ${escapeHtml(key)}
- ${renderContextValue(nested, childPath, readPaths, snippets, segments)}
`;
}).join("")}
`;
const text = String(value ?? "");
return `${highlightCorrelations(text, segments) || highlightUsed(text, snippets)}`;
}
function highlightCorrelations(text, segments) {
const matches = [];
segments.forEach((segment) => {
if (!segment.text || segment.text.length < 2) return;
let from = 0;
let found = 0;
while (from <= text.length && found < 24) {
const index = text.indexOf(segment.text, from);
if (index < 0) break;
matches.push({ start: index, end: index + segment.text.length, segment });
from = index + Math.max(segment.text.length, 1);
found += 1;
}
});
if (!matches.length) return "";
matches.sort((a, b) => a.start - b.start || (b.end - b.start) - (a.end - a.start));
const accepted = [];
let cursor = -1;
matches.forEach((match) => {
if (match.start < cursor) return;
accepted.push(match);
cursor = match.end;
});
let output = "";
cursor = 0;
accepted.forEach((match) => {
output += escapeHtml(text.slice(cursor, match.start));
output += `${escapeHtml(text.slice(match.start, match.end))}`;
cursor = match.end;
});
output += escapeHtml(text.slice(cursor));
return output.replaceAll("\n", "
");
}
function highlightUsed(text, snippets) {
const candidates = (Array.isArray(snippets) ? snippets : []).filter((snippet) => snippet && snippet.length >= 8 && text.includes(snippet)).sort((a, b) => b.length - a.length);
if (!candidates.length) return escapeHtml(text).replaceAll("\n", "
");
const snippet = candidates[0];
const index = text.indexOf(snippet);
return `${escapeHtml(text.slice(0, index))}${escapeHtml(snippet)}${escapeHtml(text.slice(index + snippet.length))}`.replaceAll("\n", "
");
}
function moduleRow(module) {
const path = (module.path || []).join(" / ");
const variant = module.displayVariant === "collapsed-summary" ? `折叠态摘要` : "";
return `
${escapeHtml(path)}
${escapeHtml(module.title)}
${variant}${escapeHtml(module.id)}${Number(module.cardCharacters || 0).toLocaleString("zh-CN")} 字卡片内容正在自动加载
`;
}
function populateModuleJump(modules) {
if (!moduleJump) return;
const groups = new Map();
modules.forEach((module) => {
const group = module.group || "其他";
if (!groups.has(group)) groups.set(group, []);
groups.get(group).push(module);
});
const options = [...groups.entries()].map(([group, items]) => ``).join("");
moduleJump.innerHTML = `${options}`;
moduleJump.disabled = modules.length === 0;
}
function jumpToModule(moduleId, options = {}) {
if (!moduleId) return;
const row = [...document.querySelectorAll(".moduleRow")].find((item) => item.dataset.moduleId === moduleId);
if (!row) return;
const reduceMotion = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
row.scrollIntoView({ behavior: reduceMotion || options.instant ? "auto" : "smooth", block: "start" });
row.focus({ preventScroll: true });
row.classList.remove("isJumpTarget");
void row.offsetWidth;
row.classList.add("isJumpTarget");
window.setTimeout(() => row.classList.remove("isJumpTarget"), 1600);
if (options.updateHash !== false) history.replaceState(null, "", `#module=${encodeURIComponent(moduleId)}`);
}
function renderModuleDetail(row, detail) {
const reads = detail.actualReads || [];
const contexts = detail.contexts || [];
const registry = buildSourceRegistry(reads, contexts);
const sourceGroups = groupReads(reads);
const cardContextGroups = groupsForAreas(sourceGroups, ["卡片"]);
const businessContextGroups = groupsForAreas(sourceGroups, ["Inspector 业务详情", "Inspector 产出与改动"]);
const assigned = new Set([...cardContextGroups, ...businessContextGroups].map((group) => group.key));
const technicalContextGroups = [
...groupsForAreas(sourceGroups, ["Inspector 技术详情", "模块处理"]),
...sourceGroups.filter((group) => !assigned.has(group.key) && group.usageAreas.size === 0),
].filter((group, index, all) => all.findIndex((candidate) => candidate.key === group.key) === index);
const cardGroups = filterGroups(cardContextGroups);
const businessGroups = filterGroups(businessContextGroups);
const technicalGroups = filterGroups(technicalContextGroups);
const cardLinks = referencesForGroups(cardGroups, registry);
const businessLinks = referencesForGroups(businessGroups, registry);
const technicalLinks = referencesForGroups(technicalGroups, registry);
const cardFieldSegments = cardSegments(detail.actualUse?.card);
const businessFieldSegments = businessSegments(detail.actualUse?.inspector);
const technicalFieldSegments = technicalSegments(detail.actualUse?.inspector);
const compact = onlyUsedFields?.checked !== false;
const rows = [
correspondenceRow("卡片", renderCard(detail.actualUse?.card, cardLinks, cardFieldSegments), cardGroups, cardContextGroups, contexts, registry, cardFieldSegments, compact),
correspondenceRow("业务详情", renderBusinessDetail(detail.actualUse?.inspector, detail.actualUse?.inspectorNotice, businessLinks, businessFieldSegments), businessGroups, businessContextGroups, contexts, registry, businessFieldSegments, compact),
correspondenceRow("技术详情", renderTechnicalDetail(detail.actualUse?.inspector, detail.actualUse?.inspectorNotice, technicalLinks, technicalFieldSegments), technicalGroups, technicalContextGroups, contexts, registry, technicalFieldSegments, compact),
];
const target = row.querySelector(".moduleDeferred, .moduleColumns, .correspondenceRows");
if (target) target.outerHTML = `${rows.join("")}
`;
row.classList.remove("hasSourceFocus", "hasCorrelationFocus");
delete row.dataset.activeSource;
delete row.dataset.activeCorrelation;
row.classList.add("isLoaded");
const state = row.querySelector(".moduleStreamState");
if (state) state.textContent = "已加载";
}
function updateLoadProgress() {
if (!runMeta) return;
const loadedCount = detailCache.size;
const renderedCount = renderedModules.size;
const progress = moduleTotal ? `${loadedCount}/${moduleTotal} 数据` : "正在建立模块目录";
const dataDone = streamComplete && loadedCount >= moduleTotal;
const renderDone = renderedCount >= moduleTotal;
const state = dataDone && renderDone ? "全部加载完成" : dataDone ? `正在展示 ${renderedCount}/${moduleTotal}` : "正在加载全部数据";
runSummary.innerHTML = `${progress}${Number(runMeta.summary?.groupCount || 0)} 个流程分组卡片内容 ${Number(runMeta.summary?.cardCharacters || 0).toLocaleString("zh-CN")} 字${state}`;
}
function drainRenderQueue() {
renderScheduled = false;
const started = performance.now();
while (renderQueue.length && performance.now() - started < 10) {
const { moduleId, detail } = renderQueue.shift();
const row = document.querySelector(`.moduleRow[data-module-id="${CSS.escape(moduleId)}"]`);
if (row) {
renderModuleDetail(row, detail);
renderedModules.add(moduleId);
}
}
updateLoadProgress();
if (renderQueue.length) scheduleRenderQueue();
}
function scheduleRenderQueue() {
if (renderScheduled) return;
renderScheduled = true;
requestAnimationFrame(drainRenderQueue);
}
function enqueueDetail(moduleId, detail) {
detailCache.set(moduleId, detail);
renderQueue.push({ moduleId, detail });
updateLoadProgress();
scheduleRenderQueue();
}
function refreshLoadedModules() {
const compact = onlyUsedFields?.checked !== false;
if (readColumnTitle) readColumnTitle.textContent = compact ? "字段来源与读取" : "模块实际读取";
if (contextColumnTitle) contextColumnTitle.textContent = compact ? "原始字段与记录" : "原始记录与上下文";
const entries = [...detailCache.entries()];
let cursor = 0;
const renderBatch = () => {
const limit = Math.min(cursor + 3, entries.length);
for (; cursor < limit; cursor += 1) {
const [moduleId, detail] = entries[cursor];
const row = document.querySelector(`.moduleRow[data-module-id="${CSS.escape(moduleId)}"]`);
if (row) renderModuleDetail(row, detail);
}
if (cursor < entries.length) requestAnimationFrame(renderBatch);
};
requestAnimationFrame(renderBatch);
}
function locateOriginalField(target) {
const row = target.closest(".moduleRow");
if (!row) return;
const readId = target.dataset.locateReadId;
const match = readId
? row.querySelector(`[data-column="context"] [data-read-ids~="${CSS.escape(readId)}"]`)
: null;
if (!match) return;
const correlationId = target.dataset.locateCorrelation;
if (correlationId) activateCorrelation(row, correlationId, target);
match.classList.remove("isLocated");
void match.offsetWidth;
match.classList.add("isLocated");
match.scrollIntoView({ behavior: window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth", block: "center", inline: "nearest" });
match.focus({ preventScroll: true });
window.setTimeout(() => match.classList.remove("isLocated"), 1800);
}
function setSourceFocus(target) {
const row = target.closest(".moduleRow");
if (!row) return;
const key = target.dataset.sourceKey;
const alreadyActive = row.dataset.activeSource === key;
row.querySelectorAll(".sourceTag").forEach((tag) => tag.setAttribute("aria-pressed", "false"));
row.querySelectorAll(".contentBox").forEach((box) => box.classList.remove("isSourceMatch"));
row.classList.remove("hasSourceFocus");
delete row.dataset.activeSource;
if (alreadyActive || !key) return;
row.dataset.activeSource = key;
row.classList.add("hasSourceFocus");
row.querySelectorAll(".sourceTag").forEach((tag) => {
if (tag.dataset.sourceKey === key) tag.setAttribute("aria-pressed", "true");
});
row.querySelectorAll(".contentBox").forEach((box) => {
const keys = (box.dataset.sourceKeys || "").split(" ");
if (keys.includes(key)) box.classList.add("isSourceMatch");
});
}
function activateCorrelation(row, id, colorTarget) {
const alreadyActive = row.dataset.activeCorrelation === id;
row.querySelectorAll("[data-correlation-id], [data-correlation-ids]").forEach((item) => {
item.classList.remove("isCorrelationMatch");
item.setAttribute("aria-pressed", "false");
});
row.classList.remove("hasCorrelationFocus");
delete row.dataset.activeCorrelation;
row.style.removeProperty("--active-correlation-color");
if (alreadyActive || !id) return;
row.dataset.activeCorrelation = id;
row.classList.add("hasCorrelationFocus");
const activeColor = getComputedStyle(colorTarget).getPropertyValue("--correlation-color").trim();
if (activeColor) row.style.setProperty("--active-correlation-color", activeColor);
row.querySelectorAll(`[data-correlation-id="${CSS.escape(id)}"], [data-correlation-ids~="${CSS.escape(id)}"]`).forEach((item) => {
item.classList.add("isCorrelationMatch");
item.setAttribute("aria-pressed", "true");
const details = item.closest("details");
if (details) details.open = true;
});
}
function setCorrelationFocus(target) {
const row = target.closest(".moduleRow");
if (!row) return;
activateCorrelation(row, target.dataset.correlationId, target);
}
root.addEventListener("click", (event) => {
const locateTarget = event.target.closest("[data-locate-read-id]");
if (locateTarget) {
event.preventDefault();
event.stopPropagation();
locateOriginalField(locateTarget);
return;
}
const correlationTarget = event.target.closest("[data-correlation-id]");
if (correlationTarget) {
event.preventDefault();
event.stopPropagation();
setCorrelationFocus(correlationTarget);
return;
}
const target = event.target.closest(".sourceTag");
if (!target) return;
event.preventDefault();
event.stopPropagation();
setSourceFocus(target);
});
root.addEventListener("keydown", (event) => {
if (event.key !== "Enter" && event.key !== " ") return;
const locateTarget = event.target.closest("[data-locate-read-id]");
if (locateTarget) {
event.preventDefault();
event.stopPropagation();
locateOriginalField(locateTarget);
return;
}
const correlationTarget = event.target.closest("[data-correlation-id]");
if (correlationTarget) {
event.preventDefault();
event.stopPropagation();
setCorrelationFocus(correlationTarget);
return;
}
const target = event.target.closest(".sourceTag");
if (!target) return;
event.preventDefault();
event.stopPropagation();
setSourceFocus(target);
});
moduleJump?.addEventListener("change", () => jumpToModule(moduleJump.value));
onlyUsedFields?.addEventListener("change", refreshLoadedModules);
sourceFilter?.addEventListener("change", refreshLoadedModules);
function initializeIndex(data) {
runMeta = data;
const modules = data.modules || [];
moduleTotal = modules.length;
updateLoadProgress();
const groups = new Map();
modules.forEach((module) => { if (!groups.has(module.group)) groups.set(module.group, []); groups.get(module.group).push(module); });
root.innerHTML = [...groups.entries()].map(([group, items]) => `${items.map(moduleRow).join("")}`).join("") || `这个 Run 没有可展示的模块。
`;
populateModuleJump(modules);
const initialModule = new URLSearchParams(location.hash.slice(1)).get("module");
if (initialModule) {
moduleJump.value = initialModule;
jumpToModule(initialModule, { instant: true, updateHash: false });
}
}
function handleStreamMessage(message) {
if (message.type === "index") {
initializeIndex(message.data || {});
return;
}
if (message.type === "detail") {
enqueueDetail(String(message.moduleId || message.data?.id || ""), message.data || {});
return;
}
if (message.type === "error") {
const row = document.querySelector(`.moduleRow[data-module-id="${CSS.escape(String(message.moduleId || ""))}"]`);
const target = row?.querySelector(".moduleDeferred");
if (target) target.innerHTML = `读取失败:${escapeHtml(message.message || "未知错误")}
`;
const state = row?.querySelector(".moduleStreamState");
if (state) state.textContent = "读取失败";
return;
}
if (message.type === "complete") {
streamComplete = true;
updateLoadProgress();
}
}
function consumeLines(text, carry = "") {
const lines = `${carry}${text}`.split("\n");
const nextCarry = lines.pop() || "";
lines.forEach((line) => {
if (!line.trim()) return;
handleStreamMessage(JSON.parse(line));
});
return nextCarry;
}
async function start() {
try {
const response = await fetch(`/api/script-builds/${encodeURIComponent(runId)}/module-audit-stream`, { headers: { Accept: "application/x-ndjson" }, cache: "no-store" });
if (!response.ok) throw new Error(`API ${response.status}`);
if (!response.body) {
consumeLines(await response.text());
return;
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let carry = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
carry = consumeLines(decoder.decode(value, { stream: true }), carry);
}
carry = consumeLines(decoder.decode(), carry);
if (carry.trim()) handleStreamMessage(JSON.parse(carry));
} catch (error) {
runSummary.textContent = "读取失败";
root.innerHTML = `无法读取模块数据对照
${escapeHtml(error instanceof Error ? error.message : String(error))}
`;
}
}
start();
})();