跳转到内容

错误码

Dougong 的所有结构化错误都带一个稳定的 code 字符串。应用代码应该 switch (error.code) 分流,而不是匹配消息文本——消息会变,code 不会。

命名规则

错误码指向哪个对象的不变量被违反,而不是笼统地说「插件出错了」:

前缀违反不变量的对象属于
SERVICE_* / CONTRACT_* / CONFIG_*Contract 身份、依赖图、配置声明Core
LIFETIME_*结构化资源所有权Core
INSTALLATION_*一次已存在的安装Core
GROUP_*一棵安装所有权子树Core
ARTIFACT_*一份外部制品内部不自洽Platform
REGISTRATION_*注册身份与 Manifest 声明的注册依赖图Platform
MANIFEST_* / MODULE_* / API_* / PERMISSION_* / PLATFORM_*信任边界与加载边界Platform

所以看到 INSTALLATION_REMOVED 就知道是 Core 的一次安装,看到 REGISTRATION_REMOVED 就知道是 Platform 的一条注册——不用点进实现确认层级。

错误类型

ts
class DougongError extends Error {
  readonly code: string
}

class ConfigValidationError extends DougongError {   // code: "CONFIG_INVALID"
  readonly issues: ReadonlyArray<StandardSchemaV1.Issue>
}

class PlatformError extends DougongError {}

class PermissionDeniedError extends PlatformError {  // code: "PERMISSION_DENIED"
  readonly manifestName: string
  readonly denied: ReadonlyArray<string>
}

多个失败会被聚合成标准的 AggregateError,每一条原因都保留在 errors 数组里。

TypeError 与 Error 的分工

除了带 code 的错误,Dougong 还用两种原生类型:

  • TypeError —— 调用者传错了东西(不是函数、不是 Contract、key 冲突,或继续使用已经撤销其局部能力的对象)
  • Error —— 内部不变量被违反,属于框架 bug,正常使用碰不到

所以你可以按构造函数分流:DougongError 是可预期的操作失败,TypeError 是你的用法问题。

