Browse Source

feat: flow.executor & display

fmode 2 weeks ago
parent
commit
718ab5b239

+ 6 - 0
src/app/app.routes.ts

@@ -38,11 +38,17 @@ export const routes: Routes = [
       redirectTo: '/chat/session/chat/:chatId',
       pathMatch: 'full'
     },
+    // 流程任务模块
     {
         path: "flow/editor/new",
         loadComponent: () => import('../modules/flow/comp-flow-editor/comp-flow-editor.component').then(m => m.CompFlowEditorComponent),
         runGuardsAndResolvers: "always",
     },
+    {
+      path: "flow/test",
+      loadComponent: () => import('../modules/flow/page-flow-test/page-flow-test.component').then(m => m.PageFlowTestComponent),
+      runGuardsAndResolvers: "always",
+  },
     // 测试任务模块
     {
       path: "task/test",

+ 143 - 0
src/modules/flow/lib/flow-display/flow-display.component.ts

@@ -0,0 +1,143 @@
+// flow-display.component.ts
+import { Component, Input } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { IonicModule } from '@ionic/angular';
+import { ModalController } from '@ionic/angular';
+import { FlowExecutor } from '../flow.executor';
+
+@Component({
+  standalone: true,
+  imports: [CommonModule, IonicModule],
+  selector: 'app-flow-display',
+  template: `
+    <ion-header>
+      <ion-toolbar>
+        <ion-title>{{executor?.workflow?.title}} <ion-button (click)="dismiss()">返回</ion-button></ion-title>
+        <ion-progress-bar [value]="executor?.currentProgress"></ion-progress-bar>
+      </ion-toolbar>
+    </ion-header>
+
+    <ion-content>
+      <ion-list>
+        <ion-item *ngFor="let task of executor?.workflow?.taskList; let i = index">
+          <ion-label (click)="toggleExpand(i)">
+            <h2>{{task.title}}</h2>
+            <p>{{task.getData('description')}}</p>
+
+            <div *ngIf="expandedTasks[i]" class="task-details">
+              <ion-text color="medium">
+                <p>状态: {{task.status}}</p>
+                <p>进度: {{task.progress * 100 | number:'1.0-0'}}%</p>
+                <p>开始时间: {{taskTimers[i]?.start | date:'mediumTime'}}</p>
+                <p>持续时间: {{calculateDuration(i) | number:'1.1-1'}}秒</p>
+                <pre *ngIf="task.output">{{task.output | json}}</pre>
+              </ion-text>
+            </div>
+          </ion-label>
+
+          <ion-badge slot="end" [color]="getStatusColor(task.status)">
+            {{task.status}} ({{task.progress * 100 | number:'1.0-0'}}%)
+          </ion-badge>
+        </ion-item>
+      </ion-list>
+
+      <ion-footer *ngIf="executor?.status === 'failed'">
+        <ion-toolbar color="danger">
+          <ion-buttons slot="start">
+            <ion-button (click)="retryFailedTask()">
+              <ion-icon slot="start" name="refresh"></ion-icon>
+              重试失败任务
+            </ion-button>
+          </ion-buttons>
+          <ion-title>工作流执行失败</ion-title>
+          <ion-buttons slot="end">
+            <ion-button (click)="retryAll()">
+              <ion-icon slot="start" name="reload"></ion-icon>
+              全部重试
+            </ion-button>
+          </ion-buttons>
+        </ion-toolbar>
+      </ion-footer>
+    </ion-content>
+  `,
+  styles: [`
+    ion-progress-bar {
+      height: 4px;
+    }
+    ion-item {
+      --padding-start: 16px;
+      --inner-padding-end: 16px;
+    }
+    ion-badge {
+      margin-inline-start: 12px;
+    }
+    .task-details {
+      margin-top: 8px;
+      padding: 8px;
+      background: var(--ion-color-light);
+      border-radius: 4px;
+    }
+    pre {
+      white-space: pre-wrap;
+      font-size: 12px;
+    }
+  `]
+})
+export class FlowDisplayComponent {
+  @Input() executor?: FlowExecutor;
+  expandedTasks: boolean[] = [];
+  taskTimers: {start?: Date, end?: Date}[] = [];
+
+  dismiss(){
+    this.modalCtrl.dismiss();
+  }
+  constructor(
+    private modalCtrl:ModalController
+  ) {
+    // 监听任务状态变化
+    this.executor?.taskStart$.subscribe((task) => {
+      const index = this.executor!.workflow!.taskList.indexOf(task);
+      this.taskTimers[index] = { start: new Date() };
+    });
+
+    this.executor?.taskSuccess$.subscribe((task) => {
+      const index = this.executor!.workflow!.taskList.indexOf(task);
+      this.taskTimers[index].end = new Date();
+    });
+  }
+
+  // 切换任务详情展开状态
+  toggleExpand(index: number) {
+    this.expandedTasks[index] = !this.expandedTasks[index];
+  }
+
+  // 计算任务持续时间
+  calculateDuration(index: number): number {
+    const timer = this.taskTimers[index];
+    if (!timer?.start) return 0;
+    const end = timer.end || new Date();
+    return (end.getTime() - timer.start.getTime()) / 1000;
+  }
+
+  // 重试失败的任务
+  async retryFailedTask() {
+    const failedIndex = this.executor?.failedTaskIndex;
+    if (failedIndex !== null && failedIndex !== undefined) {
+      await this.executor?.retryFromTask(failedIndex);
+    }
+  }
+
+  // 重试全部任务
+  async retryAll() {
+    await this.executor?.retryFromTask(0);
+  }
+
+  getStatusColor(status: string): string {
+    switch (status) {
+      case 'success': return 'success';
+      case 'failed': return 'danger';
+      case 'running': return 'primary';
+      default: return 'medium';
+    }
+  }
+}

+ 95 - 0
src/modules/flow/lib/flow.executor.ts

@@ -0,0 +1,95 @@
+// flow-executor.ts
+import { FlowWorkflow, FlowStatus } from './flow.workflow';
+import { FlowTask } from './flow.task';
+import { Subject } from 'rxjs';
+
+export class FlowExecutor {
+  public workflow?: FlowWorkflow;
+  private currentTaskIndex = 0;
+  private _status: FlowStatus = 'idle';
+  private retryCount = 0;
+
+  // 事件系统
+  public taskStart$ = new Subject<FlowTask>();
+  public taskSuccess$ = new Subject<FlowTask>();
+  public taskFailure$ = new Subject<{ task: FlowTask; error: Error }>();
+  public statusChange$ = new Subject<FlowStatus>();
+  public progressUpdate$ = new Subject<number>();
+
+  constructor(
+    public maxRetries = 3,
+    public autoRetry = false
+  ) {}
+
+  setWorkflow(workflow: FlowWorkflow) {
+    this.workflow = workflow;
+    this.reset();
+  }
+
+  async start() {
+    if (!this.workflow) throw new Error('工作流未设置');
+    this._status = 'running';
+    this.statusChange$.next(this._status);
+    await this.executeNextTask();
+  }
+
+  // 方法:从指定任务索引重新执行
+  async retryFromTask(taskIndex: number) {
+    if (!this.workflow || taskIndex < 0 || taskIndex >= this.workflow.taskList.length) {
+      throw new Error('无效的任务索引');
+    }
+
+    this.currentTaskIndex = taskIndex;
+    this.retryCount = 0;
+    this._status = 'running';
+    this.statusChange$.next(this._status);
+    await this.executeNextTask();
+  }
+  // 获取当前失败的任务索引
+  get failedTaskIndex(): number | null {
+    return this._status === 'failed' ? this.currentTaskIndex : null;
+  }
+
+  private async executeNextTask() {
+    if (!this.workflow || this.currentTaskIndex >= this.workflow.taskList.length) {
+      this._status = 'success';
+      this.statusChange$.next(this._status);
+      return;
+    }
+
+    const task = this.workflow.taskList[this.currentTaskIndex];
+    try {
+      this.taskStart$.next(task);
+      await task.execute();
+      this.taskSuccess$.next(task);
+      this.currentTaskIndex++;
+      this.retryCount = 0;
+      await this.executeNextTask();
+    } catch (error) {
+      this.taskFailure$.next({ task, error: error as Error });
+
+      if (this.autoRetry && this.retryCount < this.maxRetries) {
+        this.retryCount++;
+        await this.executeNextTask();
+      } else {
+        this._status = 'failed';
+        this.statusChange$.next(this._status);
+      }
+    }
+  }
+
+  get status() {
+    return this._status;
+  }
+
+  get currentProgress() {
+    if (!this.workflow) return 0;
+    return this.currentTaskIndex / this.workflow.taskList.length;
+  }
+
+  reset() {
+    this.currentTaskIndex = 0;
+    this.retryCount = 0;
+    this._status = 'idle';
+  }
+}

+ 180 - 0
src/modules/flow/lib/flow.task.ts

@@ -0,0 +1,180 @@
+type FieldType = 'string' | 'number' | 'boolean' | 'object' | 'array' | 'any';
+
+interface FieldSchema {
+  name: string;
+  type: FieldType;
+  description?: string;
+  required?: boolean;
+}
+
+interface FlowTaskOptions {
+  title: string;
+  input?: FieldSchema[];
+  output?: FieldSchema[];
+  initialData?: Record<string, any>;
+}
+
+export class FlowTask {
+  // 核心属性
+  readonly title: string;
+  protected data: Record<string, any> = {};
+  private _status: 'idle' | 'running' | 'success' | 'failed' = 'idle';
+  private _progress: number = 0;
+
+  // 校验规则
+  private readonly inputSchema: FieldSchema[];
+  private readonly outputSchema: FieldSchema[];
+
+  // 添加执行时间记录
+  private _startTime?: Date;
+  private _endTime?: Date;
+  // 添加执行时间信息
+  get executionTime(): number {
+    if (!this._startTime) return 0;
+    const end = this._endTime || new Date();
+    return (end.getTime() - this._startTime.getTime()) / 1000;
+  }
+
+  constructor(options: FlowTaskOptions) {
+    this.title = options.title;
+    this.data = options.initialData || {};
+    this.inputSchema = options.input || [];
+    this.outputSchema = options.output || [];
+  }
+
+  /************************************
+   *          核心执行流程              *
+   ************************************/
+
+  async execute(): Promise<void> {
+
+    try {
+      if (this._status !== 'idle') return;
+      this._startTime = new Date();
+
+      this._status = 'running';
+      this.validateInput();  // 输入校验
+
+      this.beforeExecute();
+      await this.handle();   // 执行用户自定义逻辑
+      this.afterExecute();
+
+      this.validateOutput(); // 输出校验
+      this._status = 'success';
+      this.onSuccess();
+    } catch (error) {
+      this._status = 'failed';
+      this.onFailure(error as Error);
+      throw error; // 重新抛出错误确保执行器能捕获
+    }
+    this._endTime = new Date();
+  }
+
+  /************************************
+   *          用户可覆盖方法            *
+   ************************************/
+
+  // 主处理函数(用户需要覆盖的核心方法)
+  protected async handle(): Promise<void> {
+    // 默认空实现,抛出错误提示需要实现
+    throw new Error('必须实现 handle() 方法');
+  }
+
+  // 生命周期钩子(可选覆盖)
+  protected beforeExecute(): void {}
+  protected afterExecute(): void {}
+  protected onProgress(progress: number): void {}
+  protected onSuccess(): void {}
+  protected onFailure(error: Error): void {}
+
+  /************************************
+   *          数据校验系统              *
+   ************************************/
+
+  private validateInput(): void {
+    const errors: string[] = [];
+
+    this.inputSchema.forEach(field => {
+      const value = this.data[field.name];
+
+      // 检查必填字段
+      if (field.required && value === undefined) {
+        errors.push(`缺少必要字段:${field.name}`);
+        return;
+      }
+
+      // 类型校验
+      if (value !== undefined && !this.checkType(value, field.type)) {
+        errors.push(`${field.name} 类型错误,期望 ${field.type},实际 ${this.getType(value)}`);
+      }
+    });
+
+    if (errors.length > 0) {
+      throw new Error(`输入校验失败:\n${errors.join('\n')}`);
+    }
+  }
+
+  private validateOutput(): void {
+    const missingFields = this.outputSchema
+      .filter(f => f.required)
+      .filter(f => !(f.name in this.data))
+      .map(f => f.name);
+
+    if (missingFields.length > 0) {
+      throw new Error(`输出校验失败,缺少字段:${missingFields.join(', ')}`);
+    }
+  }
+
+  /************************************
+   *          工具方法                *
+   ************************************/
+
+  // 类型检查
+  private checkType(value: any, expected: FieldType): boolean {
+    const actualType = this.getType(value);
+    return expected === 'any' || actualType === expected;
+  }
+
+  private getType(value: any): FieldType {
+    if (Array.isArray(value)) return 'array';
+    if (value === null) return 'object';
+    return typeof value as FieldType;
+  }
+
+  /************************************
+   *          公共接口                *
+   ************************************/
+
+  // 更新任务数据
+  updateData(key: string, value: any): this {
+    this.data[key] = value;
+    return this;
+  }
+
+  // 获取任务数据
+  getData<T = any>(key: string): T {
+    return this.data[key];
+  }
+
+  // 进度更新
+  setProgress(value: number): void {
+    if (value < 0 || value > 1) return;
+    this._progress = value;
+    this.onProgress(value);
+  }
+
+  // 状态访问器
+  get progress(): number {
+    return this._progress;
+  }
+
+  get status() {
+    return this._status;
+  }
+
+  get output() {
+    return Object.fromEntries(
+      this.outputSchema.map(f => [f.name, this.data[f.name]])
+    );
+  }
+}

+ 9 - 0
src/modules/flow/lib/flow.workflow.ts

@@ -0,0 +1,9 @@
+import { FlowTask } from './flow.task';
+
+export interface FlowWorkflow {
+  title: string;
+  desc?: string;
+  taskList: FlowTask[];
+}
+
+export type FlowStatus = 'idle' | 'running' | 'paused' | 'success' | 'failed';

+ 114 - 0
src/modules/flow/page-flow-test/consult-tasks/consult-tasks.ts

@@ -0,0 +1,114 @@
+
+import { FmodeChatCompletion } from 'fmode-ng';
+import { FlowTask } from '../../lib/flow.task';
+
+
+// triage.task.ts
+import { ModalController } from '@ionic/angular/standalone';
+import { Subject, takeUntil } from 'rxjs';
+import { SymptomInputModalComponent } from './symptom-input/symptom-input.modal';
+import { inject } from '@angular/core';
+
+export class TriageTask extends FlowTask {
+  private modalCtrl:ModalController;
+  private destroy$ = new Subject<void>();
+
+  constructor(options:{modalCtrl:ModalController}) {
+    super({
+      title: '智能分诊',
+      output: [
+        { name: 'department', type: 'string', required: true },
+        { name: 'symptomDescription', type: 'string' },
+        { name: 'triageReason', type: 'string' }
+      ]
+    });
+
+    this.modalCtrl = options?.modalCtrl
+  }
+
+  override async handle() {
+    // 1. 通过模态框获取用户输入
+    const symptom = await this.getSymptomDescription();
+    if (!symptom) throw new Error('症状描述不能为空');
+
+    this.updateData('symptomDescription', symptom);
+
+    // 2. 调用大模型进行分诊
+    await this.generateTriageResult(symptom);
+  }
+
+  onDestroy() {
+    this.destroy$.next();
+    this.destroy$.complete();
+  }
+
+  private async getSymptomDescription(): Promise<string> {
+    const modal = await this.modalCtrl.create({
+      component: SymptomInputModalComponent,
+      backdropDismiss: false
+    });
+
+    await modal.present();
+    const { data } = await modal.onDidDismiss();
+    return data || '';
+  }
+
+  private generateTriageResult(symptom: string): Promise<void> {
+    return new Promise((resolve, reject) => {
+      const prompt = [
+        {
+          role: "user",
+          content: `您是一名专业的分诊护士,根据患者描述推荐最合适的科室。严格按照以下JSON格式返回:
+          {
+            "department": "科室名称",
+            "reason": "分诊理由(50字以内)"
+          }`+`患者主诉:${symptom}`
+        }
+      ];
+
+      let fullResponse = '';
+      const completion = new FmodeChatCompletion(prompt);
+
+      completion.sendCompletion()
+        // .pipe(takeUntil(this.destroy$))
+        .subscribe((message) => {
+            // 处理流式响应
+            if (message.content && typeof message.content === 'string') {
+              fullResponse += message.content;
+              this.setProgress(0.5 + (fullResponse.length / 200) * 0.5); // 模拟进度
+            }
+            console.log(message)
+            if(message.complete == true){
+              try {
+                const result = this.parseTriageResponse(fullResponse);
+                this.updateData('department', result.department)
+                   .updateData('triageReason', result.reason);
+                this.setProgress(1);
+                resolve();
+              } catch (e) {
+                reject(e);
+              }
+            }
+          });
+    });
+  }
+
+  private parseTriageResponse(response: string): { department: string; reason: string } {
+    try {
+      // 尝试提取JSON部分(处理可能的非JSON前缀)
+      const jsonStart = response.indexOf('{');
+      const jsonEnd = response.lastIndexOf('}') + 1;
+      const jsonStr = response.slice(jsonStart, jsonEnd);
+
+      const result = JSON.parse(jsonStr);
+      if (!result.department) throw new Error('缺少科室信息');
+
+      return {
+        department: result.department.trim(),
+        reason: result.reason || '根据症状描述判断'
+      };
+    } catch (e) {
+      throw new Error(`分诊结果解析失败: ${response}`);
+    }
+  }
+}

+ 59 - 0
src/modules/flow/page-flow-test/consult-tasks/symptom-input/symptom-input.modal.ts

@@ -0,0 +1,59 @@
+// symptom-input.modal.ts
+import { Component } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { IonicModule, ModalController } from '@ionic/angular';
+
+@Component({
+  standalone: true,
+  imports: [IonicModule, FormsModule],
+  template: `
+    <ion-header>
+      <ion-toolbar>
+        <ion-title>症状描述</ion-title>
+        <ion-buttons slot="end">
+          <ion-button (click)="dismiss()">关闭</ion-button>
+        </ion-buttons>
+      </ion-toolbar>
+    </ion-header>
+
+    <ion-content>
+      <ion-textarea
+        [(ngModel)]="symptomDescription"
+        placeholder="请详细描述您的症状(如:头痛3天,伴有恶心呕吐)"
+        autoGrow
+        rows="5"
+      ></ion-textarea>
+    </ion-content>
+
+    <ion-footer>
+      <ion-toolbar>
+        <ion-button
+          expand="block"
+          (click)="submit()"
+          [disabled]="!symptomDescription.trim()">
+          提交
+        </ion-button>
+      </ion-toolbar>
+    </ion-footer>
+  `,
+  styles: [`
+    ion-textarea {
+      background: var(--ion-color-light);
+      border-radius: 8px;
+      margin: 16px;
+      padding: 8px;
+    }
+  `]
+})
+export class SymptomInputModalComponent {
+  symptomDescription = '';
+  constructor(private modalCtrl:ModalController){}
+
+  submit() {
+    this.modalCtrl.dismiss(this.symptomDescription.trim());
+  }
+
+  dismiss() {
+    this.modalCtrl.dismiss(null);
+  }
+}

+ 37 - 0
src/modules/flow/page-flow-test/consult-tasks/triage-result/triage-result.component.ts

@@ -0,0 +1,37 @@
+import { Component, Input } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { IonicModule } from '@ionic/angular';
+
+@Component({
+  standalone: true,
+  imports: [CommonModule, IonicModule],
+  template: `
+    <ion-card>
+      <ion-card-header>
+        <ion-card-title>分诊结果</ion-card-title>
+      </ion-card-header>
+
+      <ion-card-content>
+        <ion-item>
+          <ion-label>症状描述</ion-label>
+          <ion-text>{{ symptom }}</ion-text>
+        </ion-item>
+
+        <ion-item>
+          <ion-label>推荐科室</ion-label>
+          <ion-badge color="primary">{{ department }}</ion-badge>
+        </ion-item>
+
+        <ion-item>
+          <ion-label>分诊依据</ion-label>
+          <ion-text>{{ reason }}</ion-text>
+        </ion-item>
+      </ion-card-content>
+    </ion-card>
+  `
+})
+export class TriageResultComponent {
+  @Input() symptom = '';
+  @Input() department = '';
+  @Input() reason = '';
+}

+ 127 - 0
src/modules/flow/page-flow-test/mock-tasks/mock-tasks.ts

@@ -0,0 +1,127 @@
+// mock-tasks.ts
+import { FlowTask } from '../../lib/flow.task';
+
+export class MockSuccessTask extends FlowTask {
+  constructor() {
+    super({
+      title: '初始化系统',
+      input: [
+        { name: 'config', type: 'object', required: true, description: '系统配置对象' }
+      ],
+      output: [
+        { name: 'result', type: 'string', description: '初始化结果' },
+        { name: 'timestamp', type: 'number', description: '完成时间戳' }
+      ],
+      initialData: {
+        description: '系统初始化配置',
+        config: { env: 'production' } // 提供默认配置
+      }
+    });
+  }
+
+  override async handle() {
+    // 模拟初始化过程
+    await new Promise(resolve => setTimeout(resolve, 1000));
+
+    // 验证输入
+    if (!this.getData('config')) {
+      throw new Error('缺少必要配置');
+    }
+
+    // 设置输出
+    this.updateData('result', '初始化成功')
+       .updateData('timestamp', Date.now());
+    this.setProgress(1);
+  }
+}
+
+export class MockRetryTask extends FlowTask {
+  private static readonly MAX_ATTEMPTS = 3;
+
+  constructor() {
+    super({
+      title: '数据同步',
+      output: [
+        { name: 'syncResult', type: 'object', description: '同步结果数据' }
+      ],
+      initialData: {
+        description: '尝试连接远程服务器',
+        serverUrl: 'https://api.example.com'
+      }
+    });
+  }
+
+  override async handle() {
+    // 模拟连接尝试
+    await new Promise(resolve => setTimeout(resolve, 300));
+
+    // 模拟75%成功率
+    const shouldFail = Math.random() < 0.75;
+    if (shouldFail && this.attempts < MockRetryTask.MAX_ATTEMPTS - 1) {
+      this.attempts++;
+      throw new Error(`第 ${this.attempts} 次连接失败`);
+    }
+
+    // 成功情况
+    this.updateData('syncResult', {
+      connected: true,
+      server: this.getData('serverUrl'),
+      dataReceived: Array(5).fill(0).map((_,i) => `item_${i+1}`)
+    });
+    this.setProgress(1);
+  }
+
+  private get attempts(): number {
+    return this.getData('_attempts') || 0;
+  }
+
+  private set attempts(value: number) {
+    this.updateData('_attempts', value);
+  }
+}
+
+export class MockProgressTask extends FlowTask {
+  constructor() {
+    super({
+      title: '数据处理',
+      output: [
+        { name: 'processedItems', type: 'array' },
+        { name: 'stats', type: 'object' }
+      ],
+      initialData: {
+        description: '批量处理数据记录',
+        batchSize: 100
+      }
+    });
+  }
+
+  override async handle() {
+    const batchSize = this.getData('batchSize') || 100;
+    const results = [];
+
+    for (let i = 1; i <= batchSize; i++) {
+      await new Promise(resolve => setTimeout(resolve, 20));
+
+      // 模拟数据处理
+      results.push({
+        id: i,
+        processedAt: Date.now(),
+        value: Math.random().toString(36).substring(7)
+      });
+
+      // 每10%更新一次进度
+      if (i % (batchSize/10) === 0) {
+        this.setProgress(i / batchSize);
+      }
+    }
+
+    // 最终输出
+    this.updateData('processedItems', results)
+       .updateData('stats', {
+         total: results.length,
+         avgTime: 20,
+         firstId: results[0]?.id,
+         lastId: results[results.length-1]?.id
+       });
+  }
+}

+ 2 - 0
src/modules/flow/page-flow-test/page-flow-test.component.html

@@ -0,0 +1,2 @@
+<ion-button (click)="startTriage()">开始分诊</ion-button>
+<ion-button (click)="startWorkflow()">启动工作流</ion-button>

+ 0 - 0
src/modules/flow/page-flow-test/page-flow-test.component.scss


+ 22 - 0
src/modules/flow/page-flow-test/page-flow-test.component.spec.ts

@@ -0,0 +1,22 @@
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+
+import { PageFlowTestComponent } from './page-flow-test.component';
+
+describe('PageFlowTestComponent', () => {
+  let component: PageFlowTestComponent;
+  let fixture: ComponentFixture<PageFlowTestComponent>;
+
+  beforeEach(waitForAsync(() => {
+    TestBed.configureTestingModule({
+      imports: [PageFlowTestComponent],
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(PageFlowTestComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  }));
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 80 - 0
src/modules/flow/page-flow-test/page-flow-test.component.ts

@@ -0,0 +1,80 @@
+import { Component } from '@angular/core';
+import { ModalController } from '@ionic/angular/standalone';
+import { IonButton } from '@ionic/angular/standalone';
+import { FlowExecutor } from '../lib/flow.executor';
+import { MockProgressTask, MockRetryTask, MockSuccessTask } from './mock-tasks/mock-tasks';
+import { FlowDisplayComponent } from '../lib/flow-display/flow-display.component';
+import { TriageResultComponent } from './consult-tasks/triage-result/triage-result.component';
+import { TriageTask } from './consult-tasks/consult-tasks';
+import { SymptomInputModalComponent } from './consult-tasks/symptom-input/symptom-input.modal';
+
+@Component({
+  selector: 'app-page-flow-test',
+  templateUrl: './page-flow-test.component.html',
+  styleUrls: ['./page-flow-test.component.scss'],
+  imports:[IonButton,FlowDisplayComponent,
+    TriageResultComponent,
+    SymptomInputModalComponent
+  ],
+  standalone: true,
+})
+export class PageFlowTestComponent {
+
+  private executor = new FlowExecutor(3, true);
+
+  constructor(
+    private modalCtrl: ModalController
+  ) {
+    // 创建工作流
+    const workflow = {
+      title: '示例工作流',
+      desc: '演示工作流执行过程',
+      taskList: [
+        new MockSuccessTask(),
+        new MockRetryTask(),
+        new MockProgressTask()
+      ]
+    };
+
+    this.executor.setWorkflow(workflow);
+  }
+
+  async startWorkflow() {
+    const modal = await this.modalCtrl.create({
+      component: FlowDisplayComponent,
+      componentProps: { executor: this.executor }
+    });
+
+    await modal.present();
+    await this.executor.start();
+  }
+
+  // 问诊任务
+  async startTriage() {
+    this.executor.setWorkflow({
+      title: '医疗分诊流程',
+      taskList: [new TriageTask({modalCtrl:this.modalCtrl})]
+    });
+
+    this.executor.taskSuccess$.subscribe(task => {
+      if (task instanceof TriageTask) {
+        this.showTriageResult(task);
+      }
+    });
+
+    await this.executor.start();
+  }
+
+  private async showTriageResult(task: TriageTask) {
+    const modal = await this.modalCtrl.create({
+      component: TriageResultComponent,
+      componentProps: {
+        symptom: task.getData('symptomDescription'),
+        department: task.getData('department'),
+        reason: task.getData('triageReason')
+      }
+    });
+
+    await modal.present();
+  }
+}