Files
pikasTech-unidesk/scripts/src/hwlab-node-web-observe-runner-sampling-source.ts
T

1123 lines
64 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SPEC: PJ2026-01040111 long-running Workbench observation.
// Responsibility: Runner page sampling, screenshots, DOM digest, response body summaries, and command record source fragment.
export function nodeWebObserveRunnerSamplingSource(): string {
return String.raw`async function preflightSummary() {
return { currentUrl: currentPageUrl(), title: await page.title().catch(() => null), pageId, auth: publicAuth(auth) };
}
async function samplePage(reason, options = {}) {
if (options?.refreshObserver !== false) await maybeRefreshObserverPage(reason);
const groupSeq = sampleSeq + 1;
if (page && !page.isClosed()) {
await sampleOnePage(page, { reason, groupSeq, pageRole: "control", targetPageId: pageId, pageEpoch: controlPageEpoch })
.catch((error) => appendJsonl(files.errors, eventRecord("control-sample-error", { pageRole: "control", pageId, pageEpoch: controlPageEpoch, error: errorSummary(error) })));
await drainPagePerformanceEvents(page, { reason, groupSeq, pageRole: "control", targetPageId: pageId, pageEpoch: controlPageEpoch })
.catch((error) => appendJsonl(files.errors, eventRecord("control-performance-drain-error", { pageRole: "control", pageId, pageEpoch: controlPageEpoch, error: errorSummary(error) })));
}
if (observerPage && !observerPage.isClosed()) {
await sampleOnePage(observerPage, { reason, groupSeq, pageRole: "observer", targetPageId: observerPageId, pageEpoch: observerPageEpoch }).catch((error) => appendJsonl(files.errors, eventRecord("observer-sample-error", { pageRole: "observer", pageId: observerPageId, pageEpoch: observerPageEpoch, error: errorSummary(error) })));
await drainPagePerformanceEvents(observerPage, { reason, groupSeq, pageRole: "observer", targetPageId: observerPageId, pageEpoch: observerPageEpoch })
.catch((error) => appendJsonl(files.errors, eventRecord("observer-performance-drain-error", { pageRole: "observer", pageId: observerPageId, pageEpoch: observerPageEpoch, error: errorSummary(error) })));
}
if (options?.screenshot !== false && screenshotIntervalMs > 0 && Date.now() - lastScreenshotAtMs >= screenshotIntervalMs) {
await captureScreenshot("checkpoint", "jpeg")
.catch((error) => appendJsonl(files.errors, eventRecord("screenshot-error", { pageRole: "control", pageId, error: errorSummary(error) })));
}
await writeHeartbeat({ status: terminalStatus });
}
async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPageId, pageEpoch }) {
sampleSeq += 1;
const evaluateTimeoutMs = Math.max(3000, Math.min(8000, Number(sampleIntervalMs) || 5000));
const dom = await withHardTimeout(targetPage.evaluate((input) => {
const trim = (value, limit = 500) => String(value || "").replace(/\s+/g, " ").trim().slice(0, limit);
const visible = (element) => {
if (!element) return false;
const rect = element.getBoundingClientRect();
const style = window.getComputedStyle(element);
return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
};
const textHashInput = (element) => trim(element.textContent || "", 800);
const loadingTextPattern = /加载中|\bLoading\b/iu;
const loadingUiPattern = /loading-state|loading-spinner|spinner|progress|skeleton|busy|pending/iu;
const codeLikeSelector = "pre,code,.trace-row-body,.trace-row-markdown,.markdown-body,.message-text,[class*='trace-row' i],[class*='terminal' i],[class*='log' i],[class*='output' i]";
const elementTextForLoading = (element) => [
element.textContent || "",
element.getAttribute("aria-label") || "",
element.getAttribute("title") || "",
element.getAttribute("data-testid") || ""
].map((value) => trim(value, 240)).filter(Boolean).join(" ");
const elementLooksLikeLoadingUi = (element) => {
const signal = [
element.getAttribute("class") || "",
element.getAttribute("data-testid") || "",
element.getAttribute("role") || "",
element.getAttribute("aria-busy") || "",
element.getAttribute("aria-label") || "",
element.getAttribute("title") || ""
].join(" ");
return element.getAttribute("aria-busy") === "true" || element.getAttribute("role") === "status" || loadingUiPattern.test(signal);
};
const elementIsCodeLike = (element) => Boolean(element.closest(codeLikeSelector)) && !elementLooksLikeLoadingUi(element);
const hasLoadingText = (element) => {
const text = elementTextForLoading(element);
if (!loadingTextPattern.test(text)) return false;
if (elementIsCodeLike(element)) return false;
return true;
};
const elementDescriptor = (element) => {
if (!element) return null;
const className = String(element.className || "").replace(/\s+/g, " ").trim().split(" ").slice(0, 6).join(" ");
const identityDescendant = element.querySelector("[data-trace-id], [data-message-id], [data-session-id]");
return {
tag: element.tagName.toLowerCase(),
testId: element.getAttribute("data-testid") || null,
role: element.getAttribute("role") || null,
id: element.getAttribute("id") || null,
className: className || null,
status: element.getAttribute("data-status") || element.getAttribute("aria-busy") || null,
sessionId: element.getAttribute("data-session-id") || identityDescendant?.getAttribute("data-session-id") || null,
messageId: element.getAttribute("data-message-id") || identityDescendant?.getAttribute("data-message-id") || null,
traceId: element.getAttribute("data-trace-id") || identityDescendant?.getAttribute("data-trace-id") || null,
ariaLabel: element.getAttribute("aria-label") || null
};
};
const ownerKindFor = (element) => {
const value = [
element.getAttribute("data-testid") || "",
element.getAttribute("class") || "",
element.getAttribute("role") || "",
element.tagName || ""
].join(" ").toLowerCase();
if (/message|turn|agent|assistant/.test(value)) return "turn";
if (/session|rail|sidebar|nav/.test(value)) return "session-nav";
if (/composer|prompt|input|textarea/.test(value)) return "composer";
if (/diagnostic|alert|error|warning/.test(value)) return "diagnostic";
if (/trace|event|terminal|log/.test(value)) return "trace";
if (/performance|metric|chart|table/.test(value)) return "performance";
if (/main|workspace|workbench|root/.test(value)) return "workbench";
return "unknown";
};
const ownerLabelFor = (element) => {
const heading = element.querySelector("h1,h2,h3,h4,[data-testid*='title' i],[class*='title' i],[class*='header' i]");
return trim(
element.getAttribute("aria-label")
|| element.getAttribute("data-testid")
|| (heading ? heading.textContent || "" : "")
|| element.getAttribute("class")
|| element.tagName,
160
);
};
const ownerKeyFor = (element) => {
const descriptor = elementDescriptor(element) || {};
return [
ownerKindFor(element),
descriptor.testId || descriptor.id || descriptor.role || descriptor.className || descriptor.tag || "unknown",
descriptor.sessionId || descriptor.messageId || descriptor.traceId || ""
].filter(Boolean).join(":").slice(0, 240);
};
const collectLoadingNodes = () => {
const candidates = Array.from(document.querySelectorAll("body *"))
.filter(visible)
.filter(hasLoadingText)
.filter((element) => !Array.from(element.children).some((child) => visible(child) && hasLoadingText(child)))
.slice(-80);
return candidates.map((element, index) => {
const rect = element.getBoundingClientRect();
const identityOwner = element.closest('[data-message-id], [data-trace-id], [data-session-id]');
const structuralOwner = element.closest('article.message-card, .message-card, [data-testid*="message" i], [data-testid*="turn" i], [data-testid*="composer" i], [data-testid*="session" i], [class*="composer" i], [class*="session" i], [class*="trace" i], [class*="diagnostic" i], article, section, aside, main, form, [role="status"], [role="alert"], [role="article"], [role="navigation"]');
const owner = identityOwner || structuralOwner || element;
const ownerDescriptor = elementDescriptor(owner);
return {
index,
tag: element.tagName.toLowerCase(),
className: String(element.className || "").slice(0, 180),
testId: element.getAttribute("data-testid"),
role: element.getAttribute("role"),
text: trim(elementTextForLoading(element), 240),
ownerKind: ownerKindFor(owner),
ownerKey: ownerKeyFor(owner),
ownerLabel: ownerLabelFor(owner),
owner: ownerDescriptor,
ownerText: trim(owner.textContent || "", 300),
rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
};
});
};
const sessionIdForElement = (element) => {
const direct = element.getAttribute("data-session-id");
if (direct) return direct;
const href = element.getAttribute("href") || element.closest("a[href]")?.getAttribute("href") || "";
if (!href) return null;
try {
const parsed = new URL(href, location.href);
const match = parsed.pathname.match(/\/(?:workbench|workspace)\/sessions\/([^/?#]+)/u);
return match ? decodeURIComponent(match[1] || "") : null;
} catch {
const match = href.match(/\/(?:workbench|workspace)\/sessions\/([^/?#]+)/u);
return match ? decodeURIComponent(match[1] || "") : null;
}
};
const sessionTitleTextForElement = (element) => {
const titleNode = element.querySelector("[data-testid*='session-title' i], [data-testid*='session-name' i], [class*='session-title' i], [class*='session-name' i], [data-testid*='title' i], [class*='title' i]");
return trim(
(titleNode ? titleNode.textContent || "" : "")
|| element.getAttribute("aria-label")
|| element.getAttribute("title")
|| element.textContent
|| "",
240
);
};
const sessionTitleFallbackPattern = /^(?:Session\s+)?ses_[A-Za-z0-9_.-]+/iu;
const looksLikeSessionTitleFallback = (title, sessionId) => {
const text = trim(title, 240);
const id = String(sessionId || "").trim();
if (!text) return true;
if (sessionTitleFallbackPattern.test(text)) return true;
if (!id) return false;
return text === id || text.startsWith(id) || text.startsWith("Session " + id);
};
const collectSessionRailTitles = () => {
const candidates = Array.from(document.querySelectorAll(".session-tab[data-session-id], [role='tab'][data-session-id], [data-testid*='session' i][data-session-id], a[href*='/workbench/sessions/'], a[href*='/workspace/sessions/']"))
.filter(visible);
const seen = new Set();
const items = [];
for (const element of candidates) {
const sessionId = sessionIdForElement(element);
const titleText = sessionTitleTextForElement(element);
const key = (sessionId || "") + "|" + titleText;
if (seen.has(key)) continue;
seen.add(key);
const rect = element.getBoundingClientRect();
const fallbackTitle = looksLikeSessionTitleFallback(titleText, sessionId);
const dataStatus = element.getAttribute("data-status") || element.getAttribute("data-state") || null;
const dataRunning = element.getAttribute("data-running") || element.getAttribute("data-busy") || null;
const ariaBusy = element.getAttribute("aria-busy") || null;
items.push({
index: items.length,
tag: element.tagName.toLowerCase(),
testId: element.getAttribute("data-testid"),
role: element.getAttribute("role"),
active: element.getAttribute("data-active") === "true" || element.getAttribute("aria-selected") === "true",
status: dataStatus || ariaBusy || null,
dataStatus,
running: dataRunning === "true" || ariaBusy === "true" || element.classList.contains("is-running") || element.classList.contains("running"),
dataRunning,
ariaBusy,
sessionId,
sessionIdPrefix: sessionId ? String(sessionId).slice(0, 12) : null,
titleText,
fallbackTitle,
rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
});
}
const visibleItems = items.slice(0, 120);
const fallbackItems = visibleItems.filter((item) => item.fallbackTitle);
const visibleCount = visibleItems.length;
const fallbackTitleCount = fallbackItems.length;
return {
visibleCount,
fallbackTitleCount,
fallbackTitleRatio: visibleCount > 0 ? Number((fallbackTitleCount / visibleCount).toFixed(4)) : 0,
activeItem: visibleItems.find((item) => item.active) || null,
items: visibleItems.slice(0, 60),
fallbackItems: fallbackItems.slice(0, 12),
};
};
const diagnosticSummaryText = (element) => {
const summarySelectors = [
".api-error-diagnostic-summary-text p",
".api-error-diagnostic-summary-text",
"[class*='diagnostic-summary-text' i] p",
"[class*='diagnostic-summary-text' i]",
"[data-testid*='diagnostic-summary' i]",
"[data-testid*='error-summary' i]",
"[role='alert'] p",
"[role='alert']"
];
const parts = [];
for (const selector of summarySelectors) {
for (const candidate of Array.from(element.querySelectorAll(selector))) {
if (!visible(candidate)) continue;
const text = trim(candidate.textContent || "", 800);
if (text && !parts.includes(text)) parts.push(text);
}
}
const ownText = textHashInput(element);
const text = parts.length > 0 ? parts.join(" ") : ownText;
return text.replace(/\s+(?:!|i诊断|诊断详情)$/u, "").trim();
};
const diagnosticToggleOnly = (element, text) => {
const compact = String(text || "").trim();
if (!/^(?:!|i诊断|诊断|诊断详情)$/u.test(compact)) return false;
const tag = element.tagName.toLowerCase();
const role = element.getAttribute("role");
const aria = element.getAttribute("aria-label") || "";
const title = element.getAttribute("title") || "";
return tag === "button" || role === "button" || /诊断/u.test(aria) || /诊断/u.test(title);
};
const messageBodyTextDetail = (element) => {
const bodySelectors = [
".message-markdown.message-text",
".message-text",
"[data-message-body]",
"[data-testid='message-body']",
"[data-testid*='message-text' i]",
"[data-testid*='final-response' i]"
];
const parts = [];
for (const selector of bodySelectors) {
for (const candidate of Array.from(element.querySelectorAll(selector))) {
if (!visible(candidate)) continue;
const text = trim(candidate.textContent || "", 1200);
if (text && !parts.some((part) => part.text === text)) parts.push({ text, selector });
}
}
if (parts.length > 0) {
return {
text: parts.map((part) => part.text).join(" "),
source: parts[0]?.selector || "body-selector",
candidateCount: parts.length
};
}
return { text: "", source: null, candidateCount: 0 };
};
const stableMessageText = (element) => {
const body = messageBodyTextDetail(element);
if (body.text) return body.text;
const clone = element.cloneNode(true);
for (const selector of [
".message-duration-meta",
".message-activity-meta",
".api-error-diagnostic",
"[class*='diagnostic' i]",
"[class*='trace' i]",
"[data-trace-id]",
"[data-testid*='trace' i]",
"[data-testid*='event' i]",
"[role='status']",
"[role='alert']",
"button"
]) {
for (const child of Array.from(clone.querySelectorAll(selector))) child.remove();
}
return trim(clone.textContent || "", 1200);
};
const numericAttr = (element, names) => {
for (const name of names) {
const raw = element.getAttribute(name);
const value = Number(raw);
if (Number.isFinite(value)) return value;
}
return null;
};
const textAttr = (element, names) => {
for (const name of names) {
const raw = element.getAttribute(name);
if (raw && String(raw).trim()) return String(raw).trim();
}
return null;
};
const summarizeElements = (elements, limit) => elements.filter(visible).slice(-limit).map((element, index) => {
const rect = element.getBoundingClientRect();
const owner = element.closest('article.message-card, .message-card[data-message-id], article[data-message-id], [data-trace-id]');
const timeElement = element.matches("time,[datetime]") ? element : element.querySelector("time,[datetime]");
return {
index,
tag: element.tagName.toLowerCase(),
testId: element.getAttribute("data-testid"),
role: element.getAttribute("role"),
status: element.getAttribute("data-status") || element.getAttribute("aria-busy") || null,
sessionId: element.getAttribute("data-session-id") || owner?.getAttribute("data-session-id") || null,
messageId: element.getAttribute("data-message-id") || owner?.getAttribute("data-message-id") || element.getAttribute("id") || null,
traceId: element.getAttribute("data-trace-id") || owner?.getAttribute("data-trace-id") || null,
turnId: element.getAttribute("data-turn-id") || owner?.getAttribute("data-turn-id") || null,
projectedSeq: numericAttr(element, ["data-projected-seq", "data-projectedseq", "data-seq", "data-sequence", "aria-posinset"]),
sourceSeq: numericAttr(element, ["data-source-seq", "data-sourceseq", "data-source-event-seq"]),
eventSeq: numericAttr(element, ["data-event-seq", "data-eventseq"]),
eventTimestamp: textAttr(element, ["data-event-ts", "data-event-time", "data-timestamp", "datetime"]) || (timeElement ? textAttr(timeElement, ["datetime", "data-event-ts", "data-event-time", "data-timestamp"]) : null),
eventTimeText: timeElement ? trim(timeElement.textContent || "", 80) : null,
eventKind: textAttr(element, ["data-event-kind", "data-kind", "data-label", "data-status"]) || element.getAttribute("aria-label") || null,
text: textHashInput(element),
rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
};
});
const summarize = (selector, limit) => summarizeElements(Array.from(document.querySelectorAll(selector)), limit);
const summarizeMessages = (selector, limit) => Array.from(document.querySelectorAll(selector)).filter(visible).slice(-limit).map((element, index) => {
const rect = element.getBoundingClientRect();
const body = messageBodyTextDetail(element);
return {
index,
tag: element.tagName.toLowerCase(),
testId: element.getAttribute("data-testid"),
role: element.getAttribute("role"),
dataRole: element.getAttribute("data-role"),
status: element.getAttribute("data-status") || element.getAttribute("aria-busy") || null,
sessionId: element.getAttribute("data-session-id") || null,
messageId: element.getAttribute("data-message-id") || element.getAttribute("id") || null,
traceId: element.getAttribute("data-trace-id") || null,
turnId: element.getAttribute("data-turn-id") || null,
durationText: trim(element.querySelector(".message-duration-meta")?.textContent || "", 120),
activityText: trim(element.querySelector(".message-activity-meta")?.textContent || "", 120),
bodyText: body.text,
bodyTextSource: body.source,
bodyTextCandidateCount: body.candidateCount,
text: stableMessageText(element),
rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
};
});
const resourceTimingSample = (entry) => ({
name: entry.name.split(/[?#]/u)[0].slice(0, 240),
initiatorType: entry.initiatorType,
startTime: Math.round(entry.startTime),
duration: Math.round(entry.duration),
workerStart: Math.round(entry.workerStart || 0),
redirectStart: Math.round(entry.redirectStart || 0),
redirectEnd: Math.round(entry.redirectEnd || 0),
fetchStart: Math.round(entry.fetchStart || 0),
domainLookupStart: Math.round(entry.domainLookupStart || 0),
domainLookupEnd: Math.round(entry.domainLookupEnd || 0),
connectStart: Math.round(entry.connectStart || 0),
connectEnd: Math.round(entry.connectEnd || 0),
secureConnectionStart: Math.round(entry.secureConnectionStart || 0),
requestStart: Math.round(entry.requestStart || 0),
responseStart: Math.round(entry.responseStart || 0),
responseEnd: Math.round(entry.responseEnd || 0),
transferSize: Number.isFinite(Number(entry.transferSize)) ? Number(entry.transferSize) : null,
encodedBodySize: Number.isFinite(Number(entry.encodedBodySize)) ? Number(entry.encodedBodySize) : null,
decodedBodySize: Number.isFinite(Number(entry.decodedBodySize)) ? Number(entry.decodedBodySize) : null,
nextHopProtocol: entry.nextHopProtocol || null,
responseStatus: Number.isFinite(Number(entry.responseStatus)) ? Number(entry.responseStatus) : null,
serverTiming: Array.from(entry.serverTiming || []).slice(0, 8).map((item) => ({
name: String(item.name || "").slice(0, 80),
duration: Number.isFinite(Number(item.duration)) ? Math.round(Number(item.duration)) : null,
description: String(item.description || "").slice(0, 120)
})),
});
const opaqueDomId = (value) => String(value || "").trim();
const collectProjectManagement = () => {
const config = input?.projectManagement || {};
const targetPaths = Array.isArray(config.targetPaths) ? config.targetPaths : [];
const path = location.pathname;
const configuredPath = targetPaths.some((target) => path === target || path.startsWith(String(target) + "/"));
const root = document.querySelector('[data-testid="project-management-root"]');
const mdtodoRoot = document.querySelector('[data-testid="project-management-mdtodo"]');
const rootVisible = visible(root);
const mdtodoVisible = visible(mdtodoRoot);
if (!configuredPath && !rootVisible && !mdtodoVisible) return null;
const sourceItems = Array.from(document.querySelectorAll('[data-testid="mdtodo-source-list"] [data-source-id], [data-source-id]')).filter(visible);
const fileItems = Array.from(document.querySelectorAll('[data-testid="mdtodo-file-list"] [data-file-ref], [data-file-ref]')).filter(visible);
const sourceSelect = document.querySelector('[data-testid="mdtodo-source-select"]');
const fileSelect = document.querySelector('[data-testid="mdtodo-file-select"]');
const sourceOptionCount = sourceSelect ? Array.from(sourceSelect.options || []).filter((option) => option.value).length : 0;
const fileOptionCount = fileSelect ? Array.from(fileSelect.options || []).filter((option) => option.value).length : 0;
const fileOptions = fileSelect ? Array.from(fileSelect.options || []).filter((option) => option.value).map((option) => ({
value: option.value,
label: trim(option.textContent || option.label || "", 180),
selected: option.selected === true,
})) : [];
const selectedFileOption = fileOptions.find((option) => option.selected) || null;
const taskItems = Array.from(document.querySelectorAll('[data-testid="mdtodo-task-tree"] [data-task-ref], [data-task-ref]')).filter(visible);
const taskCandidates = Array.from(document.querySelectorAll('[data-testid="mdtodo-task-tree"] li, [data-testid="mdtodo-task-tree"] [role="treeitem"], [data-testid="mdtodo-task-tree"] [role="listitem"]')).filter(visible);
const selectedSource = document.querySelector('[data-source-id][data-selected="true"], [data-source-id][aria-selected="true"], [data-source-id].selected, [data-source-id].is-selected');
const selectedFile = document.querySelector('[data-file-ref][data-selected="true"], [data-file-ref][aria-selected="true"], [data-file-ref].selected, [data-file-ref].is-selected');
const selectedTask = document.querySelector('[data-task-ref][data-selected="true"], [data-task-ref][aria-selected="true"], [data-task-ref].selected, [data-task-ref].is-selected');
const statusCounts = {};
for (const task of taskItems) {
const status = task.getAttribute("data-task-status") || "unknown";
statusCounts[status] = (statusCounts[status] || 0) + 1;
}
const launch = document.querySelector('[data-testid="mdtodo-workbench-launch"], [data-action="launch-workbench"]');
const bodyRendered = document.querySelector('[data-testid="mdtodo-body-rendered"]');
const reportPreview = document.querySelector('[data-testid="mdtodo-report-preview"]');
const reportFullscreen = document.querySelector('[data-testid="mdtodo-report-fullscreen-dialog"]');
const reportLinks = Array.from(document.querySelectorAll('[data-testid="mdtodo-report-link"]')).filter(visible);
const blockers = Array.from(document.querySelectorAll('[data-testid="mdtodo-workbench-launch-blocker"], [data-testid="mdtodo-workbench-launch-error"], [role="alert"]')).filter(visible).slice(0, 12).map((element, index) => ({
index,
testId: element.getAttribute("data-testid"),
role: element.getAttribute("role"),
text: trim(element.textContent || "", 260),
})).filter((item) => item.text);
const workbenchLinks = Array.from(document.querySelectorAll('[data-testid="mdtodo-workbench-link-summary"] li, a[href*="/workbench/sessions/"]')).filter(visible);
const measurePaneGap = (name, paneSelector, contentSelector) => {
const pane = document.querySelector(paneSelector);
if (!visible(pane)) return { name, visible: false };
const rect = pane.getBoundingClientRect();
const contentNodes = Array.from(pane.querySelectorAll(contentSelector)).filter(visible);
const contentBottom = Math.max(rect.top, ...contentNodes.map((element) => element.getBoundingClientRect().bottom));
const bottomGapPx = Math.max(0, Math.round(rect.bottom - contentBottom));
const heightPx = Math.max(0, Math.round(rect.height));
return {
name,
visible: true,
widthPx: Math.max(0, Math.round(rect.width)),
heightPx,
bottomGapPx,
bottomGapRatio: heightPx > 0 ? Number((bottomGapPx / heightPx).toFixed(3)) : 0,
contentNodeCount: contentNodes.length,
};
};
const paneGaps = [
measurePaneGap("task-tree", '[data-testid="mdtodo-task-tree"]', '[data-task-ref], [role="treeitem"], [role="listitem"], li, button, input, select, .task-row-shell, .task-tools'),
measurePaneGap("task-detail", '[data-testid="mdtodo-task-detail"]', '[data-testid="mdtodo-body-rendered"] > *, [data-testid="mdtodo-report-section"], [data-testid="mdtodo-workbench-launch"], [data-testid="mdtodo-delete-task"], [data-testid="mdtodo-task-detail-error"], .mdtodo-detail-header, .task-status-stack > *, .task-document-footer'),
measurePaneGap("report-sidebar", '[data-testid="mdtodo-report-sidebar"]', '[data-testid="mdtodo-report-preview"] > *, [data-testid="mdtodo-report-error"], [data-testid="mdtodo-report-fullscreen"], [data-testid="mdtodo-report-close"], .report-sidebar-header, .report-preview .markdown-body > *'),
];
return {
pageKind: mdtodoVisible || path.startsWith("/projects/mdtodo") ? "project-management-mdtodo" : rootVisible || path === "/projects" || path.startsWith("/projects/") ? "project-management-root" : "project-management-unknown",
configuredPath,
rootVisible,
mdtodoVisible,
sourceCount: Math.max(sourceItems.length, sourceOptionCount),
fileCount: Math.max(fileItems.length, fileOptionCount),
taskCount: taskItems.length,
taskRefMissingCount: Math.max(0, taskCandidates.length - taskItems.length),
selectedSourceId: opaqueDomId(selectedSource?.getAttribute("data-source-id") || sourceSelect?.value),
selectedFileRef: opaqueDomId(selectedFile?.getAttribute("data-file-ref") || fileSelect?.value),
selectedFileLabel: selectedFile ? trim(selectedFile.textContent || "", 180) : selectedFileOption?.label || null,
fileOptionLabels: fileOptions.map((option) => option.label).filter(Boolean).slice(0, 24),
selectedTaskRef: opaqueDomId(selectedTask?.getAttribute("data-task-ref")),
selectedTaskStatus: selectedTask?.getAttribute("data-task-status") || null,
sourceSelectVisible: visible(sourceSelect),
fileSelectVisible: visible(fileSelect),
sourceConfigVisible: visible(document.querySelector('[data-testid="mdtodo-source-form-hwpod"], [data-testid="mdtodo-source-config-dialog"], [role="dialog"]')),
taskEditorVisible: visible(document.querySelector('[data-testid="mdtodo-edit-title"], [data-testid="mdtodo-edit-body"]')),
taskBodyVisible: visible(bodyRendered),
taskBodyText: visible(bodyRendered) ? trim(bodyRendered.textContent || "", 500) : "",
newTaskDraftVisible: visible(document.querySelector('[data-testid="mdtodo-new-title"], [data-testid="mdtodo-new-body"]')),
taskStatusCounts: statusCounts,
reportLinkCount: reportLinks.length,
reportPreviewVisible: visible(reportPreview),
reportPreviewText: visible(reportPreview) ? trim(reportPreview.textContent || "", 500) : "",
reportFullscreenVisible: visible(reportFullscreen),
launchButtonVisible: visible(launch),
launchButtonEnabled: visible(launch) && !launch.disabled && launch.getAttribute("aria-disabled") !== "true",
launchButtonText: trim(launch?.textContent || "", 120),
blockerCount: blockers.length,
blockers,
paneGaps,
workbenchLinkCount: workbenchLinks.length,
valuesRedacted: true,
};
};
const url = location.href;
const routeSessionMatch = url.match(/\/workbench\/sessions\/([^/?#]+)/u);
const activeSession = document.querySelector('[data-active="true"][data-session-id], [aria-selected="true"][data-session-id], .active[data-session-id]');
const activeSessionId = activeSession ? activeSession.getAttribute("data-session-id") : null;
const commandInput = document.querySelector("#command-input");
const commandSubmit = document.querySelector('#command-send, #command-submit, [data-testid="command-submit"], [data-testid="composer-submit"], [data-testid="send-command"]');
const composerWarning = document.querySelector(".composer-warning");
const messageSelector = 'article.message-card, .message-card[data-message-id], article[data-message-id]';
const stableTraceSelector = 'li.trace-render-row[data-row-id], li.trace-render-row[data-testid="trace-render-row"], [data-testid="trace-render-row"][data-row-id]';
const fallbackTraceSelector = '[data-testid*="trace" i], [class*="trace" i], [data-trace-id], [data-testid*="event" i]';
const diagnosticSelector = '.api-error-diagnostic, [class*="api-error-diagnostic" i], [class*="message-diagnostic" i], [class*="projection-diagnostic" i], [data-testid="api-error-diagnostic" i], [data-testid="error-diagnostic" i], [data-testid*="diagnostic" i], [role="alert"], [aria-live="assertive"]';
const messages = summarizeMessages(messageSelector, 80);
const stableTraceElements = Array.from(document.querySelectorAll(stableTraceSelector));
const fallbackTraceElements = Array.from(document.querySelectorAll(fallbackTraceSelector)).filter((element) => element.matches('li,[role="listitem"],[data-testid*="trace-row" i],[data-testid*="event-row" i]'));
const traceRows = summarizeElements(stableTraceElements.length > 0 ? stableTraceElements : fallbackTraceElements, 30);
const loadings = collectLoadingNodes();
const sessionRail = collectSessionRailTitles();
const diagnostics = Array.from(document.querySelectorAll(diagnosticSelector)).filter(visible).slice(-40).map((element, index) => {
const rect = element.getBoundingClientRect();
const text = diagnosticSummaryText(element);
if (!text || diagnosticToggleOnly(element, text)) return null;
const traceMatch = text.match(/\b(?:trace_id=)?(trc_[A-Za-z0-9_-]+|[a-f0-9]{16,64})\b/iu);
const httpStatusMatch = text.match(/\bHTTP\s+([1-5][0-9]{2})\b/iu);
const idleMatch = text.match(/\bidle\s+(\d+)s\b/iu);
const waitingForMatch = text.match(/\bwaitingFor=([^\s;,)]+)/iu);
const lastEventLabelMatch = text.match(/\blastEventLabel=([^\s;,)]+)/iu);
const diagnosticCode = httpStatusMatch ? "http-" + httpStatusMatch[1] : /turn\s*超过|无新活动/iu.test(text) ? "turn-idle-no-activity" : /Failed to fetch/iu.test(text) ? "failed-to-fetch" : "diagnostic";
return {
index,
tag: element.tagName.toLowerCase(),
className: String(element.className || "").slice(0, 240),
testId: element.getAttribute("data-testid"),
role: element.getAttribute("role"),
compact: element.getAttribute("data-compact"),
expanded: element.getAttribute("data-expanded") || element.getAttribute("aria-expanded"),
title: element.getAttribute("title"),
diagnosticCode,
traceId: traceMatch?.[1] || null,
httpStatus: httpStatusMatch ? Number(httpStatusMatch[1]) : null,
idleSeconds: idleMatch ? Number(idleMatch[1]) : null,
waitingFor: waitingForMatch?.[1] || null,
lastEventLabel: lastEventLabelMatch?.[1] || null,
text,
rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
};
}).filter(Boolean);
const turns = Array.from(document.querySelectorAll('article.message-card[data-role="agent"], .message-card[data-role="agent"], article[data-role="agent"]')).filter(visible).map((element, index) => {
const rect = element.getBoundingClientRect();
const text = textHashInput(element);
const finalResponse = messageBodyTextDetail(element);
const traceElement = element.matches("[data-trace-id]") ? element : element.querySelector("[data-trace-id]");
const traceMatch = text.match(/\btrc_[A-Za-z0-9_-]+\b/u);
const durationText = trim(element.querySelector(".message-duration-meta")?.textContent || "", 120);
const activityText = trim(element.querySelector(".message-activity-meta")?.textContent || "", 120);
const directTraceId = element.getAttribute("data-trace-id") || traceElement?.getAttribute("data-trace-id") || traceMatch?.[0] || null;
return {
index,
role: element.getAttribute("data-role") || "agent",
status: element.getAttribute("data-status") || null,
sessionId: element.getAttribute("data-session-id") || null,
messageId: element.getAttribute("data-message-id") || element.getAttribute("id") || null,
traceId: directTraceId,
turnId: element.getAttribute("data-turn-id") || directTraceId,
durationText,
activityText,
finalResponseText: finalResponse.text,
finalResponseTextSource: finalResponse.source,
finalResponseCandidateCount: finalResponse.candidateCount,
finalResponsePresent: Boolean(finalResponse.text && finalResponse.text.trim()),
text,
rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
};
}).slice(-80);
const active = document.activeElement;
return {
url,
path: location.pathname,
routeSessionId: routeSessionMatch ? decodeURIComponent(routeSessionMatch[1]) : null,
activeSessionId,
title: document.title,
focus: active ? { tag: active.tagName.toLowerCase(), testId: active.getAttribute("data-testid"), role: active.getAttribute("role") } : null,
viewport: { width: window.innerWidth, height: window.innerHeight, devicePixelRatio: window.devicePixelRatio },
scroll: { x: Math.round(window.scrollX), y: Math.round(window.scrollY), height: Math.round(document.documentElement.scrollHeight), width: Math.round(document.documentElement.scrollWidth) },
messages,
traceRows,
loadings,
sessionRail,
diagnostics,
turns,
composer: {
inputPresent: visible(commandInput),
inputDisabled: Boolean(commandInput?.disabled) || commandInput?.getAttribute("aria-disabled") === "true",
warningPresent: visible(composerWarning),
warningText: trim(composerWarning?.textContent || "", 160),
submitPresent: visible(commandSubmit),
submitDisabled: Boolean(commandSubmit?.disabled) || commandSubmit?.getAttribute("aria-disabled") === "true",
submitAction: commandSubmit?.getAttribute("data-action") || null,
submitText: trim(commandSubmit?.textContent || "", 80),
submitTestId: commandSubmit?.getAttribute("data-testid") || null,
activeStatus: activeSession?.getAttribute("data-status") || null,
valuesRedacted: true
},
projectManagement: collectProjectManagement(),
pageProvenance: {
url: location.href,
path: location.pathname,
title: document.title,
readyState: document.readyState,
timeOrigin: Math.round(performance.timeOrigin || 0),
navigationStartTime: (performance.getEntriesByType("navigation")[0] || null)?.startTime ?? null,
scripts: Array.from(document.scripts).map((element) => {
if (!element.src) return null;
try {
const url = new URL(element.src, location.href);
const keys = Array.from(url.searchParams.keys()).sort();
return url.pathname + (keys.length > 0 ? "?keys=" + keys.join(",") : "");
} catch {
return null;
}
}).filter(Boolean).sort(),
stylesheets: Array.from(document.querySelectorAll('link[rel~="stylesheet"][href]')).map((element) => {
try {
const url = new URL(element.href, location.href);
const keys = Array.from(url.searchParams.keys()).sort();
return url.pathname + (keys.length > 0 ? "?keys=" + keys.join(",") : "");
} catch {
return null;
}
}).filter(Boolean).sort(),
meta: Array.from(document.querySelectorAll("meta[name], meta[property]")).map((element) => ({
key: String(element.getAttribute("name") || element.getAttribute("property") || "").slice(0, 120),
content: String(element.getAttribute("content") || "").slice(0, 200),
})).filter((item) => item.key).sort((a, b) => a.key.localeCompare(b.key)),
},
performance: performance.getEntriesByType("resource").slice(-80).map(resourceTimingSample),
};
}, { projectManagement }), evaluateTimeoutMs, "sampleOnePage DOM evaluate exceeded " + evaluateTimeoutMs + "ms").catch((error) => ({ error: errorSummary(error), url: pageUrl(targetPage) }));
const sample = {
seq: sampleSeq,
sampleGroupSeq: groupSeq,
ts: new Date().toISOString(),
reason,
pageRole,
pageId: targetPageId,
pageEpoch: Number.isFinite(Number(pageEpoch)) ? Number(pageEpoch) : 0,
commandId: activeCommandId,
observerInitiated: false,
...digestDom(dom, pageRole),
};
await appendJsonl(files.samples, sample);
}
function digestDom(dom, pageRole = "control") {
if (dom && dom.error) return dom;
const messages = Array.isArray(dom.messages) ? dom.messages.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 160), textBytes: Buffer.byteLength(item.text || ""), bodyTextHash: sha256Text(item.bodyText || ""), bodyTextPreview: truncate(item.bodyText || "", 160), bodyTextBytes: Buffer.byteLength(item.bodyText || ""), bodyTextPresent: Boolean(String(item.bodyText || "").trim()) })) : [];
const traceRows = Array.isArray(dom.traceRows) ? dom.traceRows.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 160), textBytes: Buffer.byteLength(item.text || "") })) : [];
const loadings = Array.isArray(dom.loadings) ? dom.loadings.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 160), textBytes: Buffer.byteLength(item.text || ""), ownerTextHash: sha256Text(item.ownerText || ""), ownerTextPreview: truncate(item.ownerText || "", 160) })) : [];
const sessionRail = digestSessionRail(dom.sessionRail);
const diagnostics = Array.isArray(dom.diagnostics) ? dom.diagnostics.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 260), textBytes: Buffer.byteLength(item.text || "") })) : [];
const turns = Array.isArray(dom.turns) ? dom.turns.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 200), textBytes: Buffer.byteLength(item.text || ""), finalResponseTextHash: sha256Text(item.finalResponseText || ""), finalResponseTextPreview: truncate(item.finalResponseText || "", 200), finalResponseTextBytes: Buffer.byteLength(item.finalResponseText || ""), finalResponsePresent: Boolean(String(item.finalResponseText || "").trim()) })) : [];
const projectManagementSample = digestProjectManagement(dom.projectManagement);
const pageProvenance = normalizePageProvenance(dom.pageProvenance, { reason: "sample", pageLoadSeq: currentPageProvenance?.pageLoadSeq ?? pageLoadSeq });
if (pageRole === "control") currentPageProvenance = pageProvenance;
return { ...dom, messages, traceRows, loadings, sessionRail, diagnostics, turns, projectManagement: projectManagementSample, pageProvenance: compactPageProvenance(pageProvenance) };
}
function digestProjectManagement(value) {
if (!value || typeof value !== "object") return null;
const opaque = (raw) => {
const text = String(raw || "");
if (!text) return null;
return {
hash: sha256Text(text),
preview: text.length <= 18 ? text : text.slice(0, 10) + "..." + text.slice(-5),
bytes: Buffer.byteLength(text),
valuesRedacted: true
};
};
const textDigest = (raw, limit = 160) => {
const text = String(raw || "");
return { textHash: sha256Text(text), textPreview: truncate(text, limit), textBytes: Buffer.byteLength(text), valuesRedacted: true };
};
const fileLabelLooksDirect = (label) => /^[^/\\]+\.md$/iu.test(String(label || "").trim());
const suspiciousFileLabel = (label) => {
const text = String(label || "").trim();
return Boolean(text && (!fileLabelLooksDirect(text) || /(?:details\/|_Task_Report|_log_|\/)/iu.test(text)));
};
const fileLabels = Array.isArray(value.fileOptionLabels) ? value.fileOptionLabels.map((item) => String(item || "")).filter(Boolean) : [];
return {
pageKind: value.pageKind ?? null,
configuredPath: value.configuredPath === true,
rootVisible: value.rootVisible === true,
mdtodoVisible: value.mdtodoVisible === true,
sourceCount: Number.isFinite(Number(value.sourceCount)) ? Number(value.sourceCount) : 0,
fileCount: Number.isFinite(Number(value.fileCount)) ? Number(value.fileCount) : 0,
taskCount: Number.isFinite(Number(value.taskCount)) ? Number(value.taskCount) : 0,
taskRefMissingCount: Number.isFinite(Number(value.taskRefMissingCount)) ? Number(value.taskRefMissingCount) : 0,
selectedSourceId: opaque(value.selectedSourceId),
selectedFileRef: opaque(value.selectedFileRef),
selectedFileLabel: value.selectedFileLabel ? textDigest(value.selectedFileLabel, 120) : null,
selectedFileLabelLooksDirect: value.selectedFileLabel ? fileLabelLooksDirect(value.selectedFileLabel) : null,
fileOptionLabelSamples: fileLabels.slice(0, 10).map((item) => textDigest(item, 120)),
fileOptionSuspiciousLabelCount: fileLabels.filter(suspiciousFileLabel).length,
selectedTaskRef: opaque(value.selectedTaskRef),
selectedTaskStatus: value.selectedTaskStatus ?? null,
sourceSelectVisible: value.sourceSelectVisible === true,
fileSelectVisible: value.fileSelectVisible === true,
sourceConfigVisible: value.sourceConfigVisible === true,
taskEditorVisible: value.taskEditorVisible === true,
taskBodyVisible: value.taskBodyVisible === true,
taskBody: value.taskBodyText ? textDigest(value.taskBodyText, 200) : null,
newTaskDraftVisible: value.newTaskDraftVisible === true,
taskStatusCounts: value.taskStatusCounts && typeof value.taskStatusCounts === "object" ? value.taskStatusCounts : {},
reportLinkCount: Number.isFinite(Number(value.reportLinkCount)) ? Number(value.reportLinkCount) : 0,
reportPreviewVisible: value.reportPreviewVisible === true,
reportPreview: value.reportPreviewText ? textDigest(value.reportPreviewText, 200) : null,
reportFullscreenVisible: value.reportFullscreenVisible === true,
launchButtonVisible: value.launchButtonVisible === true,
launchButtonEnabled: value.launchButtonEnabled === true,
launchButtonText: value.launchButtonText ? textDigest(value.launchButtonText, 120) : null,
blockerCount: Number.isFinite(Number(value.blockerCount)) ? Number(value.blockerCount) : 0,
blockers: Array.isArray(value.blockers) ? value.blockers.slice(0, 12).map((item) => ({
index: item?.index ?? null,
testId: item?.testId ?? null,
role: item?.role ?? null,
...textDigest(item?.text || "", 160),
})) : [],
paneGaps: Array.isArray(value.paneGaps) ? value.paneGaps.slice(0, 8).map((item) => ({
name: item?.name ?? null,
visible: item?.visible === true,
widthPx: Number.isFinite(Number(item?.widthPx)) ? Number(item.widthPx) : null,
heightPx: Number.isFinite(Number(item?.heightPx)) ? Number(item.heightPx) : null,
bottomGapPx: Number.isFinite(Number(item?.bottomGapPx)) ? Number(item.bottomGapPx) : null,
bottomGapRatio: Number.isFinite(Number(item?.bottomGapRatio)) ? Number(item.bottomGapRatio) : null,
contentNodeCount: Number.isFinite(Number(item?.contentNodeCount)) ? Number(item.contentNodeCount) : null,
valuesRedacted: true,
})) : [],
workbenchLinkCount: Number.isFinite(Number(value.workbenchLinkCount)) ? Number(value.workbenchLinkCount) : 0,
valuesRedacted: true
};
}
function digestSessionRail(value) {
if (!value || typeof value !== "object") return null;
const items = Array.isArray(value.items) ? value.items.map((item) => {
const titleText = String(item?.titleText || item?.titlePreview || "");
return {
index: item?.index ?? null,
tag: item?.tag ?? null,
testId: item?.testId ?? null,
role: item?.role ?? null,
active: item?.active === true,
status: item?.status ?? null,
dataStatus: item?.dataStatus ?? null,
running: item?.running === true,
dataRunning: item?.dataRunning ?? null,
ariaBusy: item?.ariaBusy ?? null,
sessionId: item?.sessionId ?? null,
sessionIdPrefix: item?.sessionIdPrefix ?? (item?.sessionId ? String(item.sessionId).slice(0, 12) : null),
fallbackTitle: item?.fallbackTitle === true,
titleHash: sha256Text(titleText),
titlePreview: truncate(titleText, 160),
titleBytes: Buffer.byteLength(titleText),
rect: item?.rect ?? null,
};
}) : [];
const fallbackItems = items.filter((item) => item.fallbackTitle).slice(0, 12);
const visibleCount = Number(value.visibleCount ?? items.length);
const fallbackTitleCount = Number(value.fallbackTitleCount ?? fallbackItems.length);
return {
visibleCount: Number.isFinite(visibleCount) ? visibleCount : items.length,
fallbackTitleCount: Number.isFinite(fallbackTitleCount) ? fallbackTitleCount : fallbackItems.length,
fallbackTitleRatio: Number.isFinite(Number(value.fallbackTitleRatio)) ? Number(value.fallbackTitleRatio) : (items.length > 0 ? Number((fallbackItems.length / items.length).toFixed(4)) : 0),
activeItem: items.find((item) => item.active) || null,
items,
fallbackItems,
valuesRedacted: true,
};
}
async function captureScreenshot(reason, imageType = "png") {
if (!page || page.isClosed()) throw new Error("page is not available for screenshot");
if (screenshotCaptureState && screenshotCaptureState.settled !== true) {
const ageMs = Date.now() - Number(screenshotCaptureState.startedAtMs || Date.now());
const error = new Error("screenshot capture already in progress");
error.details = {
reason,
currentUrl: currentPageUrl(),
pageId,
activeReason: screenshotCaptureState.reason,
activeStartedAt: screenshotCaptureState.startedAt,
activeAgeMs: ageMs,
activeTimedOut: screenshotCaptureState.timedOut === true,
timeoutMs: screenshotCaptureTimeoutMs,
valuesRedacted: true,
};
lastScreenshotAtMs = Date.now();
throw error;
}
artifactSeq += 1;
const safeReason = safeId(String(reason || "manual")).slice(0, 40) || "manual";
const type = imageType === "jpeg" || imageType === "jpg" ? "jpeg" : "png";
const ext = type === "jpeg" ? "jpg" : "png";
const file = path.join(dirs.screenshots, String(sampleSeq).padStart(6, "0") + "_" + String(artifactSeq).padStart(4, "0") + "_" + safeReason + "." + ext);
const timeoutMs = screenshotCaptureTimeoutMs;
const options = type === "jpeg"
? { path: file, type: "jpeg", quality: 70, fullPage: false, animations: "disabled", timeout: timeoutMs }
: { path: file, type: "png", fullPage: false, animations: "disabled", timeout: timeoutMs };
const state = { reason, startedAtMs: Date.now(), startedAt: new Date().toISOString(), timeoutMs, settled: false, timedOut: false };
screenshotCaptureState = state;
const screenshotPromise = page.screenshot(options)
.then((value) => {
state.settled = true;
return value;
})
.catch((error) => {
state.settled = true;
throw error;
})
.finally(() => {
if (screenshotCaptureState === state) screenshotCaptureState = null;
});
try {
await withHardTimeout(screenshotPromise, timeoutMs + 1000, "captureScreenshot " + safeReason + " exceeded " + timeoutMs + "ms");
const meta = await fileMeta(file);
const artifact = { seq: artifactSeq, sampleSeq, ts: new Date().toISOString(), kind: "screenshot", reason, path: file, type, byteCount: meta.byteCount, sha256: meta.sha256, pageId, currentUrl: currentPageUrl(), timeoutMs };
await appendJsonl(files.artifacts, artifact);
lastScreenshotAtMs = Date.now();
return artifact;
} catch (error) {
if (String(error?.message || "").includes("exceeded " + timeoutMs + "ms")) state.timedOut = true;
lastScreenshotAtMs = Date.now();
const wrapped = error instanceof Error ? error : new Error(String(error));
wrapped.details = {
...(wrapped.details || {}),
reason,
currentUrl: currentPageUrl(),
pageId,
timeoutMs,
file,
valuesRedacted: true,
};
throw wrapped;
}
}
async function captureCommandScreenshot(command) {
const shouldWaitProject = command.waitProjectManagementReady === true;
const readiness = shouldWaitProject ? await waitForProjectManagementCommandReady({ timeoutMs: 15000 }) : null;
if (readiness && readiness.ok !== true) {
const error = new Error("screenshot project-management readiness wait failed: " + (readiness.reason || "not-ready"));
error.details = { readiness, currentUrl: currentPageUrl(), pageId, valuesRedacted: true };
throw error;
}
const artifact = await captureScreenshot(command.reason || command.label || "manual", command.imageType || "png");
return { ...artifact, readiness, valuesRedacted: true };
}
function eventRecord(type, data) {
const clean = sanitize(data) || {};
return { ts: new Date().toISOString(), type, jobId, pageId: clean.pageId ?? pageId, pageRole: clean.pageRole ?? "control", sampleSeq, commandId: activeCommandId, ...clean };
}
async function summarizeWorkbenchResponseBody(response, request) {
const method = String(request.method() || "GET").toUpperCase();
const path = safeUrlPath(response.url()) || "";
const resourceType = String(request.resourceType() || "");
const status = Number(response.status());
if (!shouldSummarizeWorkbenchResponseBody({ method, path, resourceType, status })) return { bodyRead: false };
const headers = response.headers();
const contentType = String(headers["content-type"] || headers["Content-Type"] || "");
if (!/json/iu.test(contentType)) return { bodyRead: false, bodyReadSkipped: "non-json", valuesRedacted: true };
const contentLength = Number(headers["content-length"] || headers["Content-Length"]);
const maxBytes = 512 * 1024;
if (Number.isFinite(contentLength) && contentLength > maxBytes) return { bodyRead: false, bodyReadSkipped: "content-length-too-large", bodyByteCount: contentLength, valuesRedacted: true };
const text = await response.text();
const byteCount = Buffer.byteLength(text);
if (byteCount > maxBytes) return { bodyRead: true, bodyReadSkipped: "body-too-large", bodyByteCount: byteCount, bodyHash: sha256Text(text), valuesRedacted: true };
let parsed = null;
try {
parsed = JSON.parse(text);
} catch (error) {
return { bodyRead: true, bodyReadSkipped: "json-parse-error", bodyByteCount: byteCount, bodyHash: sha256Text(text), bodyParseError: errorSummary(error), valuesRedacted: true };
}
return {
bodyRead: true,
bodyByteCount: byteCount,
bodyHash: sha256Text(text),
bodySummary: summarizeWorkbenchJsonBody(parsed, path),
valuesRedacted: true
};
}
function shouldSummarizeWorkbenchResponseBody({ method, path, resourceType, status }) {
if (method !== "GET" && method !== "POST") return false;
if (!Number.isFinite(status) || status < 200 || status >= 300) return false;
if (resourceType === "eventsource" || path === "/v1/workbench/events") return false;
return path === "/v1/agent/chat"
|| path === "/v1/agent/chat/steer"
|| path === "/v1/workbench/sessions"
|| /^\/v1\/workbench\/sessions\/[^/]+\/messages$/u.test(path)
|| /^\/v1\/workbench\/turns\/[^/]+$/u.test(path)
|| /^\/v1\/workbench\/traces\/[^/]+\/events$/u.test(path);
}
function summarizeWorkbenchJsonBody(value, path) {
const traceIds = new Set();
const sessionIds = new Set();
const terminalTraceIds = new Set();
const statusCounts = {};
const counters = {
objectCount: 0,
arrayCount: 0,
traceEventLikeCount: 0,
messageLikeCount: 0,
turnLikeCount: 0,
terminalStatusCount: 0,
runningStatusCount: 0,
terminalTextCount: 0,
finalTextFieldCount: 0,
finalTextByteCount: 0
};
const visit = (node, key = "", parent = null, depth = 0) => {
if (depth > 32 || node === null || node === undefined) return;
if (typeof node === "string") {
collectWorkbenchIdsFromText(node, traceIds, sessionIds);
const normalizedStatus = normalizeWorkbenchStatus(key, node);
if (normalizedStatus) {
statusCounts[normalizedStatus] = (statusCounts[normalizedStatus] || 0) + 1;
if (isWorkbenchTerminalStatus(normalizedStatus)) counters.terminalStatusCount += 1;
if (isWorkbenchRunningStatus(normalizedStatus)) counters.runningStatusCount += 1;
}
if (isWorkbenchTerminalText(node)) {
counters.terminalTextCount += 1;
for (const traceId of workbenchTraceIdsFromRecord(parent)) terminalTraceIds.add(traceId);
}
if (isLikelyWorkbenchFinalTextField(key, parent, node)) {
counters.finalTextFieldCount += 1;
counters.finalTextByteCount += Buffer.byteLength(node);
for (const traceId of workbenchTraceIdsFromRecord(parent)) terminalTraceIds.add(traceId);
}
return;
}
if (typeof node !== "object") return;
if (Array.isArray(node)) {
counters.arrayCount += 1;
for (const item of node) visit(item, key, parent, depth + 1);
return;
}
counters.objectCount += 1;
const record = node;
const recordTraceIds = workbenchTraceIdsFromRecord(record);
for (const raw of [record.traceId, record.trace_id, record.id, record.turnId, record.messageId]) {
if (typeof raw === "string") collectWorkbenchIdsFromText(raw, traceIds, sessionIds);
}
for (const raw of [record.sessionId, record.session_id]) {
if (typeof raw === "string") collectWorkbenchIdsFromText(raw, traceIds, sessionIds);
}
const statusValue = record.status ?? record.state ?? record.phase ?? record.result ?? record.lifecycle;
if (typeof statusValue === "string") {
const normalizedStatus = normalizeWorkbenchStatus("status", statusValue);
if (normalizedStatus) {
statusCounts[normalizedStatus] = (statusCounts[normalizedStatus] || 0) + 1;
if (isWorkbenchTerminalStatus(normalizedStatus)) {
counters.terminalStatusCount += 1;
for (const traceId of recordTraceIds) terminalTraceIds.add(traceId);
}
if (isWorkbenchRunningStatus(normalizedStatus)) counters.runningStatusCount += 1;
}
}
if (record.traceId || record.trace_id || record.turnId || record.turn_id) counters.turnLikeCount += 1;
if (record.messageId || record.message_id || record.role || record.author || record.content || record.text || record.finalResponse) counters.messageLikeCount += 1;
if (record.projectedSeq !== undefined || record.sourceSeq !== undefined || record.eventSeq !== undefined || record.eventKind !== undefined || record.eventTimestamp !== undefined) counters.traceEventLikeCount += 1;
for (const [childKey, childValue] of Object.entries(record)) visit(childValue, childKey, record, depth + 1);
};
visit(value);
return {
pathKind: workbenchBodyPathKind(path),
traceIds: Array.from(traceIds).sort().slice(0, 12),
terminalTraceIds: Array.from(terminalTraceIds).sort().slice(0, 12),
sessionIds: Array.from(sessionIds).sort().slice(0, 12),
statusCounts,
...counters,
terminalEvidenceCount: counters.terminalStatusCount + counters.terminalTextCount,
valuesRedacted: true
};
}
function collectWorkbenchIdsFromText(value, traceIds, sessionIds) {
const text = String(value || "");
for (const match of text.matchAll(/\btrc_[A-Za-z0-9_-]+\b/gu)) traceIds.add(match[0]);
for (const match of text.matchAll(/\bses_[A-Za-z0-9_-]+\b/gu)) sessionIds.add(match[0]);
}
function workbenchTraceIdsFromRecord(record) {
if (!record || typeof record !== "object") return [];
const values = [record.traceId, record.trace_id, record.turnId, record.turn_id, record.messageId, record.message_id, record.id];
const ids = new Set();
for (const raw of values) {
if (typeof raw !== "string") continue;
for (const match of raw.matchAll(/\btrc_[A-Za-z0-9_-]+\b/gu)) ids.add(match[0]);
}
return Array.from(ids).sort();
}
function normalizeWorkbenchStatus(key, value) {
if (!/status|state|phase|result|lifecycle/iu.test(String(key || ""))) return null;
const text = String(value || "").toLowerCase().replace(/[^a-z0-9_-]+/gu, "-").replace(/^-+|-+$/gu, "");
if (!text) return null;
if (/^(completed|complete|succeeded|success|finished|done|terminal|sealed)$/u.test(text)) return "completed";
if (/^(failed|failure|error|errored)$/u.test(text)) return "failed";
if (/^(canceled|cancelled|aborted|cancel)$/u.test(text)) return "canceled";
if (/^(running|active|in-progress|in_progress|processing|streaming|executing)$/u.test(text)) return "running";
if (/^(queued|pending|admitted|created|waiting)$/u.test(text)) return "pending";
return text.slice(0, 80);
}
function isWorkbenchTerminalStatus(value) {
return value === "completed" || value === "failed" || value === "canceled";
}
function isWorkbenchRunningStatus(value) {
return value === "running" || value === "pending";
}
function isWorkbenchTerminalText(value) {
return /轮次完成|轮次失败|轮次取消|已记录|final response|sealed final response|turn completed|turn failed|turn canceled|terminal result|\bcompleted\b|\bfailed\b|\bcanceled\b|\bcancelled\b|\bterminal\b|\bdone\b/iu.test(String(value || ""));
}
function isLikelyWorkbenchFinalTextField(key, parent, value) {
const text = String(value || "").trim();
if (!text) return false;
const field = String(key || "");
if (!/final|assistant|response|content|markdown|text|message|output|result/iu.test(field)) return false;
const parentStatus = normalizeWorkbenchStatus("status", parent?.status ?? parent?.state ?? parent?.phase ?? parent?.result ?? "");
const parentRole = String(parent?.role ?? parent?.author ?? parent?.dataRole ?? "").toLowerCase();
return isWorkbenchTerminalStatus(parentStatus) || /assistant|agent|code/iu.test(parentRole) || /final|response|result/iu.test(field);
}
function workbenchBodyPathKind(path) {
if (path === "/v1/agent/chat" || path === "/v1/agent/chat/steer") return "agent-chat-submit";
if (path === "/v1/workbench/sessions") return "workbench-sessions";
if (/^\/v1\/workbench\/sessions\/[^/]+\/messages$/u.test(path)) return "workbench-session-messages";
if (/^\/v1\/workbench\/turns\/[^/]+$/u.test(path)) return "workbench-turn";
if (/^\/v1\/workbench\/traces\/[^/]+\/events$/u.test(path)) return "workbench-trace-events";
return "workbench";
}
function controlRecord(command, phase, detail) {
return {
ts: new Date().toISOString(),
seq: commandSeq,
phase,
commandId: command.id,
type: command.type,
source: command.source || "file",
input: commandInputSummary(command),
beforeUrl: command.beforeUrl || null,
afterUrl: currentPageUrl(),
pageId,
detail: sanitize(detail),
};
}
function commandInputSummary(command) {
const text = typeof command.text === "string" ? command.text : null;
const opaque = (value) => {
const raw = typeof value === "string" ? value : null;
if (!raw) return null;
return {
hash: sha256Text(raw),
preview: raw.length <= 18 ? raw : raw.slice(0, 10) + "..." + raw.slice(-5),
bytes: Buffer.byteLength(raw),
valuesRedacted: true
};
};
return {
type: command.type,
path: command.path || null,
url: command.url ? safeUrl(command.url) : null,
sessionId: command.sessionId || command.value || null,
provider: command.provider || null,
afterRound: Number.isInteger(Number(command.afterRound)) ? Number(command.afterRound) : null,
severity: command.severity || null,
alternateSessionStrategy: command.alternateSessionStrategy || null,
expectedSentinelRange: command.expectedSentinelRange || null,
expectedActionWaitMs: command.expectedActionWaitMs === null || command.expectedActionWaitMs === undefined || command.expectedActionWaitMs === "" ? null : Number(command.expectedActionWaitMs),
durationMs: command.durationMs === null || command.durationMs === undefined || command.durationMs === "" ? null : Number(command.durationMs),
requireComposerReady: command.requireComposerReady === true,
waitProjectManagementReady: command.waitProjectManagementReady === true,
findingId: command.findingId || null,
blocking: command.blocking === true ? true : command.blocking === false ? false : null,
sourceId: opaque(command.sourceId),
fileRef: opaque(command.fileRef),
filename: command.filename ? truncate(command.filename, 200) : null,
taskRef: opaque(command.taskRef),
taskId: command.taskId || null,
field: command.field || null,
link: command.link ? truncate(command.link, 200) : null,
titleHash: command.title ? sha256Text(command.title) : null,
titleBytes: command.title ? Buffer.byteLength(command.title) : null,
bodyHash: command.body ? sha256Text(command.body) : null,
bodyBytes: command.body ? Buffer.byteLength(command.body) : null,
status: command.status || null,
hwpodId: opaque(command.hwpodId),
nodeId: opaque(command.nodeId),
workspaceRoot: opaque(command.workspaceRoot),
root: opaque(command.root),
label: command.label ? truncate(command.label, 200) : null,
textHash: text === null ? null : sha256Text(text),
textBytes: text === null ? null : Buffer.byteLength(text),
textPreview: null,
valuesRedacted: true,
};
}
`;
}