@whuanle/easytouch-windows 1.0.2 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,434 @@
1
+ # EasyTouch (et)
2
+
3
+ 跨平台系统自动化操作工具,支持 Windows、Linux、macOS。提供 CLI 命令行和 MCP 服务器两种使用方式,支持鼠标键盘控制、屏幕截图、窗口管理、系统信息查询、浏览器操作等功能。
4
+
5
+ 目前:
6
+
7
+ - [x] Windows
8
+ - [ ] Linux
9
+ - [ ] MAC(目前缺少设备验证功能)
10
+
11
+
12
+
13
+ ## 功能概览
14
+
15
+ | 模块 | 功能 |
16
+ |------|------|
17
+ | 🖱️ 鼠标控制 | 移动、点击、滚动、获取位置 |
18
+ | ⌨️ 键盘控制 | 按键、组合键、文本输入 |
19
+ | 📷 屏幕操作 | 截图、获取像素颜色、多显示器支持 |
20
+ | 🪟 窗口管理 | 列出、查找、激活窗口 |
21
+ | 🖥️ 系统信息 | CPU、内存、磁盘、进程 |
22
+ | 📋 剪贴板 | 文本读写、文件列表 |
23
+ | 🌐 浏览器控制 | 启动浏览器、页面导航、元素交互、截图 |
24
+
25
+
26
+
27
+ 提示:在 Linux 里,由于桌面环境差异很大,有些功能在某些桌面系统下可能不可用,详见 [Linux](#Linux) 环境说明。
28
+
29
+
30
+
31
+ ### 安装
32
+
33
+ ```bash
34
+ # Windows
35
+ npm i @whuanle/easytouch-windows
36
+
37
+ # Linux
38
+ npm i @whuanle/easytouch-linux
39
+
40
+ # macOS
41
+ npm i @whuanle/easytouch-mac
42
+ ```
43
+
44
+
45
+
46
+ 或者从[https://github.com/whuanle/EasyTouch/releases](https://github.com/whuanle/EasyTouch/releases)下载对应平台的可执行文件,并添加环境变量。
47
+
48
+
49
+
50
+ 执行 `et --help` 命令测试是否正常工作:
51
+
52
+ ```
53
+ PS E:\workspace\EasyTouch> et --help
54
+ EasyTouch Windows Automation Tool
55
+
56
+ Usage: et <command> [options]
57
+
58
+ Commands:
59
+ mouse_move --x <n> --y <n> [--relative] [--duration <ms>]
60
+ mouse_click [--button left|right|middle] [--double]
61
+ mouse_position
62
+ key_press --key <key>
63
+ type_text --text <text> [--interval <ms>] [--human]
64
+ screenshot [--output <path>] [--x <n>] [--y <n>] [--width <n>] [--height <n>]
65
+ pixel_color --x <n> --y <n>
66
+ window_list [--visible-only] [--filter <text>]
67
+ window_find [--title <text>] [--class <name>] [--pid <n>]
68
+ window_activate --title <text> | --handle <n>
69
+ window_foreground
70
+ os_info, cpu_info, memory_info, disk_list
71
+ process_list [--filter <text>]
72
+ clipboard_get_text, clipboard_set_text --text <text>
73
+
74
+ help Show this help
75
+ version Show version
76
+ {"success":true}
77
+ ```
78
+
79
+
80
+
81
+ ### 浏览器操作支持
82
+
83
+ Windows / Linux / macOS 三端都已统一使用 `Microsoft.Playwright`(.NET),不再依赖外部 Node.js Playwright 包。
84
+ 支持浏览器:`chromium` / `firefox` / `webkit` / `edge`(`edge` 走 Chromium 通道 `msedge`)。
85
+
86
+ 首次使用浏览器功能时,程序会自动尝试安装对应浏览器内核(Chromium/Firefox/WebKit),无需手动执行 `npx playwright install`。
87
+
88
+ 如果你希望提前安装,可以直接执行一次浏览器命令触发安装:
89
+
90
+ ```bash
91
+ et browser_launch --browser chromium --headless true
92
+ ```
93
+
94
+
95
+
96
+ 新增的 Web 自动化与测试能力(MCP)包括:
97
+
98
+ - `browser_assert_text`:断言页面或元素文本(适合测试)
99
+ - `browser_page_info`:读取页面标题、滚动位置、视口与文档尺寸
100
+ - `browser_go_back` / `browser_go_forward` / `browser_reload`
101
+ - `browser_scroll`:页面或元素滚动
102
+ - `browser_select`:选择下拉项
103
+ - `browser_upload`:文件上传
104
+ - `browser_get_cookies` / `browser_set_cookie` / `browser_clear_cookies`
105
+ - `browser_run_script`:执行本地 JS/TS Playwright 测试脚本文件
106
+
107
+ `browser_run_script` 用于执行 AI 生成或手写的 Playwright 测试脚本(如 `.spec.ts` / `.spec.js`),并返回退出码。
108
+ 常见参数:
109
+ - `--script-path`:脚本文件路径(必填)
110
+ - `--browser`:`chromium` / `firefox` / `webkit` / `edge`
111
+ - `--headless`:是否无头(默认 `true`)
112
+ - `--timeout`:测试超时(毫秒)
113
+ - `--extra-args`:透传给 Playwright CLI 的额外参数,逗号分隔(例如 `--extra-args \"--reporter=list,--workers=1\"`)
114
+
115
+
116
+
117
+ ### 作为 MCP 工具使用
118
+
119
+ 在 Claude、Cursor 等工具中,配置 MCP 的方式都是大同小异。
120
+
121
+ 通过 npm/bun 等方式安装的 EasyTouch,程序文件在 `C:\Users\{用户名}\AppData\Roaming\npm` 下面。
122
+
123
+
124
+
125
+ 在配置文件中添加:
126
+
127
+ **Windows**
128
+
129
+ ```json
130
+ {
131
+ "mcpServers": {
132
+ "easytouch": {
133
+ "command": "C:\\path\\to\\et.exe",
134
+ "args": ["--mcp"]
135
+ }
136
+ }
137
+ }
138
+ ```
139
+
140
+ **NPM 安装方式**
141
+
142
+ ```json
143
+ {
144
+ "mcpServers": {
145
+ "easytouch": {
146
+ "command": "npx",
147
+ "args": ["-y", "easytouch-windows", "--mcp"]
148
+ }
149
+ }
150
+ }
151
+ ```
152
+
153
+ **Linux / macOS**
154
+
155
+ ```json
156
+ {
157
+ "mcpServers": {
158
+ "easytouch": {
159
+ "command": "/path/to/et",
160
+ "args": ["--mcp"]
161
+ }
162
+ }
163
+ }
164
+ ```
165
+
166
+
167
+
168
+ ### 作为 Skills 给 AI 使用
169
+
170
+ 只需要执行命令安装 skills 即可。
171
+
172
+ ```bash
173
+ npx skills add https://github.com/whuanle/EasyTouch/skills
174
+ ```
175
+
176
+
177
+
178
+ ## CLI 命令参考
179
+
180
+ ### 鼠标控制
181
+
182
+ ```bash
183
+ # 移动鼠标(绝对坐标)
184
+ et mouse_move --x 100 --y 200
185
+
186
+ # 相对移动
187
+ t mouse_move --x 50 --y -30 --relative
188
+
189
+ # 平滑移动(500ms 动画)
190
+ et mouse_move --x 100 --y 200 --duration 500
191
+
192
+ # 左键单击(默认)
193
+ et mouse_click
194
+
195
+ # 右键双击
196
+ t mouse_click --button right --double
197
+
198
+ # 向上滚动3格
199
+ t mouse_scroll --amount 3
200
+
201
+ # 水平滚动
202
+ t mouse_scroll --amount 3 --horizontal
203
+
204
+ # 获取当前位置
205
+ t mouse_position
206
+ ```
207
+
208
+ ### 键盘控制
209
+
210
+ ```bash
211
+ # 按下单个键
212
+ t key_press --key "enter"
213
+
214
+ # 组合键
215
+ t key_press --key "ctrl+c"
216
+ t key_press --key "alt+tab"
217
+ t key_press --key "win+d"
218
+
219
+ # 输入文本
220
+ t type_text --text "Hello World"
221
+
222
+ # 模拟人工打字(带随机间隔)
223
+ t type_text --text "Hello World" --human --interval 50
224
+ ```
225
+
226
+ ### 屏幕操作
227
+
228
+ ```bash
229
+ # 全屏截图
230
+ t screenshot --output screenshot.png
231
+
232
+ # 区域截图
233
+ t screenshot --x 100 --y 100 --width 800 --height 600 --output region.png
234
+
235
+ # 获取像素颜色
236
+ t pixel_color --x 100 --y 200
237
+
238
+ # 列出显示器
239
+ t screen_list
240
+ ```
241
+
242
+ ### 窗口管理
243
+
244
+ ```bash
245
+ # 列出可见窗口
246
+ t window_list
247
+
248
+ # 按标题过滤
249
+ t window_list --filter "Chrome"
250
+
251
+ # 查找窗口
252
+ t window_find --title "记事本"
253
+
254
+ # 激活窗口
255
+ t window_activate --title "记事本"
256
+
257
+ # 获取前台窗口
258
+ t window_foreground
259
+ ```
260
+
261
+ ### 系统信息
262
+
263
+ ```bash
264
+ # 操作系统信息
265
+ et os_info
266
+
267
+ # CPU 信息
268
+ et cpu_info
269
+
270
+ # 内存信息
271
+ et memory_info
272
+
273
+ # 磁盘列表
274
+ et disk_list
275
+
276
+ # 进程列表
277
+ et process_list --filter "chrome"
278
+
279
+ # 锁定屏幕
280
+ et lock_screen
281
+ ```
282
+
283
+ ### 剪贴板
284
+
285
+ ```bash
286
+ # 获取文本
287
+ et clipboard_get_text
288
+
289
+ # 设置文本
290
+ et clipboard_set_text --text "Hello World"
291
+
292
+ # 清空
293
+ et clipboard_clear
294
+
295
+ # 获取文件列表
296
+ et clipboard_get_files
297
+ ```
298
+
299
+
300
+
301
+ ### 浏览器控制
302
+
303
+
304
+
305
+ 使用 `et browser_launch --browser` 命令启动浏览器后(匿名模式),使用 `et browser_list` 获取浏览器实例列表,之后可以使用不同的命令控制浏览器,最后可以自行关闭或使用 `et browser_close` 关闭浏览器。
306
+
307
+
308
+
309
+ ```bash
310
+ # 列出浏览器实例
311
+ et browser_list
312
+
313
+ # 启动 Chromium(无头模式)
314
+ et browser_launch --browser chromium --headless
315
+
316
+ # 启动 Edge(有界面)
317
+ et browser_launch --browser edge --headless false
318
+
319
+ # 打开页面
320
+ et browser_navigate --browser-id <id> --url "https://example.com"
321
+
322
+ # 导航控制
323
+ et browser_go_back --browser-id <id>
324
+ et browser_go_forward --browser-id <id>
325
+ et browser_reload --browser-id <id>
326
+
327
+ # 点击元素
328
+ et browser_click --browser-id <id> --selector "#submit"
329
+
330
+ # 输入内容
331
+ et browser_fill --browser-id <id> --selector "input[name='q']" --value "EasyTouch"
332
+
333
+ # 滚动页面(按像素)
334
+ et browser_scroll --browser-id <id> --x 0 --y 800 --behavior smooth
335
+
336
+ # 下拉选择
337
+ et browser_select --browser-id <id> --selector "#city" --values "beijing"
338
+
339
+ # 文件上传(多个文件用逗号分隔)
340
+ et browser_upload --browser-id <id> --selector "input[type='file']" --files "a.txt,b.txt"
341
+
342
+ # 页面截图
343
+ et browser_screenshot --browser-id <id> --output page.png --full-page true
344
+
345
+ # 执行脚本
346
+ et browser_evaluate --browser-id <id> --script "document.title"
347
+
348
+ # 读取页面信息
349
+ et browser_page_info --browser-id <id>
350
+
351
+ # Cookie 管理
352
+ et browser_get_cookies --browser-id <id>
353
+ et browser_set_cookie --browser-id <id> --name token --value abc --domain example.com --path / --http-only true --secure true --same-site lax
354
+ et browser_clear_cookies --browser-id <id>
355
+
356
+ # 执行本地 JS/TS Playwright 测试脚本
357
+ et browser_run_script --script-path "./tests/example.spec.ts" --browser edge --headless true
358
+
359
+ # 透传 Playwright CLI 参数(CSV)
360
+ et browser_run_script --script-path "./tests/login.spec.ts" --browser chromium --extra-args "--reporter=list,--workers=1"
361
+
362
+ # 文本断言(自动化测试)
363
+ et browser_assert_text --browser-id <id> --selector "h1" --expected-text "Example Domain" --exact-match true
364
+
365
+ # 关闭浏览器
366
+ et browser_close --browser-id <id>
367
+ ```
368
+
369
+
370
+
371
+ ### MCP Tools
372
+
373
+ | Tool | 描述 |
374
+ |------|------|
375
+ | `mouse_move` | 移动鼠标 |
376
+ | `mouse_click` | 点击鼠标 |
377
+ | `mouse_position` | 获取鼠标位置 |
378
+ | `key_press` | 按下按键 |
379
+ | `type_text` | 输入文本 |
380
+ | `screenshot` | 截图 |
381
+ | `pixel_color` | 获取像素颜色 |
382
+ | `window_list` | 列出窗口 |
383
+ | `window_find` | 查找窗口 |
384
+ | `window_activate` | 激活窗口 |
385
+ | `system_info` | 系统信息 |
386
+ | `process_list` | 进程列表 |
387
+ | `clipboard_get_text` | 获取剪贴板文本 |
388
+ | `clipboard_set_text` | 设置剪贴板文本 |
389
+ | `browser_launch` | 启动浏览器 |
390
+ | `browser_navigate` | 页面导航 |
391
+ | `browser_click` | 点击页面元素 |
392
+ | `browser_fill` | 填充输入框 |
393
+ | `browser_find` | 查找页面元素 |
394
+ | `browser_get_text` | 获取页面文本 |
395
+ | `browser_screenshot` | 浏览器截图 |
396
+ | `browser_evaluate` | 执行页面脚本 |
397
+ | `browser_wait_for` | 等待元素状态 |
398
+ | `browser_assert_text` | 断言页面或元素文本 |
399
+ | `browser_page_info` | 获取页面信息 |
400
+ | `browser_go_back` / `browser_go_forward` / `browser_reload` | 页面导航控制 |
401
+ | `browser_scroll` | 页面/元素滚动 |
402
+ | `browser_select` | 下拉选择 |
403
+ | `browser_upload` | 文件上传 |
404
+ | `browser_get_cookies` / `browser_set_cookie` / `browser_clear_cookies` | Cookie 管理 |
405
+ | `browser_run_script` | 执行 JS/TS Playwright 测试脚本 |
406
+ | `browser_close` | 关闭浏览器 |
407
+ | `browser_list` | 列出浏览器实例 |
408
+
409
+
410
+
411
+ 更多 MCP 使用文档见 [skills/SKILLS.md](skills/SKILLS.md)
412
+
413
+
414
+
415
+ ## 平台说明
416
+
417
+ ### Windows
418
+ - 完全支持所有功能
419
+ - 部分功能可能需要管理员权限
420
+
421
+ ### Linux
422
+ - 需要 X11 显示服务器
423
+ - 不支持 Wayland
424
+ - 建议在图形界面环境中使用
425
+
426
+ ### macOS
427
+ - 需要授予辅助功能权限(系统设置 → 隐私与安全性 → 辅助功能)
428
+ - 截图功能需要屏幕录制权限
429
+
430
+
431
+
432
+ ## 许可证
433
+
434
+ MIT License
package/et.exe CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whuanle/easytouch-windows",
3
- "version": "1.0.2",
3
+ "version": "1.0.5",
4
4
  "description": "Windows system automation tool with MCP support",
5
5
  "author": "MaomiAgent Team",
6
6
  "license": "MIT",
@@ -31,9 +31,13 @@
31
31
  "--mcp"
32
32
  ]
33
33
  },
