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/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,24 +287,48 @@ 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();
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
- timeout?: number
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
- { content },
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?: { timeout?: number }
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: { stream },
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
+ }