@shgroup/dsh-serenity-hooks 1.71.0 → 1.73.0

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 (44) hide show
  1. package/dsh.plugin.json +2 -2
  2. package/lib/{ccc-Dkoa2oW3.js → ccc-CZUOq3NY.js} +1 -1
  3. package/lib/{ccc-roots-jZYR2GVE.js → ccc-roots-BhoA7G-0.js} +3 -3
  4. package/lib/client.js +50 -58
  5. package/lib/{wake/cro.d.ts → cro/core.d.ts} +2 -2
  6. package/lib/{wake/cro-guide.d.ts → cro/guide.d.ts} +3 -3
  7. package/lib/{wake/cro-log.d.ts → cro/log.d.ts} +2 -2
  8. package/lib/{wake/cro-turns.d.ts → cro/turns.d.ts} +2 -2
  9. package/lib/guard/seam.d.ts +1 -1
  10. package/lib/{ilink-DEpmE3yi.js → ilink-BMk2w2KY.js} +1 -1
  11. package/lib/im/accounts.d.ts +105 -0
  12. package/lib/im/bridge.d.ts +33 -62
  13. package/lib/im/endpoint.d.ts +1 -1
  14. package/lib/im/feishu-account.d.ts +92 -0
  15. package/lib/im/inbound-event.d.ts +162 -0
  16. package/lib/im/route.d.ts +37 -2
  17. package/lib/im/weixin-account.d.ts +45 -0
  18. package/lib/index.d.ts +0 -22
  19. package/lib/index.js +1070 -996
  20. package/lib/kernel/ccc.d.ts +78 -2
  21. package/lib/kernel/ports.d.ts +2 -4
  22. package/lib/lib-TxscFKeo.js +119355 -0
  23. package/lib/{message-source-kVoNK0C5.js → message-source-ChyVfev9.js} +2 -2
  24. package/lib/{profile-DyMJ_bfr.js → profile-BIQ2widI.js} +14 -11
  25. package/lib/rolldown-runtime-B4iAMlE-.js +35 -0
  26. package/lib/{route-CPneN5iv.js → route-ItCgAsnz.js} +96 -35
  27. package/lib/seams/guards.d.ts +1 -1
  28. package/lib/seams/system-prompt.d.ts +3 -1
  29. package/lib/{settings-lt1i3OZv.js → settings-UcwIPJhM.js} +13 -62
  30. package/lib/skills/resource-index.d.ts +1 -1
  31. package/lib/surface/settings.d.ts +0 -6
  32. package/lib/surface/text.d.ts +72 -0
  33. package/lib/surface/usage.d.ts +2 -2
  34. package/lib/{role/worker/idle.d.ts → trajectory/carrier.d.ts} +12 -2
  35. package/lib/{role/worker → trajectory/delegate}/core.d.ts +7 -1
  36. package/lib/{role/worker → trajectory/delegate}/preset.d.ts +9 -2
  37. package/lib/{role/skiff → trajectory/policy}/profile.d.ts +9 -2
  38. package/lib/{role/skiff/config.d.ts → trajectory/policy/rules.d.ts} +9 -3
  39. package/lib/{role/skiff/registry.d.ts → trajectory/policy/window.d.ts} +7 -1
  40. package/lib/trajectory/prompt.d.ts +101 -0
  41. package/lib/{role/skiff/core.d.ts → trajectory/session.d.ts} +13 -4
  42. package/package.json +3 -2
  43. package/lib/role/skiff/debug.d.ts +0 -88
  44. package/lib/rolldown-runtime-8H4AJuhK.js +0 -14
@@ -26,70 +26,46 @@
26
26
  import type { Context } from 'cordis';
27
27
  import { type WeixinAccountCredential } from './route.js';
28
28
  import { type WeixinMessage } from './ilink.js';
29
- import { type SkiffProfile } from '../role/skiff/profile.js';
29
+ import { resolveInboundTarget, manualOutputFallbackNeeded, type ResolvedInboundTarget } from './inbound-event.js';
30
+ export { manualOutputFallbackNeeded, resolveInboundTarget };
31
+ export type { ResolvedInboundTarget };
30
32
  /** 测试辅助:清空 typing_ticket 缓存(生产零调用) */
31
33
  export declare function resetWeixinTypingCache(): void;
