@shgroup/dsh-serenity-hooks 1.34.0 → 1.34.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +1 -0
  2. package/dsh.plugin.json +1 -1
  3. package/lib/acp-core.d.ts +2 -22
  4. package/lib/acp-http.d.ts +8 -0
  5. package/lib/api.d.ts +12 -4
  6. package/lib/autopilot-chain.d.ts +76 -0
  7. package/lib/autopilot-core-DCRkiWEe.js +229 -0
  8. package/lib/autopilot-core.d.ts +132 -0
  9. package/lib/autopilot-ops.d.ts +48 -0
  10. package/lib/autopilot-trajectory-D9Znxo75.js +782 -0
  11. package/lib/autopilot-trajectory.d.ts +64 -97
  12. package/lib/ccc-roots-Bd_SjEs7.js +152 -0
  13. package/lib/ccc-roots.d.ts +105 -0
  14. package/lib/ccc.d.ts +3 -6
  15. package/lib/client.js +2 -2
  16. package/lib/clock-runtime.d.ts +197 -0
  17. package/lib/config-ops.d.ts +9 -14
  18. package/lib/constants.d.ts +0 -2
  19. package/lib/container-status.d.ts +183 -0
  20. package/lib/diag-ops.d.ts +25 -10
  21. package/lib/face-host.d.ts +110 -0
  22. package/lib/fs-ops.d.ts +3 -2
  23. package/lib/gateway-auth.d.ts +2 -5
  24. package/lib/gateway.d.ts +0 -34
  25. package/lib/git-ops.d.ts +3 -2
  26. package/lib/handyman-ops.d.ts +3 -2
  27. package/lib/handyman-preset-inherit.d.ts +2 -1
  28. package/lib/host/access.d.ts +8 -7
  29. package/lib/host/contract.d.ts +17 -6
  30. package/lib/host/type-contract.d.ts +1 -2
  31. package/lib/im-bridge.d.ts +6 -5
  32. package/lib/index.js +8345 -5607
  33. package/lib/invariant.d.ts +0 -15
  34. package/lib/invariant.js +1 -1
  35. package/lib/kit-ops.d.ts +11 -31
  36. package/lib/localstore-ops.d.ts +3 -17
  37. package/lib/msm-ops.d.ts +34 -52
  38. package/lib/opencode-provider.d.ts +4 -5
  39. package/lib/output-guard-seam.d.ts +0 -3
  40. package/lib/output-guard.d.ts +12 -3
  41. package/lib/ports.d.ts +59 -0
  42. package/lib/rebuild.d.ts +1 -1
  43. package/lib/seams/bootstrap.d.ts +4 -3
  44. package/lib/seams/compact.d.ts +2 -1
  45. package/lib/seams/context.d.ts +2 -4
  46. package/lib/seams/env.d.ts +2 -1
  47. package/lib/seams/guards.d.ts +4 -43
  48. package/lib/seams/keeper.d.ts +1 -1
  49. package/lib/seams/system-prompt.d.ts +1 -5
  50. package/lib/session-cleanup.d.ts +3 -2
  51. package/lib/{settings-section-BuPGQv_H.js → settings-section-DTYZjwUs.js} +65 -9
  52. package/lib/settings-section.d.ts +3 -31
  53. package/lib/skiff-core.d.ts +4 -9
  54. package/lib/skiff-debug.d.ts +15 -21
  55. package/lib/skiff-registry-FTjoWQTQ.js +25 -0
  56. package/lib/skiff-registry.d.ts +2 -1
  57. package/lib/{skiff-role-DAfPb6YQ.js → skiff-role-CEHL5cek.js} +1 -1
  58. package/lib/skiff-role.d.ts +2 -2
  59. package/lib/skills/opencode-scan.d.ts +3 -2
  60. package/lib/skills-discovery.d.ts +15 -2
  61. package/lib/status.d.ts +2 -1
  62. package/lib/tools/cce.d.ts +1 -2
  63. package/lib/tools/container-admin.d.ts +7 -3
  64. package/lib/tools/eap.d.ts +2 -3
  65. package/lib/tools/handyman.d.ts +0 -29
  66. package/lib/tools/msm.d.ts +0 -4
  67. package/lib/tools/neat.d.ts +1 -2
  68. package/lib/tools/praxis.d.ts +0 -1
  69. package/lib/tools/skiff-admin.d.ts +2 -2
  70. package/lib/tools/trajectory.d.ts +7 -15
  71. package/lib/totp.d.ts +0 -4
  72. package/lib/trajectory-assistant.d.ts +2 -1
  73. package/lib/trajectory-bound-Cax1p9ut.js +190 -0
  74. package/lib/trajectory-bound.d.ts +3 -4
  75. package/lib/{trajectory-bound-C8DUxi6_.js → trajectory-ops-mrXK0H0I.js} +2 -159
  76. package/lib/trajectory-ops.d.ts +4 -9
  77. package/lib/trajectory-skills.d.ts +71 -0
  78. package/lib/{wake-registry-D0Vj0WXb.js → wake-registry-CYxIb484.js} +7 -20
  79. package/lib/wake-registry.d.ts +3 -11
  80. package/lib/wake-scheduler.d.ts +13 -49
  81. package/lib/web-fetch-provider.d.ts +0 -21
  82. package/lib/{weixin-api-PjEhmjlZ.js → weixin-api-CtFYm53S.js} +3 -9
  83. package/lib/weixin-api.d.ts +6 -37
  84. package/lib/weixin-bridge.d.ts +5 -24
  85. package/lib/weixin-hook.d.ts +6 -12
  86. package/lib/weixin-output-guard.d.ts +2 -1
  87. package/lib/{weixin-route-D1ND823E.js → weixin-route-B2ajFypI.js} +4 -5
  88. package/lib/weixin-route.d.ts +0 -2
  89. package/lib/weixin-send-api.d.ts +15 -6
  90. package/package.json +2 -4
  91. package/experiments/autopilot-trajectory/SKILL.md +0 -115
  92. package/experiments/autopilot-trajectory/scripts/autopilot-trajectory.ts +0 -546
  93. package/lib/autopilot-script.d.ts +0 -40
  94. package/lib/skiff-debug-D83-Njy7.js +0 -2867
  95. package/lib/{ccc-eu_IPF94.js → ccc-NlLr_sxy.js} +1 -1
  96. package/lib/{session-cleanup-CMT4mHWt.js → session-cleanup-B6IHZgww.js} +1 -1
