assistsx-js 0.2.2 → 0.2.4
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 +16 -7
- package/dist/index.cjs +728 -152
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +236 -18
- package/dist/index.d.ts +236 -18
- package/dist/index.global.js +3 -1
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +720 -152
- package/dist/index.js.map +1 -1
- package/package.json +6 -1
- package/src/assistsx-async.ts +33 -0
- package/src/assistsx.ts +33 -0
- package/src/call-method.ts +1 -0
- package/src/db/db-call-method.ts +11 -0
- package/src/db/db.ts +254 -0
- package/src/global.d.ts +8 -0
- package/src/index.ts +6 -0
- package/src/log/log-call-method.ts +3 -0
- package/src/log/log.ts +124 -19
- package/src/pinia-ensure.ts +15 -0
- package/src/plugin-info.ts +70 -0
- package/src/screenshot/screenshot-call-method.ts +16 -0
- package/src/screenshot/screenshot.ts +200 -0
- package/src/step-flow/legacy-handoff.ts +50 -0
- package/src/step-flow/outcome.ts +16 -0
- package/src/step-flow/payload.ts +23 -0
- package/src/step-flow/runner.ts +128 -0
- package/src/step.ts +48 -14
- package/src/window-flags.ts +21 -0
package/src/log/log.ts
CHANGED
|
@@ -10,6 +10,44 @@ const pendingCallbacks: Map<string, (data: string) => void> = new Map();
|
|
|
10
10
|
const streamHandlers: Map<string, (data: string) => void> = new Map();
|
|
11
11
|
const subscriptionIdToCallbackId: Map<string, string> = new Map();
|
|
12
12
|
|
|
13
|
+
/** 日志文件定位:dirPath 为绝对路径目录,fileName 不含 .txt 后缀;均未传时默认 log-default.txt */
|
|
14
|
+
export interface LogTarget {
|
|
15
|
+
dirPath?: string;
|
|
16
|
+
fileName?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface LogCallOptions extends LogTarget {
|
|
20
|
+
/** 超时时间(秒),默认 30 */
|
|
21
|
+
timeout?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function buildLogArguments(
|
|
25
|
+
target?: LogTarget,
|
|
26
|
+
extra?: Record<string, unknown>
|
|
27
|
+
): Record<string, unknown> | undefined {
|
|
28
|
+
const args: Record<string, unknown> = { ...extra };
|
|
29
|
+
if (target?.dirPath !== undefined) {
|
|
30
|
+
args.dirPath = target.dirPath;
|
|
31
|
+
}
|
|
32
|
+
if (target?.fileName !== undefined) {
|
|
33
|
+
args.fileName = target.fileName;
|
|
34
|
+
}
|
|
35
|
+
return Object.keys(args).length > 0 ? args : undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveTimeout(
|
|
39
|
+
timeoutOrOptions?: number | LogCallOptions,
|
|
40
|
+
fallback = 30
|
|
41
|
+
): { timeout: number; target?: LogTarget } {
|
|
42
|
+
if (typeof timeoutOrOptions === "number") {
|
|
43
|
+
return { timeout: timeoutOrOptions };
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
timeout: timeoutOrOptions?.timeout ?? fallback,
|
|
47
|
+
target: timeoutOrOptions,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
13
51
|
if (typeof window !== "undefined" && !window.assistsxLogCallback) {
|
|
14
52
|
window.assistsxLogCallback = (data: string) => {
|
|
15
53
|
try {
|
|
@@ -49,7 +87,7 @@ export const logUpdateListeners: Array<
|
|
|
49
87
|
> = [];
|
|
50
88
|
|
|
51
89
|
/**
|
|
52
|
-
* Base64 解码后的 CallResponse,data 含 stream / text
|
|
90
|
+
* Base64 解码后的 CallResponse,data 含 stream / text / logFilePath
|
|
53
91
|
*/
|
|
54
92
|
export interface LogUpdateEvent {
|
|
55
93
|
code: number;
|
|
@@ -61,15 +99,17 @@ export interface LogUpdateEvent {
|
|
|
61
99
|
export interface LogUpdateData {
|
|
62
100
|
stream: LogStreamType;
|
|
63
101
|
text: string;
|
|
102
|
+
logFilePath?: string;
|
|
64
103
|
}
|
|
65
104
|
|
|
66
105
|
export interface LogSubscribeUpdatePayload {
|
|
67
106
|
text: string;
|
|
68
107
|
stream: string;
|
|
69
108
|
subscriptionId: string;
|
|
109
|
+
logFilePath?: string;
|
|
70
110
|
}
|
|
71
111
|
|
|
72
|
-
export interface LogUploadOptions {
|
|
112
|
+
export interface LogUploadOptions extends LogTarget {
|
|
73
113
|
baseUrl?: string;
|
|
74
114
|
/** PNG(默认)| JPEG | JPG | WEBP */
|
|
75
115
|
format?: "PNG" | "JPEG" | "JPG" | "WEBP" | string;
|
|
@@ -150,10 +190,13 @@ export class Log {
|
|
|
150
190
|
}
|
|
151
191
|
|
|
152
192
|
/** 读取当前日志全文 */
|
|
153
|
-
async readAllText(
|
|
193
|
+
async readAllText(
|
|
194
|
+
timeoutOrOptions?: number | LogCallOptions
|
|
195
|
+
): Promise<string> {
|
|
196
|
+
const { timeout, target } = resolveTimeout(timeoutOrOptions);
|
|
154
197
|
const res = await this.asyncCall(
|
|
155
198
|
LogCallMethod.readAllText,
|
|
156
|
-
|
|
199
|
+
buildLogArguments(target),
|
|
157
200
|
timeout
|
|
158
201
|
);
|
|
159
202
|
const d = res.getDataOrNull() as { text?: string } | null;
|
|
@@ -174,21 +217,42 @@ export class Log {
|
|
|
174
217
|
return d?.baseUrl ?? "";
|
|
175
218
|
}
|
|
176
219
|
|
|
220
|
+
/**
|
|
221
|
+
* 解析日志文件绝对路径(不创建文件)。
|
|
222
|
+
* AssistsX 插件环境下会经原生拦截追加 log-{packageName} 子目录。
|
|
223
|
+
*/
|
|
224
|
+
async resolveLogPath(
|
|
225
|
+
targetOrOptions?: LogTarget | LogCallOptions
|
|
226
|
+
): Promise<string> {
|
|
227
|
+
const { timeout, target } = resolveTimeout(targetOrOptions);
|
|
228
|
+
const res = await this.asyncCall(
|
|
229
|
+
LogCallMethod.resolveLogPath,
|
|
230
|
+
buildLogArguments(target),
|
|
231
|
+
timeout
|
|
232
|
+
);
|
|
233
|
+
const d = res.getDataOrNull() as { logFilePath?: string } | null;
|
|
234
|
+
return d?.logFilePath ?? "";
|
|
235
|
+
}
|
|
236
|
+
|
|
177
237
|
/** 清空日志 */
|
|
178
|
-
async clear(
|
|
238
|
+
async clear(timeoutOrOptions?: number | LogCallOptions): Promise<boolean> {
|
|
239
|
+
const { timeout, target } = resolveTimeout(timeoutOrOptions);
|
|
179
240
|
const res = await this.asyncCall(
|
|
180
241
|
LogCallMethod.clear,
|
|
181
|
-
|
|
242
|
+
buildLogArguments(target),
|
|
182
243
|
timeout
|
|
183
244
|
);
|
|
184
245
|
return res.isSuccess();
|
|
185
246
|
}
|
|
186
247
|
|
|
187
248
|
/** 从文件重新加载到内存 Flow */
|
|
188
|
-
async refreshFromFile(
|
|
249
|
+
async refreshFromFile(
|
|
250
|
+
timeoutOrOptions?: number | LogCallOptions
|
|
251
|
+
): Promise<boolean> {
|
|
252
|
+
const { timeout, target } = resolveTimeout(timeoutOrOptions);
|
|
189
253
|
const res = await this.asyncCall(
|
|
190
254
|
LogCallMethod.refreshFromFile,
|
|
191
|
-
|
|
255
|
+
buildLogArguments(target),
|
|
192
256
|
timeout
|
|
193
257
|
);
|
|
194
258
|
return res.isSuccess();
|
|
@@ -197,17 +261,25 @@ export class Log {
|
|
|
197
261
|
/** 追加一行 */
|
|
198
262
|
async appendLine(
|
|
199
263
|
line: string,
|
|
200
|
-
|
|
264
|
+
maxLengthOrOptions?:
|
|
265
|
+
| number
|
|
266
|
+
| (LogTarget & { maxLength?: number; timeout?: number }),
|
|
201
267
|
timeout?: number
|
|
202
268
|
): Promise<boolean> {
|
|
203
|
-
|
|
204
|
-
if (
|
|
205
|
-
|
|
269
|
+
let options: (LogTarget & { maxLength?: number; timeout?: number }) | undefined;
|
|
270
|
+
if (typeof maxLengthOrOptions === "number") {
|
|
271
|
+
options = { maxLength: maxLengthOrOptions, timeout };
|
|
272
|
+
} else {
|
|
273
|
+
options = maxLengthOrOptions;
|
|
274
|
+
}
|
|
275
|
+
const args = buildLogArguments(options, { line }) ?? { line };
|
|
276
|
+
if (options?.maxLength !== undefined) {
|
|
277
|
+
args.maxLength = options.maxLength;
|
|
206
278
|
}
|
|
207
279
|
const res = await this.asyncCall(
|
|
208
280
|
LogCallMethod.appendLine,
|
|
209
281
|
args,
|
|
210
|
-
timeout
|
|
282
|
+
options?.timeout
|
|
211
283
|
);
|
|
212
284
|
return res.isSuccess();
|
|
213
285
|
}
|
|
@@ -215,24 +287,48 @@ export class Log {
|
|
|
215
287
|
/** 追加带时间戳的条目 */
|
|
216
288
|
async appendTimestampedEntry(
|
|
217
289
|
message: string,
|
|
218
|
-
|
|
290
|
+
timeoutOrOptions?: number | LogCallOptions
|
|
219
291
|
): Promise<boolean> {
|
|
292
|
+
const { timeout, target } = resolveTimeout(timeoutOrOptions);
|
|
293
|
+
const args = buildLogArguments(target, { message }) ?? { message };
|
|
220
294
|
const res = await this.asyncCall(
|
|
221
295
|
LogCallMethod.appendTimestampedEntry,
|
|
222
|
-
|
|
296
|
+
args,
|
|
223
297
|
timeout
|
|
224
298
|
);
|
|
225
299
|
return res.isSuccess();
|
|
226
300
|
}
|
|
227
301
|
|
|
302
|
+
/**
|
|
303
|
+
* 追加日志(appendTimestampedEntry / appendLine 简写)。
|
|
304
|
+
* 默认带时间戳;`timestamped: false` 时走 appendLine。
|
|
305
|
+
*/
|
|
306
|
+
async append(
|
|
307
|
+
text: string,
|
|
308
|
+
options?: LogTarget & {
|
|
309
|
+
/** @default true */
|
|
310
|
+
timestamped?: boolean;
|
|
311
|
+
maxLength?: number;
|
|
312
|
+
timeout?: number;
|
|
313
|
+
}
|
|
314
|
+
): Promise<boolean> {
|
|
315
|
+
const { timestamped = true, maxLength, timeout, ...target } = options ?? {};
|
|
316
|
+
if (timestamped) {
|
|
317
|
+
return this.appendTimestampedEntry(text, { ...target, timeout });
|
|
318
|
+
}
|
|
319
|
+
return this.appendLine(text, { ...target, maxLength, timeout });
|
|
320
|
+
}
|
|
321
|
+
|
|
228
322
|
/** 替换全部内容 */
|
|
229
323
|
async replaceAll(
|
|
230
324
|
content: string,
|
|
231
|
-
|
|
325
|
+
timeoutOrOptions?: number | LogCallOptions
|
|
232
326
|
): Promise<boolean> {
|
|
327
|
+
const { timeout, target } = resolveTimeout(timeoutOrOptions);
|
|
328
|
+
const args = buildLogArguments(target, { content }) ?? { content };
|
|
233
329
|
const res = await this.asyncCall(
|
|
234
330
|
LogCallMethod.replaceAll,
|
|
235
|
-
|
|
331
|
+
args,
|
|
236
332
|
timeout
|
|
237
333
|
);
|
|
238
334
|
return res.isSuccess();
|
|
@@ -245,7 +341,7 @@ export class Log {
|
|
|
245
341
|
async subscribe(
|
|
246
342
|
stream: LogStreamType,
|
|
247
343
|
onUpdate: (payload: LogSubscribeUpdatePayload) => void,
|
|
248
|
-
options?:
|
|
344
|
+
options?: LogCallOptions
|
|
249
345
|
): Promise<{
|
|
250
346
|
subscriptionId: string;
|
|
251
347
|
dispose: () => Promise<void>;
|
|
@@ -253,6 +349,7 @@ export class Log {
|
|
|
253
349
|
const self = this;
|
|
254
350
|
const callbackId = generateUUID();
|
|
255
351
|
const timeoutSec = options?.timeout ?? 30;
|
|
352
|
+
const subscribeArgs = buildLogArguments(options, { stream }) ?? { stream };
|
|
256
353
|
|
|
257
354
|
return new Promise((resolve, reject) => {
|
|
258
355
|
let settled = false;
|
|
@@ -273,6 +370,7 @@ export class Log {
|
|
|
273
370
|
subscriptionId?: string;
|
|
274
371
|
stream?: string;
|
|
275
372
|
text?: string;
|
|
373
|
+
logFilePath?: string;
|
|
276
374
|
};
|
|
277
375
|
};
|
|
278
376
|
try {
|
|
@@ -318,6 +416,7 @@ export class Log {
|
|
|
318
416
|
text: data.text ?? "",
|
|
319
417
|
stream: data.stream ?? stream,
|
|
320
418
|
subscriptionId: data.subscriptionId,
|
|
419
|
+
logFilePath: data.logFilePath,
|
|
321
420
|
});
|
|
322
421
|
}
|
|
323
422
|
});
|
|
@@ -326,7 +425,7 @@ export class Log {
|
|
|
326
425
|
this.getBridge().call(
|
|
327
426
|
JSON.stringify({
|
|
328
427
|
method: LogCallMethod.subscribe,
|
|
329
|
-
arguments:
|
|
428
|
+
arguments: subscribeArgs,
|
|
330
429
|
callbackId,
|
|
331
430
|
})
|
|
332
431
|
);
|
|
@@ -377,6 +476,12 @@ export class Log {
|
|
|
377
476
|
if (args.uploadKey !== undefined) {
|
|
378
477
|
payload.uploadKey = args.uploadKey;
|
|
379
478
|
}
|
|
479
|
+
if (args.dirPath !== undefined) {
|
|
480
|
+
payload.dirPath = args.dirPath;
|
|
481
|
+
}
|
|
482
|
+
if (args.fileName !== undefined) {
|
|
483
|
+
payload.fileName = args.fileName;
|
|
484
|
+
}
|
|
380
485
|
const uuid = generateUUID();
|
|
381
486
|
const params = {
|
|
382
487
|
method: LogCallMethod.uploadLogs,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createPinia, getActivePinia, setActivePinia, type Pinia } from "pinia";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 当宿主应用未执行 app.use(pinia) 时,为 assistsx-js 内部使用的 Pinia store 提供默认实例,
|
|
5
|
+
* 避免 useStepStore 等在无 active Pinia 时抛错。
|
|
6
|
+
*/
|
|
7
|
+
let fallbackPinia: Pinia | null = null;
|
|
8
|
+
|
|
9
|
+
export function ensureAssistsXPinia(): void {
|
|
10
|
+
if (getActivePinia() !== undefined) return;
|
|
11
|
+
if (fallbackPinia === null) {
|
|
12
|
+
fallbackPinia = createPinia();
|
|
13
|
+
}
|
|
14
|
+
setActivePinia(fallbackPinia);
|
|
15
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AssistsX 当前运行插件信息(由宿主拦截 getCurrentPlugin 返回)
|
|
3
|
+
*/
|
|
4
|
+
export class PluginInfo {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
packageName: string;
|
|
8
|
+
versionName: string;
|
|
9
|
+
versionCode: number;
|
|
10
|
+
description: string;
|
|
11
|
+
path: string;
|
|
12
|
+
index: string;
|
|
13
|
+
port: number;
|
|
14
|
+
needScreenCapture: boolean;
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
id: string = "",
|
|
18
|
+
name: string = "",
|
|
19
|
+
packageName: string = "",
|
|
20
|
+
versionName: string = "",
|
|
21
|
+
versionCode: number = 0,
|
|
22
|
+
description: string = "",
|
|
23
|
+
path: string = "",
|
|
24
|
+
index: string = "",
|
|
25
|
+
port: number = 0,
|
|
26
|
+
needScreenCapture: boolean = false
|
|
27
|
+
) {
|
|
28
|
+
this.id = id;
|
|
29
|
+
this.name = name;
|
|
30
|
+
this.packageName = packageName;
|
|
31
|
+
this.versionName = versionName;
|
|
32
|
+
this.versionCode = versionCode;
|
|
33
|
+
this.description = description;
|
|
34
|
+
this.path = path;
|
|
35
|
+
this.index = index;
|
|
36
|
+
this.port = port;
|
|
37
|
+
this.needScreenCapture = needScreenCapture;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static fromJSON(data: unknown): PluginInfo {
|
|
41
|
+
const record = (data ?? {}) as Record<string, unknown>;
|
|
42
|
+
return new PluginInfo(
|
|
43
|
+
String(record.id ?? ""),
|
|
44
|
+
String(record.name ?? ""),
|
|
45
|
+
String(record.packageName ?? ""),
|
|
46
|
+
String(record.versionName ?? ""),
|
|
47
|
+
Number(record.versionCode ?? 0),
|
|
48
|
+
String(record.description ?? ""),
|
|
49
|
+
String(record.path ?? ""),
|
|
50
|
+
String(record.index ?? ""),
|
|
51
|
+
Number(record.port ?? 0),
|
|
52
|
+
Boolean(record.needScreenCapture ?? false)
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
toJSON(): Record<string, unknown> {
|
|
57
|
+
return {
|
|
58
|
+
id: this.id,
|
|
59
|
+
name: this.name,
|
|
60
|
+
packageName: this.packageName,
|
|
61
|
+
versionName: this.versionName,
|
|
62
|
+
versionCode: this.versionCode,
|
|
63
|
+
description: this.description,
|
|
64
|
+
path: this.path,
|
|
65
|
+
index: this.index,
|
|
66
|
+
port: this.port,
|
|
67
|
+
needScreenCapture: this.needScreenCapture,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 截图相关的 JavascriptInterface 方法常量
|
|
3
|
+
*/
|
|
4
|
+
export const ScreenshotCallMethod = {
|
|
5
|
+
/** 截取全屏并返回 Base64 */
|
|
6
|
+
takeScreenshotBase64: "takeScreenshotBase64",
|
|
7
|
+
|
|
8
|
+
/** 截取指定节点区域并返回 Base64,需传 node.nodeId */
|
|
9
|
+
takeNodeScreenshotBase64: "takeNodeScreenshotBase64",
|
|
10
|
+
|
|
11
|
+
/** 批量截取节点区域并返回 Base64 数组,nodes 为空时返回全屏 */
|
|
12
|
+
takeScreenshotNodesBase64: "takeScreenshotNodesBase64",
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
export type ScreenshotCallMethodType =
|
|
16
|
+
(typeof ScreenshotCallMethod)[keyof typeof ScreenshotCallMethod];
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 截图专用 Bridge:assistsxScreenshot
|
|
3
|
+
* 直接返回 Base64 / data URL,无需经过文件保存
|
|
4
|
+
*/
|
|
5
|
+
import { CallResponse } from "../call-response";
|
|
6
|
+
import { Node } from "../node";
|
|
7
|
+
import { decodeBase64UTF8, generateUUID } from "../utils";
|
|
8
|
+
import { ScreenshotCallMethod } from "./screenshot-call-method";
|
|
9
|
+
|
|
10
|
+
export type ScreenshotImageFormat = "PNG" | "JPEG" | "JPG" | "WEBP";
|
|
11
|
+
|
|
12
|
+
/** takeScreenshotBase64 / takeNodeScreenshotBase64 返回数据 */
|
|
13
|
+
export interface ScreenshotBase64Data {
|
|
14
|
+
base64: string;
|
|
15
|
+
dataUrl: string;
|
|
16
|
+
mimeType: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TakeScreenshotBase64Options {
|
|
20
|
+
overlayHiddenScreenshotDelayMillis?: number;
|
|
21
|
+
format?: ScreenshotImageFormat;
|
|
22
|
+
withDataUrlPrefix?: boolean;
|
|
23
|
+
timeout?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TakeScreenshotNodesBase64Options extends TakeScreenshotBase64Options {}
|
|
27
|
+
|
|
28
|
+
// 回调函数存储对象
|
|
29
|
+
const callbacks: Map<string, (data: string) => void> = new Map();
|
|
30
|
+
|
|
31
|
+
// 初始化全局回调函数
|
|
32
|
+
if (typeof window !== "undefined" && !window.assistsxScreenshotCallback) {
|
|
33
|
+
window.assistsxScreenshotCallback = (data: string) => {
|
|
34
|
+
let callbackId: string | undefined;
|
|
35
|
+
try {
|
|
36
|
+
const json = decodeBase64UTF8(data);
|
|
37
|
+
const response = JSON.parse(json);
|
|
38
|
+
callbackId = response.callbackId;
|
|
39
|
+
if (callbackId) {
|
|
40
|
+
const callback = callbacks.get(callbackId);
|
|
41
|
+
if (callback) {
|
|
42
|
+
callback(json);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch (e) {
|
|
46
|
+
console.error("Screenshot callback error:", e);
|
|
47
|
+
} finally {
|
|
48
|
+
if (callbackId) {
|
|
49
|
+
callbacks.delete(callbackId);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createNodeStub(nodeId: string): Node {
|
|
56
|
+
return { nodeId } as Node;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseScreenshotBase64Data(data: unknown): ScreenshotBase64Data | null {
|
|
60
|
+
if (!data || typeof data !== "object") {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const record = data as Record<string, unknown>;
|
|
64
|
+
const base64 = typeof record.base64 === "string" ? record.base64 : "";
|
|
65
|
+
const mimeType = typeof record.mimeType === "string" ? record.mimeType : "image/png";
|
|
66
|
+
const dataUrl =
|
|
67
|
+
typeof record.dataUrl === "string"
|
|
68
|
+
? record.dataUrl
|
|
69
|
+
: `data:${mimeType};base64,${base64}`;
|
|
70
|
+
if (!base64) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return { base64, dataUrl, mimeType };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class Screenshot {
|
|
77
|
+
/**
|
|
78
|
+
* 执行异步调用
|
|
79
|
+
*/
|
|
80
|
+
private async asyncCall(
|
|
81
|
+
method: string,
|
|
82
|
+
{
|
|
83
|
+
args,
|
|
84
|
+
node,
|
|
85
|
+
nodes,
|
|
86
|
+
timeout = 30,
|
|
87
|
+
}: {
|
|
88
|
+
args?: Record<string, unknown>;
|
|
89
|
+
node?: Node;
|
|
90
|
+
nodes?: Node[];
|
|
91
|
+
timeout?: number;
|
|
92
|
+
} = {}
|
|
93
|
+
): Promise<CallResponse> {
|
|
94
|
+
const uuid = generateUUID();
|
|
95
|
+
const params = {
|
|
96
|
+
method,
|
|
97
|
+
arguments: args ? args : undefined,
|
|
98
|
+
node: node ? node : undefined,
|
|
99
|
+
nodes: nodes ? nodes : undefined,
|
|
100
|
+
callbackId: uuid,
|
|
101
|
+
};
|
|
102
|
+
const promise = new Promise<string>((resolve) => {
|
|
103
|
+
callbacks.set(uuid, (data: string) => {
|
|
104
|
+
resolve(data);
|
|
105
|
+
});
|
|
106
|
+
setTimeout(() => {
|
|
107
|
+
callbacks.delete(uuid);
|
|
108
|
+
resolve(JSON.stringify(new CallResponse(0, null, uuid)));
|
|
109
|
+
}, timeout * 1000);
|
|
110
|
+
});
|
|
111
|
+
window.assistsxScreenshot.call(JSON.stringify(params));
|
|
112
|
+
const promiseResult = await promise;
|
|
113
|
+
if (typeof promiseResult === "string") {
|
|
114
|
+
const responseData = JSON.parse(promiseResult);
|
|
115
|
+
return new CallResponse(
|
|
116
|
+
responseData.code,
|
|
117
|
+
responseData.data,
|
|
118
|
+
responseData.callbackId
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
throw new Error("Screenshot call failed");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private buildScreenshotArgs(
|
|
125
|
+
options: TakeScreenshotBase64Options = {}
|
|
126
|
+
): Record<string, unknown> {
|
|
127
|
+
const {
|
|
128
|
+
overlayHiddenScreenshotDelayMillis = 250,
|
|
129
|
+
format = "PNG",
|
|
130
|
+
withDataUrlPrefix = true,
|
|
131
|
+
} = options;
|
|
132
|
+
return {
|
|
133
|
+
overlayHiddenScreenshotDelayMillis,
|
|
134
|
+
format,
|
|
135
|
+
withDataUrlPrefix,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 截取全屏并返回 Base64
|
|
141
|
+
*/
|
|
142
|
+
async takeScreenshotBase64(
|
|
143
|
+
options: TakeScreenshotBase64Options = {}
|
|
144
|
+
): Promise<ScreenshotBase64Data | null> {
|
|
145
|
+
const { timeout = 30, ...rest } = options;
|
|
146
|
+
const response = await this.asyncCall(ScreenshotCallMethod.takeScreenshotBase64, {
|
|
147
|
+
args: this.buildScreenshotArgs(rest),
|
|
148
|
+
timeout,
|
|
149
|
+
});
|
|
150
|
+
if (!response.isSuccess()) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
return parseScreenshotBase64Data(response.getDataOrNull());
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 截取指定节点区域并返回 Base64
|
|
158
|
+
*/
|
|
159
|
+
async takeNodeScreenshotBase64(
|
|
160
|
+
nodeId: string,
|
|
161
|
+
options: TakeScreenshotBase64Options = {}
|
|
162
|
+
): Promise<ScreenshotBase64Data | null> {
|
|
163
|
+
if (!nodeId) {
|
|
164
|
+
throw new Error("nodeId is required");
|
|
165
|
+
}
|
|
166
|
+
const { timeout = 30, ...rest } = options;
|
|
167
|
+
const response = await this.asyncCall(ScreenshotCallMethod.takeNodeScreenshotBase64, {
|
|
168
|
+
args: this.buildScreenshotArgs(rest),
|
|
169
|
+
node: createNodeStub(nodeId),
|
|
170
|
+
timeout,
|
|
171
|
+
});
|
|
172
|
+
if (!response.isSuccess()) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
return parseScreenshotBase64Data(response.getDataOrNull());
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 批量截取节点区域;nodeIds 为空时返回全屏截图
|
|
180
|
+
*/
|
|
181
|
+
async takeScreenshotNodesBase64(
|
|
182
|
+
nodeIds: string[] = [],
|
|
183
|
+
options: TakeScreenshotNodesBase64Options = {}
|
|
184
|
+
): Promise<string[]> {
|
|
185
|
+
const { timeout = 30, ...rest } = options;
|
|
186
|
+
const nodes = nodeIds.map((nodeId) => createNodeStub(nodeId));
|
|
187
|
+
const response = await this.asyncCall(ScreenshotCallMethod.takeScreenshotNodesBase64, {
|
|
188
|
+
args: this.buildScreenshotArgs(rest),
|
|
189
|
+
nodes,
|
|
190
|
+
timeout,
|
|
191
|
+
});
|
|
192
|
+
if (!response.isSuccess()) {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
const data = response.getDataOrNull() as { images?: string[] } | null;
|
|
196
|
+
return Array.isArray(data?.images) ? data.images : [];
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export const screenshot = new Screenshot();
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Step, StepImpl } from "../step";
|
|
2
|
+
import type { FlowStateDef, StepFlowData, StepFlowOutcome } from "./types";
|
|
3
|
+
import { getFlowPayload } from "./payload";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 为 legacy 步骤(如 app-launch)创建 finishMethod:
|
|
7
|
+
* 应用打开成功后跳回 StepFlow dispatcher 并切换到指定状态。
|
|
8
|
+
*/
|
|
9
|
+
export function createFinishHandoff(
|
|
10
|
+
dispatcher: StepImpl,
|
|
11
|
+
nextState: string
|
|
12
|
+
): StepImpl {
|
|
13
|
+
return async (step: Step) => {
|
|
14
|
+
const data = step.data as StepFlowData;
|
|
15
|
+
if (!data.__flow) {
|
|
16
|
+
data.__flow = { state: nextState };
|
|
17
|
+
} else {
|
|
18
|
+
data.__flow.state = nextState;
|
|
19
|
+
}
|
|
20
|
+
return step.next(dispatcher);
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 创建 launch 类流程状态:写入 finishMethod 后委托执行 legacy StepImpl 链。
|
|
26
|
+
*/
|
|
27
|
+
export function createLaunchState(
|
|
28
|
+
dispatcher: StepImpl,
|
|
29
|
+
launchImpl: StepImpl,
|
|
30
|
+
nextState: string,
|
|
31
|
+
/** legacy 步骤从 step.data 读取的字段名(默认与 payload 键一致) */
|
|
32
|
+
legacyDataKeys: string[] = ["appName", "packageName"]
|
|
33
|
+
): FlowStateDef {
|
|
34
|
+
return {
|
|
35
|
+
run: async (step): Promise<StepFlowOutcome> => {
|
|
36
|
+
const payload = getFlowPayload(step);
|
|
37
|
+
for (const key of legacyDataKeys) {
|
|
38
|
+
if (payload[key] !== undefined) {
|
|
39
|
+
(step.data as StepFlowData)[key] = payload[key];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
(step.data as StepFlowData).finishMethod = createFinishHandoff(
|
|
43
|
+
dispatcher,
|
|
44
|
+
nextState
|
|
45
|
+
);
|
|
46
|
+
return { type: "legacy", impl: launchImpl };
|
|
47
|
+
},
|
|
48
|
+
on: {},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { StepFlowOutcome } from "./types";
|
|
2
|
+
|
|
3
|
+
/** 产生流程事件,由当前状态的 on 表决定下一状态 */
|
|
4
|
+
export function flowEvent(name: string): StepFlowOutcome {
|
|
5
|
+
return { type: "event", name };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** 重复当前流程状态(沿用 Step.repeat 语义) */
|
|
9
|
+
export function flowRepeat(): StepFlowOutcome {
|
|
10
|
+
return { type: "repeat" };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** 结束整个流程 */
|
|
14
|
+
export function flowEnd(): StepFlowOutcome {
|
|
15
|
+
return { type: "end" };
|
|
16
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Step, StepData } from "../step";
|
|
2
|
+
import type { StepFlowData } from "./types";
|
|
3
|
+
|
|
4
|
+
function asFlowData(step: Step): StepFlowData {
|
|
5
|
+
return step.data as StepFlowData;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** 读取业务 payload(不存在时返回空对象) */
|
|
9
|
+
export function getFlowPayload<T extends StepData = StepData>(step: Step): T {
|
|
10
|
+
const data = asFlowData(step);
|
|
11
|
+
const payload = data.payload;
|
|
12
|
+
if (payload !== null && payload !== undefined && typeof payload === "object" && !Array.isArray(payload)) {
|
|
13
|
+
return payload as T;
|
|
14
|
+
}
|
|
15
|
+
return {} as T;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 浅合并写入 payload */
|
|
19
|
+
export function assignFlowPayload(step: Step, partial: StepData): void {
|
|
20
|
+
const data = asFlowData(step);
|
|
21
|
+
const current = getFlowPayload(step);
|
|
22
|
+
data.payload = { ...current, ...partial };
|
|
23
|
+
}
|