32
34
  /**
33
- * 手动输出模式的**兜底判定**(纯函数,v1.30.17——把决策矩阵从装配里抽出来,可独立测试)。
34
- *
35
- * 四个条件同时成立才兜底:
36
- * ① CCC 显式开启 `fallbackOnNoSend`(缺省 false = 严格静默,向后兼容)
37
- * ② 机械闸门**已装配**(未装配时无法判定"是否发送过" → 宁可静默也不冒重复发送风险)
38
- * ③ 本轮**没有**成功发送(agent 自己发过 → 输出权归 agent,桥不插话)
39
- * ④ 有最终文本可转发(空文本无意义)
40
- */
41
- export declare function manualOutputFallbackNeeded(input: {
42
- fallbackOnNoSend?: boolean;
43
- guardActive: boolean;
44
- sentThisTurn: boolean;
45
- answer: string;
46
- }): boolean;
47
- /**
48
- * 路由解析结果:目标根 + **轨迹编码** + **该轨迹的 skiff 约束面**。
49
- * `null` 从本函数出去 ⇒ **不投递**(未命中已留痕 / 目标不存在已响亮出声)。
35
+ * 🔴 **2026-10-19(P2)`runAccountLoop` 已迁出本文件** —— 逐字搬到
36
+ * `im/weixin-account.ts`,成为 `ImAccountImpl` 的一个实现(微信)。
37
+ *
38
+ * 为什么搬:装配处原先把渠道写死在这里(`channel !== 'weixin'` ⇒ 跳过)。
39
+ * owner 定向「飞书和微信一样,只不过是 account 方实现不同」⇒ 起循环这个动作
40
+ * 归**账号自己的实现**,装配处只按 `channel` 查派发表。
41
+ * ⚠️ 依赖(`getUpdates` / `handleIncoming` / `handleMachineIncoming`)通过
42
+ * `installWeixinAccountImpl` **注入**,避免 `bridge ⇄ weixin-account` 互相 import 成环。
50
43
  */
51
- interface ResolvedInboundTarget {
52
- root: string;
53
- /** 目标认知轨迹编码(= 新模型里的"身份";旧模型的角色名已退役) */
54
- trajectory: string;
55
- /**
56
- * 该轨迹的 skiff 约束面。
57
- * ⚠️ **可能为 `null`**:轨迹存在但没有 `skiff.json` ⇒ 它是一条**普通轨迹**,
58
- * 微信桥照样可以往里投(投进一条普通认知轨迹是合法的),只是不施加 skiff 约束。
59
- * ⇒ 这与旧模型的关键差别:旧模型里"角色不存在"是硬错,新模型里"没有 skiff.json"不是错。
60
- */
61
- profile: SkiffProfile | null;
62
- }
63
44
  /**
64
- * 处理单条微信消息:路由 → skiff 会话(固定 id 创建/延续)→ 提问 → 回复回写。
65
- *
66
- * 会话语义:`weixinSessionIdFor(fromUserId)` 固定可重建——同用户长期同一会话;
67
- * 进程内已有(getSkiffAgent 命中)→ 延续;无(首次/进程重启)→ createSkiffAgent
68
- * 固定 id resume-or-create(v1.27.2):磁盘已有持久化 log → resume(历史延续,
69
- * 重启后记忆保留——真正的"同用户长期延续");无 log(首次)→ create。
70
- * "新的对话已开始"通知仅真正首次(create)时发送;resume/进程内延续不发。
71
- *
72
- * 完善(v1.27.3):
73
- * - **语音支持**:`voice_item.text` = 微信服务端自带语音转写 → 与文本同路径进对话
74
- * (无需下载/ASR);语音无转写 → 降级提示"暂时无法解析"
75
- * - **正在输入**:处理前 sendtyping 1(微信显示"正在输入..."),处理后(含异常)0
76
- * - **媒体接收(图片/文件)**:桥侧 CDN 下载 + AES 解密 → 落盘 CCC 根
77
- * `_tmp/weixin-inbound/<userhash>/` → question 注入「存在性 + 路径」(ACC 层只保证
78
- * 可达性——"让会话知道文件的存在并可以拿到";识别/解析归角色 LLM 决策,不编排)。
79
- * 降级不静默:下载失败 / 超 20MB → 注入说明进对话;typing 窗口覆盖下载(M7)。
80
- *
81
- * 🔴 **P0-3(2026-09-27):路由改由机器级订阅表决定**(本函数的 `root` 入参退为
82
- * **回退目标**)。两档语义刻意分开(方案 §3.1):**未命中 ⇒ 不投递 + 留痕**(静默档);
83
- * **目标不存在(CCC / 角色)⇒ 响亮报错**(`console.error` + 机器级留痕)。两者都
84
- * **不是**"投给默认角色"。
45
+ * 处理单条**微信**消息:把 iLink 形状的 `msg` 归一成**中立入站事件**,
46
+ * 再交给通道中立的 `handleInboundEvent`(`im/inbound-event.ts`)走主流程。
47
+ *
48
+ * ## 🔴 2026-10-19(B1):本函数已退化为**薄适配器**,主流程不在本文件
49
+ *
50
+ * 原因:`handleIncoming` 原先同时承担两件不同的事 —— ① "iLink 消息长什么样"
51
+ * ② "一条已到达的消息该投到哪、怎么答、怎么记账"。接第二家 IM 时只有 ① 需要新写,
52
+ * 而 ② 应该**被两家共用**。两者混在一个函数里 ⇒ 飞书接进来只能复制一整份主流程。
53
+ *
54
+ * ⇒ 主流程搬到 `im/inbound-event.ts`,本函数只做**形状翻译**:
55
+ * `msg`(iLink)+ `cred`(iLink 凭据)→ `ImInboundEvent` + `ImAccountIo`。
56
+ *
57
+ * ## 为什么保留这个签名(而不是让调用方直接调新的主流程)
58
+ *
59
+ * 全仓约 50 处测试按 iLink 形状驱动本函数,而 B1 的验收判据是「**微信行为逐字不变**」
60
+ * (用例数守恒 + 真机一条微信)。保留签名 ⇒ 那 50 处**逐字不动**,判据可复算。
61
+ *
62
+ * 会话语义(不变):会话 id 由**轨迹编码**派生 ⇒ 同一条轨迹恒同一条 dsh 会话;
63
+ * 进程内已有 → 延续;无 → 固定 id resume-or-create。
85
64
  */
