assistsx-js 0.2.2 → 0.2.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assistsx-js",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "assistsx-js自动化开发SDK",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",
@@ -17,6 +17,11 @@
17
17
  "import": "./dist/index.js",
18
18
  "require": "./dist/index.cjs"
19
19
  },
20
+ "./step-flow": {
21
+ "types": "./dist/step-flow/index.d.ts",
22
+ "import": "./dist/step-flow/index.js",
23
+ "require": "./dist/step-flow/index.cjs"
24
+ },
20
25
  "./global": {
21
26
  "default": "./dist/index.global.js"
22
27
  }
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@ export * from "./utils";
8
8
  export * from "./bounds";
9
9
  export * from "./step";
10
10
  export * from "./step-state-store";
11
+ export { ensureAssistsXPinia } from "./pinia-ensure";
11
12
  export * from "./window-flags";
12
13
  export * from "./node-async";
13
14
  export * from "./assistsx-async";
package/src/log/log.ts CHANGED
@@ -225,6 +225,26 @@ export class Log {
225
225
  return res.isSuccess();
226
226
  }
227
227
 
228
+ /**
229
+ * 追加日志(appendTimestampedEntry / appendLine 简写)。
230
+ * 默认带时间戳;`timestamped: false` 时走 appendLine。
231
+ */
232
+ async append(
233
+ text: string,
234
+ options?: {
235
+ /** @default true */
236
+ timestamped?: boolean;
237
+ maxLength?: number;
238
+ timeout?: number;
239
+ }
240
+ ): Promise<boolean> {
241
+ const { timestamped = true, maxLength, timeout } = options ?? {};
242
+ if (timestamped) {
243
+ return this.appendTimestampedEntry(text, timeout);
244
+ }
245
+ return this.appendLine(text, maxLength, timeout);
246
+ }
247
+
228
248
  /** 替换全部内容 */
