| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805 |
- (() => {
- 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) => `<span class="sourceTag source-${escapeHtml(item.kind)}" role="button" tabindex="0" aria-pressed="false" data-source-key="${escapeHtml(item.key)}" title="突出显示三列中的对应内容">${escapeHtml(item.label)}</span>`).join("");
- const usages = usageAreas.map((area) => `<span class="usageTag">${escapeHtml(area)}</span>`).join("");
- return `<span class="sourceLinks">${refs}${usages}</span>`;
- }
- function correlationLinks(segments = []) {
- if (!segments.length) return "";
- return `<span class="correlationLinks" aria-label="字段对应关系">${segments.map((segment) => `<span class="correlationChip correlation-${segment.color}" role="button" tabindex="0" aria-pressed="false" data-correlation-id="${escapeHtml(segment.id)}" title="突出显示三列中的对应字段"><b>${String(segment.number).padStart(2, "0")}</b>${escapeHtml(segment.label)}</span>`).join("")}</span>`;
- }
- function contentBox(title, value, options = {}) {
- const count = options.characters ?? chars(value);
- const long = count > 800;
- const body = options.html ?? `<pre>${escapeHtml(jsonText(value))}</pre>`;
- 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 `<details class="contentBox ${options.tone ? `tone-${options.tone}` : ""}" data-source-keys="${escapeHtml(sourceKeys)}" ${long ? "" : "open"}>
- <summary><span class="summaryLine"><b>${escapeHtml(title)}</b><small>${escapeHtml(meta)}</small></span>${sourceLinks(options.links, options.usageAreas)}${correlationLinks(options.correlationSegments)}${long && options.showPreview ? `<em>${escapeHtml(truncate(value))}</em>` : ""}</summary>
- <div class="contentBody">${body}</div>
- </details>`;
- }
- 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 = `<div class="displayCard"><h4>${escapeHtml(card.title || "卡片")}</h4>${fields.map((field, index) => {
- const segment = segments[index];
- return `<div class="displayField${segment ? ` correspondenceTarget correlation-${segment.color}` : ""}"${targetAttrs(segment)}><b>${segment ? `<span class="correlationNumber">${String(segment.number).padStart(2, "0")}</span>` : ""}${escapeHtml(field.label || "内容")}</b><div>${renderPlainValue(field.value)}</div></div>`;
- }).join("") || `<p class="empty">卡片没有可显示字段。</p>`}</div>`;
- 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(`<section class="businessPart${segment ? ` correspondenceTarget correlation-${segment.color}` : ""}"${targetAttrs(segment)}><h4>${segment ? `<span class="correlationNumber">${String(segment.number).padStart(2, "0")}</span>` : ""}${escapeHtml(section.title || "内容")}</h4>${renderPlainValue(section.content ?? section.items ?? section)}</section>`);
- });
- (inspector.blocks || []).forEach((block) => {
- const segment = segments[segmentIndex++];
- parts.push(`<section class="businessPart${segment ? ` correspondenceTarget correlation-${segment.color}` : ""}"${targetAttrs(segment)}><h4>${segment ? `<span class="correlationNumber">${String(segment.number).padStart(2, "0")}</span>` : ""}${escapeHtml(block.title || block.type || "决策内容")}</h4>${renderPlainValue(block.value ?? block.items ?? block)}</section>`);
- });
- if (inspector.changes) {
- const segment = segments[segmentIndex];
- parts.push(`<section class="businessPart${segment ? ` correspondenceTarget correlation-${segment.color}` : ""}"${targetAttrs(segment)}><h4>${segment ? `<span class="correlationNumber">${String(segment.number).padStart(2, "0")}</span>` : ""}产出与改动</h4>${renderPlainValue(inspector.changes)}</section>`);
- }
- const payload = businessDetailPayload(inspector);
- return contentBox("业务详情", payload, {
- html: parts.length ? `<div class="businessDetailBody">${parts.join("")}</div>` : `<p class="empty">本次运行没有记录业务详情。</p>`,
- 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"
- ? `<div class="technicalDetailBody">${Object.entries(technical).map(([label, value], index) => {
- const segment = segments[index];
- return `<section class="technicalPart${segment ? ` correspondenceTarget correlation-${segment.color}` : ""}"${targetAttrs(segment)}><h4>${segment ? `<span class="correlationNumber">${String(segment.number).padStart(2, "0")}</span>` : ""}${escapeHtml(label)}</h4>${renderPlainValue(value)}</section>`;
- }).join("")}</div>`
- : 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 = `<span class="readStatus status-${escapeHtml(read.disposition)}">${escapeHtml(labelForDisposition(read.disposition))}</span>`;
- 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 `<div class="readItem" data-read-id="${escapeHtml(read.readId || "")}"><div class="readItemHeader"><b>${escapeHtml(read.label || "数据")}</b><code>${escapeHtml(field)}</code></div><div class="readMeta">${status}<span>${escapeHtml(read.transformation || relationLabel(read.relation))}</span><span>使用比例 ${escapeHtml(ratio)}</span><button type="button" class="locateContextButton" data-locate-read-id="${escapeHtml(read.readId || "")}">${locateLabel}</button></div><div class="contextTree readValueTree">${renderContextValue(read.value, "", [], [], correlated)}</div></div>`;
- }).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
- ? `<section class="exactFieldMap"><h4>第二列对应的原始字段</h4>${exactFields.map(({ read, ref, value }) => renderRawField(read, value, segments, [], ref)).join("")}</section>`
- : "";
- const html = renderContextValue(context.content, "", readPaths, usedFragments, correlated);
- return contentBox(sourceName(source), context.content, { html: `${raw}<div class="contextTree">${html}</div>`, 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))}<mark class="valueMatch">${escapeHtml(needle)}</mark>${escapeHtml(preview.slice(index + needle.length))}`;
- }
- function renderValueDisclosure(value, label = "实际读取值", needle = "") {
- const count = chars(value);
- if (count <= 260 && typeof parseStructuredText(value) !== "object") {
- return `<div class="fieldActualValue"><b>${escapeHtml(label)}</b><p>${previewWithMatch(value, needle)}</p></div>`;
- }
- return `<details class="valueDisclosure"><summary><span>${escapeHtml(label)}</span><small>${count.toLocaleString("zh-CN")} 字 · 展开全文</small><em>${previewWithMatch(value, needle)}</em></summary><div class="valueDisclosureBody">${renderPlainValue(value)}</div></details>`;
- }
- 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 `<section class="lineageSource" data-source-key="${escapeHtml(group.key)}">
- <header class="lineageSourceHeader"><span class="sourceKindPill source-${escapeHtml(sourceKind(group.source))}">${escapeHtml(compactSourceLabel(group))}</span></header>
- ${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 `<div class="readValueEntry${tone}" data-read-id="${escapeHtml(read.readId || "")}"${ids ? ` data-correlation-ids="${escapeHtml(ids)}"` : ""}><div class="readValueMeta"><span>${escapeHtml(read.label || "读取字段")}</span><code>${escapeHtml(read.fieldPath || "$" )}</code>${correlationLinks(related)}<button type="button" class="locateContextButton" data-locate-read-id="${escapeHtml(read.readId || "")}" data-locate-correlation="${escapeHtml(related[0]?.id || "")}">${exactRef ? "定位原始字段" : "定位原始记录"}</button></div>${renderValueDisclosure(read.value, "实际读取值", related[0]?.text || "")}${related.length ? "" : `<small class="lineageUnproven">参与展示处理,但无法逐字定位到左侧单一字段</small>`}</div>`;
- }).join("")}
- ${hidden > 0 ? `<small class="moreFieldsNotice">另有 ${hidden} 个读取字段,可在完整记录模式查看。</small>` : ""}
- </section>`;
- }
- function renderCompactReads(groups, contexts, segments) {
- if (!groups.length) return `<div class="compactEmpty">当前来源筛选下没有业务读取字段。</div>`;
- const items = groups.map((group) => renderCompactSource(group, segments)).join("");
- return `<section class="compactPanel"><header><h4>字段来源</h4><span>${groups.reduce((sum, group) => sum + group.items.length, 0)} 个实际读取字段</span></header><div class="lineageSources">${items}</div></section>`;
- }
- 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 `<div class="rawField${tone}" tabindex="-1" data-read-ids="${escapeHtml(read.readId || "")}"${ids ? ` data-correlation-ids="${escapeHtml(ids)}"` : ""}><header><div><span>${escapeHtml(read.label || "读取字段")}</span><code>${escapeHtml(ref.contextFieldPath || read.fieldPath || "完整记录")}</code></div><em class="lineageRelation">${escapeHtml(relation)}</em>${correlationLinks(related)}</header>${renderValueDisclosure(value, "原始字段值", exact[0]?.text || "")}</div>`;
- }
- function renderCompactContexts(contexts, groups, segments) {
- const matched = filterContexts(contextsForGroups(contexts, groups));
- if (!matched.length) return `<div class="compactEmpty">当前来源筛选下没有相关原始记录。</div>`;
- 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 `<article class="sourceDigest tone-log" tabindex="-1" data-source-key="${escapeHtml(context.relatedSourceKey || sourceKey)}"><header><span class="sourceKindPill source-log">日志</span><strong>${escapeHtml(digest.eventName)}</strong><em>${escapeHtml(records || `${context.characters || 0} 字`)}</em></header>${renderValueDisclosure(context.content, "锚定日志上下文")}</article>`;
- const fields = [...uniqueReads.values()].slice(0, 6);
- const hidden = uniqueReads.size - fields.length;
- return `<article class="sourceDigest tone-${escapeHtml(kind)}" tabindex="-1" data-source-key="${escapeHtml(sourceKey)}"><header><span class="sourceKindPill source-${escapeHtml(kind)}">${escapeHtml(sourceKindLabel(kind))}</span><strong>${escapeHtml(digest.eventName)}</strong><em>${escapeHtml(records)}</em></header><div class="recordLocation"><span>原始记录位置</span><code>${escapeHtml(context.source?.table || sourceName(context.source))}</code>${context.source?.recordId !== null && context.source?.recordId !== undefined ? `<code>#${escapeHtml(context.source.recordId)}</code>` : ""}${context.source?.eventId !== null && context.source?.eventId !== undefined ? `<code>Event ${escapeHtml(context.source.eventId)}</code>` : ""}</div>${fields.length ? `<div class="rawFields">${fields.map(({ read, value, ref }) => renderRawField(read, value, segments, [], ref)).join("")}</div>` : `<p>这条记录参与了处理,但没有可确认的独立读取字段。</p>`}${hidden > 0 ? `<small class="moreFieldsNotice">另有 ${hidden} 个字段,可在完整记录模式查看。</small>` : ""}</article>`;
- }).join("");
- return `<section class="compactPanel"><header><h4>原始字段与记录</h4><span>${grouped.size} 组</span></header>${html}</section>`;
- }
- function correspondenceRow(kind, actualUse, groups, contextGroups, contexts, registry, segments, compact) {
- return `<section class="correspondenceRow" data-area="${escapeHtml(kind)}">
- <div class="correspondenceCell" data-column="use"><div class="columnLabel">模块实际使用</div>${actualUse}</div>
- <div class="correspondenceCell" data-column="read"><div class="columnLabel">${compact ? "字段来源与读取" : "模块实际读取"}</div>${compact ? renderCompactReads(groups, contexts, segments) : renderReads(groups, registry, segments)}</div>
- <div class="correspondenceCell" data-column="context"><div class="columnLabel">${compact ? "原始字段与记录" : "原始记录与上下文"}</div>${compact ? renderCompactContexts(contexts, contextGroups, segments) : renderContexts(contexts, contextGroups, registry, segments)}</div>
- </section>`;
- }
- function renderPlainValue(value) {
- if (value === null || value === undefined || value === "") return `<span class="empty">空</span>`;
- if (typeof value === "object") return `<pre>${escapeHtml(jsonText(value))}</pre>`;
- return `<p>${escapeHtml(String(value)).replaceAll("\n", "<br>")}</p>`;
- }
- function renderContextValue(value, path, readPaths, snippets, segments = []) {
- if (Array.isArray(value)) return `<ol>${value.map((item, index) => `<li>${renderContextValue(item, `${path}[${index}]`, readPaths, snippets, segments)}</li>`).join("")}</ol>`;
- if (value && typeof value === "object") return `<dl>${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 `<div class="contextEntry ${read ? "isRead" : "isDim"}${correlated ? " hasCorrelationData" : ""}"><dt>${escapeHtml(key)}</dt><dd>${renderContextValue(nested, childPath, readPaths, snippets, segments)}</dd></div>`;
- }).join("")}</dl>`;
- const text = String(value ?? "");
- return `<span>${highlightCorrelations(text, segments) || highlightUsed(text, snippets)}</span>`;
- }
- 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 += `<mark class="correlationMark correlation-${match.segment.color}" role="button" tabindex="0" data-correlation-id="${escapeHtml(match.segment.id)}" title="对应 ${escapeHtml(match.segment.label)}">${escapeHtml(text.slice(match.start, match.end))}</mark>`;
- cursor = match.end;
- });
- output += escapeHtml(text.slice(cursor));
- return output.replaceAll("\n", "<br>");
- }
- 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", "<br>");
- const snippet = candidates[0];
- const index = text.indexOf(snippet);
- return `${escapeHtml(text.slice(0, index))}<mark>${escapeHtml(snippet)}</mark>${escapeHtml(text.slice(index + snippet.length))}`.replaceAll("\n", "<br>");
- }
- function moduleRow(module) {
- const path = (module.path || []).join(" / ");
- const variant = module.displayVariant === "collapsed-summary" ? `<span class="variant">折叠态摘要</span>` : "";
- return `<article class="moduleRow" data-module-id="${escapeHtml(module.id)}" tabindex="-1">
- <header><div><span>${escapeHtml(path)}</span><h3>${escapeHtml(module.title)}</h3></div><div class="moduleMeta">${variant}<code>${escapeHtml(module.id)}</code><span>${Number(module.cardCharacters || 0).toLocaleString("zh-CN")} 字卡片内容</span><span class="moduleStreamState">正在自动加载</span></div></header>
- <div class="moduleDeferred"><div class="cellLoading"><i></i><i></i></div></div>
- </article>`;
- }
- 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]) => `<optgroup label="${escapeHtml(group)}">${items.map((module) => {
- const path = (module.path || []).filter(Boolean).join(" / ");
- const label = path ? `${module.title} · ${path}` : module.title;
- return `<option value="${escapeHtml(module.id)}">${escapeHtml(label)}</option>`;
- }).join("")}</optgroup>`).join("");
- moduleJump.innerHTML = `<option value="">选择一张卡片…</option>${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 = `<div class="correspondenceRows ${compact ? "isCompact" : "isComplete"}">${rows.join("")}</div>`;
- 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 = `<strong>${progress}</strong><span>${Number(runMeta.summary?.groupCount || 0)} 个流程分组</span><span>卡片内容 ${Number(runMeta.summary?.cardCharacters || 0).toLocaleString("zh-CN")} 字</span><em>${state}</em>`;
- }
- 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]) => `<section class="auditGroup"><header class="groupHeader"><h2>${escapeHtml(group)}</h2><span>${items.length} 个模块</span></header>${items.map(moduleRow).join("")}</section>`).join("") || `<p class="fatal">这个 Run 没有可展示的模块。</p>`;
- 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 = `<p class="loadError">读取失败:${escapeHtml(message.message || "未知错误")}</p>`;
- 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 = `<div class="fatal"><h2>无法读取模块数据对照</h2><p>${escapeHtml(error instanceof Error ? error.message : String(error))}</p></div>`;
- }
- }
- start();
- })();
|