86
65
  export declare function handleIncoming(ctx: Context, root: string, accountId: string, cred: WeixinAccountCredential, msg: Pick<WeixinMessage, 'from_user_id' | 'context_token' | 'item_list'>,
87
66
  /**
88
- * **机器级模式**(P0-3 A):目标已由 `handleMachineIncoming` 解析好(`root` 入参即该目标根)。
89
- * 给了它 ⇒ 本函数**不再自行路由**,且**不看本 CCC 的 `weixin.enabled`** ——
90
- * 起 poller 与路由都已上移机器级(目标 CCC 没配 weixin 段也照样收得到)。
91
- * ⚠️ **行为开关仍归目标 CCC**(`hook` / `autoReplyWithLastMessage` / `fallbackOnNoSend`)
92
- * —— 机器级只管"**谁收**"与"**投到哪**",不管"**怎么答**"。
67
+ * **机器级模式**(P0-3 A):目标已解析好(`root` 入参即该目标根)⇒ 不再自行路由,
68
+ * 且**不看本 CCC 的 `weixin.enabled`**。⚠️ 行为开关仍归目标 CCC。
93
69
  */
94
70
  preset?: {
95
71
  target: ResolvedInboundTarget;
@@ -99,11 +75,7 @@ preset?: {
99
75
  *
100
76
  * 与 `handleIncoming` 的唯一差别:**目标由机器级订阅表逐条解析,且没有 CCC 级回退**
101
77
  * (方案 §3.1:机器级模式下**未命中 = 不投递**,不是"投给默认角色")。
102
- * 解析出来后把**目标根**当 `root` 交给 `handleIncoming`(并带上 `preset` ⇒ 它不再自行路由、
103
- * 也不看目标 CCC 的 `weixin.enabled`)。
104
- *
105
- * 🔴 为什么"起 poller"与"路由"必须同批(本件即 P0-2 的后半):机器级 poller 收到消息时
106
- * **不知道它属于哪个 CCC** —— 那个答案是订阅表给的。没有订阅表 ⇒ **能收不能投**。
78
+ * 解析出来后把**目标根**当 `root` 交给 `handleIncoming`(并带上 `preset`)。
107
79
  *
108
80
  * 🔵 **导出供测试直接驱动**(与 `handleIncoming` 同款:它俩都是"单条消息的入口",
109
81
  * 生产路径 `runAccountLoop` 只是循环壳)。
@@ -212,4 +184,3 @@ export declare function weixinBridgeStatus(): Array<{
212
184
  * 事件驱动(对齐 autotrajectory v1.26.15)。
213
185
  */
214
186
  export declare function registerWeixinBridge(ctx: Context): void;
215
- export {};
@@ -7,7 +7,7 @@
7
7
  * 后果实测:`acc-extras` / `ops` / `skiff-admin` 三个测试套件因解析不到宿主包而**整文件加载失败**
8
8
  * (它们只用到 MSM 注册表逻辑)。叶模块把「读」与「写」解耦:读者零依赖,写者自己承担重量。
9
9
  *
10
- * 同款先例:`host/effect.ts`(拆卸登记)、`role/worker/idle.ts`(等待结算)。
10
+ * 同款先例:`host/effect.ts`(拆卸登记)、`trajectory/carrier.ts`(等待结算)。
11
11
  */
12
12
  /** 设置当前入口地址(weixin-send-api 启停时调用;null = 未启动) */
13
13
  export declare function setWeixinSendEndpoint(value: string | null): void;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * im/feishu-account.ts — 飞书账号实现(P3,2026-10-19)
3
+ *
4
+ * ## 它凭什么这么短
5
+ *
6
+ * 这是本件最有价值的一点:**P3 只写了这一个文件**。路由 / 会话 / 提问 / 回复 / 记账 /
7
+ * 媒体落盘全都在中立层(`im/inbound-event.ts`,B1)—— 飞书只提供三样:
8
+ * ① 怎么建连接并收消息 ② 一条飞书消息长什么样 ③ 怎么把文本发回去。
9
+ *
10
+ * owner 那句「和微信一样,只不过是 account 方实现不同」在这里是**代码事实**。
11
+ *
12
+ * ## 三条官方约束(照抄会坏,必须各自处理)
13
+ *
14
+ * 1. 🔴 **回调须 3 秒内返回**(官方:超时触发重推)⇒ 回调里**只入队**,处理全异步。
15
+ * 2. 🔴 **超时会重推** ⇒ 必须按 `message_id` **去重**,否则同一句话答两遍。
16
+ * 3. 🔴 **建连成功 ≠ 能收消息** ⇒ 这两件事分开取证(`getConnectionStatus` 只证明前者)。
17
+ *
18
+ * ## 边界(R↓)
19
+ *
20
+ * - ⛔ 不做路由判断、不做会话管理 —— 那是中立层的。
21
+ * - ⛔ 不解析富文本/图片/文件(本期):只取 `message_type === 'text'` 的正文;
22
+ * 其它类型当作"有消息但没文本"交给中立层(它会静默或降级,**不会静默丢连接**)。
23
+ */
24
+ import type { ImAccountImpl } from './accounts.js';
25
+ import type { ImInboundEvent } from './inbound-event.js';
26
+ /**
27
+ * 飞书 SDK 的最小面(**注入**,不是硬 import)。
28
+ *
29
+ * 🔴 为什么注入(两条理由,一条设计、一条实测):
30
+ * ① **设计**:与微信同款(`installWeixinAccountImpl` 注入 `getUpdates` 等)。
31
+ * 飞书 SDK 是重依赖(protobufjs 等)⇒ 不该成为模块求值时的硬依赖,只在真要起账号时加载。
32
+ * ② **实测**:`vi.mock` 拦不住它 —— 本模块用 `await import('@larksuiteoapi/node-sdk')`,
33
+ * 而该包在 pnpm 下是**外置依赖**,vitest 的动态 import 不被 mock 拦截。实测证据:
34
+ * 测试里跑出了真 SDK 自己的日志 `[error]: ['[ws]', 'invalid appId: cli_x']`
35
+ * ⇒ 若坚持 mock,测的就是"能不能骗过测试装置",不是被测对象。
36
+ * ⇒ 注入后,测试直接给替身,**没有魔法**。
37
+ */
38
+ export interface FeishuSdk {
39
+ Client: new (p: {
40
+ appId: string;
41
+ appSecret: string;
42
+ }) => {
43
+ im?: {
44
+ v1?: {
45
+ message?: {
46
+ create?: (p: unknown) => Promise<unknown>;
47
+ };
48
+ };
49
+ };
50
+ };
51
+ EventDispatcher: new (p?: unknown) => {
52
+ register: (h: Record<string, (d: never) => unknown>) => unknown;
53
+ };
54
+ WSClient: new (p: {
55
+ appId: string;
56
+ appSecret: string;
57
+ }) => {
58
+ start: (p: {
59
+ eventDispatcher: unknown;
60
+ }) => Promise<void>;
61
+ close: (p?: {
62
+ force?: boolean;
63
+ }) => void;
64
+ };
65
+ }
66
+ /**
67
+ * 接入 SDK 加载器(`bridge.ts` 在模块求值时注入真加载器;测试注入替身)。
68
+ * 缺省 = 动态 import 真包 —— 这样生产路径不需要 `bridge.ts` 也能独立跑。
69
+ */
70
+ export declare function installFeishuSdk(loader: () => Promise<FeishuSdk>): void;
71
+ /** 测试辅助:关掉并清空全部长连接(生产零调用) */
72
+ export declare function __closeAllFeishuConnectionsForTest(): void;
73
+ /** 测试辅助:清空去重表(生产零调用) */
74
+ export declare function __resetFeishuSeenForTest(): void;
75
+ /**
76
+ * 从飞书 `im.message.receive_v1` 事件里取出**中立入站事件**。
77
+ *
78
+ * 🔴 这是 P3 唯一的"形状翻译"处,与 `bridge.ts` 里 iLink 的那一处对称。
79
+ * ⚠️ 用户标识用 **open_id**(不是 chat_id):路由表是"人 → 轨迹",
80
+ * 而 chat_id 在群里会变、在单聊里也与人不是一一对应。
81
+ * @returns 无可用发送者/消息体 ⇒ `null`(调用方丢弃并**出声**,不静默)
82
+ */
83
+ export declare function feishuEventToInbound(data: Record<string, unknown>): ImInboundEvent | null;
84
+ /** 飞书账号实现(登记名 = `feishu`;与配置里的 `channel` 取值一致) */
85
+ export declare const feishuAccountImpl: ImAccountImpl;
86
+ /**
87
+ * 注册飞书实现(`bridge.ts` 在**模块求值时**调用)。
88
+ *
89
+ * ⚠️ 与微信同款教训(续 220 实测 12 例红):注册必须在**模块求值**时发生,
90
+ * 不能等 `apply` —— 测试与面板会**直接**调 `syncCccBridge` / `syncHumanChannel`。
91
+ */
92
+ export declare function installFeishuAccountImpl(): void;
@@ -0,0 +1,162 @@
1
+ /**
2
+ * im/inbound-event.ts — **中立入站事件层**(B1,2026-10-19)
3
+ *
4
+ * ## 这一层解决的是什么
5
+ *
6
+ * `handleIncoming` 原先吃的 `msg` 是 **iLink 形状**(`from_user_id` / `context_token` /
7
+ * `item_list` / `message_type`)。飞书的消息形状与它**没有一处相同**,所以接第二家 IM
8
+ * 的真正工作量不在"建连接",而在**把各家消息归一成同一个东西**。
9
+ *
10
+ * 本文件就是那个"同一个东西",以及消费它的**通道中立主流程**。
11
+ *
12
+ * ## 切法(为什么这样切,而不是改 `handleIncoming` 的签名)
13
+ *
14
+ * 🔴 实测爆炸半径:全仓 **约 50 处**测试直接驱动 `handleIncoming` 的 iLink 形状。
15
+ * 直接改签名 ⇒ 那 50 处同时要改 ⇒ 而 B1 的验收判据恰恰是「**微信行为逐字不变**」
16
+ * (用例数守恒 + 真机一条微信)。判据自己会失去可复算性。
17
+ *
18
+ * ⇒ 切法是**适配器**:
19
+ * - 本文件 `handleInboundEvent()` = 真正的主流程,**零 iLink 概念**;
20
+ * - `bridge.ts` 的 `handleIncoming()` 退化成**薄适配器**:iLink 消息 → 中立事件 + iLink 的 io;
21
+ * - 飞书实现自己造中立事件 + 飞书的 io,调到**同一个** `handleInboundEvent`。
22
+ *
23
+ * 收益:微信侧调用点与测试**逐字不动**;第二条、第三条 IM 只是**新增一个适配器**。
24
+ *
25
+ * ## 边界(R↓:这层刻意**不**管什么)
26
+ *
27
+ * - ⛔ **不管连接怎么建**(长轮询 vs WebSocket 回调)—— 那是各账号实现自己的事。
28
+ * - ⛔ **不管凭据怎么取** —— `io` 与事件都由实现处构造好再交进来。
29
+ * - ✅ 只负责:「一条已到达的消息,该投到哪条认知轨迹、怎么答、怎么记账」。
30
+ */
31
+ import type { Context } from 'cordis';
32
+ import { type SkiffProfile } from '../trajectory/policy/profile.js';
33
+ /**
34
+ * 一条**已到达**的入站消息(归一后)。
35
+ *
36
+ * 刻意只留**主流程真正用到的**字段 —— 每多留一个通道特定的字段,这一层就多一处
37
+ * 需要为两家分别解释的东西(E↓)。
38
+ */
39
+ export interface ImInboundEvent {
40
+ /** 发送者在**该通道内**的稳定标识(微信 = from_user_id;飞书 = open_id) */
41
+ userId: string;
42
+ /** 正文(含语音转写)。无文本 ⇒ `null` */
43
+ text: string | null;
44
+ /** 媒体项(图片/文件)。顺序即到达顺序 */
45
+ media: ImMediaItem[];
46
+ /** 有语音但**没有转写文本**(触发降级告知;与 `text` 同时为空时才有意义) */
47
+ hasVoice: boolean;
48
+ /**
49
+ * 回复锚点。微信 = `context_token`(回复必须带上它);飞书**没有这个概念**
50
+ * ⇒ 可选。⛔ 不要在消费侧假设它一定在。
51
+ */
52
+ replyRef?: string;
53
+ }
54
+ /**
55
+ * 一个媒体项:**取字节的动作由通道自己提供**。
56
+ *
57
+ * 为什么把下载放进事件里,而不是让主流程拿着 `item` 去调 `downloadMedia`:
58
+ * 后者是 iLink 的 CDN 协议,飞书是 `im.v1.messageResource.get` —— 两者除了
59
+ * "返回一堆字节"之外没有任何共同点。⇒ 中立层只约定**取字节这个动作**。
60
+ */
61
+ export interface ImMediaItem {
62
+ kind: 'image' | 'file';
63
+ /** 原始文件名(file 才有;**未净化**,净化在落盘侧做) */
64
+ fileName?: string;
65
+ /** 取字节;失败/不可得 ⇒ `null`(主流程**降级告知**,不静默) */
66
+ fetch(): Promise<{
67
+ data: Buffer;
68
+ fileName?: string;
69
+ } | null>;
70
+ }
71
+ /**
72
+ * 账号的**动作面**(发消息 / 打字态)。
73
+ *
74
+ * 与 `ImInboundEvent` 成对:进来的是事件,出去的是动作。两者都不含通道协议细节。
75
+ */
76
+ export interface ImAccountIo {
77
+ /** 发纯文本(实现内部做各自的长度分片与格式转换) */
78
+ sendText(input: {
79
+ toUserId: string;
80
+ text: string;
81
+ replyRef?: string;
82
+ }): Promise<void>;
83
+ /** "正在输入"起始(可实现不了;飞书就没有这个能力) */
84
+ typingStart?(input: {
85
+ toUserId: string;
86
+ replyRef?: string;
87
+ }): Promise<void>;
88
+ /** "正在输入"结束(异常路径也会调 ⇒ 实现须自行容忍未开始的情形) */
89
+ typingStop?(input: {
90
+ toUserId: string;
91
+ }): Promise<void>;
92
+ }
93
+ /**
94
+ * 路由解析结果:目标根 + **轨迹编码** + **该轨迹的 skiff 约束面**。
95
+ * `null` 从本函数出去 ⇒ **不投递**(未命中已留痕 / 目标不存在已响亮出声)。
96
+ */
97
+ export interface ResolvedInboundTarget {
98
+ root: string;
99
+ /** 目标认知轨迹编码(= 新模型里的"身份";旧模型的角色名已退役) */
100
+ trajectory: string;
101
+ /**
102
+ * 该轨迹的 skiff 约束面。
103
+ * ⚠️ **可能为 `null`**:轨迹存在但没有 `skiff.json` ⇒ 它是一条**普通轨迹**,
104
+ * IM 桥照样可以往里投,只是不施加 skiff 约束。
105
+ */
106
+ profile: SkiffProfile | null;
107
+ }
108
+ /**
109
+ * **入站路由的单一解析处**(P0-3):机器级订阅表优先 → CCC 级回退;三档语义都在这里落地。
110
+ *
111
+ * 🔴 为什么抽成一个函数而不是写两遍:**两种模式(CCC 起桥 / 机器级起 poller)走的是同一套
112
+ * 判据**,差别只有一格 —— **有没有 CCC 级回退**(`cccFallback`)。写两遍必然分叉。
113
+ *
114
+ * 三档(方案 §3.1,**刻意分档**):
115
+ * - **未命中** ⇒ 不投递 + **留痕**(静默档:这是"表里没有你",不是错误)
116
+ * - **表残缺** ⇒ **响亮出声**(且**独立于是否命中**)
117
+ * - **目标不存在**(CCC / **轨迹**)⇒ **响亮报错**(不静默丢弃)
118
+ */
119
+ export declare function resolveInboundTarget(ctx: Context, input: {
120
+ account: string;
121
+ user: string;
122
+ cccFallback: {
123
+ root: string;
124
+ trajectory: string;
125
+ } | null;
126
+ }): Promise<ResolvedInboundTarget | null>;
127
+ /**
128
+ * **一条入站消息的主流程**(通道中立)。
129
+ *
130
+ * 职责:路由 → 固定会话 id 创建/延续 → 提问 → 回复(或被闸门拦下后兜底)。
131
+ * 语料(媒体)由事件自带 `fetch()`;出站走的 `io`。
132
+ *
133
+ * 🔴 **本函数没有任何 iLink 概念** —— 这是 B1 的判据。任何 `context_token` /
134
+ * `item_list` / `message_type` 出现在这里,就说明抽缝没抽干净。
135
+ *
136
+ * @param preset 机器级模式:目标已解析好 ⇒ **不再自行路由**,且**不看本 CCC 的 `enabled`**
137
+ */
138
+ export declare function handleInboundEvent(ctx: Context, root: string, accountId: string, io: ImAccountIo, event: ImInboundEvent, preset?: {
139
+ target: ResolvedInboundTarget;
140
+ }): Promise<void>;
141
+ /**
142
+ * 手动输出模式的**机制标记**(v1.30.10 引入;v1.30.16 按「ACC 管机制、CCC 管措辞」重构)。
143
+ *
144
+ * 边界(R↓):ACC 只提供**机制与数据**——"桥不转发最终文本"这个事实 + 三个已填好的参数值。
145
+ * **纪律措辞归 CCC**:怎么写、必须怎么做、后果是什么,全部写在轨迹的 `PROMPT.md` 里。
146
+ */
147
+ export declare function manualOutputMarker(accountId: string, userId: string): string;
148
+ /**
149
+ * 手动输出模式的**兜底判定**(纯函数,v1.30.17——把决策矩阵从装配里抽出来,可独立测试)。
150
+ *
151
+ * 四个条件同时成立才兜底:
152
+ * ① CCC 显式开启 `fallbackOnNoSend`(缺省 false = 严格静默)
153
+ * ② 机械闸门**已装配**(未装配时无法判定"是否发送过" → 宁可静默也不冒重复发送风险)
154
+ * ③ 本轮**没有**成功发送(agent 自己发过 → 输出权归 agent,桥不插话)
155
+ * ④ 有最终文本可转发(空文本无意义)
156
+ */
157
+ export declare function manualOutputFallbackNeeded(input: {
158
+ fallbackOnNoSend?: boolean;
159
+ guardActive: boolean;
160
+ sentThisTurn: boolean;
161
+ answer: string;
162
+ }): boolean;
package/lib/im/route.d.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  * 配置归属(S142 用户拍板):**CCC 级**——serenity.json 结构 + localstore 凭据分离;
12
12
  * 不绑定具体 role(用户自选路由目标)。
13
13
  */
14
- import { type WeixinSettings, type WeixinRouteConfig, type WeixinAccountConfig } from '../kernel/ccc.js';
14
+ import { type WeixinSettings, type WeixinRouteConfig, type WeixinAccountConfig, type ImSettings } from '../kernel/ccc.js';
15
15
  import type { WeixinMediaRef, WeixinMessageItem } from './ilink.js';
16
16
  /** 判定 sessionId 是否为微信桥会话(**命名形态**判定;权力面判据见 `isSkiffSession`) */
17
17
  export declare function isWeixinSessionId(sessionId: string | undefined): boolean;
@@ -39,9 +39,31 @@ export declare function isWeixinSessionId(sessionId: string | undefined): boolea
39
39
  * 见 `privacy-scan` 的 `weixin-session-pseudonym` 规则 —— 新形态连这个问题都没有了)。
40
40
  */
41
41
  export declare function weixinSessionIdForTrajectory(code: string): string;
42
+ /**
43
+ * **统一 IM 配置的单一读取入口**(2026-10-19 owner 定向:微信与飞书只是账号实现不同,
44
+ * 整合到统一配置)。
45
+ *
46
+ * 读取顺序(**确定性,只有这一处**):
47
+ * ① `im` 段 —— 但**判据是 `accounts` 是数组**,不是"段存在"(见下 🔴)
48
+ * ② 旧 `weixin` 段 —— 存量 CCC 零迁移
49
+ * ③ 都没有 ⇒ 归一默认(`enabled:false`、空数组)
50
+ *
51
+ * 🔴 **为什么判据是「`accounts` 是数组」**:本 CCC 的配置里**曾经**有过一个 `im` 段
52
+ * (退役的 ACP 形态:`{channel:"acp", acp:{enabled, accounts:[…]}}`)。若只判"段存在",
53
+ * 那份残留会被当成新配置读 ⇒ 得到一个没有 `accounts` 数组 / 或 `accounts` 形状完全不同的对象
54
+ * ⇒ 表现为**"配置没生效"且不报错**。⇒ 用可判别的形状把它排除掉(退役残留继续被静默忽略,
55
+ * 与 v1.59.0 的裁决一致)。
56
+ *
57
+ * ⚠️ 本函数**只做归一**,不判断"这个通道实现有没有"——那是装配处的事
58
+ * (`syncMachinePollers` 里有"未实现的 channel 出声但不静默跳过")。
59
+ */
60
+ export declare function readImSettings(root: string, paths?: string[]): ImSettings;
42
61
  /**
43
62
  * 读取 CCC 微信桥配置(未配置/未启用 → 归一默认)。
44
- * @returns 归一化 WeixinSettings(accounts/routes 数组清洗;空段 → 空数组)
63
+ *
64
+ * ⚠️ 2026-10-19 起它是 {@link readImSettings} 的**薄别名**(签名与返回形状不变):
65
+ * 旧调用点无需改动,但**配置来源已经统一**——`im` 段优先、`weixin` 段回退。
66
+ * 保留名字是为了让既有调用点与测试**逐字不动**(P1 是纯重构,读数必须不变)。
45
67
  */
46
68
  export declare function readWeixinSettings(root: string, paths?: string[]): WeixinSettings;
47
69
  /** 账号凭据(localstore credential scope;baseUrl 缺省 = 官方默认) */
@@ -56,6 +78,19 @@ export declare function readWeixinCredential(root: string, accountId: string): W
56
78
  export declare function writeWeixinCredential(root: string, accountId: string, cred: WeixinAccountCredential): void;
57
79
  /** 清除账号凭据(移除账号时) */
58
80
  export declare function clearWeixinCredential(root: string, accountId: string): void;
81
+ /** 飞书账号凭据(appSecret 缺 ⇒ 账号未绑定) */
82
+ export interface FeishuAccountCredential {
83
+ /** 应用 appId(自建应用「凭证与基础信息」页) */
84
+ appId: string;
85
+ /** 应用 appSecret(**凭据**,只在 localstore) */
86
+ appSecret: string;
87
+ }
88
+ /** 读取飞书账号凭据(appId 或 appSecret 缺失 → null——账号未绑定) */
89
+ export declare function readFeishuCredential(root: string, accountId: string): FeishuAccountCredential | null;
90
+ /** 写入飞书账号凭据(面板保存后;两者都必写——只写一半等于没绑) */
91
+ export declare function writeFeishuCredential(root: string, accountId: string, cred: FeishuAccountCredential): void;
92
+ /** 清除飞书账号凭据(移除账号时) */
93
+ export declare function clearFeishuCredential(root: string, accountId: string): void;
59
94
  /**
60
95
  * 路由匹配:exact user 优先,通配 `*` 兜底。
61
96
  * @returns 命中的**轨迹编码**;未命中 → null(无路由 → 消息不回复/忽略)
@@ -0,0 +1,45 @@
1
+ /**
2
+ * im/weixin-account.ts — 微信账号实现(P2:把既有微信轮询包成 `ImAccountImpl`)
3
+ *
4
+ * 🔴 **纯移动,零逻辑改动**。`runAccountLoop` 的函数体逐字来自 `bridge.ts`;
5
+ * 唯一新增的是把它**登记成"一种账号实现"**,这样装配处不必再写死 `channel === 'weixin'`。
6
+ *
7
+ * 为什么值得单独成一个文件(而不是留在 bridge.ts 里):
8
+ * 装配处从此只认 `ImAccountImpl` 接口;微信与飞书是**并列的两个实现**,
9
+ * 而不是"微信是主角、别的往里塞"。这正是 owner「只不过是 account 方实现不同」的结构表达。
10
+ */
11
+ import type { WeixinAccountCredential } from './route.js';
12
+ import { type ImAccountImpl } from './accounts.js';
13
+ /**
14
+ * 把实现所需的最小注入面收进来。
15
+ *
16
+ * ⚠️ 这些是**微信侧的运行依赖**(`getUpdates` / `handleIncoming` / `handleMachineIncoming`)。
17
+ * 用注入而不是 import,是为了让 `bridge.ts` 不因本文件而反向依赖 —— 否则两个文件互相
18
+ * import 会成环(本轨已有先例:`rebuild ⇄ tools/trajectory` 那个人工发现的环)。
19
+ */
20
+ export interface WeixinLoopDeps {
21
+ getUpdates: (input: {
22
+ baseUrl: string;
23
+ token: string;
24
+ getUpdatesBuf: string;
25
+ timeoutMs: number;
26
+ }) => Promise<{
27
+ get_updates_buf?: string;
28
+ msgs?: Array<Record<string, unknown>>;
29
+ }>;
30
+ handleIncoming: (ctx: unknown, root: string, accountId: string, cred: WeixinAccountCredential, msg: never) => Promise<void>;
31
+ handleMachineIncoming: (ctx: unknown, accountId: string, cred: WeixinAccountCredential, msg: never) => Promise<void>;
32
+ }
33
+ /** 装配时注入依赖(`bridge.ts` 在模块初始化末尾调用一次;见 `installWeixinAccountImpl`) */
34
+ export declare function setWeixinLoopDeps(d: WeixinLoopDeps): void;
35
+ /** 微信账号实现(登记的 channel 名 = `weixin`;与配置里的取值一致) */
36
+ export declare const weixinAccountImpl: ImAccountImpl;
37
+ /**
38
+ * 注册微信实现**并注入依赖**(`bridge.ts` 调用;幂等——重复注册覆盖同名 channel)。
39
+ *
40
+ * ⚠️ 必须在首次 `syncHumanChannel` / `syncCccBridge` **之前**发生。
41
+ * `bridge.ts` 在**模块求值时**调用它一次(不是等到 `registerWeixinBridge`)——
42
+ * 因为测试与面板会**直接**调 `syncCccBridge`,那时装配入口还没跑过;
43
+ * 若依赖装配入口注册,那些路径会全部走进"未实现"分支(本轮实测:12 例红,根因就是它)。
44
+ */
45
+ export declare function installWeixinAccountImpl(d: WeixinLoopDeps): void;
package/lib/index.d.ts CHANGED
@@ -164,10 +164,6 @@ export interface Config {
164
164
  gatewayEnabled?: Volatile<boolean | undefined>;
165
165
  /** F2 触发阈值(K token;面板层;部署层同义键 = `rebuild.thresholdK`) */
166
166
  rebuildThresholdK?: Volatile<number | undefined>;
167
- /** F4 Skiff 调试服务总开关(面板层;部署层同义键 = `skiff.enabled`) */
168
- skiffEnabled?: Volatile<boolean | undefined>;
169
- /** F4 Skiff 调试端口(面板层;部署层同义键 = `skiff.debugPort`) */
170
- skiffDebugPort?: Volatile<number | undefined>;
171
167
  /** CRO(轨迹自编程唤起)总闸(面板层;无部署层同义键;缺省开) */
172
168
  croEnabled?: Volatile<boolean | undefined>;
173
169
  /** **diagram 工具总闸**(面板层;部署层同义键 = `diagram.enabled`;缺省关;D104) */
@@ -201,13 +197,6 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
201
197
  }>>, Schemastery.ObjectT<NoInfer<{
202
198
  thresholdK: z<number, number, "defined">;
203
199
  }>>, "plain">;
204
- skiff: z<Schemastery.ObjectS<NoInfer<{
205
- enabled: z<boolean, boolean, "defined">;
206
- debugPort: z<number, number, "defined">;
207
- }>>, Schemastery.ObjectT<NoInfer<{
208
- enabled: z<boolean, boolean, "defined">;
209
- debugPort: z<number, number, "defined">;
210
- }>>, "plain">;
211
200
  webFetch: z<Schemastery.ObjectS<NoInfer<{
212
201
  enabled: z<boolean, boolean, "defined">;
213
202
  }>>, Schemastery.ObjectT<NoInfer<{
@@ -257,8 +246,6 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
257
246
  }>>, "plain">;
258
247
  gatewayEnabled: z<boolean, boolean, "volatile">;
259
248
  rebuildThresholdK: z<number, number, "volatile">;
260
- skiffEnabled: z<boolean, boolean, "volatile">;
261
- skiffDebugPort: z<number, number, "volatile">;
262
249
  croEnabled: z<boolean, boolean, "volatile">;
263
250
  diagramEnabled: z<boolean, boolean, "volatile">;
264
251
  }>>, Schemastery.ObjectT<NoInfer<{
@@ -283,13 +270,6 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
283
270
  }>>, Schemastery.ObjectT<NoInfer<{
284
271
  thresholdK: z<number, number, "defined">;
285
272
  }>>, "plain">;
286
- skiff: z<Schemastery.ObjectS<NoInfer<{
287
- enabled: z<boolean, boolean, "defined">;
288
- debugPort: z<number, number, "defined">;
289
- }>>, Schemastery.ObjectT<NoInfer<{
290
- enabled: z<boolean, boolean, "defined">;
291
- debugPort: z<number, number, "defined">;
292
- }>>, "plain">;
293
273
  webFetch: z<Schemastery.ObjectS<NoInfer<{
294
274
  enabled: z<boolean, boolean, "defined">;
295
275
  }>>, Schemastery.ObjectT<NoInfer<{
@@ -339,8 +319,6 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
339
319
  }>>, "plain">;
340
320
  gatewayEnabled: z<boolean, boolean, "volatile">;
341
321
  rebuildThresholdK: z<number, number, "volatile">;
342
- skiffEnabled: z<boolean, boolean, "volatile">;
343
- skiffDebugPort: z<number, number, "volatile">;
344
322
  croEnabled: z<boolean, boolean, "volatile">;
345
323
  diagramEnabled: z<boolean, boolean, "volatile">;
346
324
  }>>, "plain">;