229
249
  async replaceAll(
230
250
  content: string,
@@ -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,5 @@
1
+ export * from "./types";
2
+ export * from "./outcome";
3
+ export * from "./payload";
4
+ export * from "./legacy-handoff";
5
+ export * from "./runner";
@@ -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
+ }
@@ -0,0 +1,128 @@
1
+ import { Step, type StepImpl } from "../step";
2
+ import { StepError } from "../step-error";
3
+ import type { StepFlowConfig, StepFlowData, StepFlowOutcome } from "./types";
4
+
5
+ function ensureFlowMeta(step: Step, config: StepFlowConfig): void {
6
+ const data = step.data as StepFlowData;
7
+ if (!data.__flow) {
8
+ data.__flow = { id: config.id, state: config.initial };
9
+ }
10
+ }
11
+
12
+ function setFlowState(step: Step, state: string, config: StepFlowConfig): void {
13
+ const data = step.data as StepFlowData;
14
+ if (!data.__flow) {
15
+ data.__flow = { id: config.id, state };
16
+ } else {
17
+ data.__flow.state = state;
18
+ }
19
+ }
20
+
21
+ function resolveOutcome(
22
+ config: StepFlowConfig,
23
+ currentState: string,
24
+ outcome: StepFlowOutcome
25
+ ): string | null | undefined | "legacy" {
26
+ if (outcome.type === "end") {
27
+ return undefined;
28
+ }
29
+ if (outcome.type === "repeat") {
30
+ return null;
31
+ }
32
+ if (outcome.type === "legacy") {
33
+ return "legacy";
34
+ }
35
+ const stateDef = config.states[currentState];
36
+ if (!stateDef) {
37
+ throw new StepError(`StepFlow: unknown state "${currentState}"`, {
38
+ flowId: config.id,
39
+ state: currentState,
40
+ });
41
+ }
42
+ const nextState = stateDef.on[outcome.name];
43
+ if (!nextState) {
44
+ throw new StepError(
45
+ `StepFlow: no transition for event "${outcome.name}" in state "${currentState}"`,
46
+ { flowId: config.id, state: currentState, event: outcome.name }
47
+ );
48
+ }
49
+ return nextState;
50
+ }
51
+
52
+ function createDispatcher(config: StepFlowConfig): StepImpl {
53
+ const dispatcher: StepImpl = async (step) => {
54
+ ensureFlowMeta(step, config);
55
+ const data = step.data as StepFlowData;
56
+ const currentState = data.__flow!.state;
57
+ const stateDef = config.states[currentState];
58
+ if (!stateDef) {
59
+ throw new StepError(`StepFlow: unknown state "${currentState}"`, {
60
+ flowId: config.id,
61
+ state: currentState,
62
+ });
63
+ }
64
+
65
+ const outcome = await stateDef.run(step);
66
+ const resolved = resolveOutcome(config, currentState, outcome);
67
+
68
+ if (resolved === undefined) {
69
+ return undefined;
70
+ }
71
+ if (resolved === null) {
72
+ return step.repeat();
73
+ }
74
+ if (resolved === "legacy") {
75
+ if (outcome.type !== "legacy") {
76
+ throw new StepError("StepFlow: internal legacy outcome mismatch");
77
+ }
78
+ return step.next(outcome.impl);
79
+ }
80
+
81
+ setFlowState(step, resolved, config);
82
+ return step.next(dispatcher);
83
+ };
84
+ return dispatcher;
85
+ }
86
+
87
+ /** 创建流程调度 StepImpl(需先填充 config.states,见 createLaunchState) */
88
+ export function createFlowDispatcher(config: StepFlowConfig): StepImpl {
89
+ return createDispatcher(config);
90
+ }
91
+
92
+ export function buildFlowInitialData(config: StepFlowConfig): StepFlowData {
93
+ return {
94
+ payload: config.data ?? {},
95
+ __flow: {
96
+ id: config.id,
97
+ state: config.initial,
98
+ },
99
+ };
100
+ }
101
+
102
+ export class StepFlow {
103
+ /**
104
+ * 按状态机配置运行流程,内部复用 Step.run 与现有步骤循环。
105
+ */
106
+ static async run(
107
+ config: StepFlowConfig,
108
+ options?: {
109
+ stepId?: string;
110
+ tag?: string;
111
+ delayMs?: number;
112
+ exceptionRetryCountMax?: number;
113
+ }
114
+ ): Promise<Step | undefined> {
115
+ if (!config.states[config.initial]) {
116
+ throw new StepError(
117
+ `StepFlow: initial state "${config.initial}" is not defined`,
118
+ { flowId: config.id }
119
+ );
120
+ }
121
+ const dispatcher = createDispatcher(config);
122
+ return Step.run(dispatcher, {
123
+ ...options,
124
+ tag: options?.tag ?? config.id,
125
+ data: buildFlowInitialData(config),
126
+ });
127
+ }
128
+ }
@@ -0,0 +1,41 @@
1
+ import type { Step, StepData } from "../step";
2
+
3
+ /** 流程步骤执行结果:事件 / 重复 / 结束 / 委托 legacy StepImpl */
4
+ export type StepFlowOutcome =
5
+ | { type: "event"; name: string }
6
+ | { type: "repeat" }
7
+ | { type: "end" }
8
+ | { type: "legacy"; impl: import("../step").StepImpl };
9
+
10
+ /** 流程步骤实现:只描述 UI 与分支事件,不直接决定下一状态 */
11
+ export type FlowStepImpl = (step: Step) => Promise<StepFlowOutcome>;
12
+
13
+ /** 单个流程状态定义 */
14
+ export interface FlowStateDef {
15
+ run: FlowStepImpl;
16
+ /** 事件名 -> 下一状态名 */
17
+ on: Record<string, string>;
18
+ }
19
+
20
+ /** StepFlow 运行配置 */
21
+ export interface StepFlowConfig {
22
+ id?: string;
23
+ initial: string;
24
+ states: Record<string, FlowStateDef>;
25
+ /** 写入 step.data.payload 的初始业务数据 */
26
+ data?: StepData;
27
+ }
28
+
29
+ /** 库写入 step.data.__flow 的结构 */
30
+ export interface StepFlowMeta {
31
+ id?: string;
32
+ state: string;
33
+ }
34
+
35
+ /** step.data 推荐结构(payload 用户读写,__flow 库维护) */
36
+ export interface StepFlowData {
37
+ payload?: StepData;
38
+ __flow?: StepFlowMeta;
39
+ finishMethod?: import("../step").StepImpl;
40
+ [key: string]: unknown;
41
+ }
package/src/step.ts CHANGED
@@ -6,11 +6,15 @@ import { AssistsX } from "./assistsx";
6
6
  import { Node } from "./node";
