ectrol 0.0.3 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -41,7 +41,6 @@ interface ISize {
41
41
  declare class ElementHandle {
42
42
  readonly contents: WebContents;
43
43
  readonly selector: string;
44
- isNotFound: boolean;
45
44
  constructor(contents: WebContents, selector: string);
46
45
  /**
47
46
  * 生成在 renderer 端执行的查找脚本。
@@ -53,7 +52,9 @@ declare class ElementHandle {
53
52
  /**
54
53
  * 触发元素 mouseover 事件(不移动真实鼠标)。
55
54
  */
56
- hover(): Promise<any>;
55
+ hover(options?: {
56
+ timeout?: number;
57
+ }): Promise<any>;
57
58
  /**
58
59
  * 点击
59
60
  * - 通过 WebContents.sendInputEvent 发送 mouseDown/mouseUp 事件
@@ -83,14 +84,23 @@ declare class ElementHandle {
83
84
  /**
84
85
  * 将复选框或单选按钮设为选中,并派发 change 事件。
85
86
  */
86
- check(): Promise<any>;
87
+ check(options?: {
88
+ timeout?: number;
89
+ }): Promise<any>;
87
90
  /**
88
91
  * 填充输入框内容。
89
92
  * - 适用于 `<input>`、`<textarea>` 或 `contenteditable` 元素。
90
93
  * - 会触发 `input` 事件(冒泡)。
94
+ * - 支持自定义输入间隔, 'auto' 表示模拟自然输入节奏
95
+ * @example
96
+ * ```
97
+ * await element.fill('Hello, World!', { interval: 100 });
98
+ * ```
91
99
  */
92
100
  fill(value: string, options?: {
93
101
  timeout?: number;
102
+ interval?: number | 'auto';
103
+ append?: boolean;
94
104
  }): Promise<void>;
95
105
  /**
96
106
  * 在元素上发送按键事件。
@@ -124,55 +134,77 @@ declare class ElementHandle {
124
134
  * @param value 选项的值
125
135
  * @returns
126
136
  */
127
- selectOption(value: string): Promise<any>;
137
+ selectOption(value: string, options?: {
138
+ timeout?: number;
139
+ }): Promise<any>;
128
140
  /**
129
141
  * 获取元素的 HTML 内容。
130
142
  */
131
- innerHTML(): Promise<any>;
143
+ innerHTML(options?: {
144
+ timeout?: number;
145
+ }): Promise<any>;
132
146
  /**
133
147
  * 获取元素的文本内容。
134
148
  */
135
- innerText(): Promise<any>;
149
+ innerText(options?: {
150
+ timeout?: number;
151
+ }): Promise<any>;
136
152
  /**
137
153
  * 获取元素的文本内容。
138
154
  * @returns 元素的文本内容
139
155
  */
140
- textContent(): Promise<any>;
156
+ textContent(options?: {
157
+ timeout?: number;
158
+ }): Promise<any>;
141
159
  /**
142
160
  * 获取元素的输入值。
143
161
  * @returns 元素的输入值
144
162
  */
145
- inputValue(): Promise<any>;
163
+ inputValue(options?: {
164
+ timeout?: number;
165
+ }): Promise<any>;
146
166
  /**
147
167
  * 获取元素的选中状态。
148
168
  * @returns 元素的选中状态
149
169
  */
150
- isChecked(): Promise<any>;
170
+ isChecked(options?: {
171
+ timeout?: number;
172
+ }): Promise<any>;
151
173
  /**
152
174
  * 获取元素的禁用状态。
153
175
  * @returns 元素的禁用状态
154
176
  */
155
- isDisabled(): Promise<any>;
177
+ isDisabled(options?: {
178
+ timeout?: number;
179
+ }): Promise<any>;
156
180
  /**
157
181
  * 获取元素的可见状态。
158
182
  * @returns 元素的可见状态
159
183
  */
160
- isVisible(): Promise<any>;
184
+ isVisible(options?: {
185
+ timeout?: number;
186
+ }): Promise<any>;
161
187
  /**
162
188
  * 获取元素的可用状态。
163
189
  * @returns 元素的可用状态
164
190
  */
165
- isEnabled(): Promise<any>;
191
+ isEnabled(options?: {
192
+ timeout?: number;
193
+ }): Promise<any>;
166
194
  /**
167
195
  * 获取元素的可编辑状态。
168
196
  * @returns 元素的可编辑状态
169
197
  */
170
- isEditable(): Promise<any>;
198
+ isEditable(options?: {
199
+ timeout?: number;
200
+ }): Promise<any>;
171
201
  /**
172
202
  * 获取元素的隐藏状态。
173
203
  * @returns 元素的隐藏状态
174
204
  */
175
- isHidden(): Promise<any>;
205
+ isHidden(options?: {
206
+ timeout?: number;
207
+ }): Promise<any>;
176
208
  /**
177
209
  * 选中元素的文本内容。
178
210
  */
@@ -237,6 +269,11 @@ declare class SessionStorage {
237
269
  removeItem(key: string): Promise<void>;
238
270
  }
239
271
  //#endregion
272
+ //#region src/utils.d.ts
273
+ declare function randomInRange(min: number, max: number, integer?: boolean): number;
274
+ declare function getHumanTypingDelay(index: number, text: string): number;
275
+ declare function sleep(ms: number): Promise<unknown>;
276
+ //#endregion
240
277
  //#region src/index.d.ts
241
278
  /**
242
279
  * Ectrol
@@ -261,4 +298,4 @@ declare class Ectrol {
261
298
  $(selector: string): ElementHandle;
262
299
  }
263
300
  //#endregion
264
- export { Ectrol, Ectrol as default, ElementHandle, IBoundingClientRect, ISize, IVector2D, LocalStorage, SessionStorage };
301
+ export { Ectrol, Ectrol as default, ElementHandle, IBoundingClientRect, ISize, IVector2D, LocalStorage, SessionStorage, getHumanTypingDelay, randomInRange, sleep };
package/dist/index.mjs CHANGED
@@ -1,3 +1,25 @@
1
+ //#region src/utils.ts
2
+ function randomInRange(min, max, integer = false) {
3
+ if (min > max) [min, max] = [max, min];
4
+ const value = Math.random() * (max - min) + min;
5
+ return integer ? Math.floor(value) : value;
6
+ }
7
+ function getHumanTypingDelay(index, text) {
8
+ const char = text[index];
9
+ let delay = randomInRange(16, 100);
10
+ const progress = index / text.length;
11
+ if (progress < .15) delay *= 1.3;
12
+ else if (progress > .85) delay *= 1.25;
13
+ if (char === " ") delay += 16;
14
+ if (/[,.!?;:]/.test(char)) delay += randomInRange(16, 50);
15
+ if (Math.random() < .03) delay += randomInRange(100, 200);
16
+ return Math.max(40, Math.round(delay));
17
+ }
18
+ async function sleep(ms) {
19
+ return new Promise((resolve) => setTimeout(resolve, ms));
20
+ }
21
+
22
+ //#endregion
1
23
  //#region src/element.ts
2
24
  /**
3
25
  * ElementHandle
@@ -7,7 +29,6 @@
7
29
  * - 大多数 API 接受 `timeout`,轮询查找元素直到超时
8
30
  */
9
31
  var ElementHandle = class {
10
- isNotFound = false;
11
32
  constructor(contents, selector) {
12
33
  this.contents = contents;
13
34
  this.selector = selector;
@@ -20,104 +41,116 @@ var ElementHandle = class {
20
41
  */
21
42
  _getElement(timeout = 0) {
22
43
  return `
23
- (()=>{
24
- function getElement() {
25
- const selector = "${this.selector}";
26
-
27
- if (!selector) return null;
44
+ new Promise((resolve) => {
45
+ function getElement() {
46
+ const selector = "${this.selector}";
47
+ if (!selector) return null;
48
+
49
+ window.__ELECTROL__ = window.__ELECTROL__ || {};
50
+ if (window.__ELECTROL__[selector]) {
51
+ return window.__ELECTROL__[selector];
52
+ }
28
53
 
29
- window.__ELECTROL__ = window.__ELECTROL__ || {};
30
- if (window.__ELECTROL__[selector]) {
31
- return window.__ELECTROL__[selector];
32
- }
54
+ // 分层选择器,支持 iframe 穿越
55
+ const parts = selector.split('|>').map(s => s.trim());
56
+ if (!parts.length) return null;
33
57
 
34
- // 分层选择器,支持 iframe 穿越
35
- const parts = selector.split('|>').map(s => s.trim());
36
- if (!parts.length) return null;
58
+ let currentDocument = document;
59
+ let offsetLeft = 0;
60
+ let offsetTop = 0;
61
+ let element = null;
37
62
 
38
- let currentDocument = document;
39
- let offsetLeft = 0;
40
- let offsetTop = 0;
41
- let element = null;
63
+ for (let i = 0; i < parts.length; i++) {
64
+ if (!currentDocument) return null;
42
65
 
43
- for (let i = 0; i < parts.length; i++) {
44
- if (!currentDocument) return null;
66
+ const part = parts[i];
67
+ // 在当前文档查找该层选择器
68
+ const el = currentDocument.querySelector(part);
45
69
 
46
- const part = parts[i];
47
- // 在当前文档查找该层选择器
48
- const el = currentDocument.querySelector(part);
70
+ if (!el) {
71
+ console.warn('未找到元素:', part);
72
+ return null;
73
+ }
49
74
 
50
- if (!el) {
51
- console.warn('未找到元素:', part);
52
- return null;
53
- }
75
+ const rect = el.getBoundingClientRect();
54
76
 
55
- const rect = el.getBoundingClientRect();
77
+ // 累加当前层级偏移
78
+ offsetLeft += rect.left;
79
+ offsetTop += rect.top;
56
80
 
57
- // 累加当前层级偏移
58
- offsetLeft += rect.left;
59
- offsetTop += rect.top;
81
+ element = el;
60
82
 
61
- element = el;
83
+ const isLast = i === parts.length - 1;
84
+ if (!isLast) {
85
+ if (el.tagName !== 'IFRAME') {
86
+ console.warn('非 iframe 元素却尝试进入下一层:', part);
87
+ return null;
88
+ }
62
89
 
63
- const isLast = i === parts.length - 1;
64
- if (!isLast) {
65
- if (el.tagName !== 'IFRAME') {
66
- console.warn('非 iframe 元素却尝试进入下一层:', part);
67
- return null;
68
- }
90
+ const nextDoc = el.contentDocument || el.contentWindow?.document;
91
+ if (!nextDoc) {
92
+ console.warn('无法访问 iframe.contentDocument(可能跨域)');
93
+ return null;
94
+ }
69
95
 
70
- const nextDoc = el.contentDocument || el.contentWindow?.document;
71
- if (!nextDoc) {
72
- console.warn('无法访问 iframe.contentDocument(可能跨域)');
73
- return null;
96
+ currentDocument = nextDoc;
74
97
  }
75
-
76
- currentDocument = nextDoc;
77
98
  }
78
- }
79
99
 
80
- if (!element) return null;
100
+ if (!element) return null;
81
101
 
82
- const finalRect = element.getBoundingClientRect();
102
+ const finalRect = element.getBoundingClientRect();
83
103
 
84
- // 缓存元素
85
- // 缓存以避免重复查找
86
- window.__ELECTROL__[selector] = {
87
- element,
88
- rect: new DOMRect(
89
- offsetLeft,
90
- offsetTop,
91
- finalRect.width,
92
- finalRect.height
93
- )
94
- };
104
+ // 缓存元素
105
+ // 缓存以避免重复查找
106
+ window.__ELECTROL__[selector] = {
107
+ element,
108
+ rect: new DOMRect(
109
+ offsetLeft,
110
+ offsetTop,
111
+ finalRect.width,
112
+ finalRect.height
113
+ )
114
+ };
95
115
 
96
- return window.__ELECTROL__[selector]
97
- }
116
+ return window.__ELECTROL__[selector]
117
+ }
98
118
 
99
- const timeout = ${timeout}
119
+ const timeout = ${timeout}
100
120
 
101
- if(timeout <= 0) {
102
- return getElement();
103
- }
121
+ // 轮询查找元素
122
+ const interval = setInterval(() => {
123
+ const element = getElement();
124
+ if (element) {
125
+ clearInterval(interval);
126
+ clearTimeout(timeoutId);
127
+ resolve(element);
128
+ }
129
+ }, 100);
130
+
131
+ // 超时处理
132
+ const timeoutId = setTimeout(() => {
133
+ clearInterval(interval);
134
+ resolve(null);
135
+ }, timeout);
104
136
 
105
- const startTime = Date.now()
106
- while (Date.now() - startTime < timeout) {
107
137
  const element = getElement();
108
- if (element) return element;
109
- }
110
- return null;
111
- })()
138
+
139
+ if (element) {
140
+ clearInterval(interval);
141
+ clearTimeout(timeoutId);
142
+ resolve(element);
143
+ }
144
+ })
112
145
  `;
113
146
  }
114
147
  /**
115
148
  * 触发元素 mouseover 事件(不移动真实鼠标)。
116
149
  */
117
- hover() {
150
+ hover(options) {
118
151
  return this.contents.executeJavaScript(`
119
- (function() {
120
- const target = ${this._getElement()};
152
+ (async () => {
153
+ const target = await ${this._getElement(options?.timeout)};
121
154
  if (!target) return null;
122
155
 
123
156
  const event = new MouseEvent('mouseover', {
@@ -147,7 +180,7 @@ var ElementHandle = class {
147
180
  button: options?.button || "left",
148
181
  clickCount: 1
149
182
  });
150
- await new Promise((resolve) => setTimeout(resolve, options?.delay || 10));
183
+ await sleep(options?.delay || 10);
151
184
  this.contents.sendInputEvent({
152
185
  type: "mouseUp",
153
186
  x: rect.centerX,
@@ -180,10 +213,10 @@ var ElementHandle = class {
180
213
  /**
181
214
  * 将复选框或单选按钮设为选中,并派发 change 事件。
182
215
  */
183
- check() {
216
+ check(options) {
184
217
  return this.contents.executeJavaScript(`
185
- (function() {
186
- const target = ${this._getElement()};
218
+ (async () => {
219
+ const target = await ${this._getElement(options?.timeout)};
187
220
  if (!target) return null;
188
221
 
189
222
  // 验证输入框是否是复选框或单选按钮
@@ -198,22 +231,32 @@ var ElementHandle = class {
198
231
  * 填充输入框内容。
199
232
  * - 适用于 `<input>`、`<textarea>` 或 `contenteditable` 元素。
200
233
  * - 会触发 `input` 事件(冒泡)。
234
+ * - 支持自定义输入间隔, 'auto' 表示模拟自然输入节奏
235
+ * @example
236
+ * ```
237
+ * await element.fill('Hello, World!', { interval: 100 });
238
+ * ```
201
239
  */
202
240
  async fill(value, options) {
203
- await this.contents.executeJavaScript(`
204
- (function() {
205
- const target = ${this._getElement(options?.timeout)};
206
- if (!target) return null;
207
-
208
- // 验证输入框是否是文本类型
209
- if (target.element.tagName !== 'INPUT' && target.element.tagName !== 'TEXTAREA' && !target.element.isContentEditable) return null;
210
-
211
- target.element.focus();
212
-
213
- target.element.value = ${JSON.stringify(value)};
214
- target.element.dispatchEvent(new Event('input', { bubbles: true }));
241
+ await this.focus({ timeout: options?.timeout });
242
+ let interval;
243
+ if (typeof options?.interval === "number") interval = options?.interval;
244
+ if (!options?.append) await this.contents.executeJavaScript(`
245
+ (async () => {
246
+ const target = await ${this._getElement(options?.timeout)};
247
+ if (target && target.element) {
248
+ if (target.element.tagName !== 'INPUT' && target.element.tagName !== 'TEXTAREA' && !target.element.isContentEditable) return null;
249
+ target.element.value = "";
250
+ }
215
251
  })()
216
252
  `);
253
+ for (let index = 0; index < value.length; index++) {
254
+ await sleep(interval || getHumanTypingDelay(index, value));
255
+ this.contents.sendInputEvent({
256
+ type: "char",
257
+ keyCode: value[index]
258
+ });
259
+ }
217
260
  }
218
261
  /**
219
262
  * 在元素上发送按键事件。
@@ -230,7 +273,7 @@ var ElementHandle = class {
230
273
  type: "keyDown",
231
274
  keyCode: key$1
232
275
  });
233
- await new Promise((resolve) => setTimeout(resolve, options?.delay || 10));
276
+ await sleep(options?.delay || 10);
234
277
  for (const key$1 of keys) this.contents.sendInputEvent({
235
278
  type: "keyUp",
236
279
  keyCode: key$1
@@ -241,8 +284,8 @@ var ElementHandle = class {
241
284
  */
242
285
  async focus(options) {
243
286
  return this.contents.executeJavaScript(`
244
- (function() {
245
- const target = ${this._getElement(options?.timeout)};
287
+ (async () => {
288
+ const target = await ${this._getElement(options?.timeout)};
246
289
  if (!target) return null;
247
290
 
248
291
  // 验证输入框是否是文本类型
@@ -263,11 +306,9 @@ var ElementHandle = class {
263
306
  * - 返回扩展的 DOMRect 信息并包含元素中心点坐标。
264
307
  */
265
308
  getBoundingClientRect(timeout = 0) {
266
- if (this.isNotFound) return Promise.resolve(null);
267
309
  return this.contents.executeJavaScript(`
268
- (function() {
269
- const target = ${this._getElement(timeout)};
270
-
310
+ (async () => {
311
+ const target = await ${this._getElement(timeout)};
271
312
  if (!target) return null;
272
313
 
273
314
  const rect = target.rect;
@@ -291,10 +332,10 @@ var ElementHandle = class {
291
332
  * @param value 选项的值
292
333
  * @returns
293
334
  */
294
- async selectOption(value) {
335
+ async selectOption(value, options) {
295
336
  return this.contents.executeJavaScript(`
296
- (function() {
297
- const target = ${this._getElement()};
337
+ (async () => {
338
+ const target = await ${this._getElement(options?.timeout)};
298
339
  if (!target) return null;
299
340
 
300
341
  // 验证元素是否为选择框
@@ -308,10 +349,10 @@ var ElementHandle = class {
308
349
  /**
309
350
  * 获取元素的 HTML 内容。
310
351
  */
311
- async innerHTML() {
352
+ async innerHTML(options) {
312
353
  return this.contents.executeJavaScript(`
313
- (function() {
314
- const target = ${this._getElement()};
354
+ (async () => {
355
+ const target = await ${this._getElement(options?.timeout)};
315
356
  if (!target) return null;
316
357
 
317
358
  return target.element.innerHTML;
@@ -321,10 +362,10 @@ var ElementHandle = class {
321
362
  /**
322
363
  * 获取元素的文本内容。
323
364
  */
324
- async innerText() {
365
+ async innerText(options) {
325
366
  return this.contents.executeJavaScript(`
326
- (function() {
327
- const target = ${this._getElement()};
367
+ (async () => {
368
+ const target = await ${this._getElement(options?.timeout)};
328
369
  if (!target) return null;
329
370
 
330
371
  return target.element.innerText;
@@ -335,10 +376,10 @@ var ElementHandle = class {
335
376
  * 获取元素的文本内容。
336
377
  * @returns 元素的文本内容
337
378
  */
338
- async textContent() {
379
+ async textContent(options) {
339
380
  return this.contents.executeJavaScript(`
340
- (function() {
341
- const target = ${this._getElement()};
381
+ (async () => {
382
+ const target = await ${this._getElement(options?.timeout)};
342
383
  if (!target) return null;
343
384
 
344
385
  return target.element.textContent;
@@ -349,10 +390,10 @@ var ElementHandle = class {
349
390
  * 获取元素的输入值。
350
391
  * @returns 元素的输入值
351
392
  */
352
- async inputValue() {
393
+ async inputValue(options) {
353
394
  return this.contents.executeJavaScript(`
354
- (function() {
355
- const target = ${this._getElement()};
395
+ (async () => {
396
+ const target = await ${this._getElement(options?.timeout)};
356
397
  if (!target) return null;
357
398
 
358
399
  // 验证输入框是否是文本类型
@@ -366,10 +407,10 @@ var ElementHandle = class {
366
407
  * 获取元素的选中状态。
367
408
  * @returns 元素的选中状态
368
409
  */
369
- async isChecked() {
410
+ async isChecked(options) {
370
411
  return this.contents.executeJavaScript(`
371
- (function() {
372
- const target = ${this._getElement()};
412
+ (async () => {
413
+ const target = await ${this._getElement(options?.timeout)};
373
414
  if (!target) return null;
374
415
 
375
416
  // 验证输入框是否是复选框或单选框
@@ -383,10 +424,10 @@ var ElementHandle = class {
383
424
  * 获取元素的禁用状态。
384
425
  * @returns 元素的禁用状态
385
426
  */
386
- async isDisabled() {
427
+ async isDisabled(options) {
387
428
  return this.contents.executeJavaScript(`
388
- (function() {
389
- const target = ${this._getElement()};
429
+ (async () => {
430
+ const target = await ${this._getElement(options?.timeout)};
390
431
  if (!target) return null;
391
432
 
392
433
  return target.element.disabled;
@@ -397,10 +438,10 @@ var ElementHandle = class {
397
438
  * 获取元素的可见状态。
398
439
  * @returns 元素的可见状态
399
440
  */
400
- async isVisible() {
441
+ async isVisible(options) {
401
442
  return this.contents.executeJavaScript(`
402
- (function() {
403
- const target = ${this._getElement()};
443
+ (async () => {
444
+ const target = await ${this._getElement(options?.timeout)};
404
445
  if (!target) return null;
405
446
 
406
447
  return target.element.offsetParent !== null;
@@ -411,10 +452,10 @@ var ElementHandle = class {
411
452
  * 获取元素的可用状态。
412
453
  * @returns 元素的可用状态
413
454
  */
414
- async isEnabled() {
455
+ async isEnabled(options) {
415
456
  return this.contents.executeJavaScript(`
416
- (function() {
417
- const target = ${this._getElement()};
457
+ (async () => {
458
+ const target = await ${this._getElement(options?.timeout)};
418
459
  if (!target) return null;
419
460
 
420
461
  return !target.element.disabled;
@@ -425,10 +466,10 @@ var ElementHandle = class {
425
466
  * 获取元素的可编辑状态。
426
467
  * @returns 元素的可编辑状态
427
468
  */
428
- async isEditable() {
469
+ async isEditable(options) {
429
470
  return this.contents.executeJavaScript(`
430
- (function() {
431
- const target = ${this._getElement()};
471
+ (async () => {
472
+ const target = await ${this._getElement(options?.timeout)};
432
473
  if (!target) return null;
433
474
 
434
475
  return target.element.isContentEditable;
@@ -439,10 +480,10 @@ var ElementHandle = class {
439
480
  * 获取元素的隐藏状态。
440
481
  * @returns 元素的隐藏状态
441
482
  */
442
- async isHidden() {
483
+ async isHidden(options) {
443
484
  return this.contents.executeJavaScript(`
444
- (function() {
445
- const target = ${this._getElement()};
485
+ (async () => {
486
+ const target = await ${this._getElement(options?.timeout)};
446
487
  if (!target) return null;
447
488
 
448
489
  return target.element.offsetParent === null;
@@ -454,8 +495,8 @@ var ElementHandle = class {
454
495
  */
455
496
  async selectText(options) {
456
497
  return this.contents.executeJavaScript(`
457
- (function() {
458
- const target = ${this._getElement(options.timeout)};
498
+ (async () => {
499
+ const target = await ${this._getElement(options.timeout)};
459
500
  if (!target) return null;
460
501
 
461
502
  // 选中元素的文本内容
@@ -474,8 +515,8 @@ var ElementHandle = class {
474
515
  */
475
516
  async uncheck(options) {
476
517
  return this.contents.executeJavaScript(`
477
- (function() {
478
- const target = ${this._getElement(options?.timeout)};
518
+ (async () => {
519
+ const target = await ${this._getElement(options?.timeout)};
479
520
  if (!target) return null;
480
521
 
481
522
  // 验证输入框是否是复选框或单选框
@@ -587,4 +628,4 @@ var Ectrol = class {
587
628
  var src_default = Ectrol;
588
629
 
589
630
  //#endregion
590
- export { Ectrol, ElementHandle, LocalStorage, SessionStorage, src_default as default };
631
+ export { Ectrol, ElementHandle, LocalStorage, SessionStorage, src_default as default, getHumanTypingDelay, randomInRange, sleep };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ectrol",
3
3
  "type": "module",
4
- "version": "0.0.3",
4
+ "version": "0.1.0",
5
5
  "description": "一个基于 Electron WebContents 的轻量级自动化助手,用于在你的应用内对网页内容进行交互(点击、输入、聚焦、键盘事件等)。",
6
6
  "author": "Cee Vee X <ceeveex@hotmail.com>",
7
7
  "license": "MIT",