现象 宿主 app 声明了 @objectstack/service-cluster / @objectstack/service-cluster-redis(#4719 契约满足)、设 OS_CLUSTER_DRIVER=redis 走 os serve,boot 拒启:
✗ Cluster driver "redis" is not registered. Did you forget to import @objectstack/service-cluster-redis or call registerClusterDriver()? See content/docs/kernel/cluster.mdx §6.
而手动在同一宿主目录 import('@objectstack/service-cluster-redis') 完全正常(导出齐全)。serve 侧没有任何线索 :装载失败被空 catch 吞掉。
实测环境:平台 17.2.0 (objectos-ee:4.1.1 镜像,builtAt 2026-08-24),宿主 app 为 os-tianshun-ehr 的 EE 薄扩展镜像(经 file: 声明 + 符号链接指向镜像内 workspace 包)。 复现与临时绕法收录在 steedos-labs/os-project-titanwind-ehr 的 deploy-ee/README.md 与 deploy-ee/cluster-preload.mjs(PR steedos-labs/os-project-titanwind-ehr#1880)。 根因链(逐环节核实过) serve(packages/cli/src/commands/serve.ts,OS_CLUSTER_DRIVER 分支)经 importFromHost (createHostRequire 认 NODE_PATH,于是 #4699 立下的「host app 必须自己声明」在 pnpm 工作区里根本没被强制 #4719 宿主解析器)装载 @objectstack/service-cluster-${driver}。
createHostImporter(packages/types/src/node.ts)对已声明 的包走 hostRequire.resolve(pkg) —— CJS 解析 ,命中 exports 的 "require" 条件:
hr.resolve('@objectstack/service-cluster-redis')
→ …/service-cluster-redis/dist/index.cjs ← CJS 构建
hr.resolve('@objectstack/service-cluster')
→ …/service-cluster/dist/index.cjs
于是 registerClusterDriver('redis', …)(service-cluster-redis/src/index.ts 末尾的模块副作用)注册进的是 service-cluster CJS 构建实例 的 driverRegistry —— 这是个模块级 const Map (packages/services/service-cluster/src/cluster.ts),不挂 globalThis。
Runtime 的 defineCluster 从 ESM 上下文 bare import @objectstack/service-cluster → ESM 构建实例 (dist/index.js)→ 另一张空 Map → 抛 "not registered"。
加重:serve 对该 import 是空 catch { /* may already be registered by the loaded config */ } —— 真实失败模式(装载成功但注册进错实例/或解析失败)完全静默,操作者只能看到第 4 步的结果性报错,与真实原因相隔两层。
一句话:同一个包的 CJS 与 ESM 双构建各持一份 registry,注册与查找不在同一张 Map 上。
最小复现 任意宿主 app(package.json 声明上述两包并可解析),OS_CLUSTER_DRIVER=redis OS_REDIS_URL=redis://… os serve → 拒启如上。
项目侧临时绕法(可作修复验证参照) 入口改 node --import cluster-preload.mjs,预载脚本在宿主目录以纯 ESM await import('@objectstack/service-cluster-redis') 一次 —— Node 默认 realpath 到与 Runtime 相同的 ESM 实例,注册即生效。实测 3 副本集群恢复正常(redis fence 锁选主、跨副本协调均工作)。
建议修复方向(任一即可,前两者可叠加) driverRegistry(以及同类模块级单例,如 multi-node gate 的注册表)挂 globalThis 兜底 ,双构建实例共享一张表;createHostImporter 对已声明包在 ESM 上下文优先 import 条件 解析(或 resolve 后按 import 条件重定向),避免 CJS/ESM 双实例;serve 的集群驱动装载不再静默吞错 :至少把失败原因降级为 WARN 打出来 —— "may already be registered by the loaded config" 这个假设应该验证(查 registry)而不是假定。 波及面 不止 cluster:凡经 hostImporter 装载、靠模块副作用注册进模块级单例 的包,双构建下都可能注册错位。已另立 cloud 侧 issue 追多节点授权门(registerMultiNodeGate)的同型错位与接线缺口(编号见评论/引用)。
现象
宿主 app 声明了
@objectstack/service-cluster/@objectstack/service-cluster-redis(#4719 契约满足)、设OS_CLUSTER_DRIVER=redis走os serve,boot 拒启:而手动在同一宿主目录
import('@objectstack/service-cluster-redis')完全正常(导出齐全)。serve 侧没有任何线索:装载失败被空catch吞掉。deploy-ee/README.md与deploy-ee/cluster-preload.mjs(PR steedos-labs/os-project-titanwind-ehr#1880)。根因链(逐环节核实过)
serve(packages/cli/src/commands/serve.ts,OS_CLUSTER_DRIVER分支)经 importFromHost(createHostRequire认 NODE_PATH,于是 #4699 立下的「host app 必须自己声明」在 pnpm 工作区里根本没被强制 #4719 宿主解析器)装载@objectstack/service-cluster-${driver}。createHostImporter(packages/types/src/node.ts)对已声明的包走hostRequire.resolve(pkg)—— CJS 解析,命中 exports 的"require"条件:于是
registerClusterDriver('redis', …)(service-cluster-redis/src/index.ts末尾的模块副作用)注册进的是 service-cluster CJS 构建实例的driverRegistry—— 这是个模块级const Map(packages/services/service-cluster/src/cluster.ts),不挂 globalThis。Runtime的defineCluster从 ESM 上下文 bare import@objectstack/service-cluster→ ESM 构建实例(dist/index.js)→ 另一张空 Map → 抛 "not registered"。加重:serve 对该 import 是空
catch { /* may already be registered by the loaded config */ }—— 真实失败模式(装载成功但注册进错实例/或解析失败)完全静默,操作者只能看到第 4 步的结果性报错,与真实原因相隔两层。一句话:同一个包的 CJS 与 ESM 双构建各持一份 registry,注册与查找不在同一张 Map 上。
最小复现
任意宿主 app(package.json 声明上述两包并可解析),
OS_CLUSTER_DRIVER=redis OS_REDIS_URL=redis://… os serve→ 拒启如上。项目侧临时绕法(可作修复验证参照)
入口改
node --import cluster-preload.mjs,预载脚本在宿主目录以纯 ESMawait import('@objectstack/service-cluster-redis')一次 —— Node 默认 realpath 到与 Runtime 相同的 ESM 实例,注册即生效。实测 3 副本集群恢复正常(redis fence 锁选主、跨副本协调均工作)。建议修复方向(任一即可,前两者可叠加)
driverRegistry(以及同类模块级单例,如 multi-node gate 的注册表)挂 globalThis 兜底,双构建实例共享一张表;createHostImporter对已声明包在 ESM 上下文优先import条件解析(或 resolve 后按 import 条件重定向),避免 CJS/ESM 双实例;波及面
不止 cluster:凡经 hostImporter 装载、靠模块副作用注册进模块级单例的包,双构建下都可能注册错位。已另立 cloud 侧 issue 追多节点授权门(registerMultiNodeGate)的同型错位与接线缺口(编号见评论/引用)。