7
7
  import { CallMethod } from "./call-method";
8
8
  import { useStepStore } from "./step-state-store";
9
+ import { ensureAssistsXPinia } from "./pinia-ensure";
9
10
  import { generateUUID } from "./utils";
10
11
  import { StepError, StepStopError } from "./step-error";
11
12
  import { StepAsync } from "./step-async";
12
13
  import type { NodeLookupScope } from "./node-lookup-scope";
13
14
 
15
+ // 步骤数据类型,始终为普通对象
16
+ export type StepData = Record<string, any>;
17
+
14
18
  // 步骤结果类型,可以是Step实例或undefined
15
19
  export type StepResult = Step | undefined;
16
20
 
@@ -27,6 +31,34 @@ export class Step {
27
31
  static showLog: boolean = false;
28
32
  static exceptionRetryCountMaxDefault: number = 3;
29
33
 
34
+ /**
35
+ * 判断步骤数据是否有效(非空且为普通对象)
36
+ */
37
+ private static isValidStepData(data: unknown): data is StepData {
38
+ return (
39
+ data !== null &&
40
+ data !== undefined &&
41
+ typeof data === "object" &&
42
+ !Array.isArray(data)
43
+ );
44
+ }
45
+
46
+ /**
47
+ * 解析步骤数据:优先使用传入值,否则使用当前值,均无效时返回空对象
48
+ */
49
+ private static resolveStepData(
50
+ provided?: StepData,
51
+ fallback?: StepData
52
+ ): StepData {
53
+ if (provided !== undefined && Step.isValidStepData(provided)) {
54
+ return provided;
55
+ }
56
+ if (fallback !== undefined && Step.isValidStepData(fallback)) {
57
+ return fallback;
58
+ }
59
+ return {};
60
+ }
61
+
30
62
  /**
31
63
  * 当前执行步骤的ID
32
64
  */
@@ -60,13 +92,15 @@ export class Step {
60
92
  }: {
61
93
  stepId?: string | undefined;
62
94
  tag?: string | undefined;
63
- data?: any | undefined;
95
+ data?: StepData;
64
96
  delayMs?: number;
65
97
  exceptionRetryCountMax?: number;
66
98
  } = {}
67
99
  ): Promise<Step | undefined> {
68
100
  this.exception = undefined;
101
+ ensureAssistsXPinia();
69
102
  const stepStore = useStepStore();
103
+ const resolvedData = Step.resolveStepData(data);
70
104
  let implnName = impl.name;
71
105
  let currentStep: Step | undefined;
72
106
  let nextStep: Step | undefined;
@@ -84,12 +118,12 @@ export class Step {
84
118
  }
85
119
  }
86
120
 
87
- stepStore.startStep(this._stepId, tag, data);
121
+ stepStore.startStep(this._stepId, tag, resolvedData);
88
122
  currentStep = new Step({
89
123
  stepId: this._stepId,
90
124
  impl,
91
125
  tag,
92
- data,
126
+ data: resolvedData,
93
127
  delayMs,
94
128
  exceptionRetryCountMax,
95
129
  });
