@midscene/web 0.19.1 → 0.20.0
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/dist/es/agent.js +299 -247
- package/dist/es/agent.js.map +1 -1
- package/dist/es/bridge-mode-browser.js +3 -3
- package/dist/es/bridge-mode.js +301 -249
- package/dist/es/bridge-mode.js.map +1 -1
- package/dist/es/chrome-extension.js +342 -290
- package/dist/es/chrome-extension.js.map +1 -1
- package/dist/es/index.js +307 -247
- package/dist/es/index.js.map +1 -1
- package/dist/es/midscene-playground.js +341 -289
- package/dist/es/midscene-playground.js.map +1 -1
- package/dist/es/midscene-server.js +25 -12
- package/dist/es/midscene-server.js.map +1 -1
- package/dist/es/playground.js +341 -289
- package/dist/es/playground.js.map +1 -1
- package/dist/es/playwright-report.js +14 -1
- package/dist/es/playwright-report.js.map +1 -1
- package/dist/es/playwright-reporter.js +14 -1
- package/dist/es/playwright-reporter.js.map +1 -1
- package/dist/es/playwright.js +307 -247
- package/dist/es/playwright.js.map +1 -1
- package/dist/es/puppeteer-agent-launcher.js +299 -247
- package/dist/es/puppeteer-agent-launcher.js.map +1 -1
- package/dist/es/puppeteer.js +299 -247
- package/dist/es/puppeteer.js.map +1 -1
- package/dist/es/utils.js +42 -8
- package/dist/es/utils.js.map +1 -1
- package/dist/es/yaml.js +11 -4
- package/dist/es/yaml.js.map +1 -1
- package/dist/lib/agent.js +308 -256
- package/dist/lib/agent.js.map +1 -1
- package/dist/lib/bridge-mode-browser.js +3 -3
- package/dist/lib/bridge-mode.js +310 -258
- package/dist/lib/bridge-mode.js.map +1 -1
- package/dist/lib/chrome-extension.js +355 -303
- package/dist/lib/chrome-extension.js.map +1 -1
- package/dist/lib/index.js +316 -256
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/midscene-playground.js +354 -302
- package/dist/lib/midscene-playground.js.map +1 -1
- package/dist/lib/midscene-server.js +28 -15
- package/dist/lib/midscene-server.js.map +1 -1
- package/dist/lib/playground.js +354 -302
- package/dist/lib/playground.js.map +1 -1
- package/dist/lib/playwright-report.js +20 -7
- package/dist/lib/playwright-report.js.map +1 -1
- package/dist/lib/playwright-reporter.js +20 -7
- package/dist/lib/playwright-reporter.js.map +1 -1
- package/dist/lib/playwright.js +316 -256
- package/dist/lib/playwright.js.map +1 -1
- package/dist/lib/puppeteer-agent-launcher.js +308 -256
- package/dist/lib/puppeteer-agent-launcher.js.map +1 -1
- package/dist/lib/puppeteer.js +308 -256
- package/dist/lib/puppeteer.js.map +1 -1
- package/dist/lib/utils.js +48 -13
- package/dist/lib/utils.js.map +1 -1
- package/dist/lib/yaml.js +11 -4
- package/dist/lib/yaml.js.map +1 -1
- package/dist/types/agent.d.ts +6 -102
- package/dist/types/bridge-mode-browser.d.ts +3 -2
- package/dist/types/bridge-mode.d.ts +4 -4
- package/dist/types/{browser-5dbb4bfb.d.ts → browser-118d886d.d.ts} +1 -1
- package/dist/types/chrome-extension.d.ts +2 -2
- package/dist/types/index.d.ts +1 -1
- package/dist/types/midscene-server.d.ts +2 -2
- package/dist/types/{page-90e9f9a7.d.ts → page-471361cd.d.ts} +102 -3
- package/dist/types/playground.d.ts +2 -2
- package/dist/types/playwright.d.ts +6 -2
- package/dist/types/puppeteer-agent-launcher.d.ts +1 -1
- package/dist/types/puppeteer.d.ts +3 -3
- package/dist/types/utils.d.ts +2 -1
- package/dist/types/yaml.d.ts +1 -1
- package/package.json +3 -3
package/dist/es/puppeteer.js
CHANGED
|
@@ -149,7 +149,7 @@ var ScriptPlayer = class {
|
|
|
149
149
|
domIncluded: numberTask.domIncluded,
|
|
150
150
|
screenshotIncluded: numberTask.screenshotIncluded
|
|
151
151
|
};
|
|
152
|
-
assert(prompt, "missing prompt for
|
|
152
|
+
assert(prompt, "missing prompt for aiNumber");
|
|
153
153
|
assert(
|
|
154
154
|
typeof prompt === "string",
|
|
155
155
|
"prompt for number must be a string"
|
|
@@ -163,7 +163,7 @@ var ScriptPlayer = class {
|
|
|
163
163
|
domIncluded: stringTask.domIncluded,
|
|
164
164
|
screenshotIncluded: stringTask.screenshotIncluded
|
|
165
165
|
};
|
|
166
|
-
assert(prompt, "missing prompt for
|
|
166
|
+
assert(prompt, "missing prompt for aiNumber");
|
|
167
167
|
assert(
|
|
168
168
|
typeof prompt === "string",
|
|
169
169
|
"prompt for string must be a string"
|
|
@@ -177,13 +177,20 @@ var ScriptPlayer = class {
|
|
|
177
177
|
domIncluded: booleanTask.domIncluded,
|
|
178
178
|
screenshotIncluded: booleanTask.screenshotIncluded
|
|
179
179
|
};
|
|
180
|
-
assert(prompt, "missing prompt for
|
|
180
|
+
assert(prompt, "missing prompt for aiBoolean");
|
|
181
181
|
assert(
|
|
182
182
|
typeof prompt === "string",
|
|
183
183
|
"prompt for boolean must be a string"
|
|
184
184
|
);
|
|
185
185
|
const booleanResult = await agent.aiBoolean(prompt, options);
|
|
186
186
|
this.setResult(booleanTask.name, booleanResult);
|
|
187
|
+
} else if ("aiAsk" in flowItem) {
|
|
188
|
+
const askTask = flowItem;
|
|
189
|
+
const prompt = askTask.aiAsk;
|
|
190
|
+
assert(prompt, "missing prompt for aiAsk");
|
|
191
|
+
assert(typeof prompt === "string", "prompt for aiAsk must be a string");
|
|
192
|
+
const askResult = await agent.aiAsk(prompt);
|
|
193
|
+
this.setResult(askTask.name, askResult);
|
|
187
194
|
} else if ("aiLocate" in flowItem) {
|
|
188
195
|
const locateTask = flowItem;
|
|
189
196
|
const prompt = locateTask.aiLocate;
|
|
@@ -192,7 +199,7 @@ var ScriptPlayer = class {
|
|
|
192
199
|
typeof prompt === "string",
|
|
193
200
|
"prompt for aiLocate must be a string"
|
|
194
201
|
);
|
|
195
|
-
const locateResult = await agent.aiLocate(prompt);
|
|
202
|
+
const locateResult = await agent.aiLocate(prompt, locateTask);
|
|
196
203
|
this.setResult(locateTask.name, locateResult);
|
|
197
204
|
} else if ("aiWaitFor" in flowItem) {
|
|
198
205
|
const waitForTask = flowItem;
|
|
@@ -418,8 +425,8 @@ import {
|
|
|
418
425
|
} from "@midscene/core/ai-model";
|
|
419
426
|
import { sleep } from "@midscene/core/utils";
|
|
420
427
|
import { NodeType } from "@midscene/shared/constants";
|
|
421
|
-
import { getDebug } from "@midscene/shared/logger";
|
|
422
|
-
import { assert as
|
|
428
|
+
import { getDebug as getDebug2 } from "@midscene/shared/logger";
|
|
429
|
+
import { assert as assert5 } from "@midscene/shared/utils";
|
|
423
430
|
|
|
424
431
|
// src/common/ui-utils.ts
|
|
425
432
|
function typeStr(task) {
|
|
@@ -504,7 +511,7 @@ import {
|
|
|
504
511
|
traverseTree
|
|
505
512
|
} from "@midscene/shared/extractor";
|
|
506
513
|
import { resizeImgBase64 } from "@midscene/shared/img";
|
|
507
|
-
import { assert as
|
|
514
|
+
import { assert as assert4, logMsg, uuid } from "@midscene/shared/utils";
|
|
508
515
|
import dayjs from "dayjs";
|
|
509
516
|
|
|
510
517
|
// src/web-element.ts
|
|
@@ -532,9 +539,184 @@ var WebElementInfo = class {
|
|
|
532
539
|
}
|
|
533
540
|
};
|
|
534
541
|
|
|
542
|
+
// src/common/task-cache.ts
|
|
543
|
+
import assert3 from "assert";
|
|
544
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
|
|
545
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
546
|
+
import { getMidsceneRunSubDir as getMidsceneRunSubDir2 } from "@midscene/shared/common";
|
|
547
|
+
import { getDebug } from "@midscene/shared/logger";
|
|
548
|
+
import { ifInBrowser as ifInBrowser2 } from "@midscene/shared/utils";
|
|
549
|
+
import yaml3 from "js-yaml";
|
|
550
|
+
import semver from "semver";
|
|
551
|
+
|
|
552
|
+
// package.json
|
|
553
|
+
var version = "0.20.0";
|
|
554
|
+
|
|
555
|
+
// src/common/task-cache.ts
|
|
556
|
+
var debug = getDebug("cache");
|
|
557
|
+
var lowestSupportedMidsceneVersion = "0.16.10";
|
|
558
|
+
var cacheFileExt = ".cache.yaml";
|
|
559
|
+
var TaskCache = class {
|
|
560
|
+
// Track matched records
|
|
561
|
+
constructor(cacheId, isCacheResultUsed, cacheFilePath) {
|
|
562
|
+
this.matchedCacheIndices = /* @__PURE__ */ new Set();
|
|
563
|
+
assert3(cacheId, "cacheId is required");
|
|
564
|
+
this.cacheId = replaceIllegalPathCharsAndSpace(cacheId);
|
|
565
|
+
this.cacheFilePath = ifInBrowser2 ? void 0 : cacheFilePath || join2(getMidsceneRunSubDir2("cache"), `${this.cacheId}${cacheFileExt}`);
|
|
566
|
+
this.isCacheResultUsed = isCacheResultUsed;
|
|
567
|
+
let cacheContent;
|
|
568
|
+
if (this.cacheFilePath) {
|
|
569
|
+
cacheContent = this.loadCacheFromFile();
|
|
570
|
+
}
|
|
571
|
+
if (!cacheContent) {
|
|
572
|
+
cacheContent = {
|
|
573
|
+
midsceneVersion: version,
|
|
574
|
+
cacheId: this.cacheId,
|
|
575
|
+
caches: []
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
this.cache = cacheContent;
|
|
579
|
+
this.cacheOriginalLength = this.cache.caches.length;
|
|
580
|
+
}
|
|
581
|
+
matchCache(prompt, type) {
|
|
582
|
+
for (let i = 0; i < this.cacheOriginalLength; i++) {
|
|
583
|
+
const item = this.cache.caches[i];
|
|
584
|
+
const key = `${type}:${prompt}:${i}`;
|
|
585
|
+
if (item.type === type && item.prompt === prompt && !this.matchedCacheIndices.has(key)) {
|
|
586
|
+
this.matchedCacheIndices.add(key);
|
|
587
|
+
debug(
|
|
588
|
+
"cache found and marked as used, type: %s, prompt: %s, index: %d",
|
|
589
|
+
type,
|
|
590
|
+
prompt,
|
|
591
|
+
i
|
|
592
|
+
);
|
|
593
|
+
return {
|
|
594
|
+
cacheContent: item,
|
|
595
|
+
updateFn: (cb) => {
|
|
596
|
+
debug(
|
|
597
|
+
"will call updateFn to update cache, type: %s, prompt: %s, index: %d",
|
|
598
|
+
type,
|
|
599
|
+
prompt,
|
|
600
|
+
i
|
|
601
|
+
);
|
|
602
|
+
cb(item);
|
|
603
|
+
debug(
|
|
604
|
+
"cache updated, will flush to file, type: %s, prompt: %s, index: %d",
|
|
605
|
+
type,
|
|
606
|
+
prompt,
|
|
607
|
+
i
|
|
608
|
+
);
|
|
609
|
+
this.flushCacheToFile();
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
debug("no unused cache found, type: %s, prompt: %s", type, prompt);
|
|
615
|
+
return void 0;
|
|
616
|
+
}
|
|
617
|
+
matchPlanCache(prompt) {
|
|
618
|
+
return this.matchCache(prompt, "plan");
|
|
619
|
+
}
|
|
620
|
+
matchLocateCache(prompt) {
|
|
621
|
+
return this.matchCache(prompt, "locate");
|
|
622
|
+
}
|
|
623
|
+
appendCache(cache) {
|
|
624
|
+
debug("will append cache", cache);
|
|
625
|
+
this.cache.caches.push(cache);
|
|
626
|
+
this.flushCacheToFile();
|
|
627
|
+
}
|
|
628
|
+
loadCacheFromFile() {
|
|
629
|
+
const cacheFile = this.cacheFilePath;
|
|
630
|
+
assert3(cacheFile, "cache file path is required");
|
|
631
|
+
if (!existsSync2(cacheFile)) {
|
|
632
|
+
debug("no cache file found, path: %s", cacheFile);
|
|
633
|
+
return void 0;
|
|
634
|
+
}
|
|
635
|
+
const jsonTypeCacheFile = cacheFile.replace(cacheFileExt, ".json");
|
|
636
|
+
if (existsSync2(jsonTypeCacheFile) && this.isCacheResultUsed) {
|
|
637
|
+
console.warn(
|
|
638
|
+
`An outdated cache file from an earlier version of Midscene has been detected. Since version 0.17, we have implemented an improved caching strategy. Please delete the old file located at: ${jsonTypeCacheFile}.`
|
|
639
|
+
);
|
|
640
|
+
return void 0;
|
|
641
|
+
}
|
|
642
|
+
try {
|
|
643
|
+
const data = readFileSync(cacheFile, "utf8");
|
|
644
|
+
const jsonData = yaml3.load(data);
|
|
645
|
+
if (!version) {
|
|
646
|
+
debug("no midscene version info, will not read cache from file");
|
|
647
|
+
return void 0;
|
|
648
|
+
}
|
|
649
|
+
if (semver.lt(jsonData.midsceneVersion, lowestSupportedMidsceneVersion) && !jsonData.midsceneVersion.includes("beta")) {
|
|
650
|
+
console.warn(
|
|
651
|
+
`You are using an old version of Midscene cache file, and we cannot match any info from it. Starting from Midscene v0.17, we changed our strategy to use xpath for cache info, providing better performance.
|
|
652
|
+
Please delete the existing cache and rebuild it. Sorry for the inconvenience.
|
|
653
|
+
cache file: ${cacheFile}`
|
|
654
|
+
);
|
|
655
|
+
return void 0;
|
|
656
|
+
}
|
|
657
|
+
debug(
|
|
658
|
+
"cache loaded from file, path: %s, cache version: %s, record length: %s",
|
|
659
|
+
cacheFile,
|
|
660
|
+
jsonData.midsceneVersion,
|
|
661
|
+
jsonData.caches.length
|
|
662
|
+
);
|
|
663
|
+
jsonData.midsceneVersion = version;
|
|
664
|
+
return jsonData;
|
|
665
|
+
} catch (err) {
|
|
666
|
+
debug(
|
|
667
|
+
"cache file exists but load failed, path: %s, error: %s",
|
|
668
|
+
cacheFile,
|
|
669
|
+
err
|
|
670
|
+
);
|
|
671
|
+
return void 0;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
flushCacheToFile() {
|
|
675
|
+
if (!version) {
|
|
676
|
+
debug("no midscene version info, will not write cache to file");
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
if (!this.cacheFilePath) {
|
|
680
|
+
debug("no cache file path, will not write cache to file");
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
try {
|
|
684
|
+
const dir = dirname2(this.cacheFilePath);
|
|
685
|
+
if (!existsSync2(dir)) {
|
|
686
|
+
mkdirSync2(dir, { recursive: true });
|
|
687
|
+
debug("created cache directory: %s", dir);
|
|
688
|
+
}
|
|
689
|
+
const yamlData = yaml3.dump(this.cache);
|
|
690
|
+
writeFileSync2(this.cacheFilePath, yamlData);
|
|
691
|
+
debug("cache flushed to file: %s", this.cacheFilePath);
|
|
692
|
+
} catch (err) {
|
|
693
|
+
debug(
|
|
694
|
+
"write cache to file failed, path: %s, error: %s",
|
|
695
|
+
this.cacheFilePath,
|
|
696
|
+
err
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
updateOrAppendCacheRecord(newRecord, cachedRecord) {
|
|
701
|
+
if (cachedRecord) {
|
|
702
|
+
if (newRecord.type === "plan") {
|
|
703
|
+
cachedRecord.updateFn((cache) => {
|
|
704
|
+
cache.yamlWorkflow = newRecord.yamlWorkflow;
|
|
705
|
+
});
|
|
706
|
+
} else {
|
|
707
|
+
cachedRecord.updateFn((cache) => {
|
|
708
|
+
cache.xpaths = newRecord.xpaths;
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
} else {
|
|
712
|
+
this.appendCache(newRecord);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
|
|
535
717
|
// src/common/utils.ts
|
|
536
718
|
async function parseContextFromWebPage(page, _opt) {
|
|
537
|
-
|
|
719
|
+
assert4(page, "page is required");
|
|
538
720
|
if (page._forceUsePageContext) {
|
|
539
721
|
return await page._forceUsePageContext();
|
|
540
722
|
}
|
|
@@ -561,7 +743,7 @@ async function parseContextFromWebPage(page, _opt) {
|
|
|
561
743
|
isVisible
|
|
562
744
|
});
|
|
563
745
|
});
|
|
564
|
-
|
|
746
|
+
assert4(screenshotBase64, "screenshotBase64 is required");
|
|
565
747
|
const size = await page.size();
|
|
566
748
|
if (size.dpr && size.dpr > 1) {
|
|
567
749
|
screenshotBase64 = await resizeImgBase64(screenshotBase64, {
|
|
@@ -636,6 +818,28 @@ function matchElementFromPlan(planLocateParam, tree) {
|
|
|
636
818
|
}
|
|
637
819
|
return void 0;
|
|
638
820
|
}
|
|
821
|
+
async function matchElementFromCache(taskExecutor, xpaths, cachePrompt, cacheable) {
|
|
822
|
+
try {
|
|
823
|
+
if (xpaths?.length && taskExecutor.taskCache?.isCacheResultUsed && cacheable !== false) {
|
|
824
|
+
for (let i = 0; i < xpaths.length; i++) {
|
|
825
|
+
const element = await taskExecutor.page.getElementInfoByXpath(
|
|
826
|
+
xpaths[i]
|
|
827
|
+
);
|
|
828
|
+
if (element?.id) {
|
|
829
|
+
debug("cache hit, prompt: %s", cachePrompt);
|
|
830
|
+
debug(
|
|
831
|
+
"found a new new element with same xpath, xpath: %s, id: %s",
|
|
832
|
+
xpaths[i],
|
|
833
|
+
element?.id
|
|
834
|
+
);
|
|
835
|
+
return element;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
} catch (error) {
|
|
840
|
+
debug("get element info by xpath error: ", error);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
639
843
|
function trimContextByViewport(execution) {
|
|
640
844
|
function filterVisibleTree(node) {
|
|
641
845
|
if (!node)
|
|
@@ -674,7 +878,7 @@ function trimContextByViewport(execution) {
|
|
|
674
878
|
}
|
|
675
879
|
|
|
676
880
|
// src/common/tasks.ts
|
|
677
|
-
var
|
|
881
|
+
var debug2 = getDebug2("page-task-executor");
|
|
678
882
|
var replanningCountLimit = 10;
|
|
679
883
|
var isAndroidPage = (page) => {
|
|
680
884
|
return page.pageType === "android";
|
|
@@ -715,7 +919,7 @@ var PageTaskExecutor = class {
|
|
|
715
919
|
if (info?.id) {
|
|
716
920
|
elementId = info.id;
|
|
717
921
|
} else {
|
|
718
|
-
|
|
922
|
+
debug2(
|
|
719
923
|
"no element id found for position node, will not update cache",
|
|
720
924
|
element
|
|
721
925
|
);
|
|
@@ -728,7 +932,7 @@ var PageTaskExecutor = class {
|
|
|
728
932
|
const result = await this.page.getXpathsById(elementId);
|
|
729
933
|
return result;
|
|
730
934
|
} catch (error) {
|
|
731
|
-
|
|
935
|
+
debug2("getXpathsById error: ", error);
|
|
732
936
|
}
|
|
733
937
|
}
|
|
734
938
|
prependExecutorWithScreenshot(taskApply, appendAfterExecution = false) {
|
|
@@ -782,7 +986,7 @@ var PageTaskExecutor = class {
|
|
|
782
986
|
locate: plan2.locate,
|
|
783
987
|
executor: async (param, taskContext) => {
|
|
784
988
|
const { task } = taskContext;
|
|
785
|
-
|
|
989
|
+
assert5(
|
|
786
990
|
param?.prompt || param?.id || param?.bbox,
|
|
787
991
|
"No prompt or id or position or bbox to locate"
|
|
788
992
|
);
|
|
@@ -807,39 +1011,29 @@ var PageTaskExecutor = class {
|
|
|
807
1011
|
timing: "before Insight"
|
|
808
1012
|
};
|
|
809
1013
|
task.recorder = [recordItem];
|
|
810
|
-
|
|
1014
|
+
const elementFromXpath = param.xpath ? await this.page.getElementInfoByXpath(param.xpath) : void 0;
|
|
1015
|
+
const userExpectedPathHitFlag = !!elementFromXpath;
|
|
811
1016
|
const cachePrompt = param.prompt;
|
|
812
1017
|
const locateCacheRecord = this.taskCache?.matchLocateCache(cachePrompt);
|
|
813
1018
|
const xpaths = locateCacheRecord?.cacheContent?.xpaths;
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
debug(
|
|
826
|
-
"found a new new element with same xpath, xpath: %s, id: %s",
|
|
827
|
-
xpaths[i],
|
|
828
|
-
element2?.id
|
|
829
|
-
);
|
|
830
|
-
break;
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
} catch (error) {
|
|
835
|
-
debug("get element info by xpath error: ", error);
|
|
836
|
-
}
|
|
837
|
-
const startTime = Date.now();
|
|
838
|
-
const element = elementFromCache || // try to match element from cache
|
|
839
|
-
matchElementFromPlan(param, pageContext.tree) || // try to match element from plan
|
|
840
|
-
(await this.insight.locate(param, {
|
|
1019
|
+
const elementFromCache = userExpectedPathHitFlag ? null : await matchElementFromCache(
|
|
1020
|
+
this,
|
|
1021
|
+
xpaths,
|
|
1022
|
+
cachePrompt,
|
|
1023
|
+
param.cacheable
|
|
1024
|
+
);
|
|
1025
|
+
const cacheHitFlag = !!elementFromCache;
|
|
1026
|
+
const elementFromPlan = !userExpectedPathHitFlag && !cacheHitFlag ? matchElementFromPlan(param, pageContext.tree) : void 0;
|
|
1027
|
+
const planHitFlag = !!elementFromPlan;
|
|
1028
|
+
const elementFromAiLocate = !userExpectedPathHitFlag && !cacheHitFlag && !planHitFlag ? (await this.insight.locate(param, {
|
|
1029
|
+
// fallback to ai locate
|
|
841
1030
|
context: pageContext
|
|
842
|
-
})).element;
|
|
1031
|
+
})).element : void 0;
|
|
1032
|
+
const aiLocateHitFlag = !!elementFromAiLocate;
|
|
1033
|
+
const element = elementFromXpath || // highest priority
|
|
1034
|
+
elementFromCache || // second priority
|
|
1035
|
+
elementFromPlan || // third priority
|
|
1036
|
+
elementFromAiLocate;
|
|
843
1037
|
let currentXpaths;
|
|
844
1038
|
if (element && this.taskCache && !cacheHitFlag && param?.cacheable !== false) {
|
|
845
1039
|
const elementXpaths = await this.getElementXpath(
|
|
@@ -857,7 +1051,7 @@ var PageTaskExecutor = class {
|
|
|
857
1051
|
locateCacheRecord
|
|
858
1052
|
);
|
|
859
1053
|
} else {
|
|
860
|
-
|
|
1054
|
+
debug2(
|
|
861
1055
|
"no xpaths found, will not update cache",
|
|
862
1056
|
cachePrompt,
|
|
863
1057
|
elementXpaths
|
|
@@ -867,16 +1061,44 @@ var PageTaskExecutor = class {
|
|
|
867
1061
|
if (!element) {
|
|
868
1062
|
throw new Error(`Element not found: ${param.prompt}`);
|
|
869
1063
|
}
|
|
1064
|
+
let hitBy;
|
|
1065
|
+
if (userExpectedPathHitFlag) {
|
|
1066
|
+
hitBy = {
|
|
1067
|
+
from: "User expected path",
|
|
1068
|
+
context: {
|
|
1069
|
+
xpath: param.xpath
|
|
1070
|
+
}
|
|
1071
|
+
};
|
|
1072
|
+
} else if (cacheHitFlag) {
|
|
1073
|
+
hitBy = {
|
|
1074
|
+
from: "Cache",
|
|
1075
|
+
context: {
|
|
1076
|
+
xpathsFromCache: xpaths,
|
|
1077
|
+
xpathsToSave: currentXpaths
|
|
1078
|
+
}
|
|
1079
|
+
};
|
|
1080
|
+
} else if (planHitFlag) {
|
|
1081
|
+
hitBy = {
|
|
1082
|
+
from: "Planning",
|
|
1083
|
+
context: {
|
|
1084
|
+
id: elementFromPlan?.id,
|
|
1085
|
+
bbox: elementFromPlan?.bbox
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
} else if (aiLocateHitFlag) {
|
|
1089
|
+
hitBy = {
|
|
1090
|
+
from: "AI model",
|
|
1091
|
+
context: {
|
|
1092
|
+
prompt: param.prompt
|
|
1093
|
+
}
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
870
1096
|
return {
|
|
871
1097
|
output: {
|
|
872
1098
|
element
|
|
873
1099
|
},
|
|
874
1100
|
pageContext,
|
|
875
|
-
|
|
876
|
-
hit: cacheHitFlag,
|
|
877
|
-
originalXpaths: xpaths,
|
|
878
|
-
currentXpaths
|
|
879
|
-
}
|
|
1101
|
+
hitBy
|
|
880
1102
|
};
|
|
881
1103
|
}
|
|
882
1104
|
};
|
|
@@ -972,7 +1194,7 @@ var PageTaskExecutor = class {
|
|
|
972
1194
|
thought: plan2.thought,
|
|
973
1195
|
locate: plan2.locate,
|
|
974
1196
|
executor: async (param, { element }) => {
|
|
975
|
-
|
|
1197
|
+
assert5(element, "Element not found, cannot tap");
|
|
976
1198
|
await this.page.mouse.click(element.center[0], element.center[1]);
|
|
977
1199
|
}
|
|
978
1200
|
};
|
|
@@ -984,7 +1206,7 @@ var PageTaskExecutor = class {
|
|
|
984
1206
|
thought: plan2.thought,
|
|
985
1207
|
locate: plan2.locate,
|
|
986
1208
|
executor: async (param, { element }) => {
|
|
987
|
-
|
|
1209
|
+
assert5(element, "Element not found, cannot right click");
|
|
988
1210
|
await this.page.mouse.click(
|
|
989
1211
|
element.center[0],
|
|
990
1212
|
element.center[1],
|
|
@@ -1001,7 +1223,7 @@ var PageTaskExecutor = class {
|
|
|
1001
1223
|
thought: plan2.thought,
|
|
1002
1224
|
locate: plan2.locate,
|
|
1003
1225
|
executor: async (taskParam) => {
|
|
1004
|
-
|
|
1226
|
+
assert5(
|
|
1005
1227
|
taskParam?.start_box && taskParam?.end_box,
|
|
1006
1228
|
"No start_box or end_box to drag"
|
|
1007
1229
|
);
|
|
@@ -1016,7 +1238,7 @@ var PageTaskExecutor = class {
|
|
|
1016
1238
|
thought: plan2.thought,
|
|
1017
1239
|
locate: plan2.locate,
|
|
1018
1240
|
executor: async (param, { element }) => {
|
|
1019
|
-
|
|
1241
|
+
assert5(element, "Element not found, cannot hover");
|
|
1020
1242
|
await this.page.mouse.move(element.center[0], element.center[1]);
|
|
1021
1243
|
}
|
|
1022
1244
|
};
|
|
@@ -1135,7 +1357,7 @@ var PageTaskExecutor = class {
|
|
|
1135
1357
|
thought: plan2.thought,
|
|
1136
1358
|
locate: plan2.locate,
|
|
1137
1359
|
executor: async (param) => {
|
|
1138
|
-
|
|
1360
|
+
assert5(
|
|
1139
1361
|
isAndroidPage(this.page),
|
|
1140
1362
|
"Cannot use home button on non-Android devices"
|
|
1141
1363
|
);
|
|
@@ -1151,7 +1373,7 @@ var PageTaskExecutor = class {
|
|
|
1151
1373
|
thought: plan2.thought,
|
|
1152
1374
|
locate: plan2.locate,
|
|
1153
1375
|
executor: async (param) => {
|
|
1154
|
-
|
|
1376
|
+
assert5(
|
|
1155
1377
|
isAndroidPage(this.page),
|
|
1156
1378
|
"Cannot use back button on non-Android devices"
|
|
1157
1379
|
);
|
|
@@ -1167,7 +1389,7 @@ var PageTaskExecutor = class {
|
|
|
1167
1389
|
thought: plan2.thought,
|
|
1168
1390
|
locate: plan2.locate,
|
|
1169
1391
|
executor: async (param) => {
|
|
1170
|
-
|
|
1392
|
+
assert5(
|
|
1171
1393
|
isAndroidPage(this.page),
|
|
1172
1394
|
"Cannot use recent apps button on non-Android devices"
|
|
1173
1395
|
);
|
|
@@ -1318,7 +1540,7 @@ var PageTaskExecutor = class {
|
|
|
1318
1540
|
}
|
|
1319
1541
|
}
|
|
1320
1542
|
if (finalActions.length === 0) {
|
|
1321
|
-
|
|
1543
|
+
assert5(
|
|
1322
1544
|
!more_actions_needed_by_instruction || sleep3,
|
|
1323
1545
|
error ? `Failed to plan: ${error}` : planParsingError || "No plan found"
|
|
1324
1546
|
);
|
|
@@ -1556,7 +1778,7 @@ var PageTaskExecutor = class {
|
|
|
1556
1778
|
);
|
|
1557
1779
|
let outputResult = data;
|
|
1558
1780
|
if (ifTypeRestricted) {
|
|
1559
|
-
|
|
1781
|
+
assert5(data?.result !== void 0, "No result in query data");
|
|
1560
1782
|
outputResult = data.result;
|
|
1561
1783
|
}
|
|
1562
1784
|
return {
|
|
@@ -1652,9 +1874,9 @@ var PageTaskExecutor = class {
|
|
|
1652
1874
|
onTaskStart: this.onTaskStartCallback
|
|
1653
1875
|
});
|
|
1654
1876
|
const { timeoutMs, checkIntervalMs } = opt;
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1877
|
+
assert5(assertion, "No assertion for waitFor");
|
|
1878
|
+
assert5(timeoutMs, "No timeoutMs for waitFor");
|
|
1879
|
+
assert5(checkIntervalMs, "No checkIntervalMs for waitFor");
|
|
1658
1880
|
const overallStartTime = Date.now();
|
|
1659
1881
|
let startTime = Date.now();
|
|
1660
1882
|
let errorThought = "";
|
|
@@ -1708,9 +1930,9 @@ var PageTaskExecutor = class {
|
|
|
1708
1930
|
};
|
|
1709
1931
|
|
|
1710
1932
|
// src/common/plan-builder.ts
|
|
1711
|
-
import { getDebug as
|
|
1712
|
-
import { assert as
|
|
1713
|
-
var
|
|
1933
|
+
import { getDebug as getDebug3 } from "@midscene/shared/logger";
|
|
1934
|
+
import { assert as assert6 } from "@midscene/shared/utils";
|
|
1935
|
+
var debug3 = getDebug3("plan-builder");
|
|
1714
1936
|
function buildPlans(type, locateParam, param) {
|
|
1715
1937
|
let returnPlans = [];
|
|
1716
1938
|
const locatePlan = locateParam ? {
|
|
@@ -1720,8 +1942,8 @@ function buildPlans(type, locateParam, param) {
|
|
|
1720
1942
|
thought: ""
|
|
1721
1943
|
} : null;
|
|
1722
1944
|
if (type === "Tap" || type === "Hover" || type === "RightClick") {
|
|
1723
|
-
|
|
1724
|
-
|
|
1945
|
+
assert6(locateParam, `missing locate info for action "${type}"`);
|
|
1946
|
+
assert6(locatePlan, `missing locate info for action "${type}"`);
|
|
1725
1947
|
const tapPlan = {
|
|
1726
1948
|
type,
|
|
1727
1949
|
param: null,
|
|
@@ -1732,9 +1954,9 @@ function buildPlans(type, locateParam, param) {
|
|
|
1732
1954
|
}
|
|
1733
1955
|
if (type === "Input" || type === "KeyboardPress") {
|
|
1734
1956
|
if (type === "Input") {
|
|
1735
|
-
|
|
1957
|
+
assert6(locateParam, `missing locate info for action "${type}"`);
|
|
1736
1958
|
}
|
|
1737
|
-
|
|
1959
|
+
assert6(param, `missing param for action "${type}"`);
|
|
1738
1960
|
const inputPlan = {
|
|
1739
1961
|
type,
|
|
1740
1962
|
param,
|
|
@@ -1748,7 +1970,7 @@ function buildPlans(type, locateParam, param) {
|
|
|
1748
1970
|
}
|
|
1749
1971
|
}
|
|
1750
1972
|
if (type === "Scroll") {
|
|
1751
|
-
|
|
1973
|
+
assert6(param, `missing param for action "${type}"`);
|
|
1752
1974
|
const scrollPlan = {
|
|
1753
1975
|
type,
|
|
1754
1976
|
param,
|
|
@@ -1762,7 +1984,7 @@ function buildPlans(type, locateParam, param) {
|
|
|
1762
1984
|
}
|
|
1763
1985
|
}
|
|
1764
1986
|
if (type === "Sleep") {
|
|
1765
|
-
|
|
1987
|
+
assert6(param, `missing param for action "${type}"`);
|
|
1766
1988
|
const sleepPlan = {
|
|
1767
1989
|
type,
|
|
1768
1990
|
param,
|
|
@@ -1772,7 +1994,7 @@ function buildPlans(type, locateParam, param) {
|
|
|
1772
1994
|
returnPlans = [sleepPlan];
|
|
1773
1995
|
}
|
|
1774
1996
|
if (type === "Locate") {
|
|
1775
|
-
|
|
1997
|
+
assert6(locateParam, `missing locate info for action "${type}"`);
|
|
1776
1998
|
const locatePlan2 = {
|
|
1777
1999
|
type,
|
|
1778
2000
|
param: locateParam,
|
|
@@ -1782,187 +2004,12 @@ function buildPlans(type, locateParam, param) {
|
|
|
1782
2004
|
returnPlans = [locatePlan2];
|
|
1783
2005
|
}
|
|
1784
2006
|
if (returnPlans) {
|
|
1785
|
-
|
|
2007
|
+
debug3("buildPlans", returnPlans);
|
|
1786
2008
|
return returnPlans;
|
|
1787
2009
|
}
|
|
1788
2010
|
throw new Error(`Not supported type: ${type}`);
|
|
1789
2011
|
}
|
|
1790
2012
|
|
|
1791
|
-
// src/common/task-cache.ts
|
|
1792
|
-
import assert6 from "assert";
|
|
1793
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1794
|
-
import { dirname as dirname2, join as join2 } from "path";
|
|
1795
|
-
import { getMidsceneRunSubDir as getMidsceneRunSubDir2 } from "@midscene/shared/common";
|
|
1796
|
-
import { getDebug as getDebug3 } from "@midscene/shared/logger";
|
|
1797
|
-
import { ifInBrowser as ifInBrowser2 } from "@midscene/shared/utils";
|
|
1798
|
-
import yaml3 from "js-yaml";
|
|
1799
|
-
import semver from "semver";
|
|
1800
|
-
|
|
1801
|
-
// package.json
|
|
1802
|
-
var version = "0.19.1";
|
|
1803
|
-
|
|
1804
|
-
// src/common/task-cache.ts
|
|
1805
|
-
var debug3 = getDebug3("cache");
|
|
1806
|
-
var lowestSupportedMidsceneVersion = "0.16.10";
|
|
1807
|
-
var cacheFileExt = ".cache.yaml";
|
|
1808
|
-
var TaskCache = class {
|
|
1809
|
-
// Track matched records
|
|
1810
|
-
constructor(cacheId, isCacheResultUsed, cacheFilePath) {
|
|
1811
|
-
this.matchedCacheIndices = /* @__PURE__ */ new Set();
|
|
1812
|
-
assert6(cacheId, "cacheId is required");
|
|
1813
|
-
this.cacheId = replaceIllegalPathCharsAndSpace(cacheId);
|
|
1814
|
-
this.cacheFilePath = ifInBrowser2 ? void 0 : cacheFilePath || join2(getMidsceneRunSubDir2("cache"), `${this.cacheId}${cacheFileExt}`);
|
|
1815
|
-
this.isCacheResultUsed = isCacheResultUsed;
|
|
1816
|
-
let cacheContent;
|
|
1817
|
-
if (this.cacheFilePath) {
|
|
1818
|
-
cacheContent = this.loadCacheFromFile();
|
|
1819
|
-
}
|
|
1820
|
-
if (!cacheContent) {
|
|
1821
|
-
cacheContent = {
|
|
1822
|
-
midsceneVersion: version,
|
|
1823
|
-
cacheId: this.cacheId,
|
|
1824
|
-
caches: []
|
|
1825
|
-
};
|
|
1826
|
-
}
|
|
1827
|
-
this.cache = cacheContent;
|
|
1828
|
-
this.cacheOriginalLength = this.cache.caches.length;
|
|
1829
|
-
}
|
|
1830
|
-
matchCache(prompt, type) {
|
|
1831
|
-
for (let i = 0; i < this.cacheOriginalLength; i++) {
|
|
1832
|
-
const item = this.cache.caches[i];
|
|
1833
|
-
const key = `${type}:${prompt}:${i}`;
|
|
1834
|
-
if (item.type === type && item.prompt === prompt && !this.matchedCacheIndices.has(key)) {
|
|
1835
|
-
this.matchedCacheIndices.add(key);
|
|
1836
|
-
debug3(
|
|
1837
|
-
"cache found and marked as used, type: %s, prompt: %s, index: %d",
|
|
1838
|
-
type,
|
|
1839
|
-
prompt,
|
|
1840
|
-
i
|
|
1841
|
-
);
|
|
1842
|
-
return {
|
|
1843
|
-
cacheContent: item,
|
|
1844
|
-
updateFn: (cb) => {
|
|
1845
|
-
debug3(
|
|
1846
|
-
"will call updateFn to update cache, type: %s, prompt: %s, index: %d",
|
|
1847
|
-
type,
|
|
1848
|
-
prompt,
|
|
1849
|
-
i
|
|
1850
|
-
);
|
|
1851
|
-
cb(item);
|
|
1852
|
-
debug3(
|
|
1853
|
-
"cache updated, will flush to file, type: %s, prompt: %s, index: %d",
|
|
1854
|
-
type,
|
|
1855
|
-
prompt,
|
|
1856
|
-
i
|
|
1857
|
-
);
|
|
1858
|
-
this.flushCacheToFile();
|
|
1859
|
-
}
|
|
1860
|
-
};
|
|
1861
|
-
}
|
|
1862
|
-
}
|
|
1863
|
-
debug3("no unused cache found, type: %s, prompt: %s", type, prompt);
|
|
1864
|
-
return void 0;
|
|
1865
|
-
}
|
|
1866
|
-
matchPlanCache(prompt) {
|
|
1867
|
-
return this.matchCache(prompt, "plan");
|
|
1868
|
-
}
|
|
1869
|
-
matchLocateCache(prompt) {
|
|
1870
|
-
return this.matchCache(prompt, "locate");
|
|
1871
|
-
}
|
|
1872
|
-
appendCache(cache) {
|
|
1873
|
-
debug3("will append cache", cache);
|
|
1874
|
-
this.cache.caches.push(cache);
|
|
1875
|
-
this.flushCacheToFile();
|
|
1876
|
-
}
|
|
1877
|
-
loadCacheFromFile() {
|
|
1878
|
-
const cacheFile = this.cacheFilePath;
|
|
1879
|
-
assert6(cacheFile, "cache file path is required");
|
|
1880
|
-
if (!existsSync2(cacheFile)) {
|
|
1881
|
-
debug3("no cache file found, path: %s", cacheFile);
|
|
1882
|
-
return void 0;
|
|
1883
|
-
}
|
|
1884
|
-
const jsonTypeCacheFile = cacheFile.replace(cacheFileExt, ".json");
|
|
1885
|
-
if (existsSync2(jsonTypeCacheFile) && this.isCacheResultUsed) {
|
|
1886
|
-
console.warn(
|
|
1887
|
-
`An outdated cache file from an earlier version of Midscene has been detected. Since version 0.17, we have implemented an improved caching strategy. Please delete the old file located at: ${jsonTypeCacheFile}.`
|
|
1888
|
-
);
|
|
1889
|
-
return void 0;
|
|
1890
|
-
}
|
|
1891
|
-
try {
|
|
1892
|
-
const data = readFileSync(cacheFile, "utf8");
|
|
1893
|
-
const jsonData = yaml3.load(data);
|
|
1894
|
-
if (!version) {
|
|
1895
|
-
debug3("no midscene version info, will not read cache from file");
|
|
1896
|
-
return void 0;
|
|
1897
|
-
}
|
|
1898
|
-
if (semver.lt(jsonData.midsceneVersion, lowestSupportedMidsceneVersion) && !jsonData.midsceneVersion.includes("beta")) {
|
|
1899
|
-
console.warn(
|
|
1900
|
-
`You are using an old version of Midscene cache file, and we cannot match any info from it. Starting from Midscene v0.17, we changed our strategy to use xpath for cache info, providing better performance.
|
|
1901
|
-
Please delete the existing cache and rebuild it. Sorry for the inconvenience.
|
|
1902
|
-
cache file: ${cacheFile}`
|
|
1903
|
-
);
|
|
1904
|
-
return void 0;
|
|
1905
|
-
}
|
|
1906
|
-
debug3(
|
|
1907
|
-
"cache loaded from file, path: %s, cache version: %s, record length: %s",
|
|
1908
|
-
cacheFile,
|
|
1909
|
-
jsonData.midsceneVersion,
|
|
1910
|
-
jsonData.caches.length
|
|
1911
|
-
);
|
|
1912
|
-
jsonData.midsceneVersion = version;
|
|
1913
|
-
return jsonData;
|
|
1914
|
-
} catch (err) {
|
|
1915
|
-
debug3(
|
|
1916
|
-
"cache file exists but load failed, path: %s, error: %s",
|
|
1917
|
-
cacheFile,
|
|
1918
|
-
err
|
|
1919
|
-
);
|
|
1920
|
-
return void 0;
|
|
1921
|
-
}
|
|
1922
|
-
}
|
|
1923
|
-
flushCacheToFile() {
|
|
1924
|
-
if (!version) {
|
|
1925
|
-
debug3("no midscene version info, will not write cache to file");
|
|
1926
|
-
return;
|
|
1927
|
-
}
|
|
1928
|
-
if (!this.cacheFilePath) {
|
|
1929
|
-
debug3("no cache file path, will not write cache to file");
|
|
1930
|
-
return;
|
|
1931
|
-
}
|
|
1932
|
-
try {
|
|
1933
|
-
const dir = dirname2(this.cacheFilePath);
|
|
1934
|
-
if (!existsSync2(dir)) {
|
|
1935
|
-
mkdirSync2(dir, { recursive: true });
|
|
1936
|
-
debug3("created cache directory: %s", dir);
|
|
1937
|
-
}
|
|
1938
|
-
const yamlData = yaml3.dump(this.cache);
|
|
1939
|
-
writeFileSync2(this.cacheFilePath, yamlData);
|
|
1940
|
-
debug3("cache flushed to file: %s", this.cacheFilePath);
|
|
1941
|
-
} catch (err) {
|
|
1942
|
-
debug3(
|
|
1943
|
-
"write cache to file failed, path: %s, error: %s",
|
|
1944
|
-
this.cacheFilePath,
|
|
1945
|
-
err
|
|
1946
|
-
);
|
|
1947
|
-
}
|
|
1948
|
-
}
|
|
1949
|
-
updateOrAppendCacheRecord(newRecord, cachedRecord) {
|
|
1950
|
-
if (cachedRecord) {
|
|
1951
|
-
if (newRecord.type === "plan") {
|
|
1952
|
-
cachedRecord.updateFn((cache) => {
|
|
1953
|
-
cache.yamlWorkflow = newRecord.yamlWorkflow;
|
|
1954
|
-
});
|
|
1955
|
-
} else {
|
|
1956
|
-
cachedRecord.updateFn((cache) => {
|
|
1957
|
-
cache.xpaths = newRecord.xpaths;
|
|
1958
|
-
});
|
|
1959
|
-
}
|
|
1960
|
-
} else {
|
|
1961
|
-
this.appendCache(newRecord);
|
|
1962
|
-
}
|
|
1963
|
-
}
|
|
1964
|
-
};
|
|
1965
|
-
|
|
1966
2013
|
// src/common/agent.ts
|
|
1967
2014
|
var debug4 = getDebug4("web-integration");
|
|
1968
2015
|
var distanceOfTwoPoints = (p1, p2) => {
|
|
@@ -2091,10 +2138,12 @@ ${errorTask?.errorStack}`);
|
|
|
2091
2138
|
const prompt = opt.prompt ?? locatePrompt;
|
|
2092
2139
|
const deepThink = opt.deepThink ?? false;
|
|
2093
2140
|
const cacheable = opt.cacheable ?? true;
|
|
2141
|
+
const xpath = opt.xpath;
|
|
2094
2142
|
return {
|
|
2095
2143
|
prompt,
|
|
2096
2144
|
deepThink,
|
|
2097
|
-
cacheable
|
|
2145
|
+
cacheable,
|
|
2146
|
+
xpath
|
|
2098
2147
|
};
|
|
2099
2148
|
}
|
|
2100
2149
|
return {
|
|
@@ -2252,6 +2301,9 @@ ${errorTask?.errorStack}`);
|
|
|
2252
2301
|
this.afterTaskRunning(executor);
|
|
2253
2302
|
return output;
|
|
2254
2303
|
}
|
|
2304
|
+
async aiAsk(prompt, opt = defaultInsightExtractOption) {
|
|
2305
|
+
return this.aiString(prompt, opt);
|
|
2306
|
+
}
|
|
2255
2307
|
async describeElementAtPoint(center, opt) {
|
|
2256
2308
|
const { verifyPrompt = true, retryLimit = 3 } = opt || {};
|
|
2257
2309
|
let success = false;
|