@@ -0,0 +1,197 @@
1
+ /**
2
+ * clock-runtime.ts — **时钟运行时工厂**(S142 §38 复审候选 C6b,路径 **P2**)
3
+ *
4
+ * 本仓有**两条各自独立运行**的 5min 时钟:
5
+ * · 唤醒调度器(`wake-scheduler.ts`,一次性唤醒,D58)
6
+ * · autopilot 时钟(`autopilot-trajectory.ts`,周期自唤醒,D59)
7
+ * 二者的**业务判据完全不同**(谁来扫 / 扫什么 / 投递给谁 / 按什么周期),但**引擎骨架逐字同形**
8
+ * ——进程态 + 快照 + 复位 + `startTimer` + tick 外壳 + 事件接线 + disposer + `timer.unref()`。
9
+ * 本条把**引擎**抽到本工厂,**各钟仍各持自己的定时器与自己的串行链**。
10
+ *
11
+ * ───────────────────────────────────────────────────────────────────────────
12
+ * 🔴 硬约束一:**两条时钟绝不可共用"排队执行"的链**(这是 P2 与 P1 的分界判据)
13
+ *
14
+ * autopilot 每轮要跑该 CCC 的**偏见脚本**(`fetchBiasContent`,超时 60s),可能阻塞数十秒;
15
+ * 若与唤醒调度器共用一条串行链,会把另一条一起**卡住**(一次性唤醒被无关的偏见脚本拖住)。
16
+ * ⇒ 本工厂把 `chain` 作为**每实例私有状态**:`createClock()` 调用一次 = 一条独立的串行域。
17
+ * 实例之间**零共享**(无模块级单例、无共享 Promise、无共享 map)。
18
+ * ⚠️ 若日后有人把 `chain` 提到模块级"省一次分配",**两条时钟的隔离即刻失效**——不许。
19
+ *
20
+ * 🔴 硬约束二:**武装门只判全局闸**(`gate()`),**不判"本次有无目标"**
21
+ *
22
+ * 2026-09-14 F 段实测缺陷:旧实现把"有 live CCC"当**武装前置条件**,而宿主刚重启时 live 会话
23
+ * 必为空,且**"恢复旧会话"不触发 `session/created`**(只有新建会)⇒ 启动瞬间判定落空 =
24
+ * **时钟永久不武装**(实测 6.6h 零 tick)。⇒ 现在「**全局闸开即武装**」,"有无目标"降级为
25
+ * tick 内的廉价判定。**武装与"本次有无目标"是两件事,本工厂不得把二者耦合。**
26
+ *
27
+ * 🔴 硬约束三:**两条闸必须继续分开传**(v1.34 S-1 解耦)
28
+ *
29
+ * 唤醒调度器 `wakeSchedulerEnabled`(**缺省开**)|autopilot `autopilotWakeEnabled`
30
+ * (**缺省关**)——取值方向相反。工厂取 `gate` **回调**(每次现读),**不做**任何合并或缓存。
31
+ *
32
+ * ───────────────────────────────────────────────────────────────────────────
33
+ * 两条时钟**逐项语义差**(`ClockOptions` 的每个可配项都对应下表一行,不是随意的灵活性):
34
+ *
35
+ * | 项 | 唤醒调度器 | autopilot | 工厂表达 |
36
+ * |----|-----------|-----------|---------|
37
+ * | `chain` | 有(全局串行) | 有(全局串行) | **总是私有**(硬约束一) |
38
+ * | `lastTickLog` | 有(逐行摘要) | **无** | `logFrom` 不给 = 不记 |
39
+ * | `ticks` 记账 | 工厂在 body **完成后**记 | **本钟**在同步段记 | `bodyCountsTick` + `begin` |
40
+ * | body 返回类型 | `string[]`(人读摘要) | `void` | 泛型 `T` |
41
+ * | 启动日志 | 固定串 | 含启用 CCC 计数 | `startLog: () => string` |
42
+ * | body 异常兜底 | 留痕 + `console.warn` | 同 | 工厂统一(**吞异常不改语义**) |
43
+ *
44
+ * ───────────────────────────────────────────────────────────────────────────
45
+ * **本工厂是"加法式"重构**(P2 的性质):不新增任何判定逻辑,只把同形骨架收成一份代码;
46
+ * 两个模块仍各自导出 `wakeSchedulerState()` / `autopilotClockState()` 与各自的
47
+ * `__reset…ForTest()`,**对外可观测面(字段名与语义)零变化**——`acc-diag` ①b 段、
48
+ * `container-status.ts:containerClocks()` 与面板都在读它们(见该文件 :225–250 的设计红线)。
49
+ *
50
+ * 为什么**不**取 P1(真合并成一个 `setInterval` + 两条策略)——2026-09-15 所有者裁决:
51
+ * P1 只多省 ~20 行,却要把 `armed` 拆成"宿主 + 每策略"两层、并强行分开串行域,
52
+ * 即**动两条正在跑的时钟的骨架**;**不值当**(裁决原文见 SESSION §12.8②)。
53
+ */
54
+ /**
55
+ * 时钟**进程态**(诊断用,模块级 = 进程级,正是诊断对象)。
56
+ *
57
+ * 字段名与语义是**对外契约**(`acc-diag` / `containerClocks` 在读),不得因本重构改名或改义。
58
+ * `lastTickLog` 只有唤醒调度器消费(autopilot 无对应字段),故这里是**可选**。
59
+ */
60
+ export interface ClockRuntime {
61
+ /** 定时器是否在跑(start 成功 → true;disposer 拆卸 → false) */
62
+ armed: boolean;
63
+ /** 武装时刻(ms;null = 从未) */
64
+ armedAt: number | null;
65
+ /** 上次真正执行 tick 的时刻(ms;null = 从未——被闸跳过的 tick 不刷新它) */
66
+ lastTickAt: number | null;
67
+ /** 真正执行过多少次 tick */
68
+ ticks: number;
69
+ /** 上次被跳过 / 异常的原因(null = 上次正常执行) */
70
+ lastSkipReason: string | null;
71
+ /** 上次 tick 的逐行摘要(**可选**:只有给了 `render` 的钟才有此字段) */
72
+ lastTickLog?: string[];
73
+ }
74
+ /**
75
+ * 一个钟实例(= 一个定时器 + 一条**私有**串行链 + 一份进程态)
76
+ */
77
+ export interface Clock {
78
+ /** 幂等启动:已武装则原样返回;`gate()` 为假则不武装(零资源占用);成功则立即跑一次 tick */
79
+ start: () => void;
80
+ /** 拆卸:清定时器 + 复位 armed/armedAt + 跑 `onDispose` */
81
+ dispose: () => void;
82
+ /** 进程态快照(只读;`enabled` = `gate()` 的**当前**值,便于区分"未武装"与"闸关") */
83
+ snapshot: () => ClockRuntime & {
84
+ enabled: boolean;
85
+ };
86
+ /**
87
+ * body 内部留痕"本次跳过原因"(`null` = 本次正常执行)。
88
+ *
89
+ * 为什么需要它:**"没有可扫目标"是 body 才知道的事**(autopilot 的 `无 live+enabled CCC…`
90
+ * 与唤醒调度器的 `无已知 CCC 可扫…` 判定都在 body 内),但该字段是进程态的一部分 ⇒
91
+ * 由 body 经本方法写回,工厂不再替 body 猜。**只有 body 能调**(引擎自身走 `gateOffReason`)。
92
+ */
93
+ noteSkipReason: (reason: string | null) => void;
94
+ /**
95
+ * 把一段**在飞工作**排到**本钟私有**串行链的尾部(不经 tick 包装:不记账、不写 lastTickLog、不吞异常)。
96
+ *
97
+ * 何时用它:body 在一次 tick 里为**多个目标**各派一件活儿,这些活儿要**依次**跑(防模型并发挤兑),
98
+ * 而它们在**本次 tick 返回之后**仍在飞(autopilot 即此:同 tick 多 CCC 依次唤起)。
99
+ * 若 body 自己另起一条链,就是**第二条串行域**——本钟的"串行"会有两处真相。
100
+ *
101
+ * ⚠️ **风险由调用方担**:本方法**不** try/catch(与 `body` 相反)。调用方必须自带兜底
102
+ * (autopilot 的每件活儿都有自己的 try/catch/finally)——否则一个异常会**毒化整条链**,
103
+ * 该钟此后的 `enqueue` 与 tick 都会静默不执行。
104
+ */
105
+ enqueue: (work: () => Promise<void> | void) => void;
106
+ /**
107
+ * 记一次 tick(`ticks += 1` + `lastTickAt = now`)。
108
+ *
109
+ * **只有 `bodyCountsTick: true` 时才由 body 调用**(autopilot);缺省时工厂在 body 完成后
110
+ * 自动调用,body 不该再调(会重复计数)。见 {@link ClockOptions.bodyCountsTick}。
111
+ */
112
+ countTick: () => void;
113
+ /** 测试用:复位进程态(避免用例间串味) */
114
+ reset: () => void;
115
+ }
116
+ /** `createClock` 的可配项(每一项都对应文件头语义差表的一行;**没有"顺手加"的开关**) */
117
+ export interface ClockOptions<T> {
118
+ /** 本钟的**唯一**诊断名(进异常日志,用于人读定位) */
119
+ label: string;
120
+ /**
121
+ * 宿主上下文(**只读它的 `on`**)。装配时赋值(`register*` 的第一件事),
122
+ * `body` / `startLog` / 热启动接线都从**这一个格子**读——避免"三处各存一份 ctx"。
123
+ * 装配前为 `undefined`:那时也没有定时器(tick 无从发起),故不会被读到。
124
+ *
125
+ * 类型取**结构最小面**(不是 `cordis.Context`):本工厂不该依赖宿主的强类型事件表
126
+ * (`on` 在 cordis 上是泛型重载,比 `(name: string, fn) => void` **更窄** ⇒ 直接传 Context
127
+ * 反而不兼容)。装配点传 `ctx` 即可。
128
+ */
129
+ ctx: unknown;
130
+ /** 热启动触发面事件名(回调恒为 `() => start()`——两条钟的触发面同形,不开放自定义回调) */
131
+ events?: string[];
132
+ /** 全局闸:**每次现读**(不缓存——面板改开关后要能立刻反映)+ 武装门**只**判它 */
133
+ gate: () => boolean;
134
+ /** body 被**闸**跳过时写入 `lastSkipReason` 的文案(文案归各钟,工厂不编) */
135
+ gateOffReason: string;
136
+ /**
137
+ * **同步前置阶段**(在 body 被调用**之前**、同一个 tick 内同步执行)。
138
+ *
139
+ * 存在的理由不是"方便",而是**既有行为要求**(实测被两条回归钉守住):
140
+ * autopilot 的 `ticks` / `lastSkipReason` 是在 tick **同步段**里定下来的——
141
+ * · 枚举到 live+enabled CCC ⇒ 立刻 `countTick()`(早于任何 `await`);
142
+ * · 枚举为空 ⇒ 立刻 `noteSkipReason('无 live+enabled CCC…')` 并**不计 tick**。
143
+ * 若把这些挪进 body(async),观测面会在"启动即 tick"的那一瞬间读到**旧值**
144
+ * (`ticks` 停留在 0、`lastSkipReason` 仍是上一拍的)——诊断面说了假话。
145
+ *
146
+ * 唤醒调度器**不用**本项(它的 `ticks` 在 body 完成后由工厂记,见 `bodyCountsTick`)。
147
+ */
148
+ begin?: () => void;
149
+ /**
150
+ * 一次 tick 的**业务体**(引擎不关心里面做什么;返回值交给 `logFrom`)
151
+ */
152
+ body: () => Promise<T> | T;
153
+ /**
154
+ * `ticks` / `lastTickAt` 的**记账时机**(两条钟真的不同,见文件头语义差表):
155
+ *
156
+ * `false`(缺省)= **工厂在 body 完成后**记账 —— 唤醒调度器语义:
157
+ * "tick 数 = 真执行**并完成**过的 tick"。但 `body` 提前返回("无 CCC 可扫")时也照样记账
158
+ * (wake 的既有行为就是如此:`ticks` ≠ "真投递过的 tick")。
159
+ *
160
+ * `true` = **body 自己调 {@link Clock.countTick} 记账** —— autopilot 语义:
161
+ * `ticks`/`lastTickAt` 在**枚举到 live+enabled CCC 之后、任何 await 之前**同步记账;
162
+ * "无 CCC 可扫" 的那一拍**不计**(既有行为:`ticks` 停在 0 直到真有 CCC 可唤起)。
163
+ * 若把它交给工厂"body 前同步记账",就会**在闸之后无条件 +1**——那会把
164
+ * "无 CCC 可扫时不计数"这条既有语义改掉(实测:`autopilot-trajectory.test.ts`
165
+ * 的『启动时无 live 会话』与『配置关闭』两条回归钉立刻变红)。
166
+ *
167
+ * ⚠️ 故本项**不是**"记账早晚"的风格开关,而是"**谁**知道该不该记"的归属划分:
168
+ * 知道"有没有目标"的只有 body ⇒ 由 body 决定何时记账。
169
+ */
170
+ bodyCountsTick?: boolean;
171
+ /**
172
+ * 本钟是否记 tick 日志(`lastTickLog`)。
173
+ * 给了 `logFrom` **才**有此字段与逐行打印——autopilot **没有** `lastTickLog`(现状稿 §2.4),
174
+ * 故它不传本项(情形分布不同:autopilot 的结果进 `wakeHistory` 审计 ring,不走 tick 日志)。
175
+ */
176
+ logFrom?: {
177
+ prefix: string;
178
+ lines: (value: T) => string[];
179
+ };
180
+ /** 启动成功的日志行(autopilot 需在里面报启用 CCC 计数,故做成回调) */
181
+ startLog: () => string;
182
+ /** 启动时是否立刻跑一次 tick(两条钟皆为 true;留成显式项以免日后被默默改掉)@default true */
183
+ immediate?: boolean;
184
+ /** 拆卸回调(工厂负责清定时器与复位进程态;本项是**额外**清理,如 autopilot 的 `runningByRoot`) */
185
+ onDispose?: () => void;
186
+ /** 测试复位回调(如 autopilot 顺带 `runningByRoot.clear()`) */
187
+ onReset?: () => void;
188
+ }
189
+ /**
190
+ * 建一条时钟(**每钟各调一次**——本工厂**不是**单例,调用次数 = 时钟条数)。
191
+ *
192
+ * 顺序忠实于既有实现:`start()` 先武装并立即跑一次 tick → 再挂事件。
193
+ * 拆卸由调用方经 `registerDisposer(ctx, label, clock.dispose)` 落到宿主的 `ctx.effect`。
194
+ * @param opts 见 {@link ClockOptions}(每项对应一条既有的语义差)
195
+ * @returns 该钟的 `{start, dispose, snapshot, reset}`
196
+ */
197
+ export declare function createClock<T>(opts: ClockOptions<T>): Clock;
@@ -17,7 +17,7 @@
17
17
  /** 高级设定节名(localstore.json 顶层) */
