@shgroup/dsh-serenity-hooks 1.49.0 → 1.50.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.
@@ -60,7 +60,9 @@ const MECHANISM_PORTS = Object.keys(FACE_PORTS).map((id) => String(FACE_PORTS[id
60
60
  //#region src/settings-section.ts
61
61
  var settings_section_exports = /* @__PURE__ */ __exportAll({
62
62
  SERENITY_SETTINGS_NS: () => SERENITY_SETTINGS_NS,
63
+ defaultHumanChannelSettings: () => defaultHumanChannelSettings,
63
64
  defaultSimpleSettings: () => defaultSimpleSettings,
65
+ humanChannelSettingsFromConfig: () => humanChannelSettingsFromConfig,
64
66
  readSimpleSettings: () => readSimpleSettings,
65
67
  registerSettingsSection: () => registerSettingsSection,
66
68
  simpleSettingsFromConfig: () => simpleSettingsFromConfig,
@@ -77,7 +79,8 @@ function defaultSimpleSettings() {
77
79
  acpHttpPort: ACP_HTTP_PORT,
78
80
  publicAskEnabled: false,
79
81
  croEnabled: true,
80
- unattendedEnabled: false
82
+ unattendedEnabled: false,
83
+ humanChannel: defaultHumanChannelSettings()
81
84
  };
82
85
  }
83
86
  /**
@@ -104,6 +107,58 @@ function unwrapVolatile(value) {
104
107
  if (typeof value.get === "function") return value.get();
105
108
  return value;
106
109
  }
110
+ /** human-channel 的内建缺省(与 `Config` 的 schema 缺省**同源**:这里是读取面,那里是解析面) */
111
+ function defaultHumanChannelSettings() {
112
+ return {
113
+ accounts: [],
114
+ subscriptions: [],
115
+ relayCcc: null,
116
+ sendingAllow: ["*"]
117
+ };
118
+ }
119
+ /** 取"有内容的字符串"(首尾空白剥掉;空白串 ⇒ `undefined`)—— 面板空文本框会写 `''`,那是"没填"不是"填了个空名" */
120
+ function nonEmptyText(value) {
121
+ if (typeof value !== "string") return void 0;
122
+ const trimmed = value.trim();
123
+ return trimmed === "" ? void 0 : trimmed;
124
+ }
125
+ /**
126
+ * `humanChannel` 片段 → 运行时归一形态。**优先级:显式值 > 内建缺省**(与 `simpleSettingsFromConfig` 同族)。
127
+ *
128
+ * 🔴 三处"缺省 vs 显式"的语义差别,别写反:
129
+ * ① `sending.allow: []` ⇒ **`[]`**("谁都不许"),**不是**回退到 `['*']` —— 故用 `??`
130
+ * (只对 `undefined`/`null` 回退)。⚠️ **别拿 `??`→`||` 当变异探针**:空数组在 JS 里是
131
+ * **真值** ⇒ 两种写法在这个输入上**恰好同效**,那是**非变异**(会得出"测试不承重"的假结论)。
132
+ * ② `relay.ccc` 空白 ⇒ `null`("没填"与"没这一项"同义),**不是**把空白当 CCC 名;
133
+ * ③ 账号 `enabled` 缺省 **true**(老账号表里 `enabled` 不写就是开)。
134
+ *
135
+ * 🔴 **旧 CCC 侧 `weixin.*` 不在这里、也不该在这里**:机器级面**只**认 `humanChannel`
136
+ * (硬切无别名,同 D61 先例)⇒ 传进来的旧键**一律不进结构**(回归钉见
137
+ * `tests/human-channel-config.test.ts`;CCC 侧旧读取面的退役在 P0-2~P0-4)。
138
+ * @param fragment - `Config.humanChannel`(可能整个不存在 —— 那是**合法**初始态)
139
+ * @returns 归一形态(永不 `undefined`)
140
+ */
141
+ function humanChannelSettingsFromConfig(fragment) {
142
+ const d = defaultHumanChannelSettings();
143
+ if (!fragment) return d;
144
+ return {
145
+ accounts: (fragment.accounts ?? []).map((a) => ({
146
+ id: nonEmptyText(a?.id),
147
+ channel: nonEmptyText(a?.channel),
148
+ enabled: a?.enabled !== false,
149
+ token: a?.token,
150
+ userId: a?.userId
151
+ })),
152
+ subscriptions: (fragment.subscriptions ?? []).map((s) => ({
153
+ account: nonEmptyText(s?.account),
154
+ user: nonEmptyText(s?.user),
155
+ ccc: nonEmptyText(s?.ccc),
156
+ role: nonEmptyText(s?.role)
157
+ })),
158
+ relayCcc: nonEmptyText(fragment.relay?.ccc) ?? d.relayCcc,
159
+ sendingAllow: fragment.sending?.allow ?? d.sendingAllow
160
+ };
161
+ }
107
162
  /**
108
163
  * Config(宿主 Loader 校验后的解析值)→ 扁平简单配置。**本文件唯一的读取语义**。
109
164
  *
@@ -122,7 +177,8 @@ function simpleSettingsFromConfig(config) {
122
177
  acpHttpPort: unwrapVolatile(config.acpHttpPort) ?? config.acp?.httpPort ?? d.acpHttpPort,
123
178
  publicAskEnabled: unwrapVolatile(config.publicAskEnabled) ?? d.publicAskEnabled,
124
179
  croEnabled: unwrapVolatile(config.croEnabled) ?? d.croEnabled,
125
- unattendedEnabled: unwrapVolatile(config.unattendedEnabled) ?? d.unattendedEnabled
180
+ unattendedEnabled: unwrapVolatile(config.unattendedEnabled) ?? d.unattendedEnabled,
181
+ humanChannel: humanChannelSettingsFromConfig(config.humanChannel)
126
182
  };
127
183
  }
128
184
  /** 运行时源(`registerSettingsSection` 装配:读本插件 fiber 的**当前** Config) */
@@ -32,6 +32,14 @@
32
32
  * 读取优先级 = **面板层 > 部署层 > 内建缺省**(`simpleSettingsFromConfig`)。
33
33
  * 🔴 面板层的键**故意不带 schemastery 默认值** —— 有默认值就永远非 `undefined`,
34
34
  * 部署层会被无声压死("未设置"必须可观测)。
35
+ *
36
+ * ## 🔵 2026-09-26(P0-1):多一类成员 —— `humanChannel`(机器级结构化表)
37
+ *
38
+ * 它不是开关/阈值,但**读取语义归本文件**("本文件唯一的读取语义"这条不破):宿主 Loader
39
+ * 解析出的 `Config.humanChannel` 经 `humanChannelSettingsFromConfig()` 归一后,随
40
+ * `SerenitySimpleSettings.humanChannel` 一起出(⇒ 消费方仍只认 `readSimpleSettings()` 一个入口,
41
+ * 且天然享受 `fiber.update()` 的**热生效**语义 —— 方案 Q8)。缺省 / 归一规则见该函数与
42
+ * `defaultHumanChannelSettings`。设计全文 = 插件仓 `docs/human-channel-plan.md` §2。
35
43
  */
36
44
  import type { Context, Volatile } from 'cordis';
37
45
  /**
@@ -61,6 +69,8 @@ interface SimpleConfigFragment {
61
69
  enabled?: boolean;
62
70
  httpPort?: number;
63
71
  };
72
+ /** human-channel(人机信道;**机器级**;2026-09-26 owner 定案):账号 / 订阅表 / 主控 / 出站白名单 */
73
+ humanChannel?: HumanChannelConfigFragment;
64
74
  gatewayEnabled?: PanelBoolean;
65
75
  rebuildThresholdK?: PanelNumber;
66
76
  skiffEnabled?: PanelBoolean;
@@ -75,7 +85,9 @@ interface SimpleConfigFragment {
75
85
  * 设置面板命名空间 = **profile 条目 id**(v0.1.7 起;与旧 `settings.yaml` 段名逐字相同)。
76
86
  */
77
87
  export declare const SERENITY_SETTINGS_NS = "serenity-hooks";
78
- /** 简单配置的扁平形态(运行时读取的统一形状;各功能门控只看它) */
88
+ /** 简单配置的扁平形态(运行时读取的统一形状;各功能门控只看它)。
89
+ * 🔵 **2026-09-26(P0-1)起多一类成员**:`humanChannel` 是**机器级结构化表**(不是开关/阈值),
90
+ * 搭这个结构的车只为**读取入口唯一**(`readSimpleSettings()`;方案 §5-2 / §11.4 P0-1)。 */
79
91
  interface SerenitySimpleSettings {
80
92
  /** F1 双端口网关总开关 */
81
93
  gatewayEnabled: boolean;
@@ -109,6 +121,11 @@ interface SerenitySimpleSettings {
109
121
  * —— 原"任何自排唤醒都排除"会让本机制永远够不着靠续接绳活着的维护会话;改窄后
110
122
  * **自排绳不再排除**,绳退化为**长周期保险丝**)。设计全文见 S142 的 **D77**。 */
111
123
  unattendedEnabled: boolean;
124
+ /** **human-channel(人机信道)机器级配置**(2026-09-26 owner 定案;P0-1 立 schema + 投影)。
125
+ * 三张表(账号 / 订阅 / 主控)+ 出站白名单;**跨 CCC** ⇒ 归机器级,CCC 侧 `weixin.*` 随之退役
126
+ * (退役动作在 P0-2~P0-4;本版**只立机器级面**,旧面照旧)。缺省与归一规则见
127
+ * `defaultHumanChannelSettings` / `humanChannelSettingsFromConfig`。 */
128
+ humanChannel: HumanChannelSettings;
112
129
  }
113
130
  /** 进程级默认(无 Config 可读时的兜底;与 Config 各字段的缺省一致) */
114
131
  export declare function defaultSimpleSettings(): SerenitySimpleSettings;
@@ -136,6 +153,79 @@ interface VolatileRef<T> {
136
153
  * @returns 解包后的裸值(引用 ⇒ `.get()`;裸值原样;缺省 ⇒ `undefined`)
137
154
  */
138
155
  export declare function unwrapVolatile<T>(value: T | VolatileRef<T> | undefined): T | undefined;
156
+ /** 机器级 `humanChannel` 片段(**结构性**声明,刻意不从 `index.ts` 引类型:本模块保持零宿主依赖) */
157
+ export interface HumanChannelConfigFragment {
158
+ accounts?: Array<{
159
+ id?: string;
160
+ channel?: string;
161
+ enabled?: boolean;
162
+ token?: string;
163
+ userId?: string;
164
+ }>;
165
+ subscriptions?: Array<{
166
+ account?: string;
167
+ user?: string;
168
+ ccc?: string;
169
+ role?: string;
170
+ }>;
171
+ relay?: {
172
+ ccc?: string;
173
+ };
174
+ sending?: {
175
+ allow?: string[];
176
+ };
177
+ }
178
+ /** 账号行的运行时形态:`enabled` 归一为**定值布尔**(轮询者要一个确定答案,不要 `undefined`) */
179
+ export interface HumanChannelAccountSettings {
180
+ id?: string;
181
+ channel?: string;
182
+ enabled: boolean;
183
+ token?: string;
184
+ userId?: string;
185
+ }
186
+ /** 订阅行的运行时形态(透传;行内字段的**校验**归 P0-3,见下) */
187
+ export interface HumanChannelSubscriptionSettings {
188
+ account?: string;
189
+ user?: string;
190
+ ccc?: string;
191
+ role?: string;
192
+ }
193
+ /**
194
+ * human-channel 的运行时归一形态(各功能只看它)。
195
+ *
196
+ * 🔴 **本投影刻意不做"清洗"**(不丢行 / 不判重 / 不校验目标存在):那些都是**策略**,
197
+ * 而策略的答案在 P0-3(目标不存在 ⇒ 响亮报错)与 P0-4(迁移警告)里。在这里静默丢行会
198
+ * **掩盖配置错误** —— 而本方案的 §2.4 精神恰恰是"**不静默**"("家搬了"必须出声)。
199
+ */
200
+ export interface HumanChannelSettings {
201
+ /** 账号表(`enabled` 已归一为定值布尔;缺 `id` / `channel` 的行**保留**,由下游响亮报错) */
202
+ accounts: HumanChannelAccountSettings[];
203
+ /** 订阅表:`(账号, 用户) → (CCC, 角色)` */
204
+ subscriptions: HumanChannelSubscriptionSettings[];
205
+ /** 主控 CCC 别名(`null` = 未指定 / 未配置 / 空白 —— 三者语义相同:"没有主控") */
206
+ relayCcc: string | null;
207
+ /** 出站白名单(`*` = 全体);缺省 `['*']` */
208
+ sendingAllow: string[];
209
+ }
210
+ /** human-channel 的内建缺省(与 `Config` 的 schema 缺省**同源**:这里是读取面,那里是解析面) */
211
+ export declare function defaultHumanChannelSettings(): HumanChannelSettings;
212
+ /**
213
+ * `humanChannel` 片段 → 运行时归一形态。**优先级:显式值 > 内建缺省**(与 `simpleSettingsFromConfig` 同族)。
214
+ *
215
+ * 🔴 三处"缺省 vs 显式"的语义差别,别写反:
216
+ * ① `sending.allow: []` ⇒ **`[]`**("谁都不许"),**不是**回退到 `['*']` —— 故用 `??`
217
+ * (只对 `undefined`/`null` 回退)。⚠️ **别拿 `??`→`||` 当变异探针**:空数组在 JS 里是
218
+ * **真值** ⇒ 两种写法在这个输入上**恰好同效**,那是**非变异**(会得出"测试不承重"的假结论)。
219
+ * ② `relay.ccc` 空白 ⇒ `null`("没填"与"没这一项"同义),**不是**把空白当 CCC 名;
220
+ * ③ 账号 `enabled` 缺省 **true**(老账号表里 `enabled` 不写就是开)。
221
+ *
222
+ * 🔴 **旧 CCC 侧 `weixin.*` 不在这里、也不该在这里**:机器级面**只**认 `humanChannel`
223
+ * (硬切无别名,同 D61 先例)⇒ 传进来的旧键**一律不进结构**(回归钉见
224
+ * `tests/human-channel-config.test.ts`;CCC 侧旧读取面的退役在 P0-2~P0-4)。
225
+ * @param fragment - `Config.humanChannel`(可能整个不存在 —— 那是**合法**初始态)
226
+ * @returns 归一形态(永不 `undefined`)
227
+ */
228
+ export declare function humanChannelSettingsFromConfig(fragment: HumanChannelConfigFragment | undefined): HumanChannelSettings;
139
229
  /**
140
230
  * Config(宿主 Loader 校验后的解析值)→ 扁平简单配置。**本文件唯一的读取语义**。
141
231
  *
package/lib/status.d.ts CHANGED
@@ -4,10 +4,37 @@
4
4
  * WebUI 停靠栏的数据源:ACC 版本 / CCC 根 / safe-mode 状态 / 黑名单 / keeper 阈值 / handyman 模型。
5
5
  * setSafeMode 直接读写 .serenity-safe-on 标记(守卫实时读取,写即生效)。
6
6
  */
7
+ /**
8
+ * 🔴 **布局无关档**:从**正在跑的那个入口脚本**反推宿主包根,读它的 `version`。
9
+ *
10
+ * 为什么必须补这一档(R↓:一处**静默失效**的实际缺陷;2026-09-28 docker bench 实测):
11
+ * 下面那条静态候选链({@link readDshVersion} 的 ②)是**猜安装位置**——`npm_config_prefix` /
12
+ * `APPDATA\npm` / `~/.npm-global`。一旦宿主装在**别处**(典型 = 容器镜像里的
13
+ * `/usr/local/lib/node_modules/@deepseek-ai/dsh`),**三个候选全不中** ⇒ 返回 `null`
14
+ * ⇒ `checkHostVersion(null)` 只给一条 **`required: false`** 的 issue ⇒ 报告 `ok` 仍为 `true`
15
+ * ⇒ **版本门完全静默**(bench 逐字:`host contract degraded (1 optional): host version unknown`)。
16
+ * ⚠️ 这类坏法**比"偶尔报警"更危险**:一条**永远为假**的判据看起来像"没报警 = 没问题"。
17
+ *
18
+ * 而"我们正跑在哪个宿主里"**本来就有直接证据**:`process.argv[1]`(= 启动本进程的那个脚本)。
19
+ * 三条判据要点(每条都对应一个实测过的坑):
20
+ * ① **先 `realpath`** —— 全局安装的 `dsh` 是**符号链接**(`/usr/local/bin/dsh` → `…/@deepseek-ai/dsh/lib/bin.js`),
21
+ * 而 Node 的 `argv[1]` **保留调用路径、不解析符号链接** ⇒ 不 realpath 就永远上溯不到包根。
22
+ * ② **核对包名**(`name === '@deepseek-ai/dsh'`)—— 否则在 vitest / npx 等入口下会误取**别人的**
23
+ * `package.json`(这是本档**唯一**的假阳性来源)。
24
+ * ③ **上溯有界**({@link MAX_ENTRY_ASCENT})—— 不设界的话,一个不在任何 dsh 包内的入口会一路爬到文件系统根。
25
+ *
26
+ * @param entry `process.argv[1]`(单测直接注入,无需替身)
27
+ */
28
+ export declare function dshVersionFromEntry(entry: string | undefined): string | null;
7
29
  /**
8
30
  * 读取已安装 DSH CLI 版本;读不到返回 null。
9
- * 跨平台(Windows 审计问题 13):Windows npm 全局装在 %APPDATA%\npm(非 ~/.npm-global)——
10
- * 依次尝试 npm_config_prefix / APPDATA\npm / ~/.npm-global。
31
+ *
32
+ * **两档,顺序有理由**:
33
+ * ① 🔴 **布局无关档**({@link dshVersionFromEntry})= **正在跑的就是它** —— 直接证据优先于"猜安装位置"。
34
+ * ② **静态候选链**(跨平台历史档,保留):`npm_config_prefix` / `APPDATA\npm` / `~/.npm-global`
35
+ * (Windows npm 全局装在 `%APPDATA%\npm`,见审计问题 13)。
36
+ * ⚠️ 保留它是因为**本进程不一定是 dsh CLI**(单测、嵌入式、将来别的宿主进程)——
37
+ * 那时"本机装了哪个版本"仍需一个答案,只是证据等级更低。
11
38
  */
12
39
  export declare function readDshVersion(): string | null;
13
40
  interface SerenityStatus {
@@ -9,12 +9,17 @@
9
9
  * 实验性质:未配置/未启用 → 完全不启动(零资源占用)。配置变化(面板写入)
10
10
  * → 热重建受影响 CCC 的桥(对齐 gateway 热重建模式)。
11
11
  *
12
+ * 🔴 **P0-2(2026-09-27,方案 §2.6):起桥仍按 CCC,但"谁轮询"按账号判定** ——
13
+ * 同一个账号(= 同一份凭据)在同一时刻**只允许一个 poller**,判定与归属表在
14
+ * `human-channel.ts`(机器级机制;本文件只接线)。这是"微信桥上移 ACC 层"的第一步。
15
+ *
12
16
  * 外部面纯净(D9/D11 延续):微信桥会话 = 外部面——session/prompt 走
13
17
  * includeTrajectory:false(对外不返回轨迹);skiff 角色白名单即授权(G9)。
14
18
  */
15
19
  import type { Context } from 'cordis';
16
20
  import { type WeixinAccountCredential } from './weixin-route.js';
17
21
  import { type WeixinMessage } from './weixin-api.js';
22
+ import { type SkiffRoleConfig } from './skiff-role.js';
18
23
  /** 测试辅助:清空 typing_ticket 缓存(生产零调用) */
19
24
  export declare function resetWeixinTypingCache(): void;
20
25
  /**
@@ -32,6 +37,15 @@ export declare function manualOutputFallbackNeeded(input: {
32
37
  sentThisTurn: boolean;
33
38
  answer: string;
34
39
  }): boolean;
40
+ /**
41
+ * 路由解析结果:目标根 + 角色名 + **角色配置**(三者一起给,避免调用方再读一次 skiff.roles)。
42
+ * `null` 从本函数出去 ⇒ **不投递**(未命中已留痕 / 目标不存在已响亮出声)。
43
+ */
44
+ interface ResolvedInboundTarget {
45
+ root: string;
46
+ roleName: string;
47
+ role: SkiffRoleConfig;
48
+ }
35
49
  /**
36
50
  * 处理单条微信消息:路由 → skiff 会话(固定 id 创建/延续)→ 提问 → 回复回写。
37
51
  *
@@ -49,19 +63,64 @@ export declare function manualOutputFallbackNeeded(input: {
49
63
  * `_tmp/weixin-inbound/<userhash>/` → question 注入「存在性 + 路径」(ACC 层只保证
50
64
  * 可达性——"让会话知道文件的存在并可以拿到";识别/解析归角色 LLM 决策,不编排)。
51
65
  * 降级不静默:下载失败 / 超 20MB → 注入说明进对话;typing 窗口覆盖下载(M7)。
66
+ *
67
+ * 🔴 **P0-3(2026-09-27):路由改由机器级订阅表决定**(本函数的 `root` 入参退为
68
+ * **回退目标**)。两档语义刻意分开(方案 §3.1):**未命中 ⇒ 不投递 + 留痕**(静默档);
69
+ * **目标不存在(CCC / 角色)⇒ 响亮报错**(`console.error` + 机器级留痕)。两者都
70
+ * **不是**"投给默认角色"。
71
+ */
72
+ export declare function handleIncoming(ctx: Context, root: string, accountId: string, cred: WeixinAccountCredential, msg: Pick<WeixinMessage, 'from_user_id' | 'context_token' | 'item_list'>,
73
+ /**
74
+ * **机器级模式**(P0-3 A):目标已由 `handleMachineIncoming` 解析好(`root` 入参即该目标根)。
75
+ * 给了它 ⇒ 本函数**不再自行路由**,且**不看本 CCC 的 `weixin.enabled`** ——
76
+ * 起 poller 与路由都已上移机器级(目标 CCC 没配 weixin 段也照样收得到)。
77
+ * ⚠️ **行为开关仍归目标 CCC**(`hook` / `autoReplyWithLastMessage` / `fallbackOnNoSend`)
78
+ * —— 机器级只管"**谁收**"与"**投到哪**",不管"**怎么答**"。
52
79
  */
53
- export declare function handleIncoming(ctx: Context, root: string, accountId: string, cred: WeixinAccountCredential, msg: Pick<WeixinMessage, 'from_user_id' | 'context_token' | 'item_list'>): Promise<void>;
80
+ preset?: {
81
+ target: ResolvedInboundTarget;
82
+ }): Promise<void>;
83
+ /**
84
+ * **机器级 poller 的单条消息入口**(P0-3 A)。
85
+ *
86
+ * 与 `handleIncoming` 的唯一差别:**目标由机器级订阅表逐条解析,且没有 CCC 级回退**
87
+ * (方案 §3.1:机器级模式下**未命中 = 不投递**,不是"投给默认角色")。
88
+ * 解析出来后把**目标根**当 `root` 交给 `handleIncoming`(并带上 `preset` ⇒ 它不再自行路由、
89
+ * 也不看目标 CCC 的 `weixin.enabled`)。
90
+ *
91
+ * 🔴 为什么"起 poller"与"路由"必须同批(本件即 P0-2 的后半):机器级 poller 收到消息时
92
+ * **不知道它属于哪个 CCC** —— 那个答案是订阅表给的。没有订阅表 ⇒ **能收不能投**。
93
+ *
94
+ * 🔵 **导出供测试直接驱动**(与 `handleIncoming` 同款:它俩都是"单条消息的入口",
95
+ * 生产路径 `runAccountLoop` 只是循环壳)。
96
+ */
97
+ export declare function handleMachineIncoming(ctx: Context, accountId: string, cred: WeixinAccountCredential, msg: Pick<WeixinMessage, 'from_user_id' | 'context_token' | 'item_list'>): Promise<void>;
54
98
  /**
55
99
  * 启动/重建某 CCC 的桥:读配置 → 对每个 enabled + 有凭据的账号启动轮询循环。
56
100
  * 已存在(配置变化热重建)→ 先停旧循环再启动新的。
101
+ *
102
+ * 🔴 **P0-2(2026-09-27,方案 §2.6):一账号一 poller** —— 起循环**之前**先过机器级归属判定
103
+ * (`human-channel.ts`):身份被**别的** CCC 占着的账号**不起循环**,只留一条响亮日志。
104
+ * 身份 = **凭据 token 的哈希**(不是 `accountId`:那是每 CCC 的本地标签,跨 CCC 无意义)。
105
+ * 先 `releasePollersOf(root)` 再重新 claim —— 热重建语义与"归属粘住不漂"的论证见该函数注释。
57
106
  */
58
107
  export declare function syncCccBridge(ctx: Context, root: string): void;
59
- /** 停止某 CCC 的桥(移除账号/禁用时) */
108
+ /** 停止某 CCC 的桥(移除账号/禁用时);同时释放它的账号归属 */
60
109
  export declare function stopCccBridge(root: string): void;
61
110
  /**
62
- * 停止全部桥(插件 dispose)
111
+ * 停止全部桥(插件 dispose):CCC 级 + 机器级
63
112
  */
64
113
  export declare function stopAllBridges(): void;
114
+ /**
115
+ * 人机信道的**装配入口**(P0-3 A)—— 决定用哪种模式起 poller。
116
+ *
117
+ * 🔴 **模式由机器级账号表决定**(方案 §11.3 **Q5** 的过渡期语义):
118
+ * - 账号表**非空** ⇒ **机器级模式**:按账号起 poller;路由**只认订阅表**
119
+ * - 账号表**为空** ⇒ **回退模式**(今天的形态):按 CCC 起桥;路由**先查订阅表,未命中回退
120
+ * 到 CCC 级 routes** ⇒ **不打断在用的微信**
121
+ * ⚠️ 两种模式**互斥**(各自进入时先停掉另一种),否则同一个 bot 会被两处轮询。
122
+ */
123
+ export declare function syncHumanChannel(ctx: Context): void;
65
124
  /** 主动发送失败原因(稳定 code——入口层翻译成 HTTP 状态 + 可行动提示) */
66
125
  type ProactiveSendErrorCode = 'BRIDGE_DISABLED' | 'NO_ACCOUNT' | 'ACCOUNT_NOT_FOUND' | 'ACCOUNT_NOT_BOUND' | 'SEND_FAILED';
67
126
  interface ProactiveSendInput {
@@ -120,7 +179,11 @@ type WeixinAccountResolution = {
120
179
  */
121
180
  export declare function resolveWeixinAccount(root: string, accountId?: string): WeixinAccountResolution;
122
181
  export declare function sendProactiveText(input: ProactiveSendInput): Promise<ProactiveSendResult>;
123
- /** 桥状态快照(面板数据源):每 CCC → 每账号 → 轮询健康 */
182
+ /**
183
+ * 桥状态快照(面板数据源):每 CCC → 每账号 → 轮询健康。
184
+ * 🔴 **P0-3 A 起多一行**:机器级 poller 以 `ccc: '<machine>'`(`MACHINE_SCOPE`)出现 ——
185
+ * 它**不属于任何 CCC**,故**不伪装成某个 CCC 的行**(面板按原样显示这个哨兵值即可)。
186
+ */
124
187
  export declare function weixinBridgeStatus(): Array<{
125
188
  ccc: string;
126
189
  accounts: Array<{
@@ -130,8 +193,9 @@ export declare function weixinBridgeStatus(): Array<{
130
193
  }>;
131
194
  }>;
132
195
  /**
133
- * 装配(index.ts 调用):扫描 live CCC 启动桥 + 监听会话变化。
134
- * 事件驱动(对齐 autotrajectory v1.26.15):live 会话出现 → 同步该 CCC 桥。
196
+ * 装配(index.ts 调用):**按机器级配置决定模式**(P0-3 A:`syncHumanChannel`),
197
+ * 并监听会话变化以重扫(新 CCC 出现即启动其桥;配置变化也可经此路径热重建)。
198
+ * 事件驱动(对齐 autotrajectory v1.26.15)。
135
199
  */
136
200
  export declare function registerWeixinBridge(ctx: Context): void;
137
201
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.49.0",
3
+ "version": "1.50.1",
4
4
  "description": "宁静号 ACC harness(Native Cordis 插件)——给 DeepSeek Harness 装一个「AI 工作区」:11 个工具(container_fs/container_trajectory/dashboard/container_git/msm/praxis/handyman/localstore/container_admin/im-bridge/acc-diag)+ 机械约束(安全模式/工作区围墙/密钥守卫/对外输出守卫)+ 工作日志与原地重建 + 网页登录入口/微信桥/子角色/对外问答页/trajectory 唤醒注册表。适配 DSH 0.1.7-rc.1。",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -54,21 +54,21 @@
54
54
  },
55
55
  "peerDependencies": {
56
56
  "@deepseek-ai/cordis": "^4.0.4",
57
- "@deepseek-ai/dsh-agent": "^0.1.7-rc.1",
58
- "@deepseek-ai/dsh-agent-loop": "^0.1.7-rc.1",
59
- "@deepseek-ai/dsh-agent-preset-registry": "^0.1.7-rc.1",
60
- "@deepseek-ai/dsh-compaction": "^0.1.7-rc.1",
61
- "@deepseek-ai/dsh-host-webserver": "^0.1.7-rc.1",
62
- "@deepseek-ai/dsh-llm": "^0.1.7-rc.1",
63
- "@deepseek-ai/dsh-session": "^0.1.7-rc.1",
64
- "@deepseek-ai/dsh-session-title": "^0.1.7-rc.1",
65
- "@deepseek-ai/dsh-shell": "^0.1.7-rc.1",
66
- "@deepseek-ai/dsh-shell-env": "^0.1.7-rc.1",
67
- "@deepseek-ai/dsh-skill": "^0.1.7-rc.1",
68
- "@deepseek-ai/dsh-system-prompt": "^0.1.7-rc.1",
69
- "@deepseek-ai/dsh-tools": "^0.1.7-rc.1",
70
- "@deepseek-ai/dsh-web": "^0.1.7-rc.1",
71
- "@deepseek-ai/dsh-web-fetch-http": "^0.1.7-rc.1",
57
+ "@deepseek-ai/dsh-agent": "^0.1.7-rc.1 || ^0.2.0-rc.1",
58
+ "@deepseek-ai/dsh-agent-loop": "^0.1.7-rc.1 || ^0.2.0-rc.1",
59
+ "@deepseek-ai/dsh-agent-preset-registry": "^0.1.7-rc.1 || ^0.2.0-rc.1",
60
+ "@deepseek-ai/dsh-compaction": "^0.1.7-rc.1 || ^0.2.0-rc.1",
61
+ "@deepseek-ai/dsh-host-webserver": "^0.1.7-rc.1 || ^0.2.0-rc.1",
62
+ "@deepseek-ai/dsh-llm": "^0.1.7-rc.1 || ^0.2.0-rc.1",
63
+ "@deepseek-ai/dsh-session": "^0.1.7-rc.1 || ^0.2.0-rc.1",
64
+ "@deepseek-ai/dsh-session-title": "^0.1.7-rc.1 || ^0.2.0-rc.1",
65
+ "@deepseek-ai/dsh-shell": "^0.1.7-rc.1 || ^0.2.0-rc.1",
66
+ "@deepseek-ai/dsh-shell-env": "^0.1.7-rc.1 || ^0.2.0-rc.1",
67
+ "@deepseek-ai/dsh-skill": "^0.1.7-rc.1 || ^0.2.0-rc.1",
68
+ "@deepseek-ai/dsh-system-prompt": "^0.1.7-rc.1 || ^0.2.0-rc.1",
69
+ "@deepseek-ai/dsh-tools": "^0.1.7-rc.1 || ^0.2.0-rc.1",
70
+ "@deepseek-ai/dsh-web": "^0.1.7-rc.1 || ^0.2.0-rc.1",
71
+ "@deepseek-ai/dsh-web-fetch-http": "^0.1.7-rc.1 || ^0.2.0-rc.1",
72
72
  "@deepseek-ai/schemastery": "^3.18.4",
73
73
  "cordis": "^4.0.0-rc.7"
74
74
  },