// packages/core/src/zip-export.ts // Ported from apps/web-legacy/scripts/zip-export.js for Node-side use. // // Builds an output ZIP that preserves the original input ZIP's structure and // only overwrites task.json with the fully-embedded analysis package. // // The output task.json is a hybrid format: // - Preserves the echo-extension import structure (metadata + task.networkRequests) // so the ZIP can be re-imported by the echo-extension plugin without errors. // - Embeds all RL task package fields (rule_profile, golden_trajectory, rubric_checkers, // dbdiff_criteria, pass_policy, ...) at the top level for downstream consumers. // - Derives subtask groupings from golden_trajectory by matching endpoints to actual // network request IDs, so the extension shows the correct items in each group. import JSZip from "jszip"; // Permissive types — input shapes come from a Chrome extension recording with // an evolving schema, so we lean heavily on `unknown`/optional fields and only // narrow at usage sites (matching the legacy JS behavior exactly). type AnyObj = Record; export interface BuildAnalysisZipOptions { originalZipBytes: ArrayBuffer | Uint8Array | Buffer; taskPackage: AnyObj; mode?: "api" | "mcp" | string; originalFilename?: string; } export interface BuildAnalysisZipResult { buffer: Buffer; filename: string; } // The shared judge.buildGoldenTrajectory always emits mode: "mcp" + mcpCalls. // For API grouping mode, remap to mode: "api" + apiCalls so the output // matches echo-extension's groupMode terminology. export function remapTrajectoryForMode(taskPackage: AnyObj, mode: string): AnyObj { if (!taskPackage || !Array.isArray(taskPackage.golden_trajectory)) return taskPackage; if (mode !== "api") return taskPackage; const remapped = taskPackage.golden_trajectory.map(function (entry: AnyObj) { if (!entry || !entry.subtask) return entry; const sub = entry.subtask; const calls = Array.isArray(sub.mcpCalls) ? sub.mcpCalls : Array.isArray(sub.apiCalls) ? sub.apiCalls : []; return { subtask: { order: sub.order, mode: "api", description: sub.description, apiCalls: calls, }, }; }); return Object.assign({}, taskPackage, { golden_trajectory: remapped }); } // Same URL pattern normalization as scripts/api/importer.js: // /api/channels/abc-123/members -> /api/channels/{id}/members export function normalizeUrlPattern(path: string): string { if (!path) return ""; return path .split("/") .map(function (seg) { if (!seg) return seg; if (/^[A-Z0-9_-]{8,}$/i.test(seg) && /\d/.test(seg)) return "{id}"; if (/^[0-9]+$/.test(seg)) return "{id}"; return seg; }) .join("/"); } // Extract URL path from a full URL (strips scheme + host + query string). export function extractPath(url: string): string { if (!url) return ""; try { const u = new URL(url); return u.pathname; } catch (_) { // Fallback: strip scheme+host manually const noScheme = url.replace(/^https?:\/\/[^/]+/, ""); const qIdx = noScheme.indexOf("?"); return qIdx >= 0 ? noScheme.slice(0, qIdx) : noScheme; } } function normalizeRubricScoring(rubric: AnyObj): Record { const desc = rubric && rubric.description && typeof rubric.description === "object" ? rubric.description : {}; const scoringSource = rubric && rubric.scoring && typeof rubric.scoring === "object" ? rubric.scoring : desc && desc.scoring && typeof desc.scoring === "object" ? desc.scoring : {}; const scoring: Record = {}; if (scoringSource["0"] != null) scoring["0"] = String(scoringSource["0"]); if (scoringSource[0] != null && scoring["0"] == null) scoring["0"] = String(scoringSource[0]); if (scoringSource["1"] != null) scoring["1"] = String(scoringSource["1"]); if (scoringSource[1] != null && scoring["1"] == null) scoring["1"] = String(scoringSource[1]); if (Array.isArray(rubric && rubric.scorePoints)) { rubric.scorePoints.forEach(function (point: AnyObj) { if (!point) return; if (Number(point.score) === 0 && scoring["0"] == null) scoring["0"] = String(point.description || ""); if (Number(point.score) === 1 && scoring["1"] == null) scoring["1"] = String(point.description || ""); }); } if (!scoring["0"]) scoring["0"] = "Reject if the required evidence is missing or incorrect."; if (!scoring["1"]) scoring["1"] = "Accept if the required evidence is present and correct."; return scoring; } function rubricDescriptionText(rubric: AnyObj): string { if (rubric && rubric.description && typeof rubric.description === "object") { return String(rubric.description.description || ""); } return String((rubric && rubric.description) || ""); } function rubricCategory(rubric: AnyObj): string { if (rubric && rubric.description && typeof rubric.description === "object" && rubric.description.category) { return String(rubric.description.category); } return String((rubric && rubric.category) || "process"); } function scorePointsFromScoring(scoring: Record): AnyObj[] { return [ { score: 0, description: scoring["0"] }, { score: 1, description: scoring["1"] }, ]; } // Convert a rubric from rl-env internal format to echo-extension import format. // rl-env: { name, description (string), category, must_pass, checker_key, max_score, scoring } // extension: { name, must_pass, checker_key, max_score, description: { description, category, scoring } } export function convertRubricToExtensionFormat(rubric: AnyObj, checkerKeyOverride?: string): AnyObj { rubric = rubric || {}; const scoring = normalizeRubricScoring(rubric || {}); const checkerKey = String(checkerKeyOverride || rubric.checker_key || rubric.name || ""); return { name: rubric.name || "", must_pass: Boolean(rubric.must_pass), checker_key: checkerKey, max_score: rubric.max_score != null ? rubric.max_score : rubric.maxScore != null ? rubric.maxScore : 1, scoring, scorePoints: scorePointsFromScoring(scoring), description: { description: rubricDescriptionText(rubric || {}), category: rubricCategory(rubric || {}), scoring, reject: scoring["0"], accept: scoring["1"], }, }; } export function parseQueryParams(url: string): AnyObj { const out: AnyObj = {}; if (!url) return out; try { const u = new URL(url, "http://_placeholder"); u.searchParams.forEach(function (value, key) { out[key] = value; }); } catch (_) { const qIdx = String(url).indexOf("?"); if (qIdx < 0) return out; String(url) .slice(qIdx + 1) .split("&") .forEach(function (pair) { if (!pair) return; const eqIdx = pair.indexOf("="); let key = eqIdx >= 0 ? pair.slice(0, eqIdx) : pair; let value = eqIdx >= 0 ? pair.slice(eqIdx + 1) : ""; try { key = decodeURIComponent(key); value = decodeURIComponent(value); } catch (_) {} out[key] = value; }); } return out; } function objectHasKeys(obj: any): boolean { return obj && typeof obj === "object" && Object.keys(obj).length > 0; } function getHeader(headers: AnyObj | null | undefined, name: string): any { headers = headers || {}; return headers[name] || headers[name.toLowerCase()] || headers[name.toUpperCase()] || null; } function getNetworkEntries(networkJson: any): any[] { if (Array.isArray(networkJson)) return networkJson; if (networkJson && Array.isArray(networkJson.requests)) return networkJson.requests; if (networkJson && Array.isArray(networkJson.networkRequests)) return networkJson.networkRequests; if (networkJson && Array.isArray(networkJson.data)) return networkJson.data; return []; } function buildNetworkEntryIndex(networkJson: any): AnyObj { const index: AnyObj = {}; getNetworkEntries(networkJson).forEach(function (entry) { if (entry && entry.id) index[entry.id] = entry; }); return index; } export function buildApiCallObject(req: AnyObj | null | undefined, networkEntryIndex: AnyObj): AnyObj { req = req || {}; const raw = req.id && networkEntryIndex ? networkEntryIndex[req.id] : null; const rawRequest = (raw && raw.request) || {}; const rawResponse = (raw && raw.response) || {}; const url = rawRequest.url || req.url || ""; const endpoint = extractPath(url) || extractPath(req.url || "") || ""; const method = String(rawRequest.method || req.method || "").toUpperCase(); const input: AnyObj = {}; const queryParams = parseQueryParams(url || req.url || ""); if (objectHasKeys(queryParams)) input.queryParams = queryParams; if (rawRequest.body !== undefined && rawRequest.body !== null && rawRequest.body !== "") { input.bodyParams = rawRequest.body; } const output: AnyObj = {}; const status = req.status != null ? req.status : req.responseStatus != null ? req.responseStatus : rawResponse.status != null ? rawResponse.status : null; if (status != null) output.statusCode = Number(status); const contentType = req.contentType || getHeader(rawResponse.headers, "content-type"); if (contentType) output.contentType = contentType; return { id: req.id || "", name: endpoint, description: "", time: req.timestamp != null ? req.timestamp : raw && raw.timing && raw.timing.startTime != null ? raw.timing.startTime : undefined, type: "api", input: input, output: output, metadata: { duration: req.duration != null ? req.duration : raw && raw.timing ? raw.timing.duration : undefined, timestamp: req.timestamp != null ? req.timestamp : raw && raw.timing ? raw.timing.startTime : undefined, endpoint: endpoint, method: method, }, }; } export function hydrateApiCall(call: any, networkRequestsById: AnyObj, networkEntryIndex: AnyObj): AnyObj | null { if (call && typeof call === "object" && call.type === "api" && call.metadata) return call; const id = call && typeof call === "object" ? call.id : String(call || ""); const req = id && networkRequestsById ? networkRequestsById[id] : null; if (req) return buildApiCallObject(req, networkEntryIndex); return id ? { id: id } : null; } function buildNetworkRequestIndexes(networkRequests: any[]): AnyObj { const byId: AnyObj = {}; (networkRequests || []).forEach(function (req: AnyObj) { if (req && req.id) byId[req.id] = req; }); return byId; } function cloneJson(value: T): T { if (value == null) return value; try { return JSON.parse(JSON.stringify(value)); } catch (_) { return value; } } const ENTITY_KEYS = ["channelId", "guildId", "messageId", "userId", "roleId", "webhookId"]; function getMcpCallName(call: any): string { return String((call && (call.name || call.tool || call.function || call.mcpToolName || call.call)) || ""); } function getWantedMcpCallName(wanted: any): string { const raw = String(wanted && typeof wanted === "object" ? getMcpCallName(wanted) : wanted || "").trim(); const match = raw.match(/^CALL\s+(\S+)/i); return match ? match[1] : raw; } function getWantedMcpCallId(wanted: any): string { if (!wanted || typeof wanted !== "object") return ""; return String( wanted.callId || wanted.call_id || wanted.id || wanted.requestId || wanted.request_id || wanted.networkRequestId || wanted.network_request_id || "", ).trim(); } function getMcpCallTime(call: any): number { return Number(call && (call.time ?? call.metadata?.timestamp)) || 0; } function collectExtensionMcpCalls(task: AnyObj | null | undefined): AnyObj[] { if (!task || typeof task !== "object") return []; const seen: Record = {}; const calls: AnyObj[] = []; const push = function (call: any) { if (!call || typeof call !== "object") return; if (!getMcpCallName(call)) return; const id = call.id ? String(call.id) : ""; const key = id || [getMcpCallName(call), getMcpCallTime(call)].filter(Boolean).join(":"); if (key && seen[key]) return; if (key) seen[key] = true; calls.push(call); }; (Array.isArray(task.subtasks) ? task.subtasks : []).forEach(function (subtask: AnyObj) { (Array.isArray(subtask && subtask.mcpCalls) ? subtask.mcpCalls : []).forEach(push); }); (Array.isArray(task.other_mcp_calls) ? task.other_mcp_calls : []).forEach(push); return calls.sort(function (a, b) { return getMcpCallTime(a) - getMcpCallTime(b); }); } function buildMcpCallIndex(calls: any): AnyObj | null { if (!Array.isArray(calls) || !calls.length) return null; const byId: AnyObj = {}; calls.forEach(function (call: AnyObj) { if (call && call.id) byId[String(call.id)] = call; }); return Object.keys(byId).length ? byId : null; } interface McpCallPoolEntry { index: number; call: AnyObj; id: string; tool: string; used: boolean; } function buildMcpCallPool(calls: any[]): McpCallPoolEntry[] { return (Array.isArray(calls) ? calls : []) .map(function (call: AnyObj, index: number) { return { index, call, id: call && call.id != null ? String(call.id) : "", tool: getMcpCallName(call), used: false, }; }) .filter(function (entry) { return Boolean(entry.tool); }); } function takeMcpCallFromPool(wanted: any, pool?: McpCallPoolEntry[] | null): AnyObj | null { const wantedId = getWantedMcpCallId(wanted); if (wantedId && pool && pool.length) { const exact = pool.find(function (entry) { return !entry.used && entry.id === wantedId; }); if (exact) { exact.used = true; return cloneJson(exact.call); } } const target = getWantedMcpCallName(wanted); if (!target || !pool || !pool.length) return null; const hit = pool.find(function (entry) { return !entry.used && entry.tool === target; }); if (!hit) return null; hit.used = true; return cloneJson(hit.call); } function taskMentionsReadState(text: unknown): boolean { return /(?:mark(?:ed)?\s+.*read|read\s+(?:state|status|marker|receipt|position)|unread|read-position)/i.test( String(text || ""), ); } function isReadMarkerTool(tool: unknown): boolean { return /^mark_.*read$/i.test(String(tool || "")) || /read[_-]position/i.test(String(tool || "")); } function addEntityValue(out: Record, key: string, value: unknown): void { if (value == null || value === "") return; if (typeof value === "object") return; const str = String(value); if (!out[key]) out[key] = []; if (out[key].indexOf(str) < 0) out[key].push(str); } function collectEntityIds(value: unknown, out: Record = {}, depth = 0): Record { if (!value || typeof value !== "object" || depth > 5) return out; if (Array.isArray(value)) { value.forEach(function (item) { collectEntityIds(item, out, depth + 1); }); return out; } Object.keys(value as AnyObj).forEach(function (key) { const child = (value as AnyObj)[key]; if (ENTITY_KEYS.indexOf(key) >= 0) addEntityValue(out, key, child); if (child && typeof child === "object") collectEntityIds(child, out, depth + 1); }); return out; } function getExtensionMcpCallEntities(call: AnyObj): Record { return collectEntityIds(call && (call.input || call.args || {})); } function entitiesCompatible(entities: Record, anchors: Record): boolean { return ENTITY_KEYS.every(function (key) { const anchor = anchors[key]; const values = entities[key] || []; return !anchor || !values.length || values.indexOf(anchor) >= 0; }); } function updateEntityAnchors(anchors: Record, entities: Record): void { ENTITY_KEYS.forEach(function (key) { if (anchors[key]) return; const values = entities[key] || []; if (values.length === 1) anchors[key] = values[0]; }); } function filterMcpSubtasksByEntityConsistency(subtasks: AnyObj[], instructionText: unknown): AnyObj[] { const anchors: Record = {}; const keepReadMarkers = taskMentionsReadState(instructionText); return (Array.isArray(subtasks) ? subtasks : []) .map(function (st: AnyObj) { if (!st || st.groupMode !== "mcp" || !Array.isArray(st.mcpCalls)) return st; const mcpCalls = st.mcpCalls.filter(function (call: AnyObj) { const tool = getMcpCallName(call); if (!keepReadMarkers && isReadMarkerTool(tool)) return false; const entities = getExtensionMcpCallEntities(call); if (!entitiesCompatible(entities, anchors)) return false; updateEntityAnchors(anchors, entities); return true; }); return Object.assign({}, st, { mcpCalls }); }) .filter(function (st: AnyObj) { return st.groupMode !== "mcp" || (Array.isArray(st.mcpCalls) && st.mcpCalls.length > 0); }); } function filterMcpSubtasksByNetworkIds(subtasks: AnyObj[], networkRequests: AnyObj[]): AnyObj[] { const ids: Record = {}; (Array.isArray(networkRequests) ? networkRequests : []).forEach(function (req: AnyObj) { if (req && req.id) ids[String(req.id)] = true; }); if (!Object.keys(ids).length) return subtasks; return (Array.isArray(subtasks) ? subtasks : []) .map(function (st: AnyObj) { if (!st || st.groupMode !== "mcp" || !Array.isArray(st.mcpCalls)) return st; const mcpCalls = st.mcpCalls.filter(function (call: AnyObj) { const id = call && call.id != null ? String(call.id) : ""; return Boolean(id && ids[id]); }); return Object.assign({}, st, { mcpCalls }); }) .filter(function (st: AnyObj) { return st.groupMode !== "mcp" || (Array.isArray(st.mcpCalls) && st.mcpCalls.length > 0); }); } function checkerToolsFromKey(key: unknown): string[] { const raw = String(key || "").trim(); if (!raw) return []; const tools: string[] = []; raw.split("->").forEach(function (segment) { const text = segment.trim(); const call = text.match(/^CALL\s+(\S+)/i); if (call) { tools.push(call[1]); return; } const simple = text.match(/^([a-zA-Z][\w:-]*)$/); if (simple) tools.push(simple[1]); }); return tools; } function buildExportedMcpToolSet(subtasks: AnyObj[]): Record { const tools: Record = {}; (Array.isArray(subtasks) ? subtasks : []).forEach(function (st: AnyObj) { (Array.isArray(st && st.mcpCalls) ? st.mcpCalls : []).forEach(function (call: AnyObj) { const tool = getMcpCallName(call); if (tool) tools[tool] = true; }); }); return tools; } function firstExportedCheckerTool(key: unknown, exportedTools: Record): string { const tools = checkerToolsFromKey(key); for (let i = 0; i < tools.length; i += 1) { const tool = getWantedMcpCallName(tools[i]); if (tool && exportedTools[tool]) return tool; } const raw = String(key || ""); const names = Object.keys(exportedTools); for (let i = 0; i < names.length; i += 1) { if (new RegExp("(^|[^A-Za-z0-9_:-])" + names[i].replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "([^A-Za-z0-9_:-]|$)").test(raw)) { return names[i]; } } return ""; } function simplifyRubricCheckerKeysForMcp(rubrics: AnyObj[], subtasks: AnyObj[]): AnyObj[] { const exportedTools = buildExportedMcpToolSet(subtasks); if (!Object.keys(exportedTools).length) return rubrics; return (Array.isArray(rubrics) ? rubrics : []).map(function (rubric: AnyObj) { const next = Object.assign({}, rubric); const simpleKey = firstExportedCheckerTool(next.checker_key, exportedTools) || firstExportedCheckerTool(next.name, exportedTools); if (simpleKey) next.checker_key = simpleKey; return next; }); } function normalizeCheckerCatalogObject(catalog: any): AnyObj { if (!catalog) return {}; if (Array.isArray(catalog)) { const out: AnyObj = {}; catalog.forEach(function (item: AnyObj) { const key = item && (item.checker_key || item.key || item.name); if (key) out[String(key)] = item; }); return out; } return typeof catalog === "object" ? cloneJson(catalog) : {}; } function inferCheckerEvidenceSource(checkerKey: string): string { const first = String(checkerKey || "").split("->")[0].trim().replace(/^CALL\s+/i, "").split("|")[0].trim(); return first ? "tool_call:" + first : "tool_call"; } function remapCheckerCatalogForStrictKeys(originalCatalog: any, originalRubrics: AnyObj[], strictRubrics: AnyObj[]): AnyObj { const catalog = normalizeCheckerCatalogObject(originalCatalog); const out: AnyObj = {}; (strictRubrics || []).forEach(function (rubric: AnyObj, index: number) { const oldKey = String((originalRubrics && originalRubrics[index] && originalRubrics[index].checker_key) || ""); const newKey = String((rubric && rubric.checker_key) || oldKey || (rubric && rubric.name) || ""); if (!newKey) return; const scoring = normalizeRubricScoring(rubric || {}); const existing = catalog[newKey] || catalog[oldKey] || {}; out[newKey] = { evidence_source: String(existing.evidence_source || existing.evidenceSource || inferCheckerEvidenceSource(newKey)), match_field: String(existing.match_field || existing.matchField || newKey), fail_when: String(existing.fail_when || existing.failWhen || scoring["0"]), }; }); return out; } interface RequestPoolEntry { index: number; req: AnyObj; id: string; method: string; path: string; normalizedPath: string; used: boolean; } function buildRequestPool(networkRequests: any[]): RequestPoolEntry[] { return (networkRequests || []).map(function (req: AnyObj, index: number) { const path = extractPath(req && req.url ? req.url : ""); return { index, req, id: String((req && req.id) || ""), method: String((req && req.method) || "").toUpperCase(), path, normalizedPath: normalizeUrlPattern(path), used: false, }; }); } function getCallInput(call: any): AnyObj { if (call && call.input && typeof call.input === "object" && !Array.isArray(call.input)) { return call.input; } if (call && call.args && typeof call.args === "object" && !Array.isArray(call.args)) { return { bodyParams: call.args }; } return {}; } function stripTransportFields(input: AnyObj): AnyObj { const out = Object.assign({}, input || {}); delete out.method; delete out.endpoint; return out; } function getCallMethod(call: any): string { const input = getCallInput(call); return String(input.method || call?.method || call?.metadata?.method || "").toUpperCase(); } function getCallEndpoint(call: any): string { const input = getCallInput(call); return String(input.endpoint || call?.endpoint || call?.metadata?.endpoint || ""); } function getCallName(call: any, method: string, endpoint: string): string { const explicit = call?.tool || call?.name || call?.function; if (explicit) return String(explicit); return inferNameFromEndpoint(method, endpoint); } function inferNameFromEndpoint(method: string, endpoint: string): string { const path = extractPath(endpoint) || endpoint || ""; const parts = path.split("/").filter(Boolean); const last = (parts[parts.length - 1] || "call").replace(/-/g, "_"); if (method === "POST" && last === "messages") return "send_message"; if (method === "GET" && last === "me") return "get_current_user"; if (method === "PUT" || method === "PATCH") return "update_" + last; if (method === "POST") return "create_" + last; if (method === "DELETE") return "delete_" + last; if (method === "GET") return "get_" + last; return last || "recorded_call"; } function parseJsonMaybe(value: any): any { if (typeof value !== "string") return value; const trimmed = value.trim(); if (!trimmed) return value; if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return value; try { return JSON.parse(trimmed); } catch (_) { return value; } } function scalarEqual(expected: any, actual: any): boolean { if (expected === actual) return true; if (expected == null || actual == null) return expected == null && actual == null; return String(expected) === String(actual); } function containsValue(expected: any, actual: any): boolean { expected = parseJsonMaybe(expected); actual = parseJsonMaybe(actual); if (Array.isArray(expected)) { if (!Array.isArray(actual) || expected.length !== actual.length) return false; return expected.every(function (value, index) { return containsValue(value, actual[index]); }); } if (expected && typeof expected === "object") { if (!actual || typeof actual !== "object") return false; return Object.keys(expected).every(function (key) { return containsValue(expected[key], actual[key]); }); } return scalarEqual(expected, actual); } function hasMeaningfulParams(value: any): boolean { if (value == null || value === "") return false; if (typeof value === "object" && !Array.isArray(value)) return Object.keys(value).length > 0; return true; } function splitPathSegments(path: string): string[] { return (extractPath(path) || path || "").split("/").filter(Boolean); } function isPathPlaceholder(segment: string): boolean { return segment === "{id}" || /^\{[^}]+\}$/.test(segment) || /^:[A-Za-z_]\w*$/.test(segment); } function endpointPathMatchesRequest(callPath: string, entryPath: string, normalizedEndpoint: string): boolean { if (!callPath) return true; if (entryPath === callPath) return true; if (normalizeUrlPattern(entryPath) !== normalizedEndpoint) return false; const callParts = splitPathSegments(callPath); const entryParts = splitPathSegments(entryPath); if (callParts.length !== entryParts.length) return false; return callParts.every(function (part, index) { return isPathPlaceholder(part) || part === entryParts[index]; }); } const PATH_PARAM_RESOURCE_BY_KEY: Record = { channelId: "channels", guildId: "guilds", messageId: "messages", userId: "users", roleId: "roles", memberId: "members", threadId: "threads", webhookId: "webhooks", eventId: "events", }; function getPathParamAfterResource(path: string, resource: string): string { const parts = splitPathSegments(path); const index = parts.indexOf(resource); return index >= 0 && index + 1 < parts.length ? decodeURIComponent(parts[index + 1]) : ""; } function pathParamsMatch(input: AnyObj, url: string): boolean { const params = input.pathParams; if (!hasMeaningfulParams(params) || !params || typeof params !== "object" || Array.isArray(params)) { return true; } const path = extractPath(url) || url || ""; return Object.keys(params).every(function (key) { const resource = PATH_PARAM_RESOURCE_BY_KEY[key]; if (!resource) return true; const actual = getPathParamAfterResource(path, resource); return !actual || scalarEqual((params as AnyObj)[key], actual); }); } function callParamsMatch(call: any, req: AnyObj, networkEntryIndex: AnyObj): boolean { const input = getCallInput(call); const raw = req.id && networkEntryIndex ? networkEntryIndex[req.id] : null; const rawRequest = (raw && raw.request) || {}; const url = rawRequest.url || req.url || ""; if (!pathParamsMatch(input, url)) return false; if (hasMeaningfulParams(input.queryParams)) { const actualQuery = parseQueryParams(url); if (!containsValue(input.queryParams, actualQuery)) return false; } if (hasMeaningfulParams(input.bodyParams)) { const actualBody = rawRequest.body; if (!containsValue(input.bodyParams, actualBody)) return false; } return true; } function findNetworkRequestForCall( call: any, requestPool: RequestPoolEntry[], networkEntryIndex: AnyObj, ): RequestPoolEntry | null { const callId = getWantedMcpCallId(call); if (callId) { const direct = requestPool.find(function (entry) { return !entry.used && entry.id === callId; }); if (direct && callParamsMatch(call, direct.req, networkEntryIndex)) return direct; } const method = getCallMethod(call); const endpoint = getCallEndpoint(call); const callPath = extractPath(endpoint) || endpoint; const normalizedEndpoint = normalizeUrlPattern(callPath); if (!method && !callPath) return null; for (let i = 0; i < requestPool.length; i += 1) { const entry = requestPool[i]; if (entry.used) continue; if (method && entry.method !== method) continue; if (callPath && !endpointPathMatchesRequest(callPath, entry.path, normalizedEndpoint)) { continue; } if (!callParamsMatch(call, entry.req, networkEntryIndex)) continue; return entry; } return null; } function markRequestUsed(requestPool: RequestPoolEntry[] | undefined, req: AnyObj): void { if (!requestPool || !req || !req.id) return; const hit = requestPool.find(function (entry) { return !entry.used && entry.id === req.id; }); if (hit) hit.used = true; } function buildMcpCallObject( req: AnyObj | null | undefined, networkEntryIndex: AnyObj, sourceCall?: any, mcpCallIndex?: AnyObj | null, ): AnyObj | null { if (req && req.id && mcpCallIndex) { const exact = mcpCallIndex[String(req.id)]; return exact ? cloneJson(exact) : null; } return null; } function hydrateMcpCall( call: any, requestPool: RequestPoolEntry[], networkEntryIndex: AnyObj, mcpCallIndex?: AnyObj | null, ): AnyObj | null { const exactId = getWantedMcpCallId(call); if (exactId && mcpCallIndex && mcpCallIndex[exactId]) { const match = requestPool.find(function (entry) { return !entry.used && entry.id === exactId; }); if (match && callParamsMatch(call, match.req, networkEntryIndex)) match.used = true; return cloneJson(mcpCallIndex[exactId]); } if ( call && typeof call === "object" && call.type === "mcp" && call.id && call.metadata ) { return call; } const match = findNetworkRequestForCall(call, requestPool, networkEntryIndex); if (match) { match.used = true; return buildMcpCallObject(match.req, networkEntryIndex, call, mcpCallIndex); } return null; } function normalizeMcpCall(call: any): AnyObj | null { if (!call) return null; if (typeof call !== "object") { return { tool: String(call), input: {} }; } const tool = call.tool || call.name || call.function; if (!tool) return call; const input = call.input && typeof call.input === "object" && !Array.isArray(call.input) ? Object.assign({}, call.input) : {}; if ( input.bodyParams === undefined && call.args && typeof call.args === "object" && !Array.isArray(call.args) && Object.keys(call.args).length > 0 ) { input.bodyParams = call.args; } const normalized: AnyObj = { tool: String(tool), input, }; if (call.output !== undefined) normalized.output = call.output; if (call.success !== undefined) normalized.success = call.success; if (call.id !== undefined) normalized.id = call.id; if (call.time !== undefined) normalized.time = call.time; return normalized; } // Convert a subtask from rl-env recommended_groups format to echo-extension import format. // API mode writes full apiCalls objects; MCP mode writes mcpCalls only. export function convertSubtaskToExtensionFormat( st: AnyObj, index: number, networkRequests: any[], mode: string | undefined, networkEntryIndex: AnyObj, requestPool?: AnyObj[], mcpCallIndex?: AnyObj | null, mcpCallPool?: McpCallPoolEntry[] | null, ): AnyObj { const requestedMode = mode || st.mode || st.groupMode || (st.apiCalls && st.apiCalls.length ? "api" : "mcp"); const networkRequestsById = buildNetworkRequestIndexes(networkRequests || []); const mcpRequestPool = (requestPool as RequestPoolEntry[] | undefined) || buildRequestPool(networkRequests || []); // recommended_groups use name/reason; golden_trajectory uses description const instruction = st.name || st.reason || st.description || st.instruction || ""; let apiCalls: AnyObj[] = []; let mcpCalls: AnyObj[] = []; if (Array.isArray(st._requestIndices) && networkRequests) { st._requestIndices.forEach(function (idx: any) { const req = networkRequests[Number(idx)]; if (!req || !req.id) return; if (requestedMode === "mcp") { const mcpCall = req.id && mcpCallIndex && mcpCallIndex[String(req.id)] ? buildMcpCallObject(req, networkEntryIndex, null, mcpCallIndex) : null; if (mcpCall) markRequestUsed(mcpRequestPool, req); if (mcpCall) mcpCalls.push(mcpCall); } else { apiCalls.push(buildApiCallObject(req, networkEntryIndex)); } }); } const hasExactCalls = function () { return requestedMode === "mcp" ? mcpCalls.length > 0 : apiCalls.length > 0; }; if (!hasExactCalls() && requestedMode === "mcp" && Array.isArray(st.callIds) && mcpCallIndex) { st.callIds.forEach(function (id: any) { const exact = mcpCallIndex[String(id || "")]; if (exact) mcpCalls.push(cloneJson(exact)); }); } if (!hasExactCalls() && Array.isArray(st._callDetails) && networkRequests) { st._callDetails.forEach(function (detail: AnyObj) { if (!detail || detail.index == null) return; const req = networkRequests[Number(detail.index) - 1] || networkRequests[Number(detail.index)]; if (!req || !req.id) return; if (requestedMode === "mcp") { const mcpCall = takeMcpCallFromPool(detail.call || detail.tool || detail.name || detail.mcpToolName, mcpCallPool) || (req.id && mcpCallIndex && mcpCallIndex[String(req.id)] ? buildMcpCallObject(req, networkEntryIndex, detail, mcpCallIndex) : null); if (mcpCall) markRequestUsed(mcpRequestPool, req); if (mcpCall) mcpCalls.push(mcpCall); } else { apiCalls.push(buildApiCallObject(req, networkEntryIndex)); } }); } if (!apiCalls.length && requestedMode !== "mcp" && Array.isArray(st.apiCalls)) { apiCalls = st.apiCalls .map(function (call: any) { return hydrateApiCall(call, networkRequestsById, networkEntryIndex); }) .filter(Boolean) as AnyObj[]; } if (!mcpCalls.length && Array.isArray(st.mcpCalls)) { mcpCalls = st.mcpCalls .map(function (call: any) { return hydrateMcpCall(call, mcpRequestPool, networkEntryIndex, mcpCallIndex); }) .filter(Boolean) as AnyObj[]; } if (!mcpCalls.length && requestedMode === "mcp" && Array.isArray(st.calls)) { mcpCalls = st.calls .map(function (call: any) { return takeMcpCallFromPool(call, mcpCallPool); }) .filter(Boolean) as AnyObj[]; } const effectiveMode = requestedMode === "mcp" ? "mcp" : "api"; return { id: st.id || "subtask_" + (index + 1), order: st.order !== undefined ? st.order : index + 1, groupMode: effectiveMode, instruction: instruction, apiCalls: effectiveMode === "api" ? apiCalls : [], mcpCalls: effectiveMode === "mcp" ? mcpCalls : [], }; } // Derive extension-format subtasks from golden_trajectory by matching each // apiCall's (method + endpoint) back to an actual network request ID. // Uses greedy left-to-right matching (each request used at most once). export function deriveSubtasksFromTrajectory( goldenTrajectory: any[], networkRequests: any[], mode: string | undefined, networkEntryIndex: AnyObj, mcpCallIndex?: AnyObj | null, ): AnyObj[] { if (!Array.isArray(goldenTrajectory) || !Array.isArray(networkRequests)) return []; // Build a pool of network requests enriched with path info, preserving order. const reqPool = buildRequestPool(networkRequests); const result: AnyObj[] = []; goldenTrajectory.forEach(function (entry: AnyObj, i: number) { if (!entry || !entry.subtask) return; const sub = entry.subtask; const calls = Array.isArray(sub.apiCalls) ? sub.apiCalls : Array.isArray(sub.mcpCalls) ? sub.mcpCalls : []; const matchedApiCalls: AnyObj[] = []; const matchedMcpCalls: AnyObj[] = []; calls.forEach(function (call: any) { if (!call) return; if (mode === "mcp") { const mcpCall = hydrateMcpCall(call, reqPool, networkEntryIndex, mcpCallIndex); if (mcpCall) matchedMcpCalls.push(mcpCall); return; } const callMethod = getCallMethod(call); const callEndpoint = getCallEndpoint(call); // Extract path first so full URLs (http://host/path) match the same as path-only endpoints. const callPath = extractPath(callEndpoint) || callEndpoint; const normalizedEndpoint = normalizeUrlPattern(callPath); for (let j = 0; j < reqPool.length; j++) { const req = reqPool[j]; if (req.used) continue; if (callMethod && req.method !== callMethod) continue; // Match: normalized path equals normalized endpoint if (req.normalizedPath === normalizedEndpoint || req.path === callPath) { req.used = true; if (req.id) matchedApiCalls.push(buildApiCallObject(req.req, networkEntryIndex)); break; } } }); const effectiveMode = mode === "mcp" ? "mcp" : "api"; result.push({ id: "subtask_" + (i + 1), order: sub.order !== undefined ? sub.order : i + 1, groupMode: effectiveMode, instruction: sub.description || "", apiCalls: effectiveMode === "api" ? matchedApiCalls : [], mcpCalls: effectiveMode === "mcp" ? matchedMcpCalls : [], }); }); return result; } function buildStrictGoldenTrajectoryFromExtensionSubtasks(subtasks: AnyObj[], mode: string | undefined): AnyObj[] { const effectiveMode = mode === "api" ? "api" : "mcp"; return (Array.isArray(subtasks) ? subtasks : []) .map(function (st: AnyObj, index: number) { const order = st.order !== undefined ? st.order : index + 1; const description = st.instruction || st.description || st.name || ""; if (effectiveMode === "api") { const apiCalls = (Array.isArray(st.apiCalls) ? st.apiCalls : []).map(function (call: AnyObj) { return { api: call.name || call.metadata?.endpoint || "", description: call.description || "", method: call.metadata?.method || "", endpoint: call.metadata?.endpoint || "", input: call.input || {}, output: call.output || {}, }; }); if (!apiCalls.length) return null; return { subtask: { order, mode: "api", description, apiCalls } }; } const mcpCalls = (Array.isArray(st.mcpCalls) ? st.mcpCalls : []) .map(function (call: AnyObj) { const tool = getMcpCallName(call); if (!tool) return null; const out: AnyObj = { tool, input: call.input || {}, }; if (call.id != null) out.id = call.id; if (call.name != null) out.name = call.name; if (call.output != null) out.output = call.output; if (call.metadata != null) out.metadata = call.metadata; return out; }) .filter(Boolean); if (!mcpCalls.length) return null; return { subtask: { order, mode: "mcp", description, mcpCalls } }; }) .filter(Boolean) as AnyObj[]; } // Build the output task.json by merging the RL package into the original recording's // task.json structure so that both goals are satisfied: // 1. echo-extension can import it (requires metadata + task.instruction + task.networkRequests) // 2. All RL fields are embedded (rule_profile, golden_trajectory, etc.) // 3. Subtask groupings are populated with actual network request IDs. export function buildOutputTaskJson( originalTaskJson: AnyObj | null | undefined, taskPackage: AnyObj, mode: string | undefined, networkJson: any, ): AnyObj { // Deep-clone the original so we never mutate it let base: AnyObj = {}; if (originalTaskJson && typeof originalTaskJson === "object") { try { base = JSON.parse(JSON.stringify(originalTaskJson)); } catch (_) { base = {}; } } // ── echo-extension compatibility ────────────────────────────────────────── // The extension's validateImportData requires: // task.metadata (any object) // task.task (any object) // task.task.instruction (string) // task.task.networkRequests (array whose IDs all appear in network.json) if (!base.metadata || typeof base.metadata !== "object") { base.metadata = {}; } if (!base.task || typeof base.task !== "object") { base.task = {}; } // Update instruction from the RL package if (taskPackage.instruction) { base.task.instruction = taskPackage.instruction; } else if (!base.task.instruction) { base.task.instruction = ""; } // Ensure networkRequests is at least an empty array to pass validation. // If the original already had networkRequests, keep them so that the IDs // match the preserved network.json entries. if (!Array.isArray(base.task.networkRequests)) { base.task.networkRequests = []; } const networkRequests = base.task.networkRequests; const networkEntryIndex = buildNetworkEntryIndex(networkJson); const originalMcpCalls = collectExtensionMcpCalls(base.task); const mcpCallIndex = buildMcpCallIndex(originalMcpCalls); const mcpCallPool = buildMcpCallPool(originalMcpCalls); let outputRubrics: AnyObj[] = Array.isArray(taskPackage.rubrics) ? cloneJson(taskPackage.rubrics) : []; const subtasksHaveCalls = function (subtasks: AnyObj[]): boolean { return subtasks.some(function (st) { return (st.apiCalls && st.apiCalls.length > 0) || (st.mcpCalls && st.mcpCalls.length > 0); }); }; // Build subtasks for echo-extension. In MCP mode, Chrome's judge rebuilds the // trajectory from task.subtasks[].mcpCalls, so those calls must come only from // the original extension MCP call objects. Do not synthesize MCP calls from // HTTP-only requests such as a raw PUT message edit. let convertedSubtasks: AnyObj[] = []; if (mode === "mcp" && Array.isArray(taskPackage.subtasks) && taskPackage.subtasks.length > 0) { const fallbackRequestPool = buildRequestPool(networkRequests); convertedSubtasks = taskPackage.subtasks.map(function (st: AnyObj, i: number) { return convertSubtaskToExtensionFormat( st, i, networkRequests, mode, networkEntryIndex, fallbackRequestPool, mcpCallIndex, mcpCallPool, ); }); } if ( mode !== "mcp" && Array.isArray(taskPackage.golden_trajectory) && taskPackage.golden_trajectory.length > 0 ) { const derived = deriveSubtasksFromTrajectory( taskPackage.golden_trajectory, networkRequests, mode, networkEntryIndex, mcpCallIndex, ); if (derived.length > 0 && subtasksHaveCalls(derived)) { convertedSubtasks = derived; } } // API priority 1, and MCP fallback — convert recommended_groups using exact // request indices when present. if (!convertedSubtasks.length && Array.isArray(taskPackage.subtasks) && taskPackage.subtasks.length > 0) { const fallbackRequestPool = buildRequestPool(networkRequests); convertedSubtasks = taskPackage.subtasks.map(function (st: AnyObj, i: number) { return convertSubtaskToExtensionFormat( st, i, networkRequests, mode, networkEntryIndex, fallbackRequestPool, mcpCallIndex, mcpCallPool, ); }); } if (mode === "mcp") { convertedSubtasks = convertedSubtasks.filter(function (st) { return Array.isArray(st.mcpCalls) && st.mcpCalls.length > 0; }); convertedSubtasks = filterMcpSubtasksByNetworkIds(convertedSubtasks, networkRequests); convertedSubtasks = filterMcpSubtasksByEntityConsistency(convertedSubtasks, base.task.instruction || ""); } // Priority 2 — if converted subtasks still have no call items, derive from golden_trajectory // by matching (method + endpoint) to actual network request IDs (greedy, in order). const hasAnyCalls = subtasksHaveCalls(convertedSubtasks); if ( mode !== "mcp" && !hasAnyCalls && Array.isArray(taskPackage.golden_trajectory) && taskPackage.golden_trajectory.length > 0 ) { const derived = deriveSubtasksFromTrajectory( taskPackage.golden_trajectory, networkRequests, mode, networkEntryIndex, mcpCallIndex, ); if (derived.length > 0) { convertedSubtasks = derived; } } if (convertedSubtasks.length > 0) { base.task.subtasks = convertedSubtasks; } if (mode === "mcp" && outputRubrics.length) { outputRubrics = simplifyRubricCheckerKeysForMcp(outputRubrics, convertedSubtasks); } // Update rubrics in echo-extension import format after MCP subtasks are // finalized, so checker_key can be grounded in the exact exported mcpCalls. if (outputRubrics.length > 0) { base.task.rubrics = outputRubrics.map(function (rubric) { return convertRubricToExtensionFormat(rubric); }); } // ── RL package fields (for downstream RL consumers) ────────────────────── base.rule_profile = taskPackage.rule_profile; base.instruction = taskPackage.instruction || base.task.instruction || ""; base.subtasks = cloneJson(taskPackage.subtasks || []); base.rubrics = cloneJson(taskPackage.rubrics || []); base.recommended_groups = cloneJson(taskPackage.subtasks || []); base.recommended_rubrics = cloneJson(taskPackage.rubrics || []); base.rubric_checkers = mode === "mcp" && outputRubrics.length ? remapCheckerCatalogForStrictKeys(taskPackage.rubric_checkers, taskPackage.rubrics || [], outputRubrics) : taskPackage.rubric_checkers; base.dbdiff_criteria = taskPackage.dbdiff_criteria; base.pass_policy = taskPackage.pass_policy; const strictGoldenTrajectory = mode === "mcp" ? buildStrictGoldenTrajectoryFromExtensionSubtasks(base.task.subtasks || [], mode) : taskPackage.golden_trajectory; base.golden_trajectory = mode === "mcp" ? (Array.isArray(strictGoldenTrajectory) ? strictGoldenTrajectory : []) : taskPackage.golden_trajectory; if (taskPackage.dbdiff_canonical !== undefined) base.dbdiff_canonical = taskPackage.dbdiff_canonical; if (taskPackage.validation !== undefined) base.validation = taskPackage.validation; if (taskPackage.network !== undefined) base.network = taskPackage.network; // Judge 由用户在插件内重新运行,不从 RL 包写入 judge 数据。 // 原始 task.json 里的 judge 字段原样保留。 return base; } function findEntryPath(zip: JSZip, re: RegExp): string | null { let found: string | null = null; zip.forEach(function (path, entry) { if (!entry.dir && re.test(path)) found = path; }); return found; } // Node-side equivalent of legacy buildAnalysisZipBlob: returns a Buffer that // the API route can write to disk (no browser download). export async function buildAnalysisZipBuffer( options: BuildAnalysisZipOptions, ): Promise { options = options || ({} as BuildAnalysisZipOptions); const originalBytes = options.originalZipBytes; const originalFilename = options.originalFilename || "analysis.zip"; const taskPackage = options.taskPackage; const mode = options.mode === "api" ? "api" : "mcp"; if (!originalBytes) throw new Error("缺少原始 zip 字节,请重新上传 zip 后再下载。"); if (!taskPackage) throw new Error("缺少分析结果,请先完成分析。"); // Re-load the original bytes into a fresh JSZip instance to avoid mutating // any zip object held elsewhere in the importer state. const zip = await JSZip.loadAsync(originalBytes as any); // Apply golden_trajectory mode remapping for API grouping mode const packageForOutput = remapTrajectoryForMode(taskPackage, mode); // Find the task.json entry path (preserves nested paths, e.g. folder/task.json) const taskEntryPath = findEntryPath(zip, /(^|\/)task\.json$/i) || "task.json"; // Read the original task.json so we can preserve the extension-compatible structure let originalTaskJson: AnyObj | null = null; let originalNetworkJson: any = null; const taskEntry = zip.file(taskEntryPath); if (taskEntry) { try { const rawText = await taskEntry.async("text"); originalTaskJson = JSON.parse(rawText); } catch (_) { // If we can't parse the original, build from scratch below } } const networkEntryPath = findEntryPath(zip, /(^|\/)network\.json$/i); const networkEntry = networkEntryPath ? zip.file(networkEntryPath) : null; if (networkEntry) { try { originalNetworkJson = JSON.parse(await networkEntry.async("text")); } catch (_) { originalNetworkJson = null; } } // Build hybrid task.json: extension-importable structure + full RL package data const outputTaskJson = buildOutputTaskJson(originalTaskJson, packageForOutput, mode, originalNetworkJson); zip.file(taskEntryPath, JSON.stringify(outputTaskJson, null, 2)); const buffer = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE", compressionOptions: { level: 6 }, }); const baseName = String(originalFilename || "analysis.zip").replace(/\.zip$/i, "") || "analysis"; const outName = baseName + ".task-package.zip"; return { buffer: buffer as Buffer, filename: outName }; }