@wenathlan/extension 1.1.37 → 1.1.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/dist/index.js +227 -7
- package/dist/index.js.map +2 -2
- package/dist/memory.d.ts +35 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/policy.d.ts +8 -0
- package/dist/policy.d.ts.map +1 -1
- package/dist/protocol.d.ts +25 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/types.d.ts +96 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/extension/dist/background.js +784 -9
- package/extension/dist/background.js.map +4 -4
- package/extension/dist/manifest.json +1 -1
- package/extension/dist/pagebridge.js +187 -2
- package/extension/dist/pagebridge.js.map +4 -4
- package/extension/dist/popup.html +1 -1
- package/extension/dist/popup.js +12 -2
- package/extension/dist/popup.js.map +2 -2
- package/extension/dist/sidepanel.html +1 -1
- package/extension/dist/sidepanel.js +173 -2
- package/extension/dist/sidepanel.js.map +4 -4
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -520,21 +520,122 @@ var sessionmemory = class {
|
|
|
520
520
|
async getcodevalue() {
|
|
521
521
|
return this.adapter.get("codevalue");
|
|
522
522
|
}
|
|
523
|
+
/** Stores one dataset with its column specs and rows, replacing the previous record of that id. */
|
|
524
|
+
async setdataset(value) {
|
|
525
|
+
await this.adapter.set(`dataset${value.id}`, value);
|
|
526
|
+
const ids = (await this.adapter.get("datasets") ?? []).filter((id) => id !== value.id);
|
|
527
|
+
await this.adapter.set("datasets", [value.id, ...ids]);
|
|
528
|
+
}
|
|
529
|
+
/** Returns one stored dataset by its id with its column specs and row count. */
|
|
530
|
+
async getdataset(id) {
|
|
531
|
+
return this.adapter.get(`dataset${id}`);
|
|
532
|
+
}
|
|
533
|
+
/** Returns every stored dataset id, newest first. */
|
|
534
|
+
async getdatasets() {
|
|
535
|
+
const ids = await this.adapter.get("datasets") ?? [];
|
|
536
|
+
const records = [];
|
|
537
|
+
for (const id of ids) {
|
|
538
|
+
const record2 = await this.adapter.get(`dataset${id}`);
|
|
539
|
+
if (record2) records.push(record2);
|
|
540
|
+
}
|
|
541
|
+
return records;
|
|
542
|
+
}
|
|
543
|
+
/** Stores one imported csv dataset for fill loops beside the dataset store. */
|
|
544
|
+
async addimport(value) {
|
|
545
|
+
await this.setdataset(value);
|
|
546
|
+
const ids = (await this.adapter.get("imports") ?? []).filter((id) => id !== value.id);
|
|
547
|
+
await this.adapter.set("imports", [value.id, ...ids]);
|
|
548
|
+
}
|
|
549
|
+
/** Returns every imported csv dataset for fill loops, newest first. */
|
|
550
|
+
async getimports() {
|
|
551
|
+
const ids = await this.adapter.get("imports") ?? [];
|
|
552
|
+
const records = [];
|
|
553
|
+
for (const id of ids) {
|
|
554
|
+
const record2 = await this.adapter.get(`dataset${id}`);
|
|
555
|
+
if (record2) records.push(record2);
|
|
556
|
+
}
|
|
557
|
+
return records;
|
|
558
|
+
}
|
|
559
|
+
/** Stores one extraction session with its cursor and page history, replacing the previous session of that id. */
|
|
560
|
+
async setextractsession(value) {
|
|
561
|
+
await this.adapter.set(`extract${value.id}`, value);
|
|
562
|
+
const ids = (await this.adapter.get("extracts") ?? []).filter((id) => id !== value.id);
|
|
563
|
+
await this.adapter.set("extracts", [value.id, ...ids]);
|
|
564
|
+
}
|
|
565
|
+
/** Returns every extraction session with its cursor and page history, newest first. */
|
|
566
|
+
async getextractsessions() {
|
|
567
|
+
const ids = await this.adapter.get("extracts") ?? [];
|
|
568
|
+
const records = [];
|
|
569
|
+
for (const id of ids) {
|
|
570
|
+
const record2 = await this.adapter.get(`extract${id}`);
|
|
571
|
+
if (record2) records.push(record2);
|
|
572
|
+
}
|
|
573
|
+
return records;
|
|
574
|
+
}
|
|
575
|
+
/** Records one provenance record of an exported artifact. */
|
|
576
|
+
async addprovenance(record2) {
|
|
577
|
+
const records = await this.getprovenances();
|
|
578
|
+
await this.adapter.set("provenances", [record2, ...records]);
|
|
579
|
+
}
|
|
580
|
+
/** Returns every provenance record per exported artifact, newest first. */
|
|
581
|
+
async getprovenances() {
|
|
582
|
+
return await this.adapter.get("provenances") ?? [];
|
|
583
|
+
}
|
|
584
|
+
/** Stores the transform rules and dedupe keys of one task, replacing the previous record of that task. */
|
|
585
|
+
async settaskrules(value) {
|
|
586
|
+
const records = (await this.adapter.get("taskrules") ?? []).filter((item) => item.taskid !== value.taskid);
|
|
587
|
+
await this.adapter.set("taskrules", [value, ...records]);
|
|
588
|
+
}
|
|
589
|
+
/** Returns the transform rules and dedupe keys per task, newest first. */
|
|
590
|
+
async gettaskrules() {
|
|
591
|
+
return await this.adapter.get("taskrules") ?? [];
|
|
592
|
+
}
|
|
593
|
+
/** Stores one stream chunk state for resume, replacing the previous state of that dataset. */
|
|
594
|
+
async setstream(state) {
|
|
595
|
+
const records = (await this.adapter.get("streams") ?? []).filter((item) => item.datasetid !== state.datasetid);
|
|
596
|
+
await this.adapter.set("streams", [state, ...records]);
|
|
597
|
+
}
|
|
598
|
+
/** Returns every stream chunk state persisted for resume, newest first. */
|
|
599
|
+
async getstreams() {
|
|
600
|
+
return await this.adapter.get("streams") ?? [];
|
|
601
|
+
}
|
|
602
|
+
/** Stores one reviewed sheet endpoint config behind its origin grant, replacing the previous config of that origin. */
|
|
603
|
+
async setsheetendpoint(config) {
|
|
604
|
+
const records = (await this.adapter.get("sheetendpoints") ?? []).filter((item) => item.origin !== config.origin);
|
|
605
|
+
await this.adapter.set("sheetendpoints", [config, ...records]);
|
|
606
|
+
}
|
|
607
|
+
/** Returns every reviewed sheet endpoint config, newest first. */
|
|
608
|
+
async getsheetendpoints() {
|
|
609
|
+
return await this.adapter.get("sheetendpoints") ?? [];
|
|
610
|
+
}
|
|
611
|
+
/** Records one exported data artifact; artifact retention is a user setting and an absent setting keeps every artifact. */
|
|
612
|
+
async addexport(artifact) {
|
|
613
|
+
const records = await this.getexports();
|
|
614
|
+
const combined = [artifact, ...records.filter((item) => item.id !== artifact.id)];
|
|
615
|
+
const retention = (await this.getsettings())?.artifactretention;
|
|
616
|
+
await this.adapter.set("exports", retention === void 0 ? combined : combined.slice(0, retention));
|
|
617
|
+
}
|
|
618
|
+
/** Returns every exported data artifact with its content and checksum, newest first. */
|
|
619
|
+
async getexports() {
|
|
620
|
+
return await this.adapter.get("exports") ?? [];
|
|
621
|
+
}
|
|
523
622
|
};
|
|
524
623
|
function randomid() {
|
|
525
624
|
return crypto.randomUUID();
|
|
526
625
|
}
|
|
527
626
|
|
|
528
627
|
// policy.ts
|
|
529
|
-
var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select", "presskey", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "reload", "back", "forward", "writestorage", "setattribute", "removeattribute", "evaluate", "tabcreate", "tabactivate", "tabclose", "tabreload", "windowcreate", "windowclose", "windowresize", "downloadfile", "clickpoint", "shiftclick", "dismissdialog", "enterframe", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "openlink", "openprivate", "reloadcache", "stopnav", "followlink", "spanav", "rewritequery", "setfragment", "navlist", "navprofile", "handleauth", "printpdf", "prefetch", "preconnect", "deeplink", "reopentab", "pausenav", "navrate", "openclipboard", "batchopen", "duplicatetab", "closepattern", "pintab", "mutetab", "movetab", "movetabwindow", "grouptabs", "colorgroup", "collapsegroup", "discardtab", "reloadtabs", "zoomin", "zoomout", "switchtab", "maximizewindow", "minimizewindow", "restorewindow", "focuswindow", "scratchwindow", "incognitowindow", "restoretab", "restorelayout", "reopenrun", "badgetab", "fillform", "filllabel", "fillplaceholder", "submitform", "retryform", "runwizard", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcard", "fillcode", "consentpassword"]);
|
|
628
|
+
var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select", "presskey", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "reload", "back", "forward", "writestorage", "setattribute", "removeattribute", "evaluate", "tabcreate", "tabactivate", "tabclose", "tabreload", "windowcreate", "windowclose", "windowresize", "downloadfile", "clickpoint", "shiftclick", "dismissdialog", "enterframe", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "openlink", "openprivate", "reloadcache", "stopnav", "followlink", "spanav", "rewritequery", "setfragment", "navlist", "navprofile", "handleauth", "printpdf", "prefetch", "preconnect", "deeplink", "reopentab", "pausenav", "navrate", "openclipboard", "batchopen", "duplicatetab", "closepattern", "pintab", "mutetab", "movetab", "movetabwindow", "grouptabs", "colorgroup", "collapsegroup", "discardtab", "reloadtabs", "zoomin", "zoomout", "switchtab", "maximizewindow", "minimizewindow", "restorewindow", "focuswindow", "scratchwindow", "incognitowindow", "restoretab", "restorelayout", "reopenrun", "badgetab", "fillform", "filllabel", "fillplaceholder", "submitform", "retryform", "runwizard", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcard", "fillcode", "consentpassword", "exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "streamdisk", "paginateextract", "resumeextract"]);
|
|
530
629
|
var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover", "clickdeep", "rightclick", "doubleclick", "scrollpage", "scrollby", "scrollend", "scrolltop", "fullscreen", "zoomset", "movepointer", "clicktext", "clickaria", "clickname", "expanddetails", "pierceshadow", "retryaction"]);
|
|
531
|
-
var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot", "mapclicks", "verifyvisible", "verifyenabled", "resolvexpath", "a11ytree", "readvisible", "readertree", "detectlists", "detecttables", "readjson", "watchmutate", "waitquiet", "watchbanner", "detectinfinitescroll", "detectvirtual", "detectlazy", "readscrollpos", "readlang", "readoutline", "countpages", "listshadow", "listframes", "classifypage", "fingerprintsection", "diffsnapshots", "readselection", "watchfocus", "detectsticky", "detectscrolllock", "readopengraph", "detectlanguage", "deriveselector", "waitload", "waiturl", "spawait", "detecthttp", "readredirects", "readfinalurl", "trailaudit", "navintent", "checksafe", "querytabs", "watchtab", "findclones", "searchtabs", "listaudio", "snapshotsession", "savelayout", "attachmeta", "detectfields", "generatevalues", "saveprofiles", "asksubmit", "readerrors", "skiphoneypot", "detectlogin", "detecttemplate", "handoffcaptcha"]);
|
|
630
|
+
var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot", "mapclicks", "verifyvisible", "verifyenabled", "resolvexpath", "a11ytree", "readvisible", "readertree", "detectlists", "detecttables", "readjson", "watchmutate", "waitquiet", "watchbanner", "detectinfinitescroll", "detectvirtual", "detectlazy", "readscrollpos", "readlang", "readoutline", "countpages", "listshadow", "listframes", "classifypage", "fingerprintsection", "diffsnapshots", "readselection", "watchfocus", "detectsticky", "detectscrolllock", "readopengraph", "detectlanguage", "deriveselector", "waitload", "waiturl", "spawait", "detecthttp", "readredirects", "readfinalurl", "trailaudit", "navintent", "checksafe", "querytabs", "watchtab", "findclones", "searchtabs", "listaudio", "snapshotsession", "savelayout", "attachmeta", "detectfields", "generatevalues", "saveprofiles", "asksubmit", "readerrors", "skiphoneypot", "detectlogin", "detecttemplate", "handoffcaptcha", "scrapetable", "importcsv", "looprows", "transformvalues", "deduperows", "mergepages", "stamplerows", "previewgrid", "logprovenance"]);
|
|
532
631
|
var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
|
|
533
632
|
var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus", "watchtab"]);
|
|
534
|
-
var targetactions = /* @__PURE__ */ new Set(["inspect", "focus", "click", "type", "scroll", "select", "hover", "clickdeep", "rightclick", "doubleclick", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "highlight", "setattribute", "removeattribute", "waitfor", "shiftclick", "typetime", "appendtext", "setvalue", "typeedit", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "expanddetails", "verifyvisible", "verifyenabled", "pierceshadow", "deriveselector", "fingerprintsection", "submitform", "retryform", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcode", "consentpassword"]);
|
|
633
|
+
var targetactions = /* @__PURE__ */ new Set(["inspect", "focus", "click", "type", "scroll", "select", "hover", "clickdeep", "rightclick", "doubleclick", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "highlight", "setattribute", "removeattribute", "waitfor", "shiftclick", "typetime", "appendtext", "setvalue", "typeedit", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "expanddetails", "verifyvisible", "verifyenabled", "pierceshadow", "deriveselector", "fingerprintsection", "submitform", "retryform", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcode", "consentpassword", "scrapetable", "paginateextract"]);
|
|
535
634
|
var valueactions = /* @__PURE__ */ new Set(["presskey", "drag", "drop", "upload", "readattribute", "removeattribute", "waittext", "evaluate", "zoomset", "tabactivate", "tabclose", "tabreload", "windowclose", "windowresize", "tabcreate", "windowcreate", "downloadfile", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "chooseradio", "setslider", "setdate", "setcolor", "followlink", "setfragment", "handleauth", "navintent", "openclipboard", "checksafe", "reopentab", "spanav", "duplicatetab", "pintab", "mutetab", "movetab", "movetabwindow", "searchtabs", "badgetab", "attachmeta", "focuswindow", "maximizewindow", "minimizewindow", "restorewindow", "incognitowindow", "asksubmit", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcode", "consentpassword"]);
|
|
536
635
|
var tabscommandactions = /* @__PURE__ */ new Set(["querytabs", "duplicatetab", "closepattern", "pintab", "mutetab", "movetab", "movetabwindow", "grouptabs", "colorgroup", "collapsegroup", "discardtab", "reloadtabs", "zoomin", "zoomout", "watchtab", "switchtab", "maximizewindow", "minimizewindow", "restorewindow", "focuswindow", "scratchwindow", "incognitowindow", "restoretab", "savelayout", "restorelayout", "findclones", "searchtabs", "badgetab", "attachmeta", "listaudio", "reopenrun", "snapshotsession"]);
|
|
537
636
|
var formactions = /* @__PURE__ */ new Set(["fillform", "filllabel", "fillplaceholder", "detectfields", "generatevalues", "saveprofiles", "asksubmit", "submitform", "readerrors", "retryform", "runwizard", "selectchain", "picktypeahead", "pickdate", "attachfile", "handoffcaptcha", "fillcard", "fillcode", "consentpassword", "skiphoneypot", "detectlogin", "detecttemplate"]);
|
|
637
|
+
var datasetactions = /* @__PURE__ */ new Set(["scrapetable", "exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "importcsv", "looprows", "transformvalues", "deduperows", "paginateextract", "mergepages", "stamplerows", "previewgrid", "streamdisk", "resumeextract", "logprovenance"]);
|
|
638
|
+
var exportactions = /* @__PURE__ */ new Set(["exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "streamdisk"]);
|
|
538
639
|
var fieldkinds = ["text", "email", "phone", "date", "number", "select", "check", "radio", "file", "password", "card", "code"];
|
|
539
640
|
var layoutmutationactions = /* @__PURE__ */ new Set(["grouptabs", "colorgroup", "collapsegroup", "savelayout", "restorelayout"]);
|
|
540
641
|
var groupcolors = ["grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"];
|
|
@@ -569,6 +670,7 @@ function requiredcapability(kind) {
|
|
|
569
670
|
if (kind === "tablist") return "tabs";
|
|
570
671
|
if (kind === "downloadfile") return "downloads";
|
|
571
672
|
if (kind === "openclipboard") return "clipboardRead";
|
|
673
|
+
if (kind === "copytable") return "clipboardWrite";
|
|
572
674
|
if (kind === "openlink" || kind === "openprivate" || kind === "navlist" || kind === "batchopen" || kind === "reopentab" || kind === "deeplink") return "tabs";
|
|
573
675
|
if (tabscommandactions.has(kind)) return "tabs";
|
|
574
676
|
return void 0;
|
|
@@ -582,6 +684,16 @@ function islayoutkind(kind) {
|
|
|
582
684
|
function isformkind(kind) {
|
|
583
685
|
return formactions.has(kind);
|
|
584
686
|
}
|
|
687
|
+
function isdatasetkind(kind) {
|
|
688
|
+
return datasetactions.has(kind);
|
|
689
|
+
}
|
|
690
|
+
function isexportkind(kind) {
|
|
691
|
+
return exportactions.has(kind);
|
|
692
|
+
}
|
|
693
|
+
function exportgranted(session, origin) {
|
|
694
|
+
if (!origingranted(session, origin)) return { allowed: false, reason: `The export of extracted data from ${origin} needs the session origin grants before it leaves local memory.` };
|
|
695
|
+
return { allowed: true };
|
|
696
|
+
}
|
|
585
697
|
function validatefieldmatch(value) {
|
|
586
698
|
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed field match is required in options." };
|
|
587
699
|
const match = value;
|
|
@@ -679,6 +791,89 @@ function validateformgrammar(step, options) {
|
|
|
679
791
|
if (kind === "consentpassword" && !isnonempty(options.consentref)) return { allowed: false, reason: "A reviewed consent ref is required in options before any password is filled." };
|
|
680
792
|
return { allowed: true };
|
|
681
793
|
}
|
|
794
|
+
function validatetransformrule(value) {
|
|
795
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed transform rule with an expression, sources and a target is required in options." };
|
|
796
|
+
const rule = value;
|
|
797
|
+
const expression = rule.expression;
|
|
798
|
+
if (typeof expression !== "string" || !/^(trim|upper|lower|number|prefix|suffix|replace)(?::.+)?$/.test(expression)) return { allowed: false, reason: "The reviewed transform expression must be trim, upper, lower, number, prefix, suffix or replace with an optional argument." };
|
|
799
|
+
if (expression.startsWith("replace") && !expression.slice("replace".length).includes("=>")) return { allowed: false, reason: "The reviewed replace expression needs the from=>to separator." };
|
|
800
|
+
if (expression.startsWith("replace") && expression.slice("replace:".length).split("=>")[0] === "") return { allowed: false, reason: "The reviewed replace expression needs a non-empty from part." };
|
|
801
|
+
if (!Array.isArray(rule.sources) || rule.sources.length === 0 || !rule.sources.every((source) => isnonempty(source))) return { allowed: false, reason: "Every reviewed transform rule needs a non-empty list of source columns." };
|
|
802
|
+
if (!isnonempty(rule.target)) return { allowed: false, reason: "Every reviewed transform rule needs a non-empty target column." };
|
|
803
|
+
return { allowed: true };
|
|
804
|
+
}
|
|
805
|
+
function validatedatasetids(options, key) {
|
|
806
|
+
const ids = options[key];
|
|
807
|
+
if (!Array.isArray(ids) || ids.length === 0 || !ids.every((id) => isnonempty(id))) return { allowed: false, reason: `A reviewed non-empty list of dataset ids is required in options as ${key}.` };
|
|
808
|
+
return { allowed: true };
|
|
809
|
+
}
|
|
810
|
+
function validatedatagrammar(step, options, origin) {
|
|
811
|
+
const kind = step.kind;
|
|
812
|
+
if (kind === "scrapetable") {
|
|
813
|
+
if (options.name !== void 0 && !isnonempty(options.name)) return { allowed: false, reason: "The reviewed dataset name must be a non-empty string." };
|
|
814
|
+
if (options.rowlimit !== void 0 && (typeof options.rowlimit !== "number" || !Number.isInteger(options.rowlimit) || options.rowlimit < 1)) return { allowed: false, reason: "The reviewed row limit must be a positive integer with no code ceiling." };
|
|
815
|
+
}
|
|
816
|
+
if (kind === "paginateextract") {
|
|
817
|
+
if (!isnonempty(options.next)) return { allowed: false, reason: "A reviewed next control selector is required in options." };
|
|
818
|
+
if (options.pages !== void 0 && (typeof options.pages !== "number" || !Number.isInteger(options.pages) || options.pages < 1)) return { allowed: false, reason: "The reviewed page count must be a positive integer with no code ceiling." };
|
|
819
|
+
if (!nonnegativeoption(options, "wait")) return { allowed: false, reason: "The reviewed row freshness wait must be zero or a positive number of milliseconds." };
|
|
820
|
+
}
|
|
821
|
+
if (kind === "exportcsv" || kind === "exportjson" || kind === "exportexcel" || kind === "copytable" || kind === "streamdisk") {
|
|
822
|
+
if (!isnonempty(options.dataset)) return { allowed: false, reason: "A reviewed dataset id is required in options." };
|
|
823
|
+
if (options.name !== void 0 && !isnonempty(options.name)) return { allowed: false, reason: "The reviewed artifact name must be a non-empty string." };
|
|
824
|
+
}
|
|
825
|
+
if (kind === "exportcsv" && options.delimiter !== void 0 && (typeof options.delimiter !== "string" || options.delimiter.length !== 1)) return { allowed: false, reason: "The reviewed csv delimiter must be a single character." };
|
|
826
|
+
if (kind === "streamdisk" && (typeof options.chunk !== "number" || !Number.isInteger(options.chunk) || options.chunk < 1)) return { allowed: false, reason: "The reviewed streaming chunk size must be a positive integer with no code ceiling." };
|
|
827
|
+
if (kind === "pushsheets") {
|
|
828
|
+
if (!isnonempty(options.dataset)) return { allowed: false, reason: "A reviewed dataset id is required in options." };
|
|
829
|
+
if (!isnonempty(options.sheet)) return { allowed: false, reason: "A reviewed sheet endpoint url is required in options." };
|
|
830
|
+
if (!ishttpsurl(options.sheet)) return { allowed: false, reason: "The reviewed sheet endpoint url must use HTTPS." };
|
|
831
|
+
if (options.reviewed !== true) return { allowed: false, reason: "The sheet push needs the explicit reviewed flag before any data leaves local memory." };
|
|
832
|
+
}
|
|
833
|
+
if (kind === "importcsv") {
|
|
834
|
+
if (typeof options.csv !== "string" || !options.csv.trim()) return { allowed: false, reason: "Reviewed csv content is required in options." };
|
|
835
|
+
if (options.name !== void 0 && !isnonempty(options.name)) return { allowed: false, reason: "The reviewed dataset name must be a non-empty string." };
|
|
836
|
+
if (options.mapping !== void 0) {
|
|
837
|
+
const mapping = options.mapping;
|
|
838
|
+
if (!mapping || typeof mapping !== "object" || Array.isArray(mapping) || !Object.values(mapping).every((item) => typeof item === "string")) return { allowed: false, reason: "The reviewed csv column mapping must be an object of string values." };
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
if (kind === "looprows") {
|
|
842
|
+
if (!isnonempty(options.dataset)) return { allowed: false, reason: "A reviewed dataset id is required in options." };
|
|
843
|
+
if (options.variable !== void 0 && !isnonempty(options.variable)) return { allowed: false, reason: "The reviewed row variable name must be a non-empty string." };
|
|
844
|
+
const inner = validateinnerstep(options, origin);
|
|
845
|
+
if (!inner.allowed) return inner;
|
|
846
|
+
}
|
|
847
|
+
if (kind === "transformvalues") {
|
|
848
|
+
if (!isnonempty(options.dataset)) return { allowed: false, reason: "A reviewed dataset id is required in options." };
|
|
849
|
+
const rules = options.rules;
|
|
850
|
+
if (!Array.isArray(rules) || rules.length === 0) return { allowed: false, reason: "A reviewed non-empty list of transform rules is required in options." };
|
|
851
|
+
for (const item of rules) {
|
|
852
|
+
const rulecheck = validatetransformrule(item);
|
|
853
|
+
if (!rulecheck.allowed) return rulecheck;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
if (kind === "deduperows") {
|
|
857
|
+
if (!isnonempty(options.dataset)) return { allowed: false, reason: "A reviewed dataset id is required in options." };
|
|
858
|
+
const keys = options.keys;
|
|
859
|
+
if (!Array.isArray(keys) || keys.length === 0 || !keys.every((key) => isnonempty(key))) return { allowed: false, reason: "A reviewed non-empty list of dedupe column keys is required in options." };
|
|
860
|
+
}
|
|
861
|
+
if (kind === "mergepages") {
|
|
862
|
+
const listcheck = validatedatasetids(options, "datasets");
|
|
863
|
+
if (!listcheck.allowed) return listcheck;
|
|
864
|
+
}
|
|
865
|
+
if (kind === "stamplerows") {
|
|
866
|
+
if (!isnonempty(options.dataset)) return { allowed: false, reason: "A reviewed dataset id is required in options." };
|
|
867
|
+
if (options.url !== void 0 && !ishttpsurl(options.url)) return { allowed: false, reason: "The reviewed source url must use HTTPS." };
|
|
868
|
+
}
|
|
869
|
+
if (kind === "previewgrid") {
|
|
870
|
+
if (!isnonempty(options.dataset)) return { allowed: false, reason: "A reviewed dataset id is required in options." };
|
|
871
|
+
if (options.sample !== void 0 && (typeof options.sample !== "number" || !Number.isInteger(options.sample) || options.sample < 1)) return { allowed: false, reason: "The reviewed sample row count must be a positive integer with no code ceiling." };
|
|
872
|
+
}
|
|
873
|
+
if (kind === "resumeextract" && !isnonempty(options.session)) return { allowed: false, reason: "A reviewed extract session id is required in options." };
|
|
874
|
+
if (kind === "logprovenance" && !isnonempty(options.artifact)) return { allowed: false, reason: "A reviewed artifact id or name is required in options." };
|
|
875
|
+
return { allowed: true };
|
|
876
|
+
}
|
|
682
877
|
function submitreviewgranted(steps, submitid) {
|
|
683
878
|
const position = steps.findIndex((candidate) => candidate.id === submitid);
|
|
684
879
|
const asked = steps.some((candidate, index) => candidate.kind === "asksubmit" && (position === -1 || index < position));
|
|
@@ -817,7 +1012,7 @@ function validateinnerstep(options, origin) {
|
|
|
817
1012
|
return { allowed: true };
|
|
818
1013
|
}
|
|
819
1014
|
if (typeof kind !== "string" || !kind.trim()) return { allowed: false, reason: "A reviewed step id or inline step kind is required in options." };
|
|
820
|
-
if (kind === "retryaction" || kind === "enterframe") return { allowed: false, reason: "The reviewed inner step cannot be another wrapper kind." };
|
|
1015
|
+
if (kind === "retryaction" || kind === "enterframe" || kind === "looprows") return { allowed: false, reason: "The reviewed inner step cannot be another wrapper kind." };
|
|
821
1016
|
if (!allowedactions.has(kind)) return { allowed: false, reason: "The reviewed inner step kind is unsupported." };
|
|
822
1017
|
const inneroptions = options.options;
|
|
823
1018
|
if (inneroptions !== void 0 && (!inneroptions || typeof inneroptions !== "object" || Array.isArray(inneroptions))) return { allowed: false, reason: "The reviewed inner step options must be an object." };
|
|
@@ -1201,6 +1396,10 @@ function validatestep(step, origin) {
|
|
|
1201
1396
|
const formcheck = validateformgrammar(step, options);
|
|
1202
1397
|
if (!formcheck.allowed) return formcheck;
|
|
1203
1398
|
}
|
|
1399
|
+
if (isdatasetkind(step.kind)) {
|
|
1400
|
+
const datacheck = validatedatagrammar(step, options, origin);
|
|
1401
|
+
if (!datacheck.allowed) return datacheck;
|
|
1402
|
+
}
|
|
1204
1403
|
if (step.kind === "tabcreate") {
|
|
1205
1404
|
if (options.background !== void 0 && typeof options.background !== "boolean") return { allowed: false, reason: "The reviewed background flag must be a boolean." };
|
|
1206
1405
|
if (options.window !== void 0 && (typeof options.window !== "number" || !Number.isInteger(options.window) || options.window < 0)) return { allowed: false, reason: "The reviewed target window id must be a non-negative integer." };
|
|
@@ -1228,6 +1427,10 @@ function canexecute(input) {
|
|
|
1228
1427
|
if (input.plan.expiresat <= now) return { allowed: false, reason: "The approved plan has expired." };
|
|
1229
1428
|
if ((input.step.kind === "pierceshadow" || input.step.kind === "enterframe") && !origingranted(input.session, input.origin)) return { allowed: false, reason: "The shadow or frame step is outside the session origin grants." };
|
|
1230
1429
|
if (input.step.kind === "readjson" && !origingranted(input.session, input.origin)) return { allowed: false, reason: "The json state read is outside the session origin grants." };
|
|
1430
|
+
if (isexportkind(input.step.kind)) {
|
|
1431
|
+
const exportgate = exportgranted(input.session, input.origin);
|
|
1432
|
+
if (!exportgate.allowed) return exportgate;
|
|
1433
|
+
}
|
|
1231
1434
|
if (input.step.kind === "navlist") {
|
|
1232
1435
|
let options = {};
|
|
1233
1436
|
try {
|
|
@@ -1336,6 +1539,15 @@ function wizardcompletion(state) {
|
|
|
1336
1539
|
if (state.steps <= 0) return 0;
|
|
1337
1540
|
return Math.min(1, state.completed.filter(Boolean).length / state.steps);
|
|
1338
1541
|
}
|
|
1542
|
+
function extractionshare(rowscollected, estimatedtotal) {
|
|
1543
|
+
if (!Number.isFinite(estimatedtotal) || estimatedtotal <= 0) return 0;
|
|
1544
|
+
return Math.min(1, Math.max(0, rowscollected) / estimatedtotal);
|
|
1545
|
+
}
|
|
1546
|
+
function recordextraction(progress, planid, stepid, entry, now) {
|
|
1547
|
+
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
1548
|
+
const outcome = { stepid, ok: true, summary: `Extraction page ${entry.page} collected ${entry.rows} row${entry.rows === 1 ? "" : "s"} at cursor ${entry.cursor}.`, details: { extraction: entry }, at: now };
|
|
1549
|
+
return recordoutcome(base, planid, outcome, now);
|
|
1550
|
+
}
|
|
1339
1551
|
function recordwizardstep(progress, planid, stepid, state, now) {
|
|
1340
1552
|
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
1341
1553
|
const executed = Math.min(state.index, state.steps);
|
|
@@ -1345,7 +1557,7 @@ function recordwizardstep(progress, planid, stepid, state, now) {
|
|
|
1345
1557
|
}
|
|
1346
1558
|
|
|
1347
1559
|
// version.ts
|
|
1348
|
-
var packageversion = "1.1.
|
|
1560
|
+
var packageversion = "1.1.38";
|
|
1349
1561
|
|
|
1350
1562
|
// types.ts
|
|
1351
1563
|
var protocolversion = packageversion;
|
|
@@ -1382,9 +1594,9 @@ function parseproposal(value, origin) {
|
|
|
1382
1594
|
return step;
|
|
1383
1595
|
});
|
|
1384
1596
|
for (const step of steps) {
|
|
1385
|
-
if (step.kind !== "retryaction" && step.kind !== "enterframe") continue;
|
|
1597
|
+
if (step.kind !== "retryaction" && step.kind !== "enterframe" && step.kind !== "looprows") continue;
|
|
1386
1598
|
const options = parseoptions(step);
|
|
1387
|
-
if (typeof options.stepid === "string" && !steps.some((candidate) => candidate.id === options.stepid)) throw new Error("A retry or
|
|
1599
|
+
if (typeof options.stepid === "string" && !steps.some((candidate) => candidate.id === options.stepid)) throw new Error("A retry, frame or loop wrapper references an unknown step id.");
|
|
1388
1600
|
}
|
|
1389
1601
|
for (const step of steps) {
|
|
1390
1602
|
if (step.kind !== "submitform" && step.kind !== "retryform") continue;
|
|
@@ -1451,6 +1663,17 @@ function errorreportresponse(input) {
|
|
|
1451
1663
|
function wizardreport(input) {
|
|
1452
1664
|
return { version: protocolversion, ...input.sessionid ? { sessionid: input.sessionid } : {}, wizards: input.wizards, picks: input.picks };
|
|
1453
1665
|
}
|
|
1666
|
+
function datasetresponse(input) {
|
|
1667
|
+
const sample = Math.max(0, Math.floor(input.sample ?? 10));
|
|
1668
|
+
const payload = { ...input.dataset, rows: input.dataset.rows.slice(0, sample), totalrows: input.dataset.rows.length };
|
|
1669
|
+
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, dataset: payload });
|
|
1670
|
+
}
|
|
1671
|
+
function extractionreport(input) {
|
|
1672
|
+
return { version: protocolversion, sessions: input.sessions };
|
|
1673
|
+
}
|
|
1674
|
+
function provenancereport(input) {
|
|
1675
|
+
return { version: protocolversion, records: input.records };
|
|
1676
|
+
}
|
|
1454
1677
|
|
|
1455
1678
|
// extension/browsertabs.ts
|
|
1456
1679
|
var browserkinds = /* @__PURE__ */ new Set(["tablist", "tabcreate", "tabactivate", "tabclose", "tabreload", "tabsnapshot", "windowlist", "windowcreate", "windowclose", "zoomset", "windowresize", "downloadfile"]);
|
|
@@ -2213,6 +2436,261 @@ function backoffwaits(attempts, wait, factor) {
|
|
|
2213
2436
|
return windows;
|
|
2214
2437
|
}
|
|
2215
2438
|
|
|
2439
|
+
// extension/pagedata.ts
|
|
2440
|
+
function normalizeheader(label) {
|
|
2441
|
+
const slug = label.trim().toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
|
|
2442
|
+
return slug || "column";
|
|
2443
|
+
}
|
|
2444
|
+
function columnspecof(label, used = /* @__PURE__ */ new Set()) {
|
|
2445
|
+
const base = normalizeheader(label);
|
|
2446
|
+
let key = base;
|
|
2447
|
+
let suffix = 2;
|
|
2448
|
+
while (used.has(key)) {
|
|
2449
|
+
key = `${base}${suffix}`;
|
|
2450
|
+
suffix += 1;
|
|
2451
|
+
}
|
|
2452
|
+
used.add(key);
|
|
2453
|
+
return { key, label: label.trim(), kind: "text", normalized: label.trim().toLowerCase() };
|
|
2454
|
+
}
|
|
2455
|
+
function rowhash(row, keys) {
|
|
2456
|
+
const source = (keys.length > 0 ? keys : Object.keys(row).sort()).map((key) => `${key}=${row[key] ?? ""}`).join("|");
|
|
2457
|
+
let hash = 5381;
|
|
2458
|
+
for (let index = 0; index < source.length; index += 1) hash = (hash * 33 ^ source.charCodeAt(index)) >>> 0;
|
|
2459
|
+
return hash.toString(16);
|
|
2460
|
+
}
|
|
2461
|
+
function dedupebykeys(rows, keys) {
|
|
2462
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2463
|
+
const kept = [];
|
|
2464
|
+
for (const row of rows) {
|
|
2465
|
+
const hash = rowhash(row, keys);
|
|
2466
|
+
if (seen.has(hash)) continue;
|
|
2467
|
+
seen.add(hash);
|
|
2468
|
+
kept.push(row);
|
|
2469
|
+
}
|
|
2470
|
+
return { kept, removed: rows.length - kept.length };
|
|
2471
|
+
}
|
|
2472
|
+
function applyexpression(value, expression) {
|
|
2473
|
+
const split = expression.indexOf(":");
|
|
2474
|
+
const op = split === -1 ? expression : expression.slice(0, split);
|
|
2475
|
+
const argument = split === -1 ? void 0 : expression.slice(split + 1);
|
|
2476
|
+
if (op === "trim") return value.trim();
|
|
2477
|
+
if (op === "upper") return value.toUpperCase();
|
|
2478
|
+
if (op === "lower") return value.toLowerCase();
|
|
2479
|
+
if (op === "number") return value.replace(/[^\d.\-]/g, "");
|
|
2480
|
+
if (op === "prefix") return `${argument ?? ""}${value}`;
|
|
2481
|
+
if (op === "suffix") return `${value}${argument ?? ""}`;
|
|
2482
|
+
if (op === "replace") {
|
|
2483
|
+
const separator = argument?.indexOf("=>") ?? -1;
|
|
2484
|
+
if (separator === -1 || separator === 0) throw new Error(`The reviewed transform expression ${expression} needs the from=>to separator.`);
|
|
2485
|
+
const from = argument.slice(0, separator);
|
|
2486
|
+
const to = argument.slice(separator + 2);
|
|
2487
|
+
return value.split(from).join(to);
|
|
2488
|
+
}
|
|
2489
|
+
throw new Error(`The reviewed transform expression ${op} is not supported.`);
|
|
2490
|
+
}
|
|
2491
|
+
function transformrows(rows, rules) {
|
|
2492
|
+
const errors = [];
|
|
2493
|
+
const output = rows.map((row) => ({ ...row }));
|
|
2494
|
+
for (const rule of rules) {
|
|
2495
|
+
const updated = [];
|
|
2496
|
+
try {
|
|
2497
|
+
for (const row of output) updated.push({ ...row, [rule.target]: applyexpression(rule.sources.map((source) => row[source] ?? "").join(" "), rule.expression) });
|
|
2498
|
+
} catch (error) {
|
|
2499
|
+
errors.push(`${rule.target}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2500
|
+
continue;
|
|
2501
|
+
}
|
|
2502
|
+
output.splice(0, output.length, ...updated);
|
|
2503
|
+
}
|
|
2504
|
+
return { rows: output, errors };
|
|
2505
|
+
}
|
|
2506
|
+
function mergedatasets(datasets) {
|
|
2507
|
+
const columns = [];
|
|
2508
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2509
|
+
for (const dataset of datasets) {
|
|
2510
|
+
for (const column of dataset.columns) {
|
|
2511
|
+
if (seen.has(column.key)) continue;
|
|
2512
|
+
seen.add(column.key);
|
|
2513
|
+
columns.push(column);
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
const rows = datasets.flatMap((dataset) => dataset.rows.map((row) => {
|
|
2517
|
+
const merged = {};
|
|
2518
|
+
for (const column of columns) merged[column.key] = row[column.key] ?? "";
|
|
2519
|
+
return merged;
|
|
2520
|
+
}));
|
|
2521
|
+
return { columns, rows };
|
|
2522
|
+
}
|
|
2523
|
+
function samplerows(rows, url, stepid, at) {
|
|
2524
|
+
const stamped = rows.map((row) => ({ ...row, source: url, capturedat: String(at), step: stepid }));
|
|
2525
|
+
const sources = stamped.map((row, index) => ({ row: index, url, at, stepid }));
|
|
2526
|
+
return { rows: stamped, sources };
|
|
2527
|
+
}
|
|
2528
|
+
function csvfield(value, delimiter) {
|
|
2529
|
+
return value.includes(delimiter) || value.includes('"') || value.includes("\n") ? `"${value.replace(/"/g, '""')}"` : value;
|
|
2530
|
+
}
|
|
2531
|
+
function tocsv(columns, rows, delimiter = ",") {
|
|
2532
|
+
const lines = [columns.map((column) => csvfield(column.label || column.key, delimiter)).join(delimiter)];
|
|
2533
|
+
for (const row of rows) lines.push(columns.map((column) => csvfield(row[column.key] ?? "", delimiter)).join(delimiter));
|
|
2534
|
+
return lines.join("\n");
|
|
2535
|
+
}
|
|
2536
|
+
function parsecsv(text2, delimiter = ",") {
|
|
2537
|
+
const records = [];
|
|
2538
|
+
let field = "";
|
|
2539
|
+
let record2 = [];
|
|
2540
|
+
let quoted = false;
|
|
2541
|
+
for (let index = 0; index < text2.length; index += 1) {
|
|
2542
|
+
const character = text2[index];
|
|
2543
|
+
if (quoted) {
|
|
2544
|
+
if (character === '"') {
|
|
2545
|
+
if (text2[index + 1] === '"') {
|
|
2546
|
+
field += '"';
|
|
2547
|
+
index += 1;
|
|
2548
|
+
} else quoted = false;
|
|
2549
|
+
} else field += character;
|
|
2550
|
+
continue;
|
|
2551
|
+
}
|
|
2552
|
+
if (character === '"') {
|
|
2553
|
+
quoted = true;
|
|
2554
|
+
continue;
|
|
2555
|
+
}
|
|
2556
|
+
if (character === delimiter) {
|
|
2557
|
+
record2.push(field);
|
|
2558
|
+
field = "";
|
|
2559
|
+
continue;
|
|
2560
|
+
}
|
|
2561
|
+
if (character === "\n" || character === "\r") {
|
|
2562
|
+
if (character === "\r" && text2[index + 1] === "\n") index += 1;
|
|
2563
|
+
record2.push(field);
|
|
2564
|
+
field = "";
|
|
2565
|
+
if (record2.some((value) => value.length > 0) || record2.length > 1) records.push(record2);
|
|
2566
|
+
record2 = [];
|
|
2567
|
+
continue;
|
|
2568
|
+
}
|
|
2569
|
+
field += character;
|
|
2570
|
+
}
|
|
2571
|
+
record2.push(field);
|
|
2572
|
+
if (record2.some((value) => value.length > 0) || record2.length > 1) records.push(record2);
|
|
2573
|
+
const [headers = [], ...rows] = records;
|
|
2574
|
+
return { headers, rows };
|
|
2575
|
+
}
|
|
2576
|
+
function mapcolumns(headers, mapping = {}) {
|
|
2577
|
+
const used = /* @__PURE__ */ new Set();
|
|
2578
|
+
return headers.map((header) => {
|
|
2579
|
+
const target = mapping[header] ?? mapping[normalizeheader(header)] ?? header;
|
|
2580
|
+
return columnspecof(target, used);
|
|
2581
|
+
});
|
|
2582
|
+
}
|
|
2583
|
+
function tojson(columns, rows) {
|
|
2584
|
+
return JSON.stringify({ columns, rows });
|
|
2585
|
+
}
|
|
2586
|
+
function xmltext(value) {
|
|
2587
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2588
|
+
}
|
|
2589
|
+
function toexcel(columns, rows, name) {
|
|
2590
|
+
const head = columns.map((column) => `<Cell ss:StyleID="head"><Data ss:Type="String">${xmltext(column.label || column.key)}</Data></Cell>`).join("");
|
|
2591
|
+
const body = rows.map((row) => `<Row>${columns.map((column) => {
|
|
2592
|
+
const value = row[column.key] ?? "";
|
|
2593
|
+
const numeric = column.kind === "number" && value.trim() !== "" && Number.isFinite(Number(value));
|
|
2594
|
+
return numeric ? `<Cell><Data ss:Type="Number">${xmltext(value)}</Data></Cell>` : `<Cell><Data ss:Type="String">${xmltext(value)}</Data></Cell>`;
|
|
2595
|
+
}).join("")}</Row>`).join("");
|
|
2596
|
+
return `<?xml version="1.0"?><?mso-application progid="Excel.Sheet"?><Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"><Styles><Style ss:ID="head"><Font ss:Bold="1"/></Style></Styles><Worksheet ss:Name="${xmltext(name || "dataset").slice(0, 31)}"><Table><Row>${head}</Row>${body}</Table></Worksheet></Workbook>`;
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
// extension/datacommand.ts
|
|
2600
|
+
function builddataset(id, name, grid, at) {
|
|
2601
|
+
return { id, name: name || id, columns: grid.columns, rows: grid.rows, sources: [], at };
|
|
2602
|
+
}
|
|
2603
|
+
function checksum(value) {
|
|
2604
|
+
let hash = 5381;
|
|
2605
|
+
for (let index = 0; index < value.length; index += 1) hash = (hash * 33 ^ value.charCodeAt(index)) >>> 0;
|
|
2606
|
+
return `fnv1a-${hash.toString(16)}`;
|
|
2607
|
+
}
|
|
2608
|
+
function exportcontent(datasetvalue, format, delimiter = ",") {
|
|
2609
|
+
if (format === "json") return tojson(datasetvalue.columns, datasetvalue.rows);
|
|
2610
|
+
if (format === "excel") return toexcel(datasetvalue.columns, datasetvalue.rows, datasetvalue.name);
|
|
2611
|
+
return tocsv(datasetvalue.columns, datasetvalue.rows, delimiter);
|
|
2612
|
+
}
|
|
2613
|
+
function exportartifact(id, datasetvalue, format, stepid, content, at) {
|
|
2614
|
+
const extension = format === "excel" ? "xml" : format;
|
|
2615
|
+
return { id, kind: format, name: `${datasetvalue.name || datasetvalue.id}.${extension}`, stepid, rowcount: datasetvalue.rows.length, content, checksum: checksum(content), at };
|
|
2616
|
+
}
|
|
2617
|
+
function artifactrecordof(artifact) {
|
|
2618
|
+
return { id: artifact.id, kind: artifact.kind, name: artifact.name, stepid: artifact.stepid, at: artifact.at };
|
|
2619
|
+
}
|
|
2620
|
+
function chunkplan(rows, chunk) {
|
|
2621
|
+
const size = Math.max(1, Math.floor(chunk));
|
|
2622
|
+
const chunks = [];
|
|
2623
|
+
for (let from = 0; from < rows || chunks.length === 0; from += size) {
|
|
2624
|
+
const to = Math.min(rows, from + size);
|
|
2625
|
+
chunks.push({ index: chunks.length, from, to });
|
|
2626
|
+
if (to >= rows) break;
|
|
2627
|
+
}
|
|
2628
|
+
return chunks;
|
|
2629
|
+
}
|
|
2630
|
+
function backpressure(written, acknowledged) {
|
|
2631
|
+
return written - acknowledged >= 1;
|
|
2632
|
+
}
|
|
2633
|
+
function advancestream(state, chunk, at, done) {
|
|
2634
|
+
return { datasetid: state.datasetid, name: state.name, chunk: chunk.index + 1, chunks: state.chunks, written: chunk.to, ...done ? { done: true } : {}, at };
|
|
2635
|
+
}
|
|
2636
|
+
function newstream(datasetvalue, chunks, at) {
|
|
2637
|
+
return { datasetid: datasetvalue.id, name: datasetvalue.name, chunk: 0, chunks, written: 0, at };
|
|
2638
|
+
}
|
|
2639
|
+
function advancecursor(sessionvalue, page, rows, at, done) {
|
|
2640
|
+
return {
|
|
2641
|
+
id: sessionvalue.id,
|
|
2642
|
+
datasetid: sessionvalue.datasetid,
|
|
2643
|
+
name: sessionvalue.name,
|
|
2644
|
+
target: sessionvalue.target,
|
|
2645
|
+
next: sessionvalue.next,
|
|
2646
|
+
planned: sessionvalue.planned,
|
|
2647
|
+
pages: [...sessionvalue.pages, page],
|
|
2648
|
+
rows: sessionvalue.rows + rows,
|
|
2649
|
+
cursor: sessionvalue.cursor + 1,
|
|
2650
|
+
...done || sessionvalue.cursor + 1 >= sessionvalue.planned ? { done: true } : {},
|
|
2651
|
+
startedat: sessionvalue.startedat,
|
|
2652
|
+
updatedat: at
|
|
2653
|
+
};
|
|
2654
|
+
}
|
|
2655
|
+
function newextractsession(id, datasetid, name, target, next, planned, at) {
|
|
2656
|
+
return { id, datasetid, name, target, next, planned, pages: [], rows: 0, cursor: 0, startedat: at, updatedat: at };
|
|
2657
|
+
}
|
|
2658
|
+
function remainingpages(sessionvalue, planned) {
|
|
2659
|
+
if (sessionvalue.done) return 0;
|
|
2660
|
+
return Math.max(0, Math.max(sessionvalue.planned, planned) - sessionvalue.cursor);
|
|
2661
|
+
}
|
|
2662
|
+
function provenancefor(artifact, url, stepid, at) {
|
|
2663
|
+
return { artifact: artifact.id, name: artifact.name, url, stepid, rowstart: artifact.rowcount > 0 ? 1 : 0, rowend: artifact.rowcount, checksum: artifact.checksum, at };
|
|
2664
|
+
}
|
|
2665
|
+
function interpolate(text2, row) {
|
|
2666
|
+
return text2.replace(/\{\{([^}]+)\}\}/g, (_, key) => row[key.trim()] ?? "");
|
|
2667
|
+
}
|
|
2668
|
+
function loopstep(step, row) {
|
|
2669
|
+
return {
|
|
2670
|
+
...step,
|
|
2671
|
+
...step.target !== void 0 ? { target: interpolate(step.target, row) } : {},
|
|
2672
|
+
...step.value !== void 0 ? { value: interpolate(step.value, row) } : {},
|
|
2673
|
+
...step.options !== void 0 ? { options: interpolate(step.options, row) } : {}
|
|
2674
|
+
};
|
|
2675
|
+
}
|
|
2676
|
+
function loopvariables(row) {
|
|
2677
|
+
return { ...row };
|
|
2678
|
+
}
|
|
2679
|
+
function gridpreview(datasetvalue, sample) {
|
|
2680
|
+
return { datasetid: datasetvalue.id, columns: datasetvalue.columns.map((column) => column.key), rows: datasetvalue.rows.length, sample: datasetvalue.rows.slice(0, Math.max(0, Math.floor(sample))) };
|
|
2681
|
+
}
|
|
2682
|
+
function sheetpayload(datasetvalue, sheet) {
|
|
2683
|
+
return { sheet, columns: datasetvalue.columns.map((column) => column.key), rows: datasetvalue.rows };
|
|
2684
|
+
}
|
|
2685
|
+
function mergetaskrules(existing, taskid, transforms, dedupekeys, at) {
|
|
2686
|
+
return {
|
|
2687
|
+
taskid,
|
|
2688
|
+
transforms: transforms.length > 0 ? transforms : existing?.transforms ?? [],
|
|
2689
|
+
dedupekeys: dedupekeys.length > 0 ? dedupekeys : existing?.dedupekeys ?? [],
|
|
2690
|
+
at
|
|
2691
|
+
};
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2216
2694
|
// extension/background.ts
|
|
2217
2695
|
var sessionduration = 15 * 60 * 1e3;
|
|
2218
2696
|
var freshcheckkinds = /* @__PURE__ */ new Set(["focus", "inspect", "click", "type", "scroll", "select", "hover"]);
|
|
@@ -2403,6 +2881,13 @@ function stepauditkind(step, ok) {
|
|
|
2403
2881
|
if (step.kind === "submitform" || step.kind === "asksubmit") return "submit";
|
|
2404
2882
|
if (step.kind === "consentpassword") return "consent";
|
|
2405
2883
|
if (step.kind === "handoffcaptcha") return "handoff";
|
|
2884
|
+
if (isdatasetkind(step.kind)) {
|
|
2885
|
+
if (step.kind === "exportcsv" || step.kind === "exportjson" || step.kind === "exportexcel" || step.kind === "copytable" || step.kind === "pushsheets") return "export";
|
|
2886
|
+
if (step.kind === "streamdisk") return "stream";
|
|
2887
|
+
if (step.kind === "resumeextract") return "resume";
|
|
2888
|
+
if (step.kind === "logprovenance") return "provenance";
|
|
2889
|
+
return "scrape";
|
|
2890
|
+
}
|
|
2406
2891
|
if (formfillkinds.has(step.kind)) return "fill";
|
|
2407
2892
|
if (pointerkinds.has(step.kind)) return "pointer";
|
|
2408
2893
|
if (watchstepkinds.has(step.kind)) return "watch";
|
|
@@ -3585,6 +4070,220 @@ async function executeformstep(step, session, plan, tabid2, origin) {
|
|
|
3585
4070
|
}
|
|
3586
4071
|
}
|
|
3587
4072
|
}
|
|
4073
|
+
async function loaddataset(datasetid) {
|
|
4074
|
+
const record2 = await memory.getdataset(datasetid);
|
|
4075
|
+
if (!record2) throw new Error(`No dataset ${datasetid} exists yet; scrape or import it first.`);
|
|
4076
|
+
return record2;
|
|
4077
|
+
}
|
|
4078
|
+
function readgridoutput(output) {
|
|
4079
|
+
const grid = output?.details?.grid;
|
|
4080
|
+
if (!grid || typeof grid !== "object") return null;
|
|
4081
|
+
return grid;
|
|
4082
|
+
}
|
|
4083
|
+
async function storeexport(stepid, datasetvalue, format, delimiter, session, planid, origin) {
|
|
4084
|
+
const content = exportcontent(datasetvalue, format, delimiter);
|
|
4085
|
+
const artifact = exportartifact(randomid(), datasetvalue, format, stepid, content, Date.now());
|
|
4086
|
+
await memory.addexport(artifact);
|
|
4087
|
+
await memory.addartifact(artifactrecordof(artifact));
|
|
4088
|
+
await memory.addprovenance(provenancefor(artifact, datasetvalue.sources[0]?.url ?? origin, stepid, Date.now()));
|
|
4089
|
+
await audit("export", `Exported ${artifact.rowcount} row${artifact.rowcount === 1 ? "" : "s"} of dataset ${datasetvalue.name} into the ${format} artifact ${artifact.name} with checksum ${artifact.checksum}; the provenance record keeps the source url, step ref and row range.`, { ...session ? { sessionid: session.id } : {}, planid, stepid });
|
|
4090
|
+
await refreshbadge();
|
|
4091
|
+
return artifact;
|
|
4092
|
+
}
|
|
4093
|
+
async function executedatastep(step, session, plan, tabid2, origin) {
|
|
4094
|
+
const options = stepoptions2(step);
|
|
4095
|
+
const extra = { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id };
|
|
4096
|
+
switch (step.kind) {
|
|
4097
|
+
case "scrapetable": {
|
|
4098
|
+
const output = await dispatchpagestep(step, tabid2, origin, plan);
|
|
4099
|
+
const grid = readgridoutput(output);
|
|
4100
|
+
if (!output?.ok || !grid) return output ?? { ok: false, summary: "The table scrape returned no result." };
|
|
4101
|
+
const id = randomid();
|
|
4102
|
+
const name = typeof options.name === "string" && options.name ? options.name : `dataset-${step.id}`;
|
|
4103
|
+
const datasetvalue = builddataset(id, name, grid, Date.now());
|
|
4104
|
+
await memory.setdataset(datasetvalue);
|
|
4105
|
+
const extract = advancecursor(newextractsession(randomid(), id, name, step.target ?? "", "", 1, Date.now()), origin, datasetvalue.rows.length, Date.now(), true);
|
|
4106
|
+
await memory.setextractsession(extract);
|
|
4107
|
+
await memory.setprogress(recordextraction(await memory.getprogress(), plan.id, step.id, { page: 1, rows: datasetvalue.rows.length, cursor: 1 }, Date.now()));
|
|
4108
|
+
await refreshbadge();
|
|
4109
|
+
return { ok: true, summary: output.summary, details: { ...output.details ?? {}, dataset: { id, name, rows: datasetvalue.rows.length, columns: grid.columns.length } } };
|
|
4110
|
+
}
|
|
4111
|
+
case "paginateextract": {
|
|
4112
|
+
const output = await dispatchpagestep(step, tabid2, origin, plan);
|
|
4113
|
+
const grid = readgridoutput(output);
|
|
4114
|
+
if (!output?.ok || !grid) return output ?? { ok: false, summary: "The paginated extraction returned no result." };
|
|
4115
|
+
const id = randomid();
|
|
4116
|
+
const name = typeof options.name === "string" && options.name ? options.name : `dataset-${step.id}`;
|
|
4117
|
+
const datasetvalue = builddataset(id, name, grid, Date.now());
|
|
4118
|
+
await memory.setdataset(datasetvalue);
|
|
4119
|
+
const planned = typeof options.pages === "number" && Number.isInteger(options.pages) && options.pages > 0 ? options.pages : Number(output.details?.pages ?? 1);
|
|
4120
|
+
const next = output.details?.next === true;
|
|
4121
|
+
const extract = advancecursor(newextractsession(randomid(), id, name, step.target ?? "", typeof options.next === "string" ? options.next : "", planned, Date.now()), origin, datasetvalue.rows.length, Date.now(), !next);
|
|
4122
|
+
await memory.setextractsession(extract);
|
|
4123
|
+
await memory.setprogress(recordextraction(await memory.getprogress(), plan.id, step.id, { page: extract.cursor, rows: datasetvalue.rows.length, cursor: extract.cursor }, Date.now()));
|
|
4124
|
+
await refreshbadge();
|
|
4125
|
+
const extractedpages = Number(output.details?.pages ?? 1);
|
|
4126
|
+
const estimated = Math.max(1, Math.round(datasetvalue.rows.length / Math.max(1, extractedpages) * planned));
|
|
4127
|
+
return { ok: true, summary: output.summary, details: { ...output.details ?? {}, dataset: { id, name, rows: datasetvalue.rows.length, columns: grid.columns.length }, extractsession: { id: extract.id, cursor: extract.cursor, planned, done: extract.done === true }, progressshare: extractionshare(datasetvalue.rows.length, estimated) } };
|
|
4128
|
+
}
|
|
4129
|
+
case "resumeextract": {
|
|
4130
|
+
const sessionid = typeof options.session === "string" ? options.session : "";
|
|
4131
|
+
const extract = (await memory.getextractsessions()).find((item) => item.id === sessionid);
|
|
4132
|
+
if (!extract) throw new Error(`No extract session ${sessionid} is stored yet.`);
|
|
4133
|
+
if (extract.done) return { ok: true, summary: `Extraction ${extract.name} already completed at cursor ${extract.cursor}.`, details: { cursor: extract.cursor, done: true } };
|
|
4134
|
+
const datasetvalue = await loaddataset(extract.datasetid);
|
|
4135
|
+
const remaining = remainingpages(extract, extract.planned);
|
|
4136
|
+
const derived = { id: step.id, kind: "paginateextract", target: extract.target, summary: step.summary, risk: "sensitive", options: JSON.stringify({ next: extract.next, pages: remaining, cursor: extract.cursor, name: extract.name }) };
|
|
4137
|
+
const output = await dispatchpagestep(derived, tabid2, origin, plan);
|
|
4138
|
+
const grid = readgridoutput(output);
|
|
4139
|
+
if (!output?.ok || !grid) return output ?? { ok: false, summary: "The resumed extraction returned no result." };
|
|
4140
|
+
const merged = mergedatasets([{ columns: datasetvalue.columns, rows: datasetvalue.rows }, { columns: grid.columns, rows: grid.rows }]);
|
|
4141
|
+
const updated = { ...datasetvalue, columns: merged.columns, rows: merged.rows, at: Date.now() };
|
|
4142
|
+
await memory.setdataset(updated);
|
|
4143
|
+
const next = output.details?.next === true;
|
|
4144
|
+
const resumed = advancecursor({ ...extract, updatedat: Date.now() }, origin, grid.rows.length, Date.now(), !next);
|
|
4145
|
+
await memory.setextractsession(resumed);
|
|
4146
|
+
await memory.setprogress(recordextraction(await memory.getprogress(), plan.id, step.id, { page: resumed.cursor, rows: grid.rows.length, cursor: resumed.cursor }, Date.now()));
|
|
4147
|
+
await audit("resume", `Extraction ${extract.name} resumed from cursor ${extract.cursor} and collected ${grid.rows.length} further row${grid.rows.length === 1 ? "" : "s"} across ${Number(output.details?.pages ?? 1)} page${Number(output.details?.pages ?? 1) === 1 ? "" : "s"}; the dataset now holds ${updated.rows.length} rows.`, extra);
|
|
4148
|
+
return { ok: true, summary: `Resumed extraction ${extract.name} from cursor ${extract.cursor}; the dataset now holds ${updated.rows.length} rows.`, details: { dataset: { id: updated.id, rows: updated.rows.length }, extractsession: { id: resumed.id, cursor: resumed.cursor, planned: resumed.planned, done: resumed.done === true } } };
|
|
4149
|
+
}
|
|
4150
|
+
case "mergepages": {
|
|
4151
|
+
const ids = Array.isArray(options.datasets) ? options.datasets.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
4152
|
+
const records = [];
|
|
4153
|
+
for (const id2 of ids) records.push(await loaddataset(id2));
|
|
4154
|
+
const merged = mergedatasets(records);
|
|
4155
|
+
const id = randomid();
|
|
4156
|
+
const datasetvalue = { id, name: typeof options.name === "string" && options.name ? options.name : `merged-${step.id}`, columns: merged.columns, rows: merged.rows, sources: records.flatMap((record2) => record2.sources), at: Date.now() };
|
|
4157
|
+
await memory.setdataset(datasetvalue);
|
|
4158
|
+
await refreshbadge();
|
|
4159
|
+
return { ok: true, summary: `Merged ${records.length} dataset${records.length === 1 ? "" : "s"} into ${datasetvalue.name} with ${merged.columns.length} aligned column${merged.columns.length === 1 ? "" : "s"} and ${merged.rows.length} row${merged.rows.length === 1 ? "" : "s"}.`, details: { dataset: { id, name: datasetvalue.name, rows: merged.rows.length, columns: merged.columns.length }, merged: records.map((record2) => record2.id) } };
|
|
4160
|
+
}
|
|
4161
|
+
case "transformvalues": {
|
|
4162
|
+
const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
|
|
4163
|
+
const rules = (Array.isArray(options.rules) ? options.rules : []).filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item));
|
|
4164
|
+
const applied = transformrows(datasetvalue.rows, rules);
|
|
4165
|
+
await memory.setdataset({ ...datasetvalue, rows: applied.rows, at: Date.now() });
|
|
4166
|
+
const existing = (await memory.gettaskrules()).find((item) => item.taskid === plan.id);
|
|
4167
|
+
await memory.settaskrules(mergetaskrules(existing, plan.id, rules, [], Date.now()));
|
|
4168
|
+
if (applied.errors.length > 0) return { ok: false, summary: `Transform rules surfaced ${applied.errors.length} error${applied.errors.length === 1 ? "" : "s"}: ${applied.errors.join("; ")}.`, details: { errors: applied.errors, rules: rules.length } };
|
|
4169
|
+
return { ok: true, summary: `Applied ${rules.length} reviewed transform rule${rules.length === 1 ? "" : "s"} to ${applied.rows.length} row${applied.rows.length === 1 ? "" : "s"}.`, details: { rules: rules.length, rows: applied.rows.length } };
|
|
4170
|
+
}
|
|
4171
|
+
case "deduperows": {
|
|
4172
|
+
const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
|
|
4173
|
+
const keys = Array.isArray(options.keys) ? options.keys.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
4174
|
+
const result = dedupebykeys(datasetvalue.rows, keys);
|
|
4175
|
+
await memory.setdataset({ ...datasetvalue, rows: result.kept, at: Date.now() });
|
|
4176
|
+
const existing = (await memory.gettaskrules()).find((item) => item.taskid === plan.id);
|
|
4177
|
+
await memory.settaskrules(mergetaskrules(existing, plan.id, [], keys, Date.now()));
|
|
4178
|
+
return { ok: true, summary: `Deduplicated ${datasetvalue.rows.length} row${datasetvalue.rows.length === 1 ? "" : "s"} by ${keys.join(", ")}: removed ${result.removed} duplicate${result.removed === 1 ? "" : "s"}, kept ${result.kept.length}.`, details: { dedupe: { removed: result.removed, kept: result.kept.length, keys } } };
|
|
4179
|
+
}
|
|
4180
|
+
case "stamplerows": {
|
|
4181
|
+
const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
|
|
4182
|
+
const url = typeof options.url === "string" && options.url ? options.url : origin;
|
|
4183
|
+
const stamped = samplerows(datasetvalue.rows, url, step.id, Date.now());
|
|
4184
|
+
await memory.setdataset({ ...datasetvalue, rows: stamped.rows, sources: stamped.sources, at: Date.now() });
|
|
4185
|
+
return { ok: true, summary: `Stamped ${stamped.rows.length} row${stamped.rows.length === 1 ? "" : "s"} with the source url, timestamp and step ref.`, details: { rows: stamped.rows.length, url } };
|
|
4186
|
+
}
|
|
4187
|
+
case "previewgrid": {
|
|
4188
|
+
const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
|
|
4189
|
+
const sample = typeof options.sample === "number" && Number.isInteger(options.sample) && options.sample > 0 ? options.sample : 10;
|
|
4190
|
+
const preview = gridpreview(datasetvalue, sample);
|
|
4191
|
+
return { ok: true, summary: `Previewed ${preview.rows} row${preview.rows === 1 ? "" : "s"} of dataset ${datasetvalue.name} in the grid with ${preview.columns.length} column${preview.columns.length === 1 ? "" : "s"}.`, details: { preview } };
|
|
4192
|
+
}
|
|
4193
|
+
case "importcsv": {
|
|
4194
|
+
const csv = typeof options.csv === "string" ? options.csv : "";
|
|
4195
|
+
const parsed = parsecsv(csv, typeof options.delimiter === "string" && options.delimiter.length === 1 ? options.delimiter : ",");
|
|
4196
|
+
const mapping = options.mapping && typeof options.mapping === "object" && !Array.isArray(options.mapping) ? options.mapping : {};
|
|
4197
|
+
const columns = mapcolumns(parsed.headers, mapping);
|
|
4198
|
+
const rows = parsed.rows.map((line) => {
|
|
4199
|
+
const row = {};
|
|
4200
|
+
columns.forEach((column, index) => {
|
|
4201
|
+
row[column.key] = line[index] ?? "";
|
|
4202
|
+
});
|
|
4203
|
+
return row;
|
|
4204
|
+
});
|
|
4205
|
+
const id = randomid();
|
|
4206
|
+
const datasetvalue = { id, name: typeof options.name === "string" && options.name ? options.name : `import-${step.id}`, columns, rows, sources: [], at: Date.now() };
|
|
4207
|
+
await memory.addimport(datasetvalue);
|
|
4208
|
+
await refreshbadge();
|
|
4209
|
+
return { ok: true, summary: `Imported ${rows.length} row${rows.length === 1 ? "" : "s"} and ${columns.length} mapped column${columns.length === 1 ? "" : "s"} from the reviewed csv for fill loops.`, details: { dataset: { id, name: datasetvalue.name, rows: rows.length, columns: columns.length } } };
|
|
4210
|
+
}
|
|
4211
|
+
case "looprows": {
|
|
4212
|
+
const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
|
|
4213
|
+
const inner = resolvedinnerstep(step, plan);
|
|
4214
|
+
if (!inner) throw new Error("A reviewed inner step or step id is required in options.");
|
|
4215
|
+
const variable = typeof options.variable === "string" && options.variable ? options.variable : "row";
|
|
4216
|
+
let completed = 0;
|
|
4217
|
+
let failed = 0;
|
|
4218
|
+
for (const [index, row] of datasetvalue.rows.entries()) {
|
|
4219
|
+
const derived = loopstep(inner, row);
|
|
4220
|
+
const output = await dispatchpagestep(derived, tabid2, origin, plan);
|
|
4221
|
+
if (output?.ok) completed += 1;
|
|
4222
|
+
else failed += 1;
|
|
4223
|
+
await memory.setprogress(recordoutcome(await memory.getprogress(), plan.id, { stepid: step.id, ok: Boolean(output?.ok), summary: `Iteration ${index + 1} of ${datasetvalue.rows.length}: ${output?.summary ?? "no result"}`, details: { iteration: index + 1, variable, variables: loopvariables(row) }, at: Date.now() }, Date.now()));
|
|
4224
|
+
}
|
|
4225
|
+
return { ok: failed === 0, summary: `Looped ${datasetvalue.rows.length} dataset row${datasetvalue.rows.length === 1 ? "" : "s"} as ${variable} variables: ${completed} iteration${completed === 1 ? "" : "s"} completed${failed > 0 ? `, ${failed} failed` : ""}.`, details: { iterations: datasetvalue.rows.length, completed, failed, variable } };
|
|
4226
|
+
}
|
|
4227
|
+
case "exportcsv":
|
|
4228
|
+
case "exportjson":
|
|
4229
|
+
case "exportexcel": {
|
|
4230
|
+
const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
|
|
4231
|
+
const format = step.kind === "exportcsv" ? "csv" : step.kind === "exportjson" ? "json" : "excel";
|
|
4232
|
+
const delimiter = typeof options.delimiter === "string" && options.delimiter.length === 1 ? options.delimiter : ",";
|
|
4233
|
+
const artifact = await storeexport(step.id, { ...datasetvalue, ...typeof options.name === "string" && options.name ? { name: options.name } : {} }, format, delimiter, session, plan.id, origin);
|
|
4234
|
+
return { ok: true, summary: `Exported ${artifact.rowcount} row${artifact.rowcount === 1 ? "" : "s"} of dataset ${datasetvalue.name} to ${artifact.name} with checksum ${artifact.checksum}.`, details: { artifact: { id: artifact.id, name: artifact.name, kind: artifact.kind, checksum: artifact.checksum, rowcount: artifact.rowcount } } };
|
|
4235
|
+
}
|
|
4236
|
+
case "copytable": {
|
|
4237
|
+
const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
|
|
4238
|
+
const content = exportcontent(datasetvalue, "csv", typeof options.delimiter === "string" && options.delimiter.length === 1 ? options.delimiter : ",");
|
|
4239
|
+
await navigator.clipboard.writeText(content);
|
|
4240
|
+
await audit("export", `Copied ${datasetvalue.rows.length} row${datasetvalue.rows.length === 1 ? "" : "s"} of dataset ${datasetvalue.name} to the clipboard under the clipboardWrite capability.`, extra);
|
|
4241
|
+
return { ok: true, summary: `Copied ${datasetvalue.rows.length} row${datasetvalue.rows.length === 1 ? "" : "s"} to the clipboard.`, details: { rows: datasetvalue.rows.length } };
|
|
4242
|
+
}
|
|
4243
|
+
case "pushsheets": {
|
|
4244
|
+
const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
|
|
4245
|
+
const sheet = typeof options.sheet === "string" ? options.sheet : "";
|
|
4246
|
+
const config = (await memory.getsheetendpoints()).find((item) => item.endpoint === sheet || item.origin === new URL(sheet).origin);
|
|
4247
|
+
if (!config) throw new Error(`The reviewed sheet endpoint has not been configured; configure ${sheet} from the review panel first.`);
|
|
4248
|
+
const granted = await chrome.permissions.contains({ origins: [hostpattern(config.origin)] });
|
|
4249
|
+
if (!granted) throw new Error(`The sheet endpoint origin ${config.origin} has not received optional permission.`);
|
|
4250
|
+
const payload = sheetpayload(datasetvalue, sheet);
|
|
4251
|
+
const response = await fetch(config.endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload) });
|
|
4252
|
+
await audit("export", `Pushed ${datasetvalue.rows.length} row${datasetvalue.rows.length === 1 ? "" : "s"} of dataset ${datasetvalue.name} to the reviewed sheet endpoint ${config.origin} with response ${response.status}.`, extra);
|
|
4253
|
+
return { ok: response.ok, summary: response.ok ? `Pushed ${datasetvalue.rows.length} row${datasetvalue.rows.length === 1 ? "" : "s"} to the reviewed sheet endpoint ${config.origin}.` : `The reviewed sheet endpoint answered ${response.status}; the push failed.`, details: { endpoint: config.endpoint, status: response.status, rows: datasetvalue.rows.length } };
|
|
4254
|
+
}
|
|
4255
|
+
case "streamdisk": {
|
|
4256
|
+
const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
|
|
4257
|
+
const chunk = typeof options.chunk === "number" && Number.isInteger(options.chunk) && options.chunk > 0 ? options.chunk : datasetvalue.rows.length || 1;
|
|
4258
|
+
const planchunks = chunkplan(datasetvalue.rows.length, chunk);
|
|
4259
|
+
const previous = (await memory.getstreams()).find((item) => item.datasetid === datasetvalue.id && item.done !== true);
|
|
4260
|
+
let state = previous && previous.chunks === planchunks.length ? previous : newstream(datasetvalue, planchunks.length, Date.now());
|
|
4261
|
+
let written = 0;
|
|
4262
|
+
let acknowledged = 0;
|
|
4263
|
+
for (const part of planchunks.slice(state.chunk)) {
|
|
4264
|
+
written += 1;
|
|
4265
|
+
if (backpressure(written, acknowledged)) await new Promise((resolve) => setTimeout(resolve, 0));
|
|
4266
|
+
state = advancestream(state, part, Date.now(), part.to >= datasetvalue.rows.length);
|
|
4267
|
+
await memory.setstream(state);
|
|
4268
|
+
acknowledged += 1;
|
|
4269
|
+
}
|
|
4270
|
+
const artifact = await storeexport(step.id, datasetvalue, "csv", ",", session, plan.id, origin);
|
|
4271
|
+
await audit("stream", `Streamed ${datasetvalue.rows.length} row${datasetvalue.rows.length === 1 ? "" : "s"} of dataset ${datasetvalue.name} to disk in ${planchunks.length} reviewed chunk${planchunks.length === 1 ? "" : "s"} of ${chunk} row${chunk === 1 ? "" : "s"} with backpressure; the stream state stays persisted for resume.`, extra);
|
|
4272
|
+
return { ok: true, summary: `Streamed ${datasetvalue.rows.length} row${datasetvalue.rows.length === 1 ? "" : "s"} to disk in ${planchunks.length} chunk${planchunks.length === 1 ? "" : "s"} and landed the artifact ${artifact.name}.`, details: { chunks: planchunks.length, chunk, rows: datasetvalue.rows.length, artifact: { id: artifact.id, name: artifact.name, checksum: artifact.checksum } } };
|
|
4273
|
+
}
|
|
4274
|
+
case "logprovenance": {
|
|
4275
|
+
const reference = typeof options.artifact === "string" ? options.artifact : "";
|
|
4276
|
+
const artifact = (await memory.getexports()).find((item) => item.id === reference || item.name === reference);
|
|
4277
|
+
if (!artifact) throw new Error(`No exported artifact ${reference} exists yet.`);
|
|
4278
|
+
const record2 = provenancefor(artifact, origin, step.id, Date.now());
|
|
4279
|
+
await memory.addprovenance(record2);
|
|
4280
|
+
await audit("provenance", `Provenance of artifact ${artifact.name}: rows ${record2.rowstart} to ${record2.rowend}, checksum ${record2.checksum}, source ${record2.url}.`, extra);
|
|
4281
|
+
return { ok: true, summary: `Logged the provenance of ${artifact.name} for audit: rows ${record2.rowstart} to ${record2.rowend} with checksum ${record2.checksum}.`, details: { provenance: record2 } };
|
|
4282
|
+
}
|
|
4283
|
+
default:
|
|
4284
|
+
return { ok: false, summary: "Unsupported forms and data step." };
|
|
4285
|
+
}
|
|
4286
|
+
}
|
|
3588
4287
|
async function enforcewindowreview(step, session, plan) {
|
|
3589
4288
|
const windowid = step.value && /^\d+$/.test(step.value) ? Number.parseInt(step.value, 10) : 0;
|
|
3590
4289
|
const progress = plan ? await memory.getprogress() : void 0;
|
|
@@ -3620,8 +4319,9 @@ async function refreshbadge() {
|
|
|
3620
4319
|
const queues = await memory.getnavqueues();
|
|
3621
4320
|
const badges = await memory.getbadges();
|
|
3622
4321
|
const prompts = (await memory.gettickets()).filter((ticket) => ticket.approved === void 0).length;
|
|
4322
|
+
const datasets = (await memory.getdatasets()).length;
|
|
3623
4323
|
const tasktabs2 = new Set(badges.map((badge) => badge.tabid)).size;
|
|
3624
|
-
const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts;
|
|
4324
|
+
const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + datasets;
|
|
3625
4325
|
await chrome.action.setBadgeText({ text: total > 0 ? String(total) : "" }).catch(() => {
|
|
3626
4326
|
});
|
|
3627
4327
|
}
|
|
@@ -3645,6 +4345,8 @@ async function executestep(stepid) {
|
|
|
3645
4345
|
}
|
|
3646
4346
|
if (istabscommandkind(step.kind)) {
|
|
3647
4347
|
output = await executetabscommand(step, session, plan, tab.id);
|
|
4348
|
+
} else if (isdatasetkind(step.kind)) {
|
|
4349
|
+
output = await executedatastep(step, session, plan, tab.id, origin);
|
|
3648
4350
|
} else if (isformkind(step.kind)) {
|
|
3649
4351
|
output = await executeformstep(step, session, plan, tab.id, origin);
|
|
3650
4352
|
} else if (isbrowserkind(step.kind)) {
|
|
@@ -3752,6 +4454,12 @@ async function grantcapability(permission) {
|
|
|
3752
4454
|
await audit("capability", `Capability ${permission} granted by the user.`);
|
|
3753
4455
|
return refreshcapabilities();
|
|
3754
4456
|
}
|
|
4457
|
+
async function extractionreportValue() {
|
|
4458
|
+
return extractionreport({ sessions: await memory.getextractsessions() });
|
|
4459
|
+
}
|
|
4460
|
+
async function provenancereportValue() {
|
|
4461
|
+
return provenancereport({ records: await memory.getprovenances() });
|
|
4462
|
+
}
|
|
3755
4463
|
async function handlerequest(message, sender) {
|
|
3756
4464
|
if (!extensionpage(sender)) throw new Error("Requests are accepted only from Devthink extension pages.");
|
|
3757
4465
|
const input = message;
|
|
@@ -3804,13 +4512,25 @@ async function handlerequest(message, sender) {
|
|
|
3804
4512
|
const captchas = await memory.getcaptchas();
|
|
3805
4513
|
const detections = await memory.getdetections();
|
|
3806
4514
|
const codeentry = await memory.getcodevalue();
|
|
4515
|
+
const datasets = await memory.getdatasets();
|
|
4516
|
+
const imports = await memory.getimports();
|
|
4517
|
+
const extractsessions = await memory.getextractsessions();
|
|
4518
|
+
const streams = await memory.getstreams();
|
|
4519
|
+
const exports = (await memory.getexports()).map((artifact) => ({ id: artifact.id, kind: artifact.kind, name: artifact.name, stepid: artifact.stepid, rowcount: artifact.rowcount, checksum: artifact.checksum, at: artifact.at }));
|
|
4520
|
+
const provenances = await memory.getprovenances();
|
|
4521
|
+
const taskrules = await memory.gettaskrules();
|
|
4522
|
+
const sheetendpoints = await memory.getsheetendpoints();
|
|
4523
|
+
const sheetgrants = [];
|
|
4524
|
+
for (const config of sheetendpoints) {
|
|
4525
|
+
sheetgrants.push({ ...config, granted: await chrome.permissions.contains({ origins: [hostpattern(config.origin)] }).catch(() => false) });
|
|
4526
|
+
}
|
|
3807
4527
|
const clones = clonetabs(tabs);
|
|
3808
4528
|
const taskgauge = tasktabgauge(tabs.filter((tab) => badges.some((badge) => badge.tabid === tab.tabid)).length, tasktabceiling(await memory.getsettings()));
|
|
3809
4529
|
const report = await buildtabreport(tabs);
|
|
3810
4530
|
const livetab = session ? await chrome.tabs.get(session.tabid).catch(() => void 0) : void 0;
|
|
3811
4531
|
const waitprofile = session ? waitprofiles.find((record2) => record2.origin === session.origin) : void 0;
|
|
3812
4532
|
const livestate = { phase: livetab?.status === "loading" ? "loading" : "complete", ...navrecords[0] ? { finalurl: navrecords[0].finalurl, redirects: navrecords[0].chain } : {} };
|
|
3813
|
-
return { config: await memory.getconfig(), session, plan, progress: plan && progress?.planid === plan.id ? progress : void 0, diagnostic: await memory.getdiagnostic(), audit: await memory.getaudit(), capabilities: await refreshcapabilities(), outcomes: await memory.getoutcomes(), holds: heldkeysreport({ tabid: session?.tabid ?? 0, holds }), dialogs: await memory.getdialogs(), retries: await memory.getretries(), ...signals ? { signals: signalsreport({ signals }) } : { signals: signalsreport({}) }, banners: await memory.getbanners(), mutationevents: await memory.getmutationevents(), focusevents: await memory.getfocusevents(), diffs: await memory.getdiffs(), selectors: await memory.getselectors(), ...a11y ? { a11y } : {}, ...reader ? { reader } : {}, ...map ? { map } : {}, trail: trailreport({ ...session ? { sessionid: session.id } : {}, trail }), navrecords, ratestates, safeties, curated, waitprofiles, auths, navcontrol, navqueues, artifacts, navstate: livestate, ...waitprofile ? { waitprofile } : {}, offline: !navigator.onLine, tabs, windows, layouts: layoutreport({ layouts }), tabgroups, tabmetas, badges, snapshots, closedtabs, tabwatchevents, clones, tasktabgauge: taskgauge, ...controltab ? { controltab } : {}, tabreport: report, profiles, tickets, wizards: wizardreport({ ...session ? { sessionid: session.id } : {}, wizards, picks }), picks, errorreports, captchas, detections, ...codeentry !== void 0 ? { codeentry: true } : {} };
|
|
4533
|
+
return { config: await memory.getconfig(), session, plan, progress: plan && progress?.planid === plan.id ? progress : void 0, diagnostic: await memory.getdiagnostic(), audit: await memory.getaudit(), capabilities: await refreshcapabilities(), outcomes: await memory.getoutcomes(), holds: heldkeysreport({ tabid: session?.tabid ?? 0, holds }), dialogs: await memory.getdialogs(), retries: await memory.getretries(), ...signals ? { signals: signalsreport({ signals }) } : { signals: signalsreport({}) }, banners: await memory.getbanners(), mutationevents: await memory.getmutationevents(), focusevents: await memory.getfocusevents(), diffs: await memory.getdiffs(), selectors: await memory.getselectors(), ...a11y ? { a11y } : {}, ...reader ? { reader } : {}, ...map ? { map } : {}, trail: trailreport({ ...session ? { sessionid: session.id } : {}, trail }), navrecords, ratestates, safeties, curated, waitprofiles, auths, navcontrol, navqueues, artifacts, navstate: livestate, ...waitprofile ? { waitprofile } : {}, offline: !navigator.onLine, tabs, windows, layouts: layoutreport({ layouts }), tabgroups, tabmetas, badges, snapshots, closedtabs, tabwatchevents, clones, tasktabgauge: taskgauge, ...controltab ? { controltab } : {}, tabreport: report, profiles, tickets, wizards: wizardreport({ ...session ? { sessionid: session.id } : {}, wizards, picks }), picks, errorreports, captchas, detections, ...codeentry !== void 0 ? { codeentry: true } : {}, datasets, imports, extractsessions, streams, exports, provenances, taskrules, sheetendpoints: sheetgrants };
|
|
3814
4534
|
}
|
|
3815
4535
|
case "capabilities":
|
|
3816
4536
|
return refreshcapabilities();
|
|
@@ -4086,6 +4806,61 @@ async function handlerequest(message, sender) {
|
|
|
4086
4806
|
if (!report) throw new Error("No error report has been collected yet.");
|
|
4087
4807
|
return JSON.parse(errorreportresponse({ report, plan }));
|
|
4088
4808
|
}
|
|
4809
|
+
case "configuresheet": {
|
|
4810
|
+
const inputsheet = message;
|
|
4811
|
+
const config = normalizeendpoint(inputsheet.endpoint ?? "");
|
|
4812
|
+
const granted = await chrome.permissions.contains({ origins: [hostpattern(config.origin)] });
|
|
4813
|
+
if (!granted) throw new Error("The sheet endpoint origin has not received optional permission.");
|
|
4814
|
+
const record2 = { endpoint: config.endpoint, origin: config.origin, configuredat: Date.now() };
|
|
4815
|
+
await memory.setsheetendpoint(record2);
|
|
4816
|
+
await audit("configure", `Configured the reviewed sheet endpoint ${config.origin} for data pushes; pushes need the explicit reviewed flag.`);
|
|
4817
|
+
return record2;
|
|
4818
|
+
}
|
|
4819
|
+
case "exportdataset": {
|
|
4820
|
+
const session = await memory.getsession();
|
|
4821
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Data exports stay behind the consent gate of an active session.");
|
|
4822
|
+
const { tab, origin } = await activecontext();
|
|
4823
|
+
const gate = exportgranted(session, origin);
|
|
4824
|
+
if (!gate.allowed) throw new Error(gate.reason);
|
|
4825
|
+
const inputexport = message;
|
|
4826
|
+
const format = inputexport.format === "json" ? "json" : inputexport.format === "excel" ? "excel" : "csv";
|
|
4827
|
+
const datasetvalue = await loaddataset(inputexport.datasetid ?? "");
|
|
4828
|
+
const artifact = await storeexport("panel", { ...datasetvalue, ...inputexport.name?.trim() ? { name: inputexport.name.trim() } : {} }, format, ",", session, "", origin);
|
|
4829
|
+
void tab;
|
|
4830
|
+
return { id: artifact.id, kind: artifact.kind, name: artifact.name, rowcount: artifact.rowcount, checksum: artifact.checksum, at: artifact.at };
|
|
4831
|
+
}
|
|
4832
|
+
case "importcsv": {
|
|
4833
|
+
const session = await memory.getsession();
|
|
4834
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Csv imports stay behind the consent gate of an active session.");
|
|
4835
|
+
const inputimport = message;
|
|
4836
|
+
const parsed = parsecsv(inputimport.csv ?? "");
|
|
4837
|
+
if (parsed.headers.length === 0) throw new Error("The reviewed csv needs a header line.");
|
|
4838
|
+
const columns = mapcolumns(parsed.headers, inputimport.mapping ?? {});
|
|
4839
|
+
const rows = parsed.rows.map((line) => {
|
|
4840
|
+
const row = {};
|
|
4841
|
+
columns.forEach((column, index) => {
|
|
4842
|
+
row[column.key] = line[index] ?? "";
|
|
4843
|
+
});
|
|
4844
|
+
return row;
|
|
4845
|
+
});
|
|
4846
|
+
const datasetvalue = { id: randomid(), name: inputimport.name?.trim() || `import-${Date.now()}`, columns, rows, sources: [], at: Date.now() };
|
|
4847
|
+
await memory.addimport(datasetvalue);
|
|
4848
|
+
await audit("scrape", `The review panel imported ${rows.length} row${rows.length === 1 ? "" : "s"} from the reviewed csv as dataset ${datasetvalue.name} for fill loops.`, { sessionid: session.id });
|
|
4849
|
+
await refreshbadge();
|
|
4850
|
+
return { id: datasetvalue.id, name: datasetvalue.name, rows: rows.length, columns: columns.length };
|
|
4851
|
+
}
|
|
4852
|
+
case "dataset": {
|
|
4853
|
+
const plan = await memory.getplan();
|
|
4854
|
+
if (!plan) throw new Error("No plan is available for a dataset envelope.");
|
|
4855
|
+
const inputdataset = message;
|
|
4856
|
+
const datasetvalue = await memory.getdataset(inputdataset.datasetid ?? "");
|
|
4857
|
+
if (!datasetvalue) throw new Error("No dataset has been captured yet.");
|
|
4858
|
+
return JSON.parse(datasetresponse({ dataset: datasetvalue, plan, ...typeof inputdataset.sample === "number" ? { sample: inputdataset.sample } : {} }));
|
|
4859
|
+
}
|
|
4860
|
+
case "extraction":
|
|
4861
|
+
return extractionreportValue();
|
|
4862
|
+
case "provenance":
|
|
4863
|
+
return provenancereportValue();
|
|
4089
4864
|
case "stop": {
|
|
4090
4865
|
const session = await memory.getsession();
|
|
4091
4866
|
if (session) await memory.setsession({ ...session, stoppedat: Date.now() });
|