assistsx-js 0.2.3 → 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/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(timeout?: number): Promise<string> {
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
- undefined,
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(timeout?: number): Promise<boolean> {
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
- undefined,
242
+ buildLogArguments(target),
182
243
  timeout
183
244
  );
184
245
  return res.isSuccess();
185
246
  }
186
247
 
187
248
  /** 从文件重新加载到内存 Flow */
188
- async refreshFromFile(timeout?: number): Promise<boolean> {
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
- undefined,
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
- maxLength?: number,
264
+ maxLengthOrOptions?:
265
+ | number
266
+ | (LogTarget & { maxLength?: number; timeout?: number }),
201
267
  timeout?: number
202
268
  ): Promise<boolean> {
203
- const args: Record<string, unknown> = { line };
204
- if (maxLength !== undefined) {
205
- args.maxLength = maxLength;
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,11 +287,13 @@ export class Log {
215
287
  /** 追加带时间戳的条目 */
216
288
  async appendTimestampedEntry(
217
289
  message: string,
218
- timeout?: number
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
- { message },
296
+ args,
223
297
  timeout
224
298
  );
225
299
  return res.isSuccess();
@@ -231,28 +305,30 @@ export class Log {
231
305
  */
232
306
  async append(
233
307
  text: string,
234
- options?: {
308
+ options?: LogTarget & {
235
309
  /** @default true */
236
310
  timestamped?: boolean;
237
311
  maxLength?: number;
238
312
  timeout?: number;
239
313
  }
240
314
  ): Promise<boolean> {
241
- const { timestamped = true, maxLength, timeout } = options ?? {};
315
+ const { timestamped = true, maxLength, timeout, ...target } = options ?? {};
242
316
  if (timestamped) {
243
- return this.appendTimestampedEntry(text, timeout);
317
+ return this.appendTimestampedEntry(text, { ...target, timeout });
244
318
  }
245
- return this.appendLine(text, maxLength, timeout);
319
+ return this.appendLine(text, { ...target, maxLength, timeout });
246
320
  }
247
321
 
248
322
  /** 替换全部内容 */
249
323
  async replaceAll(
250
324
  content: string,
251
- timeout?: number
325
+ timeoutOrOptions?: number | LogCallOptions
252
326
  ): Promise<boolean> {
327
+ const { timeout, target } = resolveTimeout(timeoutOrOptions);
328
+ const args = buildLogArguments(target, { content }) ?? { content };
253
329
  const res = await this.asyncCall(
254
330
  LogCallMethod.replaceAll,
255
- { content },
331
+ args,
256
332
  timeout
257
333
  );
258
334
  return res.isSuccess();
@@ -265,7 +341,7 @@ export class Log {
265
341
  async subscribe(
266
342
  stream: LogStreamType,
267
343
  onUpdate: (payload: LogSubscribeUpdatePayload) => void,
268
- options?: { timeout?: number }
344
+ options?: LogCallOptions
269
345
  ): Promise<{
270
346
  subscriptionId: string;
271
347
  dispose: () => Promise<void>;
@@ -273,6 +349,7 @@ export class Log {
273
349
  const self = this;
274
350
  const callbackId = generateUUID();
275
351
  const timeoutSec = options?.timeout ?? 30;
352
+ const subscribeArgs = buildLogArguments(options, { stream }) ?? { stream };
276
353
 
277
354
  return new Promise((resolve, reject) => {
278
355
  let settled = false;
@@ -293,6 +370,7 @@ export class Log {
293
370
  subscriptionId?: string;
294
371
  stream?: string;
295
372
  text?: string;
373
+ logFilePath?: string;
296
374
  };
297
375
  };
298
376
  try {
@@ -338,6 +416,7 @@ export class Log {
338
416
  text: data.text ?? "",
339
417
  stream: data.stream ?? stream,
340
418
  subscriptionId: data.subscriptionId,
419
+ logFilePath: data.logFilePath,
341
420
  });
342
421
  }
343
422
  });
@@ -346,7 +425,7 @@ export class Log {
346
425
  this.getBridge().call(
347
426
  JSON.stringify({
348
427
  method: LogCallMethod.subscribe,
349
- arguments: { stream },
428
+ arguments: subscribeArgs,
350
429
  callbackId,
351
430
  })
352
431
  );
@@ -397,6 +476,12 @@ export class Log {
397
476
  if (args.uploadKey !== undefined) {
398
477
  payload.uploadKey = args.uploadKey;
399
478
  }
479
+ if (args.dirPath !== undefined) {
480
+ payload.dirPath = args.dirPath;
481
+ }
482
+ if (args.fileName !== undefined) {
483
+ payload.fileName = args.fileName;
484
+ }
400
485
  const uuid = generateUUID();
401
486
  const params = {
402
487
  method: LogCallMethod.uploadLogs,
@@ -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();
@@ -90,6 +90,27 @@ export class WindowFlags {
90
90
  return flags & ~flag;
91
91
  }
92
92
 
93
+ /** 输入框获取焦点时使用的悬浮窗 flag 组合(不含 FLAG_NOT_FOCUSABLE) */
94
+ static getOverlayInputFocusFlagList(): number[] {
95
+ return [
96
+ WindowFlags.FLAG_WATCH_OUTSIDE_TOUCH,
97
+ WindowFlags.FLAG_NOT_TOUCH_MODAL,
98
+ WindowFlags.FLAG_LAYOUT_NO_LIMITS,
99
+ WindowFlags.FLAG_LAYOUT_IN_SCREEN,
100
+ ];
101
+ }
102
+
103
+ /** 输入框失去焦点时使用的悬浮窗 flag 组合(含 FLAG_NOT_FOCUSABLE) */
104
+ static getOverlayInputBlurFlagList(): number[] {
105
+ return [
106
+ WindowFlags.FLAG_WATCH_OUTSIDE_TOUCH,
107
+ WindowFlags.FLAG_NOT_TOUCH_MODAL,
108
+ WindowFlags.FLAG_LAYOUT_NO_LIMITS,
109
+ WindowFlags.FLAG_NOT_FOCUSABLE,
110
+ WindowFlags.FLAG_LAYOUT_IN_SCREEN,
111
+ ];
112
+ }
113
+
93
114
  /**
94
115
  * 获取所有标志位的描述信息
95
116
  * @returns 标志位描述对象数组