首页
/ Twenty 同步实体(Syncable Entity)Runner 与 Actions:工作区迁移动作的两阶段转译与执行模式

Twenty 同步实体(Syncable Entity)Runner 与 Actions:工作区迁移动作的两阶段转译与执行模式

2026-09-06 09:59:30作者:幸俭卉

本文基于 Twenty 仓库中的 .cursor/skills/syncable-entity-runner-and-actions/SKILL.md 文档展开,讲解 Syncable Entity 工作流第 4/6 步——Runner & Actions 层:如何为 syncable entity 实现 Create / Update / Delete 三个动作处理器(Action Handler),完成从通用实体(Universal Entity)到扁平实体(Flat Entity)的转译(Transpilation),并在 metadata 数据库中落库。读完本文,你能掌握 Twenty 工作区迁移(Workspace Migration)动作处理器的两阶段设计模式、关键转译工具函数的源码级实现,以及 Handler 注册与调度机制,具备在 Twenty 中为新实体补齐 Runner 层实现的完整能力。

一、定位:Runner & Actions 在 Syncable Entity 六步工作流中的位置

.cursor/skills/ 目录下组织了一组 Syncable Entity 的技能文档,覆盖从类型定义到集成测试的完整链路:

  • syncable-entity-types-and-constants(Step 1:类型与常量)
  • syncable-entity-cache-and-transform(Step 2:缓存与转换)
  • syncable-entity-builder-and-validation(Step 3:Builder 与校验)
  • syncable-entity-runner-and-actions(Step 4:Runner 与 Actions,即本文主题)
  • syncable-entity-integration(Step 5:集成)
  • syncable-entity-testing(Step 6:测试)

按技能文档 SKILL.md 的定义,本步骤的 Purpose 是:

Execute migration actions against the database with proper transpilation from universal to flat entities.

即:针对数据库执行迁移动作,并在此过程中完成从通用实体到扁平实体的正确转译。该步骤的前置条件是已完成 Step 1–3(Types、Cache、Builder),是集成(Step 5)之前的必经环节。

本步骤需要创建四类产物:

  1. Create 动作处理器
  2. Update 动作处理器
  3. Delete 动作处理器
  4. Universal-to-Flat 转换工具函数

整个 Runner & Actions 层遵循一个双阶段(Two-Phase)核心模式

  1. Transpilation(转译阶段):Universal action → Flat action;
  2. Execution(执行阶段):Flat action → Database operation。

通用实体中,外键以「通用标识符(universal identifier)」表达,便于跨工作区/跨应用的数据同步与 diff 比较;而真实数据库表中的外键是具体的 UUID。两个阶段分别解决「语义表示」到「物理表示」的转换,以及最终的落库执行问题。

二、Create 动作处理器:批量插入的基准实现

2.1 文档给出的标准实现

文档建议的落盘路径为:

src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/services/create-my-entity-action-handler.service.ts

my-entity 为占位命名,替换为你实际实体的 metadataName。)

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

import { WorkspaceCreateActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-create-action-handler.service';
import { MyEntityEntity } from 'src/engine/metadata-modules/my-entity/entities/my-entity.entity';
import { fromUniversalFlatMyEntityToFlatMyEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/utils/from-universal-flat-my-entity-to-flat-my-entity.util';
import {
  type UniversalCreateMyEntityAction,
  type FlatCreateMyEntityAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/my-entity/types/workspace-migration-my-entity-action.type';

@Injectable()
export class CreateMyEntityActionHandlerService extends WorkspaceCreateActionHandlerService<
  'myEntity',
  UniversalCreateMyEntityAction,
  FlatCreateMyEntityAction
> {
  constructor(
    @InjectRepository(MyEntityEntity, 'metadata')
    private readonly myEntityRepository: Repository<MyEntityEntity>,
  ) {
    super();
  }

  // Phase 1: Transpile universal action to flat action
  protected transpileUniversalActionToFlatAction(
    universalAction: UniversalCreateMyEntityAction,
    flatEntityMaps: AllFlatEntityMapsByMetadataName,
  ): FlatCreateMyEntityAction {
    return {
      type: 'create',
      metadataName: 'myEntity',
      flatEntity: fromUniversalFlatMyEntityToFlatMyEntity(
        universalAction.universalFlatEntity,
        flatEntityMaps,
      ),
    };
  }

  // Phase 2: Execute flat action against database
  protected async executeForMetadata(
    flatActions: FlatCreateMyEntityAction[],
  ): Promise<void> {
    const flatEntities = flatActions.map((action) => action.flatEntity);

    await this.insertFlatEntitiesInRepository({
      repository: this.myEntityRepository,
      flatEntities,
    });
  }

  protected async executeForWorkspaceSchema(): Promise<void> {
    // No workspace schema changes needed for metadata-only entity
    return;
  }
}

Create 处理器依赖的关键方法:

  • transpileUniversalActionToFlatAction:完成 universal → flat 的转译;
  • insertFlatEntitiesInRepository:基类提供的批量插入辅助方法;
  • executeForMetadata:执行 metadata 数据库操作;
  • executeForWorkspaceSchema:执行工作区 Schema 变更(纯 metadata 实体无需变更,直接 return 即可)。

2.2 源码佐证:基类中的 insertFlatEntitiesInRepository

在当前仓库的 workspace-migration-runner-action-handler-service.interface.ts 中,基类 BaseWorkspaceMigrationRunnerActionHandlerService 实现了文档所提到的插入辅助方法:

protected async insertFlatEntitiesInRepository({
  flatEntities,
  queryRunner,
}: {
  queryRunner: QueryRunner;
  flatEntities: MetadataFlatEntity<TMetadataName>[];
}) {
  const metadataEntity = ALL_METADATA_ENTITY_BY_METADATA_NAME[this.metadataName];
  const repository = queryRunner.manager.getRepository(metadataEntity);
  const scalarFlatEntities = flatEntities.map((flatEntity) =>
    flatEntityToScalarFlatEntity({ flatEntity, metadataName: this.metadataName }),
  );
  await repository.insert(scalarFlatEntities);
}

从源码结构看,有两个值得注意的演进细节:

  1. Repository 来源:当前实现不再在子类中注入 @InjectRepository(..., 'metadata'),而是通过事务内的 queryRunner.manager.getRepository(...) 获取 Repository,并通过 ALL_METADATA_ENTITY_BY_METADATA_NAME 常量表按 metadataName 反查 TypeORM 实体类——这保证插入发生在同一个 QueryRunner 事务中,与 Runner 的整体回滚语义保持一致;
  2. 标量化:插入前调用 flatEntityToScalarFlatEntity,把扁平实体中的复合/聚合字段收敛为数据库可存储的标量形式。

一个真实存在的处理器示例是 CreateSkillActionHandlerService:它在 transpileUniversalActionToFlatAction 中为实体补齐 applicationId、生成 idv4())、写入 workspaceId,并重置空外键聚合器(getUniversalFlatEntityEmptyForeignKeyAggregators),再在 executeForMetadata 中调用基类的 insertFlatEntitiesInRepository

三、Update 动作处理器:更新对象中外键的二次解析

3.1 文档给出的标准实现

文档建议路径:

src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/services/update-my-entity-action-handler.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

import { WorkspaceUpdateActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-update-action-handler.service';
import { MyEntityEntity } from 'src/engine/metadata-modules/my-entity/entities/my-entity.entity';
import { fromUniversalFlatMyEntityToFlatMyEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/utils/from-universal-flat-my-entity-to-flat-my-entity.util';
import { resolveUniversalUpdateRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util';

@Injectable()
export class UpdateMyEntityActionHandlerService extends WorkspaceUpdateActionHandlerService<
  'myEntity',
  UniversalUpdateMyEntityAction,
  FlatUpdateMyEntityAction
> {
  constructor(
    @InjectRepository(MyEntityEntity, 'metadata')
    private readonly myEntityRepository: Repository<MyEntityEntity>,
  ) {
    super();
  }

  protected transpileUniversalActionToFlatAction(
    universalAction: UniversalUpdateMyEntityAction,
    flatEntityMaps: AllFlatEntityMapsByMetadataName,
  ): FlatUpdateMyEntityAction {
    const flatEntity = fromUniversalFlatMyEntityToFlatMyEntity(
      universalAction.universalFlatEntity,
      flatEntityMaps,
    );

    // Resolve universal foreign keys in updates to regular IDs
    const flatUpdates = resolveUniversalUpdateRelationIdentifiersToIds({
      metadataName: 'myEntity',
      universalUpdates: universalAction.universalUpdates,
      flatEntityMaps,
    });

    return {
      type: 'update',
      metadataName: 'myEntity',
      flatEntity,
      updates: flatUpdates,
    };
  }

  protected async executeForMetadata(
    flatActions: FlatUpdateMyEntityAction[],
  ): Promise<void> {
    for (const action of flatActions) {
      await this.myEntityRepository.update(
        { id: action.flatEntity.id },
        action.updates,
      );
    }
  }

  protected async executeForWorkspaceSchema(): Promise<void> {
    return;
  }
}

Update 特有的辅助工具是 resolveUniversalUpdateRelationIdentifiersToIds:它负责把 updates 对象内部的通用外键标识符映射回普通 ID。这一步不能省——Create 时整个实体一次性解析即可,而 Update 的载荷是一个部分更新对象(partial update),其中任何指向其他实体的外键字段都可能是通用标识符,必须逐字段解析。

3.2 源码佐证:resolveUniversalUpdateRelationIdentifiersToIds 的解析逻辑

当前仓库中的 resolve-universal-update-relation-identifiers-to-ids.util.ts 实现可以印证上述语义:

export const resolveUniversalUpdateRelationIdentifiersToIds = <T extends AllMetadataName>({
  metadataName,
  universalUpdate,
  allFlatEntityMaps,
}: { ... }): FlatEntityUpdate<T> => {
  const relationEntries = ALL_MANY_TO_ONE_METADATA_RELATIONS[metadataName];
  const result: Record<string, unknown> = { ...universalUpdate };

  for (const relationPropertyName of Object.keys(relationEntries)) {
    // 取出该关系对应的 { foreignKey, universalForeignKey, metadataName } 映射
    if (!Object.prototype.hasOwnProperty.call(result, universalForeignKey)) continue;

    const universalIdentifierValue = result[universalForeignKey];
    delete result[universalForeignKey];

    if (!isDefined(universalIdentifierValue)) {
      if (universalIdentifierValue === null) result[foreignKey] = null; // 允许显式置空
      continue;
    }

    const targetEntity = findFlatEntityByUniversalIdentifier({
      flatEntityMaps: allFlatEntityMaps[getMetadataFlatEntityMapsKey(targetMetadataName)],
      universalIdentifier: universalIdentifierValue,
    });
    if (!isDefined(targetEntity)) {
      throw new FlatEntityMapsException(
        `Could not resolve ${universalForeignKey} to ${foreignKey}: no ${targetMetadataName} found ...`,
        FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
      );
    }
    result[foreignKey] = targetEntity.id;
  }
  return result as FlatEntityUpdate<T>;
};

可以确认的关键行为:

  • 解析范围由常量表 ALL_MANY_TO_ONE_METADATA_RELATIONS 界定,即只处理 many-to-one 关系字段;
  • 值显式为 null 时映射为外键置空,undefined 则跳过;
  • 目标实体在 flat entity maps(即 Step 2 构建的缓存)中找不到时,抛出 FlatEntityMapsException(ENTITY_NOT_FOUND)——这正是 Runner 层「转译失败 → 整个动作执行失败并回滚」语义的来源。

一个实际的 Update 处理器参考 UpdateSkillActionHandlerService:它先用 findFlatEntityByUniversalIdentifierOrThrowallFlatEntityMaps.flatSkillMaps 中定位目标 flat 实体拿到 id,再调用 resolveUniversalUpdateRelationIdentifiersToIds 解析 action.update,最后执行 skillRepository.update({ id: entityId, workspaceId }, update)——注意条件中带了 workspaceId,保证更新只作用于本工作区的行。

四、Delete 动作处理器:复用基类转译 + 硬删除

4.1 文档给出的标准实现

文档建议路径:

src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/services/delete-my-entity-action-handler.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

import { WorkspaceDeleteActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-delete-action-handler.service';
import { MyEntityEntity } from 'src/engine/metadata-modules/my-entity/entities/my-entity.entity';
import { fromUniversalFlatMyEntityToFlatMyEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/utils/from-universal-flat-my-entity-to-flat-my-entity.util';

@Injectable()
export class DeleteMyEntityActionHandlerService extends WorkspaceDeleteActionHandlerService<
  'myEntity',
  UniversalDeleteMyEntityAction,
  FlatDeleteMyEntityAction
> {
  constructor(
    @InjectRepository(MyEntityEntity, 'metadata')
    private readonly myEntityRepository: Repository<MyEntityEntity>,
  ) {
    super();
  }

  protected transpileUniversalActionToFlatAction(
    universalAction: UniversalDeleteMyEntityAction,
    flatEntityMaps: AllFlatEntityMapsByMetadataName,
  ): FlatDeleteMyEntityAction {
    // Use base class helper for delete transpilation
    return this.transpileUniversalDeleteActionToFlatDeleteAction({
      universalAction,
      flatEntityMaps,
      fromUniversalFlatEntityToFlatEntity: fromUniversalFlatMyEntityToFlatMyEntity,
    });
  }

  protected async executeForMetadata(
    flatActions: FlatDeleteMyEntityAction[],
  ): Promise<void> {
    const ids = flatActions.map((action) => action.flatEntity.id);

    await this.myEntityRepository.delete(ids);
  }

  protected async executeForWorkspaceSchema(): Promise<void> {
    return;
  }
}

Delete 特有的辅助方法是 transpileUniversalDeleteActionToFlatDeleteAction(基类方法):删除动作只需定位「删谁」,不需要转译整条实体,因此基类把标准删除转译逻辑抽取为公共方法,子类一行复用。

4.2 源码佐证:基类中删除转译的简化形态

当前仓库基类中的 transpileUniversalDeleteActionToFlatDeleteAction 实现更简洁:

protected transpileUniversalDeleteActionToFlatDeleteAction(context): BaseFlatDeleteWorkspaceMigrationAction<TMetadataName> {
  const { action, allFlatEntityMaps } = context;
  const flatEntityMaps = allFlatEntityMaps[getMetadataFlatEntityMapsKey(action.metadataName)];
  const flatEntity = findFlatEntityByUniversalIdentifierOrThrow({
    flatEntityMaps,
    universalIdentifier: action.universalIdentifier,
  });
  return {
    type: 'delete',
    metadataName: this.metadataName,
    entityId: flatEntity.id,
  };
}

即:在 flat entity maps 缓存中通过 universalIdentifier 查出实体的数据库 id,产出的 flat 动作只携带 entityId。这与文档 Checklist 中「Delete handler uses hard delete (delete())」的要求一致——Runner 层执行的是物理删除而非软删除,缓存侧的删除同步由基类在 commit 后的缓存维护环节完成。

五、Universal-to-Flat 转换工具:外键反向解析的收口点

5.1 文档给出的标准实现

文档建议路径:

src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/utils/from-universal-flat-my-entity-to-flat-my-entity.util.ts
import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util';
import { type UniversalFlatMyEntity } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-my-entity.type';
import { type FlatMyEntity } from 'src/engine/metadata-modules/flat-my-entity/types/flat-my-entity.type';
import { type AllFlatEntityMapsByMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps-by-metadata-name.type';

export const fromUniversalFlatMyEntityToFlatMyEntity = (
  universalFlatMyEntity: UniversalFlatMyEntity,
  flatEntityMaps: AllFlatEntityMapsByMetadataName,
): FlatMyEntity => {
  // Resolve universal foreign keys back to regular IDs
  return resolveUniversalRelationIdentifiersToIds({
    metadataName: 'myEntity',
    universalFlatEntity: universalFlatMyEntity,
    flatEntityMaps,
  }) as FlatMyEntity;
};

这个工具是整个 Step 4 的枢纽:三个 Handler 的转译阶段都收敛到 resolveUniversalRelationIdentifiersToIds(整实体级解析)。文档特别强调,它是 resolveEntityRelationUniversalIdentifiers逆操作——Builder/Cache 阶段把数据库外键 ID「升格」为通用标识符以便 diff 与跨工作区比较,Runner 阶段再「降格」回数据库 ID 以便落库。

5.2 相关工具族(当前仓库实际文件)

universal-flat-entity/utils/ 目录下还有一组与该机制配套的工具,理解它们有助于把握 Runner 与 Cache 层的边界(目录:universal-flat-entity):

六、框架视角:转译与执行是如何被基类编排的

文档反复强调「每个 Handler 有两个阶段」。从源码结构看,这两阶段并不是 Handler 自己串起来的,而是由基类的 execute() 统一编排。BaseWorkspaceMigrationRunnerActionHandlerService.execute 的编排流程为:

  1. 清洗(Sanitize):对 type === 'update' 的动作先调用 sanitizeUniversalFlatEntityUpdate 清洗更新载荷,再进入子类转译;转译抛错会被包装为 WorkspaceMigrationRunnerException(EXECUTION_FAILED)
  2. 并行执行两阶段落库Promise.allSettled 并发执行 executeForMetadataexecuteForWorkspaceSchema,任一失败即抛出携带具体错误位置的 WorkspaceMigrationRunnerException;两者均带 asyncMethodPerformanceMetricWrapper 计时,记录 WorkspaceMigrationActionDurationMs 直方图指标;
  3. 缓存同步与事件派生:成功后,基类自动完成三件事——
    • deriveMetadataEventsFromFlatAction:按 create/delete/update 派生 metadata 事件(用于变更传播);
    • getAfterCommitSideEffects:收集 commit 后副作用;
    • optimisticallyApplyActionOnAllFlatEntityMaps:把 flat 动作乐观地应用到 flat entity maps 缓存,产生 partialOptimisticCache 返回给 Runner。

因此子类只需要实现四个钩子,其余的事务边界、指标、缓存一致性、错误包装都由基类承担:

钩子方法 职责 是否必须实现
transpileUniversalActionToFlatAction 阶段一:Universal → Flat 转译 必须(抽象方法)
executeForMetadata 阶段二:metadata 数据库操作 纯 Schema 类动作可留空
executeForWorkspaceSchema 阶段二:工作区 Schema 变更 纯 metadata 实体可 return 空实现
rollbackForMetadata 回滚 metadata 侧变更 基类默认空实现

6.1 上下文参数:Handler 拿到什么

当前实现中,转译与执行接收的上下文类型定义在 workspace-migration-action-runner-args.type.ts

export type WorkspaceMigrationActionRunnerArgs<TUniversalAction extends AllUniversalWorkspaceMigrationAction> = {
  queryRunner: QueryRunner;                                              // 事务 QueryRunner
  action: TUniversalAction;                                               // 待执行的通用动作
  allFlatEntityMaps: AllFlatEntityMaps;                                   // 全量扁平实体缓存(Step 2 的产物)
  workspaceId: string;                                                    // 当前工作区
  flatApplication: FlatApplication;                                      // 当前应用
  preallocatedIdByUniversalIdentifierByMetadataName?: ...;               // 预分配 ID 映射
  getSearchFieldMetadatasByTsVectorFieldId?: ...;                         // 搜索字段元数据查询器
};

export type WorkspaceMigrationActionRunnerContext<TFlatAction, ...> =
  WorkspaceMigrationActionRunnerArgs<...> & { flatAction: TFlatAction };

可以看到,文档中的「flatEntityMaps 参数」在当前实现里对应 allFlatEntityMaps,且上下文还额外携带事务级 queryRunnerworkspaceIdflatApplication 等——这正是 Handler 不需要自行注入 Repository 的原因:执行所需的一切都由 Runner 通过上下文下发。

6.2 注册机制:Handler 如何被发现与调度

文档中 Handler 继承的 Workspace*ActionHandlerService 基类,在当前仓库中对应通过类工厂装饰器 WorkspaceMigrationRunnerActionHandler(actionType, metadataName) 生成的抽象基类(interface 文件末尾)。该工厂用 SetMetadatabuildActionHandlerKey(actionType, metadataName)(形如 create:skill)写入 WORKSPACE_MIGRATION_ACTION_HANDLER_METADATA_KEY

调度侧由 WorkspaceMigrationRunnerActionHandlerRegistryService 完成:

  • onModuleInit 阶段通过 NestJS DiscoveryService 扫描 WorkspaceSchemaMigrationRunnerActionHandlersModule 的所有 provider,读取 metadata key 建立 Map<actionHandlerKey, handler> 注册表;
  • 执行时以 action.type + action.metadataName 构建 key 查找 Handler,找不到时抛出 WorkspaceMigrationActionExecutionException(INVALID_ACTION_TYPE)——这提示开发者:新增 syncable entity 后,如果忘记在模块中注册三个 Handler,运行时才会暴露,属于典型的「编译通过、启动即报错」风险点;
  • 同一注册表还提供 executeActionRollbackHandler 入口,对应基类的 rollback/rollbackForMetadata 链路。

Runner 的总入口 workspace-migration-runner.service.ts(约 557 行)负责把 Builder 产出的动作列表按序喂给注册表,并在事务内管理 commit/rollback。

从源码结构看,当前仓库中 action-handlers/ 目录下已有 108 个 Handler 实现文件(skill、role、view、field、webhook、logic-function 等 30 余种 metadataName,各含 create/update/delete 三件套),本文描述的「三 Handler + 一转译工具」是每一类 syncable entity 接入 Runner 层的最小完整集合。

七、完成度自检清单(Checklist)

技能文档给出的 Step 4 验收清单,在进入 Step 5 前应逐项确认:

  • [ ] Create 动作处理器已实现
  • [ ] Update 动作处理器已实现
  • [ ] Delete 动作处理器已实现
  • [ ] 所有 Handler 继承了对应的基类(当前实现中即 WorkspaceMigrationRunnerActionHandler('create'|'update'|'delete', '<metadataName>') 工厂基类)
  • [ ] 所有 Handler 均实现了 transpileUniversalActionToFlatAction
  • [ ] 所有 Handler 均实现了 executeForMetadata
  • [ ] executeForWorkspaceSchema 已实现(无需 Schema 变更时返回空实现)
  • [ ] Universal-to-Flat 转换工具已创建
  • [ ] Create Handler 使用了 insertFlatEntitiesInRepository
  • [ ] Update Handler 使用了 resolveUniversalUpdateRelationIdentifiersToIds
  • [ ] Delete Handler 使用了 transpileUniversalDeleteActionToFlatDeleteAction
  • [ ] Delete Handler 使用硬删除(delete())而非软删除

八、小结与下一步

Step 4 的本质,是把 Builder 产出的、以通用标识符表达的语义动作,经「转译—执行」两阶段翻译为物理数据库操作,同时保证事务边界、缓存一致性与可观测性由基类统一兜底。实现时最容易出现的问题集中在三处:Update 载荷中遗漏的外键字段未被 resolveUniversalUpdateRelationIdentifiersToIds 覆盖(表现为落库后外键为非法值)、Delete 忘记走硬删除导致缓存与数据库状态不一致、以及 Handler 未注册到 WorkspaceSchemaMigrationRunnerActionHandlersModule 导致运行时 INVALID_ACTION_TYPE 异常。

动作处理器完成后,继续推进到:

Syncable Entity: Integration(Step 5/6)

完整工作流可参考 .cursor/skills/ 下的各步骤技能文档及 @creating-syncable-entity 规则。

登录后查看全文
热门项目推荐
相关项目推荐