Rl-Auto / packages /core /src /zip-export.ts
Lazywords's picture
Deploy RL Auto Docker Space
c4ae742
Raw
History Blame Contribute Delete
49.9 kB
// 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<string, any>;
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<string, string> {
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<string, string> = {};
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<string, string>): 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<T>(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<string, boolean> = {};
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<string, string[]>, 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<string, string[]> = {}, depth = 0): Record<string, string[]> {
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<string, string[]> {
return collectEntityIds(call && (call.input || call.args || {}));
}
function entitiesCompatible(entities: Record<string, string[]>, anchors: Record<string, string>): 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<string, string>, entities: Record<string, string[]>): 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<string, string> = {};
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<string, boolean> = {};
(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<string, boolean> {
const tools: Record<string, boolean> = {};
(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, boolean>): 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<string, string> = {
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<BuildAnalysisZipResult> {
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 };
}