Core(@dougongjs/core

构图期

这些错误在任何 Instance 启动之前抛出。执行图一动没动。

Code触发条件
SERVICE_CYCLE依赖成环。消息带真实环路径:app.a:1 -> app.b:2 -> app.a:1。插件 requires 自己 provides 的 Service 也算
SERVICE_CONFLICT两个插件提供同一个 Service
SERVICE_MISSING必需 Service 没有提供者(optional() 声明的不算)
CONTRACT_CONFLICT同一个 Contract ID 被当作两种 kind 使用
CONFIG_INVALID配置未通过 Standard Schema。error.issues 是逐字段的问题列表

校验先于停机

所有受影响 Installation 的 Plugin 配置会在停止任何活动 Instance 之前全部校验完毕。一个拼错的字段不会让应用停在半路。

启动与活动执行

Code触发条件
SERVICE_NOT_RETURNEDprovides 声明了某个 key,但 setup 的返回值里没有
SERVICE_UNAVAILABLEhost.get() 在非 active 状态调用;或依赖的 Service 所属安装未处于活动状态
LIFETIME_DISPOSEDLifetime 已开始释放,因而拒绝新的监听、贡献、任务、子 Lifetime、cleanup 或 Event 发送
INSTALLATION_UNAVAILABLE安装处于 failed 状态;或 setup / 配置校验器抛出了非 Error 的值(原值在 cause 里);或在未提交的 draft 上操作
INSTALLATION_REMOVED在已移除的 Installation 上操作
INSTALLATION_IDENTITYupdate() 试图更换 Plugin 名称。更新可以换实现和配置,不能换身份

Group

Code触发条件
GROUP_REMOVED在已移除的 Group 上操作,或继续使用该 Group 删除前创建的旧 ChangeSet
GROUP_UNAVAILABLEGroup 尚未成功建立;或 Group 操作失败时抛出的是非 Error 值

Platform(@dougongjs/platform

信任边界

在加载任何外部模块代码之前抛出。

Code触发条件
MANIFEST_INVALIDManifest 形状非法,或声明了重复的激活事件 / 权限
API_INCOMPATIBLEManifest 要求的 apiVersion 不满足应用版本
PERMISSION_DENIEDAuthorizer 拒绝。error.denied 是被拒的权限列表
REGISTRATION_DUPLICATE两份 Artifact 的 Manifest 名称落入同一个注册身份

Manifest 依赖解析

这些错误来自 Manifest 声明在候选 Registration 图中的解析结果;错误码指向实际被验证的对象,而不是尚未加载的 Plugin。

Code触发条件
REGISTRATION_DEPENDENCY_MISSINGManifest 依赖没有对应 Registration
REGISTRATION_DEPENDENCY_INCOMPATIBLE依赖 Registration 的版本不满足范围
REGISTRATION_DEPENDENCY_INACTIVE依赖 Registration 存在但未激活
REGISTRATION_CYCLE候选 Registration 图中的 Manifest 依赖成环;消息带真实环路径

加载与激活

Code触发条件
MODULE_LOAD_FAILEDLoader 抛异常。原始错误在 cause
MODULE_INVALID模块加载成功但没有导出合法的 Plugin
ARTIFACT_IDENTITY同一份 Artifact 内部不自洽:Manifest 名称与 placeholder 或加载出的 Plugin 名称不一致
REGISTRATION_BUSY该 Registration 正在变更,或 Platform 结构变更已关闭新的根激活入口
REGISTRATION_UNAVAILABLERegistration 不可用;激活或 admission 抛出了非 Error 的值(原值在 cause 里);或在未提交的注册上操作
REGISTRATION_REMOVED在已移除的 Registration 上操作
REGISTRATION_IDENTITY更新 Registration 时,新 Artifact 的 Manifest 名称与原名称不同
PLATFORM_UNAVAILABLEPlatform 已释放,或处于不允许该操作的状态

三种 IDENTITY 的区别

它们描述的是三个不同对象的身份不变量:

  • INSTALLATION_IDENTITY —— 已存在的 Installation 想换 Plugin 名称(Core)
  • REGISTRATION_IDENTITY —— 已存在的 Registration 想换 Manifest 名称(Platform)
  • ARTIFACT_IDENTITY —— 一份 Artifact 自身的 Manifest 和它加载出的 Plugin 名字对不上(Platform,此时可能还不存在 Registration)

怎么处理

按 code 分流

ts
try {
  await installation.ready()
} catch (error) {
  if (!(error instanceof DougongError)) throw error

  switch (error.code) {
    case "CONFIG_INVALID":
      showFieldErrors((error as ConfigValidationError).issues)
      break
    case "SERVICE_MISSING":
      suggestInstallDependency(error.message)
      break
    case "INSTALLATION_UNAVAILABLE":
      offerRetry()
      break
    default:
      report(error)
  }
}

处理聚合错误

ts
try {
  await host.stop()
} catch (error) {
  if (error instanceof AggregateError) {
    for (const cause of error.errors) report(cause)
  }
}

接收后台错误

后台任务、监听器和诊断订阅者抛出的异常不会中断 Host 命令,它们通过 Host 的上报通道送出:

ts
const host = createHost({
  name: "app",
  onError: (error) => reportToSentry(error),
  logger: myLogger,          // onError 未提供或自身抛错时的兜底
})

上报通道本身是 fail-safe 的:onError 抛异常会退到 logger,logger 再抛就静默——错误观察永远不会改变它正在观察的 Host 命令

终态失败的信息量

Installation 脱离 Host 之后(被移除或丢弃),它只保留错误的 name / message,以及最小构造类别和可用的 code 纯数据摘要。读取时会重建正确的 DougongErrorTypeError;其他错误重建为普通 Error

原因是 JavaScript 的 Error.stack 可能携带创建错误时的整个编排调用帧,让一个历史对象反向保活整个 Host。

这不影响正常路径:等待 ready() 的调用方总是收到原始 Error;仍附着于活动 Host 的失败 Installation 也保留原始错误。只有「Installation 已脱离、且调用方没 await 过 ready()」的事后读取会拿到摘要——此时 ConfigValidationError.issues 这类子类附加数据不再可用。

终态 Registration 使用同一保留原则,并在摘要中额外记录 coded error 属于 Core 还是 Platform,以便重建正确的 DougongError / PlatformErrorTypeError 的调用者错误类别同样保留,子类专有字段则不进入摘要。它不会为了保留历史 stackcause 而反向保活 Installer、Loader 或 Platform。

相关

基于 MIT 许可证发布。