18
18
  export declare const ADVANCED_SECTION = "serenityAdvanced";
19
19
  /** 一个外部访问账号(密码只存 hash,永不落 wire) */
20
- export interface GatewayAccount {
20
+ interface GatewayAccount {
21
21
  /** 稳定键(UI CRUD 定位) */
22
22
  id: string;
23
23
  /** 登录用户名 */
@@ -28,7 +28,7 @@ export interface GatewayAccount {
28
28
  totpSecret?: string;
29
29
  }
30
30
  /** F1 双端口网关配置 */
31
- export interface GatewaySettings {
31
+ interface GatewaySettings {
32
32
  enabled: boolean;
33
33
  host: string;
34
34
  port: number;
@@ -45,7 +45,7 @@ export interface GatewaySettings {
45
45
  /** 彩蛋功能:persona 模式(v1.23.1,S142 用户需求)
46
46
  * 配置后替换 ACC 系统提示词中"输出约束/指令遵循约束"部分(EAP 块 + MSM 原则段);
47
47
  * 未配置(mode 空)→ 完全默认行为,零影响。 */
48
- export interface PersonaSettings {
48
+ interface PersonaSettings {
49
49
  /** 彩蛋模式名(显示用;空 = 彩蛋关闭) */
50
50
  mode: string;
51
51
  /** 用户替换文本(替代 EAP 块 + MSM 原则段的原文) */
@@ -54,7 +54,7 @@ export interface PersonaSettings {
54
54
  /** F4d 建议问答页(v1.26.1,S142 用户:按认知容器暴露问答页供他人验证):
55
55
  * 与 ACP HTTP 共用 3100 端口;key 首次启用自动生成(plugin 全局文件固定),无 key 不工作。
56
56
  * v1.26.2:按容器权限控制——allowed 白名单(容器名);空 = 全部开放(向后兼容 v1.26.1 全局开放) */
57
- export interface PublicAskSettings {
57
+ interface PublicAskSettings {
58
58
  /** 访问 key(空 = 未生成;首次启用时 ensurePublicAskKey 自动生成随机 key 写回固定) */
59
59
  key: string;
60
60
  /** 开放容器白名单(CCC 目录名,如 home-serenity);空数组 = 全部容器开放 */
@@ -67,7 +67,7 @@ export interface PublicAskSettings {
67
67
  * 旧高级 rebuild 字段是死双胞胎(/serenity/config PUT 回显但运行时从不读它)——
68
68
  * 删除防误导(用户在高级面板改了却无效)。历史残留字段被 mergeWithDefaults 忽略(幂等安全)。
69
69
  */
70
- export interface AdvancedSettings {
70
+ interface AdvancedSettings {
71
71
  gateway: GatewaySettings;
72
72
  persona: PersonaSettings;
73
73
  publicAsk: PublicAskSettings;
@@ -78,7 +78,7 @@ export interface AdvancedSettings {
78
78
  * 主动发送入口配置(v1.30.9,S142 用户需求"微信桥支持被调用发消息")。
79
79
  * 只监听 loopback(公网网关 3081 不可达),故不做密钥;`enabled:false` 或 `port:0` 关闭。
80
80
  */
81
- export interface WeixinApiSettings {
81
+ interface WeixinApiSettings {
82
82
  enabled: boolean;
83
83
  port: number;
84
84
  }
@@ -113,12 +113,6 @@ export declare function verifyPublicAskKey(provided: string | undefined): boolea
113
113
  * @returns 新 key(面板展示给管理员,重新分享给使用者)
114
114
  */
115
115
  export declare function rotatePublicAskKey(): string;
116
- /** 失败锁定阈值(连续失败次数) */
117
- export declare const PUBLIC_ASK_FAIL_THRESHOLD = 5;
118
- /** 首次锁定基础时长(指数退避底数) */
119
- export declare const PUBLIC_ASK_LOCK_BASE_MS: number;
120
- /** 锁定上限 */
121
- export declare const PUBLIC_ASK_LOCK_MAX_MS: number;
122
116
  /** 重置 IP 失败状态(测试/管理员解封) */
123
117
  export declare function resetPublicAskIpFail(ip: string): void;
124
118
  /** IP 当前是否锁定(到期自动解锁) */
@@ -133,7 +127,7 @@ export declare function recordPublicAskFail(ip: string): number;
133
127
  */
134
128
  export declare function migrateLegacyLocalstore(root: string | null): boolean;
135
129
  /** 账号的 wire 形态:只有 id/user + hasPassword/hasTotp(无 hash/secret) */
136
- export interface GatewayAccountWire {
130
+ interface GatewayAccountWire {
137
131
  id: string;
138
132
  user: string;
139
133
  hasPassword: boolean;
@@ -141,7 +135,7 @@ export interface GatewayAccountWire {
141
135
  hasTotp: boolean;
142
136
  }
143
137
  /** 设定 wire 形态(GET /serenity/config 返回;gateway 去 hash) */
144
- export interface AdvancedSettingsWire {
138
+ interface AdvancedSettingsWire {
145
139
  gateway: {
146
140
  enabled: boolean;
147
141
  host: string;
@@ -179,3 +173,4 @@ export declare function projectKnownWorkspaces(workspaces: Array<{
179
173
  * 新账号(id 不在现有)必须带非空 pass,否则抛错(无法生成 hash)。
180
174
  */
181
175
  export declare function applyWirePatch(wire: Partial<AdvancedSettingsWire>): AdvancedSettings;
176
+ export {};
@@ -1,6 +1,4 @@
1
1
  /** 常量(纯模块,零 DSH 依赖) */
2
- /** 插件 ID */
3
- export declare const PLUGIN_ID = "dsh-serenity-hooks";
4
2
  /**
5
3
  * ACC 版本:自动从 package.json 读取(单一真相源,消除与 CHANGELOG 的漂移)。
6
4
  * 发布时只需改 package.json 的 version。
@@ -0,0 +1,183 @@
1
+ /**
2
+ * container-status.ts — 容器状态模型的**取数层唯一出口**(C5「观察面归一」,S142 2026-09-15)
3
+ *
4
+ * 为什么存在(R↓):ACC 有多个观察面(`dashboard health` / `acc-diag` / WebUI 面板 /
5
+ * 系统提示词注入),它们读的是同一批宿主事实。此前"这些事实怎么取"写在**每个消费方各自的
6
+ * 函数体里**——同一份状态有几个读者就有几处取数代码。本模块把取数收成一个出口,消费方只留
7
+ * **渲染**(薄渲染)。
8
+ *
9
+ * 本模块**只做取数 + 形状**(设计红线):
10
+ * - **文案/格式归渲染点**。本模块给判据与事实(`rooted: true` / `configPath: '.opencode/serenity.json'`),
11
+ * **不给**人读句子("✓ .serenity marker found")。同一份事实在 `dashboard health` 与 `acc-diag`
12
+ * 里的措辞不同,正是因为它属渲染。
13
+ * - **不合并两条 registry 判据**(见 {@link checkRegistryHealth} / {@link checkRegistryQuality} 的对照注释)。
14
+ * - **时钟读进程内模块级快照**({@link containerClocks})——**不重算**:调度器的武装态/计数只存在于
15
+ * 其模块级运行时对象里,任何"重新推导"得到的都是与真实调度器不一致的假值。
16
+ *
17
+ * 依赖分层(为什么本模块"重"而 `kit-ops` / `status` 保持"轻"):
18
+ * {@link containerClocks} 必须读 `wake-scheduler` / `autopilot-trajectory` 的进程内快照,而这两个
19
+ * 模块带 **@deepseek-ai 值依赖**(`dsh-llm` 的 `createUserMessage`)。故本模块**只能**被本就重型或
20
+ * 异步的消费方使用:`diag-ops`(静态 import)、`api.ts` / `kit-ops`(`await import(...)`,与
21
+ * `api.ts` 既有的"保持静态链纯净"约定一致)。**轻链模块不得静态 import 本模块**。
22
+ *
23
+ * 已有的单一真相源继续指向原处(**不复制**):
24
+ * L0 根解析 `ccc.ts findSerenityRoot`|L1/L2 根与枚举 `ccc-roots.ts`|宿主契约 `host/contract.ts`
25
+ * |CCC 配置 `ccc.ts loadSerenityConfig`|注册表 `msm-ops.ts`|时钟 `wake-scheduler.ts` /
26
+ * `autopilot-trajectory.ts`|唤醒注册表 `wake-registry.ts`。
27
+ */
28
+ import { type HostContractReport } from './host/contract.js';
29
+ import { checkRegistryQuality, type RegistryQualityReport } from './msm-ops.js';
30
+ import { type WakeEntry } from './wake-registry.js';
31
+ import { autopilotClockState, getAutopilotStatus } from './autopilot-trajectory.js';
32
+ import { wakeSchedulerState } from './wake-scheduler.js';
33
+ import type { JsonValue } from './json.js';
34
+ export interface ContainerIdentity {
35
+ /** CCC 根(调用方给定;null = 不在任何 CCC 内) */
36
+ root: string | null;
37
+ /** CCC 名(`.serenity` 首行;无根或读不到 → null) */
38
+ ccc: string | null;
39
+ /** ACC 插件版本 */
40
+ accVersion: string;
41
+ /** 已安装 DSH CLI 版本(读不到 → null) */
42
+ dshVersion: string | null;
43
+ /** node 版本(`process.version`) */
44
+ nodeVersion: string;
45
+ }
46
+ /** 身份/版本段取数(版本自省是磁盘/常量事实,与 CCC 内容无关) */
47
+ export declare function containerIdentity(root: string | null): ContainerIdentity;
48
+ export interface ContainerPrinciples {
49
+ /** P1:`.serenity` 存在且非空 */
50
+ rooted: boolean;
51
+ /** P2:位于某个 git 仓库内 */
52
+ gitManaged: boolean;
53
+ /** P2 的证据(git 根绝对路径;null = 不是 git 仓库) */
54
+ gitRoot: string | null;
55
+ /** P3:命中的 DSH/opencode 配置路径(相对 CCC 根);null = 全部未命中 */
56
+ configPath: string | null;
57
+ /** 三项全过(health 的 healthy/degraded 判据之一) */
58
+ allPass: boolean;
59
+ }
60
+ /**
61
+ * 三原则取数。
62
+ * ⚠️ 与 `ccc.ts` 的 P1/P2 原语是**同一实现**(`existsSync(.serenity)` / `findGitRoot`),
63
+ * 本函数只把它们组装成"一次检查"的形状;判定语义未变(P1 要求文件非空、P2 要求 `.git` 存在)。
64
+ */
65
+ export declare function containerPrinciples(root: string | null): ContainerPrinciples;
66
+ export interface RegistryStructureReport {
67
+ /** 聚合档路径(相对 CCC 根);无 cccName → null */
68
+ path: string | null;
69
+ ok: boolean;
70
+ /** 该注册表文件是否存在(不存在 = 未注册任何 MSM,非坏) */
71
+ present: boolean;
72
+ issues: string[];
73
+ }
74
+ /**
75
+ * **判据 A —— 注册表「结构完整性」**(原 `kit-ops.ts checkRegistryHealth`,C5 迁入本模块)。
76
+ *
77
+ * 回答的问题:**这张表坏没坏**——文件能不能被安全解析(JSON 合法 / 顶层形状 / entry 字段类型 /
78
+ * name 全局唯一 / path 在根内且脚本存在)。
79
+ *
80
+ * 🔴 **为什么不与 {@link checkRegistryQuality} 合并**(设计红线,取证见
81
+ * `acc-c4c5-current-state.md` §⑥ 与 §⑤ 观察面 4/5):两条判据的**主语不同**——
82
+ * - A 的主语是**文件**:"解析它会不会炸"。它**绝不**因为某个 entry 的脚本缺测试文件而报 issue;
83
+ * - B 的主语是**条目契约**:"这条 entry 符合 MSM 的质量规约吗"(DC-M1~M4)。
84
+ *
85
+ * 因此**同一份注册表上两条判据可以给出相反结论**(例:结构完全合法,但没有一条 entry 达标;
86
+ * 或条目全达标,而表里有同名重复 entry)。合并成一个 `ok` 会把"表坏了(要 git restore)"与
87
+ * "表没坏但条目欠火候(去写测试)"两种**修复动作完全不同**的故障压成同一个信号。
88
+ *
89
+ * health 必须**不因坏表抛错**(注册表坏 → `loadMsmEntries` 抛 → `container_admin`/`output-guard`
90
+ * 全崩且自锁无法自救),故本判据独立解析,不走 `loadMsmEntries`。
91
+ */
92
+ export declare function checkRegistryHealth(root: string): RegistryStructureReport;
93
+ /**
94
+ * **判据 B —— 注册表「质量契约」**(DC-M1~M4;实现在 `msm-ops.ts checkRegistryQuality`,
95
+ * 执行面 `container_admin msm check` 亦走它)。
96
+ *
97
+ * 与判据 A 的关系见 {@link checkRegistryHealth} 的对照注释——**两条判据、两个名字、两份结论**。
98
+ * 本模块只做具名转出(取数出口唯一),不复制其判据实现。
99
+ */
100
+ export { checkRegistryQuality, type RegistryQualityReport };
101
+ /**
102
+ * 进程内时钟快照类型。
103
+ * ⚠️ 两个时钟的形状由**各自模块**定义(`WakeSchedulerRuntime` / `AutopilotClockRuntime`,
104
+ * 两者都未导出且 `autopilot` 少一个 `lastTickLog` 字段)——故此处取**联合**:它们对外暴露的
105
+ * 诊断字段(armed / enabled / ticks / lastTickAt / armedAt / lastSkipReason)语义同规格,
106
+ * 但类型上不是同一个。**不为了合并类型去改那两个模块的导出面**。
107
+ */
108
+ export type ClockSnapshot = ReturnType<typeof wakeSchedulerState> | ReturnType<typeof autopilotClockState>;
109
+ export interface ContainerClocks {
110
+ /** 唤醒调度器(一次性唤醒,5min tick)——**模块级快照**,不重算 */
111
+ wake: ClockSnapshot;
112
+ /** autopilot 时钟(周期自唤醒 tick)——**模块级快照**,不重算 */
113
+ autopilot: ClockSnapshot;
114
+ }
115
+ /**
116
+ * 两条时钟的进程内状态("为什么这轮没被唤起"的第一手判据)。
117
+ *
118
+ * 🔴 **只读快照,不重算**(设计红线):`armed`/`ticks`/`lastTickAt`/`lastSkipReason` 只存在于
119
+ * 调度器自己的模块级运行时对象里(`wake-scheduler.ts schedulerRuntime` /
120
+ * `autopilot-trajectory.ts` 同规格对象)。任何"从配置或文件重新推导"得到的值都会与真实
121
+ * 调度器的状态不一致——那比没有可观测量更糟(会给出假的"一切正常")。
122
+ */
123
+ export declare function containerClocks(): ContainerClocks;
124
+ export interface ContainerWakes {
125
+ /** 注册表文件读取问题(读不到时非空——不得静默) */
126
+ error: string | null;
127
+ /** 全量条目(原样,按 `at` 升序;**投影由各渲染点决定**——面板要 `message`,诊断不要) */
128
+ entries: WakeEntry[];
129
+ /** 在办(`state === 'pending'`)条目数 */
130
+ pending: number;
131
+ }
132
+ /** 唤醒注册表取数(文件 IO:每个渲染点自己决定是否要看它) */
133
+ export declare function containerWakes(root: string): ContainerWakes;
134
+ /**
135
+ * autopilot 目标状态取数(读 CCC 配置 + 目标 SESSION.md 的 mtime 探测)。
136
+ * 与 WebUI 面板 / `acc-diag` 一致——两边渲染的是**同一份** `getAutopilotStatus()` 结果。
137
+ * 类型从该函数的返回类型派生(`AutopilotTrajectoryStatus` 未从 `autopilot-trajectory.ts` 导出,
138
+ * 此处不为了转出而改动那个模块的导出面)。
139
+ */
140
+ export type AutopilotStatusSnapshot = ReturnType<typeof getAutopilotStatus>;
141
+ /** @returns CCC 根缺失(null)→ null */
142
+ export declare function containerAutopilot(root: string | null): AutopilotStatusSnapshot | null;
143
+ export interface ContainerStatus {
144
+ identity: ContainerIdentity;
145
+ principles: ContainerPrinciples;
146
+ /**
147
+ * 注册表**两条判据**(各自命名、不许合并——见 `checkRegistryHealth` / `checkRegistryQuality`)。
148
+ */
149
+ registry: {
150
+ /** 判据 A 结构完整性(**总是**算:health 的核心,成本 = 一次 JSON 解析) */
151
+ structure: RegistryStructureReport;
152
+ /**
153
+ * 判据 B 质量契约 DC-M1~M4(**默认不算**:要枚举 `skills/` 下脚本并逐个读源码查 main() 守卫,
154
+ * 是唯一有实打实成本的段)——`withRegistryQuality: true` 才取。
155
+ */
156
+ quality: RegistryQualityReport | null;
157
+ };
158
+ /** CCC 配置(`ccc.ts loadSerenityConfig`;无根 → null) */
159
+ config: JsonValue | null;
160
+ /** 宿主契约(进程内唯一来源,见 `host/contract.ts hostContractReport`) */
161
+ hostContract: HostContractReport | null;
162
+ }
163
+ export interface ContainerStatusOptions {
164
+ /** CCC 根;null = 不在任何 CCC 内(仍返回降级形状,不抛错) */
165
+ root?: string | null;
166
+ /** CCC 配置候选路径(缺省 `DEFAULT_SERENITY_CONFIG_PATHS`) */
167
+ configPaths?: string[];
168
+ /** 是否取判据 B(注册表质量契约);缺省 false */
169
+ withRegistryQuality?: boolean;
170
+ /**
171
+ * 宿主契约**兜底**捕获用的上下文:快照已存在(apply 时已捕获)时此参数被忽略,
172
+ * 进程内**不会**发生第二次探针。缺省 undefined = 只要快照(拿不到就是 null)。
173
+ */
174
+ hostCtx?: unknown;
175
+ }
176
+ /**
177
+ * 核心段组装(`dashboard health` 的取数:身份 + 三原则 + 注册表两判据 + 配置 + 宿主契约)。
178
+ *
179
+ * 时间轴三段(时钟/唤醒注册表/autopilot 目标)**不在本组装内**:它们是各自独立消费方的需要
180
+ * (`acc-diag` / `/serenity/trajectory`),硬塞进来会让只想看三原则的调用方付无谓的 IO。
181
+ * 需要时按名字调 {@link containerClocks} / {@link containerWakes} / {@link containerAutopilot}。
182
+ */
183
+ export declare function containerStatus(opts?: ContainerStatusOptions): ContainerStatus;
package/lib/diag-ops.d.ts CHANGED
@@ -9,18 +9,28 @@
9
9
  *
10
10
  * 四段内容(数据来源各自单一真相源,本模块只聚合不复制语义):
11
11
  * ① live 运行态 —— 插件进程内 `diagLive(ctx)`(live 会话清单 + 各 autopilot CCC 的
12
- * 目标命中 / agent 定位诊断)——**脚本侧看不到运行时**,这正是本工具存在的理由
12
+ * 目标命中 / agent 定位诊断)——**独立进程看不到运行时**,这正是本工具存在的理由
13
13
  * ② 面板解析 —— `diagLive.panelResolved`(无参面板请求会落到哪个 CCC)
14
14
  * ③ 唤醒注册表 —— `wake-registry.json` 全量条目(state / at / target / lastResult)+ 补跑窗口
15
- * ④ 唤起条件链 —— 包内脚本 `diag`(`runAutopilotScript(root,'diag')`):逐条件值 + 阻断点 + 修复建议
15
+ * ④ 唤起条件链 —— **进程内** {@link buildWakeChain}(⑥ C6a):逐条件值 + 阻断点 + 修复建议
16
+ * + 只有进程内能看到的三项事实(全局闸 / live 会话 / agent 可解析性)
17
+ *
18
+ * C5「观察面归一」(2026-09-15):①/② 仍走 `diagLive`(那是**live 运行态**的独有取数,
19
+ * 没有第二个来源),但 ①b 时钟与 ③ 注册表改经 `container-status.ts` 取(唯一取数出口)——
20
+ * **投影仍在本文件**(诊断段不含 `message`,面板含;这是渲染差异,不是取数差异)。
21
+ *
22
+ * ⑥ C6a(2026-09-15):④ 段由"spawn 包内脚本"改为**进程内计算**。旧实现是本报告里
23
+ * 唯一一条**跨进程**取数:脚本看不到全局闸/live/agent ⇒ 与本工具 ① 段自相矛盾
24
+ * (① 说"agent 未定位",④ 却印"条件全部满足")。现在 ④ 段与 ① 段读**同一份事实**
25
+ * (`autopilotRuntimeFacts`),且条件链**只有一份实现**(开发面 `dsh-develop diag` import 同一个)。
16
26
  */
17
- import { type DiagLiveReport, autopilotClockState } from './autopilot-trajectory.js';
18
- import { wakeSchedulerState } from './wake-scheduler.js';
27
+ import { type DiagLiveReport } from './autopilot-trajectory.js';
28
+ import { type ClockSnapshot } from './container-status.js';
19
29
  import type { Context } from 'cordis';
20
30
  /** 进程内时钟状态(唤醒调度器 / autopilot 时钟;判据各自单一真相源) */
21
- type ClockState = ReturnType<typeof wakeSchedulerState> | ReturnType<typeof autopilotClockState>;
22
- export interface AccDiagReport {
23
- /** 调用方会话所属 CCC 根(脚本诊断的目标) */
31
+ type ClockState = ClockSnapshot;
32
+ interface AccDiagReport {
33
+ /** 调用方会话所属 CCC 根(过程内诊断的目标) */
24
34
  ccc: string;
25
35
  live: DiagLiveReport;
26
36
  /**
@@ -46,16 +56,21 @@ export interface AccDiagReport {
46
56
  }>;
47
57
  pending: number;
48
58
  };
49
- /** 唤起条件链(脚本输出原文;失败时给出错误文本,不吞) */
59
+ /**
60
+ * 唤起条件链(进程内算出的渲染文本,含逐条件值 + 阻断点 + 修复建议)。
61
+ * **不是**子进程输出——判据唯一实现见 `autopilot-chain.ts`。
62
+ */
50
63
  autopilotChain: string;
64
+ /** 条件链判决档(`ready`/`waiting`/`blocked`/`unknown`;供调用方/测试断言,不再解析文本) */
65
+ autopilotVerdict: string;
51
66
  }
52
67
  /**
53
68
  * 装配报告(一次调用即全报告)。
54
69
  * @param ctx 插件上下文(进程内读 sessions/agents)
55
- * @param root 调用方 CCC 根(脚本诊断目标)
70
+ * @param root 调用方 CCC 根(条件链诊断目标)
56
71
  * @returns 结构化报告(渲染由 {@link renderAccDiag} 负责)
57
72
  */
58
- export declare function runAccDiag(ctx: Context, root: string): AccDiagReport;
73
+ export declare function runAccDiag(ctx: Context, root: string): Promise<AccDiagReport>;
59
74
  /** 人读渲染(四段;空段也要显式说明"无",避免"沉默 = 一切正常"的误读) */
60
75
  export declare function renderAccDiag(r: AccDiagReport): string;
61
76
  export {};
@@ -0,0 +1,110 @@
1
+ /**
2
+ * face-host.ts — **面宿主**:插件自起 HTTP listener 的唯一生命周期处(C4 块 A,S142 2026-09-15)
3
+ *
4
+ * ## 现状 → 本模块要收掉什么(取证:`…/acc-c4c5-current-state.md` §①/§③ 3.1)
5
+ *
6
+ * 插件自己 spawn 了**四个** node:http 监听器(B 3081 网关 / C 3082 微信发送 / D 3099 Skiff 调试
7
+ * / E 3100 ACP+问答)。此前四处各写一份同款样板:`createServer` + `server.once('error')` +
8
+ * `listen(port, host)` + 模块级 `active` 变量 + `close()`;拆卸也分裂成**两套**——
9
+ * `seams/lifecycle.ts` 有一个聚合 disposer,而 `gateway.ts` 与 `weixin-send-api.ts`
10
+ * **各自另注册**一个(D/E 干脆没有自己的 disposer)。
11
+ *
12
+ * 本模块把"listener 生命周期"收成一处:**active 表 + 统一 listen/close + 统一错误与端口占用
13
+ * 处理**。四个面各自只提供 `{ name, port, host, enabled, handler }`。
14
+ *
15
+ * ## 🔴 共享的是「生命周期与样板」,**绝不是**「路由 / 监听器 / 鉴权」
16
+ *
17
+ * 每个面**保留自己的 listener、自己的路由、自己的鉴权**(`handler` 内部原样不动):
18
+ * · **A ↔ C 解耦**(`weixin-send-api.ts:4-7`):3080 会被 3081 原样反代(连请求头),
19
+ * 把 3082 的端点挂到 3080 上 = 已登录的外部家庭账号即可冒充 bot 发消息
20
+ * ⇒ 本模块**不提供**"把面挂到别的面"的能力,也不共享路由表;
21
+ * · **E ↔ D** 共用会话核心(acp-core → skiff-core)但各自独立 listener + 独立路由/HTML;
22
+ * · **B 是 A 的代理宿主**(把请求打到 `127.0.0.1:<主端口>`),不是另一个路由面。
23
+ *
24
+ * ## 面 A(3080)不在本模块之列
25
+ * 它是**宿主 DSH 主 WebUI**,插件只往上挂 `/serenity/*` 路由(`ctx.webServer.register`),
26
+ * **插件不起这个 listener** ⇒ 面宿主只持有 B/C/D/E 四个自起面。
27
+ *
28
+ * ## 两个入口的分工(这是本模块唯一的"抽象决策",别混)
29
+ * · {@link startFace} —— **机械启动**:把一个 handler 绑到 `host:port` 上。同名面若已在监听,
30
+ * **先停后起**(替换);同名面正在启动中则**复用在飞的 Promise**(并发合并——这条替代了
31
+ * `index.ts` 里手搓的 `starting` 在飞标志与随之而来的 `EADDRINUSE` 双绑)。
32
+ * 它**不读** `spec.enabled()`:调用方保证意图(测试/程序化直启也走这里)。
33
+ * · {@link faceEnabled} —— **意图读取**:装配层据此判"该面此刻应否在监听",替代各面 sync 里
34
+ * 手写的 `const want = <配置> && ...` 判据(判据本身只写在面的 {@link FaceSpec} 里一次)。
35
+ */
36
+ import { type IncomingMessage, type Server, type ServerResponse } from 'node:http';
37
+ /** 面规格:**四个面各自只提供这 5 项**(路由/鉴权/响应体全部归面的 `handler`) */
38
+ export interface FaceSpec {
39
+ /** 面名(日志/拆卸/幂等键;全进程唯一,建议用 `FACE_*` 常量) */
40
+ name: string;
41
+ /** 期望监听端口(0 = 内核分配;实际端口见 {@link FaceHandle.port}) */
42
+ port: number;
43
+ /** 监听地址(各面自定:`127.0.0.1` 或 `0.0.0.0`——**不因归一而改绑**) */
44
+ host: string;
45
+ /** 该面**此刻是否应处于监听状态**(面自己的配置意图;由 {@link faceEnabled} 读取) */
46
+ enabled: () => boolean;
47
+ /** 单请求处理(面自己的路由 / 鉴权 / 响应体,本模块不介入其语义) */
48
+ handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>;
49
+ /**
50
+ * **EADDRINUSE 重试预算**:每次重试前等待的毫秒数序列(长度 = 最多重试次数)。
51
+ * 缺省 = {@link DEFAULT_RETRY_DELAYS}(10 × 1000ms,**生产行为原样不变**)。
52
+ * 传空数组 = 零重试(一次 EADDRINUSE 即 reject);测试用短预算证明"耗尽即 reject"。
53
+ *
54
+ * 🔒 不变量(v1.34.1 修 flake 时显式化):重试预算**可见、可控、有界**——
55
+ * 绑定尝试绝不无声挂满整段预算;耗尽即 **reject**(调用方可见),不静默悬挂。
56
+ * 此前预算是模块内硬写的 10×1s,而 vitest 单测超时 5s ⇒ "快失败"被拉长成"挂死",
57
+ * 调用方无从缩短 ⇒ 预算提到规格面上。
58
+ */
59
+ retryDelays?: readonly number[];
60
+ /**
61
+ * 绑上后 `server.unref()`(缺省 `false` = server 维持事件循环存活,**其余三面行为不变**)。
62
+ *
63
+ * 面 C(微信发送,`FACE_PORTS.weixinSend.defaultEnabled = true`)必须传 `true`:宿主若以
64
+ * **一次性命令**方式跑(`dsh <cmd>` 用完即退),未 unref 的 listener 会拖着进程不退出。
65
+ * 这是 C4 之前 `weixin-send-api.ts` 的原语义(C4 归一到面宿主时丢失),现逐字恢复。
66
+ */
67
+ unref?: boolean;
68
+ }
69
+ /** 一个在监听的面 */
70
+ export interface FaceHandle {
71
+ readonly name: string;
72
+ /** **实际**绑定端口(`spec.port=0` 时为内核分配值) */
73
+ readonly port: number;
74
+ readonly host: string;
75
+ /** 原始 http server——面若需挂自己的 server 级事件(gateway 的 `'upgrade'`)在此挂 */
76
+ readonly server: Server;
77
+ /** 停止本面(幂等;只停这一实例,不会误停同名后继实例) */
78
+ dispose: () => void;
79
+ }
80
+ /** 启动被取消(拆卸发生在重试窗口内)——不是失败,调用方按"未启动"处理 */
81
+ export declare class FaceStartCancelled extends Error {
82
+ constructor(name: string);
83
+ }
84
+ /** 面是否在监听 */
85
+ export declare function faceActive(name: string): boolean;
86
+ /** 面的实际监听端口(未在监听 → null) */
87
+ export declare function facePort(name: string): number | null;
88
+ /** 当前在监听的面名(日志/诊断/测试;顺序 = 启动顺序) */
89
+ export declare function activeFaceNames(): string[];
90
+ /**
91
+ * **机械启动**一个面(同名面已在监听 → 先停后起;正在启动中 → 复用同一 Promise)。
92
+ * 绑不上(非端口占用类的错误、或占用重试耗尽)→ **reject**,由调用方决定记日志/放弃。
93
+ */
94
+ export declare function startFace(spec: FaceSpec): Promise<FaceHandle>;
95
+ /**
96
+ * **意图读取**:该面此刻是否应处于监听状态(`spec.enabled()` 的安全求值——配置源抛错视为"不启用")。
97
+ *
98
+ * 装配层用它替代此前散在各面 sync 里的 `const want = ...` 手写判据;**启动本身不读它**
99
+ * (`startFace` 是机械入口:调用方保证意图,测试/程序化直启即用)。
100
+ * 例:微信发送面的 sync = `faceEnabled(spec) ? startFace(spec) : stopWeixinSendApi()`。
101
+ */
102
+ export declare function faceEnabled(spec: FaceSpec): boolean;
103
+ /** 停止一个面(幂等)。返回"是否真的停了一个在监听的面"(取消在飞启动不计入)。 */
104
+ export declare function stopFace(name: string): boolean;
105
+ /**
106
+ * 停止**全部**在监听的面 + 取消全部在飞启动。
107
+ * 这是插件卸载/HMR 拆卸自起 listener 的**唯一入口**(`seams/lifecycle.ts` 的聚合 disposer 调用)。
108
+ * @returns 实际停掉的面名(顺序 = 启动顺序)
109
+ */
110
+ export declare function stopAllFaces(): string[];