34
+ "dependencies": {
35
+ "playwright": "^1.50.0"
36
+ },
34
37
  "files": [
35
38
  "et.exe",
36
39
  "et-launcher.js",
40
+ "scripts/",
37
41
  "SKILL.md"
38
42
  ],
39
43
  "repository": {
@@ -0,0 +1,560 @@
1
+ #!/usr/bin/env node
2
+
3
+ const net = require('net');
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
7
+ const { execSync, spawn } = require('child_process');
8
+ let chromium;
9
+ let firefox;
10
+ let webkit;
11
+
12
+ function loadPlaywright() {
13
+ try {
14
+ return require('playwright');
15
+ } catch {
16
+ try {
17
+ const globalRoot = execSync('npm root -g', {
18
+ encoding: 'utf8',
19
+ windowsHide: true
20
+ }).trim();
21
+ return require(path.join(globalRoot, 'playwright'));
22
+ } catch {
23
+ throw new Error(
24
+ 'Playwright package not found. Run: npm install -g playwright && npx playwright install chromium'
25
+ );
26
+ }
27
+ }
28
+ }
29
+
30
+ const playwright = loadPlaywright();
31
+ chromium = playwright.chromium;
32
+ firefox = playwright.firefox;
33
+ webkit = playwright.webkit;
34
+
35
+ const DAEMON_FILE = path.join(os.tmpdir(), 'easytouch-playwright-daemon.json');
36
+ const browserTypeMap = {
37
+ chromium,
38
+ chrome: chromium,
39
+ firefox,
40
+ webkit,
41
+ safari: webkit
42
+ };
43
+
44
+ const browsers = new Map();
45
+
46
+ function parseArg(args, key) {
47
+ const idx = args.indexOf(key);
48
+ if (idx >= 0 && idx + 1 < args.length) {
49
+ return args[idx + 1];
50
+ }
51
+ return null;
52
+ }
53
+
54
+ function safeJsonParse(text) {
55
+ try {
56
+ return JSON.parse(text);
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ function writeJsonLine(socket, obj) {
63
+ socket.write(JSON.stringify(obj) + '\n');
64
+ }
65
+
66
+ async function ensureDaemon() {
67
+ if (fs.existsSync(DAEMON_FILE)) {
68
+ const existing = safeJsonParse(fs.readFileSync(DAEMON_FILE, 'utf8'));
69
+ if (existing && existing.port) {
70
+ try {
71
+ await sendRequest(existing.port, { command: 'ping', args: [] }, 800);
72
+ return existing;
73
+ } catch {}
74
+ }
75
+ }
76
+
77
+ const child = spawn(process.execPath, [__filename, 'daemon'], {
78
+ detached: true,
79
+ stdio: 'ignore',
80
+ windowsHide: true
81
+ });
82
+ child.unref();
83
+
84
+ const deadline = Date.now() + 5000;
85
+ while (Date.now() < deadline) {
86
+ if (fs.existsSync(DAEMON_FILE)) {
87
+ const created = safeJsonParse(fs.readFileSync(DAEMON_FILE, 'utf8'));
88
+ if (created && created.port) {
89
+ try {
90
+ await sendRequest(created.port, { command: 'ping', args: [] }, 800);
91
+ return created;
92
+ } catch {}
93
+ }
94
+ }
95
+ await new Promise((r) => setTimeout(r, 100));
96
+ }
97
+
98
+ throw new Error('Failed to start Playwright bridge daemon');
99
+ }
100
+
101
+ function sendRequest(port, payload, timeoutMs = 30000) {
102
+ return new Promise((resolve, reject) => {
103
+ const socket = net.createConnection({ host: '127.0.0.1', port }, () => {
104
+ writeJsonLine(socket, payload);
105
+ });
106
+
107
+ let buffer = '';
108
+ let done = false;
109
+
110
+ const timer = setTimeout(() => {
111
+ if (done) return;
112
+ done = true;
113
+ socket.destroy();
114
+ reject(new Error('Bridge request timeout'));
115
+ }, timeoutMs);
116
+
117
+ socket.on('data', (chunk) => {
118
+ buffer += chunk.toString();
119
+ const lineEnd = buffer.indexOf('\n');
120
+ if (lineEnd === -1) return;
121
+ const line = buffer.slice(0, lineEnd).trim();
122
+ if (!line || done) return;
123
+ done = true;
124
+ clearTimeout(timer);
125
+ socket.end();
126
+ const parsed = safeJsonParse(line);
127
+ if (!parsed) {
128
+ reject(new Error('Invalid bridge response'));
129
+ return;
130
+ }
131
+ resolve(parsed);
132
+ });
133
+
134
+ socket.on('error', (err) => {
135
+ if (done) return;
136
+ done = true;
137
+ clearTimeout(timer);
138
+ reject(err);
139
+ });
140
+ });
141
+ }
142
+
143
+ function getLauncher(type) {
144
+ const launcher = browserTypeMap[(type || 'chromium').toLowerCase()];
145
+ return launcher || chromium;
146
+ }
147
+
148
+ async function getPrimaryPage(browser) {
149
+ let context = browser.contexts()[0];
150
+ if (!context) {
151
+ context = await browser.newContext();
152
+ }
153
+ let page = context.pages()[0];
154
+ if (!page) {
155
+ page = await context.newPage();
156
+ }
157
+ return page;
158
+ }
159
+
160
+ async function runLaunch(args) {
161
+ const browserId = parseArg(args, '--browser-id');
162
+ if (!browserId) throw new Error('Missing --browser-id');
163
+
164
+ const browserType = (parseArg(args, '--browser') || 'chromium').toLowerCase();
165
+ const headless = args.includes('--headless');
166
+ const executablePath = parseArg(args, '--executable');
167
+ const url = parseArg(args, '--url');
168
+
169
+ const launcher = getLauncher(browserType);
170
+ const browser = await launcher.launch({
171
+ headless,
172
+ ...(executablePath ? { executablePath } : {})
173
+ });
174
+
175
+ const page = await getPrimaryPage(browser);
176
+ if (url) {
177
+ await page.goto(url, { waitUntil: 'load', timeout: 30000 });
178
+ }
179
+
180
+ browsers.set(browserId, {
181
+ id: browserId,
182
+ type: browserType,
183
+ browser,
184
+ createdAt: Date.now()
185
+ });
186
+
187
+ return {
188
+ BrowserId: browserId,
189
+ BrowserType: browserType,
190
+ Version: browser.version()
191
+ };
192
+ }
193
+
194
+ function getBrowserOrThrow(browserId) {
195
+ const instance = browsers.get(browserId);
196
+ if (!instance) throw new Error(`Browser not found: ${browserId}`);
197
+ return instance;
198
+ }
199
+
200
+ async function runNavigate(args) {
201
+ const browserId = parseArg(args, '--browser-id');
202
+ const url = parseArg(args, '--url');
203
+ if (!browserId || !url) throw new Error('Missing --browser-id or --url');
204
+
205
+ const waitUntil = parseArg(args, '--wait-until') || 'load';
206
+ const timeout = Number.parseInt(parseArg(args, '--timeout') || '30000', 10);
207
+
208
+ const instance = getBrowserOrThrow(browserId);
209
+ const page = await getPrimaryPage(instance.browser);
210
+ const response = await page.goto(url, { waitUntil, timeout });
211
+
212
+ return {
213
+ Url: page.url(),
214
+ Title: await page.title(),
215
+ StatusCode: response ? response.status() : 0
216
+ };
217
+ }
218
+
219
+ async function runClick(args) {
220
+ const browserId = parseArg(args, '--browser-id');
221
+ const selector = parseArg(args, '--selector');
222
+ if (!browserId || !selector) throw new Error('Missing --browser-id or --selector');
223
+
224
+ const selectorType = parseArg(args, '--selector-type') || 'css';
225
+ const button = Number.parseInt(parseArg(args, '--button') || '0', 10);
226
+ const clickCount = Number.parseInt(parseArg(args, '--click-count') || '1', 10);
227
+ const timeout = Number.parseInt(parseArg(args, '--timeout') || '30000', 10);
228
+
229
+ const buttonMap = ['left', 'middle', 'right'];
230
+ const instance = getBrowserOrThrow(browserId);
231
+ const page = await getPrimaryPage(instance.browser);
232
+ const locator = getLocator(page, selector, selectorType);
233
+
234
+ await locator.click({
235
+ button: buttonMap[button] || 'left',
236
+ clickCount,
237
+ timeout
238
+ });
239
+ return { Message: 'OK' };
240
+ }
241
+
242
+ async function runFill(args) {
243
+ const browserId = parseArg(args, '--browser-id');
244
+ const selector = parseArg(args, '--selector');
245
+ const value = parseArg(args, '--value') ?? '';
246
+ if (!browserId || !selector) throw new Error('Missing --browser-id or --selector');
247
+
248
+ const selectorType = parseArg(args, '--selector-type') || 'css';
249
+ const timeout = Number.parseInt(parseArg(args, '--timeout') || '30000', 10);
250
+ const noClear = args.includes('--no-clear');
251
+
252
+ const instance = getBrowserOrThrow(browserId);
253
+ const page = await getPrimaryPage(instance.browser);
254
+ const locator = getLocator(page, selector, selectorType);
255
+
256
+ if (!noClear) {
257
+ await locator.clear({ timeout });
258
+ }
259
+ await locator.fill(value, { timeout });
260
+ return { Message: 'OK' };
261
+ }
262
+
263
+ async function runFind(args) {
264
+ const browserId = parseArg(args, '--browser-id');
265
+ const selector = parseArg(args, '--selector');
266
+ if (!browserId || !selector) throw new Error('Missing --browser-id or --selector');
267
+
268
+ const selectorType = parseArg(args, '--selector-type') || 'css';
269
+ const timeout = Number.parseInt(parseArg(args, '--timeout') || '5000', 10);
270
+
271
+ const instance = getBrowserOrThrow(browserId);
272
+ const page = await getPrimaryPage(instance.browser);
273
+ const locator = getLocator(page, selector, selectorType);
274
+
275
+ await locator.first.waitFor({ timeout, state: 'attached' }).catch(() => {});
276
+ const count = await locator.count();
277
+ if (count === 0) return { Found: false };
278
+
279
+ const element = locator.first;
280
+ const tagName = await element.evaluate((el) => el.tagName.toLowerCase());
281
+ const text = await element.textContent();
282
+ const value = await element.inputValue().catch(() => null);
283
+ const bbox = await element.boundingBox();
284
+
285
+ return {
286
+ Found: true,
287
+ TagName: tagName,
288
+ Text: text,
289
+ Value: value,
290
+ BoundingBox: bbox
291
+ ? { X: bbox.x, Y: bbox.y, Width: bbox.width, Height: bbox.height }
292
+ : null
293
+ };
294
+ }
295
+
296
+ async function runGetText(args) {
297
+ const browserId = parseArg(args, '--browser-id');
298
+ if (!browserId) throw new Error('Missing --browser-id');
299
+
300
+ const selector = parseArg(args, '--selector');
301
+ const selectorType = parseArg(args, '--selector-type') || 'css';
302
+
303
+ const instance = getBrowserOrThrow(browserId);
304
+ const page = await getPrimaryPage(instance.browser);
305
+ if (!selector) {
306
+ return { Text: await page.content(), Selector: null };
307
+ }
308
+
309
+ const locator = getLocator(page, selector, selectorType);
310
+ return { Text: (await locator.textContent()) || '', Selector: selector };
311
+ }
312
+
313
+ async function runScreenshot(args) {
314
+ const browserId = parseArg(args, '--browser-id');
315
+ if (!browserId) throw new Error('Missing --browser-id');
316
+
317
+ const output = parseArg(args, '--output');
318
+ if (!output) throw new Error('Missing --output');
319
+
320
+ const selector = parseArg(args, '--selector');
321
+ const selectorType = parseArg(args, '--selector-type') || 'css';
322
+ const type = (parseArg(args, '--type') || 'png').toLowerCase();
323
+ const fullPage = args.includes('--full-page');
324
+ const quality = parseArg(args, '--quality');
325
+
326
+ const instance = getBrowserOrThrow(browserId);
327
+ const page = await getPrimaryPage(instance.browser);
328
+
329
+ const options = {
330
+ path: output,
331
+ type: type === 'jpeg' ? 'jpeg' : 'png'
332
+ };
333
+ if (quality && options.type === 'jpeg') {
334
+ options.quality = Number.parseInt(quality, 10);
335
+ }
336
+
337
+ if (selector) {
338
+ const locator = getLocator(page, selector, selectorType);
339
+ await locator.screenshot(options);
340
+ } else {
341
+ await page.screenshot({ ...options, fullPage });
342
+ }
343
+
344
+ return { ImagePath: output, Width: 0, Height: 0 };
345
+ }
346
+
347
+ async function runEvaluate(args) {
348
+ const browserId = parseArg(args, '--browser-id');
349
+ const scriptFile = parseArg(args, '--script-file');
350
+ if (!browserId || !scriptFile) throw new Error('Missing --browser-id or --script-file');
351
+
352
+ const script = fs.readFileSync(scriptFile, 'utf8');
353
+ const instance = getBrowserOrThrow(browserId);
354
+ const page = await getPrimaryPage(instance.browser);
355
+ const result = await page.evaluate(script);
356
+
357
+ return {
358
+ Result: result,
359
+ ResultType: typeof result
360
+ };
361
+ }
362
+
363
+ async function runWaitFor(args) {
364
+ const browserId = parseArg(args, '--browser-id');
365
+ const selector = parseArg(args, '--selector');
366
+ if (!browserId || !selector) throw new Error('Missing --browser-id or --selector');
367
+
368
+ const selectorType = parseArg(args, '--selector-type') || 'css';
369
+ const state = parseArg(args, '--state') || 'visible';
370
+ const timeout = Number.parseInt(parseArg(args, '--timeout') || '30000', 10);
371
+
372
+ const instance = getBrowserOrThrow(browserId);
373
+ const page = await getPrimaryPage(instance.browser);
374
+ const locator = getLocator(page, selector, selectorType);
375
+ await locator.waitFor({ state, timeout });
376
+
377
+ return { Message: 'OK' };
378
+ }
379
+
380
+ async function runList() {
381
+ const list = [];
382
+ for (const [id, instance] of browsers) {
383
+ let currentUrl = '';
384
+ let currentTitle = '';
385
+ let connected = false;
386
+ try {
387
+ connected = instance.browser.isConnected();
388
+ if (connected) {
389
+ const page = await getPrimaryPage(instance.browser);
390
+ currentUrl = page.url();
391
+ currentTitle = await page.title();
392
+ }
393
+ } catch {
394
+ connected = false;
395
+ }
396
+
397
+ list.push({
398
+ Id: id,
399
+ Type: instance.type,
400
+ Version: instance.browser.version(),
401
+ CurrentUrl: currentUrl,
402
+ CurrentTitle: currentTitle,
403
+ IsConnected: connected
404
+ });
405
+ }
406
+ return { Browsers: list };
407
+ }
408
+
409
+ async function runClose(args) {
410
+ const browserId = parseArg(args, '--browser-id');
411
+ if (!browserId) throw new Error('Missing --browser-id');
412
+
413
+ const instance = browsers.get(browserId);
414
+ if (!instance) {
415
+ return { Message: 'OK' };
416
+ }
417
+
418
+ try {
419
+ await instance.browser.close();
420
+ } catch {}
421
+ browsers.delete(browserId);
422
+ return { Message: 'OK' };
423
+ }
424
+
425
+ function getLocator(page, selector, selectorType) {
426
+ switch ((selectorType || 'css').toLowerCase()) {
427
+ case 'xpath':
428
+ return page.locator(`xpath=${selector}`);
429
+ case 'text':
430
+ return page.getByText(selector);
431
+ case 'id':
432
+ return page.locator(`#${selector}`);
433
+ default:
434
+ return page.locator(selector);
435
+ }
436
+ }
437
+
438
+ async function executeCommand(command, args) {
439
+ switch (command) {
440
+ case 'ping':
441
+ return { Message: 'pong' };
442
+ case 'launch':
443
+ return runLaunch(args);
444
+ case 'list':
445
+ return runList();
446
+ case 'navigate':
447
+ return runNavigate(args);
448
+ case 'click':
449
+ return runClick(args);
450
+ case 'fill':
451
+ return runFill(args);
452
+ case 'find':
453
+ return runFind(args);
454
+ case 'get-text':
455
+ return runGetText(args);
456
+ case 'screenshot':
457
+ return runScreenshot(args);
458
+ case 'evaluate':
459
+ return runEvaluate(args);
460
+ case 'wait-for':
461
+ return runWaitFor(args);
462
+ case 'close':
463
+ return runClose(args);
464
+ default:
465
+ throw new Error(`Unknown command: ${command}`);
466
+ }
467
+ }
468
+
469
+ async function runDaemon() {
470
+ const server = net.createServer((socket) => {
471
+ let buffer = '';
472
+ socket.on('data', async (chunk) => {
473
+ buffer += chunk.toString();
474
+ const lineEnd = buffer.indexOf('\n');
475
+ if (lineEnd === -1) return;
476
+
477
+ const line = buffer.slice(0, lineEnd).trim();
478
+ buffer = '';
479
+ const req = safeJsonParse(line);
480
+ if (!req || !req.command) {
481
+ writeJsonLine(socket, { ok: false, error: 'Invalid request' });
482
+ return;
483
+ }
484
+
485
+ try {
486
+ const data = await executeCommand(req.command, req.args || []);
487
+ writeJsonLine(socket, { ok: true, data });
488
+ } catch (err) {
489
+ writeJsonLine(socket, { ok: false, error: err.message || String(err) });
490
+ }
491
+ });
492
+ });
493
+
494
+ server.listen(0, '127.0.0.1', () => {
495
+ const port = server.address().port;
496
+ fs.writeFileSync(DAEMON_FILE, JSON.stringify({ pid: process.pid, port }));
497
+ });
498
+
499
+ const shutdown = async () => {
500
+ for (const instance of browsers.values()) {
501
+ try {
502
+ await instance.browser.close();
503
+ } catch {}
504
+ }
505
+ try {
506
+ fs.unlinkSync(DAEMON_FILE);
507
+ } catch {}
508
+ process.exit(0);
509
+ };
510
+
511
+ process.on('SIGINT', shutdown);
512
+ process.on('SIGTERM', shutdown);
513
+ }
514
+
515
+ async function runClient(command, args) {
516
+ const daemon = await ensureDaemon();
517
+ const result = await sendRequest(daemon.port, { command, args });
518
+ if (!result.ok) {
519
+ throw new Error(result.error || 'Bridge command failed');
520
+ }
521
+
522
+ const data = result.data;
523
+ if (data === undefined || data === null) {
524
+ return;
525
+ }
526
+ if (typeof data === 'string') {
527
+ console.log(data);
528
+ return;
529
+ }
530
+ console.log(JSON.stringify(data));
531
+ }
532
+
533
+ async function main() {
534
+ const args = process.argv.slice(2);
535
+ const command = args[0];
536
+
537
+ if (!command) {
538
+ console.error('ERROR: Missing command');
539
+ process.exit(1);
540
+ }
541
+
542
+ if (command === 'daemon') {
543
+ await runDaemon();
544
+ return;
545
+ }
546
+
547
+ if (command === '--version' || command === '-v') {
548
+ console.log('EasyTouch Playwright Bridge v2.0.0');
549
+ return;
550
+ }
551
+
552
+ try {
553
+ await runClient(command, args.slice(1));
554
+ } catch (err) {
555
+ console.error(`ERROR: ${err.message || String(err)}`);
556
+ process.exit(1);
557
+ }
558
+ }
559
+
560
+ main();