From d0e1af63c2dba8b9b4c0724a27fe309b3fa948b4 Mon Sep 17 00:00:00 2001 From: yutao Date: Fri, 27 Dec 2024 11:07:28 +0800 Subject: [PATCH 01/40] feat: add bridge files --- .../src/component/playground-component.tsx | 9 ++-- packages/visualizer/src/extension/popup.tsx | 6 ++- .../src/chrome-extension/index.ts | 2 + .../src/chrome-extension/page.ts | 51 +------------------ 4 files changed, 10 insertions(+), 58 deletions(-) diff --git a/packages/visualizer/src/component/playground-component.tsx b/packages/visualizer/src/component/playground-component.tsx index c79e235d4..f63e896e9 100644 --- a/packages/visualizer/src/component/playground-component.tsx +++ b/packages/visualizer/src/component/playground-component.tsx @@ -162,14 +162,11 @@ const serverLaunchTip = ( ); // remember to destroy the agent when the tab is destroyed: agent.page.destroy() -export const extensionAgentForTabId = ( - tabId: number | null, - windowId: number | null, -) => { - if (!tabId || !windowId) { +export const extensionAgentForTabId = (tabId: number | null) => { + if (!tabId) { return null; } - const page = new ChromeExtensionProxyPage(tabId, windowId); + const page = new ChromeExtensionProxyPage(tabId); return new ChromeExtensionProxyPageAgent(page); }; diff --git a/packages/visualizer/src/extension/popup.tsx b/packages/visualizer/src/extension/popup.tsx index 8450be43a..8ccaf02f6 100644 --- a/packages/visualizer/src/extension/popup.tsx +++ b/packages/visualizer/src/extension/popup.tsx @@ -22,6 +22,7 @@ import { useChromeTabInfo } from '@/component/store'; import { SendOutlined } from '@ant-design/icons'; import type { ChromeExtensionProxyPageAgent } from '@midscene/web/chrome-extension'; import { useEffect, useState } from 'react'; +import Bridge from './bridge'; const shotAndOpenPlayground = async ( agent?: ChromeExtensionProxyPageAgent | null, @@ -58,7 +59,7 @@ function PlaygroundPopup() { } setLoading(true); try { - const agent = extensionAgentForTabId(tabId, windowId); + const agent = extensionAgentForTabId(tabId); await shotAndOpenPlayground(agent); await agent!.page.destroy(); } catch (e: any) { @@ -94,12 +95,13 @@ function PlaygroundPopup() {

+
{ - return extensionAgentForTabId(tabId, windowId); + return extensionAgentForTabId(tabId); }} showContextPreview={false} /> diff --git a/packages/web-integration/src/chrome-extension/index.ts b/packages/web-integration/src/chrome-extension/index.ts index bf7afe270..9ae6ece8b 100644 --- a/packages/web-integration/src/chrome-extension/index.ts +++ b/packages/web-integration/src/chrome-extension/index.ts @@ -1,9 +1,11 @@ import { ERROR_CODE_NOT_IMPLEMENTED_AS_DESIGNED } from '../common/utils'; import { ChromeExtensionProxyPageAgent } from './agent'; +import { ChromeExtensionBridgeServer } from './bridge'; import ChromeExtensionProxyPage from './page'; export { ChromeExtensionProxyPage, ChromeExtensionProxyPageAgent, + ChromeExtensionBridgeServer, ERROR_CODE_NOT_IMPLEMENTED_AS_DESIGNED, }; diff --git a/packages/web-integration/src/chrome-extension/page.ts b/packages/web-integration/src/chrome-extension/page.ts index ba73ddf06..b1a8279d3 100644 --- a/packages/web-integration/src/chrome-extension/page.ts +++ b/packages/web-integration/src/chrome-extension/page.ts @@ -27,49 +27,19 @@ const scriptFileContent = async () => { return fs.readFileSync(scriptFileToRetrieve, 'utf8'); }; -const lastTwoCallTime = [0, 0]; -const callInterval = 1050; -async function getScreenshotBase64FromWindowId(windowId: number) { - // check if this window is active - const activeWindow = await chrome.windows.getAll({ populate: true }); - if (activeWindow.find((w) => w.id === windowId) === undefined) { - throw new Error(`Window with id ${windowId} is not active`); - } - - // avoid MAX_CAPTURE_VISIBLE_TAB_CALLS_PER_SECOND - const now = Date.now(); - if (now - lastTwoCallTime[0] < callInterval) { - const sleepTime = callInterval - (now - lastTwoCallTime[0]); - console.warn( - `Sleep for ${sleepTime}ms to avoid too frequent screenshot calls`, - ); - await new Promise((resolve) => setTimeout(resolve, sleepTime)); - } - const base64 = await chrome.tabs.captureVisibleTab(windowId, { - format: 'jpeg', - quality: 70, - }); - lastTwoCallTime.shift(); - lastTwoCallTime.push(Date.now()); - return base64; -} - export default class ChromeExtensionProxyPage implements AbstractPage { pageType = 'chrome-extension-proxy'; private tabId: number; - private windowId: number; - private viewportSize?: Size; private debuggerAttached = false; private attachingDebugger: Promise | null = null; - constructor(tabId: number, windowId: number) { + constructor(tabId: number) { this.tabId = tabId; - this.windowId = windowId; } private async attachDebugger() { @@ -158,25 +128,6 @@ export default class ChromeExtensionProxyPage implements AbstractPage { return returnValue.result.value; } - // private async rectOfNodeId(nodeId: number): Promise { - // try { - // const { model } = - // await this.sendCommandToDebugger( - // 'DOM.getBoxModel', - // { nodeId }, - // ); - // return { - // left: model.border[0], - // top: model.border[1], - // width: model.border[2] - model.border[0], - // height: model.border[5] - model.border[1], - // }; - // } catch (error) { - // console.error('Error getting box model for nodeId', nodeId, error); - // return null; - // } - // } - async getElementInfos() { const content = await this.getPageContentByCDP(); if (content?.size) { From 15afbdc0d8b0ebc26f7d59e92d714caf8c5f86e0 Mon Sep 17 00:00:00 2001 From: yutao Date: Fri, 27 Dec 2024 11:08:40 +0800 Subject: [PATCH 02/40] feat: add bridge files --- .../src/chrome-extension/bridge.ts | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 packages/web-integration/src/chrome-extension/bridge.ts diff --git a/packages/web-integration/src/chrome-extension/bridge.ts b/packages/web-integration/src/chrome-extension/bridge.ts new file mode 100644 index 000000000..233d41979 --- /dev/null +++ b/packages/web-integration/src/chrome-extension/bridge.ts @@ -0,0 +1,64 @@ +import assert from 'node:assert'; +import type { AbstractPage } from '@/page'; +import ChromeExtensionProxyPage from './page'; + +interface ConnectedPage { + tabId: number; + page: ChromeExtensionProxyPage; + status: 'connected' | 'disconnected'; +} + +export class ChromeExtensionBridgeServer { + public connectedPages: Record = {}; + + constructor() { + this.connectedPages = {}; + } + + listen() {} + + async newTabWithUrl(url: string) { + assert(url, 'url is required'); + + // new tab + const tab = await chrome.tabs.create({ url }); + const tabId = tab.id; + assert(tabId, 'failed to get tabId after creating a new tab'); + + const page = new ChromeExtensionProxyPage(tabId); + this.connectedPages[tabId] = { + tabId, + page, + status: 'connected', + }; + return { + tabId, + page, + }; + } + + async call(tabId: number, method: string, ...args: any[]) { + assert(tabId, 'tabId is required'); + assert(this.connectedPages[tabId], 'tabId is not connected'); + return ( + this.connectedPages[tabId].page[ + method as keyof ChromeExtensionProxyPage + ] as any + )(...args); + } + + disconnect(tabId: number, closeTab = true) { + assert(tabId, 'tabId is required'); + assert(this.connectedPages[tabId], 'tabId is not connected'); + this.connectedPages[tabId].status = 'disconnected'; + if (closeTab) { + chrome.tabs.remove(tabId); + } + } +} + +// export class PageOverChromeExtensionBridge implements AbstractPage { +// pageType = 'page-over-chrome-extension-bridge'; + +// constructor() {} +// } From 1eb8e14176e9ae5c0bc6f1674d7626d3094a520a Mon Sep 17 00:00:00 2001 From: yutao Date: Fri, 27 Dec 2024 11:09:35 +0800 Subject: [PATCH 03/40] feat: add bridge files --- packages/visualizer/src/extension/bridge.tsx | 33 ++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 packages/visualizer/src/extension/bridge.tsx diff --git a/packages/visualizer/src/extension/bridge.tsx b/packages/visualizer/src/extension/bridge.tsx new file mode 100644 index 000000000..bcd1c9858 --- /dev/null +++ b/packages/visualizer/src/extension/bridge.tsx @@ -0,0 +1,33 @@ +import { ChromeExtensionBridgeServer } from '@midscene/web/chrome-extension'; +import { Button } from 'antd'; +import { useEffect, useState } from 'react'; + +export default function Bridge() { + const [bridge, setBridge] = useState( + null, + ); + useEffect(() => { + const bridge = new ChromeExtensionBridgeServer(); + bridge.listen(); + setBridge(bridge); + }, []); + + return ( +
+

Bridge List

+
+ {Object.entries(bridge?.connectedPages || {}).map(([tabId, page]) => ( +
{tabId}
+ ))} +
+ +
+ ); +} From 445b3489cbd96f3d0a25a29e21d64ec3c37f01c4 Mon Sep 17 00:00:00 2001 From: yutao Date: Sun, 29 Dec 2024 22:16:44 +0800 Subject: [PATCH 04/40] feat: bridge mode for local connect --- apps/site/docs/en/model-provider.md | 18 +- apps/site/docs/zh/model-provider.md | 9 +- .../midscene/src/ai-model/openai/index.ts | 3 + packages/visualizer/src/extension/bridge.tsx | 43 ++- packages/web-integration/modern.config.ts | 8 +- packages/web-integration/package.json | 7 +- .../src/chrome-extension/bridge-cli-side.ts | 59 ++++ .../src/chrome-extension/bridge-common.ts | 31 ++ .../src/chrome-extension/bridge-io-client.ts | 58 ++++ .../src/chrome-extension/bridge-io-server.ts | 129 ++++++++ .../src/chrome-extension/bridge-page.ts | 70 +++++ .../src/chrome-extension/bridge.ts | 64 ---- .../src/chrome-extension/index.ts | 6 +- .../src/chrome-extension/page.ts | 4 +- packages/web-integration/src/page.ts | 19 +- .../unit-test/extension/bridge-io.test.ts | 113 +++++++ pnpm-lock.yaml | 291 ++++++++++++++---- 17 files changed, 789 insertions(+), 143 deletions(-) create mode 100644 packages/web-integration/src/chrome-extension/bridge-cli-side.ts create mode 100644 packages/web-integration/src/chrome-extension/bridge-common.ts create mode 100644 packages/web-integration/src/chrome-extension/bridge-io-client.ts create mode 100644 packages/web-integration/src/chrome-extension/bridge-io-server.ts create mode 100644 packages/web-integration/src/chrome-extension/bridge-page.ts delete mode 100644 packages/web-integration/src/chrome-extension/bridge.ts create mode 100644 packages/web-integration/tests/unit-test/extension/bridge-io.test.ts diff --git a/apps/site/docs/en/model-provider.md b/apps/site/docs/en/model-provider.md index 9f9877b69..b1cb7842f 100644 --- a/apps/site/docs/en/model-provider.md +++ b/apps/site/docs/en/model-provider.md @@ -35,10 +35,17 @@ export OPENAI_MAX_TOKENS=2048 ## Using Azure OpenAI Service +Usually Azure OpenAI Service will provide you with endpoint (`AZURE_OPENAI_ENDPOINT`), deployment (`AZURE_OPENAI_DEPLOYMENT_NAME`) and apiVersion (`AZURE_OPENAI_API_VERSION`) parameters, you need to put them into `MIDSCENE_AZURE_OPENAI_INIT_CONFIG_JSON` as a valid JSON string. + ```bash +# this is always true when using Azure OpenAI Service export MIDSCENE_USE_AZURE_OPENAI=1 + +# this is the default scope export MIDSCENE_AZURE_OPENAI_SCOPE="https://cognitiveservices.azure.com/.default" -export MIDSCENE_AZURE_OPENAI_INIT_CONFIG_JSON='{"apiVersion": "2024-11-01-preview", "endpoint": "...", "deployment": "..."}' + +# replace the json with your own parameters +export MIDSCENE_AZURE_OPENAI_INIT_CONFIG_JSON='{"apiVersion": "2024-11-01-preview", "endpoint": "", "deployment": ""}' ``` ## Choose a model other than `gpt-4o` @@ -96,6 +103,15 @@ export OPENAI_API_KEY="..." export MIDSCENE_MODEL_NAME="ep-202....." ``` +## Example: config request headers (like for openrouter) + +```bash +export OPENAI_BASE_URL="https://openrouter.ai/api/v1" +export OPENAI_API_KEY="..." +export MIDSCENE_MODEL_NAME="..." +export MIDSCENE_OPENAI_INIT_CONFIG_JSON='{"defaultHeaders":{"HTTP-Referer":"...","X-Title":"..."}}' +``` + ## Troubleshooting LLM Service Connectivity Issues If you want to troubleshoot connectivity issues, you can use the 'connectivity-test' folder in our example project: [https://github.com/web-infra-dev/midscene-example/tree/main/connectivity-test](https://github.com/web-infra-dev/midscene-example/tree/main/connectivity-test) diff --git a/apps/site/docs/zh/model-provider.md b/apps/site/docs/zh/model-provider.md index bb131cae2..61a0a2f6a 100644 --- a/apps/site/docs/zh/model-provider.md +++ b/apps/site/docs/zh/model-provider.md @@ -32,10 +32,17 @@ export OPENAI_MAX_TOKENS=2048 ## 使用 Azure OpenAI 服务时的配置 +Azure 一般会提供给你 endpoint (AZURE_OPENAI_ENDPOINT), deployment (AZURE_OPENAI_DEPLOYMENT_NAME) , apiVersion 这些参数,你需要将它们合并配置在 `MIDSCENE_AZURE_OPENAI_INIT_CONFIG_JSON` 中,拼接为合法的 JSON 字符串。 + ```bash +# 使用 Azure OpenAI 服务时,配置为 1 export MIDSCENE_USE_AZURE_OPENAI=1 + +# Azure OpenAI 的 scope export MIDSCENE_AZURE_OPENAI_SCOPE="https://cognitiveservices.azure.com/.default" -export MIDSCENE_AZURE_OPENAI_INIT_CONFIG_JSON='{"apiVersion": "2024-11-01-preview", "endpoint": "...", "deployment": "..."}' + +# 把 JSON 中的参数替换为你的参数 +export MIDSCENE_AZURE_OPENAI_INIT_CONFIG_JSON='{"apiVersion": "2024-11-01-preview", "endpoint": "<此处替换为 AZURE_OPENAI_ENDPOINT>", "deployment": "<此处替换为 AZURE_OPENAI_DEPLOYMENT_NAME>"}' ``` ## 选用 `gpt-4o` 以外的其他模型 diff --git a/packages/midscene/src/ai-model/openai/index.ts b/packages/midscene/src/ai-model/openai/index.ts index b17aa301c..04e6852f1 100644 --- a/packages/midscene/src/ai-model/openai/index.ts +++ b/packages/midscene/src/ai-model/openai/index.ts @@ -102,6 +102,9 @@ async function createChatClient(): Promise<{ httpAgent: socksAgent, ...extraConfig, dangerouslyAllowBrowser: true, + defaultHeaders: { + Origin: 'https://midscenejs.com', + }, }); } diff --git a/packages/visualizer/src/extension/bridge.tsx b/packages/visualizer/src/extension/bridge.tsx index bcd1c9858..b121f9f4c 100644 --- a/packages/visualizer/src/extension/bridge.tsx +++ b/packages/visualizer/src/extension/bridge.tsx @@ -1,17 +1,45 @@ -import { ChromeExtensionBridgeServer } from '@midscene/web/chrome-extension'; +import { + ChromeExtensionPageBridgeSide, + getBridgePageInCliSide, +} from '@midscene/web/chrome-extension'; import { Button } from 'antd'; import { useEffect, useState } from 'react'; export default function Bridge() { - const [bridge, setBridge] = useState( + const [bridge, setBridge] = useState( null, ); + const [tabId, setTabId] = useState(null); useEffect(() => { - const bridge = new ChromeExtensionBridgeServer(); - bridge.listen(); + const bridge = new ChromeExtensionPageBridgeSide(); + bridge.connect(); setBridge(bridge); }, []); + const newTab = async () => { + if (!bridge) { + throw new Error('bridge is not initialized'); + } + const { tabId } = await bridge.connectNewTabWithUrl( + 'https://www.baidu.com', + ); + setTabId(tabId); + }; + + const doSomething = async () => { + if (!tabId) { + throw new Error('bridge is not initialized'); + } + + const proxy: any = getBridgePageInCliSide(tabId); + console.log('1'); + console.log(proxy.screenshotBase64()); + console.log('2'); + console.log(await proxy.mouse.click()); + // const page = + // await bridge.call(tabId, 'screenshotBase64'); + }; + return (

Bridge List

