
Activepieces 平台配置行设计以首次读取创建 行即权威取代环境变量的进程级开关【免费下载链接】activepiecesAI Agents MCPs AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows AI Agents • MCPs for AI Agents项目地址: https://gitcode.com/GitHub_Trending/ac/activepieces本文基于仓库中的架构决策记录 000033-platform-configuration-rows-are-authoritative-and-created-on-first-read.md 展开并结合当前仓库中已经落地的迁移、实体、服务与前端代码逐项印证。读者读完后可以完整理解 Activepieces 如何用一张每平台一行、懒加载创建、行内容即最终事实的platform_configuration表取代原先每进程一个、模块加载时读取一次的AP_TELEMETRY_ENABLED环境变量并掌握其 API、SQL 过滤、前端门控与部署影响的全貌。一、背景环境变量的三个局限在引入platform_configuration表之前产品遥测product analytics开关完全由AP_TELEMETRY_ENABLED这一个环境变量控制它有几个结构性局限每进程一个值该变量在 system.ts 中被系统属性默认值机制兜底为true并在 system-props.ts 中登记为TELEMETRY_ENABLED。它属于进程级配置模块加载时被读取一次。自托管用户无法自助修改想关掉遥测只能改环境变量并重启进程没有任何 UI 告诉用户到底采集了什么。缺少可见性没有任何界面展示遥测开关的状态。期望的终态是平台管理界面Infrastructure → Configurations上出现一个可以持续增长的开关面板未来每增加一个可配置项就在上面多一个拨杆。而拨杆背后的数据载体就是本决策引入的platform_configuration表。二、决策一张表、一行一平台、一个设置一个类型化列决策的核心可以用三句话概括新增platform_configuration表一个平台一行一个设置一个类型化列——表结构与platform_plan完全同构与platform一对一、多对一关系、级联删除。迁移只建表、绝不播种迁移只创建表结构不插入任何行行由getOrCreateForPlatform在首次读取时懒加载创建与platform_plan的做法完全一致。行的默认值取自AP_TELEMETRY_ENABLED环境变量从此只是还没有行的平台的默认值来源而不再是一次性播种的种子值一旦某平台的行被创建该平台就再也不看环境变量。决策文档中最初设想的第一批设置是productAnalyticsEnabled在 当前仓库实现 中这一列最终命名为isProductTelemetryEnabled并同时新增了第二列isInfraSetupTelemetryEnabled基础设施遥测。zod 校验模型如下export const PlatformConfiguration z.object({ ...BaseModelSchema, platformId: z.string(), isProductTelemetryEnabled: z.boolean(), isInfraSetupTelemetryEnabled: z.boolean(), }) export const UpdatePlatformConfigurationRequestBody PlatformConfiguration.pick({ isProductTelemetryEnabled: true, isInfraSetupTelemetryEnabled: true, }).partial()2.1 表结构实体与迁移表结构由 platform-configuration.entity.ts 定义两列均为非空布尔、默认trueplatformId上有唯一索引idx_platform_configuration_platform_id并通过外键fk_platform_configuration_platform_id级联删除columns: { ...BaseColumnSchemaPart, platformId: ApIdSchema, isProductTelemetryEnabled: { type: Boolean, nullable: false, default: true }, isInfraSetupTelemetryEnabled: { type: Boolean, nullable: false, default: true }, },对应迁移 1841000000000-AddPlatformConfiguration.tsrelease 0.91.0事务包裹严格只做三件事建表、建唯一索引、加外键——没有任何INSERTCREATE TABLE platform_configuration ( id character varying(21) NOT NULL, created TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), updated TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), platformId character varying(21) NOT NULL, isProductTelemetryEnabled boolean NOT NULL DEFAULT true, isInfraSetupTelemetryEnabled boolean NOT NULL DEFAULT true, CONSTRAINT pk_platform_configuration PRIMARY KEY (id) )2.2 懒加载创建先查后锁创建逻辑位于 platform-configuration.service.ts 的getOrCreateForPlatformasync getOrCreateForPlatform({ platformId }: GetOrCreateParams): PromisePlatformConfiguration { const existing await platformConfigurationRepo().findOneBy({ platformId }) if (!isNil(existing)) { return existing } return distributedLock(log).runExclusive({ key: platform_configuration_${platformId}, timeoutInSeconds: CREATE_LOCK_TIMEOUT_SECONDS, fn: async () { const configuration await platformConfigurationRepo().findOneBy({ platformId }) if (!isNil(configuration)) { return configuration } return createInitialConfiguration({ platformId }) }, }) }便宜路径优先先做一次普通findOneBy绝大多数读请求在无锁路径上直接返回。只有创建路径才加分布式锁锁键为platform_configuration_${platformId}超时 60 秒进入锁后再查一次double-check确认不存在才真正插入避免并发下重复建行。创建默认值读环境变量createInitialConfiguration中isProductTelemetryEnabled取system.getBoolean(AppSystemProp.TELEMETRY_ENABLED)变量不存在时通过spreadIfNotUndefined省略该列从而回退到 schema 自己的DEFAULT true而不是在 TypeScript 里再写一个true。2.3 一个细节那层永远够不到的保险带决策文档特别指出createInitialConfiguration里省略列的分支实际上永远不会发生——因为 system.ts 中systemPropDefaultValues已经把TELEMETRY_ENABLED的默认值写成true所以system.getBoolean不可能返回undefined列总是被显式写入同理filterProjectsWithProductTelemetryEnabled里的?? true回退也永远走不到。这两处都是故意保留的双保险只有当systemPropDefaultValues中该默认值被移除时它们才开始真正发挥作用。三、为什么是按平台而不是按部署决策文档给出了两个直接理由编辑它的 UI 是按平台的Configurations 页面属于某个平台的管理界面数据按平台组织行自然也应该按平台组织。部署级单例是权限泄漏在一个托管多个平台的实例上如果配置是全部署共享一个单例那么任意一个租户的管理员都能通过自己的管理页面改动影响所有人的设置——这是多租户场景下不能接受的越权路径。同时决策文档承认这刻意制造了每平台一行与环境变量进程级作用域之间的错配但它是可接受的因为当时梳理发现所有消费者手里都已经有platformId——enterpriseFlagsHooks.modify已按请求解析出platformId主体验证否则回退 hostname并支持按平台覆盖标志位THEME、EMAIL_AUTH_ENABLED就是这样工作的服务端trackUser/trackProject/trackPlatform的调用点也都携带平台。四、三个关键取舍的论证4.1 不播种胜过播种迁移为什么不写一个一行一平台的回填迁移文档的论证分三层成本Cloud 的平台数量级意味着回填是单个迁移事务里数百条批量的INSERT——而懒路径本来就会做那次查找回填只是为了省一次本来就要做的查询而付出的一次性代价。双创建路径回填是第二条创建路径它必须与懒创建路径永远保持一致——两份逻辑在今后每一个 schema 演进里都要同步修改是长期的维护负债。回归风险如果创建默认值写死为true那么一个以AP_TELEMETRY_ENABLEDfalse运行的运维人员升级后会得到没有行 默认开的结果——遥测被悄悄重新打开这正是播种要防止的回归。取消预填充后已存在平台和新平台被折叠成同一种情况保证也从迁移复制了你的值迁移为任何一行生来就带着你的值。4.2 环境变量作默认值胜过作锁另一种备选方案是环境变量作锁当环境变量被显式设置时UI 上的开关禁用。它的优点是不是破坏性变更且能保住完全离网air-gapped运维人员的保证缺点则是会出现两个事实来源的 UI 和一个需要解释的禁用控件。而环境变量作默认值只有一个事实来源代价被公开接受这是一次功能性的破坏性变更——为了合规而设置AP_TELEMETRY_ENABLEDfalse的运维人员升级后保留该值但从此失去环境变量的强制力详见下文后果。4.3 类型化列胜过 key/value 行与 jsonb 大字段platform_plan已经承载了 49 个类型化列仓库禁止any与类型强转。类型化列让 zod 校验模型直接对 schema 负责而不是对一个可能漂移的注册表负责。代价是每个新设置都要写一次迁移——在这个场景下很便宜。五、API 面读公开、写仅限平台管理员控制器 platform-configuration.controller.ts 暴露两个端点GET /v1/platform-configurationssecurityAccess.publicPlatform([PrincipalType.USER])——放宽到任何成员可读因为浏览器登录后需要读取该行来初始化遥测。POST /v1/platform-configurationssecurityAccess.platformAdminOnly([PrincipalType.USER])——仅平台管理员可写。服务端update逻辑先getOrCreateForPlatform确保行存在再用spreadIfNotUndefined只打补丁传入的字段最后findOneByOrFail返回完整行async update({ platformId, isProductTelemetryEnabled, isInfraSetupTelemetryEnabled }: UpdateParams): PromisePlatformConfiguration { await this.getOrCreateForPlatform({ platformId }) const patch { ...spreadIfNotUndefined(isProductTelemetryEnabled, isProductTelemetryEnabled), ...spreadIfNotUndefined(isInfraSetupTelemetryEnabled, isInfraSetupTelemetryEnabled), } if (!isEmpty(patch)) { await platformConfigurationRepo().update({ platformId }, patch) } return platformConfigurationRepo().findOneByOrFail({ platformId }) }前端对应封装在 platform-configuration-api.tsget()与update()分别打到上述两个端点。六、服务端事件门的平台化telemetry.utils.ts 的全面改写环境变量原先在 telemetry.utils.ts 里是模块加载时的一个布尔一票否决所有事件。改写后每个事件在发出前都要用平台配置做一次门控identify、trackPlatform、trackProject、trackUser参数里携带必填的platformId: string统一走platformConfigurationService.isProductTelemetryEnabled({ platformId })。trackIdentity签名是platformId: string | null唯一一个允许空平台的入口。isProductTelemetryEnabled服务方法本身还带一层 Cloud 快捷通道——Cloud 版直接返回true非 Cloud 才去读必要时创建配置行async isProductTelemetryEnabled({ platformId }: GetOrCreateParams): Promiseboolean { if (system.getEdition() ApEdition.CLOUD) { return true } const configuration await this.getOrCreateForPlatform({ platformId }) return configuration.isProductTelemetryEnabled }6.1 trackUser 与 trackIdentity 的边界User行是按平台存的所以trackUser/trackProject/trackPlatform/identify全部要求platformId: string并经过isProductTelemetryEnabled。UserIdentity是每个邮箱一行、横跨所有平台的实体它先于任何平台存在因此trackIdentity接受platformId: string | null当它为 null 时事件属于租户创建前的认证漏斗门控条件是edition CLOUD——这是浏览器端isCloudAuthFunnel常量的服务端镜像因此不需要读环境变量。未归属事件不携带groups永远不会落到不属于它的平台上。6.2 未归属事件安全的前提null 必须真的意味着没有平台trackIdentity的 null 分支以edition CLOUD为门只有 null不可伪造时才成立。决策文档记录了一次真实的踩坑requestCode原本把请求的platformId直接透传而在 Cloud 上未认证请求的platformId恒为 null——即使是对一个长期租户EMAIL_CODE_REQUESTED也会绕过 edition 检查被发送给isProductTelemetryEnabled: false的 Cloud 平台行存在、写着不要发却从未被咨询。修复后两个调用点都在请求不带平台时走authenticationService.resolvePreferredPlatformId让 null 在两个位置语义一致edition 回退只在真的没有平台可归属时才触发。6.3 Cloud 上登录前认证调用的 platformId 恒为 nullEMAIL_CODE_REQUESTED与EMAIL_CODE_VERIFIED都从PrincipalType.UNKNOWN的未认证路由发出而platformUtils.getPlatformIdForRequest在 Cloud 上对非 principal 请求无条件返回 null从不咨询 hostname自托管则相反会回退到getOldestPlatform()除全新安装外总有平台在手。因此任何在这些调用点加!isNil(platformId)守卫的尝试都会静默删除整个 Cloud 注册漏斗——EMAIL_CODE_VERIFIED的needsNameStep: isNil(preferredPlatformId)恰恰就是全新身份的判定。七、SQL 过滤的铁律LEFT JOIN COALESCE永远不 INNER JOIN因为行是懒创建的绝大多数平台在表里根本没有行。因此任何基于该表做 SQL 过滤的逻辑都必须用LEFT JOIN而不是INNER JOIN后者会静默丢掉所有无行平台用COALESCE(配置列, 环境变量默认值)而不是裸WHERE 配置列 true。仓库中的典型实现是filterProjectsWithProductTelemetryEnabled按 1,000 一批分块查询const fallback system.getBoolean(AppSystemProp.TELEMETRY_ENABLED) ?? true // ... .createQueryBuilder() .select(project.id, id) .from(project, project) .leftJoin(platform_configuration, configuration, configuration.platformId project.platformId) .where(project.id IN (:...projectIds), { projectIds: batch }) .andWhere(COALESCE(configuration.isProductTelemetryEnabled, :fallback) true, { fallback })同样原则适用于每日RUN_TELEMETRY作业注册于 flow-run-module.ts对应SystemJobName.RUN_TELEMETRY见 common.ts。决策文档强调在这种聚合任务里要先过滤再逐行处理——如果在trackProject内部逐行检查就成了每个项目一次配置查询 一次项目查询的 N1 形状这正是 packages/server/CLAUDE.md 明令禁止的。八、浏览器端ApFlagId.TELEMETRY_ENABLED 被彻底移除main分支上环境变量一共有三处功能门浏览器是最大的一扇telemetry.utils.ts模块加载期布尔覆盖identify/trackPlatform/trackProject/trackUser/isEnabled即每一个服务端事件认证、flow.created、两个mcp.*、run.created。flag.service.ts把它重新发布为ApFlagId.TELEMETRY_ENABLED给整个浏览器 SDK——12 个命名事件外加 pageviews、autocapture、dead clicks、rageclick、heatmaps、session recording占了 Cloud 绝大部分遥测流量。template-telemetry.service.ts独立再读一次环境变量。其余两处引用是system-validator.ts的booleanValidator与system.ts里true的默认值worker 从不读它。新设计中ApFlagId.TELEMETRY_ENABLED被完全移除浏览器改为直接读配置行。第一个方案保留 flag 作为投递通道被否决障碍看起来是结构性的——TelemetryProvider包裹整个路由树posthog.init在登录页、任何 principal 存在之前就会执行而/v1/platform-configurations是认证端点——但解法是把登录前窗口重新定性它只属于 Cloud 自己的认证漏斗autocapture.url_allowlist恰好就是 sign-in/sign-up 路由且离云版本来就不开。因此isNil(currentUser) edition CLOUD hostname cloud.activepieces.com这是一个常量判定无需任何查询其他所有场景都等登录后再读配置行。代价是登录后、配置请求返回前的短暂窗口内capture()会丢事件identify只是延迟因为配置行落地后会重放效果。决策文档明确要求保持 fail-closed——把仍在加载当作已启用会在征得同意前就开始采集。8.1 登录前窗口只属于 cloud.activepieces.com代价是 8 个命名事件因为配置查询是enabled: !isNil(currentUser)登录前无人能读到它所以自托管与 Cloud 自定义域名下posthog.init登录前不再运行。这会丢掉capture_pageview: history_change以及features/authentication/components/ 下的每一个capture()——SIGN_IN_SUBMITTED、SIGN_IN_FAILED、SIGN_UP_SUBMITTED、SIGN_UP_FAILED、EMAIL_CODE_REJECTED、EMAIL_CODE_RESEND_REQUESTED、FEDERATED_LOGIN_STARTED、CAPTCHA_UNAVAILABLE外加EMAIL_VERIFICATION_COMPLETED。这是设计使然自托管与 Cloud 自定义域名的注册漏斗遥测归零。autocapture 与 session recording 本就仅限 Cloud不受影响。8.2 Cloud 恒开必须由服务端落实浏览器不得粉饰POST /v1/platform-configurations没有 edition 守卫——Cloud 平台管理员技术上仍可写入false只是页面被隐藏。决策文档指出第一版实现曾把浏览器在cloud.activepieces.com上整会话硬编码为开启结果造成脑裂服务端事件停了而identifyCloud 上通过pickTelemetryPii发送邮箱和姓名、group、capture、autocapture、dead clicks、heatmaps、session recording 全都在跑——告诉客户我们停了其实没停。而且会让主域名租户与自定义域名租户行为不一致。正确做法是在服务端、对值本身做校验拒绝或强制改写Cloud 版对update的调用。九、边界与红线Configurations 与 platform_plan 的职责分离Configurations 持有行为behaviorplatform_plan持有权益entitlements——同一个值绝不能同时出现在两者。配额usersLimit、activeFlowsLimit是套餐授予的权益如果它们同时也是管理员可编辑的配置客户就能自己改自己的配额。这正是MAX_RECORDS_PER_TABLE/MAX_FIELDS_PER_TABLE看似天然适合这个页面、却绝对不能放进来的原因今天它因页面在 Cloud 隐藏而无害但页面一旦开放表里的限额就是自助配额绕过。total_runs_per_day刻意不受此开关约束它是计费仪表billing meter客户不能关掉自己的计量。只有产品遥测和未来的setup report 是可切换的。页面只配置 app 侧值。Worker 拨杆AP_EXECUTION_MODE、AP_WORKER_CONCURRENCY、AP_SANDBOX_MEMORY_LIMIT继续停留在环境变量因为 worker 没有读取该表的路径。十、后果与破坏性影响清单这是文档明确定义的功能性破坏性变更波及面如下发布流程需要打⛓️ breaking-change标签在 breaking-changes.mdx 增加条目并重写 telemetry.mdx 与 environment-variables.mdx。部署面AP_TELEMETRY_ENABLED已渗透到运维人员拥有的基础设施声明里——Helm 的 values.yaml 把它列在activepieces-telemetry-secrets下Pulumi 的 index.ts 显式把它设为true。变更后这些位置只是种下平台第一行。它同样以false存在于.env.dev、packages/server/api/.env.tests、packages/tests-e2e/.env.e2e、两个tests-e2e-*workflow、benchmark/docker-compose.yml与benchmark/k8s-sandbox.yaml——这些都能继续工作因为生而为 false 的行保持 false。环境变量失效的时点升级后变量仍会对每个平台生效直到该平台的行被创建首次读 flags 或首次访问管理页面。在那之后编辑变量不再有任何效果——这就是它仍是破坏性变更的原因。对运维人员的实际影响为合规目的设置AP_TELEMETRY_ENABLEDfalse的实例升级时值被保留第一行继承 false但之后失去环境变量的强制力。flags.hooks.ts与enterpriseFlagsHooks无需改动早期计划曾给两者加按平台覆盖以便让/v1/flags携带该值移除ApFlagId.TELEMETRY_ENABLED后钩子失去意义已还原为纯透传。类型即执行isProductAnalyticsEnabled的platformId是必填参数没有可选参数、没有 env 回退——正是必填类型在编译期暴露了全部五个调用点。不要重新引入回退到AP_TELEMETRY_ENABLED的可选参数那看起来像安全默认值实际上是让不可归属的事件逃出开关。迁移下一个环境变量的成本预期AP_TELEMETRY_ENABLED在三个地方各自模块加载时读取一次每个读取点都要被找到并改造为平台感知。把下一个变量迁进来不是一次 schema 变更而是同样一轮全仓排查。十一、总结platform_configuration设计把遥测开关从一个进程级、模块加载期、一次性读取的环境变量改造成了一张每平台一行、懒创建、行内容即最终事实的配置表。它的核心价值在于单一事实来源迁移不播种行由getOrCreateForPlatform在首次读取时以AP_TELEMETRY_ENABLED为默认值创建已有平台与新平台合并为同一种情况权限安全编辑入口收敛到平台管理员的 Configurations 页面杜绝多租户实例上的部署级单例越权类型安全一个设置一个类型化列zod 校验直接对 schema 负责边界清晰行为配置与套餐权益platform_plan严格分离计费仪表永不可被客户关闭。围绕它沉淀下的一系列工程原则——懒加载行的 SQL 必须LEFT JOIN COALESCE、未归属事件的门控必须建立在null 不可伪造之上、Cloud 恒开必须由服务端落实——对后续把更多环境变量迁入这张表、以及设计任何租户级可编辑行为配置的场景都有直接的复用价值。【免费下载链接】activepiecesAI Agents MCPs AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows AI Agents • MCPs for AI Agents项目地址: https://gitcode.com/GitHub_Trending/ac/activepieces创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考