@@ -228,7 +262,7 @@ export class Step {
228
262
  const errorMsg = JSON.stringify({
229
263
  impl: implnName,
230
264
  tag: tag,
231
- data: data,
265
+ data: resolvedData,
232
266
  error: e?.message ?? String(e),
233
267
  });
234
268
  stepStore.setError(errorMsg);
@@ -402,7 +436,7 @@ export class Step {
402
436
  /**
403
437
  * 步骤数据
404
438
  */
405
- data: any | undefined;
439
+ data: StepData;
406
440
 
407
441
  /**
408
442
  * 步骤延迟时间(毫秒)
@@ -435,7 +469,7 @@ export class Step {
435
469
  stepId: string;
436
470
  impl: StepImpl | undefined;
437
471
  tag?: string | undefined;
438
- data?: any | undefined;
472
+ data?: StepData;
439
473
  delayMs?: number;
440
474
  repeatCountMax?: number;
441
475
  exceptionRetryCountMax?: number;
@@ -443,7 +477,7 @@ export class Step {
443
477
  }) {
444
478
  this.tag = tag;
445
479
  this.stepId = stepId;
446
- this.data = data ?? {};
480
+ this.data = Step.resolveStepData(data);
447
481
  this.impl = impl;
448
482
  this.delayMs = delayMs;
449
483
  this.repeatCountMax = repeatCountMax;
@@ -472,7 +506,7 @@ export class Step {
472
506
  exceptionRetryCountMax = Step.exceptionRetryCountMaxDefault,
473
507
  }: {
474
508
  tag?: string | undefined;
475
- data?: any | undefined;
509
+ data?: StepData;
476
510
  delayMs?: number;
477
511
  repeatCountMax?: number;
478
512
  exceptionRetryCountMax?: number;
@@ -483,7 +517,7 @@ export class Step {
483
517
  stepId: this.stepId,
484
518
  impl,
485
519
  tag,
486
- data: data ?? this.data ?? {},
520
+ data: Step.resolveStepData(data, this.data),
487
521
  delayMs,
488
522
  repeatCountMax,
489
523
  exceptionRetryCountMax,
@@ -498,7 +532,7 @@ export class Step {
498
532
  exceptionRetryCountMax = Step.exceptionRetryCountMaxDefault,
499
533
  }: {
500
534
  tag?: string | undefined;
501
- data?: any | undefined;
535
+ data?: StepData;
502
536
  delayMs?: number;
503
537
  repeatCountMax?: number;
504
538
  exceptionRetryCountMax?: number;
@@ -509,7 +543,7 @@ export class Step {
509
543
  stepId: this.stepId,
510
544
  impl: undefined,
511
545
  tag,
512
- data: data ?? this.data ?? {},
546
+ data: Step.resolveStepData(data, this.data),
513
547
  delayMs,
514
548
  repeatCountMax,
515
549
  exceptionRetryCountMax,
@@ -528,14 +562,14 @@ export class Step {
528
562
  repeat({
529
563
  stepId = this.stepId,
530
564
  tag = this.tag,
531
- data = this.data ?? {},
565
+ data,
532
566
  delayMs = this.delayMs,
533
567
  repeatCountMax = this.repeatCountMax,
534
568
  exceptionRetryCountMax = this.exceptionRetryCountMax,
535
569
  }: {
536
570
  stepId?: string;
537
571
  tag?: string | undefined;
538
- data?: any | undefined;
572
+ data?: StepData;
539
573
  delayMs?: number;
540
574
  repeatCountMax?: number;
541
575
  exceptionRetryCountMax?: number;
@@ -544,7 +578,7 @@ export class Step {
544
578
  this.repeatCount++;
545
579
  this.stepId = stepId;
546
580
  this.tag = tag;
547
- this.data = data;
581
+ this.data = Step.resolveStepData(data, this.data);
548
582
  this.delayMs = delayMs;
549
583
  this.repeatCountMax = repeatCountMax;
550
584
  this.exceptionRetryCountMax = exceptionRetryCountMax;