@@ -20,14 +48,15 @@ export default function Bridge() {
{tabId}
))}
+ +
); } diff --git a/packages/web-integration/modern.config.ts b/packages/web-integration/modern.config.ts index 8f4377de0..7b97a8978 100644 --- a/packages/web-integration/modern.config.ts +++ b/packages/web-integration/modern.config.ts @@ -20,6 +20,12 @@ export default defineConfig({ yaml: 'src/yaml/index.ts', }, target: 'es2018', - externals: ['@midscene/core', '@midscene/shared', 'puppeteer'], + externals: [ + '@midscene/core', + '@midscene/shared', + 'puppeteer', + 'bufferutil', + 'utf-8-validate', + ], }, }); diff --git a/packages/web-integration/package.json b/packages/web-integration/package.json index e36cf5888..5052e9b9e 100644 --- a/packages/web-integration/package.json +++ b/packages/web-integration/package.json @@ -71,23 +71,24 @@ "cors": "2.8.5", "express": "4.21.1", "inquirer": "10.1.5", - "openai": "4.57.1" + "openai": "4.57.1", + "socket.io": "4.8.1" }, "devDependencies": { - "@types/js-yaml": "4.0.9", - "js-yaml": "4.1.0", "@modern-js/module-tools": "2.60.6", "@playwright/test": "1.44.1", "@types/chrome": "0.0.279", "@types/cors": "2.8.12", "@types/express": "4.17.14", "@types/fs-extra": "11.0.4", + "@types/js-yaml": "4.0.9", "@types/node": "^18.0.0", "@wdio/types": "9.0.4", "devtools-protocol": "0.0.1380148", "dotenv": "16.4.5", "fs-extra": "11.2.0", "js-sha256": "0.11.0", + "js-yaml": "4.1.0", "playwright": "1.44.1", "puppeteer": "23.0.2", "typescript": "~5.0.4", diff --git a/packages/web-integration/src/chrome-extension/bridge-cli-side.ts b/packages/web-integration/src/chrome-extension/bridge-cli-side.ts new file mode 100644 index 000000000..6613e46b0 --- /dev/null +++ b/packages/web-integration/src/chrome-extension/bridge-cli-side.ts @@ -0,0 +1,59 @@ +import assert from 'node:assert'; +import type { KeyboardAction, MouseAction } from '@/page'; +import { DefaultBridgeServerPort } from './bridge-common'; +import { BridgeServer } from './bridge-io-server'; + +// TODO: handle the connection timeout +export const getBridgePageInCliSide = () => { + const server = new BridgeServer(DefaultBridgeServerPort); + + server.listen(); + const bridgeCaller = (method: string) => { + return async (...args: any[]) => { + const response = await server.call(method, args); + return response; + }; + }; + const page = {}; + // const page = { + // pendingCalls: [] as BridgeCall[], + // listen: () => { + // const io = new Server(DefaultBridgeServerPort); + // io.on('connection', (socket) => { + // socket.emit('bridge-connected', tabId); + // }); + // }, + // }; + return new Proxy(page, { + get(target, prop, receiver) { + assert(typeof prop === 'string', 'prop must be a string'); + + if (Object.keys(page).includes(prop)) { + return page[prop as keyof typeof page]; + } + + if (prop === 'pageType') { + return 'page-over-chrome-extension-bridge'; + } + + if (prop === 'mouse') { + const mouse: MouseAction = { + click: bridgeCaller('mouse.click'), + wheel: bridgeCaller('mouse.wheel'), + move: bridgeCaller('mouse.move'), + }; + return mouse; + } + + if (prop === 'keyboard') { + const keyboard: KeyboardAction = { + type: bridgeCaller('keyboard.type'), + press: bridgeCaller('keyboard.press'), + }; + return keyboard; + } + + return bridgeCaller(prop); + }, + }); +}; diff --git a/packages/web-integration/src/chrome-extension/bridge-common.ts b/packages/web-integration/src/chrome-extension/bridge-common.ts new file mode 100644 index 000000000..05215a51f --- /dev/null +++ b/packages/web-integration/src/chrome-extension/bridge-common.ts @@ -0,0 +1,31 @@ +export const DefaultBridgeServerPort = 3766; +export const DefaultLocalEndpoint = `http://127.0.0.1:${DefaultBridgeServerPort}`; +export const BridgeCallTimeout = 30000; +export const BridgeCallEvent = 'bridge-call'; +export const BridgeCallResponseEvent = 'bridge-call-response'; +export const BridgeMessageEvent = 'bridge-message'; +export const BridgeConnectedEvent = 'bridge-connected'; +export const BridgeRefusedEvent = 'bridge-refused'; +export const BridgeErrorCodeNoClientConnected = 'no-client-connected'; + +export interface BridgeCall { + method: string; + args: any[]; + response: any; + callTime: number; + responseTime: number; + callback: (response: any) => void; + error?: Error; +} + +export interface BridgeCallRequest { + id: number; + method: string; + args: any[]; +} + +export interface BridgeCallResponse { + id: number; + response: any; + error?: any; +} diff --git a/packages/web-integration/src/chrome-extension/bridge-io-client.ts b/packages/web-integration/src/chrome-extension/bridge-io-client.ts new file mode 100644 index 000000000..b300c0c4d --- /dev/null +++ b/packages/web-integration/src/chrome-extension/bridge-io-client.ts @@ -0,0 +1,58 @@ +import assert from 'node:assert'; +import { io as ClientIO, type Socket as ClientSocket } from 'socket.io-client'; +import { + BridgeCallEvent, + type BridgeCallRequest, + type BridgeCallResponse, + BridgeCallResponseEvent, + BridgeConnectedEvent, + BridgeRefusedEvent, +} from './bridge-common'; + +// ws client, this is where the request is processed +export class BridgeClient { + private socket: ClientSocket | null = null; + constructor( + public endpoint: string, + public onBridgeCall: (method: string, args: any[]) => Promise, + ) {} + + async connect() { + return new Promise((resolve, reject) => { + this.socket = ClientIO(this.endpoint); + + this.socket.on(BridgeConnectedEvent, () => { + console.log('bridge-connected'); + resolve(this.socket); + }); + this.socket.on(BridgeRefusedEvent, (e: any) => { + console.error('bridge-refused', e); + reject(new Error(e || 'bridge refused')); + }); + this.socket.on(BridgeCallEvent, (call: BridgeCallRequest) => { + const id = call.id; + assert(typeof id !== 'undefined', 'call id is required'); + Promise.resolve().then(async () => { + let response: any; + try { + response = await this.onBridgeCall(call.method, call.args); + } catch (e) { + return this.socket?.emit(BridgeCallResponseEvent, { + id, + error: e, + } as BridgeCallResponse); + } + this.socket?.emit(BridgeCallResponseEvent, { + id, + response, + } as BridgeCallResponse); + }); + }); + }); + } + + disconnect() { + this.socket?.disconnect(); + this.socket = null; + } +} diff --git a/packages/web-integration/src/chrome-extension/bridge-io-server.ts b/packages/web-integration/src/chrome-extension/bridge-io-server.ts new file mode 100644 index 000000000..5f68a04e0 --- /dev/null +++ b/packages/web-integration/src/chrome-extension/bridge-io-server.ts @@ -0,0 +1,129 @@ +import { Server, type Socket as ServerSocket } from 'socket.io'; +import { + type BridgeCall, + BridgeCallEvent, + type BridgeCallResponse, + BridgeCallResponseEvent, + BridgeCallTimeout, + BridgeConnectedEvent, + BridgeErrorCodeNoClientConnected, + BridgeRefusedEvent, +} from './bridge-common'; + +// ws server, this is where the request is sent +export class BridgeServer { + private callId = 0; + private io: Server | null = null; + private socket: ServerSocket | null = null; + public calls: Record = {}; + + constructor(public port: number) {} + + async listen(timeout = 30000): Promise { + return new Promise((resolve, reject) => { + const timeoutListener = setTimeout(() => { + reject( + new Error( + `no client connected after ${timeout}ms (${BridgeErrorCodeNoClientConnected})`, + ), + ); + }, timeout); + + this.io = new Server(this.port); + this.io.on('connection', (socket) => { + if (this.socket) { + console.log('server already connected, refusing new connection'); + socket.emit(BridgeRefusedEvent); + reject(new Error('server already connected by another client')); + } + try { + console.log('one client connected'); + this.socket = socket; + socket.emit(BridgeConnectedEvent); + + socket.on(BridgeCallResponseEvent, (params: BridgeCallResponse) => { + const id = params.id; + const response = params.response; + const call = this.calls[id]; + if (!call) { + throw new Error(`call ${id} not found`); + } + call.response = response; + call.responseTime = Date.now(); + + call.callback(response); + }); + + // flush all calls + for (const id in this.calls) { + if (this.calls[id].callTime === 0) { + this.emitCall(id); + } + } + + clearTimeout(timeoutListener); + resolve(); + } catch (e) { + console.error('failed to handle connection event', e); + reject(e); + } + }); + }); + } + + private async emitCall(id: string) { + const call = this.calls[id]; + if (!call) { + throw new Error(`call ${id} not found`); + } + + if (this.socket) { + this.socket.emit(BridgeCallEvent, { + id, + method: call.method, + args: call.args, + }); + call.callTime = Date.now(); + } + } + + async call( + method: string, + args: any[], + timeout = BridgeCallTimeout, + ): Promise { + const id = `${this.callId++}`; + + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + console.log( + `bridge call timeout, id=${id}, method=${method}, args=`, + args, + ); + this.calls[id].error = new Error( + `Bridge call timeout after ${timeout}ms: ${method}`, + ); + reject(this.calls[id].error); + }, timeout); + + this.calls[id] = { + method, + args, + response: null, + callTime: 0, + responseTime: 0, + callback: (response: any) => { + clearTimeout(timeoutId); + resolve(response); + }, + }; + + this.emitCall(id); + }); + } + + close() { + this.io?.close(); + this.io = null; + } +} diff --git a/packages/web-integration/src/chrome-extension/bridge-page.ts b/packages/web-integration/src/chrome-extension/bridge-page.ts new file mode 100644 index 000000000..8c40d4207 --- /dev/null +++ b/packages/web-integration/src/chrome-extension/bridge-page.ts @@ -0,0 +1,70 @@ +import assert from 'node:assert'; +import { DefaultBridgeServerPort } from './bridge-common'; +import { BridgeClient } from './bridge-io-client'; +import ChromeExtensionProxyPage from './page'; + +export class ChromeExtensionPageBridgeSide extends ChromeExtensionProxyPage { + public bridgeClient: BridgeClient | null = null; + + constructor() { + super(0); + } + + private async setupBridgeClient() { + this.bridgeClient = new BridgeClient( + `ws://localhost:${DefaultBridgeServerPort}`, + (method, args: any[]) => { + if (method === 'newTabWithUrl') { + return this.connectNewTabWithUrl.apply( + this, + args as unknown as [string], + ); + } + + if (!this.tabId || this.tabId === 0) { + throw new Error('no tab is connected'); + } + + // @ts-expect-error + return this[method as keyof ChromeExtensionProxyPage](...args); + }, + ); + await this.bridgeClient.connect(); + } + + public async connect(timeout = 30 * 1000) { + const startTime = Date.now(); + while (Date.now() - startTime < timeout) { + try { + await this.setupBridgeClient(); + return; + } catch (e) { + console.error('failed to connect to bridge server', e); + } + // wait for 300ms before retrying + await new Promise((resolve) => setTimeout(resolve, 300)); + } + throw new Error(`failed to connect to bridge server after ${timeout}ms`); + } + + public async connectNewTabWithUrl(url: string) { + assert(url, 'url is required to create a new tab'); + if (this.tabId) { + throw new Error('tab is already connected'); + } + + // new tab + const tab = await chrome.tabs.create({ url }); + const tabId = tab.id; + assert(tabId, 'failed to get tabId after creating a new tab'); + + this.tabId = tabId; + } + + disconnect() { + if (this.bridgeClient) { + this.bridgeClient.disconnect(); + } + this.tabId = 0; + } +} diff --git a/packages/web-integration/src/chrome-extension/bridge.ts b/packages/web-integration/src/chrome-extension/bridge.ts deleted file mode 100644 index 233d41979..000000000 --- a/packages/web-integration/src/chrome-extension/bridge.ts +++ /dev/null @@ -1,64 +0,0 @@ -import assert from 'node:assert'; -import type { AbstractPage } from '@/page'; -import ChromeExtensionProxyPage from './page'; - -interface ConnectedPage { - tabId: number; - page: ChromeExtensionProxyPage; - status: 'connected' | 'disconnected'; -} - -export class ChromeExtensionBridgeServer { - public connectedPages: Record = {}; - - constructor() { - this.connectedPages = {}; - } - - listen() {} - - async newTabWithUrl(url: string) { - assert(url, 'url is required'); - - // new tab - const tab = await chrome.tabs.create({ url }); - const tabId = tab.id; - assert(tabId, 'failed to get tabId after creating a new tab'); - - const page = new ChromeExtensionProxyPage(tabId); - this.connectedPages[tabId] = { - tabId, - page, - status: 'connected', - }; - return { - tabId, - page, - }; - } - - async call(tabId: number, method: string, ...args: any[]) { - assert(tabId, 'tabId is required'); - assert(this.connectedPages[tabId], 'tabId is not connected'); - return ( - this.connectedPages[tabId].page[ - method as keyof ChromeExtensionProxyPage - ] as any - )(...args); - } - - disconnect(tabId: number, closeTab = true) { - assert(tabId, 'tabId is required'); - assert(this.connectedPages[tabId], 'tabId is not connected'); - this.connectedPages[tabId].status = 'disconnected'; - if (closeTab) { - chrome.tabs.remove(tabId); - } - } -} - -// export class PageOverChromeExtensionBridge implements AbstractPage { -// pageType = 'page-over-chrome-extension-bridge'; - -// constructor() {} -// } diff --git a/packages/web-integration/src/chrome-extension/index.ts b/packages/web-integration/src/chrome-extension/index.ts index 9ae6ece8b..035a4db32 100644 --- a/packages/web-integration/src/chrome-extension/index.ts +++ b/packages/web-integration/src/chrome-extension/index.ts @@ -1,11 +1,13 @@ import { ERROR_CODE_NOT_IMPLEMENTED_AS_DESIGNED } from '../common/utils'; import { ChromeExtensionProxyPageAgent } from './agent'; -import { ChromeExtensionBridgeServer } from './bridge'; +import { getBridgePageInCliSide } from './bridge-cli-side'; +import { ChromeExtensionPageBridgeSide } from './bridge-page'; import ChromeExtensionProxyPage from './page'; export { + getBridgePageInCliSide, ChromeExtensionProxyPage, ChromeExtensionProxyPageAgent, - ChromeExtensionBridgeServer, + ChromeExtensionPageBridgeSide, ERROR_CODE_NOT_IMPLEMENTED_AS_DESIGNED, }; diff --git a/packages/web-integration/src/chrome-extension/page.ts b/packages/web-integration/src/chrome-extension/page.ts index b1a8279d3..bc6b7aa53 100644 --- a/packages/web-integration/src/chrome-extension/page.ts +++ b/packages/web-integration/src/chrome-extension/page.ts @@ -9,7 +9,7 @@ import fs from 'node:fs'; import type { WebKeyInput } from '@/common/page'; import type { ElementInfo } from '@/extractor'; import type { AbstractPage } from '@/page'; -import type { Point, Rect, Size } from '@midscene/core'; +import type { Point, Size } from '@midscene/core'; import { ifInBrowser } from '@midscene/shared/utils'; import type { Protocol as CDPTypes } from 'devtools-protocol'; import { CdpKeyboard } from './cdpInput'; @@ -30,7 +30,7 @@ const scriptFileContent = async () => { export default class ChromeExtensionProxyPage implements AbstractPage { pageType = 'chrome-extension-proxy'; - private tabId: number; + public tabId: number; private viewportSize?: Size; diff --git a/packages/web-integration/src/page.ts b/packages/web-integration/src/page.ts index b4c3c2c34..e42a8f14a 100644 --- a/packages/web-integration/src/page.ts +++ b/packages/web-integration/src/page.ts @@ -5,6 +5,21 @@ import type { ElementInfo } from './extractor'; export type MouseButton = 'left' | 'right' | 'middle'; +export interface MouseAction { + click: ( + x: number, + y: number, + options: { button: MouseButton }, + ) => Promise; + wheel: (deltaX: number, deltaY: number) => Promise; + move: (x: number, y: number) => Promise; +} + +export interface KeyboardAction { + type: (text: string) => Promise; + press: (key: WebKeyInput) => Promise; +} + export abstract class AbstractPage { abstract pageType: string; abstract getElementInfos(): Promise; @@ -12,7 +27,7 @@ export abstract class AbstractPage { abstract screenshotBase64?(): Promise; abstract size(): Promise; - get mouse() { + get mouse(): MouseAction { return { click: async ( x: number, @@ -24,7 +39,7 @@ export abstract class AbstractPage { }; } - get keyboard() { + get keyboard(): KeyboardAction { return { type: async (text: string) => {}, press: async (key: WebKeyInput) => {}, diff --git a/packages/web-integration/tests/unit-test/extension/bridge-io.test.ts b/packages/web-integration/tests/unit-test/extension/bridge-io.test.ts new file mode 100644 index 000000000..a83aff2a1 --- /dev/null +++ b/packages/web-integration/tests/unit-test/extension/bridge-io.test.ts @@ -0,0 +1,113 @@ +import { describe, expect, it } from 'vitest'; + +import { BridgeClient } from '@/chrome-extension/bridge-io-client'; +import { BridgeServer } from '@/chrome-extension/bridge-io-server'; + +let testPort = 1234; +describe('bridge-io', () => { + it('server launch and close', () => { + const server = new BridgeServer(testPort++); + server.listen(); + server.close(); + }); + + it('refuse 2nd client connection', async () => { + const port = testPort++; + const server = new BridgeServer(port); + server.listen(); + const client = new BridgeClient( + `ws://localhost:${port}`, + (method, args) => { + return Promise.resolve('ok'); + }, + ); + await client.connect(); + + const client2 = new BridgeClient( + `ws://localhost:${port}`, + (method, args) => { + return Promise.resolve('ok'); + }, + ); + await expect(client2.connect()).rejects.toThrow(); + + server.close(); + client.disconnect(); + }); + + it('server listen timeout', async () => { + const server = new BridgeServer(testPort++); + await expect(server.listen(100)).rejects.toThrow(); + }); + + it('server and client communicate', async () => { + const port = testPort++; + const method = 'test'; + const args = ['a', 'b', { foo: 'bar' }]; + const responseValue = { hello: 'world' }; + + const server = new BridgeServer(port); + server.listen(); + const client = new BridgeClient( + `ws://localhost:${port}`, + (method, args) => { + expect(method).toBe(method); + expect(args).toEqual(args); + return Promise.resolve(responseValue); + }, + ); + await client.connect(); + + const response = await server.call(method, args); + expect(response).toEqual(responseValue); + + server.close(); + client.disconnect(); + }); + + it('flush all calls', async () => { + const port = testPort++; + const server = new BridgeServer(port); + server.listen(); + + const client = new BridgeClient( + `ws://localhost:${port}`, + (method, args) => { + return Promise.resolve('ok'); + }, + ); + + const call = server.call('test', ['a', 'b']); + const call2 = server.call('test2', ['a', 'b']); + + await new Promise((resolve) => setTimeout(resolve, 100)); + await client.connect(); + const response = await call; + expect(response).toEqual('ok'); + + const response2 = await call2; + expect(response2).toEqual('ok'); + + server.close(); + client.disconnect(); + }); + + it('server timeout', async () => { + const port = testPort++; + const server = new BridgeServer(port); + server.listen(); + + const client = new BridgeClient( + `ws://localhost:${port}`, + (method, args) => { + throw new Error('internal error'); + }, + ); + await client.connect(); + + expect(server.call('test', ['a', 'b'], 1000)).rejects.toThrow(); + + server.close(); + client.disconnect(); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 678bcaf2f..d946363b8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,7 +92,7 @@ importers: version: 14.1.1 puppeteer: specifier: 23.0.2 - version: 23.0.2(typescript@5.0.4) + version: 23.0.2(bufferutil@4.0.9)(typescript@5.0.4)(utf-8-validate@6.0.5) devDependencies: '@modern-js/module-tools': specifier: 2.60.6 @@ -227,7 +227,7 @@ importers: version: 2.60.6(typescript@5.0.4) '@modern-js/plugin-module-doc': specifier: ^2.33.1 - version: 2.33.1(@modern-js/core@2.60.6)(@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(webpack@5.95.0))(@modern-js/module-tools@2.60.6(typescript@5.0.4))(@types/express@4.17.21)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.27.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(webpack@5.95.0) + version: 2.33.1(@modern-js/core@2.60.6)(@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)(webpack@5.95.0))(@modern-js/module-tools@2.60.6(typescript@5.0.4))(@types/express@4.17.21)(@types/react@18.3.3)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.27.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)(webpack@5.95.0) '@modern-js/plugin-module-node-polyfill': specifier: 2.60.6 version: 2.60.6(@modern-js/module-tools@2.60.6(typescript@5.0.4)) @@ -318,6 +318,12 @@ importers: openai: specifier: 4.57.1 version: 4.57.1(zod@3.23.8) + socket.io: + specifier: 4.8.1 + version: 4.8.1(bufferutil@4.0.9)(utf-8-validate@6.0.5) + socket.io-client: + specifier: 4.8.1 + version: 4.8.1(bufferutil@4.0.9)(utf-8-validate@6.0.5) devDependencies: '@modern-js/module-tools': specifier: 2.60.6 @@ -366,7 +372,7 @@ importers: version: 1.44.1 puppeteer: specifier: 23.0.2 - version: 23.0.2(typescript@5.0.4) + version: 23.0.2(bufferutil@4.0.9)(typescript@5.0.4)(utf-8-validate@6.0.5) typescript: specifier: ~5.0.4 version: 5.0.4 @@ -375,7 +381,7 @@ importers: version: 1.6.0(@types/node@18.19.62)(jsdom@24.1.1)(sass-embedded@1.80.5)(terser@5.36.0) webdriverio: specifier: 9.0.6 - version: 9.0.6 + version: 9.0.6(bufferutil@4.0.9)(utf-8-validate@6.0.5) packages: @@ -3315,6 +3321,9 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@svgr/babel-plugin-add-jsx-attribute@8.0.0': resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==} engines: {node: '>=14'} @@ -3467,6 +3476,9 @@ packages: '@types/conventional-commits-parser@5.0.0': resolution: {integrity: sha512-loB369iXNmAZglwWATL+WRe+CRMmmBPtpolYzIebFaX4YA3x+BEfLqhUAV9WanycKI3TG1IMr5bMJDajDKLlUQ==} + '@types/cookie@0.4.1': + resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} + '@types/cors@2.8.12': resolution: {integrity: sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==} @@ -4120,6 +4132,10 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + basic-auth@2.0.1: resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} engines: {node: '>= 0.8'} @@ -4231,6 +4247,10 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + bufferutil@4.0.9: + resolution: {integrity: sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==} + engines: {node: '>=6.14.2'} + builtin-status-codes@3.0.0: resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==} @@ -5082,6 +5102,17 @@ packages: end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + engine.io-client@6.6.2: + resolution: {integrity: sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw==} + + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + + engine.io@6.6.2: + resolution: {integrity: sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==} + engines: {node: '>=10.2.0'} + enhanced-resolve@5.12.0: resolution: {integrity: sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==} engines: {node: '>=10.13.0'} @@ -7118,6 +7149,10 @@ packages: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + node-html-parser@6.1.13: resolution: {integrity: sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==} @@ -8902,6 +8937,21 @@ packages: snake-case@3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} + socket.io-adapter@2.5.5: + resolution: {integrity: sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==} + + socket.io-client@4.8.1: + resolution: {integrity: sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==} + engines: {node: '>=10.0.0'} + + socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + + socket.io@4.8.1: + resolution: {integrity: sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==} + engines: {node: '>=10.2.0'} + socks-proxy-agent@8.0.4: resolution: {integrity: sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==} engines: {node: '>= 14'} @@ -9575,6 +9625,10 @@ packages: resolution: {integrity: sha512-5cnLm4gseXjAclKowC4IjByaGsjtAoV6PrOQOljplNB54ReUYJP8HdAFq2muHinSDAh09PPX/uXDPfdxRHvuSA==} engines: {node: '>= 0.8.0'} + utf-8-validate@6.0.5: + resolution: {integrity: sha512-EYZR+OpIXp9Y1eG1iueg8KRsY8TuT8VNgnanZ0uA3STqhHQTLwbl+WX76/9X5OY12yQubymBpaBSmMPkSTQcKA==} + engines: {node: '>=6.14.2'} + utf8@3.0.0: resolution: {integrity: sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==} @@ -9865,6 +9919,18 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + ws@8.18.0: resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -9902,6 +9968,10 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xmlhttprequest-ssl@2.1.2: + resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} + engines: {node: '>=0.4.0'} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -12427,12 +12497,12 @@ snapshots: - react-dom - supports-color - '@modern-js/builder-rspack-provider@2.31.2(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)': + '@modern-js/builder-rspack-provider@2.31.2(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)': dependencies: '@babel/core': 7.26.0 '@babel/preset-typescript': 7.26.0(@babel/core@7.26.0) - '@modern-js/builder-shared': 2.31.2(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(typescript@5.0.4) - '@modern-js/server': 2.31.2(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0) + '@modern-js/builder-shared': 2.31.2(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(typescript@5.0.4)(utf-8-validate@6.0.5) + '@modern-js/server': 2.31.2(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(utf-8-validate@6.0.5) '@modern-js/types': 2.31.2 '@modern-js/utils': 2.31.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@rspack/core': 0.2.12(type-fest@3.13.1)(webpack@5.95.0) @@ -12469,12 +12539,12 @@ snapshots: - webpack-hot-middleware - webpack-plugin-serve - '@modern-js/builder-rspack-provider@2.32.1(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)': + '@modern-js/builder-rspack-provider@2.32.1(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)': dependencies: '@babel/core': 7.26.0 '@babel/preset-typescript': 7.26.0(@babel/core@7.26.0) - '@modern-js/builder-shared': 2.32.1(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(typescript@5.0.4) - '@modern-js/server': 2.32.1(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0) + '@modern-js/builder-shared': 2.32.1(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(typescript@5.0.4)(utf-8-validate@6.0.5) + '@modern-js/server': 2.32.1(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(utf-8-validate@6.0.5) '@modern-js/types': 2.32.1 '@modern-js/utils': 2.32.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@rspack/core': 0.2.12(type-fest@3.13.1)(webpack@5.95.0) @@ -12511,13 +12581,13 @@ snapshots: - webpack-hot-middleware - webpack-plugin-serve - '@modern-js/builder-shared@2.31.2(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(typescript@5.0.4)': + '@modern-js/builder-shared@2.31.2(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(typescript@5.0.4)(utf-8-validate@6.0.5)': dependencies: '@babel/core': 7.26.0 '@babel/parser': 7.26.2 '@babel/types': 7.26.0 '@modern-js/prod-server': 2.31.2(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@modern-js/server': 2.31.2(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0) + '@modern-js/server': 2.31.2(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(utf-8-validate@6.0.5) '@modern-js/types': 2.31.2 '@modern-js/utils': 2.31.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@swc/helpers': 0.5.1 @@ -12550,13 +12620,13 @@ snapshots: - utf-8-validate - webpack-cli - '@modern-js/builder-shared@2.32.1(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(typescript@5.0.4)': + '@modern-js/builder-shared@2.32.1(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(typescript@5.0.4)(utf-8-validate@6.0.5)': dependencies: '@babel/core': 7.26.0 '@babel/parser': 7.26.2 '@babel/types': 7.26.0 '@modern-js/prod-server': 2.32.1(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@modern-js/server': 2.32.1(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0) + '@modern-js/server': 2.32.1(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(utf-8-validate@6.0.5) '@modern-js/types': 2.32.1 '@modern-js/utils': 2.32.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@swc/helpers': 0.5.1 @@ -12589,9 +12659,9 @@ snapshots: - utf-8-validate - webpack-cli - '@modern-js/builder@2.31.2(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(typescript@5.0.4)': + '@modern-js/builder@2.31.2(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(typescript@5.0.4)(utf-8-validate@6.0.5)': dependencies: - '@modern-js/builder-shared': 2.31.2(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(typescript@5.0.4) + '@modern-js/builder-shared': 2.31.2(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(typescript@5.0.4)(utf-8-validate@6.0.5) '@modern-js/monorepo-utils': 2.31.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@modern-js/utils': 2.31.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@svgr/webpack': 8.0.1(typescript@5.0.4) @@ -12613,9 +12683,9 @@ snapshots: - utf-8-validate - webpack-cli - '@modern-js/builder@2.32.1(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(typescript@5.0.4)': + '@modern-js/builder@2.32.1(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(typescript@5.0.4)(utf-8-validate@6.0.5)': dependencies: - '@modern-js/builder-shared': 2.32.1(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(typescript@5.0.4) + '@modern-js/builder-shared': 2.32.1(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(typescript@5.0.4)(utf-8-validate@6.0.5) '@modern-js/monorepo-utils': 2.32.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@modern-js/utils': 2.32.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@svgr/webpack': 8.0.1(typescript@5.0.4) @@ -12654,17 +12724,17 @@ snapshots: '@modern-js/utils': 2.60.6 '@swc/helpers': 0.5.13 - '@modern-js/doc-core@2.31.2(@modern-js/core@2.60.6)(@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(webpack@5.95.0))(@types/express@4.17.21)(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(webpack@5.95.0)': + '@modern-js/doc-core@2.31.2(@modern-js/core@2.60.6)(@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)(webpack@5.95.0))(@types/express@4.17.21)(bufferutil@4.0.9)(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)(webpack@5.95.0)': dependencies: '@headlessui/react': 1.7.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@loadable/component': 5.15.2(react@18.3.1) '@mdx-js/loader': 2.2.1(webpack@5.95.0) '@mdx-js/mdx': 2.2.1 '@mdx-js/react': 2.2.1(react@18.3.1) - '@modern-js/builder': 2.31.2(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(typescript@5.0.4) - '@modern-js/builder-rspack-provider': 2.31.2(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4) + '@modern-js/builder': 2.31.2(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(typescript@5.0.4)(utf-8-validate@6.0.5) + '@modern-js/builder-rspack-provider': 2.31.2(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5) '@modern-js/core': 2.60.6 - '@modern-js/doc-plugin-medium-zoom': 2.31.2(@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(webpack@5.95.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@modern-js/doc-plugin-medium-zoom': 2.31.2(@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)(webpack@5.95.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@modern-js/mdx-rs-binding': 0.2.4 '@modern-js/remark-container': 2.31.2 '@modern-js/utils': 2.31.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -12683,7 +12753,7 @@ snapshots: hast: 1.0.0 hast-util-from-html: 1.0.2 html-to-text: 9.0.5 - jsdom: 20.0.3 + jsdom: 20.0.3(bufferutil@4.0.9)(utf-8-validate@6.0.5) lodash-es: 4.17.21 mdast-util-mdxjs-esm: 1.3.1 node-fetch: 3.3.0 @@ -12738,17 +12808,17 @@ snapshots: - webpack-hot-middleware - webpack-plugin-serve - '@modern-js/doc-core@2.32.1(@modern-js/core@2.32.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(webpack@5.95.0))(@types/express@4.17.21)(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(webpack@5.95.0)': + '@modern-js/doc-core@2.32.1(@modern-js/core@2.32.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)(webpack@5.95.0))(@types/express@4.17.21)(bufferutil@4.0.9)(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)(webpack@5.95.0)': dependencies: '@headlessui/react': 1.7.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@loadable/component': 5.15.2(react@18.3.1) '@mdx-js/loader': 2.2.1(webpack@5.95.0) '@mdx-js/mdx': 2.2.1 '@mdx-js/react': 2.2.1(react@18.3.1) - '@modern-js/builder': 2.32.1(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(typescript@5.0.4) - '@modern-js/builder-rspack-provider': 2.32.1(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4) + '@modern-js/builder': 2.32.1(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(typescript@5.0.4)(utf-8-validate@6.0.5) + '@modern-js/builder-rspack-provider': 2.32.1(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5) '@modern-js/core': 2.32.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@modern-js/doc-plugin-medium-zoom': 2.32.1(@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(webpack@5.95.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@modern-js/doc-plugin-medium-zoom': 2.32.1(@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)(webpack@5.95.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@modern-js/mdx-rs-binding': 0.2.4 '@modern-js/remark-container': 2.32.1 '@modern-js/utils': 2.32.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -12767,7 +12837,7 @@ snapshots: hast: 1.0.0 hast-util-from-html: 1.0.2 html-to-text: 9.0.5 - jsdom: 20.0.3 + jsdom: 20.0.3(bufferutil@4.0.9)(utf-8-validate@6.0.5) lodash-es: 4.17.21 mdast-util-mdxjs-esm: 1.3.1 node-fetch: 3.3.0 @@ -12822,9 +12892,9 @@ snapshots: - webpack-hot-middleware - webpack-plugin-serve - '@modern-js/doc-plugin-api-docgen@2.31.2(@modern-js/core@2.60.6)(@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(webpack@5.95.0))(@types/express@4.17.21)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.27.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(webpack@5.95.0)': + '@modern-js/doc-plugin-api-docgen@2.31.2(@modern-js/core@2.60.6)(@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)(webpack@5.95.0))(@types/express@4.17.21)(@types/react@18.3.3)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.27.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)(webpack@5.95.0)': dependencies: - '@modern-js/doc-core': 2.31.2(@modern-js/core@2.60.6)(@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(webpack@5.95.0))(@types/express@4.17.21)(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(webpack@5.95.0) + '@modern-js/doc-core': 2.31.2(@modern-js/core@2.60.6)(@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)(webpack@5.95.0))(@types/express@4.17.21)(bufferutil@4.0.9)(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)(webpack@5.95.0) '@modern-js/utils': 2.31.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) documentation: 14.0.3 react: 18.3.1 @@ -12859,28 +12929,28 @@ snapshots: - webpack-hot-middleware - webpack-plugin-serve - '@modern-js/doc-plugin-medium-zoom@2.31.2(@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(webpack@5.95.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@modern-js/doc-plugin-medium-zoom@2.31.2(@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)(webpack@5.95.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@modern-js/doc-tools': 2.32.1(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(webpack@5.95.0) + '@modern-js/doc-tools': 2.32.1(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)(webpack@5.95.0) '@modern-js/utils': 2.31.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) medium-zoom: 1.0.8 react: 18.3.1 transitivePeerDependencies: - react-dom - '@modern-js/doc-plugin-medium-zoom@2.32.1(@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(webpack@5.95.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@modern-js/doc-plugin-medium-zoom@2.32.1(@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)(webpack@5.95.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@modern-js/doc-tools': 2.32.1(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(webpack@5.95.0) + '@modern-js/doc-tools': 2.32.1(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)(webpack@5.95.0) '@modern-js/utils': 2.32.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) medium-zoom: 1.0.8 react: 18.3.1 transitivePeerDependencies: - react-dom - '@modern-js/doc-plugin-preview@2.31.2(@modern-js/core@2.60.6)(@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(webpack@5.95.0))(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.27.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(webpack@5.95.0)': + '@modern-js/doc-plugin-preview@2.31.2(@modern-js/core@2.60.6)(@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)(webpack@5.95.0))(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.27.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)(webpack@5.95.0)': dependencies: '@mdx-js/mdx': 2.2.1 - '@modern-js/doc-core': 2.31.2(@modern-js/core@2.60.6)(@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(webpack@5.95.0))(@types/express@4.17.21)(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(webpack@5.95.0) + '@modern-js/doc-core': 2.31.2(@modern-js/core@2.60.6)(@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)(webpack@5.95.0))(@types/express@4.17.21)(bufferutil@4.0.9)(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)(webpack@5.95.0) '@modern-js/utils': 2.31.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) qrcode.react: 3.2.0(react@18.3.1) react: 18.3.1 @@ -12912,10 +12982,10 @@ snapshots: - webpack-hot-middleware - webpack-plugin-serve - '@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(webpack@5.95.0)': + '@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)(webpack@5.95.0)': dependencies: '@modern-js/core': 2.32.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@modern-js/doc-core': 2.32.1(@modern-js/core@2.32.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(webpack@5.95.0))(@types/express@4.17.21)(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(webpack@5.95.0) + '@modern-js/doc-core': 2.32.1(@modern-js/core@2.32.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)(webpack@5.95.0))(@types/express@4.17.21)(bufferutil@4.0.9)(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)(webpack@5.95.0) '@modern-js/utils': 2.32.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 transitivePeerDependencies: @@ -13078,11 +13148,11 @@ snapshots: '@modern-js/utils': 2.60.6 '@swc/helpers': 0.5.13 - '@modern-js/plugin-module-doc@2.33.1(@modern-js/core@2.60.6)(@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(webpack@5.95.0))(@modern-js/module-tools@2.60.6(typescript@5.0.4))(@types/express@4.17.21)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.27.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(webpack@5.95.0)': + '@modern-js/plugin-module-doc@2.33.1(@modern-js/core@2.60.6)(@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)(webpack@5.95.0))(@modern-js/module-tools@2.60.6(typescript@5.0.4))(@types/express@4.17.21)(@types/react@18.3.3)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.27.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)(webpack@5.95.0)': dependencies: - '@modern-js/doc-core': 2.31.2(@modern-js/core@2.60.6)(@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(webpack@5.95.0))(@types/express@4.17.21)(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(webpack@5.95.0) - '@modern-js/doc-plugin-api-docgen': 2.31.2(@modern-js/core@2.60.6)(@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(webpack@5.95.0))(@types/express@4.17.21)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.27.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(webpack@5.95.0) - '@modern-js/doc-plugin-preview': 2.31.2(@modern-js/core@2.60.6)(@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(webpack@5.95.0))(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.27.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(webpack@5.95.0) + '@modern-js/doc-core': 2.31.2(@modern-js/core@2.60.6)(@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)(webpack@5.95.0))(@types/express@4.17.21)(bufferutil@4.0.9)(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)(webpack@5.95.0) + '@modern-js/doc-plugin-api-docgen': 2.31.2(@modern-js/core@2.60.6)(@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)(webpack@5.95.0))(@types/express@4.17.21)(@types/react@18.3.3)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.27.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)(webpack@5.95.0) + '@modern-js/doc-plugin-preview': 2.31.2(@modern-js/core@2.60.6)(@modern-js/doc-tools@2.32.1(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)(webpack@5.95.0))(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.27.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(type-fest@3.13.1)(typescript@5.0.4)(utf-8-validate@6.0.5)(webpack@5.95.0) '@modern-js/module-tools': 2.60.6(typescript@5.0.4) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -13315,7 +13385,7 @@ snapshots: - react-dom - supports-color - '@modern-js/server@2.31.2(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)': + '@modern-js/server@2.31.2(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(utf-8-validate@6.0.5)': dependencies: '@babel/core': 7.26.0 '@babel/register': 7.25.9(@babel/core@7.26.0) @@ -13329,7 +13399,7 @@ snapshots: http-compression: 1.0.6 minimatch: 3.1.2 path-to-regexp: 6.3.0 - ws: 8.18.0 + ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@6.0.5) optionalDependencies: ts-node: 10.9.2(@types/node@18.19.62)(typescript@5.0.4) tsconfig-paths: 4.2.0 @@ -13342,7 +13412,7 @@ snapshots: - supports-color - utf-8-validate - '@modern-js/server@2.32.1(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)': + '@modern-js/server@2.32.1(@types/express@4.17.21)(bufferutil@4.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.62)(typescript@5.0.4))(tsconfig-paths@4.2.0)(utf-8-validate@6.0.5)': dependencies: '@babel/core': 7.26.0 '@babel/register': 7.25.9(@babel/core@7.26.0) @@ -13356,7 +13426,7 @@ snapshots: http-compression: 1.0.6 minimatch: 3.1.2 path-to-regexp: 6.3.0 - ws: 8.18.0 + ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@6.0.5) optionalDependencies: ts-node: 10.9.2(@types/node@18.19.62)(typescript@5.0.4) tsconfig-paths: 4.2.0 @@ -14102,6 +14172,8 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} + '@socket.io/component-emitter@3.1.2': {} + '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -14279,6 +14351,8 @@ snapshots: '@types/node': 18.19.62 optional: true + '@types/cookie@0.4.1': {} + '@types/cors@2.8.12': {} '@types/css-font-loading-module@0.0.12': {} @@ -15118,6 +15192,8 @@ snapshots: base64-js@1.5.1: {} + base64id@2.0.0: {} + basic-auth@2.0.1: dependencies: safe-buffer: 5.1.2 @@ -15258,6 +15334,11 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + bufferutil@4.0.9: + dependencies: + node-gyp-build: 4.8.4 + optional: true + builtin-status-codes@3.0.0: {} bytes@3.0.0: {} @@ -16247,6 +16328,37 @@ snapshots: dependencies: once: 1.4.0 + engine.io-client@6.6.2(bufferutil@4.0.9)(utf-8-validate@6.0.5): + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7(supports-color@5.5.0) + engine.io-parser: 5.2.3 + ws: 8.17.1(bufferutil@4.0.9)(utf-8-validate@6.0.5) + xmlhttprequest-ssl: 2.1.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + engine.io-parser@5.2.3: {} + + engine.io@6.6.2(bufferutil@4.0.9)(utf-8-validate@6.0.5): + dependencies: + '@types/cookie': 0.4.1 + '@types/cors': 2.8.12 + '@types/node': 18.19.62 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.7.2 + cors: 2.8.5 + debug: 4.3.7(supports-color@5.5.0) + engine.io-parser: 5.2.3 + ws: 8.17.1(bufferutil@4.0.9)(utf-8-validate@6.0.5) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + enhanced-resolve@5.12.0: dependencies: graceful-fs: 4.2.11 @@ -17890,7 +18002,7 @@ snapshots: jsbn@1.1.0: {} - jsdom@20.0.3: + jsdom@20.0.3(bufferutil@4.0.9)(utf-8-validate@6.0.5): dependencies: abab: 2.0.6 acorn: 8.14.0 @@ -17916,14 +18028,14 @@ snapshots: whatwg-encoding: 2.0.0 whatwg-mimetype: 3.0.0 whatwg-url: 11.0.0 - ws: 8.18.0 + ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@6.0.5) xml-name-validator: 4.0.0 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - jsdom@24.1.1: + jsdom@24.1.1(bufferutil@4.0.9)(utf-8-validate@6.0.5): dependencies: cssstyle: 4.1.0 data-urls: 5.0.0 @@ -17944,7 +18056,7 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.0.0 - ws: 8.18.0 + ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@6.0.5) xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil @@ -18924,6 +19036,9 @@ snapshots: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 + node-gyp-build@4.8.4: + optional: true + node-html-parser@6.1.13: dependencies: css-select: 5.1.0 @@ -19796,25 +19911,25 @@ snapshots: punycode@2.3.1: {} - puppeteer-core@23.0.2: + puppeteer-core@23.0.2(bufferutil@4.0.9)(utf-8-validate@6.0.5): dependencies: '@puppeteer/browsers': 2.3.0 chromium-bidi: 0.6.4(devtools-protocol@0.0.1312386) debug: 4.3.7(supports-color@5.5.0) devtools-protocol: 0.0.1312386 - ws: 8.18.0 + ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@6.0.5) transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - puppeteer@23.0.2(typescript@5.0.4): + puppeteer@23.0.2(bufferutil@4.0.9)(typescript@5.0.4)(utf-8-validate@6.0.5): dependencies: '@puppeteer/browsers': 2.3.0 chromium-bidi: 0.6.4(devtools-protocol@0.0.1312386) cosmiconfig: 9.0.0(typescript@5.0.4) devtools-protocol: 0.0.1312386 - puppeteer-core: 23.0.2 + puppeteer-core: 23.0.2(bufferutil@4.0.9)(utf-8-validate@6.0.5) transitivePeerDependencies: - bufferutil - supports-color @@ -20989,6 +21104,47 @@ snapshots: dot-case: 3.0.4 tslib: 2.8.1 + socket.io-adapter@2.5.5(bufferutil@4.0.9)(utf-8-validate@6.0.5): + dependencies: + debug: 4.3.7(supports-color@5.5.0) + ws: 8.17.1(bufferutil@4.0.9)(utf-8-validate@6.0.5) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-client@4.8.1(bufferutil@4.0.9)(utf-8-validate@6.0.5): + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7(supports-color@5.5.0) + engine.io-client: 6.6.2(bufferutil@4.0.9)(utf-8-validate@6.0.5) + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.4: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + socket.io@4.8.1(bufferutil@4.0.9)(utf-8-validate@6.0.5): + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.5 + debug: 4.3.7(supports-color@5.5.0) + engine.io: 6.6.2(bufferutil@4.0.9)(utf-8-validate@6.0.5) + socket.io-adapter: 2.5.5(bufferutil@4.0.9)(utf-8-validate@6.0.5) + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + socks-proxy-agent@8.0.4: dependencies: agent-base: 7.1.1 @@ -21705,6 +21861,11 @@ snapshots: userhome@1.0.1: {} + utf-8-validate@6.0.5: + dependencies: + node-gyp-build: 4.8.4 + optional: true + utf8@3.0.0: {} utif2@4.1.0: @@ -21851,7 +22012,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 18.19.62 - jsdom: 24.1.1 + jsdom: 24.1.1(bufferutil@4.0.9)(utf-8-validate@6.0.5) transitivePeerDependencies: - less - lightningcss @@ -21904,7 +22065,7 @@ snapshots: web-streams-polyfill@4.0.0-beta.3: {} - webdriver@9.0.6: + webdriver@9.0.6(bufferutil@4.0.9)(utf-8-validate@6.0.5): dependencies: '@types/node': 20.17.4 '@types/ws': 8.5.12 @@ -21914,13 +22075,13 @@ snapshots: '@wdio/types': 9.0.4 '@wdio/utils': 9.0.6 deepmerge-ts: 7.1.3 - ws: 8.18.0 + ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@6.0.5) transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - webdriverio@9.0.6: + webdriverio@9.0.6(bufferutil@4.0.9)(utf-8-validate@6.0.5): dependencies: '@types/node': 20.17.4 '@types/sinonjs__fake-timers': 8.1.5 @@ -21948,7 +22109,7 @@ snapshots: rgb2hex: 0.2.5 serialize-error: 11.0.3 urlpattern-polyfill: 10.0.0 - webdriver: 9.0.6 + webdriver: 9.0.6(bufferutil@4.0.9)(utf-8-validate@6.0.5) transitivePeerDependencies: - bufferutil - supports-color @@ -22087,7 +22248,15 @@ snapshots: wrappy@1.0.2: {} - ws@8.18.0: {} + ws@8.17.1(bufferutil@4.0.9)(utf-8-validate@6.0.5): + optionalDependencies: + bufferutil: 4.0.9 + utf-8-validate: 6.0.5 + + ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@6.0.5): + optionalDependencies: + bufferutil: 4.0.9 + utf-8-validate: 6.0.5 xhr@2.6.0: dependencies: @@ -22112,6 +22281,8 @@ snapshots: xmlchars@2.2.0: {} + xmlhttprequest-ssl@2.1.2: {} + xtend@4.0.2: {} y18n@4.0.3: {} From 6a8a6edaddefb8e1be14e02d8ed208401570e40c Mon Sep 17 00:00:00 2001 From: yutao Date: Mon, 30 Dec 2024 17:41:24 +0800 Subject: [PATCH 05/40] feat: add bridge for remote page --- packages/midscene/src/action/executor.ts | 3 +- packages/visualizer/modern.config.ts | 3 +- packages/visualizer/package.json | 20 ++++- packages/visualizer/src/extension/bridge.tsx | 73 ++++++++----------- .../src/extension/playground-entry.tsx | 6 ++ packages/visualizer/src/extension/popup.tsx | 4 +- packages/visualizer/src/index.tsx | 4 + .../src/chrome-extension/bridge-common.ts | 2 +- .../src/chrome-extension/bridge-io-client.ts | 23 +++++- .../src/chrome-extension/bridge-io-server.ts | 13 +++- ...ge-cli-side.ts => bridge-page-cli-side.ts} | 38 +++++++--- .../src/chrome-extension/bridge-page.ts | 32 ++++++-- .../src/chrome-extension/index.ts | 8 +- .../extension/bridge-io-agent.test.ts | 33 +++++++++ .../unit-test/extension/bridge-io.test.ts | 47 +++++++++++- pnpm-lock.yaml | 58 +++------------ 16 files changed, 240 insertions(+), 127 deletions(-) rename packages/web-integration/src/chrome-extension/{bridge-cli-side.ts => bridge-page-cli-side.ts} (65%) create mode 100644 packages/web-integration/tests/unit-test/extension/bridge-io-agent.test.ts diff --git a/packages/midscene/src/action/executor.ts b/packages/midscene/src/action/executor.ts index 1b74aed55..9362dc7a9 100644 --- a/packages/midscene/src/action/executor.ts +++ b/packages/midscene/src/action/executor.ts @@ -143,7 +143,8 @@ export class Executor { taskIndex++; } catch (e: any) { successfullyCompleted = false; - task.error = e?.message || 'error-without-message'; + task.error = + e?.message || (typeof e === 'string' ? e : 'error-without-message'); task.errorStack = e.stack; task.status = 'failed'; diff --git a/packages/visualizer/modern.config.ts b/packages/visualizer/modern.config.ts index 6e6021757..8d7eda720 100644 --- a/packages/visualizer/modern.config.ts +++ b/packages/visualizer/modern.config.ts @@ -2,7 +2,7 @@ import path from 'node:path'; import { defineConfig, moduleTools } from '@modern-js/module-tools'; import { modulePluginNodePolyfill } from '@modern-js/plugin-module-node-polyfill'; import { version } from './package.json'; -const externals = ['playwright', 'langsmith']; +const externals = ['playwright', 'langsmith', 'bufferutil', 'utf-8-validate']; const commonConfig = { asset: { @@ -18,7 +18,6 @@ const commonConfig = { : undefined, define: { __VERSION__: JSON.stringify(version), - global: 'globalThis', }, }; diff --git a/packages/visualizer/package.json b/packages/visualizer/package.json index 3a4f68b81..02024606b 100644 --- a/packages/visualizer/package.json +++ b/packages/visualizer/package.json @@ -6,10 +6,16 @@ "types": "./dist/types/index.d.ts", "main": "./dist/lib/index.js", "module": "./dist/es/index.js", - "files": ["dist", "html", "README.md"], + "files": [ + "dist", + "html", + "README.md" + ], "watch": { "build": { - "patterns": ["src"], + "patterns": [ + "src" + ], "extensions": "tsx,less,scss,css,js,jsx,ts", "quiet": false } @@ -53,8 +59,16 @@ "typescript": "~5.0.4", "zustand": "4.5.2" }, - "sideEffects": ["**/*.css", "**/*.less", "**/*.sass", "**/*.scss"], + "sideEffects": [ + "**/*.css", + "**/*.less", + "**/*.sass", + "**/*.scss" + ], "publishConfig": { "access": "public" + }, + "dependencies": { + "buffer": "6.0.3" } } diff --git a/packages/visualizer/src/extension/bridge.tsx b/packages/visualizer/src/extension/bridge.tsx index b121f9f4c..efcc1e2be 100644 --- a/packages/visualizer/src/extension/bridge.tsx +++ b/packages/visualizer/src/extension/bridge.tsx @@ -1,62 +1,49 @@ -import { - ChromeExtensionPageBridgeSide, - getBridgePageInCliSide, -} from '@midscene/web/chrome-extension'; +import { ChromeExtensionPageBrowserSide } from '@midscene/web/chrome-extension'; import { Button } from 'antd'; import { useEffect, useState } from 'react'; export default function Bridge() { - const [bridge, setBridge] = useState( - null, - ); - const [tabId, setTabId] = useState(null); - useEffect(() => { - const bridge = new ChromeExtensionPageBridgeSide(); - bridge.connect(); - setBridge(bridge); - }, []); + const [bridgePage, setBridgePage] = + useState(null); - const newTab = async () => { - if (!bridge) { - throw new Error('bridge is not initialized'); - } - const { tabId } = await bridge.connectNewTabWithUrl( - 'https://www.baidu.com', - ); - setTabId(tabId); - }; + const [bridgeStatus, setBridgeStatus] = useState< + 'init' | 'connecting' | 'connected' | 'error' | 'closed' + >('init'); - const doSomething = async () => { - if (!tabId) { - throw new Error('bridge is not initialized'); + const startConnection = async () => { + const bridgePage = new ChromeExtensionPageBrowserSide(() => { + setBridgeStatus('closed'); + }); + try { + setBridgeStatus('connecting'); + await bridgePage.connect(); + console.log('bridgePage connected !', bridgePage); + setBridgePage(bridgePage); + setBridgeStatus('connected'); + } catch (e) { + console.error(e); + setBridgeStatus('error'); } - - const proxy: any = getBridgePageInCliSide(tabId); - console.log('1'); - console.log(proxy.screenshotBase64()); - console.log('2'); - console.log(await proxy.mouse.click()); - // const page = - // await bridge.call(tabId, 'screenshotBase64'); }; return (
-

Bridge List

-
- {Object.entries(bridge?.connectedPages || {}).map(([tabId, page]) => ( -
{tabId}
- ))} -
- +

Bridge Mode ({bridgeStatus})

-
); } diff --git a/packages/visualizer/src/extension/playground-entry.tsx b/packages/visualizer/src/extension/playground-entry.tsx index 260823c10..b8f1e20d4 100644 --- a/packages/visualizer/src/extension/playground-entry.tsx +++ b/packages/visualizer/src/extension/playground-entry.tsx @@ -1,3 +1,5 @@ +import '../init'; + import queryString from 'query-string'; import type { WebUIContext } from '@midscene/web/utils'; @@ -6,12 +8,16 @@ import { useEffect, useMemo, useState } from 'react'; import ReactDOM from 'react-dom/client'; import { globalThemeConfig } from '../component/color'; import { StaticPlayground } from '../component/playground-component'; +import { setSideEffect } from '../init'; import type { WorkerResponseGetContext } from './utils'; import { sendToWorker } from './utils'; import type { WorkerRequestGetContext } from './utils'; import { workerMessageTypes } from './utils'; + import './playground-entry.less'; +setSideEffect(); + const PlaygroundEntry = () => { // extension proxy agent const query = useMemo( diff --git a/packages/visualizer/src/extension/popup.tsx b/packages/visualizer/src/extension/popup.tsx index 8ccaf02f6..2724b016f 100644 --- a/packages/visualizer/src/extension/popup.tsx +++ b/packages/visualizer/src/extension/popup.tsx @@ -1,6 +1,7 @@ -/// import { Button, ConfigProvider, message } from 'antd'; import ReactDOM from 'react-dom/client'; +import { setSideEffect } from '../init'; +/// import './popup.less'; import { @@ -24,6 +25,7 @@ import type { ChromeExtensionProxyPageAgent } from '@midscene/web/chrome-extensi import { useEffect, useState } from 'react'; import Bridge from './bridge'; +setSideEffect(); const shotAndOpenPlayground = async ( agent?: ChromeExtensionProxyPageAgent | null, ) => { diff --git a/packages/visualizer/src/index.tsx b/packages/visualizer/src/index.tsx index 670d90f8f..f979e12ce 100644 --- a/packages/visualizer/src/index.tsx +++ b/packages/visualizer/src/index.tsx @@ -1,4 +1,5 @@ import './index.less'; +import './init'; import DetailSide from '@/component/detail-side'; import Sidebar from '@/component/sidebar'; import { useExecutionDump } from '@/component/store'; @@ -26,6 +27,9 @@ import Logo from './component/logo'; import { iconForStatus, timeCostStrElement } from './component/misc'; import Player from './component/player'; import Timeline from './component/timeline'; +import { setSideEffect } from './init'; + +setSideEffect(); const { Dragger } = Upload; let globalRenderCount = 1; diff --git a/packages/web-integration/src/chrome-extension/bridge-common.ts b/packages/web-integration/src/chrome-extension/bridge-common.ts index 05215a51f..7b3bae383 100644 --- a/packages/web-integration/src/chrome-extension/bridge-common.ts +++ b/packages/web-integration/src/chrome-extension/bridge-common.ts @@ -14,7 +14,7 @@ export interface BridgeCall { response: any; callTime: number; responseTime: number; - callback: (response: any) => void; + callback: (error: Error | undefined, response: any) => void; error?: Error; } diff --git a/packages/web-integration/src/chrome-extension/bridge-io-client.ts b/packages/web-integration/src/chrome-extension/bridge-io-client.ts index b300c0c4d..3aa497c0b 100644 --- a/packages/web-integration/src/chrome-extension/bridge-io-client.ts +++ b/packages/web-integration/src/chrome-extension/bridge-io-client.ts @@ -15,13 +15,28 @@ export class BridgeClient { constructor( public endpoint: string, public onBridgeCall: (method: string, args: any[]) => Promise, + public onDisconnect?: () => void, ) {} async connect() { return new Promise((resolve, reject) => { - this.socket = ClientIO(this.endpoint); + this.socket = ClientIO(this.endpoint, { + reconnection: false, + }); + + const timeout = setTimeout(() => { + reject(new Error('failed to connect to bridge server')); + }, 1 * 1000); + + // on disconnect + this.socket.on('disconnect', (reason: string) => { + console.log('bridge-disconnected, reason:', reason); + this.socket = null; + this.onDisconnect?.(); + }); this.socket.on(BridgeConnectedEvent, () => { + clearTimeout(timeout); console.log('bridge-connected'); resolve(this.socket); }); @@ -36,10 +51,12 @@ export class BridgeClient { let response: any; try { response = await this.onBridgeCall(call.method, call.args); - } catch (e) { + } catch (e: any) { + const errorContent = `Error from bridge client when calling ${call.method}: ${e?.message || e}\n${e?.stack || ''}`; + console.error(errorContent); return this.socket?.emit(BridgeCallResponseEvent, { id, - error: e, + error: errorContent, } as BridgeCallResponse); } this.socket?.emit(BridgeCallResponseEvent, { diff --git a/packages/web-integration/src/chrome-extension/bridge-io-server.ts b/packages/web-integration/src/chrome-extension/bridge-io-server.ts index 5f68a04e0..5c0cd5548 100644 --- a/packages/web-integration/src/chrome-extension/bridge-io-server.ts +++ b/packages/web-integration/src/chrome-extension/bridge-io-server.ts @@ -48,10 +48,13 @@ export class BridgeServer { if (!call) { throw new Error(`call ${id} not found`); } + call.error = params.error; call.response = response; call.responseTime = Date.now(); - call.callback(response); + // console.log('callback', call.method, call.error, response); + + call.callback(call.error, response); }); // flush all calls @@ -112,9 +115,13 @@ export class BridgeServer { response: null, callTime: 0, responseTime: 0, - callback: (response: any) => { + callback: (error: Error | undefined, response: any) => { clearTimeout(timeoutId); - resolve(response); + if (error) { + reject(error); + } else { + resolve(response); + } }, }; diff --git a/packages/web-integration/src/chrome-extension/bridge-cli-side.ts b/packages/web-integration/src/chrome-extension/bridge-page-cli-side.ts similarity index 65% rename from packages/web-integration/src/chrome-extension/bridge-cli-side.ts rename to packages/web-integration/src/chrome-extension/bridge-page-cli-side.ts index 6613e46b0..a46f15fa5 100644 --- a/packages/web-integration/src/chrome-extension/bridge-cli-side.ts +++ b/packages/web-integration/src/chrome-extension/bridge-page-cli-side.ts @@ -2,9 +2,10 @@ import assert from 'node:assert'; import type { KeyboardAction, MouseAction } from '@/page'; import { DefaultBridgeServerPort } from './bridge-common'; import { BridgeServer } from './bridge-io-server'; +import type { ChromeExtensionPageBrowserSide } from './bridge-page'; // TODO: handle the connection timeout -export const getBridgePageInCliSide = () => { +export const getBridgePageInCliSide = (): ChromeExtensionPageBrowserSide => { const server = new BridgeServer(DefaultBridgeServerPort); server.listen(); @@ -15,19 +16,23 @@ export const getBridgePageInCliSide = () => { }; }; const page = {}; - // const page = { - // pendingCalls: [] as BridgeCall[], - // listen: () => { - // const io = new Server(DefaultBridgeServerPort); - // io.on('connection', (socket) => { - // socket.emit('bridge-connected', tabId); - // }); - // }, - // }; + return new Proxy(page, { get(target, prop, receiver) { assert(typeof prop === 'string', 'prop must be a string'); + if (prop === 'toJSON') { + return () => { + return { + pageType: 'page-over-chrome-extension-bridge', + }; + }; + } + + if (prop === '_forceUsePageContext') { + return undefined; + } + if (Object.keys(page).includes(prop)) { return page[prop as keyof typeof page]; } @@ -53,7 +58,18 @@ export const getBridgePageInCliSide = () => { return keyboard; } + if (prop === 'destroy') { + return async () => { + try { + await bridgeCaller('destroy'); + } catch (e) { + console.error('error calling destroy', e); + } + return server.close(); + }; + } + return bridgeCaller(prop); }, - }); + }) as ChromeExtensionPageBrowserSide; }; diff --git a/packages/web-integration/src/chrome-extension/bridge-page.ts b/packages/web-integration/src/chrome-extension/bridge-page.ts index 8c40d4207..cf80d8fb1 100644 --- a/packages/web-integration/src/chrome-extension/bridge-page.ts +++ b/packages/web-integration/src/chrome-extension/bridge-page.ts @@ -1,20 +1,23 @@ import assert from 'node:assert'; +import { MouseAction } from '@/page'; +import { KeyboardAction } from '@/page'; import { DefaultBridgeServerPort } from './bridge-common'; import { BridgeClient } from './bridge-io-client'; import ChromeExtensionProxyPage from './page'; -export class ChromeExtensionPageBridgeSide extends ChromeExtensionProxyPage { +export class ChromeExtensionPageBrowserSide extends ChromeExtensionProxyPage { public bridgeClient: BridgeClient | null = null; - constructor() { + constructor(public onDisconnect: () => void = () => {}) { super(0); } private async setupBridgeClient() { this.bridgeClient = new BridgeClient( `ws://localhost:${DefaultBridgeServerPort}`, - (method, args: any[]) => { - if (method === 'newTabWithUrl') { + async (method, args: any[]) => { + console.log('bridge call from cli side', method, args); + if (method === 'connectNewTabWithUrl') { return this.connectNewTabWithUrl.apply( this, args as unknown as [string], @@ -25,9 +28,24 @@ export class ChromeExtensionPageBridgeSide extends ChromeExtensionProxyPage { throw new Error('no tab is connected'); } + if (method.startsWith('mouse.')) { + const actionName = method.split('.')[1] as keyof MouseAction; + return this.mouse[actionName].apply(this.mouse, args as any); + } + + if (method.startsWith('keyboard.')) { + const actionName = method.split('.')[1] as keyof KeyboardAction; + return this.keyboard[actionName].apply(this.keyboard, args as any); + } + // @ts-expect-error return this[method as keyof ChromeExtensionProxyPage](...args); }, + // on disconnect + () => { + this.bridgeClient = null; + return this.destroy(); + }, ); await this.bridgeClient.connect(); } @@ -37,6 +55,7 @@ export class ChromeExtensionPageBridgeSide extends ChromeExtensionProxyPage { while (Date.now() - startTime < timeout) { try { await this.setupBridgeClient(); + console.log('bridge client connected'); return; } catch (e) { console.error('failed to connect to bridge server', e); @@ -61,10 +80,13 @@ export class ChromeExtensionPageBridgeSide extends ChromeExtensionProxyPage { this.tabId = tabId; } - disconnect() { + async destroy() { if (this.bridgeClient) { this.bridgeClient.disconnect(); + this.bridgeClient = null; } + super.destroy(); this.tabId = 0; + this.onDisconnect(); } } diff --git a/packages/web-integration/src/chrome-extension/index.ts b/packages/web-integration/src/chrome-extension/index.ts index 035a4db32..a69bee8a5 100644 --- a/packages/web-integration/src/chrome-extension/index.ts +++ b/packages/web-integration/src/chrome-extension/index.ts @@ -1,13 +1,13 @@ import { ERROR_CODE_NOT_IMPLEMENTED_AS_DESIGNED } from '../common/utils'; import { ChromeExtensionProxyPageAgent } from './agent'; -import { getBridgePageInCliSide } from './bridge-cli-side'; -import { ChromeExtensionPageBridgeSide } from './bridge-page'; +// import { getBridgePageInCliSide } from './bridge-page-cli-side'; +import { ChromeExtensionPageBrowserSide } from './bridge-page'; import ChromeExtensionProxyPage from './page'; export { - getBridgePageInCliSide, + // getBridgePageInCliSide, ChromeExtensionProxyPage, ChromeExtensionProxyPageAgent, - ChromeExtensionPageBridgeSide, + ChromeExtensionPageBrowserSide, ERROR_CODE_NOT_IMPLEMENTED_AS_DESIGNED, }; diff --git a/packages/web-integration/tests/unit-test/extension/bridge-io-agent.test.ts b/packages/web-integration/tests/unit-test/extension/bridge-io-agent.test.ts new file mode 100644 index 000000000..419884ee2 --- /dev/null +++ b/packages/web-integration/tests/unit-test/extension/bridge-io-agent.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { ChromeExtensionProxyPageAgent } from '@/chrome-extension/agent'; +import { getBridgePageInCliSide } from '@/chrome-extension/bridge-page-cli-side'; + +describe('fully functional agent in server(cli) side', () => { + it('basic', async () => { + const page = getBridgePageInCliSide(); + expect(page).toBeDefined(); + + // server should be destroyed as well + await page.destroy(); + }); + + it( + 'run', + async () => { + const page = getBridgePageInCliSide(); + + // make sure the extension bridge is launched before timeout + await page.connectNewTabWithUrl('https://www.baidu.com'); + console.log('connected !'); + + const agent = new ChromeExtensionProxyPageAgent(page); + + console.log('will call aiAction'); + await agent.aiAction('tap "百度一下"'); + console.log('will destroy'); + await agent.destroy(); + }, + 30 * 1000, + ); +}); diff --git a/packages/web-integration/tests/unit-test/extension/bridge-io.test.ts b/packages/web-integration/tests/unit-test/extension/bridge-io.test.ts index a83aff2a1..6c8758f89 100644 --- a/packages/web-integration/tests/unit-test/extension/bridge-io.test.ts +++ b/packages/web-integration/tests/unit-test/extension/bridge-io.test.ts @@ -1,7 +1,6 @@ -import { describe, expect, it } from 'vitest'; - import { BridgeClient } from '@/chrome-extension/bridge-io-client'; import { BridgeServer } from '@/chrome-extension/bridge-io-server'; +import { describe, expect, it, vi } from 'vitest'; let testPort = 1234; describe('bridge-io', () => { @@ -65,7 +64,49 @@ describe('bridge-io', () => { client.disconnect(); }); - it('flush all calls', async () => { + it('client call error', async () => { + const port = testPort++; + const server = new BridgeServer(port); + const errMsg = 'internal error'; + server.listen(); + + const client = new BridgeClient( + `ws://localhost:${port}`, + (method, args) => { + return Promise.reject(new Error(errMsg)); + }, + ); + + await client.connect(); + // await server.call('test', ['a', 'b']); + expect(server.call('test', ['a', 'b'])).rejects.toThrow(errMsg); + }); + + it('client disconnect event', async () => { + const port = testPort++; + const server = new BridgeServer(port); + server.listen(); + + const fn = vi.fn(); + + const client = new BridgeClient( + `ws://localhost:${port}`, + (method, args) => { + return Promise.resolve('ok'); + }, + fn, + ); + + await client.connect(); + + await server.close(); + + // sleep 1s + await new Promise((resolve) => setTimeout(resolve, 1000)); + expect(fn).toHaveBeenCalled(); + }); + + it('flush all calls before connecting', async () => { const port = testPort++; const server = new BridgeServer(port); server.listen(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d946363b8..c5d2a1c5d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -135,7 +135,7 @@ importers: version: 5.0.4 vitest: specifier: ^1.6.0 - version: 1.6.0(@types/node@18.19.62)(jsdom@24.1.1)(sass-embedded@1.80.5)(terser@5.36.0) + version: 1.6.0(@types/node@18.19.62)(jsdom@24.1.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(sass-embedded@1.80.5)(terser@5.36.0) yargs: specifier: 17.7.2 version: 17.7.2 @@ -184,7 +184,7 @@ importers: version: 5.0.4 vitest: specifier: ^1.6.0 - version: 1.6.0(@types/node@18.19.62)(jsdom@24.1.1)(sass-embedded@1.80.5)(terser@5.36.0) + version: 1.6.0(@types/node@18.19.62)(jsdom@24.1.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(sass-embedded@1.80.5)(terser@5.36.0) packages/shared: dependencies: @@ -206,9 +206,13 @@ importers: version: 5.0.4 vitest: specifier: ^1.6.0 - version: 1.6.0(@types/node@18.19.62)(jsdom@24.1.1)(sass-embedded@1.80.5)(terser@5.36.0) + version: 1.6.0(@types/node@18.19.62)(jsdom@24.1.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(sass-embedded@1.80.5)(terser@5.36.0) packages/visualizer: + dependencies: + buffer: + specifier: 6.0.3 + version: 6.0.3 devDependencies: '@ant-design/icons': specifier: 5.3.7 @@ -321,9 +325,6 @@ importers: socket.io: specifier: 4.8.1 version: 4.8.1(bufferutil@4.0.9)(utf-8-validate@6.0.5) - socket.io-client: - specifier: 4.8.1 - version: 4.8.1(bufferutil@4.0.9)(utf-8-validate@6.0.5) devDependencies: '@modern-js/module-tools': specifier: 2.60.6 @@ -378,7 +379,7 @@ importers: version: 5.0.4 vitest: specifier: ^1.6.0 - version: 1.6.0(@types/node@18.19.62)(jsdom@24.1.1)(sass-embedded@1.80.5)(terser@5.36.0) + version: 1.6.0(@types/node@18.19.62)(jsdom@24.1.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(sass-embedded@1.80.5)(terser@5.36.0) webdriverio: specifier: 9.0.6 version: 9.0.6(bufferutil@4.0.9)(utf-8-validate@6.0.5) @@ -5102,9 +5103,6 @@ packages: end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} - engine.io-client@6.6.2: - resolution: {integrity: sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw==} - engine.io-parser@5.2.3: resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} engines: {node: '>=10.0.0'} @@ -8940,10 +8938,6 @@ packages: socket.io-adapter@2.5.5: resolution: {integrity: sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==} - socket.io-client@4.8.1: - resolution: {integrity: sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==} - engines: {node: '>=10.0.0'} - socket.io-parser@4.2.4: resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} engines: {node: '>=10.0.0'} @@ -9968,10 +9962,6 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} - xmlhttprequest-ssl@2.1.2: - resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} - engines: {node: '>=0.4.0'} - xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -14372,7 +14362,7 @@ snapshots: '@types/express-serve-static-core@4.19.6': dependencies: '@types/node': 18.19.62 - '@types/qs': 6.9.16 + '@types/qs': 6.9.17 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -14517,8 +14507,7 @@ snapshots: '@types/qs@6.9.16': {} - '@types/qs@6.9.17': - optional: true + '@types/qs@6.9.17': {} '@types/range-parser@1.2.7': {} @@ -16328,18 +16317,6 @@ snapshots: dependencies: once: 1.4.0 - engine.io-client@6.6.2(bufferutil@4.0.9)(utf-8-validate@6.0.5): - dependencies: - '@socket.io/component-emitter': 3.1.2 - debug: 4.3.7(supports-color@5.5.0) - engine.io-parser: 5.2.3 - ws: 8.17.1(bufferutil@4.0.9)(utf-8-validate@6.0.5) - xmlhttprequest-ssl: 2.1.2 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - engine.io-parser@5.2.3: {} engine.io@6.6.2(bufferutil@4.0.9)(utf-8-validate@6.0.5): @@ -21113,17 +21090,6 @@ snapshots: - supports-color - utf-8-validate - socket.io-client@4.8.1(bufferutil@4.0.9)(utf-8-validate@6.0.5): - dependencies: - '@socket.io/component-emitter': 3.1.2 - debug: 4.3.7(supports-color@5.5.0) - engine.io-client: 6.6.2(bufferutil@4.0.9)(utf-8-validate@6.0.5) - socket.io-parser: 4.2.4 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - socket.io-parser@4.2.4: dependencies: '@socket.io/component-emitter': 3.1.2 @@ -21988,7 +21954,7 @@ snapshots: sass-embedded: 1.80.5 terser: 5.36.0 - vitest@1.6.0(@types/node@18.19.62)(jsdom@24.1.1)(sass-embedded@1.80.5)(terser@5.36.0): + vitest@1.6.0(@types/node@18.19.62)(jsdom@24.1.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(sass-embedded@1.80.5)(terser@5.36.0): dependencies: '@vitest/expect': 1.6.0 '@vitest/runner': 1.6.0 @@ -22281,8 +22247,6 @@ snapshots: xmlchars@2.2.0: {} - xmlhttprequest-ssl@2.1.2: {} - xtend@4.0.2: {} y18n@4.0.3: {} From b75005ee11d22f388c063719a2bf057945917e11 Mon Sep 17 00:00:00 2001 From: yutao Date: Mon, 30 Dec 2024 17:51:27 +0800 Subject: [PATCH 06/40] chore: global polyfill --- packages/visualizer/src/init.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 packages/visualizer/src/init.ts diff --git a/packages/visualizer/src/init.ts b/packages/visualizer/src/init.ts new file mode 100644 index 000000000..38e33301c --- /dev/null +++ b/packages/visualizer/src/init.ts @@ -0,0 +1,12 @@ +// biome-ignore lint/style/useNodejsImportProtocol: +import { Buffer } from 'buffer'; + +window.global ||= window; +window.Buffer = Buffer; + +let sideEffect = 0; + +export const setSideEffect = () => { + sideEffect++; + return sideEffect; +}; From 43f9d17b2eecde778f6916852838884544ebf0bd Mon Sep 17 00:00:00 2001 From: yutao Date: Mon, 30 Dec 2024 22:02:17 +0800 Subject: [PATCH 07/40] feat: bridge panel --- packages/visualizer/src/component/common.less | 3 +- .../visualizer/src/component/env-config.tsx | 5 +- packages/visualizer/src/component/logo.less | 8 +- packages/visualizer/src/component/logo.tsx | 23 ++++- .../src/component/playground-component.less | 2 +- packages/visualizer/src/component/store.tsx | 6 ++ packages/visualizer/src/extension/bridge.tsx | 68 ++++++++++----- .../src/extension/playground-entry.tsx | 2 - packages/visualizer/src/extension/popup.less | 20 ++++- packages/visualizer/src/extension/popup.tsx | 86 +++++++++++++------ packages/visualizer/src/index.tsx | 1 - 11 files changed, 164 insertions(+), 60 deletions(-) diff --git a/packages/visualizer/src/component/common.less b/packages/visualizer/src/component/common.less index e1b64474f..98febc65a 100644 --- a/packages/visualizer/src/component/common.less +++ b/packages/visualizer/src/component/common.less @@ -11,8 +11,9 @@ @selected-bg: #bfc4da80; @hover-bg: #dcdcdc80; -@weak-text: #777; @weak-bg: #F3F3F3; +@weak-text: #777; +@footer-text: #CCC; @toolbar-btn-bg: #E9E9E9; diff --git a/packages/visualizer/src/component/env-config.tsx b/packages/visualizer/src/component/env-config.tsx index 22a59249f..c43d9c33a 100644 --- a/packages/visualizer/src/component/env-config.tsx +++ b/packages/visualizer/src/component/env-config.tsx @@ -8,6 +8,7 @@ export function EnvConfig() { const [isModalOpen, setIsModalOpen] = useState(false); const [tempConfigString, setTempConfigString] = useState(configString); + const popupTab = useEnvConfig((state) => state.popupTab); const showModal = (e: React.MouseEvent) => { setIsModalOpen(true); e.preventDefault(); @@ -36,9 +37,9 @@ export function EnvConfig() { {iconForStatus('failed')} No config

+

+ In Bridge Mode, you can control this browser by the Midscene SDK running + in the local terminal.{' '} +

+

+ This is useful for interacting both through scripts and manually, or to + reuse cookies. +

+ +
+
+

Bridge Status

+

{bridgeStatus}

+ +
+
); } diff --git a/packages/visualizer/src/extension/playground-entry.tsx b/packages/visualizer/src/extension/playground-entry.tsx index b8f1e20d4..6ee9f71b6 100644 --- a/packages/visualizer/src/extension/playground-entry.tsx +++ b/packages/visualizer/src/extension/playground-entry.tsx @@ -1,5 +1,3 @@ -import '../init'; - import queryString from 'query-string'; import type { WebUIContext } from '@midscene/web/utils'; diff --git a/packages/visualizer/src/extension/popup.less b/packages/visualizer/src/extension/popup.less index e9bd32f04..fa4c8b825 100644 --- a/packages/visualizer/src/extension/popup.less +++ b/packages/visualizer/src/extension/popup.less @@ -7,6 +7,7 @@ body { font-size: 14px; } +@footer-height: 40px; .popup-wrapper { width: 100%; height: 100%; @@ -16,6 +17,15 @@ body { display: flex; flex-direction: column; + .tabs-container { + flex-grow: 1; + } + + .ant-tabs-nav { + padding: 0 @layout-extension-space-horizontal; + box-sizing: border-box; + } + .popup-header { padding: 0 @layout-extension-space-horizontal; } @@ -28,12 +38,18 @@ body { box-sizing: border-box; } - .popup-playground-container { + .popup-playground-container, + .popup-bridge-container { flex-grow: 1; } + .popup-bridge-container { + padding: 0 @layout-extension-space-horizontal; + box-sizing: border-box; + } + .popup-footer { - color: @weak-text; + color: @footer-text; text-align: center; width: 100%; } diff --git a/packages/visualizer/src/extension/popup.tsx b/packages/visualizer/src/extension/popup.tsx index 2724b016f..fd547f730 100644 --- a/packages/visualizer/src/extension/popup.tsx +++ b/packages/visualizer/src/extension/popup.tsx @@ -1,4 +1,4 @@ -import { Button, ConfigProvider, message } from 'antd'; +import { Button, ConfigProvider, Tabs, message } from 'antd'; import ReactDOM from 'react-dom/client'; import { setSideEffect } from '../init'; /// @@ -20,7 +20,8 @@ import { extensionAgentForTabId, } from '@/component/playground-component'; import { useChromeTabInfo } from '@/component/store'; -import { SendOutlined } from '@ant-design/icons'; +import { useEnvConfig } from '@/component/store'; +import { ApiOutlined, SendOutlined } from '@ant-design/icons'; import type { ChromeExtensionProxyPageAgent } from '@midscene/web/chrome-extension'; import { useEffect, useState } from 'react'; import Bridge from './bridge'; @@ -49,10 +50,26 @@ const shotAndOpenPlayground = async ( }); }; +// { +// /*

+// To keep the current page context, you can also{' '} +// +//

*/ +// } + function PlaygroundPopup() { const [loading, setLoading] = useState(false); const extensionVersion = getExtensionVersion(); const { tabId, windowId } = useChromeTabInfo(); + const { popupTab, setPopupTab } = useEnvConfig(); const handleSendToPlayground = async () => { if (!tabId || !windowId) { @@ -70,44 +87,57 @@ function PlaygroundPopup() { setLoading(false); }; + const items = [ + { + key: 'playground', + label: 'Playground', + icon: , + children: ( +
+ { + return extensionAgentForTabId(tabId); + }} + showContextPreview={false} + /> +
+ ), + }, + { + key: 'bridge', + label: 'Bridge Mode', + children: ( +
+ +
+ ), + icon: , + }, + ]; + return (
- +

- Midscene.js helps to automate browser actions, perform assertions, - and extract data in JSON format using natural language.{' '} + Automate browser actions, perform assertions, and extract data by + AI. Also offers a javascript SDK.{' '} Learn more

-

This is a panel for experimenting with Midscene.js.

-

- To keep the current page context, you can also{' '} - -

- - -
-
- { - return extensionAgentForTabId(tabId); - }} - showContextPreview={false} +
+ setPopupTab(key as 'playground' | 'bridge')} />
+

