misoai-web 1.0.6 → 1.5.7
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 -349
- package/dist/es/agent.js +165 -428
- package/dist/es/agent.js.map +1 -1
- package/dist/es/bridge-mode-browser.js +10 -9
- package/dist/es/bridge-mode-browser.js.map +1 -1
- package/dist/es/bridge-mode.js +167 -430
- package/dist/es/bridge-mode.js.map +1 -1
- package/dist/es/chrome-extension.js +173 -435
- package/dist/es/chrome-extension.js.map +1 -1
- package/dist/es/index.js +185 -432
- package/dist/es/index.js.map +1 -1
- package/dist/es/midscene-playground.js +165 -428
- package/dist/es/midscene-playground.js.map +1 -1
- package/dist/es/midscene-server.js.map +1 -1
- package/dist/es/playground.js +165 -428
- package/dist/es/playground.js.map +1 -1
- package/dist/es/playwright-report.js +1 -1
- package/dist/es/playwright-report.js.map +1 -1
- package/dist/es/playwright.js +182 -429
- package/dist/es/playwright.js.map +1 -1
- package/dist/es/puppeteer-agent-launcher.js +169 -432
- package/dist/es/puppeteer-agent-launcher.js.map +1 -1
- package/dist/es/puppeteer.js +169 -432
- package/dist/es/puppeteer.js.map +1 -1
- package/dist/es/ui-utils.js.map +1 -1
- package/dist/es/utils.js +7 -4
- package/dist/es/utils.js.map +1 -1
- package/dist/es/yaml.js +29 -3
- package/dist/es/yaml.js.map +1 -1
- package/dist/lib/agent.js +163 -426
- package/dist/lib/agent.js.map +1 -1
- package/dist/lib/bridge-mode-browser.js +10 -9
- package/dist/lib/bridge-mode-browser.js.map +1 -1
- package/dist/lib/bridge-mode.js +165 -428
- package/dist/lib/bridge-mode.js.map +1 -1
- package/dist/lib/chrome-extension.js +171 -433
- package/dist/lib/chrome-extension.js.map +1 -1
- package/dist/lib/index.js +183 -430
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/midscene-playground.js +163 -426
- package/dist/lib/midscene-playground.js.map +1 -1
- package/dist/lib/midscene-server.js.map +1 -1
- package/dist/lib/playground.js +163 -426
- package/dist/lib/playground.js.map +1 -1
- package/dist/lib/playwright-report.js +1 -1
- package/dist/lib/playwright-report.js.map +1 -1
- package/dist/lib/playwright.js +180 -427
- package/dist/lib/playwright.js.map +1 -1
- package/dist/lib/puppeteer-agent-launcher.js +167 -430
- package/dist/lib/puppeteer-agent-launcher.js.map +1 -1
- package/dist/lib/puppeteer.js +167 -430
- package/dist/lib/puppeteer.js.map +1 -1
- package/dist/lib/ui-utils.js.map +1 -1
- package/dist/lib/utils.js +7 -4
- package/dist/lib/utils.js.map +1 -1
- package/dist/lib/yaml.js +29 -3
- package/dist/lib/yaml.js.map +1 -1
- package/dist/types/agent.d.ts +13 -51
- package/dist/types/bridge-mode-browser.d.ts +2 -3
- package/dist/types/bridge-mode.d.ts +2 -3
- package/dist/types/{browser-aec1055d.d.ts → browser-9b472ffb.d.ts} +1 -1
- package/dist/types/chrome-extension.d.ts +2 -3
- package/dist/types/index.d.ts +1 -2
- package/dist/types/midscene-server.d.ts +1 -2
- package/dist/types/{page-86ab0fe1.d.ts → page-ed0ecb44.d.ts} +19 -9
- package/dist/types/playground.d.ts +2 -3
- package/dist/types/playwright.d.ts +9 -2
- package/dist/types/puppeteer-agent-launcher.d.ts +1 -2
- package/dist/types/puppeteer.d.ts +6 -5
- package/dist/types/ui-utils.d.ts +1 -1
- package/dist/types/utils.d.ts +1 -2
- package/dist/types/yaml.d.ts +1 -2
- package/iife-script/htmlElement.js +53 -75
- package/iife-script/htmlElementDebug.js +35 -56
- package/package.json +24 -24
- package/LICENSE +0 -21
@@ -22,10 +22,11 @@ var ScriptPlayer = class {
|
|
22
22
|
this.unnamedResultIndex = 0;
|
23
23
|
this.pageAgent = null;
|
24
24
|
this.result = {};
|
25
|
+
const target = script.target || script.web || script.android;
|
25
26
|
if (ifInBrowser) {
|
26
27
|
this.output = void 0;
|
27
|
-
} else if (
|
28
|
-
this.output = resolve(process.cwd(),
|
28
|
+
} else if (target?.output) {
|
29
|
+
this.output = resolve(process.cwd(), target.output);
|
29
30
|
} else {
|
30
31
|
this.output = join(getMidsceneRunSubDir("output"), `${process.pid}.json`);
|
31
32
|
}
|
@@ -99,15 +100,20 @@ var ScriptPlayer = class {
|
|
99
100
|
} else if ("aiAssert" in flowItem) {
|
100
101
|
const assertTask = flowItem;
|
101
102
|
const prompt = assertTask.aiAssert;
|
103
|
+
const msg = assertTask.errorMessage;
|
102
104
|
assert(prompt, "missing prompt for aiAssert");
|
103
105
|
assert(
|
104
106
|
typeof prompt === "string",
|
105
107
|
"prompt for aiAssert must be a string"
|
106
108
|
);
|
107
|
-
await agent.aiAssert(prompt);
|
109
|
+
await agent.aiAssert(prompt, msg);
|
108
110
|
} else if ("aiQuery" in flowItem) {
|
109
111
|
const queryTask = flowItem;
|
110
112
|
const prompt = queryTask.aiQuery;
|
113
|
+
const options = {
|
114
|
+
domIncluded: queryTask.domIncluded,
|
115
|
+
screenshotIncluded: queryTask.screenshotIncluded
|
116
|
+
};
|
111
117
|
assert(prompt, "missing prompt for aiQuery");
|
112
118
|
assert(
|
113
119
|
typeof prompt === "string",
|
@@ -118,6 +124,10 @@ var ScriptPlayer = class {
|
|
118
124
|
} else if ("aiNumber" in flowItem) {
|
119
125
|
const numberTask = flowItem;
|
120
126
|
const prompt = numberTask.aiNumber;
|
127
|
+
const options = {
|
128
|
+
domIncluded: numberTask.domIncluded,
|
129
|
+
screenshotIncluded: numberTask.screenshotIncluded
|
130
|
+
};
|
121
131
|
assert(prompt, "missing prompt for number");
|
122
132
|
assert(
|
123
133
|
typeof prompt === "string",
|
@@ -128,6 +138,10 @@ var ScriptPlayer = class {
|
|
128
138
|
} else if ("aiString" in flowItem) {
|
129
139
|
const stringTask = flowItem;
|
130
140
|
const prompt = stringTask.aiString;
|
141
|
+
const options = {
|
142
|
+
domIncluded: stringTask.domIncluded,
|
143
|
+
screenshotIncluded: stringTask.screenshotIncluded
|
144
|
+
};
|
131
145
|
assert(prompt, "missing prompt for string");
|
132
146
|
assert(
|
133
147
|
typeof prompt === "string",
|
@@ -138,6 +152,10 @@ var ScriptPlayer = class {
|
|
138
152
|
} else if ("aiBoolean" in flowItem) {
|
139
153
|
const booleanTask = flowItem;
|
140
154
|
const prompt = booleanTask.aiBoolean;
|
155
|
+
const options = {
|
156
|
+
domIncluded: booleanTask.domIncluded,
|
157
|
+
screenshotIncluded: booleanTask.screenshotIncluded
|
158
|
+
};
|
141
159
|
assert(prompt, "missing prompt for boolean");
|
142
160
|
assert(
|
143
161
|
typeof prompt === "string",
|
@@ -180,6 +198,9 @@ var ScriptPlayer = class {
|
|
180
198
|
} else if ("aiTap" in flowItem) {
|
181
199
|
const tapTask = flowItem;
|
182
200
|
await agent.aiTap(tapTask.aiTap, tapTask);
|
201
|
+
} else if ("aiRightClick" in flowItem) {
|
202
|
+
const rightClickTask = flowItem;
|
203
|
+
await agent.aiRightClick(rightClickTask.aiRightClick, rightClickTask);
|
183
204
|
} else if ("aiHover" in flowItem) {
|
184
205
|
const hoverTask = flowItem;
|
185
206
|
await agent.aiHover(hoverTask.aiHover, hoverTask);
|
@@ -202,6 +223,11 @@ var ScriptPlayer = class {
|
|
202
223
|
evaluateJavaScriptTask.javascript
|
203
224
|
);
|
204
225
|
this.setResult(evaluateJavaScriptTask.name, result);
|
226
|
+
} else if ("logScreenshot" in flowItem) {
|
227
|
+
const logScreenshotTask = flowItem;
|
228
|
+
await agent.logScreenshot(logScreenshotTask.logScreenshot, {
|
229
|
+
content: logScreenshotTask.content || ""
|
230
|
+
});
|
205
231
|
} else {
|
206
232
|
throw new Error(`unknown flowItem: ${JSON.stringify(flowItem)}`);
|
207
233
|
}
|
@@ -470,7 +496,8 @@ var WebElementInfo = class {
|
|
470
496
|
id,
|
471
497
|
attributes,
|
472
498
|
indexId,
|
473
|
-
xpaths
|
499
|
+
xpaths,
|
500
|
+
isVisible
|
474
501
|
}) {
|
475
502
|
this.content = content;
|
476
503
|
this.rect = rect;
|
@@ -483,6 +510,7 @@ var WebElementInfo = class {
|
|
483
510
|
this.attributes = attributes;
|
484
511
|
this.indexId = indexId;
|
485
512
|
this.xpaths = xpaths;
|
513
|
+
this.isVisible = isVisible;
|
486
514
|
}
|
487
515
|
};
|
488
516
|
|
@@ -505,14 +533,15 @@ async function parseContextFromWebPage(page, _opt) {
|
|
505
533
|
})
|
506
534
|
]);
|
507
535
|
const webTree = traverseTree(tree, (elementInfo) => {
|
508
|
-
const { rect, id, content, attributes, locator, indexId } = elementInfo;
|
536
|
+
const { rect, id, content, attributes, locator, indexId, isVisible } = elementInfo;
|
509
537
|
return new WebElementInfo({
|
510
538
|
rect,
|
511
539
|
locator,
|
512
540
|
id,
|
513
541
|
content,
|
514
542
|
attributes,
|
515
|
-
indexId
|
543
|
+
indexId,
|
544
|
+
isVisible
|
516
545
|
});
|
517
546
|
});
|
518
547
|
assert3(screenshotBase64, "screenshotBase64 is required");
|
@@ -542,7 +571,7 @@ function printReportMsg(filepath) {
|
|
542
571
|
logMsg(`Midscene - report file updated: ${filepath}`);
|
543
572
|
}
|
544
573
|
function replaceIllegalPathCharsAndSpace(str) {
|
545
|
-
return str.replace(/[
|
574
|
+
return str.replace(/[:*?"<>| ]/g, "-");
|
546
575
|
}
|
547
576
|
function forceClosePopup(page, debug6) {
|
548
577
|
page.on("popup", async (popup) => {
|
@@ -856,10 +885,10 @@ var PageTaskExecutor = class {
|
|
856
885
|
if (!taskParam || !taskParam.value) {
|
857
886
|
return;
|
858
887
|
}
|
859
|
-
await this.page.keyboard.type(taskParam.value);
|
860
|
-
} else {
|
861
|
-
await this.page.keyboard.type(taskParam.value);
|
862
888
|
}
|
889
|
+
await this.page.keyboard.type(taskParam.value, {
|
890
|
+
autoDismissKeyboard: taskParam.autoDismissKeyboard
|
891
|
+
});
|
863
892
|
}
|
864
893
|
};
|
865
894
|
tasks.push(taskActionInput);
|
@@ -888,6 +917,22 @@ var PageTaskExecutor = class {
|
|
888
917
|
}
|
889
918
|
};
|
890
919
|
tasks.push(taskActionTap);
|
920
|
+
} else if (plan2.type === "RightClick") {
|
921
|
+
const taskActionRightClick = {
|
922
|
+
type: "Action",
|
923
|
+
subType: "RightClick",
|
924
|
+
thought: plan2.thought,
|
925
|
+
locate: plan2.locate,
|
926
|
+
executor: async (param, { element }) => {
|
927
|
+
assert4(element, "Element not found, cannot right click");
|
928
|
+
await this.page.mouse.click(
|
929
|
+
element.center[0],
|
930
|
+
element.center[1],
|
931
|
+
{ button: "right" }
|
932
|
+
);
|
933
|
+
}
|
934
|
+
};
|
935
|
+
tasks.push(taskActionRightClick);
|
891
936
|
} else if (plan2.type === "Drag") {
|
892
937
|
const taskActionDrag = {
|
893
938
|
type: "Action",
|
@@ -1416,7 +1461,7 @@ var PageTaskExecutor = class {
|
|
1416
1461
|
executor: taskExecutor
|
1417
1462
|
};
|
1418
1463
|
}
|
1419
|
-
async createTypeQueryTask(type, demand) {
|
1464
|
+
async createTypeQueryTask(type, demand, opt) {
|
1420
1465
|
const taskExecutor = new Executor(
|
1421
1466
|
taskTitleStr(
|
1422
1467
|
type,
|
@@ -1447,7 +1492,10 @@ var PageTaskExecutor = class {
|
|
1447
1492
|
result: `${type}, ${demand}`
|
1448
1493
|
};
|
1449
1494
|
}
|
1450
|
-
const { data, usage } = await this.insight.extract(
|
1495
|
+
const { data, usage } = await this.insight.extract(
|
1496
|
+
demandInput,
|
1497
|
+
opt
|
1498
|
+
);
|
1451
1499
|
let outputResult = data;
|
1452
1500
|
if (ifTypeRestricted) {
|
1453
1501
|
assert4(data?.result !== void 0, "No result in query data");
|
@@ -1467,17 +1515,17 @@ var PageTaskExecutor = class {
|
|
1467
1515
|
executor: taskExecutor
|
1468
1516
|
};
|
1469
1517
|
}
|
1470
|
-
async query(demand) {
|
1471
|
-
return this.createTypeQueryTask("Query", demand);
|
1518
|
+
async query(demand, opt) {
|
1519
|
+
return this.createTypeQueryTask("Query", demand, opt);
|
1472
1520
|
}
|
1473
|
-
async boolean(prompt) {
|
1474
|
-
return this.createTypeQueryTask("Boolean", prompt);
|
1521
|
+
async boolean(prompt, opt) {
|
1522
|
+
return this.createTypeQueryTask("Boolean", prompt, opt);
|
1475
1523
|
}
|
1476
|
-
async number(prompt) {
|
1477
|
-
return this.createTypeQueryTask("Number", prompt);
|
1524
|
+
async number(prompt, opt) {
|
1525
|
+
return this.createTypeQueryTask("Number", prompt, opt);
|
1478
1526
|
}
|
1479
|
-
async string(prompt) {
|
1480
|
-
return this.createTypeQueryTask("String", prompt);
|
1527
|
+
async string(prompt, opt) {
|
1528
|
+
return this.createTypeQueryTask("String", prompt, opt);
|
1481
1529
|
}
|
1482
1530
|
async assert(assertion) {
|
1483
1531
|
const description = `assert: ${assertion}`;
|
@@ -1613,7 +1661,7 @@ function buildPlans(type, locateParam, param) {
|
|
1613
1661
|
param: locateParam,
|
1614
1662
|
thought: ""
|
1615
1663
|
} : null;
|
1616
|
-
if (type === "Tap" || type === "Hover") {
|
1664
|
+
if (type === "Tap" || type === "Hover" || type === "RightClick") {
|
1617
1665
|
assert5(locateParam, `missing locate info for action "${type}"`);
|
1618
1666
|
assert5(locatePlan, `missing locate info for action "${type}"`);
|
1619
1667
|
const tapPlan = {
|
@@ -1684,8 +1732,8 @@ function buildPlans(type, locateParam, param) {
|
|
1684
1732
|
|
1685
1733
|
// src/common/task-cache.ts
|
1686
1734
|
import assert6 from "assert";
|
1687
|
-
import { existsSync as existsSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
|
1688
|
-
import { join as join2 } from "path";
|
1735
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
|
1736
|
+
import { dirname as dirname2, join as join2 } from "path";
|
1689
1737
|
import { getMidsceneRunSubDir as getMidsceneRunSubDir2 } from "misoai-shared/common";
|
1690
1738
|
import { getDebug as getDebug3 } from "misoai-shared/logger";
|
1691
1739
|
import { ifInBrowser as ifInBrowser2 } from "misoai-shared/utils";
|
@@ -1693,7 +1741,7 @@ import yaml3 from "js-yaml";
|
|
1693
1741
|
import semver from "semver";
|
1694
1742
|
|
1695
1743
|
// package.json
|
1696
|
-
var version = "1.0.
|
1744
|
+
var version = "1.0.3";
|
1697
1745
|
|
1698
1746
|
// src/common/task-cache.ts
|
1699
1747
|
var debug3 = getDebug3("cache");
|
@@ -1721,70 +1769,44 @@ var TaskCache = class {
|
|
1721
1769
|
this.cache = cacheContent;
|
1722
1770
|
this.cacheOriginalLength = this.cache.caches.length;
|
1723
1771
|
}
|
1724
|
-
matchCache(prompt, type
|
1725
|
-
const contextHash = contextData ? this.generateContextHash(contextData) : void 0;
|
1772
|
+
matchCache(prompt, type) {
|
1726
1773
|
for (let i = 0; i < this.cacheOriginalLength; i++) {
|
1727
1774
|
const item = this.cache.caches[i];
|
1728
1775
|
const key = `${type}:${prompt}:${i}`;
|
1729
|
-
if (item.type
|
1730
|
-
|
1731
|
-
|
1732
|
-
|
1733
|
-
|
1734
|
-
|
1735
|
-
|
1736
|
-
|
1737
|
-
|
1776
|
+
if (item.type === type && item.prompt === prompt && !this.matchedCacheIndices.has(key)) {
|
1777
|
+
this.matchedCacheIndices.add(key);
|
1778
|
+
debug3(
|
1779
|
+
"cache found and marked as used, type: %s, prompt: %s, index: %d",
|
1780
|
+
type,
|
1781
|
+
prompt,
|
1782
|
+
i
|
1783
|
+
);
|
1784
|
+
return {
|
1785
|
+
cacheContent: item,
|
1786
|
+
updateFn: (cb) => {
|
1787
|
+
debug3(
|
1788
|
+
"will call updateFn to update cache, type: %s, prompt: %s, index: %d",
|
1789
|
+
type,
|
1790
|
+
prompt,
|
1791
|
+
i
|
1792
|
+
);
|
1793
|
+
cb(item);
|
1794
|
+
debug3(
|
1795
|
+
"cache updated, will flush to file, type: %s, prompt: %s, index: %d",
|
1796
|
+
type,
|
1797
|
+
prompt,
|
1798
|
+
i
|
1799
|
+
);
|
1800
|
+
this.flushCacheToFile();
|
1738
1801
|
}
|
1739
|
-
}
|
1740
|
-
debug3("cache context availability mismatch, type: %s, prompt: %s, index: %d", type, prompt, i);
|
1741
|
-
continue;
|
1742
|
-
}
|
1802
|
+
};
|
1743
1803
|
}
|
1744
|
-
this.matchedCacheIndices.add(key);
|
1745
|
-
debug3(
|
1746
|
-
"cache found and marked as used, type: %s, prompt: %s, index: %d, contextMatch: %s",
|
1747
|
-
type,
|
1748
|
-
prompt,
|
1749
|
-
i,
|
1750
|
-
contextHash ? "yes" : "no-context"
|
1751
|
-
);
|
1752
|
-
return {
|
1753
|
-
cacheContent: item,
|
1754
|
-
updateFn: (cb) => {
|
1755
|
-
debug3(
|
1756
|
-
"will call updateFn to update cache, type: %s, prompt: %s, index: %d",
|
1757
|
-
type,
|
1758
|
-
prompt,
|
1759
|
-
i
|
1760
|
-
);
|
1761
|
-
cb(item);
|
1762
|
-
debug3(
|
1763
|
-
"cache updated, will flush to file, type: %s, prompt: %s, index: %d",
|
1764
|
-
type,
|
1765
|
-
prompt,
|
1766
|
-
i
|
1767
|
-
);
|
1768
|
-
this.flushCacheToFile();
|
1769
|
-
}
|
1770
|
-
};
|
1771
1804
|
}
|
1772
|
-
debug3("no unused cache found, type: %s, prompt: %s
|
1805
|
+
debug3("no unused cache found, type: %s, prompt: %s", type, prompt);
|
1773
1806
|
return void 0;
|
1774
1807
|
}
|
1775
|
-
|
1776
|
-
|
1777
|
-
const stableString = sortedKeys.map((key) => `${key}:${JSON.stringify(contextData[key])}`).join("|");
|
1778
|
-
let hash = 0;
|
1779
|
-
for (let i = 0; i < stableString.length; i++) {
|
1780
|
-
const char = stableString.charCodeAt(i);
|
1781
|
-
hash = (hash << 5) - hash + char;
|
1782
|
-
hash = hash & hash;
|
1783
|
-
}
|
1784
|
-
return hash.toString(36);
|
1785
|
-
}
|
1786
|
-
matchPlanCache(prompt, contextData) {
|
1787
|
-
return this.matchCache(prompt, "plan", contextData);
|
1808
|
+
matchPlanCache(prompt) {
|
1809
|
+
return this.matchCache(prompt, "plan");
|
1788
1810
|
}
|
1789
1811
|
matchLocateCache(prompt) {
|
1790
1812
|
return this.matchCache(prompt, "locate");
|
@@ -1850,8 +1872,14 @@ cache file: ${cacheFile}`
|
|
1850
1872
|
return;
|
1851
1873
|
}
|
1852
1874
|
try {
|
1875
|
+
const dir = dirname2(this.cacheFilePath);
|
1876
|
+
if (!existsSync2(dir)) {
|
1877
|
+
mkdirSync2(dir, { recursive: true });
|
1878
|
+
debug3("created cache directory: %s", dir);
|
1879
|
+
}
|
1853
1880
|
const yamlData = yaml3.dump(this.cache);
|
1854
1881
|
writeFileSync2(this.cacheFilePath, yamlData);
|
1882
|
+
debug3("cache flushed to file: %s", this.cacheFilePath);
|
1855
1883
|
} catch (err) {
|
1856
1884
|
debug3(
|
1857
1885
|
"write cache to file failed, path: %s, error: %s",
|
@@ -1860,16 +1888,11 @@ cache file: ${cacheFile}`
|
|
1860
1888
|
);
|
1861
1889
|
}
|
1862
1890
|
}
|
1863
|
-
updateOrAppendCacheRecord(newRecord, cachedRecord
|
1891
|
+
updateOrAppendCacheRecord(newRecord, cachedRecord) {
|
1864
1892
|
if (cachedRecord) {
|
1865
1893
|
if (newRecord.type === "plan") {
|
1866
1894
|
cachedRecord.updateFn((cache) => {
|
1867
|
-
|
1868
|
-
planCache.yamlWorkflow = newRecord.yamlWorkflow;
|
1869
|
-
if (contextData) {
|
1870
|
-
planCache.contextHash = this.generateContextHash(contextData);
|
1871
|
-
planCache.contextData = { ...contextData };
|
1872
|
-
}
|
1895
|
+
cache.yamlWorkflow = newRecord.yamlWorkflow;
|
1873
1896
|
});
|
1874
1897
|
} else {
|
1875
1898
|
cachedRecord.updateFn((cache) => {
|
@@ -1877,11 +1900,6 @@ cache file: ${cacheFile}`
|
|
1877
1900
|
});
|
1878
1901
|
}
|
1879
1902
|
} else {
|
1880
|
-
if (newRecord.type === "plan" && contextData) {
|
1881
|
-
const planRecord = newRecord;
|
1882
|
-
planRecord.contextHash = this.generateContextHash(contextData);
|
1883
|
-
planRecord.contextData = { ...contextData };
|
1884
|
-
}
|
1885
1903
|
this.appendCache(newRecord);
|
1886
1904
|
}
|
1887
1905
|
}
|
@@ -1911,13 +1929,10 @@ var PageAgent = class {
|
|
1911
1929
|
generateReport: true,
|
1912
1930
|
autoPrintReportMsg: true,
|
1913
1931
|
groupName: "Midscene Report",
|
1914
|
-
groupDescription: ""
|
1915
|
-
enableCumulativeContext: true,
|
1916
|
-
autoClearContext: false
|
1932
|
+
groupDescription: ""
|
1917
1933
|
},
|
1918
1934
|
opts || {}
|
1919
1935
|
);
|
1920
|
-
this.initializeContextStore();
|
1921
1936
|
if (this.page.pageType === "puppeteer" || this.page.pageType === "playwright") {
|
1922
1937
|
this.page.waitForNavigationTimeout = this.opts.waitForNavigationTimeout || DEFAULT_WAIT_FOR_NAVIGATION_TIMEOUT;
|
1923
1938
|
this.page.waitForNetworkIdleTimeout = this.opts.waitForNetworkIdleTimeout || DEFAULT_WAIT_FOR_NETWORK_IDLE_TIMEOUT;
|
@@ -1944,69 +1959,6 @@ var PageAgent = class {
|
|
1944
1959
|
opts?.testId || this.page.pageType || "web"
|
1945
1960
|
);
|
1946
1961
|
}
|
1947
|
-
/**
|
1948
|
-
* Initialize context store for cumulative context functionality
|
1949
|
-
*/
|
1950
|
-
async initializeContextStore() {
|
1951
|
-
if (!this.opts.enableCumulativeContext) {
|
1952
|
-
debug4("Cumulative context disabled via options");
|
1953
|
-
return;
|
1954
|
-
}
|
1955
|
-
try {
|
1956
|
-
const aiModel = await import("misoai-core/ai-model");
|
1957
|
-
this.contextStore = aiModel.getContextStore();
|
1958
|
-
debug4("Context store initialized successfully", {
|
1959
|
-
autoClearContext: this.opts.autoClearContext,
|
1960
|
-
testId: this.opts.testId
|
1961
|
-
});
|
1962
|
-
if (this.opts.autoClearContext) {
|
1963
|
-
this.contextStore.clear();
|
1964
|
-
debug4("Context store cleared due to autoClearContext option");
|
1965
|
-
} else {
|
1966
|
-
const existingData = this.contextStore.getAllData();
|
1967
|
-
const existingSteps = this.contextStore.getRecentSteps(100).length;
|
1968
|
-
debug4("Context store preserving existing data", {
|
1969
|
-
existingDataKeys: Object.keys(existingData),
|
1970
|
-
existingStepsCount: existingSteps
|
1971
|
-
});
|
1972
|
-
}
|
1973
|
-
} catch (error) {
|
1974
|
-
debug4("Failed to initialize context store:", error);
|
1975
|
-
console.warn("⚠️ Could not initialize context store:", error);
|
1976
|
-
}
|
1977
|
-
}
|
1978
|
-
/**
|
1979
|
-
* Get the context store instance
|
1980
|
-
*/
|
1981
|
-
getContextStore() {
|
1982
|
-
return this.contextStore;
|
1983
|
-
}
|
1984
|
-
/**
|
1985
|
-
* Clear the context store
|
1986
|
-
*/
|
1987
|
-
clearContext() {
|
1988
|
-
if (this.contextStore) {
|
1989
|
-
this.contextStore.clear();
|
1990
|
-
}
|
1991
|
-
}
|
1992
|
-
/**
|
1993
|
-
* Get all stored data from context store
|
1994
|
-
*/
|
1995
|
-
getStoredData() {
|
1996
|
-
if (this.contextStore) {
|
1997
|
-
return this.contextStore.getAllData();
|
1998
|
-
}
|
1999
|
-
return {};
|
2000
|
-
}
|
2001
|
-
/**
|
2002
|
-
* Get step summary from context store
|
2003
|
-
*/
|
2004
|
-
getStepSummary() {
|
2005
|
-
if (this.contextStore) {
|
2006
|
-
return this.contextStore.getStepSummary();
|
2007
|
-
}
|
2008
|
-
return "";
|
2009
|
-
}
|
2010
1962
|
async getUIContext(action) {
|
2011
1963
|
if (action && (action === "extract" || action === "assert" || action === "captcha")) {
|
2012
1964
|
return await parseContextFromWebPage(this.page, {
|
@@ -2185,6 +2137,23 @@ var PageAgent = class {
|
|
2185
2137
|
metadata
|
2186
2138
|
};
|
2187
2139
|
}
|
2140
|
+
async aiRightClick(locatePrompt, opt) {
|
2141
|
+
const detailedLocateParam = this.buildDetailedLocateParam(
|
2142
|
+
locatePrompt,
|
2143
|
+
opt
|
2144
|
+
);
|
2145
|
+
const plans = buildPlans("RightClick", detailedLocateParam);
|
2146
|
+
const { executor, output } = await this.taskExecutor.runPlans(
|
2147
|
+
taskTitleStr("RightClick", locateParamStr(detailedLocateParam)),
|
2148
|
+
plans,
|
2149
|
+
{ cacheable: opt?.cacheable }
|
2150
|
+
);
|
2151
|
+
const metadata = this.afterTaskRunning(executor);
|
2152
|
+
return {
|
2153
|
+
result: output,
|
2154
|
+
metadata
|
2155
|
+
};
|
2156
|
+
}
|
2188
2157
|
async aiInput(value, locatePrompt, opt) {
|
2189
2158
|
assert7(
|
2190
2159
|
typeof value === "string",
|
@@ -2242,35 +2211,9 @@ var PageAgent = class {
|
|
2242
2211
|
};
|
2243
2212
|
}
|
2244
2213
|
async aiAction(taskPrompt, opt) {
|
2245
|
-
const originalPrompt = taskPrompt;
|
2246
|
-
let processedPrompt = taskPrompt;
|
2247
|
-
if (this.opts.enableCumulativeContext && this.contextStore) {
|
2248
|
-
try {
|
2249
|
-
const storedData = this.contextStore.getAllData();
|
2250
|
-
if (Object.keys(storedData).length > 0) {
|
2251
|
-
debug4("Available data for aiAction:", {
|
2252
|
-
prompt: taskPrompt,
|
2253
|
-
availableData: storedData
|
2254
|
-
});
|
2255
|
-
}
|
2256
|
-
} catch (error) {
|
2257
|
-
debug4("Context store operation failed:", error);
|
2258
|
-
}
|
2259
|
-
}
|
2260
2214
|
const cacheable = opt?.cacheable;
|
2261
2215
|
const isVlmUiTars = vlLocateMode() === "vlm-ui-tars";
|
2262
|
-
|
2263
|
-
if (this.opts.enableCumulativeContext && this.contextStore) {
|
2264
|
-
try {
|
2265
|
-
contextData = this.contextStore.getAllData();
|
2266
|
-
if (contextData && Object.keys(contextData).length === 0) {
|
2267
|
-
contextData = void 0;
|
2268
|
-
}
|
2269
|
-
} catch (error) {
|
2270
|
-
debug4("Failed to get context data for cache:", error);
|
2271
|
-
}
|
2272
|
-
}
|
2273
|
-
const matchedCache = isVlmUiTars || cacheable === false ? void 0 : this.taskCache?.matchPlanCache(taskPrompt, contextData);
|
2216
|
+
const matchedCache = isVlmUiTars || cacheable === false ? void 0 : this.taskCache?.matchPlanCache(taskPrompt);
|
2274
2217
|
if (matchedCache && this.taskCache?.isCacheResultUsed) {
|
2275
2218
|
const { executor: executor2 } = await this.taskExecutor.loadYamlFlowAsPlanning(
|
2276
2219
|
taskPrompt,
|
@@ -2280,28 +2223,6 @@ var PageAgent = class {
|
|
2280
2223
|
debug4("matched cache, will call .runYaml to run the action");
|
2281
2224
|
const yaml5 = matchedCache.cacheContent?.yamlWorkflow;
|
2282
2225
|
const result = await this.runYaml(yaml5);
|
2283
|
-
if (this.opts.enableCumulativeContext && this.contextStore) {
|
2284
|
-
try {
|
2285
|
-
const executionResult = {
|
2286
|
-
success: true,
|
2287
|
-
actionType: "cached",
|
2288
|
-
description: `Executed cached action: ${processedPrompt}`,
|
2289
|
-
timing: result.metadata?.totalTime
|
2290
|
-
};
|
2291
|
-
this.contextStore.addStep({
|
2292
|
-
type: "action",
|
2293
|
-
summary: `Action: ${processedPrompt} (cached)`,
|
2294
|
-
prompt: processedPrompt,
|
2295
|
-
executionResult
|
2296
|
-
});
|
2297
|
-
debug4("Added cached action step to context store:", {
|
2298
|
-
stepNumber: this.contextStore.getRecentSteps(1)[0]?.stepNumber,
|
2299
|
-
totalSteps: this.contextStore.getRecentSteps(100).length
|
2300
|
-
});
|
2301
|
-
} catch (error) {
|
2302
|
-
debug4("Failed to add cached action step:", error);
|
2303
|
-
}
|
2304
|
-
}
|
2305
2226
|
return {
|
2306
2227
|
result: result.result,
|
2307
2228
|
metadata: metadata2
|
@@ -2326,114 +2247,17 @@ var PageAgent = class {
|
|
2326
2247
|
prompt: taskPrompt,
|
2327
2248
|
yamlWorkflow: yamlFlowStr
|
2328
2249
|
},
|
2329
|
-
matchedCache
|
2330
|
-
contextData
|
2331
|
-
// Pass context data for cache creation
|
2250
|
+
matchedCache
|
2332
2251
|
);
|
2333
2252
|
}
|
2334
2253
|
const metadata = this.afterTaskRunning(executor);
|
2335
|
-
if (this.opts.enableCumulativeContext && this.contextStore) {
|
2336
|
-
try {
|
2337
|
-
const executionResult = this.analyzeExecutionResults(executor, originalPrompt);
|
2338
|
-
this.contextStore.addStep({
|
2339
|
-
type: "action",
|
2340
|
-
summary: `Action: ${processedPrompt}`,
|
2341
|
-
prompt: processedPrompt,
|
2342
|
-
executionResult
|
2343
|
-
});
|
2344
|
-
debug4("Added action step with execution result to context store:", {
|
2345
|
-
stepNumber: this.contextStore.getRecentSteps(1)[0]?.stepNumber,
|
2346
|
-
totalSteps: this.contextStore.getRecentSteps(100).length,
|
2347
|
-
executionResult
|
2348
|
-
});
|
2349
|
-
} catch (error) {
|
2350
|
-
debug4("Failed to analyze execution results, adding step without execution result:", error);
|
2351
|
-
try {
|
2352
|
-
this.contextStore.addStep({
|
2353
|
-
type: "action",
|
2354
|
-
summary: `Action: ${processedPrompt}`,
|
2355
|
-
prompt: processedPrompt
|
2356
|
-
});
|
2357
|
-
} catch (stepError) {
|
2358
|
-
debug4("Failed to add action step:", stepError);
|
2359
|
-
}
|
2360
|
-
}
|
2361
|
-
}
|
2362
2254
|
return {
|
2363
2255
|
result: output,
|
2364
2256
|
metadata
|
2365
2257
|
};
|
2366
2258
|
}
|
2367
2259
|
async aiQuery(demand) {
|
2368
|
-
|
2369
|
-
let storageKey;
|
2370
|
-
try {
|
2371
|
-
const aiModel = await import("misoai-core/ai-model");
|
2372
|
-
const contextStore = aiModel.getContextStore();
|
2373
|
-
if (typeof demand === "string") {
|
2374
|
-
const storageInstruction = contextStore.parseStorageInstruction(demand);
|
2375
|
-
if (storageInstruction) {
|
2376
|
-
storageKey = storageInstruction.key;
|
2377
|
-
processedDemand = storageInstruction.cleanText;
|
2378
|
-
contextStore._pendingAliases = storageInstruction.aliases;
|
2379
|
-
} else {
|
2380
|
-
const storageMatch = demand.match(/store\s+(?:as\s+)?(\w+)/i);
|
2381
|
-
if (storageMatch) {
|
2382
|
-
storageKey = storageMatch[1];
|
2383
|
-
processedDemand = demand.replace(/,?\s*store\s+(?:as\s+)?\w+/i, "").trim();
|
2384
|
-
}
|
2385
|
-
}
|
2386
|
-
}
|
2387
|
-
} catch (error) {
|
2388
|
-
debug4("Context store not available:", error);
|
2389
|
-
}
|
2390
|
-
const { output, executor } = await this.taskExecutor.query(processedDemand);
|
2391
|
-
if (this.opts.enableCumulativeContext && this.contextStore) {
|
2392
|
-
if (storageKey && output) {
|
2393
|
-
try {
|
2394
|
-
const pendingAliases = this.contextStore._pendingAliases;
|
2395
|
-
if (pendingAliases) {
|
2396
|
-
this.contextStore.storeDataWithAliases(storageKey, output, pendingAliases, typeof processedDemand === "string" ? processedDemand : JSON.stringify(processedDemand));
|
2397
|
-
delete this.contextStore._pendingAliases;
|
2398
|
-
debug4("Stored query result with aliases:", {
|
2399
|
-
key: storageKey,
|
2400
|
-
value: output,
|
2401
|
-
aliases: pendingAliases
|
2402
|
-
});
|
2403
|
-
} else {
|
2404
|
-
this.contextStore.storeData(storageKey, output);
|
2405
|
-
debug4("Stored query result:", {
|
2406
|
-
key: storageKey,
|
2407
|
-
value: output
|
2408
|
-
});
|
2409
|
-
}
|
2410
|
-
this.contextStore.addStep({
|
2411
|
-
type: "query",
|
2412
|
-
summary: `Query: ${typeof processedDemand === "string" ? processedDemand : JSON.stringify(processedDemand)} (stored as ${storageKey})`,
|
2413
|
-
data: output,
|
2414
|
-
prompt: typeof processedDemand === "string" ? processedDemand : JSON.stringify(processedDemand)
|
2415
|
-
});
|
2416
|
-
debug4("Added query step to context store:", {
|
2417
|
-
storageKey,
|
2418
|
-
totalStoredItems: Object.keys(this.contextStore.getAllData()).length,
|
2419
|
-
totalSteps: this.contextStore.getRecentSteps(100).length
|
2420
|
-
});
|
2421
|
-
} catch (error) {
|
2422
|
-
debug4("Failed to store query result:", error);
|
2423
|
-
}
|
2424
|
-
} else {
|
2425
|
-
try {
|
2426
|
-
this.contextStore.addStep({
|
2427
|
-
type: "query",
|
2428
|
-
summary: `Query: ${typeof processedDemand === "string" ? processedDemand : JSON.stringify(processedDemand)}`,
|
2429
|
-
data: output,
|
2430
|
-
prompt: typeof processedDemand === "string" ? processedDemand : JSON.stringify(processedDemand)
|
2431
|
-
});
|
2432
|
-
} catch (error) {
|
2433
|
-
debug4("Failed to add query step:", error);
|
2434
|
-
}
|
2435
|
-
}
|
2436
|
-
}
|
2260
|
+
const { output, executor } = await this.taskExecutor.query(demand);
|
2437
2261
|
const metadata = this.afterTaskRunning(executor);
|
2438
2262
|
return {
|
2439
2263
|
result: output,
|
@@ -2543,48 +2367,6 @@ var PageAgent = class {
|
|
2543
2367
|
};
|
2544
2368
|
}
|
2545
2369
|
async aiAssert(assertion, msg, opt) {
|
2546
|
-
let executionContext = "";
|
2547
|
-
if (this.opts.enableCumulativeContext && this.contextStore) {
|
2548
|
-
try {
|
2549
|
-
const recentSteps = this.contextStore.getRecentSteps(3);
|
2550
|
-
const stepsWithExecutionResults = recentSteps.filter((step) => step.executionResult);
|
2551
|
-
const storedData = this.contextStore.getAllData();
|
2552
|
-
if (stepsWithExecutionResults.length > 0) {
|
2553
|
-
const recentActions = stepsWithExecutionResults.map((step) => {
|
2554
|
-
const result = step.executionResult;
|
2555
|
-
return `- ${result.description}${result.success ? "" : " (FAILED)"}`;
|
2556
|
-
}).join("\n");
|
2557
|
-
executionContext = `
|
2558
|
-
|
2559
|
-
Recent actions performed:
|
2560
|
-
${recentActions}
|
2561
|
-
|
2562
|
-
This context may help verify the assertion.`;
|
2563
|
-
}
|
2564
|
-
if (storedData && Object.keys(storedData).length > 0) {
|
2565
|
-
executionContext += `
|
2566
|
-
|
2567
|
-
Available data for reference:
|
2568
|
-
${JSON.stringify(storedData, null, 2)}
|
2569
|
-
|
2570
|
-
Note: If the assertion references any data keys or natural language equivalents, consider the stored values when verifying.`;
|
2571
|
-
debug4("Available data for aiAssert:", {
|
2572
|
-
assertion,
|
2573
|
-
availableData: storedData
|
2574
|
-
});
|
2575
|
-
}
|
2576
|
-
this.contextStore.addStep({
|
2577
|
-
type: "assertion",
|
2578
|
-
summary: `Assertion: ${assertion}`,
|
2579
|
-
prompt: assertion
|
2580
|
-
});
|
2581
|
-
debug4("Added assertion step to context store:", {
|
2582
|
-
totalSteps: this.contextStore.getRecentSteps(100).length
|
2583
|
-
});
|
2584
|
-
} catch (error) {
|
2585
|
-
debug4("Context store operation failed:", error);
|
2586
|
-
}
|
2587
|
-
}
|
2588
2370
|
let currentUrl = "";
|
2589
2371
|
if (this.page.url) {
|
2590
2372
|
try {
|
@@ -2592,13 +2374,7 @@ Note: If the assertion references any data keys or natural language equivalents,
|
|
2592
2374
|
} catch (e) {
|
2593
2375
|
}
|
2594
2376
|
}
|
2595
|
-
|
2596
|
-
if (currentUrl) {
|
2597
|
-
assertionWithContext = `For the page at URL "${currentUrl}", ${assertion}`;
|
2598
|
-
}
|
2599
|
-
if (executionContext) {
|
2600
|
-
assertionWithContext += executionContext;
|
2601
|
-
}
|
2377
|
+
const assertionWithContext = currentUrl ? `For the page at URL "${currentUrl}", ${assertion}` : assertion;
|
2602
2378
|
const { output, executor } = await this.taskExecutor.assert(assertionWithContext);
|
2603
2379
|
const metadata = this.afterTaskRunning(executor, true);
|
2604
2380
|
if (output && opt?.keepRawResponse) {
|
@@ -2809,81 +2585,42 @@ ${errors}`);
|
|
2809
2585
|
}
|
2810
2586
|
throw new Error("evaluateJavaScript is not supported in current agent");
|
2811
2587
|
}
|
2812
|
-
async
|
2813
|
-
|
2814
|
-
|
2815
|
-
|
2816
|
-
|
2817
|
-
|
2818
|
-
|
2819
|
-
|
2820
|
-
|
2821
|
-
|
2822
|
-
|
2823
|
-
|
2824
|
-
|
2825
|
-
|
2826
|
-
|
2827
|
-
|
2828
|
-
|
2829
|
-
|
2830
|
-
|
2831
|
-
|
2832
|
-
|
2833
|
-
|
2834
|
-
|
2835
|
-
|
2836
|
-
|
2837
|
-
|
2838
|
-
|
2588
|
+
async logScreenshot(title, options) {
|
2589
|
+
const screenshotTitle = title || "untitled";
|
2590
|
+
const content = options?.content || "";
|
2591
|
+
const screenshot = await this.page.screenshotBase64?.();
|
2592
|
+
if (screenshot) {
|
2593
|
+
const executionDump = {
|
2594
|
+
name: screenshotTitle,
|
2595
|
+
description: content,
|
2596
|
+
tasks: [{
|
2597
|
+
type: "Screenshot",
|
2598
|
+
subType: "log",
|
2599
|
+
status: "finished",
|
2600
|
+
executor: null,
|
2601
|
+
param: {
|
2602
|
+
title: screenshotTitle,
|
2603
|
+
content
|
2604
|
+
},
|
2605
|
+
output: {
|
2606
|
+
screenshot
|
2607
|
+
},
|
2608
|
+
thought: `Logged screenshot: ${screenshotTitle}`,
|
2609
|
+
timing: {
|
2610
|
+
start: Date.now(),
|
2611
|
+
end: Date.now(),
|
2612
|
+
cost: 0
|
2613
|
+
}
|
2614
|
+
}],
|
2615
|
+
sdkVersion: "1.0.0",
|
2616
|
+
logTime: Date.now(),
|
2617
|
+
model_name: "screenshot"
|
2839
2618
|
};
|
2619
|
+
this.appendExecutionDump(executionDump);
|
2840
2620
|
}
|
2841
|
-
const actionType = lastAction.subType || "unknown";
|
2842
|
-
const elementInfo = this.extractElementInfo(lastLocate, lastAction);
|
2843
|
-
const description = this.generateActionDescription(actionType, lastAction.param, elementInfo);
|
2844
|
-
return {
|
2845
|
-
success: true,
|
2846
|
-
actionType,
|
2847
|
-
description,
|
2848
|
-
elementInfo,
|
2849
|
-
timing: lastAction.timing?.cost
|
2850
|
-
};
|
2851
|
-
}
|
2852
|
-
/**
|
2853
|
-
* Extract element information from locate task
|
2854
|
-
*/
|
2855
|
-
extractElementInfo(locateTask, _actionTask) {
|
2856
|
-
if (!locateTask?.output?.element)
|
2857
|
-
return void 0;
|
2858
|
-
const element = locateTask.output.element;
|
2859
|
-
return {
|
2860
|
-
type: element.attributes?.nodeType || "unknown",
|
2861
|
-
text: element.content || element.attributes?.placeholder || element.attributes?.title || "",
|
2862
|
-
location: `(${element.center[0]}, ${element.center[1]})`
|
2863
|
-
};
|
2864
2621
|
}
|
2865
|
-
|
2866
|
-
|
2867
|
-
*/
|
2868
|
-
generateActionDescription(actionType, param, elementInfo) {
|
2869
|
-
const elementDesc = elementInfo ? `'${elementInfo.text || elementInfo.type}' element` : "element";
|
2870
|
-
switch (actionType) {
|
2871
|
-
case "Tap":
|
2872
|
-
return `Clicked on ${elementDesc}`;
|
2873
|
-
case "Input":
|
2874
|
-
const inputValue = param?.value || "";
|
2875
|
-
return `Entered "${inputValue}" into ${elementDesc}`;
|
2876
|
-
case "KeyboardPress":
|
2877
|
-
return `Pressed ${param?.value || "key"}`;
|
2878
|
-
case "Scroll":
|
2879
|
-
return `Scrolled ${param?.direction || "on page"}`;
|
2880
|
-
case "Hover":
|
2881
|
-
return `Hovered over ${elementDesc}`;
|
2882
|
-
case "Drag":
|
2883
|
-
return `Dragged ${elementDesc}`;
|
2884
|
-
default:
|
2885
|
-
return `Performed ${actionType} action on ${elementDesc}`;
|
2886
|
-
}
|
2622
|
+
async destroy() {
|
2623
|
+
await this.page.destroy();
|
2887
2624
|
}
|
2888
2625
|
};
|
2889
2626
|
|
@@ -2914,7 +2651,7 @@ var Page = class {
|
|
2914
2651
|
this.everMoved = false;
|
2915
2652
|
this.underlyingPage = underlyingPage;
|
2916
2653
|
this.pageType = pageType;
|
2917
|
-
this.waitForNavigationTimeout = opts?.waitForNavigationTimeout
|
2654
|
+
this.waitForNavigationTimeout = opts?.waitForNavigationTimeout ?? DEFAULT_WAIT_FOR_NAVIGATION_TIMEOUT2;
|
2918
2655
|
}
|
2919
2656
|
async evaluate(pageFunction, arg) {
|
2920
2657
|
let result;
|
@@ -3194,9 +2931,9 @@ var WebPage = class extends Page {
|
|
3194
2931
|
}
|
3195
2932
|
async waitUntilNetworkIdle(options) {
|
3196
2933
|
await this.underlyingPage.waitForNetworkIdle({
|
3197
|
-
idleTime: options?.idleTime
|
3198
|
-
concurrency: options?.concurrency
|
3199
|
-
timeout: options?.timeout
|
2934
|
+
idleTime: options?.idleTime ?? DEFAULT_WAIT_FOR_NETWORK_IDLE_TIME,
|
2935
|
+
concurrency: options?.concurrency ?? DEFAULT_WAIT_FOR_NETWORK_IDLE_CONCURRENCY,
|
2936
|
+
timeout: options?.timeout ?? this.waitForNetworkIdleTimeout
|
3200
2937
|
});
|
3201
2938
|
}
|
3202
2939
|
};
|