@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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnOEjO8Z0PDgQyfvawGcaO2j+o0GLCFTLNj7TkYC/Avo9l2NenMRq7gp90Nfd7E9MViv/OMcCKSYZ5unv12QPRtv31C+a5UQWDFAOP/cH5mwMd6hsayElrSoW8ta+FwFqmr9dIFkn7cQEU3YhZr4Gcbs+ycUHOxVgDA4NBKB0rQ6e9VW5LvTw0isRYUrqM+M72vKxHk9zUIYYn/LGPvottKBYi2GLr0PHSeC2UE+Shmq7vcFIXj6hDjvD4kLJ5sKoUllEcZ1TPuBcnHUQ9ndKA5iktXDQOIJCUJmi7a0YJ2PGg7fvpYfT9k0ai/qZ+pIoRfoOEwE01bPoDn7NjeYnNQIDAQAB",
|
|
4
4
|
"name": "Devthink",
|
|
5
|
-
"version": "1.1.
|
|
5
|
+
"version": "1.1.38",
|
|
6
6
|
"description": "A consent-first bridge for reviewed browser-agent tasks.",
|
|
7
7
|
"permissions": [
|
|
8
8
|
"activeTab",
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
(() => {
|
|
3
3
|
// policy.ts
|
|
4
|
-
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"]);
|
|
4
|
+
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"]);
|
|
5
5
|
var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover", "clickdeep", "rightclick", "doubleclick", "scrollpage", "scrollby", "scrollend", "scrolltop", "fullscreen", "zoomset", "movepointer", "clicktext", "clickaria", "clickname", "expanddetails", "pierceshadow", "retryaction"]);
|
|
6
|
-
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"]);
|
|
6
|
+
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"]);
|
|
7
7
|
var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
|
|
8
8
|
function parseoptions(step) {
|
|
9
9
|
if (step.options === void 0) return {};
|
|
@@ -2934,6 +2934,189 @@
|
|
|
2934
2934
|
}
|
|
2935
2935
|
}
|
|
2936
2936
|
|
|
2937
|
+
// extension/pagedata.ts
|
|
2938
|
+
function normalizeheader(label) {
|
|
2939
|
+
const slug = label.trim().toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
|
|
2940
|
+
return slug || "column";
|
|
2941
|
+
}
|
|
2942
|
+
function columnspecof(label, used = /* @__PURE__ */ new Set()) {
|
|
2943
|
+
const base = normalizeheader(label);
|
|
2944
|
+
let key = base;
|
|
2945
|
+
let suffix = 2;
|
|
2946
|
+
while (used.has(key)) {
|
|
2947
|
+
key = `${base}${suffix}`;
|
|
2948
|
+
suffix += 1;
|
|
2949
|
+
}
|
|
2950
|
+
used.add(key);
|
|
2951
|
+
return { key, label: label.trim(), kind: "text", normalized: label.trim().toLowerCase() };
|
|
2952
|
+
}
|
|
2953
|
+
function classifycolumn(values) {
|
|
2954
|
+
const present = values.filter((value) => value.trim().length > 0);
|
|
2955
|
+
if (present.length === 0) return "text";
|
|
2956
|
+
return present.every((value) => Number.isFinite(Number(value.replace(/,/g, ".")))) ? "number" : "text";
|
|
2957
|
+
}
|
|
2958
|
+
function expandspans(rows) {
|
|
2959
|
+
const filled = [];
|
|
2960
|
+
const pending = /* @__PURE__ */ new Map();
|
|
2961
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
2962
|
+
const row = filled[index] ?? (filled[index] = []);
|
|
2963
|
+
let column = 0;
|
|
2964
|
+
for (const cell of rows[index].cells) {
|
|
2965
|
+
while (row[column] !== void 0 || pending.has(`${index},${column}`)) {
|
|
2966
|
+
if (row[column] === void 0) row[column] = pending.get(`${index},${column}`);
|
|
2967
|
+
column += 1;
|
|
2968
|
+
}
|
|
2969
|
+
row[column] = cell.text;
|
|
2970
|
+
for (let spanrow = 0; spanrow < Math.max(1, cell.rowspan); spanrow += 1) {
|
|
2971
|
+
for (let spancol = 0; spancol < Math.max(1, cell.colspan); spancol += 1) {
|
|
2972
|
+
if (spanrow === 0 && spancol === 0) continue;
|
|
2973
|
+
pending.set(`${index + spanrow},${column + spancol}`, cell.text);
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
column += Math.max(1, cell.colspan);
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
for (const [key, value] of pending) {
|
|
2980
|
+
const [rowpart, columnpart] = key.split(",");
|
|
2981
|
+
const rowindex = Number.parseInt(rowpart ?? "0", 10);
|
|
2982
|
+
const columnindex = Number.parseInt(columnpart ?? "0", 10);
|
|
2983
|
+
const target = filled[rowindex] ?? (filled[rowindex] = []);
|
|
2984
|
+
if (target[columnindex] === void 0) target[columnindex] = value;
|
|
2985
|
+
}
|
|
2986
|
+
const width = filled.reduce((largest, row) => Math.max(largest, row.length), 0);
|
|
2987
|
+
return filled.map((row) => Array.from({ length: width }, (_, column) => row[column] ?? ""));
|
|
2988
|
+
}
|
|
2989
|
+
function readgrid(rows) {
|
|
2990
|
+
const headerindex = rows.findIndex((row) => row.header || (row.cells[0]?.header ?? false));
|
|
2991
|
+
const useheader = headerindex !== -1 ? headerindex : 0;
|
|
2992
|
+
const hasheader = headerindex !== -1 || rows.length > 0;
|
|
2993
|
+
const grid = expandspans(rows);
|
|
2994
|
+
const headercells = hasheader ? grid[useheader] ?? [] : [];
|
|
2995
|
+
const used = /* @__PURE__ */ new Set();
|
|
2996
|
+
const columns = Array.from({ length: headercells.length || (grid[0]?.length ?? 0) }, (_, index) => columnspecof(headercells[index] ?? `column${index + 1}`, used));
|
|
2997
|
+
const bodyrows = grid.filter((_, index) => hasheader ? index !== useheader : true).filter((line) => line.some((value) => value.trim().length > 0)).map((line) => {
|
|
2998
|
+
const row = {};
|
|
2999
|
+
columns.forEach((column, index) => {
|
|
3000
|
+
row[column.key] = line[index] ?? "";
|
|
3001
|
+
});
|
|
3002
|
+
return row;
|
|
3003
|
+
});
|
|
3004
|
+
for (const column of columns) column.kind = classifycolumn(bodyrows.map((row) => row[column.key] ?? ""));
|
|
3005
|
+
const children = [];
|
|
3006
|
+
rows.forEach((row, rowindex) => {
|
|
3007
|
+
if (hasheader && rowindex === useheader) return;
|
|
3008
|
+
const parentrow = hasheader && rowindex > useheader ? rowindex - 1 : rowindex;
|
|
3009
|
+
row.cells.forEach((cell) => {
|
|
3010
|
+
if (!cell.nested) return;
|
|
3011
|
+
const child = readgrid(cell.nested.rows);
|
|
3012
|
+
children.push({ parentrow, selector: cell.nested.selector, columns: child.columns, rows: child.rows });
|
|
3013
|
+
});
|
|
3014
|
+
});
|
|
3015
|
+
return { columns, rows: bodyrows, children };
|
|
3016
|
+
}
|
|
3017
|
+
function rowsfresh(previous, current) {
|
|
3018
|
+
if (current.length === 0) return false;
|
|
3019
|
+
const known = new Set(previous.map((row) => JSON.stringify(row)));
|
|
3020
|
+
return current.some((row) => !known.has(JSON.stringify(row)));
|
|
3021
|
+
}
|
|
3022
|
+
function mergedatasets(datasets) {
|
|
3023
|
+
const columns = [];
|
|
3024
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3025
|
+
for (const dataset of datasets) {
|
|
3026
|
+
for (const column of dataset.columns) {
|
|
3027
|
+
if (seen.has(column.key)) continue;
|
|
3028
|
+
seen.add(column.key);
|
|
3029
|
+
columns.push(column);
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
const rows = datasets.flatMap((dataset) => dataset.rows.map((row) => {
|
|
3033
|
+
const merged = {};
|
|
3034
|
+
for (const column of columns) merged[column.key] = row[column.key] ?? "";
|
|
3035
|
+
return merged;
|
|
3036
|
+
}));
|
|
3037
|
+
return { columns, rows };
|
|
3038
|
+
}
|
|
3039
|
+
function collectrowshapes(table) {
|
|
3040
|
+
return [...table.rows].map((row) => ({
|
|
3041
|
+
header: [...row.cells].every((cell) => cell.tagName === "TH") && row.cells.length > 0,
|
|
3042
|
+
cells: [...row.cells].map((cell) => {
|
|
3043
|
+
const nested = cell.querySelector("table");
|
|
3044
|
+
return {
|
|
3045
|
+
text: clean(nested ? `${nested.rows.length} rows` : cell.textContent ?? ""),
|
|
3046
|
+
header: cell.tagName === "TH",
|
|
3047
|
+
rowspan: cell.rowSpan,
|
|
3048
|
+
colspan: cell.colSpan,
|
|
3049
|
+
...nested instanceof HTMLTableElement ? { nested: { selector: elementselector(nested), rows: collectrowshapes(nested) } } : {}
|
|
3050
|
+
};
|
|
3051
|
+
})
|
|
3052
|
+
}));
|
|
3053
|
+
}
|
|
3054
|
+
function readtable(target, root, fallbackselector) {
|
|
3055
|
+
const table = target instanceof HTMLTableElement ? target : fallbackselector ? root.querySelector(fallbackselector) : root.querySelector("table");
|
|
3056
|
+
if (!table) return null;
|
|
3057
|
+
return readgrid(collectrowshapes(table));
|
|
3058
|
+
}
|
|
3059
|
+
async function runpagedata(step, target, root = document) {
|
|
3060
|
+
let options = {};
|
|
3061
|
+
try {
|
|
3062
|
+
options = parseoptions(step);
|
|
3063
|
+
} catch {
|
|
3064
|
+
options = {};
|
|
3065
|
+
}
|
|
3066
|
+
if (step.kind === "scrapetable") {
|
|
3067
|
+
const grid = readtable(target, root, step.target);
|
|
3068
|
+
if (!grid) return { ok: false, summary: "The reviewed table selector matches no table element." };
|
|
3069
|
+
const rowlimit = typeof options.rowlimit === "number" && Number.isInteger(options.rowlimit) && options.rowlimit > 0 ? options.rowlimit : grid.rows.length;
|
|
3070
|
+
const limited = grid.rows.slice(0, rowlimit);
|
|
3071
|
+
return {
|
|
3072
|
+
ok: true,
|
|
3073
|
+
summary: `Scraped ${limited.length} row${limited.length === 1 ? "" : "s"} into ${grid.columns.length} normalized column${grid.columns.length === 1 ? "" : "s"}${grid.children.length > 0 ? ` with ${grid.children.length} nested child table${grid.children.length === 1 ? "" : "s"}` : ""}.`,
|
|
3074
|
+
details: { grid: { columns: grid.columns, rows: limited, children: grid.children }, rows: limited.length, columns: grid.columns.length }
|
|
3075
|
+
};
|
|
3076
|
+
}
|
|
3077
|
+
if (step.kind === "paginateextract") {
|
|
3078
|
+
const nextselector = typeof options.next === "string" ? options.next : "";
|
|
3079
|
+
const pages = typeof options.pages === "number" && Number.isInteger(options.pages) && options.pages > 0 ? options.pages : 1;
|
|
3080
|
+
const wait5 = typeof options.wait === "number" && Number.isFinite(options.wait) && options.wait > 0 ? options.wait : 0;
|
|
3081
|
+
const cursor = typeof options.cursor === "number" && Number.isInteger(options.cursor) && options.cursor > 0 ? options.cursor : 0;
|
|
3082
|
+
const grids = [];
|
|
3083
|
+
let previous = [];
|
|
3084
|
+
for (let page = 0; page < pages + cursor; page += 1) {
|
|
3085
|
+
if (page < cursor) {
|
|
3086
|
+
const control = root.querySelector(nextselector);
|
|
3087
|
+
if (!control) break;
|
|
3088
|
+
control.click();
|
|
3089
|
+
await new Promise((resolve) => window.setTimeout(resolve, 0));
|
|
3090
|
+
continue;
|
|
3091
|
+
}
|
|
3092
|
+
if (page > cursor) {
|
|
3093
|
+
const control = root.querySelector(nextselector);
|
|
3094
|
+
if (!control) break;
|
|
3095
|
+
control.click();
|
|
3096
|
+
const deadline = Date.now() + wait5;
|
|
3097
|
+
let fresh = false;
|
|
3098
|
+
while (!fresh && Date.now() < deadline) {
|
|
3099
|
+
await new Promise((resolve) => window.setTimeout(resolve, Math.min(100, Math.max(16, deadline - Date.now()))));
|
|
3100
|
+
const probe = readtable(target, root, step.target);
|
|
3101
|
+
fresh = probe !== null && rowsfresh(previous, probe.rows);
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
const grid = readtable(target, root, step.target);
|
|
3105
|
+
if (!grid) return { ok: false, summary: "The reviewed table selector matches no table element." };
|
|
3106
|
+
grids.push(grid);
|
|
3107
|
+
previous = grid.rows;
|
|
3108
|
+
}
|
|
3109
|
+
const merged = mergedatasets(grids);
|
|
3110
|
+
const hasnext = Boolean(root.querySelector(nextselector));
|
|
3111
|
+
return {
|
|
3112
|
+
ok: grids.length > 0,
|
|
3113
|
+
summary: grids.length > 0 ? `Followed ${grids.length} page${grids.length === 1 ? "" : "s"} of the reviewed table into ${merged.rows.length} row${merged.rows.length === 1 ? "" : "s"}${hasnext ? "; a next control remains" : ""}.` : "No page of the reviewed table was extracted.",
|
|
3114
|
+
details: { grid: { columns: merged.columns, rows: merged.rows, children: grids.flatMap((grid) => grid.children) }, pages: grids.length, rows: merged.rows.length, next: hasnext }
|
|
3115
|
+
};
|
|
3116
|
+
}
|
|
3117
|
+
return { ok: false, summary: "Unsupported data step." };
|
|
3118
|
+
}
|
|
3119
|
+
|
|
2937
3120
|
// extension/pagebridge.ts
|
|
2938
3121
|
function stepoptions3(step) {
|
|
2939
3122
|
if (!step.options) return {};
|
|
@@ -3043,6 +3226,7 @@
|
|
|
3043
3226
|
var navstepkinds = /* @__PURE__ */ new Set(["waitload", "waiturl", "followlink", "spanav", "spawait", "rewritequery", "setfragment", "stopnav", "prefetch", "preconnect", "printpdf"]);
|
|
3044
3227
|
var formkinds = /* @__PURE__ */ new Set(["fillform", "filllabel", "fillplaceholder", "detectfields", "generatevalues", "readerrors", "skiphoneypot", "detectlogin", "detecttemplate", "handoffcaptcha", "asksubmit", "submitform", "consentpassword", "attachfile"]);
|
|
3045
3228
|
var wizardkinds = /* @__PURE__ */ new Set(["runwizard", "selectchain", "picktypeahead", "pickdate", "fillcard", "fillcode"]);
|
|
3229
|
+
var datastepkinds = /* @__PURE__ */ new Set(["scrapetable", "paginateextract"]);
|
|
3046
3230
|
async function performstep(step, expectedorigin, rootdocument = document) {
|
|
3047
3231
|
if (location.origin !== expectedorigin) return { ok: false, summary: "Page origin changed before action." };
|
|
3048
3232
|
if (step.kind === "observe") return { ok: true, summary: "Observation completed." };
|
|
@@ -3091,6 +3275,7 @@
|
|
|
3091
3275
|
let result;
|
|
3092
3276
|
if (formkinds.has(step.kind)) result = runpageform(step, element, rootdocument);
|
|
3093
3277
|
else if (wizardkinds.has(step.kind)) result = runpagewizard(step, element, rootdocument);
|
|
3278
|
+
else if (datastepkinds.has(step.kind)) result = runpagedata(step, element, rootdocument);
|
|
3094
3279
|
else if (readkinds.has(step.kind)) result = runpageread(step, element, rootdocument);
|
|
3095
3280
|
else if (controlkinds.has(step.kind)) result = runpagecontrol(step, element, rootdocument);
|
|
3096
3281
|
else if (interactkinds.has(step.kind)) return await runinteractstep(step, expectedorigin, performstep);
|