Midscene.js Chrome Extension v{extensionVersion}

diff --git a/packages/visualizer/src/index.tsx b/packages/visualizer/src/index.tsx index f979e12ce..214d8aa54 100644 --- a/packages/visualizer/src/index.tsx +++ b/packages/visualizer/src/index.tsx @@ -1,5 +1,4 @@ import './index.less'; -import './init'; import DetailSide from '@/component/detail-side'; import Sidebar from '@/component/sidebar'; import { useExecutionDump } from '@/component/store'; From ce224697ce49d82572453b31aa0d1ef0ee3f71cb Mon Sep 17 00:00:00 2001 From: yutao Date: Tue, 31 Dec 2024 10:12:48 +0800 Subject: [PATCH 08/40] feat: update bridge style --- apps/site/docs/en/index.mdx | 2 +- packages/visualizer/src/extension/bridge.less | 25 ++++++ packages/visualizer/src/extension/bridge.tsx | 90 +++++++++++++------ packages/visualizer/src/extension/popup.tsx | 5 +- 4 files changed, 93 insertions(+), 29 deletions(-) create mode 100644 packages/visualizer/src/extension/bridge.less diff --git a/apps/site/docs/en/index.mdx b/apps/site/docs/en/index.mdx index 69a44ad7d..41895bb03 100644 --- a/apps/site/docs/en/index.mdx +++ b/apps/site/docs/en/index.mdx @@ -46,7 +46,7 @@ await aiAssert("There is a category filter on the left"); ## Multiple ways to integrate -To start experiencing the core feature of Midscene, we recommend you use [The Chrome Extension](./quick-experience). You can call Action / Query / Assert by natural language on any webpage, without needing to set up a code project. +To start experiencing the core feature of Midscene, we recommend you use [the Chrome Extension](./quick-experience). You can call Action / Query / Assert by natural language on any webpage, without needing to set up a code project. Also, there are several ways to integrate Midscene into your code project: diff --git a/packages/visualizer/src/extension/bridge.less b/packages/visualizer/src/extension/bridge.less new file mode 100644 index 000000000..7101c1ca3 --- /dev/null +++ b/packages/visualizer/src/extension/bridge.less @@ -0,0 +1,25 @@ +@import '../component/common.less'; + +.bridge-status-bar { + height: 56px; + line-height: 56px; + font-weight: bold; + display: flex; + flex-direction: row; + justify-content: space-between; + box-sizing: border-box; + padding: 0 10px; + border: 1px solid @footer-text; + border-radius: 5px; + + .bridge-status-btn { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + + .bridge-log-container { + flex-grow: 1; + } +} diff --git a/packages/visualizer/src/extension/bridge.tsx b/packages/visualizer/src/extension/bridge.tsx index 20e052e0a..937ce1bb8 100644 --- a/packages/visualizer/src/extension/bridge.tsx +++ b/packages/visualizer/src/extension/bridge.tsx @@ -1,7 +1,8 @@ +import { LoadingOutlined } from '@ant-design/icons'; import { ChromeExtensionPageBrowserSide } from '@midscene/web/chrome-extension'; -import { Button } from 'antd'; +import { Button, Spin } from 'antd'; import { useEffect, useMemo, useState } from 'react'; - +import './bridge.less'; export default function Bridge() { const [bridgePage, setBridgePage] = useState(null); @@ -10,6 +11,12 @@ export default function Bridge() { 'closed' | 'open-for-connection' | 'connected' >('closed'); + useEffect(() => { + if (bridgeStatus === 'connected') { + bridgePage?.destroy(); + } + }, [bridgeStatus]); + const startConnection = async () => { const bridgePage = new ChromeExtensionPageBrowserSide(() => { setBridgeStatus('closed'); @@ -27,17 +34,43 @@ export default function Bridge() { } }; - const btnText = useMemo(() => { - if (bridgeStatus === 'open-for-connection') { - return 'Waiting for Connection...'; - } - if (bridgeStatus === 'connected') { - return 'Connected'; + const stopListening = () => { + console.warn('not implemented'); + }; + + const stopConnection = () => { + if (bridgePage) { + bridgePage.destroy(); } + }; - // closed - return 'Allow Connection'; - }, [bridgeStatus]); + let statusText: any; + let statusBtn: any; + if (bridgeStatus === 'closed') { + statusText = 'Closed'; + statusBtn = ( + + ); + } else if (bridgeStatus === 'open-for-connection') { + statusText = ( + + } size="small" /> + {' '} + + Listening for Connection... + + + ); + statusBtn = ; + } else if (bridgeStatus === 'connected') { + statusText = Connected; + statusBtn = ; + } else { + statusText = Unknown Status - {bridgeStatus}; + statusBtn = ; + } return (
@@ -53,21 +86,26 @@ export default function Bridge() {

Bridge Status

-

{bridgeStatus}

- +
+
{statusText}
+
{statusBtn}
+
+
+
+

Bridge Log

+
+
+
12:00:00
+
+
+ Bridge Connected +
+
+ Bridge connected successfully +
+
+
+
diff --git a/packages/visualizer/src/extension/popup.tsx b/packages/visualizer/src/extension/popup.tsx index fd547f730..0f5f33a21 100644 --- a/packages/visualizer/src/extension/popup.tsx +++ b/packages/visualizer/src/extension/popup.tsx @@ -122,8 +122,9 @@ function PlaygroundPopup() {

- Automate browser actions, perform assertions, and extract data by - AI. Also offers a javascript SDK.{' '} + Automate browser actions, extract data, and perform assertions using + AI, including a Chrome extension, JavaScript SDK, and support for + scripting in YAML.{' '} Learn more From 82ea46654c278792ac8ac4599855ac584936478f Mon Sep 17 00:00:00 2001 From: yutao Date: Tue, 31 Dec 2024 11:51:17 +0800 Subject: [PATCH 09/40] feat: add bridge files --- packages/visualizer/src/extension/bridge.less | 1 - packages/visualizer/src/extension/bridge.tsx | 104 +++++++++++------- .../src/chrome-extension/bridge-io-server.ts | 3 +- ...ge-page.ts => bridge-page-browser-side.ts} | 19 +--- .../chrome-extension/bridge-page-cli-side.ts | 2 +- .../src/chrome-extension/index.ts | 2 +- 6 files changed, 73 insertions(+), 58 deletions(-) rename packages/web-integration/src/chrome-extension/{bridge-page.ts => bridge-page-browser-side.ts} (78%) diff --git a/packages/visualizer/src/extension/bridge.less b/packages/visualizer/src/extension/bridge.less index 7101c1ca3..a18c9f667 100644 --- a/packages/visualizer/src/extension/bridge.less +++ b/packages/visualizer/src/extension/bridge.less @@ -3,7 +3,6 @@ .bridge-status-bar { height: 56px; line-height: 56px; - font-weight: bold; display: flex; flex-direction: row; justify-content: space-between; diff --git a/packages/visualizer/src/extension/bridge.tsx b/packages/visualizer/src/extension/bridge.tsx index 937ce1bb8..4e9ab1429 100644 --- a/packages/visualizer/src/extension/bridge.tsx +++ b/packages/visualizer/src/extension/bridge.tsx @@ -1,47 +1,80 @@ import { LoadingOutlined } from '@ant-design/icons'; import { ChromeExtensionPageBrowserSide } from '@midscene/web/chrome-extension'; import { Button, Spin } from 'antd'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useState } from 'react'; import './bridge.less'; +import dayjs from 'dayjs'; + +interface BridgeLogItem { + time: string; + content: string; +} + +const connectTimeout = 30 * 1000; +const connectRetryInterval = 300; export default function Bridge() { - const [bridgePage, setBridgePage] = + const [activeBridgePage, setActiveBridgePage] = useState(null); const [bridgeStatus, setBridgeStatus] = useState< 'closed' | 'open-for-connection' | 'connected' >('closed'); + const [bridgeLog, setBridgeLog] = useState([]); + const appendBridgeLog = (content: string) => { + setBridgeLog((prev) => [ + ...prev, + { + time: dayjs().format('HH:mm:ss.SSS'), + content, + }, + ]); + }; + useEffect(() => { if (bridgeStatus === 'connected') { - bridgePage?.destroy(); + activeBridgePage?.destroy(); } }, [bridgeStatus]); - const startConnection = async () => { - const bridgePage = new ChromeExtensionPageBrowserSide(() => { - setBridgeStatus('closed'); - }); - try { - setBridgeStatus('open-for-connection'); - await bridgePage.connect(); - console.log('bridgePage connected !', bridgePage); - setBridgePage(bridgePage); - setBridgeStatus('connected'); - } catch (e) { - // TODO: log error - console.error(e); - setBridgeStatus('closed'); + const stopConnection = () => { + if (activeBridgePage) { + activeBridgePage.destroy(); } + setBridgeStatus('closed'); + setActiveBridgePage(null); }; - const stopListening = () => { - console.warn('not implemented'); + const startConnection = async (timeout = connectTimeout) => { + if (activeBridgePage) { + console.error('activeBridgePage', activeBridgePage); + throw new Error('There is already a connection, cannot start a new one'); + } + const startTime = Date.now(); + setBridgeStatus('open-for-connection'); + while (Date.now() - startTime < timeout) { + try { + const activeBridgePage = new ChromeExtensionPageBrowserSide(() => { + stopConnection(); + }); + await activeBridgePage.connect(); + setActiveBridgePage(activeBridgePage); + setBridgeStatus('connected'); + appendBridgeLog('Bridge connected'); + return; + } catch (e) { + // console.warn('failed to connect to bridge server', e); + } + console.log('waiting for connection...'); + await new Promise((resolve) => setTimeout(resolve, connectRetryInterval)); + } + + setBridgeStatus('closed'); + appendBridgeLog('No connection found within timeout'); }; - const stopConnection = () => { - if (bridgePage) { - bridgePage.destroy(); - } + const stopListening = () => { + console.warn('not implemented'); }; let statusText: any; @@ -49,7 +82,7 @@ export default function Bridge() { if (bridgeStatus === 'closed') { statusText = 'Closed'; statusBtn = ( - ); @@ -72,6 +105,15 @@ export default function Bridge() { statusBtn = ; } + const logs = bridgeLog.map((log) => { + return ( +

+
{log.time}
+
{log.content}
+
+ ); + }); + return (

@@ -93,19 +135,7 @@ export default function Bridge() {

Bridge Log

-
-
-
12:00:00
-
-
- Bridge Connected -
-
- Bridge connected successfully -
-
-
-
+
{logs}
diff --git a/packages/web-integration/src/chrome-extension/bridge-io-server.ts b/packages/web-integration/src/chrome-extension/bridge-io-server.ts index 5c0cd5548..0802b313f 100644 --- a/packages/web-integration/src/chrome-extension/bridge-io-server.ts +++ b/packages/web-integration/src/chrome-extension/bridge-io-server.ts @@ -52,8 +52,7 @@ export class BridgeServer { call.response = response; call.responseTime = Date.now(); - // console.log('callback', call.method, call.error, response); - + console.log('callback', call.method, call.error, response); call.callback(call.error, response); }); diff --git a/packages/web-integration/src/chrome-extension/bridge-page.ts b/packages/web-integration/src/chrome-extension/bridge-page-browser-side.ts similarity index 78% rename from packages/web-integration/src/chrome-extension/bridge-page.ts rename to packages/web-integration/src/chrome-extension/bridge-page-browser-side.ts index cf80d8fb1..12832729d 100644 --- a/packages/web-integration/src/chrome-extension/bridge-page.ts +++ b/packages/web-integration/src/chrome-extension/bridge-page-browser-side.ts @@ -1,6 +1,5 @@ import assert from 'node:assert'; -import { MouseAction } from '@/page'; -import { KeyboardAction } from '@/page'; +import type { KeyboardAction, MouseAction } from '@/page'; import { DefaultBridgeServerPort } from './bridge-common'; import { BridgeClient } from './bridge-io-client'; import ChromeExtensionProxyPage from './page'; @@ -50,20 +49,8 @@ export class ChromeExtensionPageBrowserSide extends ChromeExtensionProxyPage { await this.bridgeClient.connect(); } - public async connect(timeout = 30 * 1000) { - const startTime = Date.now(); - while (Date.now() - startTime < timeout) { - try { - await this.setupBridgeClient(); - console.log('bridge client connected'); - return; - } catch (e) { - console.error('failed to connect to bridge server', e); - } - // wait for 300ms before retrying - await new Promise((resolve) => setTimeout(resolve, 300)); - } - throw new Error(`failed to connect to bridge server after ${timeout}ms`); + public async connect() { + return await this.setupBridgeClient(); } public async connectNewTabWithUrl(url: string) { diff --git a/packages/web-integration/src/chrome-extension/bridge-page-cli-side.ts b/packages/web-integration/src/chrome-extension/bridge-page-cli-side.ts index a46f15fa5..4deb1657a 100644 --- a/packages/web-integration/src/chrome-extension/bridge-page-cli-side.ts +++ b/packages/web-integration/src/chrome-extension/bridge-page-cli-side.ts @@ -2,7 +2,7 @@ import assert from 'node:assert'; import type { KeyboardAction, MouseAction } from '@/page'; import { DefaultBridgeServerPort } from './bridge-common'; import { BridgeServer } from './bridge-io-server'; -import type { ChromeExtensionPageBrowserSide } from './bridge-page'; +import type { ChromeExtensionPageBrowserSide } from './bridge-page-browser-side'; // TODO: handle the connection timeout export const getBridgePageInCliSide = (): ChromeExtensionPageBrowserSide => { diff --git a/packages/web-integration/src/chrome-extension/index.ts b/packages/web-integration/src/chrome-extension/index.ts index a69bee8a5..3b0bdf0bb 100644 --- a/packages/web-integration/src/chrome-extension/index.ts +++ b/packages/web-integration/src/chrome-extension/index.ts @@ -1,7 +1,7 @@ import { ERROR_CODE_NOT_IMPLEMENTED_AS_DESIGNED } from '../common/utils'; import { ChromeExtensionProxyPageAgent } from './agent'; // import { getBridgePageInCliSide } from './bridge-page-cli-side'; -import { ChromeExtensionPageBrowserSide } from './bridge-page'; +import { ChromeExtensionPageBrowserSide } from './bridge-page-browser-side'; import ChromeExtensionProxyPage from './page'; export { From a4756d6705f8742b71b2ced7f0e2fef2014fe2dc Mon Sep 17 00:00:00 2001 From: yutao Date: Tue, 31 Dec 2024 15:56:11 +0800 Subject: [PATCH 10/40] feat: add bridge files --- packages/visualizer/src/extension/bridge.tsx | 12 +++++----- packages/visualizer/src/index.tsx | 3 ++- .../src/chrome-extension/bridge-io-server.ts | 22 +++++++++++-------- .../bridge-page-browser-side.ts | 2 +- .../chrome-extension/bridge-page-cli-side.ts | 1 - .../extension/bridge-io-agent.test.ts | 4 ---- 6 files changed, 23 insertions(+), 21 deletions(-) diff --git a/packages/visualizer/src/extension/bridge.tsx b/packages/visualizer/src/extension/bridge.tsx index 4e9ab1429..014343349 100644 --- a/packages/visualizer/src/extension/bridge.tsx +++ b/packages/visualizer/src/extension/bridge.tsx @@ -32,9 +32,11 @@ export default function Bridge() { }; useEffect(() => { - if (bridgeStatus === 'connected') { - activeBridgePage?.destroy(); - } + return () => { + if (bridgeStatus === 'connected') { + activeBridgePage?.destroy(); + } + }; }, [bridgeStatus]); const stopConnection = () => { @@ -63,9 +65,9 @@ export default function Bridge() { appendBridgeLog('Bridge connected'); return; } catch (e) { - // console.warn('failed to connect to bridge server', e); + console.warn('failed to connect to bridge server', e); } - console.log('waiting for connection...'); + console.log('will retry...'); await new Promise((resolve) => setTimeout(resolve, connectRetryInterval)); } diff --git a/packages/visualizer/src/index.tsx b/packages/visualizer/src/index.tsx index 214d8aa54..224fb4b8c 100644 --- a/packages/visualizer/src/index.tsx +++ b/packages/visualizer/src/index.tsx @@ -1,4 +1,6 @@ import './index.less'; +import { setSideEffect } from './init'; + import DetailSide from '@/component/detail-side'; import Sidebar from '@/component/sidebar'; import { useExecutionDump } from '@/component/store'; @@ -26,7 +28,6 @@ import Logo from './component/logo'; import { iconForStatus, timeCostStrElement } from './component/misc'; import Player from './component/player'; import Timeline from './component/timeline'; -import { setSideEffect } from './init'; setSideEffect(); diff --git a/packages/web-integration/src/chrome-extension/bridge-io-server.ts b/packages/web-integration/src/chrome-extension/bridge-io-server.ts index 0802b313f..17352e75a 100644 --- a/packages/web-integration/src/chrome-extension/bridge-io-server.ts +++ b/packages/web-integration/src/chrome-extension/bridge-io-server.ts @@ -19,7 +19,7 @@ export class BridgeServer { constructor(public port: number) {} - async listen(timeout = 30000): Promise { + async listen(timeout = 10000): Promise { return new Promise((resolve, reject) => { const timeoutListener = setTimeout(() => { reject( @@ -31,6 +31,8 @@ export class BridgeServer { this.io = new Server(this.port); this.io.on('connection', (socket) => { + clearTimeout(timeoutListener); + if (this.socket) { console.log('server already connected, refusing new connection'); socket.emit(BridgeRefusedEvent); @@ -39,7 +41,6 @@ export class BridgeServer { try { console.log('one client connected'); this.socket = socket; - socket.emit(BridgeConnectedEvent); socket.on(BridgeCallResponseEvent, (params: BridgeCallResponse) => { const id = params.id; @@ -56,14 +57,17 @@ export class BridgeServer { call.callback(call.error, response); }); - // flush all calls - for (const id in this.calls) { - if (this.calls[id].callTime === 0) { - this.emitCall(id); - } - } + setTimeout(() => { + socket.emit(BridgeConnectedEvent); + Promise.resolve().then(() => { + for (const id in this.calls) { + if (this.calls[id].callTime === 0) { + this.emitCall(id); + } + } + }); + }, 0); - clearTimeout(timeoutListener); resolve(); } catch (e) { console.error('failed to handle connection event', e); diff --git a/packages/web-integration/src/chrome-extension/bridge-page-browser-side.ts b/packages/web-integration/src/chrome-extension/bridge-page-browser-side.ts index 12832729d..951b54c26 100644 --- a/packages/web-integration/src/chrome-extension/bridge-page-browser-side.ts +++ b/packages/web-integration/src/chrome-extension/bridge-page-browser-side.ts @@ -71,9 +71,9 @@ export class ChromeExtensionPageBrowserSide extends ChromeExtensionProxyPage { if (this.bridgeClient) { this.bridgeClient.disconnect(); this.bridgeClient = null; + this.onDisconnect(); } super.destroy(); this.tabId = 0; - this.onDisconnect(); } } diff --git a/packages/web-integration/src/chrome-extension/bridge-page-cli-side.ts b/packages/web-integration/src/chrome-extension/bridge-page-cli-side.ts index 4deb1657a..d9ae602df 100644 --- a/packages/web-integration/src/chrome-extension/bridge-page-cli-side.ts +++ b/packages/web-integration/src/chrome-extension/bridge-page-cli-side.ts @@ -7,7 +7,6 @@ import type { ChromeExtensionPageBrowserSide } from './bridge-page-browser-side' // TODO: handle the connection timeout export const getBridgePageInCliSide = (): ChromeExtensionPageBrowserSide => { const server = new BridgeServer(DefaultBridgeServerPort); - server.listen(); const bridgeCaller = (method: string) => { return async (...args: any[]) => { diff --git a/packages/web-integration/tests/unit-test/extension/bridge-io-agent.test.ts b/packages/web-integration/tests/unit-test/extension/bridge-io-agent.test.ts index 419884ee2..488be12f9 100644 --- a/packages/web-integration/tests/unit-test/extension/bridge-io-agent.test.ts +++ b/packages/web-integration/tests/unit-test/extension/bridge-io-agent.test.ts @@ -19,13 +19,9 @@ describe('fully functional agent in server(cli) side', () => { // make sure the extension bridge is launched before timeout await page.connectNewTabWithUrl('https://www.baidu.com'); - console.log('connected !'); const agent = new ChromeExtensionProxyPageAgent(page); - - console.log('will call aiAction'); await agent.aiAction('tap "百度一下"'); - console.log('will destroy'); await agent.destroy(); }, 30 * 1000, From 044cdc6349c325ac51e71b0277e7c2905cbb0b42 Mon Sep 17 00:00:00 2001 From: yutao Date: Tue, 31 Dec 2024 22:09:19 +0800 Subject: [PATCH 11/40] fix: bridge io --- packages/visualizer/src/extension/bridge.tsx | 54 +++++++++++++------ .../src/chrome-extension/bridge-io-server.ts | 9 ++-- .../bridge-page-browser-side.ts | 12 ++++- .../chrome-extension/bridge-page-cli-side.ts | 2 +- .../extension/bridge-io-agent.test.ts | 2 +- 5 files changed, 56 insertions(+), 23 deletions(-) diff --git a/packages/visualizer/src/extension/bridge.tsx b/packages/visualizer/src/extension/bridge.tsx index 014343349..06015ab1d 100644 --- a/packages/visualizer/src/extension/bridge.tsx +++ b/packages/visualizer/src/extension/bridge.tsx @@ -1,7 +1,11 @@ -import { LoadingOutlined } from '@ant-design/icons'; +import { + CheckOutlined, + CloseOutlined, + LoadingOutlined, +} from '@ant-design/icons'; import { ChromeExtensionPageBrowserSide } from '@midscene/web/chrome-extension'; import { Button, Spin } from 'antd'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import './bridge.less'; import dayjs from 'dayjs'; @@ -47,15 +51,26 @@ export default function Bridge() { setActiveBridgePage(null); }; + const stopListeningFlag = useRef(false); + const stopListening = () => { + stopListeningFlag.current = true; + }; + const startConnection = async (timeout = connectTimeout) => { if (activeBridgePage) { console.error('activeBridgePage', activeBridgePage); throw new Error('There is already a connection, cannot start a new one'); } const startTime = Date.now(); + appendBridgeLog('Start listening for connection'); setBridgeStatus('open-for-connection'); + stopListeningFlag.current = false; + while (Date.now() - startTime < timeout) { try { + if (stopListeningFlag.current) { + break; + } const activeBridgePage = new ChromeExtensionPageBrowserSide(() => { stopConnection(); }); @@ -75,21 +90,23 @@ export default function Bridge() { appendBridgeLog('No connection found within timeout'); }; - const stopListening = () => { - console.warn('not implemented'); - }; - - let statusText: any; + let statusElement: any; let statusBtn: any; if (bridgeStatus === 'closed') { - statusText = 'Closed'; + statusElement = ( + + + {' '} + Closed + + ); statusBtn = ( ); } else if (bridgeStatus === 'open-for-connection') { - statusText = ( + statusElement = ( } size="small" /> {' '} @@ -100,18 +117,25 @@ export default function Bridge() { ); statusBtn = ; } else if (bridgeStatus === 'connected') { - statusText = Connected; + statusElement = ( + + + {' '} + Connected + + ); statusBtn = ; } else { - statusText = Unknown Status - {bridgeStatus}; + statusElement = Unknown Status - {bridgeStatus}; statusBtn = ; } - const logs = bridgeLog.map((log) => { + const logs = [...bridgeLog].reverse().map((log) => { return (
-
{log.time}
-
{log.content}
+
+ {log.time} - {log.content} +
); }); @@ -131,7 +155,7 @@ export default function Bridge() {

Bridge Status

-
{statusText}
+
{statusElement}
{statusBtn}
diff --git a/packages/web-integration/src/chrome-extension/bridge-io-server.ts b/packages/web-integration/src/chrome-extension/bridge-io-server.ts index 17352e75a..a4c24bfdb 100644 --- a/packages/web-integration/src/chrome-extension/bridge-io-server.ts +++ b/packages/web-integration/src/chrome-extension/bridge-io-server.ts @@ -15,13 +15,14 @@ export class BridgeServer { private callId = 0; private io: Server | null = null; private socket: ServerSocket | null = null; + private listeningTimeoutId: NodeJS.Timeout | null = null; public calls: Record = {}; constructor(public port: number) {} - async listen(timeout = 10000): Promise { + async listen(timeout = 30000): Promise { return new Promise((resolve, reject) => { - const timeoutListener = setTimeout(() => { + this.listeningTimeoutId = setTimeout(() => { reject( new Error( `no client connected after ${timeout}ms (${BridgeErrorCodeNoClientConnected})`, @@ -31,7 +32,7 @@ export class BridgeServer { this.io = new Server(this.port); this.io.on('connection', (socket) => { - clearTimeout(timeoutListener); + this.listeningTimeoutId && clearTimeout(this.listeningTimeoutId); if (this.socket) { console.log('server already connected, refusing new connection'); @@ -53,7 +54,6 @@ export class BridgeServer { call.response = response; call.responseTime = Date.now(); - console.log('callback', call.method, call.error, response); call.callback(call.error, response); }); @@ -133,6 +133,7 @@ export class BridgeServer { } close() { + this.listeningTimeoutId && clearTimeout(this.listeningTimeoutId); this.io?.close(); this.io = null; } diff --git a/packages/web-integration/src/chrome-extension/bridge-page-browser-side.ts b/packages/web-integration/src/chrome-extension/bridge-page-browser-side.ts index 951b54c26..19de4b21a 100644 --- a/packages/web-integration/src/chrome-extension/bridge-page-browser-side.ts +++ b/packages/web-integration/src/chrome-extension/bridge-page-browser-side.ts @@ -7,7 +7,10 @@ import ChromeExtensionProxyPage from './page'; export class ChromeExtensionPageBrowserSide extends ChromeExtensionProxyPage { public bridgeClient: BridgeClient | null = null; - constructor(public onDisconnect: () => void = () => {}) { + constructor( + public onDisconnect: () => void = () => {}, + public onStatusMessage: (message: string) => void = () => {}, + ) { super(0); } @@ -37,12 +40,16 @@ export class ChromeExtensionPageBrowserSide extends ChromeExtensionProxyPage { return this.keyboard[actionName].apply(this.keyboard, args as any); } + // TODO: property white list + // const properties = Object.getOwnPropertyNames(this).concat( + // Object.getOwnPropertyNames(ChromeExtensionProxyPage.prototype), + // ); + // console.log('properties', properties); // @ts-expect-error return this[method as keyof ChromeExtensionProxyPage](...args); }, // on disconnect () => { - this.bridgeClient = null; return this.destroy(); }, ); @@ -60,6 +67,7 @@ export class ChromeExtensionPageBrowserSide extends ChromeExtensionProxyPage { } // new tab + this.onStatusMessage(`creating new tab with url: ${url}`); const tab = await chrome.tabs.create({ url }); const tabId = tab.id; assert(tabId, 'failed to get tabId after creating a new tab'); diff --git a/packages/web-integration/src/chrome-extension/bridge-page-cli-side.ts b/packages/web-integration/src/chrome-extension/bridge-page-cli-side.ts index d9ae602df..43fa48aa8 100644 --- a/packages/web-integration/src/chrome-extension/bridge-page-cli-side.ts +++ b/packages/web-integration/src/chrome-extension/bridge-page-cli-side.ts @@ -28,7 +28,7 @@ export const getBridgePageInCliSide = (): ChromeExtensionPageBrowserSide => { }; } - if (prop === '_forceUsePageContext') { + if (prop === '_forceUsePageContext' || prop === 'waitUntilNetworkIdle') { return undefined; } diff --git a/packages/web-integration/tests/unit-test/extension/bridge-io-agent.test.ts b/packages/web-integration/tests/unit-test/extension/bridge-io-agent.test.ts index 488be12f9..e652ab8d1 100644 --- a/packages/web-integration/tests/unit-test/extension/bridge-io-agent.test.ts +++ b/packages/web-integration/tests/unit-test/extension/bridge-io-agent.test.ts @@ -21,7 +21,7 @@ describe('fully functional agent in server(cli) side', () => { await page.connectNewTabWithUrl('https://www.baidu.com'); const agent = new ChromeExtensionProxyPageAgent(page); - await agent.aiAction('tap "百度一下"'); + await agent.aiAction('type "AI 101" and tap "百度一下"'); await agent.destroy(); }, 30 * 1000, From db7ed5fff0a18910c758510294f07092170988ba Mon Sep 17 00:00:00 2001 From: yutao Date: Thu, 2 Jan 2025 19:07:04 +0800 Subject: [PATCH 12/40] feat: update bridge implementation --- packages/midscene/package.json | 9 +-- .../midscene/src/ai-model/openai/index.ts | 1 + packages/visualizer/package.json | 17 +---- packages/visualizer/src/component/common.less | 2 +- packages/visualizer/src/component/logo.less | 6 +- packages/visualizer/src/component/logo.tsx | 5 +- packages/visualizer/src/component/misc.tsx | 1 + packages/visualizer/src/extension/bridge.tsx | 66 ++++++++++------ .../src/chrome-extension/bridge-common.ts | 4 +- .../src/chrome-extension/bridge-io-server.ts | 76 ++++++++++++++++--- .../bridge-page-browser-side.ts | 13 ++-- .../chrome-extension/bridge-page-cli-side.ts | 3 +- .../src/chrome-extension/index.ts | 2 +- .../src/chrome-extension/page.ts | 30 +++++++- .../src/extractor/web-extractor.ts | 8 +- .../extension/bridge-io-agent.test.ts | 63 ++++++++++----- .../unit-test/extension/bridge-io.test.ts | 21 +++++ pnpm-lock.yaml | 24 ++---- 18 files changed, 242 insertions(+), 109 deletions(-) diff --git a/packages/midscene/package.json b/packages/midscene/package.json index 557e8deee..4679c18f7 100644 --- a/packages/midscene/package.json +++ b/packages/midscene/package.json @@ -37,18 +37,17 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@anthropic-ai/sdk": "0.33.1", "@azure/identity": "4.5.0", + "@anthropic-ai/sdk": "0.33.1", "@midscene/shared": "workspace:*", - "dirty-json": "0.9.2", - "openai": "4.57.1", - "optional": "0.1.4", - "socks-proxy-agent": "8.0.4" + "socks-proxy-agent": "8.0.4", + "openai": "4.57.1" }, "devDependencies": { "@modern-js/module-tools": "2.60.6", "@types/node": "^18.0.0", "@types/node-fetch": "2.6.11", + "dirty-json": "0.9.2", "dotenv": "16.4.5", "langsmith": "0.1.36", "typescript": "~5.0.4", diff --git a/packages/midscene/src/ai-model/openai/index.ts b/packages/midscene/src/ai-model/openai/index.ts index ecdc6f44f..8c4a38603 100644 --- a/packages/midscene/src/ai-model/openai/index.ts +++ b/packages/midscene/src/ai-model/openai/index.ts @@ -112,6 +112,7 @@ async function createChatClient(): Promise<{ endpoint: getAIConfig(AZURE_OPENAI_ENDPOINT), apiVersion: getAIConfig(AZURE_OPENAI_API_VERSION), deployment: getAIConfig(AZURE_OPENAI_DEPLOYMENT), + dangerouslyAllowBrowser: true, ...extraConfig, ...extraAzureConfig, }); diff --git a/packages/visualizer/package.json b/packages/visualizer/package.json index 606dadcc1..3aad07f02 100644 --- a/packages/visualizer/package.json +++ b/packages/visualizer/package.json @@ -6,16 +6,10 @@ "types": "./dist/types/index.d.ts", "main": "./dist/lib/index.js", "module": "./dist/es/index.js", - "files": [ - "dist", - "html", - "README.md" - ], + "files": ["dist", "html", "README.md"], "watch": { "build": { - "patterns": [ - "src" - ], + "patterns": ["src"], "extensions": "tsx,less,scss,css,js,jsx,ts", "quiet": false } @@ -59,12 +53,7 @@ "typescript": "~5.0.4", "zustand": "4.5.2" }, - "sideEffects": [ - "**/*.css", - "**/*.less", - "**/*.sass", - "**/*.scss" - ], + "sideEffects": ["**/*.css", "**/*.less", "**/*.sass", "**/*.scss"], "publishConfig": { "access": "public" }, diff --git a/packages/visualizer/src/component/common.less b/packages/visualizer/src/component/common.less index 98febc65a..7d2d02e4c 100644 --- a/packages/visualizer/src/component/common.less +++ b/packages/visualizer/src/component/common.less @@ -24,4 +24,4 @@ @layout-extension-space-horizontal: 20px; -@layout-extension-space-vertical: 30px; +@layout-extension-space-vertical: 20px; diff --git a/packages/visualizer/src/component/logo.less b/packages/visualizer/src/component/logo.less index e7043a4a7..d7e859ba1 100644 --- a/packages/visualizer/src/component/logo.less +++ b/packages/visualizer/src/component/logo.less @@ -5,8 +5,12 @@ vertical-align: -webkit-baseline-middle; } -.logo-with-star { +.logo-with-star-wrapper { display: flex; flex-direction: row; justify-content: space-between; + + .github-star { + height: 22px; + } } diff --git a/packages/visualizer/src/component/logo.tsx b/packages/visualizer/src/component/logo.tsx index 60531a105..c8faaa5ac 100644 --- a/packages/visualizer/src/component/logo.tsx +++ b/packages/visualizer/src/component/logo.tsx @@ -1,9 +1,9 @@ import './logo.less'; -const Logo = ({ withGithubStar = true }: { withGithubStar?: boolean }) => { +const Logo = ({ withGithubStar = false }: { withGithubStar?: boolean }) => { if (withGithubStar) { return ( -
+
Midscene_logo { rel="noreferrer" > Github star diff --git a/packages/visualizer/src/component/misc.tsx b/packages/visualizer/src/component/misc.tsx index cefe65cc3..fa26fe755 100644 --- a/packages/visualizer/src/component/misc.tsx +++ b/packages/visualizer/src/component/misc.tsx @@ -51,6 +51,7 @@ export const iconForStatus = (status: string): JSX.Element => { ); case 'failed': + case 'closed': case 'timedOut': case 'interrupted': return ( diff --git a/packages/visualizer/src/extension/bridge.tsx b/packages/visualizer/src/extension/bridge.tsx index 06015ab1d..e77016285 100644 --- a/packages/visualizer/src/extension/bridge.tsx +++ b/packages/visualizer/src/extension/bridge.tsx @@ -7,6 +7,7 @@ import { ChromeExtensionPageBrowserSide } from '@midscene/web/chrome-extension'; import { Button, Spin } from 'antd'; import { useEffect, useRef, useState } from 'react'; import './bridge.less'; +import { iconForStatus } from '@/component/misc'; import dayjs from 'dayjs'; interface BridgeLogItem { @@ -17,8 +18,9 @@ interface BridgeLogItem { const connectTimeout = 30 * 1000; const connectRetryInterval = 300; export default function Bridge() { - const [activeBridgePage, setActiveBridgePage] = - useState(null); + const activeBridgePageRef = useRef( + null, + ); const [bridgeStatus, setBridgeStatus] = useState< 'closed' | 'open-for-connection' | 'connected' @@ -35,20 +37,21 @@ export default function Bridge() { ]); }; + const destroyBridgePage = () => {}; + useEffect(() => { return () => { - if (bridgeStatus === 'connected') { - activeBridgePage?.destroy(); - } + destroyBridgePage(); }; - }, [bridgeStatus]); + }, []); const stopConnection = () => { - if (activeBridgePage) { - activeBridgePage.destroy(); + if (activeBridgePageRef.current) { + appendBridgeLog('bridge page destroyed'); + activeBridgePageRef.current.destroy(); + activeBridgePageRef.current = null; } setBridgeStatus('closed'); - setActiveBridgePage(null); }; const stopListeningFlag = useRef(false); @@ -57,12 +60,13 @@ export default function Bridge() { }; const startConnection = async (timeout = connectTimeout) => { - if (activeBridgePage) { - console.error('activeBridgePage', activeBridgePage); + if (activeBridgePageRef.current) { + console.error('activeBridgePage', activeBridgePageRef.current); throw new Error('There is already a connection, cannot start a new one'); } const startTime = Date.now(); - appendBridgeLog('Start listening for connection'); + setBridgeLog([]); + appendBridgeLog('listening for connection...'); setBridgeStatus('open-for-connection'); stopListeningFlag.current = false; @@ -71,13 +75,18 @@ export default function Bridge() { if (stopListeningFlag.current) { break; } - const activeBridgePage = new ChromeExtensionPageBrowserSide(() => { - stopConnection(); - }); + const activeBridgePage = new ChromeExtensionPageBrowserSide( + () => { + stopConnection(); + }, + (message) => { + appendBridgeLog(message); + }, + ); await activeBridgePage.connect(); - setActiveBridgePage(activeBridgePage); + activeBridgePageRef.current = activeBridgePage; setBridgeStatus('connected'); - appendBridgeLog('Bridge connected'); + appendBridgeLog('bridge connected'); return; } catch (e) { console.warn('failed to connect to bridge server', e); @@ -95,7 +104,7 @@ export default function Bridge() { if (bridgeStatus === 'closed') { statusElement = ( - + {iconForStatus('closed')} {' '} Closed @@ -119,7 +128,7 @@ export default function Bridge() { } else if (bridgeStatus === 'connected') { statusElement = ( - + {iconForStatus('connected')} {' '} Connected @@ -130,10 +139,16 @@ export default function Bridge() { statusBtn = ; } - const logs = [...bridgeLog].reverse().map((log) => { + const logs = [...bridgeLog].reverse().map((log, index) => { return ( -
-
+
+
{log.time} - {log.content}
@@ -160,7 +175,12 @@ export default function Bridge() {
-

Bridge Log

+

+ Bridge Log{' '} + +

{logs}
diff --git a/packages/web-integration/src/chrome-extension/bridge-common.ts b/packages/web-integration/src/chrome-extension/bridge-common.ts index 7b3bae383..ed160b5e0 100644 --- a/packages/web-integration/src/chrome-extension/bridge-common.ts +++ b/packages/web-integration/src/chrome-extension/bridge-common.ts @@ -19,13 +19,13 @@ export interface BridgeCall { } export interface BridgeCallRequest { - id: number; + id: string; method: string; args: any[]; } export interface BridgeCallResponse { - id: number; + id: string; response: any; error?: any; } diff --git a/packages/web-integration/src/chrome-extension/bridge-io-server.ts b/packages/web-integration/src/chrome-extension/bridge-io-server.ts index a4c24bfdb..f9a3b31db 100644 --- a/packages/web-integration/src/chrome-extension/bridge-io-server.ts +++ b/packages/web-integration/src/chrome-extension/bridge-io-server.ts @@ -18,20 +18,31 @@ export class BridgeServer { private listeningTimeoutId: NodeJS.Timeout | null = null; public calls: Record = {}; - constructor(public port: number) {} + private connectionLost = false; + private connectionLostReason = ''; + + constructor( + public port: number, + public onConnect?: () => void, + public onDisconnect?: (reason: string) => void, + ) {} async listen(timeout = 30000): Promise { return new Promise((resolve, reject) => { this.listeningTimeoutId = setTimeout(() => { reject( new Error( - `no client connected after ${timeout}ms (${BridgeErrorCodeNoClientConnected})`, + `no extension connected after ${timeout}ms (${BridgeErrorCodeNoClientConnected})`, ), ); }, timeout); - this.io = new Server(this.port); + this.io = new Server(this.port, { + maxHttpBufferSize: 100 * 1024 * 1024, // 100MB + }); this.io.on('connection', (socket) => { + this.connectionLost = false; + this.connectionLostReason = ''; this.listeningTimeoutId && clearTimeout(this.listeningTimeoutId); if (this.socket) { @@ -40,24 +51,39 @@ export class BridgeServer { reject(new Error('server already connected by another client')); } try { - console.log('one client connected'); + // console.log('one client connected'); this.socket = socket; socket.on(BridgeCallResponseEvent, (params: BridgeCallResponse) => { const id = params.id; const response = params.response; - const call = this.calls[id]; - if (!call) { - throw new Error(`call ${id} not found`); - } - call.error = params.error; - call.response = response; - call.responseTime = Date.now(); + const error = params.error; - call.callback(call.error, response); + this.triggerCallResponseCallback(id, error, response); + }); + + socket.on('disconnect', (reason: string) => { + this.connectionLost = true; + this.connectionLostReason = reason; + this.onDisconnect?.(reason); + + // flush all pending calls as error + for (const id in this.calls) { + const call = this.calls[id]; + + if (!call.responseTime) { + const errorMessage = this.connectionLostErrorMsg(); + this.triggerCallResponseCallback( + id, + new Error(errorMessage), + null, + ); + } + } }); setTimeout(() => { + this.onConnect?.(); socket.emit(BridgeConnectedEvent); Promise.resolve().then(() => { for (const id in this.calls) { @@ -77,12 +103,38 @@ export class BridgeServer { }); } + private connectionLostErrorMsg = () => { + return `Connection lost, reason: ${this.connectionLostReason}`; + }; + + private async triggerCallResponseCallback( + id: string | number, + error: Error | null, + response: any, + ) { + const call = this.calls[id]; + if (!call) { + throw new Error(`call ${id} not found`); + } + call.error = error || undefined; + call.response = response; + call.responseTime = Date.now(); + + call.callback(call.error, response); + } + private async emitCall(id: string) { const call = this.calls[id]; if (!call) { throw new Error(`call ${id} not found`); } + if (this.connectionLost) { + const message = `Connection lost, reason: ${this.connectionLostReason}`; + call.callback(new Error(message), null); + return; + } + if (this.socket) { this.socket.emit(BridgeCallEvent, { id, diff --git a/packages/web-integration/src/chrome-extension/bridge-page-browser-side.ts b/packages/web-integration/src/chrome-extension/bridge-page-browser-side.ts index 19de4b21a..dcde9cef6 100644 --- a/packages/web-integration/src/chrome-extension/bridge-page-browser-side.ts +++ b/packages/web-integration/src/chrome-extension/bridge-page-browser-side.ts @@ -30,6 +30,8 @@ export class ChromeExtensionPageBrowserSide extends ChromeExtensionProxyPage { throw new Error('no tab is connected'); } + this.onStatusMessage(`calling method: ${method}`); + if (method.startsWith('mouse.')) { const actionName = method.split('.')[1] as keyof MouseAction; return this.mouse[actionName].apply(this.mouse, args as any); @@ -40,11 +42,6 @@ export class ChromeExtensionPageBrowserSide extends ChromeExtensionProxyPage { return this.keyboard[actionName].apply(this.keyboard, args as any); } - // TODO: property white list - // const properties = Object.getOwnPropertyNames(this).concat( - // Object.getOwnPropertyNames(ChromeExtensionProxyPage.prototype), - // ); - // console.log('properties', properties); // @ts-expect-error return this[method as keyof ChromeExtensionProxyPage](...args); }, @@ -66,13 +63,13 @@ export class ChromeExtensionPageBrowserSide extends ChromeExtensionProxyPage { throw new Error('tab is already connected'); } - // new tab - this.onStatusMessage(`creating new tab with url: ${url}`); const tab = await chrome.tabs.create({ url }); const tabId = tab.id; assert(tabId, 'failed to get tabId after creating a new tab'); - this.tabId = tabId; + + // new tab + this.onStatusMessage(`creating new tab with url: ${url}`); } async destroy() { diff --git a/packages/web-integration/src/chrome-extension/bridge-page-cli-side.ts b/packages/web-integration/src/chrome-extension/bridge-page-cli-side.ts index 43fa48aa8..84224eb51 100644 --- a/packages/web-integration/src/chrome-extension/bridge-page-cli-side.ts +++ b/packages/web-integration/src/chrome-extension/bridge-page-cli-side.ts @@ -4,7 +4,6 @@ import { DefaultBridgeServerPort } from './bridge-common'; import { BridgeServer } from './bridge-io-server'; import type { ChromeExtensionPageBrowserSide } from './bridge-page-browser-side'; -// TODO: handle the connection timeout export const getBridgePageInCliSide = (): ChromeExtensionPageBrowserSide => { const server = new BridgeServer(DefaultBridgeServerPort); server.listen(); @@ -28,7 +27,7 @@ export const getBridgePageInCliSide = (): ChromeExtensionPageBrowserSide => { }; } - if (prop === '_forceUsePageContext' || prop === 'waitUntilNetworkIdle') { + if (prop === '_forceUsePageContext') { return undefined; } diff --git a/packages/web-integration/src/chrome-extension/index.ts b/packages/web-integration/src/chrome-extension/index.ts index 3b0bdf0bb..74eb296e1 100644 --- a/packages/web-integration/src/chrome-extension/index.ts +++ b/packages/web-integration/src/chrome-extension/index.ts @@ -1,8 +1,8 @@ import { ERROR_CODE_NOT_IMPLEMENTED_AS_DESIGNED } from '../common/utils'; import { ChromeExtensionProxyPageAgent } from './agent'; -// import { getBridgePageInCliSide } from './bridge-page-cli-side'; import { ChromeExtensionPageBrowserSide } from './bridge-page-browser-side'; import ChromeExtensionProxyPage from './page'; +// import { getBridgePageInCliSide } from './bridge-page-cli-side'; export { // getBridgePageInCliSide, diff --git a/packages/web-integration/src/chrome-extension/page.ts b/packages/web-integration/src/chrome-extension/page.ts index bc6b7aa53..85812c2ce 100644 --- a/packages/web-integration/src/chrome-extension/page.ts +++ b/packages/web-integration/src/chrome-extension/page.ts @@ -122,12 +122,40 @@ export default class ChromeExtensionProxyPage implements AbstractPage { }); if (!returnValue.result.value) { - throw new Error('Failed to get page content from page'); + const errorDescription = + returnValue.exceptionDetails?.exception?.description || ''; + if (!errorDescription) { + console.error('returnValue from cdp', returnValue); + } + throw new Error( + `Failed to get page content from page, error: ${errorDescription}`, + ); } // console.log('returnValue', returnValue.result.value); return returnValue.result.value; } + // current implementation is wait until domReadyState is complete + public async waitUntilNetworkIdle() { + const timeout = 10000; + const startTime = Date.now(); + let lastReadyState = ''; + while (Date.now() - startTime < timeout) { + const result = await this.sendCommandToDebugger('Runtime.evaluate', { + expression: 'document.readyState', + }); + lastReadyState = result.result.value; + if (lastReadyState === 'complete') { + await new Promise((resolve) => setTimeout(resolve, 300)); + return; + } + await new Promise((resolve) => setTimeout(resolve, 300)); + } + throw new Error( + `Failed to wait until network idle, last readyState: ${lastReadyState}`, + ); + } + async getElementInfos() { const content = await this.getPageContentByCDP(); if (content?.size) { diff --git a/packages/web-integration/src/extractor/web-extractor.ts b/packages/web-integration/src/extractor/web-extractor.ts index 70f7fffa2..2a0a94fc3 100644 --- a/packages/web-integration/src/extractor/web-extractor.ts +++ b/packages/web-integration/src/extractor/web-extractor.ts @@ -265,6 +265,11 @@ export function extractTextWithPosition( return null; } + if (node.nodeType && node.nodeType === 10) { + // Doctype node + return null; + } + const elementInfo = collectElementInfo(node, nodePath, baseZoom); // stop collecting if the node is a Button or Image if ( @@ -291,7 +296,8 @@ export function extractTextWithPosition( return elementInfo; } - dfs(initNode || getDocument(), '0'); + const rootNode = initNode || getDocument(); + dfs(rootNode, '0'); if (currentFrame.left !== 0 || currentFrame.top !== 0) { for (let i = 0; i < elementInfoArray.length; i++) { diff --git a/packages/web-integration/tests/unit-test/extension/bridge-io-agent.test.ts b/packages/web-integration/tests/unit-test/extension/bridge-io-agent.test.ts index e652ab8d1..d774544bf 100644 --- a/packages/web-integration/tests/unit-test/extension/bridge-io-agent.test.ts +++ b/packages/web-integration/tests/unit-test/extension/bridge-io-agent.test.ts @@ -1,29 +1,52 @@ import { describe, expect, it, vi } from 'vitest'; import { ChromeExtensionProxyPageAgent } from '@/chrome-extension/agent'; +import { ChromePageOverBridgeAgent } from '@/chrome-extension/bridge-agent-cli-side'; import { getBridgePageInCliSide } from '@/chrome-extension/bridge-page-cli-side'; -describe('fully functional agent in server(cli) side', () => { - it('basic', async () => { - const page = getBridgePageInCliSide(); - expect(page).toBeDefined(); +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); +describe.skipIf(process.env.CI)( + 'fully functional agent in server(cli) side', + () => { + it('basic', async () => { + const page = getBridgePageInCliSide(); + expect(page).toBeDefined(); - // server should be destroyed as well - await page.destroy(); - }); + // server should be destroyed as well + await page.destroy(); + }); - it( - 'run', - async () => { - const page = getBridgePageInCliSide(); + it( + 'page in cli side', + async () => { + const page = getBridgePageInCliSide(); + + // make sure the extension bridge is launched before timeout + await page.connectNewTabWithUrl('https://www.baidu.com'); + + // sleep 3s + await sleep(3000); + + await page.destroy(); + }, + 40 * 1000, // longer than the timeout of the bridge io + ); + + it.only( + 'agent in cli side', + async () => { + const agent = new ChromePageOverBridgeAgent(); + + await agent.connectNewTabWithUrl('https://www.bing.com'); + await sleep(3000); - // make sure the extension bridge is launched before timeout - await page.connectNewTabWithUrl('https://www.baidu.com'); + await agent.ai('type "AI 101" and tap "Search"'); + await sleep(3000); - const agent = new ChromeExtensionProxyPageAgent(page); - await agent.aiAction('type "AI 101" and tap "百度一下"'); - await agent.destroy(); - }, - 30 * 1000, - ); -}); + await agent.aiAssert('there are some search results'); + await agent.destroy(); + }, + 60 * 1000, + ); + }, +); diff --git a/packages/web-integration/tests/unit-test/extension/bridge-io.test.ts b/packages/web-integration/tests/unit-test/extension/bridge-io.test.ts index 6c8758f89..567325fa1 100644 --- a/packages/web-integration/tests/unit-test/extension/bridge-io.test.ts +++ b/packages/web-integration/tests/unit-test/extension/bridge-io.test.ts @@ -106,6 +106,27 @@ describe('bridge-io', () => { expect(fn).toHaveBeenCalled(); }); + it('client close before server', async () => { + const port = testPort++; + const onConnect = vi.fn(); + const onDisconnect = vi.fn(); + const server = new BridgeServer(port, onConnect, onDisconnect); + server.listen(); + + const client = new BridgeClient(`ws://localhost:${port}`, () => { + return Promise.resolve('ok'); + }); + await client.connect(); + + await new Promise((resolve) => setTimeout(resolve, 500)); + expect(onConnect).toHaveBeenCalled(); + + expect(onDisconnect).not.toHaveBeenCalled(); + await client.disconnect(); + await new Promise((resolve) => setTimeout(resolve, 500)); + expect(onDisconnect).toHaveBeenCalled(); + }); + it('flush all calls before connecting', async () => { const port = testPort++; const server = new BridgeServer(port); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c5d2a1c5d..2e9f386e9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -135,7 +135,7 @@ importers: version: 5.0.4 vitest: specifier: ^1.6.0 - version: 1.6.0(@types/node@18.19.62)(jsdom@24.1.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(sass-embedded@1.80.5)(terser@5.36.0) + version: 1.6.0(@types/node@18.19.62)(jsdom@24.1.1)(sass-embedded@1.80.5)(terser@5.36.0) yargs: specifier: 17.7.2 version: 17.7.2 @@ -151,15 +151,9 @@ importers: '@midscene/shared': specifier: workspace:* version: link:../shared - dirty-json: - specifier: 0.9.2 - version: 0.9.2 openai: specifier: 4.57.1 version: 4.57.1(zod@3.23.8) - optional: - specifier: 0.1.4 - version: 0.1.4 socks-proxy-agent: specifier: 8.0.4 version: 8.0.4 @@ -173,6 +167,9 @@ importers: '@types/node-fetch': specifier: 2.6.11 version: 2.6.11 + dirty-json: + specifier: 0.9.2 + version: 0.9.2 dotenv: specifier: 16.4.5 version: 16.4.5 @@ -184,7 +181,7 @@ importers: version: 5.0.4 vitest: specifier: ^1.6.0 - version: 1.6.0(@types/node@18.19.62)(jsdom@24.1.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(sass-embedded@1.80.5)(terser@5.36.0) + version: 1.6.0(@types/node@18.19.62)(jsdom@24.1.1)(sass-embedded@1.80.5)(terser@5.36.0) packages/shared: dependencies: @@ -206,7 +203,7 @@ importers: version: 5.0.4 vitest: specifier: ^1.6.0 - version: 1.6.0(@types/node@18.19.62)(jsdom@24.1.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(sass-embedded@1.80.5)(terser@5.36.0) + version: 1.6.0(@types/node@18.19.62)(jsdom@24.1.1)(sass-embedded@1.80.5)(terser@5.36.0) packages/visualizer: dependencies: @@ -379,7 +376,7 @@ importers: version: 5.0.4 vitest: specifier: ^1.6.0 - version: 1.6.0(@types/node@18.19.62)(jsdom@24.1.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(sass-embedded@1.80.5)(terser@5.36.0) + version: 1.6.0(@types/node@18.19.62)(jsdom@24.1.1)(sass-embedded@1.80.5)(terser@5.36.0) webdriverio: specifier: 9.0.6 version: 9.0.6(bufferutil@4.0.9)(utf-8-validate@6.0.5) @@ -7280,9 +7277,6 @@ packages: resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} hasBin: true - optional@0.1.4: - resolution: {integrity: sha512-gtvrrCfkE08wKcgXaVwQVgwEQ8vel2dc5DDBn9RLQZ3YtmtkBss6A2HY6BnJH4N/4Ku97Ri/SF8sNWE2225WJw==} - ora@5.3.0: resolution: {integrity: sha512-zAKMgGXUim0Jyd6CXK9lraBnD3H5yPGBPPOkC23a2BG6hsm4Zu6OQSjQuEtV0BHDf4aKHcUFvJiGRrFuW3MG8g==} engines: {node: '>=10'} @@ -19195,8 +19189,6 @@ snapshots: opener@1.5.2: {} - optional@0.1.4: {} - ora@5.3.0: dependencies: bl: 4.1.0 @@ -21954,7 +21946,7 @@ snapshots: sass-embedded: 1.80.5 terser: 5.36.0 - vitest@1.6.0(@types/node@18.19.62)(jsdom@24.1.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(sass-embedded@1.80.5)(terser@5.36.0): + vitest@1.6.0(@types/node@18.19.62)(jsdom@24.1.1)(sass-embedded@1.80.5)(terser@5.36.0): dependencies: '@vitest/expect': 1.6.0 '@vitest/runner': 1.6.0 From 6f3c7c49cf06c09c18ab1421707a9820628d6efd Mon Sep 17 00:00:00 2001 From: yutao Date: Thu, 2 Jan 2025 19:07:53 +0800 Subject: [PATCH 13/40] doc: update doc for yaml scripts --- .../chrome-extension/bridge-agent-cli-side.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 packages/web-integration/src/chrome-extension/bridge-agent-cli-side.ts diff --git a/packages/web-integration/src/chrome-extension/bridge-agent-cli-side.ts b/packages/web-integration/src/chrome-extension/bridge-agent-cli-side.ts new file mode 100644 index 000000000..dd79b1b70 --- /dev/null +++ b/packages/web-integration/src/chrome-extension/bridge-agent-cli-side.ts @@ -0,0 +1,16 @@ +import { PageAgent } from '@/common/agent'; +import type { ChromeExtensionPageBrowserSide } from './bridge-page-browser-side'; +import { getBridgePageInCliSide } from './bridge-page-cli-side'; + +export class ChromePageOverBridgeAgent extends PageAgent { + constructor() { + const page = getBridgePageInCliSide(); + super(page, {}); + } + + async connectNewTabWithUrl(url: string) { + await (this.page as ChromeExtensionPageBrowserSide).connectNewTabWithUrl( + url, + ); + } +} From e8368ff9a10b257eb0e52841da7ecac66ab90bd9 Mon Sep 17 00:00:00 2001 From: yutao Date: Thu, 2 Jan 2025 19:36:31 +0800 Subject: [PATCH 14/40] feat: update folder and build script --- packages/visualizer/src/extension/bridge.tsx | 8 +-- packages/web-integration/modern.config.ts | 2 + packages/web-integration/package.json | 65 +++++++++++++++---- .../agent-cli-side.ts} | 21 +++++- .../src/bridge-mode/browser.ts | 3 + .../common.ts} | 0 .../io-client.ts} | 2 +- .../io-server.ts} | 2 +- .../page-browser-side.ts} | 6 +- .../chrome-extension/bridge-agent-cli-side.ts | 16 ----- .../src/chrome-extension/index.ts | 4 -- .../agent.test.ts} | 12 ++-- .../bridge-io.test.ts => bridge/io.test.ts} | 4 +- 13 files changed, 89 insertions(+), 56 deletions(-) rename packages/web-integration/src/{chrome-extension/bridge-page-cli-side.ts => bridge-mode/agent-cli-side.ts} (76%) create mode 100644 packages/web-integration/src/bridge-mode/browser.ts rename packages/web-integration/src/{chrome-extension/bridge-common.ts => bridge-mode/common.ts} (100%) rename packages/web-integration/src/{chrome-extension/bridge-io-client.ts => bridge-mode/io-client.ts} (98%) rename packages/web-integration/src/{chrome-extension/bridge-io-server.ts => bridge-mode/io-server.ts} (99%) rename packages/web-integration/src/{chrome-extension/bridge-page-browser-side.ts => bridge-mode/page-browser-side.ts} (93%) delete mode 100644 packages/web-integration/src/chrome-extension/bridge-agent-cli-side.ts rename packages/web-integration/tests/unit-test/{extension/bridge-io-agent.test.ts => bridge/agent.test.ts} (79%) rename packages/web-integration/tests/unit-test/{extension/bridge-io.test.ts => bridge/io.test.ts} (97%) diff --git a/packages/visualizer/src/extension/bridge.tsx b/packages/visualizer/src/extension/bridge.tsx index e77016285..9e33def25 100644 --- a/packages/visualizer/src/extension/bridge.tsx +++ b/packages/visualizer/src/extension/bridge.tsx @@ -1,9 +1,5 @@ -import { - CheckOutlined, - CloseOutlined, - LoadingOutlined, -} from '@ant-design/icons'; -import { ChromeExtensionPageBrowserSide } from '@midscene/web/chrome-extension'; +import { LoadingOutlined } from '@ant-design/icons'; +import { ChromeExtensionPageBrowserSide } from '@midscene/web/bridge-mode-browser'; import { Button, Spin } from 'antd'; import { useEffect, useRef, useState } from 'react'; import './bridge.less'; diff --git a/packages/web-integration/modern.config.ts b/packages/web-integration/modern.config.ts index 7b97a8978..84d3a1fe8 100644 --- a/packages/web-integration/modern.config.ts +++ b/packages/web-integration/modern.config.ts @@ -7,6 +7,8 @@ export default defineConfig({ format: 'cjs', input: { index: 'src/index.ts', + 'bridge-mode': 'src/bridge-mode/index.ts', + 'bridge-mode-browser': 'src/bridge-mode/browser.ts', utils: 'src/common/utils.ts', 'ui-utils': 'src/common/ui-utils.ts', debug: 'src/debug/index.ts', diff --git a/packages/web-integration/package.json b/packages/web-integration/package.json index 5e7f2ac5c..9b3f773c9 100644 --- a/packages/web-integration/package.json +++ b/packages/web-integration/package.json @@ -12,6 +12,8 @@ }, "exports": { ".": "./dist/lib/index.js", + "./bridge-mode": "./dist/lib/bridge-mode", + "./bridge-mode-browser": "./dist/lib/bridge-mode-browser.js", "./utils": "./dist/lib/utils.js", "./ui-utils": "./dist/lib/ui-utils.js", "./puppeteer": "./dist/lib/puppeteer.js", @@ -26,18 +28,48 @@ }, "typesVersions": { "*": { - ".": ["./dist/types/index.d.ts"], - "utils": ["./dist/types/utils.d.ts"], - "ui-utils": ["./dist/types/ui-utils.d.ts"], - "puppeteer": ["./dist/types/puppeteer.d.ts"], - "playwright": ["./dist/types/playwright.d.ts"], - "playwright-report": ["./dist/types/playwright-report.d.ts"], - "playground": ["./dist/types/playground.d.ts"], - "debug": ["./dist/types/debug.d.ts"], - "constants": ["./dist/types/constants.d.ts"], - "html-element": ["./dist/types/html-element/index.d.ts"], - "chrome-extension": ["./dist/types/chrome-extension.d.ts"], - "yaml": ["./dist/types/yaml.d.ts"] + ".": [ + "./dist/types/index.d.ts" + ], + "bridge-mode": [ + "./dist/types/bridge-mode.d.ts" + ], + "bridge-mode-browser": [ + "./dist/types/bridge-mode-browser.d.ts" + ], + "utils": [ + "./dist/types/utils.d.ts" + ], + "ui-utils": [ + "./dist/types/ui-utils.d.ts" + ], + "puppeteer": [ + "./dist/types/puppeteer.d.ts" + ], + "playwright": [ + "./dist/types/playwright.d.ts" + ], + "playwright-report": [ + "./dist/types/playwright-report.d.ts" + ], + "playground": [ + "./dist/types/playground.d.ts" + ], + "debug": [ + "./dist/types/debug.d.ts" + ], + "constants": [ + "./dist/types/constants.d.ts" + ], + "html-element": [ + "./dist/types/html-element/index.d.ts" + ], + "chrome-extension": [ + "./dist/types/chrome-extension.d.ts" + ], + "yaml": [ + "./dist/types/yaml.d.ts" + ] } }, "scripts": { @@ -63,7 +95,12 @@ "e2e:generate-test-data": "GENERATE_TEST_DATA=true playwright test ./tests/ai/web/playwright/generate-test-data.spec.ts", "e2e:generate-test-data:headed": "GENERATE_TEST_DATA=true playwright test ./tests/ai/web/playwright/generate-test-data.spec.ts --headed" }, - "files": ["static", "dist", "README.md", "bin"], + "files": [ + "static", + "dist", + "README.md", + "bin" + ], "dependencies": { "@midscene/core": "workspace:*", "@midscene/shared": "workspace:*", @@ -120,4 +157,4 @@ "registry": "https://registry.npmjs.org" }, "license": "MIT" -} +} \ No newline at end of file diff --git a/packages/web-integration/src/chrome-extension/bridge-page-cli-side.ts b/packages/web-integration/src/bridge-mode/agent-cli-side.ts similarity index 76% rename from packages/web-integration/src/chrome-extension/bridge-page-cli-side.ts rename to packages/web-integration/src/bridge-mode/agent-cli-side.ts index 84224eb51..811e721b0 100644 --- a/packages/web-integration/src/chrome-extension/bridge-page-cli-side.ts +++ b/packages/web-integration/src/bridge-mode/agent-cli-side.ts @@ -1,9 +1,11 @@ import assert from 'node:assert'; +import { PageAgent } from '@/common/agent'; import type { KeyboardAction, MouseAction } from '@/page'; -import { DefaultBridgeServerPort } from './bridge-common'; -import { BridgeServer } from './bridge-io-server'; -import type { ChromeExtensionPageBrowserSide } from './bridge-page-browser-side'; +import { DefaultBridgeServerPort } from './common'; +import { BridgeServer } from './io-server'; +import type { ChromeExtensionPageBrowserSide } from './page-browser-side'; +// actually, this is a proxy to the page in browser side export const getBridgePageInCliSide = (): ChromeExtensionPageBrowserSide => { const server = new BridgeServer(DefaultBridgeServerPort); server.listen(); @@ -71,3 +73,16 @@ export const getBridgePageInCliSide = (): ChromeExtensionPageBrowserSide => { }, }) as ChromeExtensionPageBrowserSide; }; + +export class ChromePageOverBridgeAgent extends PageAgent { + constructor() { + const page = getBridgePageInCliSide(); + super(page, {}); + } + + async connectNewTabWithUrl(url: string) { + await (this.page as ChromeExtensionPageBrowserSide).connectNewTabWithUrl( + url, + ); + } +} diff --git a/packages/web-integration/src/bridge-mode/browser.ts b/packages/web-integration/src/bridge-mode/browser.ts new file mode 100644 index 000000000..1cd92cacb --- /dev/null +++ b/packages/web-integration/src/bridge-mode/browser.ts @@ -0,0 +1,3 @@ +import { ChromeExtensionPageBrowserSide } from '../bridge-mode/page-browser-side'; + +export { ChromeExtensionPageBrowserSide }; diff --git a/packages/web-integration/src/chrome-extension/bridge-common.ts b/packages/web-integration/src/bridge-mode/common.ts similarity index 100% rename from packages/web-integration/src/chrome-extension/bridge-common.ts rename to packages/web-integration/src/bridge-mode/common.ts diff --git a/packages/web-integration/src/chrome-extension/bridge-io-client.ts b/packages/web-integration/src/bridge-mode/io-client.ts similarity index 98% rename from packages/web-integration/src/chrome-extension/bridge-io-client.ts rename to packages/web-integration/src/bridge-mode/io-client.ts index 3aa497c0b..4509b0549 100644 --- a/packages/web-integration/src/chrome-extension/bridge-io-client.ts +++ b/packages/web-integration/src/bridge-mode/io-client.ts @@ -7,7 +7,7 @@ import { BridgeCallResponseEvent, BridgeConnectedEvent, BridgeRefusedEvent, -} from './bridge-common'; +} from './common'; // ws client, this is where the request is processed export class BridgeClient { diff --git a/packages/web-integration/src/chrome-extension/bridge-io-server.ts b/packages/web-integration/src/bridge-mode/io-server.ts similarity index 99% rename from packages/web-integration/src/chrome-extension/bridge-io-server.ts rename to packages/web-integration/src/bridge-mode/io-server.ts index f9a3b31db..8b979d3e8 100644 --- a/packages/web-integration/src/chrome-extension/bridge-io-server.ts +++ b/packages/web-integration/src/bridge-mode/io-server.ts @@ -8,7 +8,7 @@ import { BridgeConnectedEvent, BridgeErrorCodeNoClientConnected, BridgeRefusedEvent, -} from './bridge-common'; +} from './common'; // ws server, this is where the request is sent export class BridgeServer { diff --git a/packages/web-integration/src/chrome-extension/bridge-page-browser-side.ts b/packages/web-integration/src/bridge-mode/page-browser-side.ts similarity index 93% rename from packages/web-integration/src/chrome-extension/bridge-page-browser-side.ts rename to packages/web-integration/src/bridge-mode/page-browser-side.ts index dcde9cef6..96de55bcc 100644 --- a/packages/web-integration/src/chrome-extension/bridge-page-browser-side.ts +++ b/packages/web-integration/src/bridge-mode/page-browser-side.ts @@ -1,8 +1,8 @@ import assert from 'node:assert'; import type { KeyboardAction, MouseAction } from '@/page'; -import { DefaultBridgeServerPort } from './bridge-common'; -import { BridgeClient } from './bridge-io-client'; -import ChromeExtensionProxyPage from './page'; +import ChromeExtensionProxyPage from '../chrome-extension/page'; +import { DefaultBridgeServerPort } from './common'; +import { BridgeClient } from './io-client'; export class ChromeExtensionPageBrowserSide extends ChromeExtensionProxyPage { public bridgeClient: BridgeClient | null = null; diff --git a/packages/web-integration/src/chrome-extension/bridge-agent-cli-side.ts b/packages/web-integration/src/chrome-extension/bridge-agent-cli-side.ts deleted file mode 100644 index dd79b1b70..000000000 --- a/packages/web-integration/src/chrome-extension/bridge-agent-cli-side.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { PageAgent } from '@/common/agent'; -import type { ChromeExtensionPageBrowserSide } from './bridge-page-browser-side'; -import { getBridgePageInCliSide } from './bridge-page-cli-side'; - -export class ChromePageOverBridgeAgent extends PageAgent { - constructor() { - const page = getBridgePageInCliSide(); - super(page, {}); - } - - async connectNewTabWithUrl(url: string) { - await (this.page as ChromeExtensionPageBrowserSide).connectNewTabWithUrl( - url, - ); - } -} diff --git a/packages/web-integration/src/chrome-extension/index.ts b/packages/web-integration/src/chrome-extension/index.ts index 74eb296e1..bf7afe270 100644 --- a/packages/web-integration/src/chrome-extension/index.ts +++ b/packages/web-integration/src/chrome-extension/index.ts @@ -1,13 +1,9 @@ import { ERROR_CODE_NOT_IMPLEMENTED_AS_DESIGNED } from '../common/utils'; import { ChromeExtensionProxyPageAgent } from './agent'; -import { ChromeExtensionPageBrowserSide } from './bridge-page-browser-side'; import ChromeExtensionProxyPage from './page'; -// import { getBridgePageInCliSide } from './bridge-page-cli-side'; export { - // getBridgePageInCliSide, ChromeExtensionProxyPage, ChromeExtensionProxyPageAgent, - ChromeExtensionPageBrowserSide, ERROR_CODE_NOT_IMPLEMENTED_AS_DESIGNED, }; diff --git a/packages/web-integration/tests/unit-test/extension/bridge-io-agent.test.ts b/packages/web-integration/tests/unit-test/bridge/agent.test.ts similarity index 79% rename from packages/web-integration/tests/unit-test/extension/bridge-io-agent.test.ts rename to packages/web-integration/tests/unit-test/bridge/agent.test.ts index d774544bf..98371be57 100644 --- a/packages/web-integration/tests/unit-test/extension/bridge-io-agent.test.ts +++ b/packages/web-integration/tests/unit-test/bridge/agent.test.ts @@ -1,8 +1,8 @@ -import { describe, expect, it, vi } from 'vitest'; - -import { ChromeExtensionProxyPageAgent } from '@/chrome-extension/agent'; -import { ChromePageOverBridgeAgent } from '@/chrome-extension/bridge-agent-cli-side'; -import { getBridgePageInCliSide } from '@/chrome-extension/bridge-page-cli-side'; +import { + ChromePageOverBridgeAgent, + getBridgePageInCliSide, +} from '@/bridge-mode/agent-cli-side'; +import { describe, expect, it } from 'vitest'; const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); describe.skipIf(process.env.CI)( @@ -32,7 +32,7 @@ describe.skipIf(process.env.CI)( 40 * 1000, // longer than the timeout of the bridge io ); - it.only( + it( 'agent in cli side', async () => { const agent = new ChromePageOverBridgeAgent(); diff --git a/packages/web-integration/tests/unit-test/extension/bridge-io.test.ts b/packages/web-integration/tests/unit-test/bridge/io.test.ts similarity index 97% rename from packages/web-integration/tests/unit-test/extension/bridge-io.test.ts rename to packages/web-integration/tests/unit-test/bridge/io.test.ts index 567325fa1..25f5bcb53 100644 --- a/packages/web-integration/tests/unit-test/extension/bridge-io.test.ts +++ b/packages/web-integration/tests/unit-test/bridge/io.test.ts @@ -1,5 +1,5 @@ -import { BridgeClient } from '@/chrome-extension/bridge-io-client'; -import { BridgeServer } from '@/chrome-extension/bridge-io-server'; +import { BridgeClient } from '@/bridge-mode/io-client'; +import { BridgeServer } from '@/bridge-mode/io-server'; import { describe, expect, it, vi } from 'vitest'; let testPort = 1234; From 1e00708ecc6639dd4c6837584404d62388f311a1 Mon Sep 17 00:00:00 2001 From: yutao Date: Thu, 2 Jan 2025 19:36:56 +0800 Subject: [PATCH 15/40] feat: add bridge files --- packages/web-integration/src/bridge-mode/index.ts | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 packages/web-integration/src/bridge-mode/index.ts diff --git a/packages/web-integration/src/bridge-mode/index.ts b/packages/web-integration/src/bridge-mode/index.ts new file mode 100644 index 000000000..8e3705d4a --- /dev/null +++ b/packages/web-integration/src/bridge-mode/index.ts @@ -0,0 +1,3 @@ +import { ChromePageOverBridgeAgent } from './agent-cli-side'; + +export { ChromePageOverBridgeAgent }; From 449c396410acb5484de8458e6cf55285871af20e Mon Sep 17 00:00:00 2001 From: yutao Date: Thu, 2 Jan 2025 21:35:35 +0800 Subject: [PATCH 16/40] fix: lint --- packages/web-integration/package.json | 65 +++++++-------------------- 1 file changed, 16 insertions(+), 49 deletions(-) diff --git a/packages/web-integration/package.json b/packages/web-integration/package.json index 9b3f773c9..fd894a440 100644 --- a/packages/web-integration/package.json +++ b/packages/web-integration/package.json @@ -28,48 +28,20 @@ }, "typesVersions": { "*": { - ".": [ - "./dist/types/index.d.ts" - ], - "bridge-mode": [ - "./dist/types/bridge-mode.d.ts" - ], - "bridge-mode-browser": [ - "./dist/types/bridge-mode-browser.d.ts" - ], - "utils": [ - "./dist/types/utils.d.ts" - ], - "ui-utils": [ - "./dist/types/ui-utils.d.ts" - ], - "puppeteer": [ - "./dist/types/puppeteer.d.ts" - ], - "playwright": [ - "./dist/types/playwright.d.ts" - ], - "playwright-report": [ - "./dist/types/playwright-report.d.ts" - ], - "playground": [ - "./dist/types/playground.d.ts" - ], - "debug": [ - "./dist/types/debug.d.ts" - ], - "constants": [ - "./dist/types/constants.d.ts" - ], - "html-element": [ - "./dist/types/html-element/index.d.ts" - ], - "chrome-extension": [ - "./dist/types/chrome-extension.d.ts" - ], - "yaml": [ - "./dist/types/yaml.d.ts" - ] + ".": ["./dist/types/index.d.ts"], + "bridge-mode": ["./dist/types/bridge-mode.d.ts"], + "bridge-mode-browser": ["./dist/types/bridge-mode-browser.d.ts"], + "utils": ["./dist/types/utils.d.ts"], + "ui-utils": ["./dist/types/ui-utils.d.ts"], + "puppeteer": ["./dist/types/puppeteer.d.ts"], + "playwright": ["./dist/types/playwright.d.ts"], + "playwright-report": ["./dist/types/playwright-report.d.ts"], + "playground": ["./dist/types/playground.d.ts"], + "debug": ["./dist/types/debug.d.ts"], + "constants": ["./dist/types/constants.d.ts"], + "html-element": ["./dist/types/html-element/index.d.ts"], + "chrome-extension": ["./dist/types/chrome-extension.d.ts"], + "yaml": ["./dist/types/yaml.d.ts"] } }, "scripts": { @@ -95,12 +67,7 @@ "e2e:generate-test-data": "GENERATE_TEST_DATA=true playwright test ./tests/ai/web/playwright/generate-test-data.spec.ts", "e2e:generate-test-data:headed": "GENERATE_TEST_DATA=true playwright test ./tests/ai/web/playwright/generate-test-data.spec.ts --headed" }, - "files": [ - "static", - "dist", - "README.md", - "bin" - ], + "files": ["static", "dist", "README.md", "bin"], "dependencies": { "@midscene/core": "workspace:*", "@midscene/shared": "workspace:*", @@ -157,4 +124,4 @@ "registry": "https://registry.npmjs.org" }, "license": "MIT" -} \ No newline at end of file +} From aaca87b2087e9233f60193f738923f0da44338a2 Mon Sep 17 00:00:00 2001 From: yutao Date: Fri, 3 Jan 2025 10:24:08 +0800 Subject: [PATCH 17/40] feat: update tips --- .../src/component/playground-component.tsx | 2 - packages/visualizer/src/extension/bridge.tsx | 41 +++++++++++++------ .../src/bridge-mode/agent-cli-side.ts | 40 ++++++++++++++---- .../web-integration/src/bridge-mode/common.ts | 1 + .../src/bridge-mode/io-server.ts | 17 +++++++- .../src/bridge-mode/page-browser-side.ts | 18 ++++++-- packages/web-integration/src/common/agent.ts | 6 +-- .../tests/unit-test/bridge/agent.test.ts | 2 +- .../tests/unit-test/bridge/io.test.ts | 8 ++++ 9 files changed, 104 insertions(+), 31 deletions(-) diff --git a/packages/visualizer/src/component/playground-component.tsx b/packages/visualizer/src/component/playground-component.tsx index f63e896e9..beeb3814f 100644 --- a/packages/visualizer/src/component/playground-component.tsx +++ b/packages/visualizer/src/component/playground-component.tsx @@ -2,8 +2,6 @@ import { DownOutlined, LoadingOutlined, SendOutlined } from '@ant-design/icons'; import type { GroupedActionDump, MidsceneYamlFlowItemAIAction, - MidsceneYamlFlowItemAIQuery, - MidsceneYamlTask, UIContext, } from '@midscene/core'; import { Helmet } from '@modern-js/runtime/head'; diff --git a/packages/visualizer/src/extension/bridge.tsx b/packages/visualizer/src/extension/bridge.tsx index 9e33def25..5c7faf74b 100644 --- a/packages/visualizer/src/extension/bridge.tsx +++ b/packages/visualizer/src/extension/bridge.tsx @@ -23,6 +23,7 @@ export default function Bridge() { >('closed'); const [bridgeLog, setBridgeLog] = useState([]); + const [bridgeAgentStatus, setBridgeAgentStatus] = useState(''); const appendBridgeLog = (content: string) => { setBridgeLog((prev) => [ ...prev, @@ -43,7 +44,7 @@ export default function Bridge() { const stopConnection = () => { if (activeBridgePageRef.current) { - appendBridgeLog('bridge page destroyed'); + appendBridgeLog('Bridge disconnected'); activeBridgePageRef.current.destroy(); activeBridgePageRef.current = null; } @@ -62,7 +63,8 @@ export default function Bridge() { } const startTime = Date.now(); setBridgeLog([]); - appendBridgeLog('listening for connection...'); + setBridgeAgentStatus(''); + appendBridgeLog('Listening for connection...'); setBridgeStatus('open-for-connection'); stopListeningFlag.current = false; @@ -75,14 +77,17 @@ export default function Bridge() { () => { stopConnection(); }, - (message) => { + (message, type) => { appendBridgeLog(message); + if (type === 'status') { + setBridgeAgentStatus(message); + } }, ); await activeBridgePage.connect(); activeBridgePageRef.current = activeBridgePage; setBridgeStatus('connected'); - appendBridgeLog('bridge connected'); + appendBridgeLog('Bridge connected'); return; } catch (e) { console.warn('failed to connect to bridge server', e); @@ -107,7 +112,7 @@ export default function Bridge() { ); statusBtn = ( ); } else if (bridgeStatus === 'open-for-connection') { @@ -116,7 +121,7 @@ export default function Bridge() { } size="small" /> {' '} - Listening for Connection... + Listening for connection... ); @@ -127,6 +132,14 @@ export default function Bridge() { {iconForStatus('connected')} {' '} Connected + + - {bridgeAgentStatus} + ); statusBtn = ; @@ -155,11 +168,8 @@ export default function Bridge() {

In Bridge Mode, you can control this browser by the Midscene SDK running - in the local terminal.{' '} -

-

- This is useful for interacting both through scripts and manually, or to - reuse cookies. + in the local terminal. This is useful for interacting both through + scripts and manually, or to reuse cookies.

@@ -173,7 +183,14 @@ export default function Bridge() {

Bridge Log{' '} -

diff --git a/packages/web-integration/src/bridge-mode/agent-cli-side.ts b/packages/web-integration/src/bridge-mode/agent-cli-side.ts index 811e721b0..c440e50a5 100644 --- a/packages/web-integration/src/bridge-mode/agent-cli-side.ts +++ b/packages/web-integration/src/bridge-mode/agent-cli-side.ts @@ -1,12 +1,20 @@ import assert from 'node:assert'; import { PageAgent } from '@/common/agent'; +import { paramStr, typeStr } from '@/common/ui-utils'; import type { KeyboardAction, MouseAction } from '@/page'; -import { DefaultBridgeServerPort } from './common'; +import { + BridgeUpdateAgentStatusEvent, + DefaultBridgeServerPort, +} from './common'; import { BridgeServer } from './io-server'; import type { ChromeExtensionPageBrowserSide } from './page-browser-side'; +interface ChromeExtensionPageCliSide extends ChromeExtensionPageBrowserSide { + showStatusMessage: (message: string) => Promise; +} + // actually, this is a proxy to the page in browser side -export const getBridgePageInCliSide = (): ChromeExtensionPageBrowserSide => { +export const getBridgePageInCliSide = (): ChromeExtensionPageCliSide => { const server = new BridgeServer(DefaultBridgeServerPort); server.listen(); const bridgeCaller = (method: string) => { @@ -15,7 +23,11 @@ export const getBridgePageInCliSide = (): ChromeExtensionPageBrowserSide => { return response; }; }; - const page = {}; + const page = { + showStatusMessage: async (message: string) => { + await server.call(BridgeUpdateAgentStatusEvent, [message]); + }, + }; return new Proxy(page, { get(target, prop, receiver) { @@ -71,18 +83,30 @@ export const getBridgePageInCliSide = (): ChromeExtensionPageBrowserSide => { return bridgeCaller(prop); }, - }) as ChromeExtensionPageBrowserSide; + }) as ChromeExtensionPageCliSide; }; -export class ChromePageOverBridgeAgent extends PageAgent { +export class ChromePageOverBridgeAgent extends PageAgent { constructor() { const page = getBridgePageInCliSide(); super(page, {}); } async connectNewTabWithUrl(url: string) { - await (this.page as ChromeExtensionPageBrowserSide).connectNewTabWithUrl( - url, - ); + await this.page.connectNewTabWithUrl(url); + } + + async aiAction(prompt: string, options?: any) { + if (options) { + console.warn( + 'the `options` parameter of aiAction is not supported in cli side', + ); + } + return await super.aiAction(prompt, { + onTaskStart: (task) => { + const tip = `${typeStr(task)} - ${paramStr(task)}`; + this.page.showStatusMessage(tip); + }, + }); } } diff --git a/packages/web-integration/src/bridge-mode/common.ts b/packages/web-integration/src/bridge-mode/common.ts index ed160b5e0..faf753b96 100644 --- a/packages/web-integration/src/bridge-mode/common.ts +++ b/packages/web-integration/src/bridge-mode/common.ts @@ -3,6 +3,7 @@ export const DefaultLocalEndpoint = `http://127.0.0.1:${DefaultBridgeServerPort} export const BridgeCallTimeout = 30000; export const BridgeCallEvent = 'bridge-call'; export const BridgeCallResponseEvent = 'bridge-call-response'; +export const BridgeUpdateAgentStatusEvent = 'bridge-update-agent-status'; export const BridgeMessageEvent = 'bridge-message'; export const BridgeConnectedEvent = 'bridge-connected'; export const BridgeRefusedEvent = 'bridge-refused'; diff --git a/packages/web-integration/src/bridge-mode/io-server.ts b/packages/web-integration/src/bridge-mode/io-server.ts index 8b979d3e8..82396419b 100644 --- a/packages/web-integration/src/bridge-mode/io-server.ts +++ b/packages/web-integration/src/bridge-mode/io-server.ts @@ -16,6 +16,7 @@ export class BridgeServer { private io: Server | null = null; private socket: ServerSocket | null = null; private listeningTimeoutId: NodeJS.Timeout | null = null; + private connectionTipTimer: NodeJS.Timeout | null = null; public calls: Record = {}; private connectionLost = false; @@ -29,6 +30,10 @@ export class BridgeServer { async listen(timeout = 30000): Promise { return new Promise((resolve, reject) => { + if (this.listeningTimeoutId) { + return reject(new Error('already listening')); + } + this.listeningTimeoutId = setTimeout(() => { reject( new Error( @@ -37,6 +42,13 @@ export class BridgeServer { ); }, timeout); + this.connectionTipTimer = + timeout > 3000 + ? setTimeout(() => { + console.log('waiting for bridge to connect...'); + }, 2000) + : null; + this.io = new Server(this.port, { maxHttpBufferSize: 100 * 1024 * 1024, // 100MB }); @@ -44,7 +56,9 @@ export class BridgeServer { this.connectionLost = false; this.connectionLostReason = ''; this.listeningTimeoutId && clearTimeout(this.listeningTimeoutId); - + this.listeningTimeoutId = null; + this.connectionTipTimer && clearTimeout(this.connectionTipTimer); + this.connectionTipTimer = null; if (this.socket) { console.log('server already connected, refusing new connection'); socket.emit(BridgeRefusedEvent); @@ -186,6 +200,7 @@ export class BridgeServer { close() { this.listeningTimeoutId && clearTimeout(this.listeningTimeoutId); + this.connectionTipTimer && clearTimeout(this.connectionTipTimer); this.io?.close(); this.io = null; } diff --git a/packages/web-integration/src/bridge-mode/page-browser-side.ts b/packages/web-integration/src/bridge-mode/page-browser-side.ts index 96de55bcc..f2b835e7a 100644 --- a/packages/web-integration/src/bridge-mode/page-browser-side.ts +++ b/packages/web-integration/src/bridge-mode/page-browser-side.ts @@ -1,7 +1,10 @@ import assert from 'node:assert'; import type { KeyboardAction, MouseAction } from '@/page'; import ChromeExtensionProxyPage from '../chrome-extension/page'; -import { DefaultBridgeServerPort } from './common'; +import { + BridgeUpdateAgentStatusEvent, + DefaultBridgeServerPort, +} from './common'; import { BridgeClient } from './io-client'; export class ChromeExtensionPageBrowserSide extends ChromeExtensionProxyPage { @@ -9,7 +12,10 @@ export class ChromeExtensionPageBrowserSide extends ChromeExtensionProxyPage { constructor( public onDisconnect: () => void = () => {}, - public onStatusMessage: (message: string) => void = () => {}, + public onLogMessage: ( + message: string, + type: 'log' | 'status', + ) => void = () => {}, ) { super(0); } @@ -26,11 +32,15 @@ export class ChromeExtensionPageBrowserSide extends ChromeExtensionProxyPage { ); } + if (method === BridgeUpdateAgentStatusEvent) { + return this.onLogMessage(args[0] as string, 'status'); + } + if (!this.tabId || this.tabId === 0) { throw new Error('no tab is connected'); } - this.onStatusMessage(`calling method: ${method}`); + // this.onLogMessage(`calling method: ${method}`); if (method.startsWith('mouse.')) { const actionName = method.split('.')[1] as keyof MouseAction; @@ -69,7 +79,7 @@ export class ChromeExtensionPageBrowserSide extends ChromeExtensionProxyPage { this.tabId = tabId; // new tab - this.onStatusMessage(`creating new tab with url: ${url}`); + this.onLogMessage(`Creating new tab: ${url}`, 'log'); } async destroy() { diff --git a/packages/web-integration/src/common/agent.ts b/packages/web-integration/src/common/agent.ts index 2a9b5460d..d7a55c89b 100644 --- a/packages/web-integration/src/common/agent.ts +++ b/packages/web-integration/src/common/agent.ts @@ -34,8 +34,8 @@ export interface PageAgentOpt { autoPrintReportMsg?: boolean; } -export class PageAgent { - page: WebPage; +export class PageAgent { + page: PageType; insight: Insight; @@ -54,7 +54,7 @@ export class PageAgent { */ dryMode = false; - constructor(page: WebPage, opts?: PageAgentOpt) { + constructor(page: PageType, opts?: PageAgentOpt) { this.page = page; this.opts = Object.assign( { diff --git a/packages/web-integration/tests/unit-test/bridge/agent.test.ts b/packages/web-integration/tests/unit-test/bridge/agent.test.ts index 98371be57..fe994da53 100644 --- a/packages/web-integration/tests/unit-test/bridge/agent.test.ts +++ b/packages/web-integration/tests/unit-test/bridge/agent.test.ts @@ -40,7 +40,7 @@ describe.skipIf(process.env.CI)( await agent.connectNewTabWithUrl('https://www.bing.com'); await sleep(3000); - await agent.ai('type "AI 101" and tap "Search"'); + await agent.ai('type "AI 101" and hit Enter'); await sleep(3000); await agent.aiAssert('there are some search results'); diff --git a/packages/web-integration/tests/unit-test/bridge/io.test.ts b/packages/web-integration/tests/unit-test/bridge/io.test.ts index 25f5bcb53..285a78c71 100644 --- a/packages/web-integration/tests/unit-test/bridge/io.test.ts +++ b/packages/web-integration/tests/unit-test/bridge/io.test.ts @@ -10,6 +10,14 @@ describe('bridge-io', () => { server.close(); }); + it('server already listening', async () => { + const port = testPort++; + const server = new BridgeServer(port); + server.listen(); + await expect(server.listen()).rejects.toThrow(); + server.close(); + }); + it('refuse 2nd client connection', async () => { const port = testPort++; const server = new BridgeServer(port); From 785aca638b632bce3ecefcced811c6ce52411acb Mon Sep 17 00:00:00 2001 From: yutao Date: Fri, 3 Jan 2025 11:00:56 +0800 Subject: [PATCH 18/40] feat: allow connect to current tab --- packages/visualizer/src/extension/bridge.less | 8 +++++ .../src/bridge-mode/agent-cli-side.ts | 4 +++ .../src/bridge-mode/page-browser-side.ts | 36 +++++++++++++++++-- .../tests/unit-test/bridge/agent.test.ts | 19 +++++++++- 4 files changed, 64 insertions(+), 3 deletions(-) diff --git a/packages/visualizer/src/extension/bridge.less b/packages/visualizer/src/extension/bridge.less index a18c9f667..27909d37b 100644 --- a/packages/visualizer/src/extension/bridge.less +++ b/packages/visualizer/src/extension/bridge.less @@ -20,5 +20,13 @@ .bridge-log-container { flex-grow: 1; + + .bridge-log-item-content { + word-wrap: break-word; + white-space: nowrap; + text-overflow: ellipsis; + width: 100%; + overflow: hidden; + } } } diff --git a/packages/web-integration/src/bridge-mode/agent-cli-side.ts b/packages/web-integration/src/bridge-mode/agent-cli-side.ts index c440e50a5..3c17bfb0e 100644 --- a/packages/web-integration/src/bridge-mode/agent-cli-side.ts +++ b/packages/web-integration/src/bridge-mode/agent-cli-side.ts @@ -96,6 +96,10 @@ export class ChromePageOverBridgeAgent extends PageAgent { @@ -82,6 +99,21 @@ export class ChromeExtensionPageBrowserSide extends ChromeExtensionProxyPage { this.onLogMessage(`Creating new tab: ${url}`, 'log'); } + public async connectCurrentTab() { + if (this.tabId) { + throw new Error( + `already connected with tab id ${this.tabId}, cannot reconnect`, + ); + } + const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); + console.log('current tab', tabs); + const tabId = tabs[0]?.id; + assert(tabId, 'failed to get tabId'); + this.tabId = tabId; + + this.onLogMessage(`Connected to current tab: ${tabs[0]?.url}`, 'log'); + } + async destroy() { if (this.bridgeClient) { this.bridgeClient.disconnect(); diff --git a/packages/web-integration/tests/unit-test/bridge/agent.test.ts b/packages/web-integration/tests/unit-test/bridge/agent.test.ts index fe994da53..5bf842242 100644 --- a/packages/web-integration/tests/unit-test/bridge/agent.test.ts +++ b/packages/web-integration/tests/unit-test/bridge/agent.test.ts @@ -33,7 +33,7 @@ describe.skipIf(process.env.CI)( ); it( - 'agent in cli side', + 'agent in cli side, new tab', async () => { const agent = new ChromePageOverBridgeAgent(); @@ -48,5 +48,22 @@ describe.skipIf(process.env.CI)( }, 60 * 1000, ); + + it( + 'agent in cli side, current tab', + async () => { + const agent = new ChromePageOverBridgeAgent(); + await agent.connectCurrentTab(); + await sleep(3000); + const answer = await agent.aiQuery( + 'what is the current page? return {description: string}', + ); + + console.log(answer); + expect(answer.description).toBeTruthy(); + await agent.destroy(); + }, + 60 * 1000, + ); }, ); From a8f11875ef0c5ad7b284d576102fca19aeb9d504 Mon Sep 17 00:00:00 2001 From: yutao Date: Fri, 3 Jan 2025 11:04:41 +0800 Subject: [PATCH 19/40] feat: disable cache for nx build --- nx.json | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/nx.json b/nx.json index 9c805f64a..b0cef872f 100644 --- a/nx.json +++ b/nx.json @@ -2,25 +2,36 @@ "$schema": "./node_modules/nx/schemas/nx-schema.json", "targetDefaults": { "dev": { - "dependsOn": ["^build"] + "dependsOn": [ + "^build" + ] }, "build": { - "dependsOn": ["^build"], - "cache": true + "dependsOn": [ + "^build" + ] }, "build:watch": { - "dependsOn": ["^build"] + "dependsOn": [ + "^build" + ] }, "test": { - "dependsOn": ["^build"], + "dependsOn": [ + "^build" + ], "cache": false }, "e2e": { - "dependsOn": ["^build"] + "dependsOn": [ + "^build" + ] }, "e2e:ui": { - "dependsOn": ["^build"] + "dependsOn": [ + "^build" + ] } }, "defaultBase": "main" -} +} \ No newline at end of file From df284664de08992275c377bd497655a20cac16aa Mon Sep 17 00:00:00 2001 From: yutao Date: Fri, 3 Jan 2025 13:52:51 +0800 Subject: [PATCH 20/40] fix: add the missing deps --- packages/web-integration/package.json | 66 ++++++++++++++----- .../tests/unit-test/bridge/agent.test.ts | 4 +- pnpm-lock.yaml | 39 +++++++++++ 3 files changed, 91 insertions(+), 18 deletions(-) diff --git a/packages/web-integration/package.json b/packages/web-integration/package.json index fd894a440..28ba5588f 100644 --- a/packages/web-integration/package.json +++ b/packages/web-integration/package.json @@ -28,20 +28,48 @@ }, "typesVersions": { "*": { - ".": ["./dist/types/index.d.ts"], - "bridge-mode": ["./dist/types/bridge-mode.d.ts"], - "bridge-mode-browser": ["./dist/types/bridge-mode-browser.d.ts"], - "utils": ["./dist/types/utils.d.ts"], - "ui-utils": ["./dist/types/ui-utils.d.ts"], - "puppeteer": ["./dist/types/puppeteer.d.ts"], - "playwright": ["./dist/types/playwright.d.ts"], - "playwright-report": ["./dist/types/playwright-report.d.ts"], - "playground": ["./dist/types/playground.d.ts"], - "debug": ["./dist/types/debug.d.ts"], - "constants": ["./dist/types/constants.d.ts"], - "html-element": ["./dist/types/html-element/index.d.ts"], - "chrome-extension": ["./dist/types/chrome-extension.d.ts"], - "yaml": ["./dist/types/yaml.d.ts"] + ".": [ + "./dist/types/index.d.ts" + ], + "bridge-mode": [ + "./dist/types/bridge-mode.d.ts" + ], + "bridge-mode-browser": [ + "./dist/types/bridge-mode-browser.d.ts" + ], + "utils": [ + "./dist/types/utils.d.ts" + ], + "ui-utils": [ + "./dist/types/ui-utils.d.ts" + ], + "puppeteer": [ + "./dist/types/puppeteer.d.ts" + ], + "playwright": [ + "./dist/types/playwright.d.ts" + ], + "playwright-report": [ + "./dist/types/playwright-report.d.ts" + ], + "playground": [ + "./dist/types/playground.d.ts" + ], + "debug": [ + "./dist/types/debug.d.ts" + ], + "constants": [ + "./dist/types/constants.d.ts" + ], + "html-element": [ + "./dist/types/html-element/index.d.ts" + ], + "chrome-extension": [ + "./dist/types/chrome-extension.d.ts" + ], + "yaml": [ + "./dist/types/yaml.d.ts" + ] } }, "scripts": { @@ -67,7 +95,12 @@ "e2e:generate-test-data": "GENERATE_TEST_DATA=true playwright test ./tests/ai/web/playwright/generate-test-data.spec.ts", "e2e:generate-test-data:headed": "GENERATE_TEST_DATA=true playwright test ./tests/ai/web/playwright/generate-test-data.spec.ts --headed" }, - "files": ["static", "dist", "README.md", "bin"], + "files": [ + "static", + "dist", + "README.md", + "bin" + ], "dependencies": { "@midscene/core": "workspace:*", "@midscene/shared": "workspace:*", @@ -76,7 +109,8 @@ "express": "4.21.1", "inquirer": "10.1.5", "openai": "4.57.1", - "socket.io": "4.8.1" + "socket.io": "4.8.1", + "socket.io-client": "4.8.1" }, "devDependencies": { "@modern-js/module-tools": "2.60.6", diff --git a/packages/web-integration/tests/unit-test/bridge/agent.test.ts b/packages/web-integration/tests/unit-test/bridge/agent.test.ts index 5bf842242..b3000c558 100644 --- a/packages/web-integration/tests/unit-test/bridge/agent.test.ts +++ b/packages/web-integration/tests/unit-test/bridge/agent.test.ts @@ -56,11 +56,11 @@ describe.skipIf(process.env.CI)( await agent.connectCurrentTab(); await sleep(3000); const answer = await agent.aiQuery( - 'what is the current page? return {description: string}', + 'name of the current page? return {name: string}', ); console.log(answer); - expect(answer.description).toBeTruthy(); + expect(answer.name).toBeTruthy(); await agent.destroy(); }, 60 * 1000, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2e9f386e9..089047245 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -322,6 +322,9 @@ importers: socket.io: specifier: 4.8.1 version: 4.8.1(bufferutil@4.0.9)(utf-8-validate@6.0.5) + socket.io-client: + specifier: 4.8.1 + version: 4.8.1(bufferutil@4.0.9)(utf-8-validate@6.0.5) devDependencies: '@modern-js/module-tools': specifier: 2.60.6 @@ -5100,6 +5103,9 @@ packages: end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + engine.io-client@6.6.2: + resolution: {integrity: sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw==} + engine.io-parser@5.2.3: resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} engines: {node: '>=10.0.0'} @@ -8932,6 +8938,10 @@ packages: socket.io-adapter@2.5.5: resolution: {integrity: sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==} + socket.io-client@4.8.1: + resolution: {integrity: sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==} + engines: {node: '>=10.0.0'} + socket.io-parser@4.2.4: resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} engines: {node: '>=10.0.0'} @@ -9956,6 +9966,10 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xmlhttprequest-ssl@2.1.2: + resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} + engines: {node: '>=0.4.0'} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -16311,6 +16325,18 @@ snapshots: dependencies: once: 1.4.0 + engine.io-client@6.6.2(bufferutil@4.0.9)(utf-8-validate@6.0.5): + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7(supports-color@5.5.0) + engine.io-parser: 5.2.3 + ws: 8.17.1(bufferutil@4.0.9)(utf-8-validate@6.0.5) + xmlhttprequest-ssl: 2.1.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + engine.io-parser@5.2.3: {} engine.io@6.6.2(bufferutil@4.0.9)(utf-8-validate@6.0.5): @@ -21082,6 +21108,17 @@ snapshots: - supports-color - utf-8-validate + socket.io-client@4.8.1(bufferutil@4.0.9)(utf-8-validate@6.0.5): + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7(supports-color@5.5.0) + engine.io-client: 6.6.2(bufferutil@4.0.9)(utf-8-validate@6.0.5) + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + socket.io-parser@4.2.4: dependencies: '@socket.io/component-emitter': 3.1.2 @@ -22239,6 +22276,8 @@ snapshots: xmlchars@2.2.0: {} + xmlhttprequest-ssl@2.1.2: {} + xtend@4.0.2: {} y18n@4.0.3: {} From b2a9b44975d2738063435874dedf6e3f7b43517d Mon Sep 17 00:00:00 2001 From: yutao Date: Fri, 3 Jan 2025 14:19:30 +0800 Subject: [PATCH 21/40] chore: update nx config --- nx.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/nx.json b/nx.json index b0cef872f..f66a55762 100644 --- a/nx.json +++ b/nx.json @@ -17,9 +17,6 @@ ] }, "test": { - "dependsOn": [ - "^build" - ], "cache": false }, "e2e": { From 79326a32f8b6b96e0b9dad01aa51ed34d52ebcdf Mon Sep 17 00:00:00 2001 From: yutao Date: Fri, 3 Jan 2025 14:45:38 +0800 Subject: [PATCH 22/40] doc: add doc for bridge mode --- apps/site/rspress.config.ts | 14 ++++---------- .../web-integration/src/bridge-mode/io-client.ts | 4 ++-- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/apps/site/rspress.config.ts b/apps/site/rspress.config.ts index 49e2e036d..000d64771 100644 --- a/apps/site/rspress.config.ts +++ b/apps/site/rspress.config.ts @@ -26,16 +26,6 @@ export default defineConfig({ 'https://applink.larkoffice.com/client/chat/chatter/add_by_link?link_token=291q2b25-e913-411a-8c51-191e59aab14d', }, ], - // footer: { - // message: ` - //
- // - //
- // `, - // }, locales: [ { lang: 'en', @@ -70,6 +60,10 @@ export default defineConfig({ text: 'Automate with Scripts in YAML', link: '/automate-with-scripts-in-yaml', }, + { + text: 'Bridge Mode by Chrome Extension', + link: '/bridge-mode-by-chrome-extension', + }, { text: 'Integrate with Playwright', link: '/integrate-with-playwright', diff --git a/packages/web-integration/src/bridge-mode/io-client.ts b/packages/web-integration/src/bridge-mode/io-client.ts index 4509b0549..1b2c2df19 100644 --- a/packages/web-integration/src/bridge-mode/io-client.ts +++ b/packages/web-integration/src/bridge-mode/io-client.ts @@ -30,14 +30,14 @@ export class BridgeClient { // on disconnect this.socket.on('disconnect', (reason: string) => { - console.log('bridge-disconnected, reason:', reason); + // console.log('bridge-disconnected, reason:', reason); this.socket = null; this.onDisconnect?.(); }); this.socket.on(BridgeConnectedEvent, () => { clearTimeout(timeout); - console.log('bridge-connected'); + // console.log('bridge-connected'); resolve(this.socket); }); this.socket.on(BridgeRefusedEvent, (e: any) => { From d716acc49b51a3c1723226e996f1d05b7837c678 Mon Sep 17 00:00:00 2001 From: yutao Date: Fri, 3 Jan 2025 17:01:53 +0800 Subject: [PATCH 23/40] doc: update doc for bridge mode --- .../en/bridge-mode-by-chrome-extension.mdx | 92 +++++++++++++++++++ packages/visualizer/src/extension/bridge.tsx | 5 +- 2 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 apps/site/docs/en/bridge-mode-by-chrome-extension.mdx diff --git a/apps/site/docs/en/bridge-mode-by-chrome-extension.mdx b/apps/site/docs/en/bridge-mode-by-chrome-extension.mdx new file mode 100644 index 000000000..96d683472 --- /dev/null +++ b/apps/site/docs/en/bridge-mode-by-chrome-extension.mdx @@ -0,0 +1,92 @@ +# Bridge Mode by Chrome Extension + +import { PackageManagerTabs } from '@theme'; + +The bridge mode in the Midscene Chrome extension is a tool that allows you to use local scripts to control the desktop version of Chrome. Your scripts can connect to either a new tab or the currently active tab. + +Using the desktop version of Chrome allows you to reuse all cookies, plugins, page status, and everything else you want. You can work with automation scripts to complete your tasks. This mode is commonly referred to as 'man-in-the-loop' in the context of automation. + +:::info Demo Project +you can check the demo project of bridge mode here: [https://github.com/web-infra-dev/midscene-example/blob/main/bridge-mode-demo](https://github.com/web-infra-dev/midscene-example/blob/main/bridge-mode-demo) +::: + +## Preparation + +Install [Midscene extension from Chrome web store](https://chromewebstore.google.com/detail/midscene/gbldofcpkknbggpkmbdaefngejllnief). We will use it later. + +## Step 1. install dependencies + + + +## Step 2. write scripts + +Write and save the following code as `./demo-new-tab.ts`. + +```typescript +import { ChromePageOverBridgeAgent } from "@midscene/web/bridge-mode"; + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +Promise.resolve( + (async () => { + const agent = new ChromePageOverBridgeAgent(); + + // This will connect to a new tab on your desktop Chrome + // remember to start your chrome extension, click 'allow connection' button. Otherwise you will get an timeout error + await agent.connectNewTabWithUrl("https://www.bing.com"); + + // these are the same as normal Midscene agent + await agent.ai('type "AI 101" and hit Enter'); + await sleep(3000); + + await agent.aiAssert("there are some search results"); + await agent.destroy(); + })() +); +``` + +## Step 3. run + +Launch your desktop Chrome. Start Midscene extension and switch to 'Bridge Mode' tab. Click "Allow connection". + +Run your scripts + +```bash +tsx demo-new-tab.ts +``` + +After executing the script, you should see the status of the Chrome extension switched to 'connected', and a new tab has been opened. Now this tab is controlled by your scripts. + +:::info +⁠Whether the scripts are run before or after clicking 'Allow connection' in the browser is not significant. +::: + +## API + +Except [the normal agent interface](./api), `ChromePageOverBridgeAgent` provides some other interfaces to control the desktop Chrome. + +:::info +You should always call `connectCurrentTab` or `connectNewTabWithUrl` before doing further actions. + +Each of the agent instance can only connect to one tab instance, and it cannot be reconnected after destroy. +::: + +### `connectCurrentTab` + +Connect to the current active tab on Chrome. + +### `connectNewTabWithUrl(ur: string)` + +Create a new tab with url and connect to immediately. + +### `destroy` + +Destroy the connection. + +## Use bridge mode in yaml-script + +We are still building this, and it will be ready soon. + + + + + diff --git a/packages/visualizer/src/extension/bridge.tsx b/packages/visualizer/src/extension/bridge.tsx index 5c7faf74b..46dfed569 100644 --- a/packages/visualizer/src/extension/bridge.tsx +++ b/packages/visualizer/src/extension/bridge.tsx @@ -169,7 +169,10 @@ export default function Bridge() {

In Bridge Mode, you can control this browser by the Midscene SDK running in the local terminal. This is useful for interacting both through - scripts and manually, or to reuse cookies. + scripts and manually, or to reuse cookies.{' '} + + More about bridge mode +

From 00c5ff42774c45bf411ac3e2ab085373fcd5c02d Mon Sep 17 00:00:00 2001 From: yutao Date: Fri, 3 Jan 2025 17:13:26 +0800 Subject: [PATCH 24/40] fix: lint --- nx.json | 22 +++------- packages/midscene/package.json | 24 +++------- packages/web-integration/package.json | 63 +++++++-------------------- 3 files changed, 27 insertions(+), 82 deletions(-) diff --git a/nx.json b/nx.json index f66a55762..e94705fe6 100644 --- a/nx.json +++ b/nx.json @@ -2,33 +2,23 @@ "$schema": "./node_modules/nx/schemas/nx-schema.json", "targetDefaults": { "dev": { - "dependsOn": [ - "^build" - ] + "dependsOn": ["^build"] }, "build": { - "dependsOn": [ - "^build" - ] + "dependsOn": ["^build"] }, "build:watch": { - "dependsOn": [ - "^build" - ] + "dependsOn": ["^build"] }, "test": { "cache": false }, "e2e": { - "dependsOn": [ - "^build" - ] + "dependsOn": ["^build"] }, "e2e:ui": { - "dependsOn": [ - "^build" - ] + "dependsOn": ["^build"] } }, "defaultBase": "main" -} \ No newline at end of file +} diff --git a/packages/midscene/package.json b/packages/midscene/package.json index a8d5651c8..370e0f904 100644 --- a/packages/midscene/package.json +++ b/packages/midscene/package.json @@ -8,11 +8,7 @@ "type": "commonjs", "main": "./dist/lib/index.js", "types": "./dist/lib/types/index.d.ts", - "files": [ - "dist", - "report", - "README.md" - ], + "files": ["dist", "report", "README.md"], "exports": { ".": "./dist/lib/index.js", "./env": "./dist/lib/env.js", @@ -21,18 +17,10 @@ }, "typesVersions": { "*": { - ".": [ - "./dist/lib/types/index.d.ts" - ], - "env": [ - "./dist/lib/types/env.d.ts" - ], - "utils": [ - "./dist/lib/types/utils.d.ts" - ], - "ai-model": [ - "./dist/lib/types/ai-model.d.ts" - ] + ".": ["./dist/lib/types/index.d.ts"], + "env": ["./dist/lib/types/env.d.ts"], + "utils": ["./dist/lib/types/utils.d.ts"], + "ai-model": ["./dist/lib/types/ai-model.d.ts"] } }, "scripts": { @@ -75,4 +63,4 @@ "registry": "https://registry.npmjs.org" }, "license": "MIT" -} \ No newline at end of file +} diff --git a/packages/web-integration/package.json b/packages/web-integration/package.json index 74ebee957..d1a533be1 100644 --- a/packages/web-integration/package.json +++ b/packages/web-integration/package.json @@ -28,48 +28,20 @@ }, "typesVersions": { "*": { - ".": [ - "./dist/types/index.d.ts" - ], - "bridge-mode": [ - "./dist/types/bridge-mode.d.ts" - ], - "bridge-mode-browser": [ - "./dist/types/bridge-mode-browser.d.ts" - ], - "utils": [ - "./dist/types/utils.d.ts" - ], - "ui-utils": [ - "./dist/types/ui-utils.d.ts" - ], - "puppeteer": [ - "./dist/types/puppeteer.d.ts" - ], - "playwright": [ - "./dist/types/playwright.d.ts" - ], - "playwright-report": [ - "./dist/types/playwright-report.d.ts" - ], - "playground": [ - "./dist/types/playground.d.ts" - ], - "debug": [ - "./dist/types/debug.d.ts" - ], - "constants": [ - "./dist/types/constants.d.ts" - ], - "html-element": [ - "./dist/types/html-element/index.d.ts" - ], - "chrome-extension": [ - "./dist/types/chrome-extension.d.ts" - ], - "yaml": [ - "./dist/types/yaml.d.ts" - ] + ".": ["./dist/types/index.d.ts"], + "bridge-mode": ["./dist/types/bridge-mode.d.ts"], + "bridge-mode-browser": ["./dist/types/bridge-mode-browser.d.ts"], + "utils": ["./dist/types/utils.d.ts"], + "ui-utils": ["./dist/types/ui-utils.d.ts"], + "puppeteer": ["./dist/types/puppeteer.d.ts"], + "playwright": ["./dist/types/playwright.d.ts"], + "playwright-report": ["./dist/types/playwright-report.d.ts"], + "playground": ["./dist/types/playground.d.ts"], + "debug": ["./dist/types/debug.d.ts"], + "constants": ["./dist/types/constants.d.ts"], + "html-element": ["./dist/types/html-element/index.d.ts"], + "chrome-extension": ["./dist/types/chrome-extension.d.ts"], + "yaml": ["./dist/types/yaml.d.ts"] } }, "scripts": { @@ -95,12 +67,7 @@ "e2e:generate-test-data": "GENERATE_TEST_DATA=true playwright test ./tests/ai/web/playwright/generate-test-data.spec.ts", "e2e:generate-test-data:headed": "GENERATE_TEST_DATA=true playwright test ./tests/ai/web/playwright/generate-test-data.spec.ts --headed" }, - "files": [ - "static", - "dist", - "README.md", - "bin" - ], + "files": ["static", "dist", "README.md", "bin"], "dependencies": { "@midscene/core": "workspace:*", "@midscene/shared": "workspace:*", From 40cb9cb41d307560485338f0eede1abd316ec087 Mon Sep 17 00:00:00 2001 From: yutao Date: Fri, 3 Jan 2025 19:01:05 +0800 Subject: [PATCH 25/40] fix: bridge style --- packages/visualizer/src/extension/bridge.less | 7 +++++++ packages/visualizer/src/extension/bridge.tsx | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/visualizer/src/extension/bridge.less b/packages/visualizer/src/extension/bridge.less index 27909d37b..c0fe71f6d 100644 --- a/packages/visualizer/src/extension/bridge.less +++ b/packages/visualizer/src/extension/bridge.less @@ -11,6 +11,13 @@ border: 1px solid @footer-text; border-radius: 5px; + .bridge-status-text { + flex-grow: 1; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + .bridge-status-btn { display: flex; flex-direction: column; diff --git a/packages/visualizer/src/extension/bridge.tsx b/packages/visualizer/src/extension/bridge.tsx index 46dfed569..2fe994efc 100644 --- a/packages/visualizer/src/extension/bridge.tsx +++ b/packages/visualizer/src/extension/bridge.tsx @@ -179,7 +179,7 @@ export default function Bridge() {

Bridge Status

-
{statusElement}
+
{statusElement}
{statusBtn}
From 225df69481aa7dc9703905306317b48a91d33278 Mon Sep 17 00:00:00 2001 From: yutao Date: Fri, 3 Jan 2025 19:21:15 +0800 Subject: [PATCH 26/40] feat: update doc --- apps/site/docs/en/bridge-mode-by-chrome-extension.mdx | 6 +++--- apps/site/docs/en/index.mdx | 3 ++- apps/site/docs/zh/index.mdx | 3 ++- apps/site/rspress.config.ts | 4 ++++ packages/web-integration/package.json | 2 +- packages/web-integration/src/bridge-mode/agent-cli-side.ts | 2 +- packages/web-integration/src/bridge-mode/index.ts | 4 ++-- .../web-integration/tests/unit-test/bridge/agent.test.ts | 6 +++--- 8 files changed, 18 insertions(+), 12 deletions(-) diff --git a/apps/site/docs/en/bridge-mode-by-chrome-extension.mdx b/apps/site/docs/en/bridge-mode-by-chrome-extension.mdx index 96d683472..6acf78f64 100644 --- a/apps/site/docs/en/bridge-mode-by-chrome-extension.mdx +++ b/apps/site/docs/en/bridge-mode-by-chrome-extension.mdx @@ -23,12 +23,12 @@ Install [Midscene extension from Chrome web store](https://chromewebstore.google Write and save the following code as `./demo-new-tab.ts`. ```typescript -import { ChromePageOverBridgeAgent } from "@midscene/web/bridge-mode"; +import { AgentOverChromeBridge } from "@midscene/web/bridge-mode"; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); Promise.resolve( (async () => { - const agent = new ChromePageOverBridgeAgent(); + const agent = new AgentOverChromeBridge(); // This will connect to a new tab on your desktop Chrome // remember to start your chrome extension, click 'allow connection' button. Otherwise you will get an timeout error @@ -62,7 +62,7 @@ After executing the script, you should see the status of the Chrome extension sw ## API -Except [the normal agent interface](./api), `ChromePageOverBridgeAgent` provides some other interfaces to control the desktop Chrome. +Except [the normal agent interface](./api), `AgentOverChromeBridge` provides some other interfaces to control the desktop Chrome. :::info You should always call `connectCurrentTab` or `connectNewTabWithUrl` before doing further actions. diff --git a/apps/site/docs/en/index.mdx b/apps/site/docs/en/index.mdx index 2b812cda7..3bf01d4f4 100644 --- a/apps/site/docs/en/index.mdx +++ b/apps/site/docs/en/index.mdx @@ -50,7 +50,8 @@ To start experiencing the core feature of Midscene, we recommend you use [the Ch Also, there are several ways to integrate Midscene into your code project: -* [Automate with Scripts in YAML](./automate-with-scripts-in-yaml) +* [Automate with Scripts in YAML](./automate-with-scripts-in-yaml), use this if you prefer to write YAML file instead of code +* [Bridge Mode by Chrome Extension](./bridge-mode-by-chrome-extension), use this to control the desktop Chrome by scripts * [Integrate with Puppeteer](./integrate-with-puppeteer) * [Integrate with Playwright](./integrate-with-playwright) diff --git a/apps/site/docs/zh/index.mdx b/apps/site/docs/zh/index.mdx index 05421f184..b2bf44223 100644 --- a/apps/site/docs/zh/index.mdx +++ b/apps/site/docs/zh/index.mdx @@ -37,7 +37,8 @@ console.log("headphones in stock", items); 此外,还有几种形式将 Midscene 集成到代码: -* [使用 YAML 格式的自动化脚本](./automate-with-scripts-in-yaml) +* [使用 YAML 格式的自动化脚本](./automate-with-scripts-in-yaml),如果你更喜欢写 YAML 文件而不是代码 +* [使用 Chrome 插件的桥接模式](./bridge-mode-by-chrome-extension),用它来通过脚本控制桌面 Chrome * [集成到 Puppeteer](./integrate-with-puppeteer) * [集成到 Playwright](./integrate-with-playwright) diff --git a/apps/site/rspress.config.ts b/apps/site/rspress.config.ts index f83048ce2..95639e15c 100644 --- a/apps/site/rspress.config.ts +++ b/apps/site/rspress.config.ts @@ -121,6 +121,10 @@ export default defineConfig({ text: '使用 YAML 格式的自动化脚本', link: '/zh/automate-with-scripts-in-yaml', }, + { + text: '使用 Chrome 插件的桥接模式(Bridge Mode)', + link: '/zh/bridge-mode-by-chrome-extension', + }, { text: '集成到 Playwright', link: '/zh/integrate-with-playwright', diff --git a/packages/web-integration/package.json b/packages/web-integration/package.json index d1a533be1..b3e702d3b 100644 --- a/packages/web-integration/package.json +++ b/packages/web-integration/package.json @@ -12,7 +12,7 @@ }, "exports": { ".": "./dist/lib/index.js", - "./bridge-mode": "./dist/lib/bridge-mode", + "./bridge-mode": "./dist/lib/bridge-mode.js", "./bridge-mode-browser": "./dist/lib/bridge-mode-browser.js", "./utils": "./dist/lib/utils.js", "./ui-utils": "./dist/lib/ui-utils.js", diff --git a/packages/web-integration/src/bridge-mode/agent-cli-side.ts b/packages/web-integration/src/bridge-mode/agent-cli-side.ts index 3c17bfb0e..8288f55bf 100644 --- a/packages/web-integration/src/bridge-mode/agent-cli-side.ts +++ b/packages/web-integration/src/bridge-mode/agent-cli-side.ts @@ -86,7 +86,7 @@ export const getBridgePageInCliSide = (): ChromeExtensionPageCliSide => { }) as ChromeExtensionPageCliSide; }; -export class ChromePageOverBridgeAgent extends PageAgent { +export class AgentOverChromeBridge extends PageAgent { constructor() { const page = getBridgePageInCliSide(); super(page, {}); diff --git a/packages/web-integration/src/bridge-mode/index.ts b/packages/web-integration/src/bridge-mode/index.ts index 8e3705d4a..07f2cf83d 100644 --- a/packages/web-integration/src/bridge-mode/index.ts +++ b/packages/web-integration/src/bridge-mode/index.ts @@ -1,3 +1,3 @@ -import { ChromePageOverBridgeAgent } from './agent-cli-side'; +import { AgentOverChromeBridge } from './agent-cli-side'; -export { ChromePageOverBridgeAgent }; +export { AgentOverChromeBridge }; diff --git a/packages/web-integration/tests/unit-test/bridge/agent.test.ts b/packages/web-integration/tests/unit-test/bridge/agent.test.ts index b3000c558..81694a21b 100644 --- a/packages/web-integration/tests/unit-test/bridge/agent.test.ts +++ b/packages/web-integration/tests/unit-test/bridge/agent.test.ts @@ -1,5 +1,5 @@ import { - ChromePageOverBridgeAgent, + AgentOverChromeBridge, getBridgePageInCliSide, } from '@/bridge-mode/agent-cli-side'; import { describe, expect, it } from 'vitest'; @@ -35,7 +35,7 @@ describe.skipIf(process.env.CI)( it( 'agent in cli side, new tab', async () => { - const agent = new ChromePageOverBridgeAgent(); + const agent = new AgentOverChromeBridge(); await agent.connectNewTabWithUrl('https://www.bing.com'); await sleep(3000); @@ -52,7 +52,7 @@ describe.skipIf(process.env.CI)( it( 'agent in cli side, current tab', async () => { - const agent = new ChromePageOverBridgeAgent(); + const agent = new AgentOverChromeBridge(); await agent.connectCurrentTab(); await sleep(3000); const answer = await agent.aiQuery( From aa26c5db9d731b711cb46a581116b2408be48bed Mon Sep 17 00:00:00 2001 From: yutao Date: Fri, 3 Jan 2025 21:55:54 +0800 Subject: [PATCH 27/40] doc: update doc for bridge mode --- README.md | 1 - .../zh/bridge-mode-by-chrome-extension.mdx | 92 +++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 apps/site/docs/zh/bridge-mode-by-chrome-extension.mdx diff --git a/README.md b/README.md index 82e92b531..10751831b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ Midscene.js

-

Midscene.js

diff --git a/apps/site/docs/zh/bridge-mode-by-chrome-extension.mdx b/apps/site/docs/zh/bridge-mode-by-chrome-extension.mdx new file mode 100644 index 000000000..5b4185f4f --- /dev/null +++ b/apps/site/docs/zh/bridge-mode-by-chrome-extension.mdx @@ -0,0 +1,92 @@ +# 使用 Chrome 插件的桥接模式(Bridge Mode) + +import { PackageManagerTabs } from '@theme'; + +使用 Midscene 的 Chrome 插件的桥接模式,你可以用本地脚本控制桌面版本的 Chrome。你的脚本可以连接到新标签页或当前已激活的标签页。 + +使用桌面版本的 Chrome 可以让你复用已有的 cookie、插件、页面状态等。你可以使用自动化脚本与操作者互动,来完成你的任务。 + +:::info Demo Project +you can check the demo project of bridge mode here: [https://github.com/web-infra-dev/midscene-example/blob/main/bridge-mode-demo](https://github.com/web-infra-dev/midscene-example/blob/main/bridge-mode-demo) +::: + +## 准备工作 + +安装 [Midscene 插件](https://chromewebstore.google.com/detail/midscene/gbldofcpkknbggpkmbdaefngejllnief)。 + +## 第一步:安装依赖 + + + +## 第二步:编写脚本 + +编写并保存以下代码为 `./demo-new-tab.ts`。 + +```typescript +import { AgentOverChromeBridge } from "@midscene/web/bridge-mode"; + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +Promise.resolve( + (async () => { + const agent = new AgentOverChromeBridge(); + + // 这个方法将连接到你的桌面 Chrome 的新标签页 + // 记得启动你的 Chrome 插件,并点击 'allow connection' 按钮。否则你会得到一个 timeout 错误 + await agent.connectNewTabWithUrl("https://www.bing.com"); + + // 这些方法与普通 Midscene agent 相同 + await agent.ai('type "AI 101" and hit Enter'); + await sleep(3000); + + await agent.aiAssert("there are some search results"); + await agent.destroy(); + })() +); +``` + +## 第三步:运行脚本 + +启动你的桌面 Chrome。启动 Midscene 插件,并切换到 'Bridge Mode' 标签页。点击 "Allow connection"。 + +运行你的脚本 + +```bash +tsx demo-new-tab.ts +``` + +执行脚本后,你应该看到 Chrome 插件的状态展示切换为 'connected',并且新标签页已打开。现在这个标签页由你的脚本控制。 + +:::info +执行脚本和点击插件中的 'Allow connection' 按钮没有顺序要求。 +::: + +## API + +除了 [普通的 agent 接口](./api),`AgentOverChromeBridge` 还提供了一些额外的接口来控制桌面 Chrome。 + +:::info +你应该在执行其他操作前,先调用 `connectCurrentTab` 或 `connectNewTabWithUrl`。 + +每个 agent 实例只能连接到一个标签页实例,并且一旦被销毁,就无法重新连接。 +::: + +### `connectCurrentTab` + +连接到当前已激活的标签页。 + +### `connectNewTabWithUrl(ur: string)` + +创建一个新标签页,并立即连接到它。 + +### `destroy` + +销毁连接。 + +## 在 YAML 脚本中使用桥接模式 + +这个功能正在开发中,很快就会与你见面。 + + + + + From af72e50aa6aa547425c3b67e33a5cb30d5135186 Mon Sep 17 00:00:00 2001 From: yutao Date: Sun, 5 Jan 2025 23:07:37 +0800 Subject: [PATCH 28/40] doc: add image explaination for bridge mode --- .../en/bridge-mode-by-chrome-extension.mdx | 2 ++ apps/site/docs/public/midscene-bridge-mode.jpg | Bin 0 -> 237371 bytes .../zh/bridge-mode-by-chrome-extension.mdx | 2 ++ 3 files changed, 4 insertions(+) create mode 100644 apps/site/docs/public/midscene-bridge-mode.jpg diff --git a/apps/site/docs/en/bridge-mode-by-chrome-extension.mdx b/apps/site/docs/en/bridge-mode-by-chrome-extension.mdx index 6acf78f64..fa4f49d02 100644 --- a/apps/site/docs/en/bridge-mode-by-chrome-extension.mdx +++ b/apps/site/docs/en/bridge-mode-by-chrome-extension.mdx @@ -6,6 +6,8 @@ The bridge mode in the Midscene Chrome extension is a tool that allows you to us Using the desktop version of Chrome allows you to reuse all cookies, plugins, page status, and everything else you want. You can work with automation scripts to complete your tasks. This mode is commonly referred to as 'man-in-the-loop' in the context of automation. +![bridge mode](/midscene-bridge-mode.jpg) + :::info Demo Project you can check the demo project of bridge mode here: [https://github.com/web-infra-dev/midscene-example/blob/main/bridge-mode-demo](https://github.com/web-infra-dev/midscene-example/blob/main/bridge-mode-demo) ::: diff --git a/apps/site/docs/public/midscene-bridge-mode.jpg b/apps/site/docs/public/midscene-bridge-mode.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d90d9120680e45c134758821b4c86fc3ca55a9e3 GIT binary patch literal 237371 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 Date: Mon, 6 Jan 2025 11:51:19 +0800 Subject: [PATCH 29/40] chore: move agent test --- .../tests/unit-test/bridge/agent.test.ts | 69 ------------------- 1 file changed, 69 deletions(-) delete mode 100644 packages/web-integration/tests/unit-test/bridge/agent.test.ts diff --git a/packages/web-integration/tests/unit-test/bridge/agent.test.ts b/packages/web-integration/tests/unit-test/bridge/agent.test.ts deleted file mode 100644 index 81694a21b..000000000 --- a/packages/web-integration/tests/unit-test/bridge/agent.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { - AgentOverChromeBridge, - getBridgePageInCliSide, -} from '@/bridge-mode/agent-cli-side'; -import { describe, expect, it } from 'vitest'; - -const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); -describe.skipIf(process.env.CI)( - 'fully functional agent in server(cli) side', - () => { - it('basic', async () => { - const page = getBridgePageInCliSide(); - expect(page).toBeDefined(); - - // server should be destroyed as well - await page.destroy(); - }); - - it( - 'page in cli side', - async () => { - const page = getBridgePageInCliSide(); - - // make sure the extension bridge is launched before timeout - await page.connectNewTabWithUrl('https://www.baidu.com'); - - // sleep 3s - await sleep(3000); - - await page.destroy(); - }, - 40 * 1000, // longer than the timeout of the bridge io - ); - - it( - 'agent in cli side, new tab', - async () => { - const agent = new AgentOverChromeBridge(); - - await agent.connectNewTabWithUrl('https://www.bing.com'); - await sleep(3000); - - await agent.ai('type "AI 101" and hit Enter'); - await sleep(3000); - - await agent.aiAssert('there are some search results'); - await agent.destroy(); - }, - 60 * 1000, - ); - - it( - 'agent in cli side, current tab', - async () => { - const agent = new AgentOverChromeBridge(); - await agent.connectCurrentTab(); - await sleep(3000); - const answer = await agent.aiQuery( - 'name of the current page? return {name: string}', - ); - - console.log(answer); - expect(answer.name).toBeTruthy(); - await agent.destroy(); - }, - 60 * 1000, - ); - }, -); From 336445036db1e81f8b9abcd076a65004c6078dac Mon Sep 17 00:00:00 2001 From: yutao Date: Mon, 6 Jan 2025 12:00:37 +0800 Subject: [PATCH 30/40] chore: move test case --- .../tests/ai/bridge/agent.test.ts | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 packages/web-integration/tests/ai/bridge/agent.test.ts diff --git a/packages/web-integration/tests/ai/bridge/agent.test.ts b/packages/web-integration/tests/ai/bridge/agent.test.ts new file mode 100644 index 000000000..81694a21b --- /dev/null +++ b/packages/web-integration/tests/ai/bridge/agent.test.ts @@ -0,0 +1,69 @@ +import { + AgentOverChromeBridge, + getBridgePageInCliSide, +} from '@/bridge-mode/agent-cli-side'; +import { describe, expect, it } from 'vitest'; + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); +describe.skipIf(process.env.CI)( + 'fully functional agent in server(cli) side', + () => { + it('basic', async () => { + const page = getBridgePageInCliSide(); + expect(page).toBeDefined(); + + // server should be destroyed as well + await page.destroy(); + }); + + it( + 'page in cli side', + async () => { + const page = getBridgePageInCliSide(); + + // make sure the extension bridge is launched before timeout + await page.connectNewTabWithUrl('https://www.baidu.com'); + + // sleep 3s + await sleep(3000); + + await page.destroy(); + }, + 40 * 1000, // longer than the timeout of the bridge io + ); + + it( + 'agent in cli side, new tab', + async () => { + const agent = new AgentOverChromeBridge(); + + await agent.connectNewTabWithUrl('https://www.bing.com'); + await sleep(3000); + + await agent.ai('type "AI 101" and hit Enter'); + await sleep(3000); + + await agent.aiAssert('there are some search results'); + await agent.destroy(); + }, + 60 * 1000, + ); + + it( + 'agent in cli side, current tab', + async () => { + const agent = new AgentOverChromeBridge(); + await agent.connectCurrentTab(); + await sleep(3000); + const answer = await agent.aiQuery( + 'name of the current page? return {name: string}', + ); + + console.log(answer); + expect(answer.name).toBeTruthy(); + await agent.destroy(); + }, + 60 * 1000, + ); + }, +); From ed603aed0078261a6c0e509c40d08a0c01a38b52 Mon Sep 17 00:00:00 2001 From: yutao Date: Mon, 6 Jan 2025 17:08:52 +0800 Subject: [PATCH 31/40] feat: print version when connected --- packages/visualizer/src/extension/bridge.tsx | 3 +-- packages/web-integration/modern.config.ts | 4 ++++ .../web-integration/src/bridge-mode/common.ts | 4 ++++ .../src/bridge-mode/io-client.ts | 18 ++++++++++++------ .../src/bridge-mode/io-server.ts | 9 ++++++++- .../src/bridge-mode/page-browser-side.ts | 6 ++++++ packages/web-integration/vitest.config.ts | 9 ++++++++- 7 files changed, 43 insertions(+), 10 deletions(-) diff --git a/packages/visualizer/src/extension/bridge.tsx b/packages/visualizer/src/extension/bridge.tsx index 2fe994efc..164490341 100644 --- a/packages/visualizer/src/extension/bridge.tsx +++ b/packages/visualizer/src/extension/bridge.tsx @@ -87,10 +87,9 @@ export default function Bridge() { await activeBridgePage.connect(); activeBridgePageRef.current = activeBridgePage; setBridgeStatus('connected'); - appendBridgeLog('Bridge connected'); return; } catch (e) { - console.warn('failed to connect to bridge server', e); + console.warn('failed to setup connection', e); } console.log('will retry...'); await new Promise((resolve) => setTimeout(resolve, connectRetryInterval)); diff --git a/packages/web-integration/modern.config.ts b/packages/web-integration/modern.config.ts index 84d3a1fe8..1ff409fed 100644 --- a/packages/web-integration/modern.config.ts +++ b/packages/web-integration/modern.config.ts @@ -1,4 +1,5 @@ import { defineConfig, moduleTools } from '@modern-js/module-tools'; +import { version } from './package.json'; export default defineConfig({ plugins: [moduleTools()], @@ -29,5 +30,8 @@ export default defineConfig({ 'bufferutil', 'utf-8-validate', ], + define: { + __VERSION__: version, + }, }, }); diff --git a/packages/web-integration/src/bridge-mode/common.ts b/packages/web-integration/src/bridge-mode/common.ts index faf753b96..9a695f9e6 100644 --- a/packages/web-integration/src/bridge-mode/common.ts +++ b/packages/web-integration/src/bridge-mode/common.ts @@ -30,3 +30,7 @@ export interface BridgeCallResponse { response: any; error?: any; } + +export interface BridgeConnectedEventPayload { + version: string; +} diff --git a/packages/web-integration/src/bridge-mode/io-client.ts b/packages/web-integration/src/bridge-mode/io-client.ts index 1b2c2df19..69d25132c 100644 --- a/packages/web-integration/src/bridge-mode/io-client.ts +++ b/packages/web-integration/src/bridge-mode/io-client.ts @@ -6,12 +6,14 @@ import { type BridgeCallResponse, BridgeCallResponseEvent, BridgeConnectedEvent, + type BridgeConnectedEventPayload, BridgeRefusedEvent, } from './common'; // ws client, this is where the request is processed export class BridgeClient { private socket: ClientSocket | null = null; + public serverVersion: string | null = null; constructor( public endpoint: string, public onBridgeCall: (method: string, args: any[]) => Promise, @@ -25,7 +27,7 @@ export class BridgeClient { }); const timeout = setTimeout(() => { - reject(new Error('failed to connect to bridge server')); + reject(new Error('failed to connect to bridge server after timeout')); }, 1 * 1000); // on disconnect @@ -35,11 +37,15 @@ export class BridgeClient { this.onDisconnect?.(); }); - this.socket.on(BridgeConnectedEvent, () => { - clearTimeout(timeout); - // console.log('bridge-connected'); - resolve(this.socket); - }); + this.socket.on( + BridgeConnectedEvent, + (payload: BridgeConnectedEventPayload) => { + clearTimeout(timeout); + // console.log('bridge-connected'); + this.serverVersion = payload?.version || 'unknown'; + resolve(this.socket); + }, + ); this.socket.on(BridgeRefusedEvent, (e: any) => { console.error('bridge-refused', e); reject(new Error(e || 'bridge refused')); diff --git a/packages/web-integration/src/bridge-mode/io-server.ts b/packages/web-integration/src/bridge-mode/io-server.ts index 82396419b..fbd678ff6 100644 --- a/packages/web-integration/src/bridge-mode/io-server.ts +++ b/packages/web-integration/src/bridge-mode/io-server.ts @@ -6,10 +6,13 @@ import { BridgeCallResponseEvent, BridgeCallTimeout, BridgeConnectedEvent, + type BridgeConnectedEventPayload, BridgeErrorCodeNoClientConnected, BridgeRefusedEvent, } from './common'; +declare const __VERSION__: string; + // ws server, this is where the request is sent export class BridgeServer { private callId = 0; @@ -98,7 +101,11 @@ export class BridgeServer { setTimeout(() => { this.onConnect?.(); - socket.emit(BridgeConnectedEvent); + + const payload = { + version: __VERSION__, + } as BridgeConnectedEventPayload; + socket.emit(BridgeConnectedEvent, payload); Promise.resolve().then(() => { for (const id in this.calls) { if (this.calls[id].callTime === 0) { diff --git a/packages/web-integration/src/bridge-mode/page-browser-side.ts b/packages/web-integration/src/bridge-mode/page-browser-side.ts index fe9c660bd..9d31c89f0 100644 --- a/packages/web-integration/src/bridge-mode/page-browser-side.ts +++ b/packages/web-integration/src/bridge-mode/page-browser-side.ts @@ -7,6 +7,8 @@ import { } from './common'; import { BridgeClient } from './io-client'; +declare const __VERSION__: string; + export class ChromeExtensionPageBrowserSide extends ChromeExtensionProxyPage { public bridgeClient: BridgeClient | null = null; @@ -78,6 +80,10 @@ export class ChromeExtensionPageBrowserSide extends ChromeExtensionProxyPage { }, ); await this.bridgeClient.connect(); + this.onLogMessage( + `Bridge connected, cli-side version ${this.bridgeClient.serverVersion}, browser-side version: ${__VERSION__}`, + 'log', + ); } public async connect() { diff --git a/packages/web-integration/vitest.config.ts b/packages/web-integration/vitest.config.ts index 78c765dd3..3f87439f6 100644 --- a/packages/web-integration/vitest.config.ts +++ b/packages/web-integration/vitest.config.ts @@ -2,6 +2,7 @@ import path from 'node:path'; //@ts-ignore import dotenv from 'dotenv'; import { defineConfig } from 'vitest/config'; +import { version } from './package.json'; /** * Read environment variables from file. @@ -13,7 +14,10 @@ dotenv.config({ const aiTestType = process.env.AI_TEST_TYPE; const unitTests = ['tests/unit-test/**/*.test.ts']; -const aiWebTests = ['tests/ai/web/**/*.test.ts']; +const aiWebTests = [ + 'tests/ai/web/**/*.test.ts', + 'tests/ai/bridge/**/*.test.ts', +]; const aiNativeTests = ['tests/ai/native/**/*.test.ts']; // const aiNativeTests = ['tests/ai/native/appium/dongchedi.test.ts']; const testFiles = (() => { @@ -36,4 +40,7 @@ export default defineConfig({ test: { include: testFiles, }, + define: { + __VERSION__: `'${version}'`, + }, }); From 578556ba8298ee6957d56aa29eb4e38d6b030ae7 Mon Sep 17 00:00:00 2001 From: yutao Date: Mon, 6 Jan 2025 17:14:43 +0800 Subject: [PATCH 32/40] feat: print version when connected --- packages/web-integration/src/bridge-mode/io-client.ts | 5 +++++ packages/web-integration/src/bridge-mode/io-server.ts | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/packages/web-integration/src/bridge-mode/io-client.ts b/packages/web-integration/src/bridge-mode/io-client.ts index 69d25132c..a21e06fb0 100644 --- a/packages/web-integration/src/bridge-mode/io-client.ts +++ b/packages/web-integration/src/bridge-mode/io-client.ts @@ -10,6 +10,8 @@ import { BridgeRefusedEvent, } from './common'; +declare const __VERSION__: string; + // ws client, this is where the request is processed export class BridgeClient { private socket: ClientSocket | null = null; @@ -24,6 +26,9 @@ export class BridgeClient { return new Promise((resolve, reject) => { this.socket = ClientIO(this.endpoint, { reconnection: false, + query: { + version: __VERSION__, + }, }); const timeout = setTimeout(() => { diff --git a/packages/web-integration/src/bridge-mode/io-server.ts b/packages/web-integration/src/bridge-mode/io-server.ts index fbd678ff6..2af6b1ae5 100644 --- a/packages/web-integration/src/bridge-mode/io-server.ts +++ b/packages/web-integration/src/bridge-mode/io-server.ts @@ -67,10 +67,19 @@ export class BridgeServer { socket.emit(BridgeRefusedEvent); reject(new Error('server already connected by another client')); } + try { // console.log('one client connected'); this.socket = socket; + const clientVersion = socket.handshake.query.version; + console.log( + 'Bridge connected, cli-side version:', + __VERSION__, + 'browser-side version:', + clientVersion, + ); + socket.on(BridgeCallResponseEvent, (params: BridgeCallResponse) => { const id = params.id; const response = params.response; From 323281409892a5eeb32a7e0ec4c74f08d3ed50b8 Mon Sep 17 00:00:00 2001 From: yutao Date: Mon, 6 Jan 2025 17:17:33 +0800 Subject: [PATCH 33/40] feat: print version when connected --- packages/visualizer/modern.config.ts | 2 +- packages/visualizer/src/extension/popup.tsx | 8 +++++++- packages/web-integration/src/bridge-mode/io-server.ts | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/visualizer/modern.config.ts b/packages/visualizer/modern.config.ts index 85299932e..a96cccb79 100644 --- a/packages/visualizer/modern.config.ts +++ b/packages/visualizer/modern.config.ts @@ -17,7 +17,7 @@ const commonConfig = { } : undefined, define: { - __VERSION__: JSON.stringify(version), + __VERSION__: version, }, }; diff --git a/packages/visualizer/src/extension/popup.tsx b/packages/visualizer/src/extension/popup.tsx index 0f5f33a21..e0c5cf745 100644 --- a/packages/visualizer/src/extension/popup.tsx +++ b/packages/visualizer/src/extension/popup.tsx @@ -27,6 +27,9 @@ import { useEffect, useState } from 'react'; import Bridge from './bridge'; setSideEffect(); + +declare const __VERSION__: string; + const shotAndOpenPlayground = async ( agent?: ChromeExtensionProxyPageAgent | null, ) => { @@ -140,7 +143,10 @@ function PlaygroundPopup() {
-

Midscene.js Chrome Extension v{extensionVersion}

+

+ Midscene.js Chrome Extension v{extensionVersion} (SDK v{__VERSION__} + ) +

diff --git a/packages/web-integration/src/bridge-mode/io-server.ts b/packages/web-integration/src/bridge-mode/io-server.ts index 2af6b1ae5..feeed65b5 100644 --- a/packages/web-integration/src/bridge-mode/io-server.ts +++ b/packages/web-integration/src/bridge-mode/io-server.ts @@ -76,7 +76,7 @@ export class BridgeServer { console.log( 'Bridge connected, cli-side version:', __VERSION__, - 'browser-side version:', + ', browser-side version:', clientVersion, ); From 98724c66b2fea90281774766c7222659d97258b9 Mon Sep 17 00:00:00 2001 From: yutao Date: Mon, 6 Jan 2025 17:45:41 +0800 Subject: [PATCH 34/40] fix: update export path --- .../en/bridge-mode-by-chrome-extension.mdx | 2 +- .../zh/bridge-mode-by-chrome-extension.mdx | 2 +- packages/visualizer/src/extension/bridge.less | 18 +++++++++--------- packages/web-integration/modern.config.ts | 2 +- packages/web-integration/package.json | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/site/docs/en/bridge-mode-by-chrome-extension.mdx b/apps/site/docs/en/bridge-mode-by-chrome-extension.mdx index fa4f49d02..3375eb6a9 100644 --- a/apps/site/docs/en/bridge-mode-by-chrome-extension.mdx +++ b/apps/site/docs/en/bridge-mode-by-chrome-extension.mdx @@ -25,7 +25,7 @@ Install [Midscene extension from Chrome web store](https://chromewebstore.google Write and save the following code as `./demo-new-tab.ts`. ```typescript -import { AgentOverChromeBridge } from "@midscene/web/bridge-mode"; +import { AgentOverChromeBridge } from "@midscene/web/bridge-mode-cli"; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); Promise.resolve( diff --git a/apps/site/docs/zh/bridge-mode-by-chrome-extension.mdx b/apps/site/docs/zh/bridge-mode-by-chrome-extension.mdx index 923cfd524..a5e82b00a 100644 --- a/apps/site/docs/zh/bridge-mode-by-chrome-extension.mdx +++ b/apps/site/docs/zh/bridge-mode-by-chrome-extension.mdx @@ -25,7 +25,7 @@ you can check the demo project of bridge mode here: [https://github.com/web-infr 编写并保存以下代码为 `./demo-new-tab.ts`。 ```typescript -import { AgentOverChromeBridge } from "@midscene/web/bridge-mode"; +import { AgentOverChromeBridge } from "@midscene/web/bridge-mode-cli"; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); Promise.resolve( diff --git a/packages/visualizer/src/extension/bridge.less b/packages/visualizer/src/extension/bridge.less index c0fe71f6d..922863ac3 100644 --- a/packages/visualizer/src/extension/bridge.less +++ b/packages/visualizer/src/extension/bridge.less @@ -24,16 +24,16 @@ align-items: center; justify-content: center; } +} - .bridge-log-container { - flex-grow: 1; +.bridge-log-container { + flex-grow: 1; - .bridge-log-item-content { - word-wrap: break-word; - white-space: nowrap; - text-overflow: ellipsis; - width: 100%; - overflow: hidden; - } + .bridge-log-item-content { + word-break: break-all; + white-space: nowrap; + text-overflow: ellipsis; + width: 100%; + overflow: hidden; } } diff --git a/packages/web-integration/modern.config.ts b/packages/web-integration/modern.config.ts index 1ff409fed..8e43f1104 100644 --- a/packages/web-integration/modern.config.ts +++ b/packages/web-integration/modern.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ format: 'cjs', input: { index: 'src/index.ts', - 'bridge-mode': 'src/bridge-mode/index.ts', + 'bridge-mode-cli': 'src/bridge-mode/index.ts', 'bridge-mode-browser': 'src/bridge-mode/browser.ts', utils: 'src/common/utils.ts', 'ui-utils': 'src/common/ui-utils.ts', diff --git a/packages/web-integration/package.json b/packages/web-integration/package.json index b3e702d3b..225f7bb8d 100644 --- a/packages/web-integration/package.json +++ b/packages/web-integration/package.json @@ -12,7 +12,7 @@ }, "exports": { ".": "./dist/lib/index.js", - "./bridge-mode": "./dist/lib/bridge-mode.js", + "./bridge-mode-cli": "./dist/lib/bridge-mode-cli.js", "./bridge-mode-browser": "./dist/lib/bridge-mode-browser.js", "./utils": "./dist/lib/utils.js", "./ui-utils": "./dist/lib/ui-utils.js", @@ -29,7 +29,7 @@ "typesVersions": { "*": { ".": ["./dist/types/index.d.ts"], - "bridge-mode": ["./dist/types/bridge-mode.d.ts"], + "bridge-mode-cli": ["./dist/types/bridge-mode-cli.d.ts"], "bridge-mode-browser": ["./dist/types/bridge-mode-browser.d.ts"], "utils": ["./dist/types/utils.d.ts"], "ui-utils": ["./dist/types/ui-utils.d.ts"], From 9e86ad70de1632cb11465a3bf45cc57883813516 Mon Sep 17 00:00:00 2001 From: yutao Date: Mon, 6 Jan 2025 19:02:17 +0800 Subject: [PATCH 35/40] fix: exports of web integration --- packages/visualizer/src/extension/bridge.tsx | 20 ++++-- packages/visualizer/src/init.ts | 2 + packages/web-integration/package.json | 75 ++++++++++++++++---- 3 files changed, 76 insertions(+), 21 deletions(-) diff --git a/packages/visualizer/src/extension/bridge.tsx b/packages/visualizer/src/extension/bridge.tsx index 164490341..ec9f7e831 100644 --- a/packages/visualizer/src/extension/bridge.tsx +++ b/packages/visualizer/src/extension/bridge.tsx @@ -11,6 +11,12 @@ interface BridgeLogItem { content: string; } +enum BridgeStatus { + Closed = 'closed', + OpenForConnection = 'open-for-connection', + Connected = 'connected', +} + const connectTimeout = 30 * 1000; const connectRetryInterval = 300; export default function Bridge() { @@ -18,9 +24,9 @@ export default function Bridge() { null, ); - const [bridgeStatus, setBridgeStatus] = useState< - 'closed' | 'open-for-connection' | 'connected' - >('closed'); + const [bridgeStatus, setBridgeStatus] = useState( + BridgeStatus.Closed, + ); const [bridgeLog, setBridgeLog] = useState([]); const [bridgeAgentStatus, setBridgeAgentStatus] = useState(''); @@ -48,7 +54,7 @@ export default function Bridge() { activeBridgePageRef.current.destroy(); activeBridgePageRef.current = null; } - setBridgeStatus('closed'); + setBridgeStatus(BridgeStatus.Closed); }; const stopListeningFlag = useRef(false); @@ -65,7 +71,7 @@ export default function Bridge() { setBridgeLog([]); setBridgeAgentStatus(''); appendBridgeLog('Listening for connection...'); - setBridgeStatus('open-for-connection'); + setBridgeStatus(BridgeStatus.OpenForConnection); stopListeningFlag.current = false; while (Date.now() - startTime < timeout) { @@ -86,7 +92,7 @@ export default function Bridge() { ); await activeBridgePage.connect(); activeBridgePageRef.current = activeBridgePage; - setBridgeStatus('connected'); + setBridgeStatus(BridgeStatus.Connected); return; } catch (e) { console.warn('failed to setup connection', e); @@ -95,7 +101,7 @@ export default function Bridge() { await new Promise((resolve) => setTimeout(resolve, connectRetryInterval)); } - setBridgeStatus('closed'); + setBridgeStatus(BridgeStatus.Closed); appendBridgeLog('No connection found within timeout'); }; diff --git a/packages/visualizer/src/init.ts b/packages/visualizer/src/init.ts index 38e33301c..9bed4bc98 100644 --- a/packages/visualizer/src/init.ts +++ b/packages/visualizer/src/init.ts @@ -1,6 +1,8 @@ // biome-ignore lint/style/useNodejsImportProtocol: import { Buffer } from 'buffer'; +// To solve the '"global is not defined" in randomBytes +// https://www.perplexity.ai/search/how-to-solve-global-is-not-def-xOrpDcfOSKqz_IXtwmK4_Q window.global ||= window; window.Buffer = Buffer; diff --git a/packages/web-integration/package.json b/packages/web-integration/package.json index 225f7bb8d..a430dcb86 100644 --- a/packages/web-integration/package.json +++ b/packages/web-integration/package.json @@ -11,20 +11,67 @@ "midscene-playground": "./bin/midscene-playground" }, "exports": { - ".": "./dist/lib/index.js", - "./bridge-mode-cli": "./dist/lib/bridge-mode-cli.js", - "./bridge-mode-browser": "./dist/lib/bridge-mode-browser.js", - "./utils": "./dist/lib/utils.js", - "./ui-utils": "./dist/lib/ui-utils.js", - "./puppeteer": "./dist/lib/puppeteer.js", - "./playwright": "./dist/lib/playwright.js", - "./playwright-report": "./dist/lib/playwright-report.js", - "./playground": "./dist/lib/playground.js", - "./debug": "./dist/lib/debug.js", - "./constants": "./dist/lib/constants.js", - "./html-element": "./dist/lib/html-element/index.js", - "./chrome-extension": "./dist/lib/chrome-extension.js", - "./yaml": "./dist/lib/yaml.js" + ".": { + "require": "./dist/lib/index.js", + "types": "./dist/types/index.d.ts" + }, + "./bridge-mode-cli": { + "require": "./dist/lib/bridge-mode-cli.js", + "types": "./dist/types/bridge-mode-cli.d.ts" + }, + "./bridge-mode-browser": { + "require": "./dist/lib/bridge-mode-browser.js", + "import": "./dist/es/bridge-mode-browser.js", + "types": "./dist/types/bridge-mode-browser.d.ts" + }, + "./utils": { + "require": "./dist/lib/utils.js", + "types": "./dist/types/utils.d.ts" + }, + "./ui-utils": { + "require": "./dist/lib/ui-utils.js", + "import": "./dist/es/ui-utils.js", + "types": "./dist/types/ui-utils.d.ts" + }, + "./puppeteer": { + "require": "./dist/lib/puppeteer.js", + "types": "./dist/types/puppeteer.d.ts" + }, + "./playwright": { + "require": "./dist/lib/playwright.js", + "types": "./dist/types/playwright.d.ts" + }, + "./playwright-report": { + "require": "./dist/lib/playwright-report.js", + "types": "./dist/types/playwright-report.d.ts" + }, + "./playground": { + "require": "./dist/lib/playground.js", + "import": "./dist/es/playground.js", + "types": "./dist/types/playground.d.ts" + }, + "./debug": { + "require": "./dist/lib/debug.js", + "types": "./dist/types/debug.d.ts" + }, + "./constants": { + "require": "./dist/lib/constants.js", + "types": "./dist/types/constants.d.ts" + }, + "./html-element": { + "require": "./dist/lib/html-element/index.js", + "types": "./dist/types/html-element/index.d.ts" + }, + "./chrome-extension": { + "require": "./dist/lib/chrome-extension.js", + "import": "./dist/es/chrome-extension.js", + "types": "./dist/types/chrome-extension.d.ts" + }, + "./yaml": { + "require": "./dist/lib/yaml.js", + "import": "./dist/es/yaml.js", + "types": "./dist/types/yaml.d.ts" + } }, "typesVersions": { "*": { From 39959bf7bc93f8c980e3e2ffe4f7ceedf228ff1f Mon Sep 17 00:00:00 2001 From: yutao Date: Mon, 6 Jan 2025 19:13:52 +0800 Subject: [PATCH 36/40] fix: build --- packages/web-integration/package.json | 71 +++++++++++++++++++++------ 1 file changed, 55 insertions(+), 16 deletions(-) diff --git a/packages/web-integration/package.json b/packages/web-integration/package.json index a430dcb86..50feaa186 100644 --- a/packages/web-integration/package.json +++ b/packages/web-integration/package.json @@ -13,10 +13,12 @@ "exports": { ".": { "require": "./dist/lib/index.js", + "import": "./dist/es/index.js", "types": "./dist/types/index.d.ts" }, "./bridge-mode-cli": { "require": "./dist/lib/bridge-mode-cli.js", + "import": "./dist/es/bridge-mode-cli.js", "types": "./dist/types/bridge-mode-cli.d.ts" }, "./bridge-mode-browser": { @@ -26,6 +28,7 @@ }, "./utils": { "require": "./dist/lib/utils.js", + "import": "./dist/es/utils.js", "types": "./dist/types/utils.d.ts" }, "./ui-utils": { @@ -35,10 +38,12 @@ }, "./puppeteer": { "require": "./dist/lib/puppeteer.js", + "import": "./dist/es/puppeteer.js", "types": "./dist/types/puppeteer.d.ts" }, "./playwright": { "require": "./dist/lib/playwright.js", + "import": "./dist/es/playwright.js", "types": "./dist/types/playwright.d.ts" }, "./playwright-report": { @@ -56,6 +61,7 @@ }, "./constants": { "require": "./dist/lib/constants.js", + "import": "./dist/es/constants.js", "types": "./dist/types/constants.d.ts" }, "./html-element": { @@ -75,20 +81,48 @@ }, "typesVersions": { "*": { - ".": ["./dist/types/index.d.ts"], - "bridge-mode-cli": ["./dist/types/bridge-mode-cli.d.ts"], - "bridge-mode-browser": ["./dist/types/bridge-mode-browser.d.ts"], - "utils": ["./dist/types/utils.d.ts"], - "ui-utils": ["./dist/types/ui-utils.d.ts"], - "puppeteer": ["./dist/types/puppeteer.d.ts"], - "playwright": ["./dist/types/playwright.d.ts"], - "playwright-report": ["./dist/types/playwright-report.d.ts"], - "playground": ["./dist/types/playground.d.ts"], - "debug": ["./dist/types/debug.d.ts"], - "constants": ["./dist/types/constants.d.ts"], - "html-element": ["./dist/types/html-element/index.d.ts"], - "chrome-extension": ["./dist/types/chrome-extension.d.ts"], - "yaml": ["./dist/types/yaml.d.ts"] + ".": [ + "./dist/types/index.d.ts" + ], + "bridge-mode-cli": [ + "./dist/types/bridge-mode-cli.d.ts" + ], + "bridge-mode-browser": [ + "./dist/types/bridge-mode-browser.d.ts" + ], + "utils": [ + "./dist/types/utils.d.ts" + ], + "ui-utils": [ + "./dist/types/ui-utils.d.ts" + ], + "puppeteer": [ + "./dist/types/puppeteer.d.ts" + ], + "playwright": [ + "./dist/types/playwright.d.ts" + ], + "playwright-report": [ + "./dist/types/playwright-report.d.ts" + ], + "playground": [ + "./dist/types/playground.d.ts" + ], + "debug": [ + "./dist/types/debug.d.ts" + ], + "constants": [ + "./dist/types/constants.d.ts" + ], + "html-element": [ + "./dist/types/html-element/index.d.ts" + ], + "chrome-extension": [ + "./dist/types/chrome-extension.d.ts" + ], + "yaml": [ + "./dist/types/yaml.d.ts" + ] } }, "scripts": { @@ -114,7 +148,12 @@ "e2e:generate-test-data": "GENERATE_TEST_DATA=true playwright test ./tests/ai/web/playwright/generate-test-data.spec.ts", "e2e:generate-test-data:headed": "GENERATE_TEST_DATA=true playwright test ./tests/ai/web/playwright/generate-test-data.spec.ts --headed" }, - "files": ["static", "dist", "README.md", "bin"], + "files": [ + "static", + "dist", + "README.md", + "bin" + ], "dependencies": { "@midscene/core": "workspace:*", "@midscene/shared": "workspace:*", @@ -172,4 +211,4 @@ "registry": "https://registry.npmjs.org" }, "license": "MIT" -} +} \ No newline at end of file From b8b6686c8d790c344b028db777e419cde9dee98e Mon Sep 17 00:00:00 2001 From: yutao Date: Mon, 6 Jan 2025 19:24:56 +0800 Subject: [PATCH 37/40] fix: lint --- packages/web-integration/package.json | 65 +++++-------------- .../src/bridge-mode/agent-cli-side.ts | 7 +- .../web-integration/src/bridge-mode/common.ts | 18 +++-- .../src/bridge-mode/io-client.ts | 15 ++--- .../src/bridge-mode/io-server.ts | 13 ++-- .../src/bridge-mode/page-browser-side.ts | 11 ++-- 6 files changed, 45 insertions(+), 84 deletions(-) diff --git a/packages/web-integration/package.json b/packages/web-integration/package.json index 50feaa186..7ca39ec63 100644 --- a/packages/web-integration/package.json +++ b/packages/web-integration/package.json @@ -81,48 +81,20 @@ }, "typesVersions": { "*": { - ".": [ - "./dist/types/index.d.ts" - ], - "bridge-mode-cli": [ - "./dist/types/bridge-mode-cli.d.ts" - ], - "bridge-mode-browser": [ - "./dist/types/bridge-mode-browser.d.ts" - ], - "utils": [ - "./dist/types/utils.d.ts" - ], - "ui-utils": [ - "./dist/types/ui-utils.d.ts" - ], - "puppeteer": [ - "./dist/types/puppeteer.d.ts" - ], - "playwright": [ - "./dist/types/playwright.d.ts" - ], - "playwright-report": [ - "./dist/types/playwright-report.d.ts" - ], - "playground": [ - "./dist/types/playground.d.ts" - ], - "debug": [ - "./dist/types/debug.d.ts" - ], - "constants": [ - "./dist/types/constants.d.ts" - ], - "html-element": [ - "./dist/types/html-element/index.d.ts" - ], - "chrome-extension": [ - "./dist/types/chrome-extension.d.ts" - ], - "yaml": [ - "./dist/types/yaml.d.ts" - ] + ".": ["./dist/types/index.d.ts"], + "bridge-mode-cli": ["./dist/types/bridge-mode-cli.d.ts"], + "bridge-mode-browser": ["./dist/types/bridge-mode-browser.d.ts"], + "utils": ["./dist/types/utils.d.ts"], + "ui-utils": ["./dist/types/ui-utils.d.ts"], + "puppeteer": ["./dist/types/puppeteer.d.ts"], + "playwright": ["./dist/types/playwright.d.ts"], + "playwright-report": ["./dist/types/playwright-report.d.ts"], + "playground": ["./dist/types/playground.d.ts"], + "debug": ["./dist/types/debug.d.ts"], + "constants": ["./dist/types/constants.d.ts"], + "html-element": ["./dist/types/html-element/index.d.ts"], + "chrome-extension": ["./dist/types/chrome-extension.d.ts"], + "yaml": ["./dist/types/yaml.d.ts"] } }, "scripts": { @@ -148,12 +120,7 @@ "e2e:generate-test-data": "GENERATE_TEST_DATA=true playwright test ./tests/ai/web/playwright/generate-test-data.spec.ts", "e2e:generate-test-data:headed": "GENERATE_TEST_DATA=true playwright test ./tests/ai/web/playwright/generate-test-data.spec.ts --headed" }, - "files": [ - "static", - "dist", - "README.md", - "bin" - ], + "files": ["static", "dist", "README.md", "bin"], "dependencies": { "@midscene/core": "workspace:*", "@midscene/shared": "workspace:*", @@ -211,4 +178,4 @@ "registry": "https://registry.npmjs.org" }, "license": "MIT" -} \ No newline at end of file +} diff --git a/packages/web-integration/src/bridge-mode/agent-cli-side.ts b/packages/web-integration/src/bridge-mode/agent-cli-side.ts index 8288f55bf..f27f740fc 100644 --- a/packages/web-integration/src/bridge-mode/agent-cli-side.ts +++ b/packages/web-integration/src/bridge-mode/agent-cli-side.ts @@ -2,10 +2,7 @@ import assert from 'node:assert'; import { PageAgent } from '@/common/agent'; import { paramStr, typeStr } from '@/common/ui-utils'; import type { KeyboardAction, MouseAction } from '@/page'; -import { - BridgeUpdateAgentStatusEvent, - DefaultBridgeServerPort, -} from './common'; +import { BridgeEvent, DefaultBridgeServerPort } from './common'; import { BridgeServer } from './io-server'; import type { ChromeExtensionPageBrowserSide } from './page-browser-side'; @@ -25,7 +22,7 @@ export const getBridgePageInCliSide = (): ChromeExtensionPageCliSide => { }; const page = { showStatusMessage: async (message: string) => { - await server.call(BridgeUpdateAgentStatusEvent, [message]); + await server.call(BridgeEvent.UpdateAgentStatus, [message]); }, }; diff --git a/packages/web-integration/src/bridge-mode/common.ts b/packages/web-integration/src/bridge-mode/common.ts index 9a695f9e6..75336c1a4 100644 --- a/packages/web-integration/src/bridge-mode/common.ts +++ b/packages/web-integration/src/bridge-mode/common.ts @@ -1,12 +1,18 @@ export const DefaultBridgeServerPort = 3766; export const DefaultLocalEndpoint = `http://127.0.0.1:${DefaultBridgeServerPort}`; export const BridgeCallTimeout = 30000; -export const BridgeCallEvent = 'bridge-call'; -export const BridgeCallResponseEvent = 'bridge-call-response'; -export const BridgeUpdateAgentStatusEvent = 'bridge-update-agent-status'; -export const BridgeMessageEvent = 'bridge-message'; -export const BridgeConnectedEvent = 'bridge-connected'; -export const BridgeRefusedEvent = 'bridge-refused'; + +export enum BridgeEvent { + Call = 'bridge-call', + CallResponse = 'bridge-call-response', + UpdateAgentStatus = 'bridge-update-agent-status', + Message = 'bridge-message', + Connected = 'bridge-connected', + Refused = 'bridge-refused', + ConnectNewTabWithUrl = 'connectNewTabWithUrl', + ConnectCurrentTab = 'connectCurrentTab', +} + export const BridgeErrorCodeNoClientConnected = 'no-client-connected'; export interface BridgeCall { diff --git a/packages/web-integration/src/bridge-mode/io-client.ts b/packages/web-integration/src/bridge-mode/io-client.ts index a21e06fb0..39cbece27 100644 --- a/packages/web-integration/src/bridge-mode/io-client.ts +++ b/packages/web-integration/src/bridge-mode/io-client.ts @@ -1,13 +1,10 @@ import assert from 'node:assert'; import { io as ClientIO, type Socket as ClientSocket } from 'socket.io-client'; import { - BridgeCallEvent, type BridgeCallRequest, type BridgeCallResponse, - BridgeCallResponseEvent, - BridgeConnectedEvent, type BridgeConnectedEventPayload, - BridgeRefusedEvent, + BridgeEvent, } from './common'; declare const __VERSION__: string; @@ -43,7 +40,7 @@ export class BridgeClient { }); this.socket.on( - BridgeConnectedEvent, + BridgeEvent.Connected, (payload: BridgeConnectedEventPayload) => { clearTimeout(timeout); // console.log('bridge-connected'); @@ -51,11 +48,11 @@ export class BridgeClient { resolve(this.socket); }, ); - this.socket.on(BridgeRefusedEvent, (e: any) => { + this.socket.on(BridgeEvent.Refused, (e: any) => { console.error('bridge-refused', e); reject(new Error(e || 'bridge refused')); }); - this.socket.on(BridgeCallEvent, (call: BridgeCallRequest) => { + this.socket.on(BridgeEvent.Call, (call: BridgeCallRequest) => { const id = call.id; assert(typeof id !== 'undefined', 'call id is required'); Promise.resolve().then(async () => { @@ -65,12 +62,12 @@ export class BridgeClient { } catch (e: any) { const errorContent = `Error from bridge client when calling ${call.method}: ${e?.message || e}\n${e?.stack || ''}`; console.error(errorContent); - return this.socket?.emit(BridgeCallResponseEvent, { + return this.socket?.emit(BridgeEvent.CallResponse, { id, error: errorContent, } as BridgeCallResponse); } - this.socket?.emit(BridgeCallResponseEvent, { + this.socket?.emit(BridgeEvent.CallResponse, { id, response, } as BridgeCallResponse); diff --git a/packages/web-integration/src/bridge-mode/io-server.ts b/packages/web-integration/src/bridge-mode/io-server.ts index feeed65b5..9e8c3aa58 100644 --- a/packages/web-integration/src/bridge-mode/io-server.ts +++ b/packages/web-integration/src/bridge-mode/io-server.ts @@ -1,14 +1,11 @@ import { Server, type Socket as ServerSocket } from 'socket.io'; import { type BridgeCall, - BridgeCallEvent, type BridgeCallResponse, - BridgeCallResponseEvent, BridgeCallTimeout, - BridgeConnectedEvent, type BridgeConnectedEventPayload, BridgeErrorCodeNoClientConnected, - BridgeRefusedEvent, + BridgeEvent, } from './common'; declare const __VERSION__: string; @@ -64,7 +61,7 @@ export class BridgeServer { this.connectionTipTimer = null; if (this.socket) { console.log('server already connected, refusing new connection'); - socket.emit(BridgeRefusedEvent); + socket.emit(BridgeEvent.Refused); reject(new Error('server already connected by another client')); } @@ -80,7 +77,7 @@ export class BridgeServer { clientVersion, ); - socket.on(BridgeCallResponseEvent, (params: BridgeCallResponse) => { + socket.on(BridgeEvent.CallResponse, (params: BridgeCallResponse) => { const id = params.id; const response = params.response; const error = params.error; @@ -114,7 +111,7 @@ export class BridgeServer { const payload = { version: __VERSION__, } as BridgeConnectedEventPayload; - socket.emit(BridgeConnectedEvent, payload); + socket.emit(BridgeEvent.Connected, payload); Promise.resolve().then(() => { for (const id in this.calls) { if (this.calls[id].callTime === 0) { @@ -166,7 +163,7 @@ export class BridgeServer { } if (this.socket) { - this.socket.emit(BridgeCallEvent, { + this.socket.emit(BridgeEvent.Call, { id, method: call.method, args: call.args, diff --git a/packages/web-integration/src/bridge-mode/page-browser-side.ts b/packages/web-integration/src/bridge-mode/page-browser-side.ts index 9d31c89f0..56958a5f7 100644 --- a/packages/web-integration/src/bridge-mode/page-browser-side.ts +++ b/packages/web-integration/src/bridge-mode/page-browser-side.ts @@ -1,10 +1,7 @@ import assert from 'node:assert'; import type { KeyboardAction, MouseAction } from '@/page'; import ChromeExtensionProxyPage from '../chrome-extension/page'; -import { - BridgeUpdateAgentStatusEvent, - DefaultBridgeServerPort, -} from './common'; +import { BridgeEvent, DefaultBridgeServerPort } from './common'; import { BridgeClient } from './io-client'; declare const __VERSION__: string; @@ -27,18 +24,18 @@ export class ChromeExtensionPageBrowserSide extends ChromeExtensionProxyPage { `ws://localhost:${DefaultBridgeServerPort}`, async (method, args: any[]) => { console.log('bridge call from cli side', method, args); - if (method === 'connectNewTabWithUrl') { + if (method === BridgeEvent.ConnectNewTabWithUrl) { return this.connectNewTabWithUrl.apply( this, args as unknown as [string], ); } - if (method === 'connectCurrentTab') { + if (method === BridgeEvent.ConnectCurrentTab) { return this.connectCurrentTab.apply(this, args as any); } - if (method === BridgeUpdateAgentStatusEvent) { + if (method === BridgeEvent.UpdateAgentStatus) { return this.onLogMessage(args[0] as string, 'status'); } From 99c7abcdf435e751837a280e72530ae57ca5d9eb Mon Sep 17 00:00:00 2001 From: yutao Date: Tue, 7 Jan 2025 09:33:00 +0800 Subject: [PATCH 38/40] fix: update export --- .../en/bridge-mode-by-chrome-extension.mdx | 2 +- .../zh/bridge-mode-by-chrome-extension.mdx | 2 +- packages/midscene/src/ai-model/inspect.ts | 8 +- packages/midscene/src/insight/index.ts | 6 +- packages/visualizer/src/extension/bridge.less | 2 +- packages/web-integration/modern.config.ts | 2 +- packages/web-integration/package.json | 10 +- .../web/playwright/todo-app-midscene.spec.ts | 98 ------------------- 8 files changed, 16 insertions(+), 114 deletions(-) delete mode 100644 packages/web-integration/tests/ai/web/playwright/todo-app-midscene.spec.ts diff --git a/apps/site/docs/en/bridge-mode-by-chrome-extension.mdx b/apps/site/docs/en/bridge-mode-by-chrome-extension.mdx index 3375eb6a9..fa4f49d02 100644 --- a/apps/site/docs/en/bridge-mode-by-chrome-extension.mdx +++ b/apps/site/docs/en/bridge-mode-by-chrome-extension.mdx @@ -25,7 +25,7 @@ Install [Midscene extension from Chrome web store](https://chromewebstore.google Write and save the following code as `./demo-new-tab.ts`. ```typescript -import { AgentOverChromeBridge } from "@midscene/web/bridge-mode-cli"; +import { AgentOverChromeBridge } from "@midscene/web/bridge-mode"; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); Promise.resolve( diff --git a/apps/site/docs/zh/bridge-mode-by-chrome-extension.mdx b/apps/site/docs/zh/bridge-mode-by-chrome-extension.mdx index a5e82b00a..923cfd524 100644 --- a/apps/site/docs/zh/bridge-mode-by-chrome-extension.mdx +++ b/apps/site/docs/zh/bridge-mode-by-chrome-extension.mdx @@ -25,7 +25,7 @@ you can check the demo project of bridge mode here: [https://github.com/web-infr 编写并保存以下代码为 `./demo-new-tab.ts`。 ```typescript -import { AgentOverChromeBridge } from "@midscene/web/bridge-mode-cli"; +import { AgentOverChromeBridge } from "@midscene/web/bridge-mode"; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); Promise.resolve( diff --git a/packages/midscene/src/ai-model/inspect.ts b/packages/midscene/src/ai-model/inspect.ts index ce7fd68be..b5ac2c59c 100644 --- a/packages/midscene/src/ai-model/inspect.ts +++ b/packages/midscene/src/ai-model/inspect.ts @@ -161,6 +161,7 @@ export async function AiInspectElement< type: 'image_url', image_url: { url: screenshotBase64WithElementMarker || screenshotBase64, + detail: 'high', }, }, { @@ -228,6 +229,7 @@ export async function AiExtractElementInfo< type: 'image_url', image_url: { url: screenshotBase64, + detail: 'high', }, }, { @@ -251,10 +253,7 @@ export async function AiExtractElementInfo< export async function AiAssert< ElementType extends BaseElement = BaseElement, ->(options: { - assertion: string; - context: UIContext; -}) { +>(options: { assertion: string; context: UIContext }) { const { assertion, context } = options; assert(assertion, 'assertion should be a string'); @@ -272,6 +271,7 @@ export async function AiAssert< type: 'image_url', image_url: { url: screenshotBase64, + detail: 'high', }, }, { diff --git a/packages/midscene/src/insight/index.ts b/packages/midscene/src/insight/index.ts index 495199455..9c3d9c175 100644 --- a/packages/midscene/src/insight/index.ts +++ b/packages/midscene/src/insight/index.ts @@ -208,7 +208,7 @@ export default class Insight< let errorLog: string | undefined; if (parseResult.errors?.length) { - errorLog = `segment - AI response error: \n${parseResult.errors.join('\n')}`; + errorLog = `AI response error: \n${parseResult.errors.join('\n')}`; } const dumpData: PartialInsightDumpFromSDK = { @@ -225,12 +225,12 @@ export default class Insight< }; const logId = writeInsightDump(dumpData, undefined, dumpSubscriber); - if (errorLog) { + const { data } = parseResult; + if (errorLog && !data) { console.error(errorLog); throw new Error(errorLog); } - const { data } = parseResult; let mergedData = data; // expand elements in object style data diff --git a/packages/visualizer/src/extension/bridge.less b/packages/visualizer/src/extension/bridge.less index 922863ac3..61134ef87 100644 --- a/packages/visualizer/src/extension/bridge.less +++ b/packages/visualizer/src/extension/bridge.less @@ -31,7 +31,7 @@ .bridge-log-item-content { word-break: break-all; - white-space: nowrap; + white-space: pre-wrap; text-overflow: ellipsis; width: 100%; overflow: hidden; diff --git a/packages/web-integration/modern.config.ts b/packages/web-integration/modern.config.ts index 8e43f1104..1ff409fed 100644 --- a/packages/web-integration/modern.config.ts +++ b/packages/web-integration/modern.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ format: 'cjs', input: { index: 'src/index.ts', - 'bridge-mode-cli': 'src/bridge-mode/index.ts', + 'bridge-mode': 'src/bridge-mode/index.ts', 'bridge-mode-browser': 'src/bridge-mode/browser.ts', utils: 'src/common/utils.ts', 'ui-utils': 'src/common/ui-utils.ts', diff --git a/packages/web-integration/package.json b/packages/web-integration/package.json index 7ca39ec63..df9f2c435 100644 --- a/packages/web-integration/package.json +++ b/packages/web-integration/package.json @@ -16,10 +16,10 @@ "import": "./dist/es/index.js", "types": "./dist/types/index.d.ts" }, - "./bridge-mode-cli": { - "require": "./dist/lib/bridge-mode-cli.js", - "import": "./dist/es/bridge-mode-cli.js", - "types": "./dist/types/bridge-mode-cli.d.ts" + "./bridge-mode": { + "require": "./dist/lib/bridge-mode.js", + "import": "./dist/es/bridge-mode.js", + "types": "./dist/types/bridge-mode.d.ts" }, "./bridge-mode-browser": { "require": "./dist/lib/bridge-mode-browser.js", @@ -82,7 +82,7 @@ "typesVersions": { "*": { ".": ["./dist/types/index.d.ts"], - "bridge-mode-cli": ["./dist/types/bridge-mode-cli.d.ts"], + "bridge-mode": ["./dist/types/bridge-mode.d.ts"], "bridge-mode-browser": ["./dist/types/bridge-mode-browser.d.ts"], "utils": ["./dist/types/utils.d.ts"], "ui-utils": ["./dist/types/ui-utils.d.ts"], diff --git a/packages/web-integration/tests/ai/web/playwright/todo-app-midscene.spec.ts b/packages/web-integration/tests/ai/web/playwright/todo-app-midscene.spec.ts deleted file mode 100644 index 4a966bacc..000000000 --- a/packages/web-integration/tests/ai/web/playwright/todo-app-midscene.spec.ts +++ /dev/null @@ -1,98 +0,0 @@ -// import { test, expect, type Page } from '@playwright/test'; -// import Insight, { TextElement, query } from 'midscene'; -// import { retrieveElements, retrieveOneElement } from 'midscene/query'; - -// test.beforeEach(async ({ page }) => { -// await page.goto('https://todomvc.com/examples/react/dist/'); -// }); - -// const TODO_ITEMS = ['buy some cheese', 'feed the cat', 'book a doctors appointment']; - -// interface InputBoxSection { -// element: TextElement; -// toggleAllBtn: TextElement; -// placeholder: string; -// inputValue: string; -// } - -// interface TodoItem { -// name: string; -// finished: boolean; -// } - -// interface ControlLayerSection { -// numbersLeft: number; -// tipElement: TextElement; -// controlElements: TextElement[]; -// } - -// // A comprehensive parser for page content -// const parsePage = async (page: Page) => { -// const insight = await Insight.fromPlaywrightPage(page); -// const todoListPage = await insight.segment({ -// 'input-box': query('an input box to type item and a "toggle-all" button', { -// element: retrieveOneElement('input box'), -// toggleAllBtn: retrieveOneElement('toggle all button, if exists'), -// placeholder: 'placeholder string in the input box, string, if exists', -// inputValue: 'the value in the input box, string, if exists', -// }), -// 'todo-list': query<{ todoItems: TodoItem[] }>('a list with todo-data (if exists)', { -// todoItems: '{name: string, finished: boolean}[]', -// }), -// 'control-layer': query('status and control layer of todo (if exists)', { -// numbersLeft: 'number', -// tipElement: retrieveOneElement( -// 'the element indicates the number of remaining items, like ` items left`', -// ), -// controlElements: retrieveElements('control elements, used to filter items'), -// }), -// }); - -// return todoListPage; -// }; - -// test.describe('New Todo', () => { -// test('should allow me to add todo items', async ({ page }) => { -// // add a todo item -// const todoPage = await parsePage(page); -// const inputBox = todoPage['input-box']; -// expect(inputBox).toBeTruthy(); - -// await page.mouse.click(...inputBox!.element.center); -// await page.keyboard.type(TODO_ITEMS[0], { delay: 100 }); -// await page.keyboard.press('Enter'); - -// // update page parsing result, and check the interface -// const todoPage2 = await parsePage(page); -// expect(todoPage2['input-box'].inputValue).toBeFalsy(); -// expect(todoPage2['input-box'].placeholder).toBeTruthy(); -// expect(todoPage2['todo-list'].todoItems.length).toBe(1); -// expect(todoPage2['todo-list'].todoItems[0].name).toBe(TODO_ITEMS[0]); - -// // add another item -// await page.mouse.click(...todoPage2['input-box'].element.center); -// await page.keyboard.type(TODO_ITEMS[1], { delay: 100 }); -// await page.keyboard.press('Enter'); - -// // update page parsing result -// const todoPage3 = await parsePage(page); -// const items = todoPage3['todo-list'].todoItems; -// expect(items.length).toBe(2); -// expect(items[1].name).toEqual(TODO_ITEMS[1]); -// expect(items.some((item) => item.finished)).toBeFalsy(); -// expect(todoPage3['control-layer'].numbersLeft).toBe(2); - -// // will mark all as completed -// const toggleBtn = todoPage3['input-box'].toggleAllBtn; -// expect(toggleBtn).toBeTruthy(); -// expect(todoPage3['todo-list'].todoItems.filter((item) => item.finished).length).toBe(0); - -// await page.mouse.click(...toggleBtn!.center, { delay: 500 }); -// await page.waitForTimeout(3000); - -// const todoPage4 = await parsePage(page); -// const allItems = todoPage4['todo-list'].todoItems; -// expect(allItems.length).toBe(2); -// expect(allItems.filter((item) => item.finished).length).toBe(allItems.length); -// }); -// }); From 03df48d7121fe8c69f8e9ca7e08461cec22c83ae Mon Sep 17 00:00:00 2001 From: yutao Date: Tue, 7 Jan 2025 09:43:07 +0800 Subject: [PATCH 39/40] fix: use enum for mouse and keyboard events --- .../src/bridge-mode/agent-cli-side.ts | 28 +++++++++++-------- .../web-integration/src/bridge-mode/common.ts | 15 ++++++++++ .../src/bridge-mode/page-browser-side.ts | 11 ++++++-- 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/packages/web-integration/src/bridge-mode/agent-cli-side.ts b/packages/web-integration/src/bridge-mode/agent-cli-side.ts index f27f740fc..c596b5ca2 100644 --- a/packages/web-integration/src/bridge-mode/agent-cli-side.ts +++ b/packages/web-integration/src/bridge-mode/agent-cli-side.ts @@ -2,7 +2,13 @@ import assert from 'node:assert'; import { PageAgent } from '@/common/agent'; import { paramStr, typeStr } from '@/common/ui-utils'; import type { KeyboardAction, MouseAction } from '@/page'; -import { BridgeEvent, DefaultBridgeServerPort } from './common'; +import { + BridgeEvent, + BridgePageType, + DefaultBridgeServerPort, + KeyboardEvent, + MouseEvent, +} from './common'; import { BridgeServer } from './io-server'; import type { ChromeExtensionPageBrowserSide } from './page-browser-side'; @@ -33,11 +39,15 @@ export const getBridgePageInCliSide = (): ChromeExtensionPageCliSide => { if (prop === 'toJSON') { return () => { return { - pageType: 'page-over-chrome-extension-bridge', + pageType: BridgePageType, }; }; } + if (prop === 'pageType') { + return BridgePageType; + } + if (prop === '_forceUsePageContext') { return undefined; } @@ -46,23 +56,19 @@ export const getBridgePageInCliSide = (): ChromeExtensionPageCliSide => { return page[prop as keyof typeof page]; } - if (prop === 'pageType') { - return 'page-over-chrome-extension-bridge'; - } - if (prop === 'mouse') { const mouse: MouseAction = { - click: bridgeCaller('mouse.click'), - wheel: bridgeCaller('mouse.wheel'), - move: bridgeCaller('mouse.move'), + click: bridgeCaller(MouseEvent.Click), + wheel: bridgeCaller(MouseEvent.Wheel), + move: bridgeCaller(MouseEvent.Move), }; return mouse; } if (prop === 'keyboard') { const keyboard: KeyboardAction = { - type: bridgeCaller('keyboard.type'), - press: bridgeCaller('keyboard.press'), + type: bridgeCaller(KeyboardEvent.Type), + press: bridgeCaller(KeyboardEvent.Press), }; return keyboard; } diff --git a/packages/web-integration/src/bridge-mode/common.ts b/packages/web-integration/src/bridge-mode/common.ts index 75336c1a4..1fb67c966 100644 --- a/packages/web-integration/src/bridge-mode/common.ts +++ b/packages/web-integration/src/bridge-mode/common.ts @@ -13,6 +13,21 @@ export enum BridgeEvent { ConnectCurrentTab = 'connectCurrentTab', } +export enum MouseEvent { + PREFIX = 'mouse.', + Click = 'click', + Wheel = 'wheel', + Move = 'move', +} + +export enum KeyboardEvent { + PREFIX = 'keyboard.', + Type = 'type', + Press = 'press', +} + +export const BridgePageType = 'page-over-chrome-extension-bridge'; + export const BridgeErrorCodeNoClientConnected = 'no-client-connected'; export interface BridgeCall { diff --git a/packages/web-integration/src/bridge-mode/page-browser-side.ts b/packages/web-integration/src/bridge-mode/page-browser-side.ts index 56958a5f7..9bac4c859 100644 --- a/packages/web-integration/src/bridge-mode/page-browser-side.ts +++ b/packages/web-integration/src/bridge-mode/page-browser-side.ts @@ -1,7 +1,12 @@ import assert from 'node:assert'; import type { KeyboardAction, MouseAction } from '@/page'; import ChromeExtensionProxyPage from '../chrome-extension/page'; -import { BridgeEvent, DefaultBridgeServerPort } from './common'; +import { + BridgeEvent, + DefaultBridgeServerPort, + KeyboardEvent, + MouseEvent, +} from './common'; import { BridgeClient } from './io-client'; declare const __VERSION__: string; @@ -45,12 +50,12 @@ export class ChromeExtensionPageBrowserSide extends ChromeExtensionProxyPage { // this.onLogMessage(`calling method: ${method}`); - if (method.startsWith('mouse.')) { + if (method.startsWith(MouseEvent.PREFIX)) { const actionName = method.split('.')[1] as keyof MouseAction; return this.mouse[actionName].apply(this.mouse, args as any); } - if (method.startsWith('keyboard.')) { + if (method.startsWith(KeyboardEvent.PREFIX)) { const actionName = method.split('.')[1] as keyof KeyboardAction; return this.keyboard[actionName].apply(this.keyboard, args as any); } From 6ea827a9d094f63b9aef97152f348657d0fadf0c Mon Sep 17 00:00:00 2001 From: yutao Date: Tue, 7 Jan 2025 10:07:35 +0800 Subject: [PATCH 40/40] fix: mouse and keyboard event --- packages/web-integration/src/bridge-mode/common.ts | 10 +++++----- packages/web-integration/src/bridge-mode/io-client.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/web-integration/src/bridge-mode/common.ts b/packages/web-integration/src/bridge-mode/common.ts index 1fb67c966..d6e383d09 100644 --- a/packages/web-integration/src/bridge-mode/common.ts +++ b/packages/web-integration/src/bridge-mode/common.ts @@ -15,15 +15,15 @@ export enum BridgeEvent { export enum MouseEvent { PREFIX = 'mouse.', - Click = 'click', - Wheel = 'wheel', - Move = 'move', + Click = 'mouse.click', + Wheel = 'mouse.wheel', + Move = 'mouse.move', } export enum KeyboardEvent { PREFIX = 'keyboard.', - Type = 'type', - Press = 'press', + Type = 'keyboard.type', + Press = 'keyboard.press', } export const BridgePageType = 'page-over-chrome-extension-bridge'; diff --git a/packages/web-integration/src/bridge-mode/io-client.ts b/packages/web-integration/src/bridge-mode/io-client.ts index 39cbece27..44a42a640 100644 --- a/packages/web-integration/src/bridge-mode/io-client.ts +++ b/packages/web-integration/src/bridge-mode/io-client.ts @@ -60,7 +60,7 @@ export class BridgeClient { try { response = await this.onBridgeCall(call.method, call.args); } catch (e: any) { - const errorContent = `Error from bridge client when calling ${call.method}: ${e?.message || e}\n${e?.stack || ''}`; + const errorContent = `Error from bridge client when calling, method: ${call.method}, args: ${call.args}, error: ${e?.message || e}\n${e?.stack || ''}`; console.error(errorContent); return this.socket?.emit(BridgeEvent.CallResponse, { id,