@shgroup/dsh-serenity-hooks 1.72.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 (41) hide show
  1. package/dsh.plugin.json +1 -1
  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/bridge.d.ts +33 -62
  12. package/lib/im/endpoint.d.ts +1 -1
  13. package/lib/im/feishu-account.d.ts +92 -0
  14. package/lib/im/inbound-event.d.ts +162 -0
  15. package/lib/im/route.d.ts +13 -0
  16. package/lib/index.d.ts +0 -22
  17. package/lib/index.js +700 -914
  18. package/lib/kernel/ccc.d.ts +2 -2
  19. package/lib/kernel/ports.d.ts +2 -4
  20. package/lib/lib-TxscFKeo.js +119355 -0
  21. package/lib/{message-source-kVoNK0C5.js → message-source-ChyVfev9.js} +2 -2
  22. package/lib/{profile-DyMJ_bfr.js → profile-BIQ2widI.js} +14 -11
  23. package/lib/rolldown-runtime-B4iAMlE-.js +35 -0
  24. package/lib/{route-f7JlinIt.js → route-ItCgAsnz.js} +19 -4
  25. package/lib/seams/guards.d.ts +1 -1
  26. package/lib/{settings-lt1i3OZv.js → settings-UcwIPJhM.js} +13 -62
  27. package/lib/skills/resource-index.d.ts +1 -1
  28. package/lib/surface/settings.d.ts +0 -6
  29. package/lib/surface/text.d.ts +72 -0
  30. package/lib/surface/usage.d.ts +2 -2
  31. package/lib/{role/worker/idle.d.ts → trajectory/carrier.d.ts} +12 -2
  32. package/lib/{role/worker → trajectory/delegate}/core.d.ts +7 -1
  33. package/lib/{role/worker → trajectory/delegate}/preset.d.ts +9 -2
  34. package/lib/{role/skiff → trajectory/policy}/profile.d.ts +9 -2
  35. package/lib/{role/skiff/config.d.ts → trajectory/policy/rules.d.ts} +9 -3
  36. package/lib/{role/skiff/registry.d.ts → trajectory/policy/window.d.ts} +7 -1
  37. package/lib/trajectory/prompt.d.ts +1 -1
  38. package/lib/{role/skiff/core.d.ts → trajectory/session.d.ts} +13 -4
  39. package/package.json +2 -1
  40. package/lib/role/skiff/debug.d.ts +0 -88
  41. package/lib/rolldown-runtime-8H4AJuhK.js +0 -14
package/lib/index.js CHANGED
@@ -1,12 +1,12 @@
1
- import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
2
- import { c as hostInjected, d as hostSessions, f as hostSettings, h as hostWebServer, i as cccRootForExec, l as hostLlm, m as hostWeb, n as agentCwdFor, o as listCccs, p as hostSubagents, r as cccRootForCwd, s as hostAgents, t as NO_CCC_FROM_AGENT_CWD, u as hostService } from "./ccc-roots-jZYR2GVE.js";
3
- import { _ as resolveInside, a as findSerenityRoot$1, c as matchBlacklist, d as readCccName$1, f as readCheapWorkerConfig, g as readUtf8, h as readSafeModeHiddenTools, i as findGitRoot, l as pathInside, m as readExclusiveTools, n as SAFE_MODE_MARKER, o as isSafeModeOn, p as readCognitiveTrajectorySkills, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-Dkoa2oW3.js";
4
- import { A as sessionEvents$1, C as listSessions, D as readActiveSessionMd, E as parseTrajectoryCode, F as useSession, I as isoLocal, L as localFileStamp, M as setActiveSessionInfo, N as showSession, O as readTrajectoryUid, P as summarize, R as localHuman, S as listActiveTrajectories, T as parseSessionContextFromEvents, _ as createSession, a as readSkiffProfileAt, b as getActiveSessionInfo, c as registerSkiffSession$1, d as unregisterSkiffSession$1, f as COGNITIVE_TRAJECTORY_ACTIONS, g as clearActiveSessionInfo, h as archiveSession, j as sessionsRoot, k as resolveSessionByTitle, l as skiffSessionInfo$1, m as UID_FILENAME, n as isSkiffSession, o as readSkiffProfileByCode, p as DEFAULT_SESSION_SCOPE, r as listSkiffProfiles, s as skiffProfileForSession, t as SKIFF_PROFILE_FILENAME, u as skiffTrajectoryDirNameFor, v as extractSessionMdPathFromText, w as namingTitleFor, y as findSession, z as localIdStamp } from "./profile-DyMJ_bfr.js";
5
- import { a as readWeixinCredential, c as sanitizeFileName$1, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, g as runLocalStore, h as readStore, i as matchWeixinRoute, l as weixinInboundDir, m as readGitTrack, n as extractWeixinText, o as readWeixinSettings, p as localstorePath, r as hasVoiceItem, t as extractWeixinMedia, u as weixinSessionIdForTrajectory } from "./route-f7JlinIt.js";
6
- import { _ as resolveProfileSystemPrompt, a as GATEWAY_PORT, c as SKIFF_DEBUG_PORT, d as buildSkiffBasePrompt, f as createProfilePromptReader, g as profileToolWhitelist, h as profileSystemPromptSource, i as FACE_PORTS, l as WEIXIN_SEND_PORT, m as profileMsmWhitelist, n as readSimpleSettings, o as MAIN_WEB_PORT, p as profileCognitiveTrajectory, r as registerSettingsSection, s as MECHANISM_PORTS, u as SKIFF_PROFILE_HINT, v as ACC_VERSION, y as findPackageRoot } from "./settings-lt1i3OZv.js";
7
- import { a as pruneMissingBindings, c as setBindingStore, i as listBoundSessionIds, n as PLUGIN_SOURCE, o as readLastBound, r as appendBound, s as resolveSessionCognitiveTrajectoryLabel, t as ACC_MESSAGE_KIND } from "./message-source-kVoNK0C5.js";
1
+ import { i as __toESM, n as __exportAll } from "./rolldown-runtime-B4iAMlE-.js";
2
+ import { c as hostInjected, d as hostSessions, f as hostSettings, h as hostWebServer, i as cccRootForExec, l as hostLlm, m as hostWeb, n as agentCwdFor, o as listCccs, p as hostSubagents, r as cccRootForCwd, s as hostAgents, t as NO_CCC_FROM_AGENT_CWD, u as hostService } from "./ccc-roots-BhoA7G-0.js";
3
+ import { _ as resolveInside, a as findSerenityRoot$1, c as matchBlacklist, d as readCccName$1, f as readCheapWorkerConfig, g as readUtf8, h as readSafeModeHiddenTools, i as findGitRoot, l as pathInside, m as readExclusiveTools, n as SAFE_MODE_MARKER, o as isSafeModeOn, p as readCognitiveTrajectorySkills, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-CZUOq3NY.js";
4
+ import { A as sessionsRoot, C as namingTitleFor, D as readTrajectoryUid, E as readActiveSessionMd, F as isoLocal, I as localFileStamp, L as localHuman, M as showSession, N as summarize, O as resolveSessionByTitle, P as useSession, R as localIdStamp, S as listSessions, T as parseTrajectoryCode, _ as extractSessionMdPathFromText, a as readSkiffProfileAt, c as registerSkiffSession$1, d as COGNITIVE_TRAJECTORY_ACTIONS, f as DEFAULT_SESSION_SCOPE, g as createSession, h as clearActiveSessionInfo, j as setActiveSessionInfo, k as sessionEvents$1, l as skiffTrajectoryDirNameFor, m as archiveSession, n as isSkiffSession, o as readSkiffProfileByCode, p as UID_FILENAME, r as listSkiffProfiles, s as skiffProfileForSession, t as SKIFF_PROFILE_FILENAME, u as unregisterSkiffSession$1, v as findSession, w as parseSessionContextFromEvents, x as listActiveTrajectories, y as getActiveSessionInfo } from "./profile-BIQ2widI.js";
5
+ import { _ as runLocalStore, a as readFeishuCredential, d as weixinSessionIdForTrajectory, f as LOCALSTORE_SCOPES, g as readStore, h as readGitTrack, i as matchWeixinRoute, l as sanitizeFileName$1, m as localstorePath, n as extractWeixinText, o as readWeixinCredential, p as checkLocalstoreGitCompliance, r as hasVoiceItem, s as readWeixinSettings, t as extractWeixinMedia, u as weixinInboundDir } from "./route-ItCgAsnz.js";
6
+ import { a as buildSkiffBasePrompt, c as profileMsmWhitelist, d as resolveProfileSystemPrompt, f as ACC_VERSION, i as SKIFF_PROFILE_HINT, l as profileSystemPromptSource, n as readSimpleSettings, o as createProfilePromptReader, p as findPackageRoot, r as registerSettingsSection, s as profileCognitiveTrajectory, u as profileToolWhitelist } from "./settings-UcwIPJhM.js";
7
+ import { a as pruneMissingBindings, c as setBindingStore, i as listBoundSessionIds, n as PLUGIN_SOURCE, o as readLastBound, r as appendBound, s as resolveSessionCognitiveTrajectoryLabel, t as ACC_MESSAGE_KIND } from "./message-source-ChyVfev9.js";
8
8
  import { REGISTERED_TOOLS } from "./invariant.js";
9
- import { c as sendTextMessage, i as getUpdates, l as sendTyping, n as downloadMedia, o as markdownToPlainText, r as getConfig, s as sendFileMessage, t as TypingStatus, u as sniffImageExt } from "./ilink-DEpmE3yi.js";
9
+ import { c as sendTextMessage, i as getUpdates, l as sendTyping, n as downloadMedia, o as markdownToPlainText, r as getConfig, s as sendFileMessage, t as TypingStatus, u as sniffImageExt } from "./ilink-BMk2w2KY.js";
10
10
  import { createRequire } from "node:module";
11
11
  import z from "@deepseek-ai/schemastery";
12
12
  import { defineTool } from "@deepseek-ai/dsh-tools";
@@ -981,7 +981,7 @@ const gitTool = defineTool({
981
981
  * 后果实测:`acc-extras` / `ops` / `skiff-admin` 三个测试套件因解析不到宿主包而**整文件加载失败**
982
982
  * (它们只用到 MSM 注册表逻辑)。叶模块把「读」与「写」解耦:读者零依赖,写者自己承担重量。
983
983
  *
984
- * 同款先例:`host/effect.ts`(拆卸登记)、`role/worker/idle.ts`(等待结算)。
984
+ * 同款先例:`host/effect.ts`(拆卸登记)、`trajectory/carrier.ts`(等待结算)。
985
985
  */
986
986
  let endpoint = null;
987
987
  /** 设置当前入口地址(weixin-send-api 启停时调用;null = 未启动) */
@@ -1958,7 +1958,7 @@ function trajectoryPromptBlock(text) {
1958
1958
  ].join("\n");
1959
1959
  }
1960
1960
  //#endregion
1961
- //#region src/role/worker/idle.ts
1961
+ //#region src/trajectory/carrier.ts
1962
1962
  /**
1963
1963
  * 等待 agent 空闲或销毁。
1964
1964
  * @param ctx 宿主插件上下文(事件通道来源;**结构化访问**,故类型为 `unknown`,见文件头 I2 段)
@@ -2105,7 +2105,7 @@ function truncateContent(content, maxChars) {
2105
2105
  * 六条硬性规则(逐条对应设计 §4.2,改这里先改那里):
2106
2106
  * ① **只枚举,不注入正文** —— 索引里**绝不**出现任何参考文件的正文;
2107
2107
  * ② **确定性排序** = 相对路径的 **UTF-8 字节序**(🔴 **不用 mtime**:mtime 随检出漂移,
2108
- * 会让每请求的注入文本抖动;对照 `src/wake/cro.ts:101` 那份**落盘快照**用 {name,bytes,mtime} ——
2108
+ * 会让每请求的注入文本抖动;对照 `src/cro/core.ts:101` 那份**落盘快照**用 {name,bytes,mtime} ——
2109
2109
  * **同一件事两处的取舍相反,那个差异是判据不是巧合**);
2110
2110
  * ③ **有界 + 响亮** —— 条数/字符各有上限,超出**必须显式写出还剩多少项**,**绝不静默截断**;
2111
2111
  * ④ **摘要可复现** —— 取该文件首个 `#` 标题(剥 markdown 记号);取不到 ⇒ 留空,**不编**;
@@ -2600,7 +2600,7 @@ function buildCognitiveTrajectorySkillsSection(root, mdPath, maxChars = COGNITIV
2600
2600
  * ⇒ 本模块让每个 CCC 留下一份**纯计数**账本:**用了哪个、多少次、第一次和最近一次是什么时候**。
2601
2601
  * 用途 = 清理/精简的**依据**(没有人用的东西与天天在用的东西,不再靠猜)。
2602
2602
  *
2603
- * ## 🔴 铁律(与 `wake/cro-log.ts` 同族)
2603
+ * ## 🔴 铁律(与 `cro/log.ts` 同族)
2604
2604
  *
2605
2605
  * **本模块每个导出函数都不抛错**:读盘 / 解析 / 写盘失败一律返回结构化失败,
2606
2606
  * 由调用方只记一行日志(或什么都不做)⇒ **统计的问题绝不影响工具执行与既有链路**。
@@ -2616,7 +2616,7 @@ function buildCognitiveTrajectorySkillsSection(root, mdPath, maxChars = COGNITIV
2616
2616
  *
2617
2617
  * 宿主会被重启(本容器实测 24h 内 3 次)⇒ 计数**不能只活在内存里**:
2618
2618
  * 每次写盘 = **读旧文件 → 在旧值上自增 → 原子写**(`tmp + rename`,与 `wake/registry.ts` /
2619
- * `wake/cro-log.ts` 同款形态)⇒ **进程重启后计数自然延续**,无需额外的 flush/落盘时机。
2619
+ * `cro/log.ts` 同款形态)⇒ **进程重启后计数自然延续**,无需额外的 flush/落盘时机。
2620
2620
  *
2621
2621
  * ## 口径(两个字段,勿合并)
2622
2622
  *
@@ -2899,6 +2899,48 @@ function verifyTotpCode(secretBase32, code, nowSeconds = nowEpochSeconds()) {
2899
2899
  return null;
2900
2900
  }
2901
2901
  //#endregion
2902
+ //#region src/kernel/ports.ts
2903
+ /**
2904
+ * 集中端口表。
2905
+ * A=宿主主面(插件不监听)/ B=网关(可配,默认关)/ C=微信发送(默认开,仅 loopback)
2906
+ * / D=Skiff 调试(默认关)。
2907
+ */
2908
+ const FACE_PORTS = {
2909
+ main: {
2910
+ port: 3080,
2911
+ host: null,
2912
+ defaultEnabled: true,
2913
+ meaning: "宿主 DSH 主 WebUI + 插件 /serenity/* 路由面(**插件不起此 listener**,端口自宿主读)"
2914
+ },
2915
+ gateway: {
2916
+ port: 3081,
2917
+ host: "0.0.0.0",
2918
+ defaultEnabled: false,
2919
+ meaning: "双端口网关(外部登录 + 全量反代宿主主面;host/port 走 plugin 全局配置)"
2920
+ },
2921
+ weixinSend: {
2922
+ port: 3082,
2923
+ host: "127.0.0.1",
2924
+ defaultEnabled: true,
2925
+ meaning: "微信主动发送面(容器外进程 → POST /send;与 3080 显式解耦,不经网关)"
2926
+ }
2927
+ };
2928
+ FACE_PORTS.main.port;
2929
+ /** 网关端口默认值(面 B) */
2930
+ const GATEWAY_PORT = FACE_PORTS.gateway.port;
2931
+ /** 微信主动发送端口默认值(面 C) */
2932
+ const WEIXIN_SEND_PORT = FACE_PORTS.weixinSend.port;
2933
+ /**
2934
+ * **机制端口**词表(output-guard 用:agent 在**用户可见输出**里提及内部端口即打回)。
2935
+ *
2936
+ * 由 {@link FACE_PORTS} 机械派生 ⇒ 与面清单**永不漂移**。
2937
+ * ⚠️ 修复记录(C4 块 B,2026-09-15):旧常量硬写 `['3080','3081','3099','3100']`
2938
+ * ——**漏 3082**(微信发送面,默认开)⇒ 守卫拦不住"3082"这类提及。现由表派生,四面齐全。
2939
+ * ⚠️ 面变更记录(2026-10-15):**面 E(3100 ACP/问答)已整体退役**(owner 裁「砍掉外部问答」)
2940
+ * ⇒ 本词表随之少一项,**无需另改**(这正是"派生而非手抄"的收益)。
2941
+ */
2942
+ const MECHANISM_PORTS = Object.keys(FACE_PORTS).map((id) => String(FACE_PORTS[id].port));
2943
+ //#endregion
2902
2944
  //#region src/kernel/config.ts
2903
2945
  /**
2904
2946
  * kernel/config.ts — plugin 全局配置读写(结构化,`~/.dsh/serenity-hooks.json`)
@@ -3768,13 +3810,19 @@ function registerCognitiveTrajectorySkillSection(agent, root) {
3768
3810
  }
3769
3811
  }
3770
3812
  //#endregion
3771
- //#region src/role/worker/core.ts
3813
+ //#region src/trajectory/delegate/core.ts
3772
3814
  /**
3773
- * role/worker/core.ts — cheap-worker(杂工)纯逻辑层(零 DSH 依赖,可独立单测)
3815
+ * trajectory/delegate/core.ts — cheap-worker(杂工)纯逻辑层(零 DSH 依赖,可独立单测)
3774
3816
  *
3775
3817
  * 🆕 2026-10-14(owner 令「删除后台运行能力」):**进度文件 / stop token / 轮次循环**整族已退场 ——
3776
3818
  * 它们**只**服务于 background 模式。本模块现存 = 前台委派真正需要的三件:
3777
3819
  * 模型串解析 / 白名单校验 / provider 可用性判读(+ 使用指引文本)。
3820
+ *
3821
+ * ## S14(2026-10-19,轨迹中心重构):此前位于 `role/worker/` 下(该目录已消失)
3822
+ *
3823
+ * 落点 = `trajectory/delegate/`。判据(判读表 §3.1):它是**轨迹 × 轨迹**那种合法形态 ——
3824
+ * 主语是「主轨迹把活交给子 agent」,既不是某一条轨迹的切面,也不是容器宪法。
3825
+ * ⚠️ 它**不属于** `policy/`:policy 管"这条轨迹允许什么",本模块管"一条轨迹怎么把活派出去"。
3778
3826
  */
3779
3827
  /** 解析 model 字符串(provider/model)→ {provider, model};无 / 视为 model-only */
3780
3828
  function splitModel(model) {
@@ -3910,13 +3958,9 @@ Before calling cheap-worker, load eap (acc-eap skill) and design the "scale-up c
3910
3958
  (and "output" for whatever partial text was produced).
3911
3959
  `;
3912
3960
  //#endregion
3913
- //#region src/role/skiff/core.ts
3914
- /** `provider/model` → agentOptions(由 worker/core 提供,保持单一实现) */
3961
+ //#region src/trajectory/session.ts
3962
+ /** `provider/model` → agentOptions(由 delegate/core 提供,保持单一实现) */
3915
3963
  const skiffAgents = /* @__PURE__ */ new Map();
3916
- /** 查 sessionId 的会话绑定(轨迹 + ccc;无 → null)——追问校验 */
3917
- function skiffSessionInfo(sessionId) {
3918
- return skiffSessionInfo$1(sessionId);
3919
- }
3920
3964
  /** 查 sessionId 的活体 agent(进程内会话延续用;未注册/已清理 → undefined) */
3921
3965
  function getSkiffAgent(sessionId) {
3922
3966
  return skiffAgents.get(sessionId);
@@ -10043,9 +10087,9 @@ function listWakes(root, opts = {}) {
10043
10087
  };
10044
10088
  }
10045
10089
  //#endregion
10046
- //#region src/wake/cro.ts
10090
+ //#region src/cro/core.ts
10047
10091
  /**
10048
- * wake/cro.ts — CRO(Continuous Re-Occurrence · 持续再发生)机制层(S142 §7.8,2026-09-19)
10092
+ * cro/core.ts — CRO(Continuous Re-Occurrence · 持续再发生)机制层(S142 §7.8,2026-09-19)
10049
10093
  *
10050
10094
  * ## 这东西用来干什么(白话)
10051
10095
  *
@@ -10669,9 +10713,9 @@ function registerDisposer(ctx, label, cleanup) {
10669
10713
  }
10670
10714
  }
10671
10715
  //#endregion
10672
- //#region src/wake/cro-turns.ts
10716
+ //#region src/cro/turns.ts
10673
10717
  /**
10674
- * wake/cro-turns.ts — 「这条轨迹是否正在跑轮次」的追踪(设计 §3.2,2026-09-19 S142)
10718
+ * cro/turns.ts — 「这条轨迹是否正在跑轮次」的追踪(设计 §3.2,2026-09-19 S142)
10675
10719
  *
10676
10720
  * ## 为什么需要这个模块(CRO 唯一需要 ACC 新增的状态)
10677
10721
  *
@@ -10797,7 +10841,7 @@ function runningCarriersOf(boundSessionIds, nowMs, ttlMs = TURN_TTL_MS) {
10797
10841
  * 🔴 **本处顺带更正 §2.7 的一处分档**:本文件原被列在"剩余 3 个属**真依赖**档"里,
10798
10842
  * 但**逐符号查证后与那个判断不符** ——
10799
10843
  * · `Context`:`ctx` 只被**结构化访问**(原码本就写着 `ctx as unknown as { on: … }`)
10800
- * ⇒ 该注解未提供任何类型保证(与 `role/worker/idle.ts` 同档)。
10844
+ * ⇒ 该注解未提供任何类型保证(与 `trajectory/carrier.ts` 同档)。
10801
10845
  * · `Agent`:**导入后从未使用** —— 事件 payload 一律按 `{ agent?: unknown }` 解构,
10802
10846
  * 再交给 `sessionIdOf(...)`(它收 `unknown`)。⇒ **纯粹是死 import**。
10803
10847
  * ⇒ **判据(本件再证一次,且比上一件更硬)**:分档**必须逐符号看"它有没有参与类型检查"**,
@@ -10845,9 +10889,9 @@ function registerCroTurnTracking(ctx) {
10845
10889
  });
10846
10890
  }
10847
10891
  //#endregion
10848
- //#region src/wake/cro-log.ts
10892
+ //#region src/cro/log.ts
10849
10893
  /**
10850
- * wake/cro-log.ts — CRO 唤起日志(**ACC 侧**,每轨迹一份固定名文件,2026-09-20 S142 §7.16)
10894
+ * cro/log.ts — CRO 唤起日志(**ACC 侧**,每轨迹一份固定名文件,2026-09-20 S142 §7.16)
10851
10895
  *
10852
10896
  * ## 所有者令(本模块的存在理由)
10853
10897
  *
@@ -10883,7 +10927,7 @@ function registerCroTurnTracking(ctx) {
10883
10927
  * ⇒ 清理与写入**是同一笔操作** ⇒ **结构上不存在"忘了清"的可能**(比"另设一个每 tick 的清理步"更硬:
10884
10928
  * 那多一个"可能忘了跑"的面)。窗宽是**常量**,**无配置键、无人工清理动作**。
10885
10929
  *
10886
- * ## 🔴 铁律(与 `wake/cro.ts` 同族)
10930
+ * ## 🔴 铁律(与 `cro/core.ts` 同族)
10887
10931
  *
10888
10932
  * **本模块的每个导出函数都不抛错**:写盘 / 裁剪 / 解析失败一律返回结构化失败,
10889
10933
  * 由调用方只记一行 tick 日志 ⇒ **CRO 流水的问题绝不影响投递与既有链路**(设计 §5)。
@@ -12006,9 +12050,9 @@ function registerWakeScheduler(ctx) {
12006
12050
  registerDisposer(ctx, "认知轨迹唤醒调度器", () => clock.dispose());
12007
12051
  }
12008
12052
  //#endregion
12009
- //#region src/wake/cro-guide.ts
12053
+ //#region src/cro/guide.ts
12010
12054
  /**
12011
- * wake/cro-guide.ts — CRO 编写指南(**ACC 内置**,S142 §7.8,2026-09-19)
12055
+ * cro/guide.ts — CRO 编写指南(**ACC 内置**,S142 §7.8,2026-09-19)
12012
12056
  *
12013
12057
  * ## 为什么指南在 ACC 而不在 CCC(所有者令)
12014
12058
  *
@@ -15312,7 +15356,7 @@ function saveFileToTmp(root, fileName, data) {
15312
15356
  writeFileSync(join(dir, filename), bytes);
15313
15357
  return `${FILE_UPLOAD_DIR}/${filename}`;
15314
15358
  }
15315
- function readBody$3(req, maxBytes) {
15359
+ function readBody$2(req, maxBytes) {
15316
15360
  return new Promise((resolve, reject) => {
15317
15361
  let data = "";
15318
15362
  req.on("data", (chunk) => {
@@ -15326,7 +15370,7 @@ function readBody$3(req, maxBytes) {
15326
15370
  req.on("error", reject);
15327
15371
  });
15328
15372
  }
15329
- function sendJson$3(res, code, body) {
15373
+ function sendJson$2(res, code, body) {
15330
15374
  const payload = JSON.stringify(body);
15331
15375
  res.writeHead(code, {
15332
15376
  "content-type": "application/json; charset=utf-8",
@@ -15351,7 +15395,7 @@ function senderIsWebUi(req) {
15351
15395
  */
15352
15396
  function requireWebUi(req, res, message) {
15353
15397
  if (senderIsWebUi(req)) return true;
15354
- sendJson$3(res, 403, { error: message });
15398
+ sendJson$2(res, 403, { error: message });
15355
15399
  return false;
15356
15400
  }
15357
15401
  /**
@@ -15409,11 +15453,11 @@ function registerStatusApi(ctx, opts = {}) {
15409
15453
  handler: async (req, res) => {
15410
15454
  try {
15411
15455
  if (req.method !== "POST") {
15412
- sendJson$3(res, 405, { error: "method not allowed" });
15456
+ sendJson$2(res, 405, { error: "method not allowed" });
15413
15457
  return;
15414
15458
  }
15415
15459
  if (!requireWebUi(req, res, "图片落盘仅限 WebUI(client 专用)")) return;
15416
- const raw = await readBody$3(req, 20971520);
15460
+ const raw = await readBody$2(req, 20971520);
15417
15461
  const body = JSON.parse(raw);
15418
15462
  const workspace = resolveWorkspace(ctx, {
15419
15463
  sessionId: body.sessionId,
@@ -15421,12 +15465,12 @@ function registerStatusApi(ctx, opts = {}) {
15421
15465
  });
15422
15466
  const root = cccRootForCwd(workspace);
15423
15467
  if (!root) {
15424
- sendJson$3(res, 404, { error: `no CCC found from workspace: ${workspace}` });
15468
+ sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
15425
15469
  return;
15426
15470
  }
15427
- sendJson$3(res, 200, { path: saveImageToTmp(root, body.mediaType ?? "", body.data ?? "") });
15471
+ sendJson$2(res, 200, { path: saveImageToTmp(root, body.mediaType ?? "", body.data ?? "") });
15428
15472
  } catch (err) {
15429
- sendJson$3(res, 400, { error: err.message ?? String(err) });
15473
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
15430
15474
  }
15431
15475
  }
15432
15476
  });
@@ -15436,11 +15480,11 @@ function registerStatusApi(ctx, opts = {}) {
15436
15480
  handler: async (req, res) => {
15437
15481
  try {
15438
15482
  if (req.method !== "POST") {
15439
- sendJson$3(res, 405, { error: "method not allowed" });
15483
+ sendJson$2(res, 405, { error: "method not allowed" });
15440
15484
  return;
15441
15485
  }
15442
15486
  if (!requireWebUi(req, res, "文件落盘仅限 WebUI(client 专用)")) return;
15443
- const raw = await readBody$3(req, 20971520);
15487
+ const raw = await readBody$2(req, 20971520);
15444
15488
  const body = JSON.parse(raw);
15445
15489
  const workspace = resolveWorkspace(ctx, {
15446
15490
  sessionId: body.sessionId,
@@ -15448,12 +15492,12 @@ function registerStatusApi(ctx, opts = {}) {
15448
15492
  });
15449
15493
  const root = cccRootForCwd(workspace);
15450
15494
  if (!root) {
15451
- sendJson$3(res, 404, { error: `no CCC found from workspace: ${workspace}` });
15495
+ sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
15452
15496
  return;
15453
15497
  }
15454
- sendJson$3(res, 200, { path: saveFileToTmp(root, body.name ?? "", body.data ?? "") });
15498
+ sendJson$2(res, 200, { path: saveFileToTmp(root, body.name ?? "", body.data ?? "") });
15455
15499
  } catch (err) {
15456
- sendJson$3(res, 400, { error: err.message ?? String(err) });
15500
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
15457
15501
  }
15458
15502
  }
15459
15503
  });
@@ -15469,7 +15513,7 @@ function registerStatusApi(ctx, opts = {}) {
15469
15513
  workspace: url.searchParams.get("workspace") ?? void 0
15470
15514
  }), configPaths);
15471
15515
  const runtime = ctx.get("codeRuntime");
15472
- sendJson$3(res, 200, {
15516
+ sendJson$2(res, 200, {
15473
15517
  ...status,
15474
15518
  ...runtime === void 0 ? { codeRuntime: null } : { codeRuntime: {
15475
15519
  language: runtime.language,
@@ -15480,7 +15524,7 @@ function registerStatusApi(ctx, opts = {}) {
15480
15524
  }
15481
15525
  if (req.method === "POST") {
15482
15526
  if (!requireWebUi(req, res, "safe-mode 切换仅限 WebUI(agent 不可自行开关)")) return;
15483
- const raw = await readBody$3(req, 65536);
15527
+ const raw = await readBody$2(req, 65536);
15484
15528
  const body = JSON.parse(raw);
15485
15529
  const workspace = resolveWorkspace(ctx, {
15486
15530
  sessionId: body.sessionId,
@@ -15488,19 +15532,19 @@ function registerStatusApi(ctx, opts = {}) {
15488
15532
  });
15489
15533
  const root = cccRootForCwd(workspace);
15490
15534
  if (!root) {
15491
- sendJson$3(res, 404, { error: `no CCC found from workspace: ${workspace}` });
15535
+ sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
15492
15536
  return;
15493
15537
  }
15494
15538
  const result = setSafeMode(root, body.on === true);
15495
- sendJson$3(res, 200, {
15539
+ sendJson$2(res, 200, {
15496
15540
  ...getStatus(workspace, configPaths),
15497
15541
  ...result
15498
15542
  });
15499
15543
  return;
15500
15544
  }
15501
- sendJson$3(res, 405, { error: "method not allowed" });
15545
+ sendJson$2(res, 405, { error: "method not allowed" });
15502
15546
  } catch (err) {
15503
- sendJson$3(res, 400, { error: err.message ?? String(err) });
15547
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
15504
15548
  }
15505
15549
  }
15506
15550
  });
@@ -15516,24 +15560,24 @@ function registerStatusApi(ctx, opts = {}) {
15516
15560
  try {
15517
15561
  known = projectKnownWorkspaces(hostService(ctx, "workspaceRegistry")?.list?.() ?? [], settings.gateway.workspaces);
15518
15562
  } catch {}
15519
- sendJson$3(res, 200, {
15563
+ sendJson$2(res, 200, {
15520
15564
  config: toWire(settings),
15521
15565
  knownWorkspaces: known
15522
15566
  });
15523
15567
  return;
15524
15568
  }
15525
15569
  if (req.method === "PUT") {
15526
- const raw = await readBody$3(req, 131072);
15570
+ const raw = await readBody$2(req, 131072);
15527
15571
  const saved = applyWirePatch(JSON.parse(raw).config ?? {});
15528
15572
  try {
15529
15573
  ctx.emit?.("serenity/config-updated");
15530
15574
  } catch {}
15531
- sendJson$3(res, 200, { config: toWire(saved) });
15575
+ sendJson$2(res, 200, { config: toWire(saved) });
15532
15576
  return;
15533
15577
  }
15534
- sendJson$3(res, 405, { error: "method not allowed" });
15578
+ sendJson$2(res, 405, { error: "method not allowed" });
15535
15579
  } catch (err) {
15536
- sendJson$3(res, 400, { error: err.message ?? String(err) });
15580
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
15537
15581
  }
15538
15582
  }
15539
15583
  });
@@ -15543,7 +15587,7 @@ function registerStatusApi(ctx, opts = {}) {
15543
15587
  handler: async (req, res) => {
15544
15588
  try {
15545
15589
  if (req.method !== "GET") {
15546
- sendJson$3(res, 405, { error: "method not allowed" });
15590
+ sendJson$2(res, 405, { error: "method not allowed" });
15547
15591
  return;
15548
15592
  }
15549
15593
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
@@ -15552,12 +15596,12 @@ function registerStatusApi(ctx, opts = {}) {
15552
15596
  workspace: url.searchParams.get("workspace") ?? void 0
15553
15597
  });
15554
15598
  const root = cccRootForCwd(workspace) ?? "";
15555
- sendJson$3(res, 200, { cccs: await listCccs(ctx, {
15599
+ sendJson$2(res, 200, { cccs: await listCccs(ctx, {
15556
15600
  defaultRoot: root,
15557
15601
  withRoles: true
15558
15602
  }) });
15559
15603
  } catch (err) {
15560
- sendJson$3(res, 400, { error: err.message ?? String(err) });
15604
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
15561
15605
  }
15562
15606
  }
15563
15607
  });
@@ -15571,21 +15615,21 @@ function registerStatusApi(ctx, opts = {}) {
15571
15615
  if (req.method === "GET") {
15572
15616
  const chk = requireCcc(url.searchParams.get("ccc") ?? void 0);
15573
15617
  if (!chk.ok) {
15574
- sendJson$3(res, 400, { error: chk.error });
15618
+ sendJson$2(res, 400, { error: chk.error });
15575
15619
  return;
15576
15620
  }
15577
- const { readWeixinSettings } = await import("./route-f7JlinIt.js").then((n) => n.s);
15621
+ const { readWeixinSettings } = await import("./route-ItCgAsnz.js").then((n) => n.c);
15578
15622
  const { weixinBridgeStatus } = await Promise.resolve().then(() => bridge_exports);
15579
15623
  const settings = readWeixinSettings(chk.root);
15580
15624
  const bridge = weixinBridgeStatus().find((b) => b.ccc === chk.root);
15581
- const { readWeixinCredential } = await import("./route-f7JlinIt.js").then((n) => n.s);
15625
+ const { readWeixinCredential } = await import("./route-ItCgAsnz.js").then((n) => n.c);
15582
15626
  const accounts = (settings.accounts ?? []).map((a) => ({
15583
15627
  accountId: a.accountId,
15584
15628
  name: a.name ?? void 0,
15585
15629
  enabled: a.enabled !== false,
15586
15630
  bound: readWeixinCredential(chk.root, a.accountId) !== null
15587
15631
  }));
15588
- sendJson$3(res, 200, {
15632
+ sendJson$2(res, 200, {
15589
15633
  enabled: settings.enabled === true,
15590
15634
  botType: settings.botType ?? void 0,
15591
15635
  accounts,
@@ -15595,22 +15639,22 @@ function registerStatusApi(ctx, opts = {}) {
15595
15639
  return;
15596
15640
  }
15597
15641
  if (req.method !== "POST") {
15598
- sendJson$3(res, 405, { error: "method not allowed" });
15642
+ sendJson$2(res, 405, { error: "method not allowed" });
15599
15643
  return;
15600
15644
  }
15601
- const raw = await readBody$3(req, 131072);
15645
+ const raw = await readBody$2(req, 131072);
15602
15646
  const body = JSON.parse(raw);
15603
15647
  const chk = requireCcc(body.ccc);
15604
15648
  if (!chk.ok) {
15605
- sendJson$3(res, 400, { error: chk.error });
15649
+ sendJson$2(res, 400, { error: chk.error });
15606
15650
  return;
15607
15651
  }
15608
15652
  const root = chk.root;
15609
15653
  if (body.action === "login-start") {
15610
- const { fetchQRCode } = await import("./ilink-DEpmE3yi.js").then((n) => n.a);
15654
+ const { fetchQRCode } = await import("./ilink-BMk2w2KY.js").then((n) => n.a);
15611
15655
  const { randomUUID } = await import("node:crypto");
15612
15656
  for (const [key, v] of weixinLogins) if (Date.now() - v.startedAt > WEIXIN_LOGIN_TTL_MS) weixinLogins.delete(key);
15613
- const { readWeixinSettings } = await import("./route-f7JlinIt.js").then((n) => n.s);
15657
+ const { readWeixinSettings } = await import("./route-ItCgAsnz.js").then((n) => n.c);
15614
15658
  const qr = await fetchQRCode({ botType: readWeixinSettings(root).botType ?? void 0 });
15615
15659
  const loginKey = randomUUID();
15616
15660
  weixinLogins.set(loginKey, {
@@ -15619,7 +15663,7 @@ function registerStatusApi(ctx, opts = {}) {
15619
15663
  startedAt: Date.now(),
15620
15664
  polling: false
15621
15665
  });
15622
- sendJson$3(res, 200, {
15666
+ sendJson$2(res, 200, {
15623
15667
  qrcode: qr.qrcode,
15624
15668
  qrcode_img_content: qr.qrcode_img_content,
15625
15669
  loginKey
@@ -15628,20 +15672,20 @@ function registerStatusApi(ctx, opts = {}) {
15628
15672
  }
15629
15673
  if (body.action === "remove-account") {
15630
15674
  if (!body.accountId) {
15631
- sendJson$3(res, 400, { error: "missing accountId" });
15675
+ sendJson$2(res, 400, { error: "missing accountId" });
15632
15676
  return;
15633
15677
  }
15634
- const { removeWeixinAccount } = await import("./route-f7JlinIt.js").then((n) => n.s);
15678
+ const { removeWeixinAccount } = await import("./route-ItCgAsnz.js").then((n) => n.c);
15635
15679
  const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
15636
15680
  removeWeixinAccount(root, body.accountId);
15637
15681
  syncCccBridge(ctx, root);
15638
- sendJson$3(res, 200, { removed: body.accountId });
15682
+ sendJson$2(res, 200, { removed: body.accountId });
15639
15683
  return;
15640
15684
  }
15641
15685
  if (body.action === "save-routes") {
15642
15686
  const raw = body.routes ?? [];
15643
15687
  if (!Array.isArray(raw)) {
15644
- sendJson$3(res, 400, { error: "invalid routes (expected [{user, trajectory}, ...])" });
15688
+ sendJson$2(res, 400, { error: "invalid routes (expected [{user, trajectory}, ...])" });
15645
15689
  return;
15646
15690
  }
15647
15691
  const routes = [];
@@ -15649,7 +15693,7 @@ function registerStatusApi(ctx, opts = {}) {
15649
15693
  const user = typeof r?.user === "string" ? r.user : "";
15650
15694
  const code = typeof r?.trajectory === "string" && r.trajectory !== "" ? r.trajectory : typeof r?.role === "string" ? r.role : "";
15651
15695
  if (user === "" || code === "") {
15652
- sendJson$3(res, 400, { error: "invalid routes (each row needs non-empty user and trajectory)" });
15696
+ sendJson$2(res, 400, { error: "invalid routes (each row needs non-empty user and trajectory)" });
15653
15697
  return;
15654
15698
  }
15655
15699
  routes.push({
@@ -15657,23 +15701,23 @@ function registerStatusApi(ctx, opts = {}) {
15657
15701
  trajectory: code
15658
15702
  });
15659
15703
  }
15660
- const { saveWeixinRoutes } = await import("./route-f7JlinIt.js").then((n) => n.s);
15661
- const { findDirNameByCode } = await import("./profile-DyMJ_bfr.js").then((n) => n.i);
15704
+ const { saveWeixinRoutes } = await import("./route-ItCgAsnz.js").then((n) => n.c);
15705
+ const { findDirNameByCode } = await import("./profile-BIQ2widI.js").then((n) => n.i);
15662
15706
  for (const r of routes) if (findDirNameByCode(root, r.trajectory) === null) {
15663
- sendJson$3(res, 400, { error: `unknown trajectory: ${r.trajectory} (no such trajectory code in ${root})` });
15707
+ sendJson$2(res, 400, { error: `unknown trajectory: ${r.trajectory} (no such trajectory code in ${root})` });
15664
15708
  return;
15665
15709
  }
15666
15710
  saveWeixinRoutes(root, routes);
15667
- sendJson$3(res, 200, { saved: routes.length });
15711
+ sendJson$2(res, 200, { saved: routes.length });
15668
15712
  return;
15669
15713
  }
15670
15714
  if (body.action === "set-enabled") {
15671
15715
  const enabled = body.enabled === true;
15672
- const { readWeixinSettings, setWeixinEnabled } = await import("./route-f7JlinIt.js").then((n) => n.s);
15716
+ const { readWeixinSettings, setWeixinEnabled } = await import("./route-ItCgAsnz.js").then((n) => n.c);
15673
15717
  const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
15674
15718
  const settings = readWeixinSettings(root);
15675
15719
  if (enabled && (settings.accounts ?? []).length === 0) {
15676
- sendJson$3(res, 400, { error: "启用前请先扫码绑定至少一个账号" });
15720
+ sendJson$2(res, 400, { error: "启用前请先扫码绑定至少一个账号" });
15677
15721
  return;
15678
15722
  }
15679
15723
  setWeixinEnabled(root, enabled);
@@ -15682,12 +15726,12 @@ function registerStatusApi(ctx, opts = {}) {
15682
15726
  const { stopCccBridge } = await Promise.resolve().then(() => bridge_exports);
15683
15727
  stopCccBridge(root);
15684
15728
  }
15685
- sendJson$3(res, 200, { enabled });
15729
+ sendJson$2(res, 200, { enabled });
15686
15730
  return;
15687
15731
  }
15688
- sendJson$3(res, 400, { error: `unsupported action: ${body.action ?? ""}` });
15732
+ sendJson$2(res, 400, { error: `unsupported action: ${body.action ?? ""}` });
15689
15733
  } catch (err) {
15690
- sendJson$3(res, 400, { error: err.message ?? String(err) });
15734
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
15691
15735
  }
15692
15736
  }
15693
15737
  });
@@ -15698,22 +15742,22 @@ function registerStatusApi(ctx, opts = {}) {
15698
15742
  try {
15699
15743
  if (!requireWebUi(req, res, "微信桥配置仅限 WebUI(client 专用)")) return;
15700
15744
  if (req.method !== "GET") {
15701
- sendJson$3(res, 405, { error: "method not allowed" });
15745
+ sendJson$2(res, 405, { error: "method not allowed" });
15702
15746
  return;
15703
15747
  }
15704
15748
  const key = new URL(req.url ?? "/", "http://127.0.0.1").searchParams.get("key") ?? "";
15705
15749
  const login = weixinLogins.get(key);
15706
15750
  if (!login) {
15707
- sendJson$3(res, 404, { error: "login not found or expired" });
15751
+ sendJson$2(res, 404, { error: "login not found or expired" });
15708
15752
  return;
15709
15753
  }
15710
15754
  if (Date.now() - login.startedAt > WEIXIN_LOGIN_TTL_MS) {
15711
15755
  weixinLogins.delete(key);
15712
- sendJson$3(res, 200, { status: "expired" });
15756
+ sendJson$2(res, 200, { status: "expired" });
15713
15757
  return;
15714
15758
  }
15715
- const { pollQRStatus } = await import("./ilink-DEpmE3yi.js").then((n) => n.a);
15716
- const { readWeixinSettings } = await import("./route-f7JlinIt.js").then((n) => n.s);
15759
+ const { pollQRStatus } = await import("./ilink-BMk2w2KY.js").then((n) => n.a);
15760
+ const { readWeixinSettings } = await import("./route-ItCgAsnz.js").then((n) => n.c);
15717
15761
  const settings = readWeixinSettings(login.root);
15718
15762
  const status = await pollQRStatus({
15719
15763
  baseUrl: void 0,
@@ -15722,13 +15766,13 @@ function registerStatusApi(ctx, opts = {}) {
15722
15766
  switch (status.status) {
15723
15767
  case "confirmed": {
15724
15768
  if (!status.bot_token) {
15725
- sendJson$3(res, 200, {
15769
+ sendJson$2(res, 200, {
15726
15770
  status: "error",
15727
15771
  error: "confirmed but no bot_token"
15728
15772
  });
15729
15773
  return;
15730
15774
  }
15731
- const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./route-f7JlinIt.js").then((n) => n.s);
15775
+ const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./route-ItCgAsnz.js").then((n) => n.c);
15732
15776
  const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
15733
15777
  const accountId = nextWeixinAccountId(settings);
15734
15778
  upsertWeixinAccount(login.root, {
@@ -15743,7 +15787,7 @@ function registerStatusApi(ctx, opts = {}) {
15743
15787
  });
15744
15788
  syncCccBridge(ctx, login.root);
15745
15789
  weixinLogins.delete(key);
15746
- sendJson$3(res, 200, {
15790
+ sendJson$2(res, 200, {
15747
15791
  status: "confirmed",
15748
15792
  accountId,
15749
15793
  tokenSaved: true
@@ -15752,14 +15796,14 @@ function registerStatusApi(ctx, opts = {}) {
15752
15796
  }
15753
15797
  case "expired":
15754
15798
  weixinLogins.delete(key);
15755
- sendJson$3(res, 200, { status: "expired" });
15799
+ sendJson$2(res, 200, { status: "expired" });
15756
15800
  return;
15757
15801
  default:
15758
- sendJson$3(res, 200, { status: status.status ?? "wait" });
15802
+ sendJson$2(res, 200, { status: status.status ?? "wait" });
15759
15803
  return;
15760
15804
  }
15761
15805
  } catch (err) {
15762
- sendJson$3(res, 400, { error: err.message ?? String(err) });
15806
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
15763
15807
  }
15764
15808
  }
15765
15809
  });
@@ -15781,7 +15825,7 @@ function registerStatusApi(ctx, opts = {}) {
15781
15825
  const root = sessionsRootDir();
15782
15826
  if (req.method === "GET") {
15783
15827
  const { candidates } = performCleanup(root, cutoffDaysAgo(days), liveIds, { dryRun: true });
15784
- sendJson$3(res, 200, {
15828
+ sendJson$2(res, 200, {
15785
15829
  root,
15786
15830
  olderThanDays: days,
15787
15831
  dryRun: true,
@@ -15795,12 +15839,12 @@ function registerStatusApi(ctx, opts = {}) {
15795
15839
  return;
15796
15840
  }
15797
15841
  if (req.method !== "POST") {
15798
- sendJson$3(res, 405, { error: "method not allowed" });
15842
+ sendJson$2(res, 405, { error: "method not allowed" });
15799
15843
  return;
15800
15844
  }
15801
15845
  if (!requireWebUi(req, res, "会话清理仅限 WebUI(client 专用)")) return;
15802
15846
  const { result } = performCleanup(root, cutoffDaysAgo(days), liveIds);
15803
- sendJson$3(res, 200, {
15847
+ sendJson$2(res, 200, {
15804
15848
  root,
15805
15849
  olderThanDays: days,
15806
15850
  dryRun: false,
@@ -15808,7 +15852,7 @@ function registerStatusApi(ctx, opts = {}) {
15808
15852
  errors: result.errors
15809
15853
  });
15810
15854
  } catch (err) {
15811
- sendJson$3(res, 400, { error: err.message ?? String(err) });
15855
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
15812
15856
  }
15813
15857
  }
15814
15858
  });
@@ -15987,7 +16031,7 @@ function loadVoyageHtml(assetPath = voyageAssetPath()) {
15987
16031
  return readFileSync(assetPath, "utf-8");
15988
16032
  }
15989
16033
  /** 发 JSON(错误分支用;与 surface/http.ts 的 sendJson 同形,但本模块不依赖它,保持零耦合) */
15990
- function sendJson$2(res, status, body) {
16034
+ function sendJson$1(res, status, body) {
15991
16035
  res.statusCode = status;
15992
16036
  res.setHeader("content-type", "application/json; charset=utf-8");
15993
16037
  res.end(JSON.stringify(body));
@@ -16005,14 +16049,14 @@ function registerVoyageApi(ctx, opts = {}) {
16005
16049
  path: VOYAGE_PATH,
16006
16050
  handler: (req, res) => {
16007
16051
  if (req.method !== "GET") {
16008
- sendJson$2(res, 405, { error: "method not allowed" });
16052
+ sendJson$1(res, 405, { error: "method not allowed" });
16009
16053
  return;
16010
16054
  }
16011
16055
  let html;
16012
16056
  try {
16013
16057
  html = loadVoyageHtml(opts.assetPath ?? voyageAssetPath());
16014
16058
  } catch (err) {
16015
- sendJson$2(res, 500, { error: `voyage asset unavailable: ${err?.message ?? String(err)}` });
16059
+ sendJson$1(res, 500, { error: `voyage asset unavailable: ${err?.message ?? String(err)}` });
16016
16060
  return;
16017
16061
  }
16018
16062
  let root = null;
@@ -16650,10 +16694,6 @@ var FaceStartCancelled = class extends Error {
16650
16694
  this.name = "FaceStartCancelled";
16651
16695
  }
16652
16696
  };
16653
- /** 面是否在监听 */
16654
- function faceActive(name) {
16655
- return faces.has(name);
16656
- }
16657
16697
  /** 面的实际监听端口(未在监听 → null) */
16658
16698
  function facePort(name) {
16659
16699
  return faces.get(name)?.handle.port ?? null;
@@ -17240,7 +17280,7 @@ function createDshCookieProvider(connection, authority) {
17240
17280
  /** 面名(C4 块 A:listener 生命周期归 `face-host`) */
17241
17281
  const FACE_GATEWAY = "gateway";
17242
17282
  /** 读取请求体(≤ maxBytes;超限 reject) */
17243
- function readBody$2(req, maxBytes) {
17283
+ function readBody$1(req, maxBytes) {
17244
17284
  return new Promise((resolve, reject) => {
17245
17285
  let data = "";
17246
17286
  req.on("data", (chunk) => {
@@ -17367,7 +17407,7 @@ async function startGateway(config, getAccounts, getDshCookie) {
17367
17407
  const session = authed(req);
17368
17408
  if (session !== void 0) {
17369
17409
  if (req.method === "POST" && isWorkspaceCreatePath(url.pathname)) {
17370
- readBody$2(req, 131072).then((body) => {
17410
+ readBody$1(req, 131072).then((body) => {
17371
17411
  const { rpcId, path } = parseWorkspaceCreateBody(body);
17372
17412
  if (!allowWorkspaceCreate) {
17373
17413
  res.writeHead(403, { "content-type": "application/json" });
@@ -19687,234 +19727,16 @@ f.parseInline;
19687
19727
  b.parse;
19688
19728
  x.lex;
19689
19729
  //#endregion
19690
- //#region src/role/skiff/debug.ts
19691
- /** 面名(C4 块 A) */
19692
- const FACE_SKIFF_DEBUG = "skiff-debug";
19693
- /** 监听地址:**只绑 loopback**(本面无认证——仅靠 loopback 挡住外部) */
19694
- const SKIFF_DEBUG_HOST = FACE_PORTS.skiffDebug.host ?? "127.0.0.1";
19695
- function skiffDebugActive() {
19696
- return faceActive(FACE_SKIFF_DEBUG);
19697
- }
19698
- function readBody$1(req) {
19699
- return new Promise((resolve, reject) => {
19700
- const chunks = [];
19701
- req.on("data", (c) => chunks.push(c));
19702
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
19703
- req.on("error", reject);
19704
- });
19705
- }
19706
- function sendJson$1(res, status, payload) {
19707
- const body = JSON.stringify(payload);
19708
- res.writeHead(status, {
19709
- "Content-Type": "application/json; charset=utf-8",
19710
- "Cache-Control": "no-store"
19711
- });
19712
- res.end(body);
19713
- }
19714
- function sendHtml(res, html) {
19715
- res.writeHead(200, {
19716
- "Content-Type": "text/html; charset=utf-8",
19717
- "Cache-Control": "no-store"
19718
- });
19719
- res.end(html);
19720
- }
19721
- /** 问答页 HTML:CCC 切换器 + 角色下拉 + 输入 + 答案区 + 轨迹区(JS 渲染)+ WebUI 链接 */
19722
- function skiffDebugPage(cccs, defaultRoot, webPort) {
19723
- const data = jscSafeJsonText(JSON.stringify(cccs).replace(/</g, "\\u003c"));
19724
- return `<!DOCTYPE html>
19725
- <html lang="zh">
19726
- <head>
19727
- <meta charset="utf-8">
19728
- <meta name="viewport" content="width=device-width, initial-scale=1">
19729
- <title>Skiff Debug — CCC cognitive subset roles</title>
19730
- <style>
19731
- :root { color-scheme: light dark; }
19732
- body { font-family: system-ui, -apple-system, sans-serif; margin: 0; padding: 24px; background: #f6f7f9; color: #1f2328; }
19733
- @media (prefers-color-scheme: dark) { body { background: #1a1b1e; color: #e6e6e6; } }
19734
- main { max-width: 720px; margin: 0 auto; }
19735
- h1 { font-size: 18px; margin: 0 0 4px; }
19736
- .sub { opacity: .65; font-size: 13px; margin-bottom: 16px; }
19737
- .ccc { display: inline-block; padding: 3px 10px; border-radius: 999px; background: rgba(11,168,117,.12); color: #0ba875; font-size: 12px; font-weight: 600; margin-bottom: 12px; word-break: break-all; }
19738
- @media (prefers-color-scheme: dark) { .ccc { color: #3ddc9a; } }
19739
- .sessionbar { display: flex; align-items: center; gap: 10px; margin-bottom: 4px; }
19740
- .session-badge { font-family: ui-monospace, SFMono-Regular, monospace; font-size: 12px; padding: 3px 10px; border-radius: 999px; background: rgba(127,127,127,.12); word-break: break-all; }
19741
- .session-badge.continued { background: rgba(11,168,117,.12); color: #0ba875; }
19742
- @media (prefers-color-scheme: dark) { .session-badge.continued { color: #3ddc9a; } }
19743
- button.ghost { background: transparent; color: #57606a; border: 1px solid #d0d7de; padding: 4px 12px; font-size: 12px; border-radius: 8px; cursor: pointer; margin-top: 0; }
19744
- @media (prefers-color-scheme: dark) { button.ghost { color: #8b949e; border-color: #3a3d42; } }
19745
- button.ghost:hover { background: rgba(127,127,127,.1); }
19746
- label { font-size: 13px; font-weight: 600; display: block; margin: 12px 0 4px; }
19747
- select, textarea { width: 100%; box-sizing: border-box; padding: 8px 10px; border-radius: 8px; border: 1px solid #d0d7de; background: #fff; color: inherit; font-size: 14px; }
19748
- @media (prefers-color-scheme: dark) { select, textarea { background: #26282c; border-color: #3a3d42; } }
19749
- textarea { min-height: 72px; resize: vertical; }
19750
- button { margin-top: 12px; padding: 9px 18px; border-radius: 8px; border: 0; background: #0ba875; color: #fff; font-size: 14px; font-weight: 600; cursor: pointer; }
19751
- button:disabled { opacity: .55; cursor: wait; }
19752
- #answer { background: #fff; border: 1px solid #d0d7de; border-radius: 8px; padding: 12px; margin-top: 16px; font-size: 14px; line-height: 1.6; min-height: 48px; word-break: break-word; }
19753
- @media (prefers-color-scheme: dark) { #answer { background: #26282c; border-color: #3a3d42; } }
19754
- /* Markdown 渲染(v1.25.8):代码/标题/列表/引用/think 折叠 */
19755
- #answer p { margin: 6px 0; }
19756
- #answer h1, #answer h2, #answer h3 { margin: 12px 0 6px; font-size: 15px; line-height: 1.4; }
19757
- #answer h1 { font-size: 17px; }
19758
- #answer code { background: rgba(127,127,127,.14); border-radius: 4px; padding: 1px 5px; font-size: 13px; font-family: ui-monospace, SFMono-Regular, monospace; }
19759
- #answer pre { background: rgba(127,127,127,.1); border-radius: 8px; padding: 10px 12px; overflow-x: auto; margin: 8px 0; }
19760
- #answer pre code { background: none; padding: 0; font-size: 13px; line-height: 1.5; }
19761
- #answer ul, #answer ol { margin: 6px 0; padding-left: 22px; }
19762
- #answer li { margin: 2px 0; }
19763
- #answer blockquote { margin: 6px 0; padding: 2px 12px; border-left: 3px solid rgba(127,127,127,.35); opacity: .88; }
19764
- #answer a { color: #0ba875; text-decoration: underline; }
19765
- details.think { margin: 8px 0; border: 1px solid rgba(210,153,34,.4); border-radius: 8px; background: rgba(210,153,34,.06); }
19766
- details.think summary { cursor: pointer; padding: 6px 10px; font-size: 12px; color: #d29922; font-weight: 600; user-select: none; list-style-position: inside; }
19767
- details.think summary:hover { opacity: .8; }
19768
- details.think .think-body { padding: 2px 12px 10px; font-size: 13px; opacity: .78; white-space: normal; }
19769
- #cognitiveTrajectory { margin-top: 12px; font-size: 13px; }
19770
- .t-entry { border-left: 2px solid #d0d7de; padding: 6px 10px; margin: 6px 0; border-radius: 0 6px 6px 0; background: rgba(127,127,127,.06); white-space: pre-wrap; word-break: break-word; }
19771
- .t-user { border-left-color: #0ba875; }
19772
- .t-tool { border-left-color: #d29922; opacity: .85; font-family: ui-monospace, monospace; font-size: 12px; }
19773
- .t-role { font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; opacity: .6; margin-bottom: 2px; }
19774
- .muted { opacity: .6; font-size: 13px; }
19775
- a { color: #0ba875; }
19776
- .err { color: #cf222e; white-space: pre-wrap; }
19777
- </style>
19778
- </head>
19779
- <body>
19780
- <main>
19781
- <h1>Skiff Debug</h1>
19782
- <div class="sub">宁静号轨迹的子集角色问答页(实验性)。可切换 CCC;同一会话可追问,点「新对话」另起一条;回答在服务端渲染,think 块折叠。</div>
19783
- <div class="ccc" id="cccBadge"></div>
19784
- <div class="sessionbar">
19785
- <span id="sessionBadge" class="session-badge muted">(新会话)</span>
19786
- <button id="newChat" type="button" class="ghost">新对话</button>
19787
- </div>
19788
- <label for="ccc">认知容器</label>
19789
- <select id="ccc"></select>
19790
- <label for="role">角色</label>
19791
- <select id="role"></select>
19792
- <label for="q">问题</label>
19793
- <textarea id="q" placeholder="向该角色提问…(追问直接继续输入)"></textarea>
19794
- <button id="ask">提问</button>
19795
- <div id="answer" class="muted">等待提问…</div>
19796
- <div id="cognitiveTrajectory"></div>
19797
- <p class="muted"><a href="${`http://127.0.0.1:${webPort}`}" target="_blank" rel="noopener">在 dsh WebUI 查看完整会话</a>(会话列表搜索 sessionId;WebUI 有完整交互)</p>
19798
- </main>
19799
- <script id="skiff-data" type="application/json" data-default="${escapeHtml(defaultRoot)}">${data}<\/script>
19800
- <script>
19801
- const CCCS = JSON.parse(document.getElementById('skiff-data').textContent)
19802
- const defaultRoot = document.getElementById('skiff-data').dataset.default
19803
- const cccSel = document.getElementById('ccc')
19804
- const roleSel = document.getElementById('role')
19805
- const badge = document.getElementById('cccBadge')
19806
- const sessionBadge = document.getElementById('sessionBadge')
19807
- const newChatBtn = document.getElementById('newChat')
19808
- const btn = document.getElementById('ask')
19809
- const answer = document.getElementById('answer')
19810
- const traj = document.getElementById('cognitiveTrajectory')
19811
- const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]))
19812
- let sessionId = ''
19813
- function shortId(id) { return id.length > 24 ? id.slice(0, 18) + '…' + id.slice(-6) : id }
19814
- function renderSessionBadge() {
19815
- sessionBadge.textContent = sessionId ? '会话 ' + shortId(sessionId) + '(追问续接)' : '(新会话)'
19816
- sessionBadge.className = 'session-badge' + (sessionId ? ' continued' : '')
19817
- }
19818
- function newConversation() {
19819
- sessionId = ''
19820
- renderSessionBadge()
19821
- answer.className = 'muted'
19822
- answer.textContent = '已开启新对话'
19823
- traj.innerHTML = ''
19824
- document.getElementById('q').value = ''
19825
- }
19826
- function currentCcc() {
19827
- return CCCS.find((c) => c.root === cccSel.value) || (CCCS[0] || { root: '', name: '', roles: [] })
19828
- }
19829
- function fillCccs() {
19830
- cccSel.innerHTML = CCCS.map((c) => {
19831
- const n = c.roles.length
19832
- return '<option value="' + esc(c.root) + '">' + esc(c.name) + (n ? ' (' + n + ' 角色)' : ' (无角色)') + '</option>'
19833
- }).join('') || '<option value="">(未发现 CCC)</option>'
19834
- if (defaultRoot) cccSel.value = CCCS.some((c) => c.root === defaultRoot) ? defaultRoot : (CCCS[0] && CCCS[0].root)
19835
- fillRoles()
19836
- }
19837
- function fillRoles() {
19838
- const c = currentCcc()
19839
- badge.textContent = 'CCC: ' + c.root
19840
- roleSel.innerHTML = (c.roles && c.roles.length)
19841
- ? c.roles.map((r) => '<option value="' + esc(r) + '">' + esc(r) + '</option>').join('')
19842
- : '<option value="">(未配置角色)</option>'
19843
- }
19844
- cccSel.addEventListener('change', () => { fillRoles(); newConversation() })
19845
- roleSel.addEventListener('change', () => newConversation())
19846
- newChatBtn.addEventListener('click', newConversation)
19847
- fillCccs()
19848
- renderSessionBadge()
19849
- btn.addEventListener('click', async () => {
19850
- const c = currentCcc()
19851
- const role = roleSel.value
19852
- const q = document.getElementById('q').value.trim()
19853
- if (!c.root || !role || !q) { answer.className = 'err'; answer.textContent = '请选择认知容器与角色并输入问题'; return }
19854
- btn.disabled = true
19855
- answer.className = 'muted'
19856
- answer.textContent = '运行中…'
19857
- traj.innerHTML = ''
19858
- try {
19859
- const res = await fetch('/ask', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ccc: c.root, role, question: q, sessionId: sessionId || undefined }) })
19860
- const data = await res.json()
19861
- if (!res.ok) {
19862
- // 会话不可恢复/绑定不匹配 → 前端清空 sessionId 重建(用户拍板:切换即新对话)
19863
- sessionId = ''
19864
- renderSessionBadge()
19865
- throw new Error(data.error || ('HTTP ' + res.status))
19866
- }
19867
- answer.className = ''
19868
- answer.innerHTML = data.answer_html || esc(data.answer || '') || '(空回答)'
19869
- sessionId = data.sessionId || sessionId
19870
- renderSessionBadge()
19871
- renderCognitiveTrajectory(data.cognitiveTrajectory || [], data.sessionId || '')
19872
- } catch (err) {
19873
- answer.className = 'err'
19874
- answer.textContent = String(err.message || err)
19875
- } finally {
19876
- btn.disabled = false
19877
- }
19878
- })
19879
- function renderCognitiveTrajectory(entries, sessionId) {
19880
- if (entries.length === 0) { traj.innerHTML = '<div class="muted">(本轮无轨迹)</div>'; return }
19881
- traj.innerHTML = '<div class="sub">本轮轨迹 · ' + esc(sessionId) + '</div>' + entries.map((e) => {
19882
- const cls = e.role === 'user' ? 't-user' : (e.role === 'tool' ? 't-tool' : '')
19883
- const role = e.role === 'tool' ? 'tool' + (e.tool ? ' · ' + esc(e.tool) : '') : e.role
19884
- return '<div class="t-entry ' + cls + '"><div class="t-role">' + role + '</div>' + esc(e.text) + '</div>'
19885
- }).join('')
19886
- }
19887
- <\/script>
19888
- </body>
19889
- </html>`;
19890
- }
19891
- function escapeHtml(s) {
19892
- return s.replace(/[&<>"']/g, (c) => ({
19893
- "&": "&amp;",
19894
- "<": "&lt;",
19895
- ">": "&gt;",
19896
- "\"": "&quot;",
19897
- "'": "&#39;"
19898
- })[c] ?? c);
19899
- }
19730
+ //#region src/surface/text.ts
19900
19731
  /**
19901
- * JSC (Safari/iOS) JSON.parse 快速路径正则兼容化(v1.26.9,S142 调研定稿)。
19902
- *
19903
- * 背景:WebKit bug 200190「JavaScriptCore's Regex can't match the content」——JSC 的
19904
- * JSON.parse 用**内部正则**预校验字符串;内容含**原始** `\u2028`(行分隔符)/`\u2029`
19905
- * (段分隔符)时正则无法匹配 → 对**合法 JSON** 也抛
19906
- * `SyntaxError: The string did not match the expected pattern`(sentry-javascript #2487 同源)。
19907
- * JSON.stringify **不转义** `\u2028`/`\u2029`/`\uFEFF`(它们都是合法 JSON 字符串字符)→
19908
- * 在 JSON **文本层**把它们替换为 `\uXXXX` 转义序列:JSON.parse 后语义完全一致(还原原字符),
19909
- * 且 JSC 正则看到的是常规 ASCII 转义(与 JSON.stringify 对控制字符的输出同形态,安全)。
19910
- *
19911
- * 3099 调试页客户端 `await res.json()` 与页面内嵌 JSON 均需此兼容层
19912
- * ——iOS Safari 用户实测"复杂回答"触发。(原同批还列了 3100 问答页,该面已退役,2026-10-15)
19913
- *
19914
- * @param jsonText JSON.stringify 的输出文本;原地等价替换,返回 JSC 安全文本
19732
+ * 剥离 `<think>…</think>` 块(v1.27.1,微信桥用户反馈:"过微信桥回复给用户的消息,
19733
+ * 要去掉 think 标签,用户不应看到")。复用 extractThinkBlocks 状态机(v1.26.8,
19734
+ * 弃正则)——提取后**只保留正文**(占位符替换为空,think 内容丢弃),
19735
+ * 未闭合 think 优雅截断。适用于纯文本输出面(微信等不支持 think 折叠的通道)。
19915
19736
  */
19916
- function jscSafeJsonText(jsonText) {
19917
- return jsonText.replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029").replace(/\uFEFF/g, "\\uFEFF");
19737
+ function stripThink(raw) {
19738
+ const { body } = extractThinkBlocks(raw);
19739
+ return body.replace(/\u0001T(\d+)\u0001/g, "");
19918
19740
  }
19919
19741
  /**
19920
19742
  * 提取 `<think>…</think>` 块(v1.26.8:**状态机扫描,弃正则**——用户批评"老用正则不是个办法")。
@@ -19992,140 +19814,6 @@ function matchCloseThink(s, contentStart) {
19992
19814
  }
19993
19815
  return -1;
19994
19816
  }
19995
- /**
19996
- * Markdown 渲染(v1.25.9,正经库 marked 服务端渲染——替代手写正则渲染器,S142 用户要求):
19997
- * ① 提取 `<think>…</think>` 块(v1.26.8 状态机扫描,占位符 \u0001T<idx>\u0001)
19998
- * ② 正文与 think 内容**先 escapeHtml 再 marked.parse**(GFM + breaks)——markdown 语法不受
19999
- * 转义影响,原始 HTML 注入被消除(安全);代码块内 `<` 显示为实体(可接受)
20000
- * ③ think 占位符:默认还原为 `<details class="think">` 折叠卡(🧠 思考过程,默认收起);
20001
- * **hideThink=true(v1.26.4,public 口)→ 直接移除**(思考过程对普通用户不展示)
20002
- * @param hideThink 为 true 时 `<think>` 内容完全不渲染(public 问答页体验)
20003
- */
20004
- function renderSkiffMarkdown(raw, hideThink = false) {
20005
- const { body, thinks } = extractThinkBlocks(raw);
20006
- const parsed = f.parse(escapeHtml(body), {
20007
- breaks: true,
20008
- gfm: true
20009
- });
20010
- return (typeof parsed === "string" ? parsed : "").replace(/\u0001T(\d+)\u0001/g, (_m, idx) => {
20011
- if (hideThink) return "";
20012
- const inner = thinks[Number(idx)] ?? "";
20013
- const innerHtml = inner === "" ? "" : f.parse(escapeHtml(inner), {
20014
- breaks: true,
20015
- gfm: true
20016
- });
20017
- return `<details class="think"><summary>🧠 思考过程</summary><div class="think-body">${typeof innerHtml === "string" ? innerHtml : ""}</div></details>`;
20018
- });
20019
- }
20020
- /**
20021
- * 剥离 `<think>…</think>` 块(v1.27.1,微信桥用户反馈:"过微信桥回复给用户的消息,
20022
- * 要去掉 think 标签,用户不应看到")。复用 extractThinkBlocks 状态机(v1.26.8,
20023
- * 弃正则)——提取后**只保留正文**(占位符替换为空,think 内容丢弃),
20024
- * 未闭合 think 优雅截断。适用于纯文本输出面(微信等不支持 think 折叠的通道)。
20025
- */
20026
- function stripThink(raw) {
20027
- const { body } = extractThinkBlocks(raw);
20028
- return body.replace(/\u0001T(\d+)\u0001/g, "");
20029
- }
20030
- /**
20031
- * 本面的规格(C4 块 A/④:**只提供** name/port/host/enabled/handler)。
20032
- * 路由/鉴权/HTML 全部仍归本模块的 {@link handle}(面宿主不介入面语义)。
20033
- */
20034
- function skiffDebugSpec(ctx, root, port, webPort) {
20035
- return {
20036
- name: FACE_SKIFF_DEBUG,
20037
- port,
20038
- host: SKIFF_DEBUG_HOST,
20039
- enabled: () => readSimpleSettings().skiffEnabled,
20040
- handler: (req, res) => handle(ctx, root, webPort, req, res)
20041
- };
20042
- }
20043
- /**
20044
- * 启动调试问答服务(单实例;重复启动幂等返回既有实例)。
20045
- *
20046
- * **CCC 绑定(v1.25.2 用户指出)**:服务绑定一个默认 CCC root(调用方 resolveSkiffRoot
20047
- * 解析:live 会话中**含 skiff.roles 的 CCC 优先**);v1.25.4 起页面可**手工切换**到
20048
- * 其它候选 CCC(live 会话发现的全部 CCC);角色配置**每次请求实时读取**(不缓存快照)。
20049
- *
20050
- * C4 块 A:listener 的创建/监听/错误/端口占用/关闭归 `face-host`(本函数只提供规格 + 记日志)。
20051
- *
20052
- * @param root 默认绑定的 CCC 根(首次加载选中;角色配置读取 + skiff agent cwd)
20053
- * @param port 调试端口(仅 127.0.0.1)
20054
- * @param webPort 主 WebUI 端口(WebUI 链接)
20055
- */
20056
- async function startSkiffDebugServer(ctx, root, port, webPort) {
20057
- if (skiffDebugActive()) return;
20058
- const handle = await startFace(skiffDebugSpec(ctx, root, port, webPort));
20059
- console.log(`[serenity-hooks] ✓ Skiff 调试问答页: http://127.0.0.1:${handle.port}(默认 CCC: ${root},WebUI: ${webPort})`);
20060
- }
20061
- function stopSkiffDebugServer() {
20062
- stopFace(FACE_SKIFF_DEBUG);
20063
- }
20064
- async function handle(ctx, defaultRoot, webPort, req, res) {
20065
- try {
20066
- const url = (req.url ?? "/").split("?")[0] ?? "/";
20067
- if (req.method === "GET" && url === "/") {
20068
- sendHtml(res, skiffDebugPage(await listCccs(ctx, {
20069
- defaultRoot,
20070
- withRoles: true
20071
- }), defaultRoot, webPort));
20072
- return;
20073
- }
20074
- if (req.method === "POST" && url === "/ask") {
20075
- let body;
20076
- try {
20077
- const parsed = JSON.parse(await readBody$1(req));
20078
- if (parsed === null || typeof parsed !== "object") throw new Error("not an object");
20079
- body = parsed;
20080
- } catch {
20081
- sendJson$1(res, 400, { error: "invalid JSON body" });
20082
- return;
20083
- }
20084
- const ccc = typeof body.ccc === "string" && body.ccc !== "" ? body.ccc : defaultRoot;
20085
- const code = typeof body.role === "string" ? body.role : "";
20086
- const question = typeof body.question === "string" ? body.question : "";
20087
- const sessionId = typeof body.sessionId === "string" && body.sessionId !== "" ? body.sessionId : void 0;
20088
- const profile = code !== "" ? readSkiffProfileByCode(ccc, code) : null;
20089
- if (!profile) {
20090
- sendJson$1(res, 400, { error: `unknown skiff trajectory: ${code} (no trajectory with code "${code}" has ${SKIFF_PROFILE_FILENAME} in ${ccc})` });
20091
- return;
20092
- }
20093
- if (!question.trim()) {
20094
- sendJson$1(res, 400, { error: "empty question" });
20095
- return;
20096
- }
20097
- let agent;
20098
- let continued = false;
20099
- if (sessionId) {
20100
- const info = skiffSessionInfo(sessionId);
20101
- const live = getSkiffAgent(sessionId);
20102
- if (!info || !live) {
20103
- sendJson$1(res, 400, { error: "session is not recoverable (process restarted or session unknown) — start a new conversation" });
20104
- return;
20105
- }
20106
- if (info.trajectoryDirName !== profile.dirName || info.ccc !== ccc) {
20107
- sendJson$1(res, 400, { error: `session "${sessionId}" belongs to trajectory "${info.trajectoryDirName}" in another CCC — start a new conversation` });
20108
- return;
20109
- }
20110
- agent = live;
20111
- continued = true;
20112
- }
20113
- const hc = readCheapWorkerConfig(ccc);
20114
- if (!agent) agent = (await createSkiffAgent(ctx, ccc, profile, hc?.defaultModel)).agent;
20115
- const result = await askSkiff(ctx, agent, question);
20116
- sendJson$1(res, 200, {
20117
- answer: result.answer,
20118
- answer_html: renderSkiffMarkdown(result.answer),
20119
- sessionId: result.sessionId,
20120
- continued
20121
- });
20122
- return;
20123
- }
20124
- sendJson$1(res, 404, { error: "not found" });
20125
- } catch (err) {
20126
- sendJson$1(res, 500, { error: err?.message ?? String(err) });
20127
- }
20128
- }
20129
19817
  //#endregion
20130
19818
  //#region src/guard/filter.ts
20131
19819
  /**
@@ -20597,131 +20285,6 @@ function appendHumanChannelAudit(event) {
20597
20285
  })}\n`, "utf-8");
20598
20286
  } catch {}
20599
20287
  }
20600
- const manualSessions = /* @__PURE__ */ new Map();
20601
- /**
20602
- * 本轮是否已成功发送(**由桥在每轮开始时清空** `clearSentThisTurn`;闸门只置位不清空——
20603
- * v1.30.17 起桥要在 `askSkiff` 返回后读它决定是否兜底,故 turn-stopping 不再结算清空)。
20604
- */
20605
- const sentThisTurn = /* @__PURE__ */ new Set();
20606
- /** 连续打回计数(成功发送即清零) */
20607
- const rebukeCounts = /* @__PURE__ */ new Map();
20608
- /** 闸门是否装配成功(v1.30.17:桥据此判断能否信任"未发送"这一判定——未装配时不兜底,防重复发送) */
20609
- let guardActive = false;
20610
- /** 桥:登记手动模式会话(每轮 incoming 都调用,幂等覆盖) */
20611
- function noteManualOutputSession(sessionId, session) {
20612
- if (typeof sessionId !== "string" || sessionId === "") return;
20613
- manualSessions.set(sessionId, session);
20614
- }
20615
- /** 桥:非手动模式 / 会话失效 → 注销(闸门随之失效,零干预) */
20616
- function forgetManualOutputSession(sessionId) {
20617
- manualSessions.delete(sessionId);
20618
- sentThisTurn.delete(sessionId);
20619
- rebukeCounts.delete(sessionId);
20620
- }
20621
- /** 本轮是否已成功发送(测试/诊断用;桥在每轮结束时读它决定是否兜底) */
20622
- function hasSentThisTurn(sessionId) {
20623
- return sentThisTurn.has(sessionId);
20624
- }
20625
- /**
20626
- * 桥:**每轮开始时**清空本会话的"已发送"标记(turn 边界由桥掌握——v1.30.17)。
20627
- * 闸门侧不再于 turn-stopping 结算清空,否则桥读不到本轮结果、无法兜底。
20628
- */
20629
- function clearSentThisTurn(sessionId) {
20630
- sentThisTurn.delete(sessionId);
20631
- }
20632
- /** 闸门是否装配成功(未装配 → 桥不做兜底:宁可静默也不冒重复发送的风险) */
20633
- function isWeixinOutputGuardActive() {
20634
- return guardActive;
20635
- }
20636
- /** 从 agent / 会话对象取会话 id(形状宽容) */
20637
- function sessionIdOf$1(value) {
20638
- const v = value;
20639
- const id = v?.session?.id ?? v?.id;
20640
- return typeof id === "string" && id !== "" ? id : null;
20641
- }
20642
- /**
20643
- * 工具调用是否为「成功的微信发送」。
20644
- *
20645
- * v1.31.0 起有两种合法形态(标记与闸门同时认,迁移期并存):
20646
- * - `im-bridge({channel:"weixin", action:"send"|"send-file", …})` —— ACC 工具(推荐)
20647
- * - `msm("weixin-send", ["send"|"send-file", …])` —— CCC MSM(v1.30.x 通道,逐步退役)
20648
- */
20649
- function isSuccessfulWeixinSend(exec, result) {
20650
- const e = exec;
20651
- if (result?.isError === true) return false;
20652
- const args = e?.arguments;
20653
- if (e?.name === "im-bridge") {
20654
- if ((typeof args?.channel === "string" ? args.channel : "") !== "weixin") return false;
20655
- const action = typeof args?.action === "string" ? args.action : "";
20656
- return action === "send" || action === "send-file";
20657
- }
20658
- if (e?.name === "msm") return args?.name === "weixin-send";
20659
- return false;
20660
- }
20661
- /** 打回消息(机制事实 + 标记;怎么做由 CCC 角色提示词定义) */
20662
- function buildManualOutputRebuke(session) {
20663
- return [
20664
- "[serenity:weixin-manual-output] no send detected in this turn — the user has received NOTHING.",
20665
- `ccc=${session.root} account=${session.accountId} user=${session.userId}`,
20666
- `im-bridge({channel:"weixin", action:"send", account:"${session.accountId}", user:"${session.userId}", text:"<回复>"})`
20667
- ].join("\n");
20668
- }
20669
- /**
20670
- * 装配闸门(index.ts apply 调用)。
20671
- * 只对**已登记的手动模式会话**生效——其他会话(主舱/其他 skiff 会话)零干预。
20672
- */
20673
- function registerWeixinOutputGuard(ctx) {
20674
- try {
20675
- ctx.on("tools/post-execute", async (exec, result, next) => {
20676
- const downstream = await next();
20677
- try {
20678
- const sessionId = sessionIdOf$1(exec?.agent);
20679
- if (sessionId && manualSessions.has(sessionId) && isSuccessfulWeixinSend(exec, result)) {
20680
- sentThisTurn.add(sessionId);
20681
- rebukeCounts.delete(sessionId);
20682
- }
20683
- } catch {}
20684
- return downstream;
20685
- });
20686
- } catch {
20687
- console.warn("[serenity-hooks] ✗ weixin 输出闸门未装配:tools/post-execute 不可用");
20688
- return;
20689
- }
20690
- try {
20691
- ctx.on("agent/turn-stopping", (payload) => {
20692
- const agent = payload?.agent;
20693
- if (!agent) return;
20694
- const sessionId = sessionIdOf$1(agent);
20695
- if (!sessionId) return;
20696
- const session = manualSessions.get(sessionId);
20697
- if (!session) return;
20698
- if (sentThisTurn.has(sessionId)) return;
20699
- const count = (rebukeCounts.get(sessionId) ?? 0) + 1;
20700
- if (count > 2) {
20701
- rebukeCounts.delete(sessionId);
20702
- console.warn(`[serenity-hooks] ✗ weixin 输出闸门:连续 2 次打回后仍未发送(session=${sessionId}, role=${session.role})——本轮用户将收不到 agent 自己发的消息`);
20703
- return;
20704
- }
20705
- rebukeCounts.set(sessionId, count);
20706
- try {
20707
- agent.steer(createUserMessage({
20708
- content: [{
20709
- type: "text",
20710
- text: buildManualOutputRebuke(session)
20711
- }],
20712
- source: PLUGIN_SOURCE
20713
- }));
20714
- console.log(`[serenity-hooks] weixin 输出闸门:打回 ${count}/2(session=${sessionId}, turn=${payload.turn ?? "?"})`);
20715
- } catch (error) {
20716
- console.warn(`[serenity-hooks] ✗ weixin 输出闸门 steer 失败: ${String(error?.message ?? error)}`);
20717
- }
20718
- });
20719
- } catch {
20720
- console.warn("[serenity-hooks] ✗ weixin 输出闸门未装配:agent/turn-stopping 不可用");
20721
- return;
20722
- }
20723
- guardActive = true;
20724
- }
20725
20288
  //#endregion
20726
20289
  //#region src/im/hook.ts
20727
20290
  /**
@@ -20980,138 +20543,145 @@ function installWeixinAccountImpl(d) {
20980
20543
  setWeixinLoopDeps(d);
20981
20544
  registerImAccountImpl(weixinAccountImpl);
20982
20545
  }
20983
- //#endregion
20984
- //#region src/im/bridge.ts
20985
- var bridge_exports = /* @__PURE__ */ __exportAll({
20986
- handleIncoming: () => handleIncoming,
20987
- handleMachineIncoming: () => handleMachineIncoming,
20988
- manualOutputFallbackNeeded: () => manualOutputFallbackNeeded,
20989
- registerWeixinBridge: () => registerWeixinBridge,
20990
- resolveWeixinAccount: () => resolveWeixinAccount,
20991
- sendProactiveText: () => sendProactiveText,
20992
- stopAllBridges: () => stopAllBridges,
20993
- stopCccBridge: () => stopCccBridge,
20994
- syncCccBridge: () => syncCccBridge,
20995
- syncHumanChannel: () => syncHumanChannel,
20996
- weixinBridgeStatus: () => weixinBridgeStatus
20997
- });
20998
- const bridges = /* @__PURE__ */ new Map();
20546
+ const manualSessions = /* @__PURE__ */ new Map();
20999
20547
  /**
21000
- * 🔴 **机器级 poller 的"根"哨兵**(P0-3 A)—— 它**不是一个目录**。
21001
- *
21002
- * 机器级 poller **不属于任何 CCC**(这正是"上移 ACC 层"的含义:谁轮询不再由 CCC 数量决定)。
21003
- * 用途有二:① `runAccountLoop` 的 `root` 形参在机器级模式下不被使用,传它以示"无根";
21004
- * ② 归属表(`im/inbound.ts`)按 root 区分拥有者 ⇒ 机器级用这个哨兵值,
21005
- * 与任何**真实** CCC 根都不会撞(真根一律是绝对路径)。
20548
+ * 本轮是否已成功发送(**由桥在每轮开始时清空** `clearSentThisTurn`;闸门只置位不清空——
20549
+ * v1.30.17 起桥要在 `askSkiff` 返回后读它决定是否兜底,故 turn-stopping 不再结算清空)。
21006
20550
  */
21007
- const MACHINE_SCOPE = "<machine>";
21008
- /** 机器级 poller(账号 id → 循环)。与 `bridges` **互斥**:同一时刻只有一种模式在跑。 */
21009
- const machineLoops = /* @__PURE__ */ new Map();
21010
- /** 媒体大小上限(M6:20MB,对齐 vlm-describe;超限降级告知不落盘) */
21011
- const MEDIA_MAX_BYTES = 20971520;
21012
- /** typing_ticket 缓存(每 (accountId, fromUserId);getconfig 一次后续复用——对齐参考实现 typingTicketCache) */
21013
- const typingTicketByUser = /* @__PURE__ */ new Map();
21014
- function typingCacheKey(accountId, fromUserId) {
21015
- return `${accountId}|${fromUserId}`;
20551
+ const sentThisTurn = /* @__PURE__ */ new Set();
20552
+ /** 连续打回计数(成功发送即清零) */
20553
+ const rebukeCounts = /* @__PURE__ */ new Map();
20554
+ /** 闸门是否装配成功(v1.30.17:桥据此判断能否信任"未发送"这一判定——未装配时不兜底,防重复发送) */
20555
+ let guardActive = false;
20556
+ /** 桥:登记手动模式会话(每轮 incoming 都调用,幂等覆盖) */
20557
+ function noteManualOutputSession(sessionId, session) {
20558
+ if (typeof sessionId !== "string" || sessionId === "") return;
20559
+ manualSessions.set(sessionId, session);
21016
20560
  }
21017
- /** 处理开始:getconfig(无缓存时)→ sendtyping status=1(微信侧显示"正在输入...")。
21018
- * 任何失败静默(typing 不影响主流程——对齐参考实现 typingCallbacks try/catch 吞错)。 */
21019
- async function sendTypingStart(cred, accountId, fromUserId, contextToken) {
21020
- try {
21021
- const key = typingCacheKey(accountId, fromUserId);
21022
- let ticket = typingTicketByUser.get(key);
21023
- if (!ticket) {
21024
- ticket = (await getConfig({
21025
- baseUrl: cred.baseUrl,
21026
- token: cred.token,
21027
- ilinkUserId: fromUserId,
21028
- contextToken: contextToken ?? ""
21029
- })).typing_ticket;
21030
- if (ticket) typingTicketByUser.set(key, ticket);
21031
- }
21032
- if (!ticket) return;
21033
- await sendTyping({
21034
- baseUrl: cred.baseUrl,
21035
- token: cred.token,
21036
- ilinkUserId: fromUserId,
21037
- typingTicket: ticket,
21038
- status: TypingStatus.TYPING
21039
- });
21040
- } catch (err) {
21041
- console.log(`[serenity-hooks] weixin-bridge typing start skipped: ${err instanceof Error ? err.message : String(err)}`);
21042
- }
20561
+ /** 桥:非手动模式 / 会话失效 → 注销(闸门随之失效,零干预) */
20562
+ function forgetManualOutputSession(sessionId) {
20563
+ manualSessions.delete(sessionId);
20564
+ sentThisTurn.delete(sessionId);
20565
+ rebukeCounts.delete(sessionId);
21043
20566
  }
21044
- /** 处理结束(含异常路径 finally):sendtyping status=0;缓存保留(ticket 可复用)。 */
21045
- async function sendTypingStop(cred, accountId, fromUserId) {
21046
- try {
21047
- const ticket = typingTicketByUser.get(typingCacheKey(accountId, fromUserId));
21048
- if (!ticket) return;
21049
- await sendTyping({
21050
- baseUrl: cred.baseUrl,
21051
- token: cred.token,
21052
- ilinkUserId: fromUserId,
21053
- typingTicket: ticket,
21054
- status: TypingStatus.CANCEL
21055
- });
21056
- } catch (err) {
21057
- console.log(`[serenity-hooks] weixin-bridge typing stop skipped: ${err instanceof Error ? err.message : String(err)}`);
21058
- }
20567
+ /** 本轮是否已成功发送(测试/诊断用;桥在每轮结束时读它决定是否兜底) */
20568
+ function hasSentThisTurn(sessionId) {
20569
+ return sentThisTurn.has(sessionId);
21059
20570
  }
21060
20571
  /**
21061
- * 🔴 **2026-10-19(P2)`runAccountLoop` 已迁出本文件** —— 逐字搬到
21062
- * `im/weixin-account.ts`,成为 `ImAccountImpl` 的一个实现(微信)。
21063
- *
21064
- * 为什么搬:装配处原先把渠道写死在这里(`channel !== 'weixin'` ⇒ 跳过)。
21065
- * owner 定向「飞书和微信一样,只不过是 account 方实现不同」⇒ 起循环这个动作
21066
- * 归**账号自己的实现**,装配处只按 `channel` 查派发表。
21067
- * ⚠️ 依赖(`getUpdates` / `handleIncoming` / `handleMachineIncoming`)通过
21068
- * `installWeixinAccountImpl` **注入**,避免 `bridge ⇄ weixin-account` 互相 import 成环。
20572
+ * 桥:**每轮开始时**清空本会话的"已发送"标记(turn 边界由桥掌握——v1.30.17)。
20573
+ * 闸门侧不再于 turn-stopping 结算清空,否则桥读不到本轮结果、无法兜底。
21069
20574
  */
20575
+ function clearSentThisTurn(sessionId) {
20576
+ sentThisTurn.delete(sessionId);
20577
+ }
20578
+ /** 闸门是否装配成功(未装配 → 桥不做兜底:宁可静默也不冒重复发送的风险) */
20579
+ function isWeixinOutputGuardActive() {
20580
+ return guardActive;
20581
+ }
20582
+ /** 从 agent / 会话对象取会话 id(形状宽容) */
20583
+ function sessionIdOf$1(value) {
20584
+ const v = value;
20585
+ const id = v?.session?.id ?? v?.id;
20586
+ return typeof id === "string" && id !== "" ? id : null;
20587
+ }
21070
20588
  /**
21071
- * 手动输出模式的**机制标记**(v1.30.10 引入;v1.30.16 按 S142 用户"这个词不能让 ACC 定义,
21072
- * 要让 CCC 定义"重构)。
21073
- *
21074
- * 边界(R↓):ACC 只提供**机制与数据**——"桥不转发最终文本"这个事实 + 三个已填好的参数值
21075
- * (CCC 根 / 账号 / 用户 id)。**纪律措辞归 CCC**:怎么写、必须怎么做、后果是什么,全部写在
21076
- * CCC 的角色提示词文件里(如 `.opencode/skiff/<role>.md` 的「输出通道」节),CCC 可随时改,
21077
- * 且 v1.30.15 起角色提示词热重载——改完立即生效,无需 ACC 发版。
21078
- *
21079
- * 为什么不用 ACC 写措辞(历史教训):v1.30.10~v1.30.15 由 ACC 内嵌一段纪律文案,用户实测
21080
- * "约束不够,LLM 不听"——且措辞迭代要动插件代码 + 发版 + 重启;把措辞放 CCC 才能快速迭代,
21081
- * 也符合"ACC 管机制、CCC 管内容"的归属二分。
20589
+ * 工具调用是否为「成功的微信发送」。
21082
20590
  *
21083
- * 标记格式(稳定,供 CCC 提示词引用;v1.31.0 起为 ACC 工具形态):
21084
- * ```
21085
- * [serenity:weixin-manual-output]
21086
- * im-bridge({channel:"weixin", action:"send", account:"<account>", user:"<user>", text:"<回复>"})
21087
- * ```
20591
+ * v1.31.0 起有两种合法形态(标记与闸门同时认,迁移期并存):
20592
+ * - `im-bridge({channel:"weixin", action:"send"|"send-file", …})` —— ACC 工具(推荐)
20593
+ * - `msm("weixin-send", ["send"|"send-file", …])` —— CCC MSM(v1.30.x 通道,逐步退役)
21088
20594
  */
21089
- function weixinManualOutputMarker(root, accountId, userId) {
21090
- return ["[serenity:weixin-manual-output]", `im-bridge({channel:"weixin", action:"send", account:"${accountId}", user:"${userId}", text:"<回复>"})`].join("\n");
20595
+ function isSuccessfulWeixinSend(exec, result) {
20596
+ const e = exec;
20597
+ if (result?.isError === true) return false;
20598
+ const args = e?.arguments;
20599
+ if (e?.name === "im-bridge") {
20600
+ if ((typeof args?.channel === "string" ? args.channel : "") !== "weixin") return false;
20601
+ const action = typeof args?.action === "string" ? args.action : "";
20602
+ return action === "send" || action === "send-file";
20603
+ }
20604
+ if (e?.name === "msm") return args?.name === "weixin-send";
20605
+ return false;
20606
+ }
20607
+ /** 打回消息(机制事实 + 标记;怎么做由 CCC 角色提示词定义) */
20608
+ function buildManualOutputRebuke(session) {
20609
+ return [
20610
+ "[serenity:weixin-manual-output] no send detected in this turn — the user has received NOTHING.",
20611
+ `ccc=${session.root} account=${session.accountId} user=${session.userId}`,
20612
+ `im-bridge({channel:"weixin", action:"send", account:"${session.accountId}", user:"${session.userId}", text:"<回复>"})`
20613
+ ].join("\n");
21091
20614
  }
21092
20615
  /**
21093
- * 手动输出模式的**兜底判定**(纯函数,v1.30.17——把决策矩阵从装配里抽出来,可独立测试)。
21094
- *
21095
- * 四个条件同时成立才兜底:
21096
- * ① CCC 显式开启 `fallbackOnNoSend`(缺省 false = 严格静默,向后兼容)
21097
- * ② 机械闸门**已装配**(未装配时无法判定"是否发送过" → 宁可静默也不冒重复发送风险)
21098
- * ③ 本轮**没有**成功发送(agent 自己发过 → 输出权归 agent,桥不插话)
21099
- * ④ 有最终文本可转发(空文本无意义)
20616
+ * 装配闸门(index.ts apply 调用)。
20617
+ * 只对**已登记的手动模式会话**生效——其他会话(主舱/其他 skiff 会话)零干预。
21100
20618
  */
21101
- function manualOutputFallbackNeeded(input) {
21102
- return input.fallbackOnNoSend === true && input.guardActive && !input.sentThisTurn && input.answer !== "";
20619
+ function registerWeixinOutputGuard(ctx) {
20620
+ try {
20621
+ ctx.on("tools/post-execute", async (exec, result, next) => {
20622
+ const downstream = await next();
20623
+ try {
20624
+ const sessionId = sessionIdOf$1(exec?.agent);
20625
+ if (sessionId && manualSessions.has(sessionId) && isSuccessfulWeixinSend(exec, result)) {
20626
+ sentThisTurn.add(sessionId);
20627
+ rebukeCounts.delete(sessionId);
20628
+ }
20629
+ } catch {}
20630
+ return downstream;
20631
+ });
20632
+ } catch {
20633
+ console.warn("[serenity-hooks] ✗ weixin 输出闸门未装配:tools/post-execute 不可用");
20634
+ return;
20635
+ }
20636
+ try {
20637
+ ctx.on("agent/turn-stopping", (payload) => {
20638
+ const agent = payload?.agent;
20639
+ if (!agent) return;
20640
+ const sessionId = sessionIdOf$1(agent);
20641
+ if (!sessionId) return;
20642
+ const session = manualSessions.get(sessionId);
20643
+ if (!session) return;
20644
+ if (sentThisTurn.has(sessionId)) return;
20645
+ const count = (rebukeCounts.get(sessionId) ?? 0) + 1;
20646
+ if (count > 2) {
20647
+ rebukeCounts.delete(sessionId);
20648
+ console.warn(`[serenity-hooks] ✗ weixin 输出闸门:连续 2 次打回后仍未发送(session=${sessionId}, role=${session.role})——本轮用户将收不到 agent 自己发的消息`);
20649
+ return;
20650
+ }
20651
+ rebukeCounts.set(sessionId, count);
20652
+ try {
20653
+ agent.steer(createUserMessage({
20654
+ content: [{
20655
+ type: "text",
20656
+ text: buildManualOutputRebuke(session)
20657
+ }],
20658
+ source: PLUGIN_SOURCE
20659
+ }));
20660
+ console.log(`[serenity-hooks] weixin 输出闸门:打回 ${count}/2(session=${sessionId}, turn=${payload.turn ?? "?"})`);
20661
+ } catch (error) {
20662
+ console.warn(`[serenity-hooks] ✗ weixin 输出闸门 steer 失败: ${String(error?.message ?? error)}`);
20663
+ }
20664
+ });
20665
+ } catch {
20666
+ console.warn("[serenity-hooks] ✗ weixin 输出闸门未装配:agent/turn-stopping 不可用");
20667
+ return;
20668
+ }
20669
+ guardActive = true;
21103
20670
  }
20671
+ //#endregion
20672
+ //#region src/im/inbound-event.ts
20673
+ /** 媒体大小上限(M6:20MB,对齐 vlm-describe;超限降级告知不落盘) */
20674
+ const MEDIA_MAX_BYTES = 20971520;
21104
20675
  /**
21105
20676
  * 订阅表里的 `ccc` **别名 → CCC 根**(P0-3)。
21106
20677
  *
21107
- * 别名取值与面板下拉**同源**(方案 §2.3:`listCccs({withRoles:true})` 的下拉)⇒
21108
- * 先按 **`CccEntry.name`**(= CCC 根目录名,`basename(root)`)匹配;再退一步接受**绝对路径**
21109
- * (手写配置时人会直接粘路径)。两条都试 ⇒ 两种写法都可用,且**不引入第二张名字表**。
21110
- * @returns 命中的 CCC 根;枚举里没有 ⇒ `null`(调用方**响亮报错**,不静默丢弃)
20678
+ * 别名取值与面板下拉**同源** ⇒ 先按 `CccEntry.name`(= CCC 根目录名)匹配;
20679
+ * 再退一步接受**绝对路径**(手写配置时人会直接粘路径)。两条都试
20680
+ * ⇒ 两种写法都可用,且**不引入第二张名字表**。
21111
20681
  */
21112
20682
  async function resolveCccRootByAlias(ctx, alias) {
21113
20683
  try {
21114
- const { listCccs } = await import("./ccc-roots-jZYR2GVE.js").then((n) => n.a);
20684
+ const { listCccs } = await import("./ccc-roots-BhoA7G-0.js").then((n) => n.a);
21115
20685
  const entries = await listCccs(ctx);
21116
20686
  const byName = entries.find((e) => e.name === alias);
21117
20687
  if (byName) return byName.root;
@@ -21128,11 +20698,9 @@ async function resolveCccRootByAlias(ctx, alias) {
21128
20698
  * 判据**,差别只有一格 —— **有没有 CCC 级回退**(`cccFallback`)。写两遍必然分叉。
21129
20699
  *
21130
20700
  * 三档(方案 §3.1,**刻意分档**):
21131
- * - **未命中** ⇒ 不投递 + **留痕**(`console.warn`,静默档:这是"表里没有你",不是错误)
21132
- * - **表残缺** ⇒ **响亮出声**(`console.error`;且**独立于是否命中**)
21133
- * - **目标不存在**(CCC / **轨迹**)⇒ **响亮报错**(`console.error`;不静默丢弃)
21134
- *
21135
- * @param input.cccFallback 过渡期回退(方案 Q5);**机器级模式传 `null`** ⇒ 未命中即不投递
20701
+ * - **未命中** ⇒ 不投递 + **留痕**(静默档:这是"表里没有你",不是错误)
20702
+ * - **表残缺** ⇒ **响亮出声**(且**独立于是否命中**)
20703
+ * - **目标不存在**(CCC / **轨迹**)⇒ **响亮报错**(不静默丢弃)
21136
20704
  */
21137
20705
  async function resolveInboundTarget(ctx, input) {
21138
20706
  const route = planInboundRoute({
@@ -21181,7 +20749,7 @@ async function resolveInboundTarget(ctx, input) {
21181
20749
  });
21182
20750
  return null;
21183
20751
  }
21184
- const { findDirNameByCode } = await import("./profile-DyMJ_bfr.js").then((n) => n.i);
20752
+ const { findDirNameByCode } = await import("./profile-BIQ2widI.js").then((n) => n.i);
21185
20753
  if (findDirNameByCode(root, target.role) === null) {
21186
20754
  const detail = `目标 CCC "${target.ccc}" 内没有编码为 "${target.role}" 的认知轨迹`;
21187
20755
  console.error(`[serenity-hooks] ✗ human-channel: ${detail}(account=${input.account} user=${input.user})—— 该消息**未投递**,请改订阅表里的目标编码`);
@@ -21203,33 +20771,21 @@ async function resolveInboundTarget(ctx, input) {
21203
20771
  };
21204
20772
  }
21205
20773
  /**
21206
- * 处理单条微信消息:路由 → skiff 会话(固定 id 创建/延续)→ 提问 → 回复回写。
20774
+ * **一条入站消息的主流程**(通道中立)。
21207
20775
  *
21208
- * 会话语义:`weixinSessionIdFor(fromUserId)` 固定可重建——同用户长期同一会话;
21209
- * 进程内已有(getSkiffAgent 命中)→ 延续;无(首次/进程重启)→ createSkiffAgent
21210
- * 固定 id resume-or-create(v1.27.2):磁盘已有持久化 log → resume(历史延续,
21211
- * 重启后记忆保留——真正的"同用户长期延续");无 log(首次)→ create。
21212
- * "新的对话已开始"通知仅真正首次(create)时发送;resume/进程内延续不发。
20776
+ * 职责:路由 → 固定会话 id 创建/延续 → 提问 → 回复(或被闸门拦下后兜底)。
20777
+ * 语料(媒体)由事件自带 `fetch()`;出站走的 `io`。
21213
20778
  *
21214
- * 完善(v1.27.3):
21215
- * - **语音支持**:`voice_item.text` = 微信服务端自带语音转写 → 与文本同路径进对话
21216
- * (无需下载/ASR);语音无转写 → 降级提示"暂时无法解析"
21217
- * - **正在输入**:处理前 sendtyping 1(微信显示"正在输入..."),处理后(含异常)0
21218
- * - **媒体接收(图片/文件)**:桥侧 CDN 下载 + AES 解密 → 落盘 CCC 根
21219
- * `_tmp/weixin-inbound/<userhash>/` → question 注入「存在性 + 路径」(ACC 层只保证
21220
- * 可达性——"让会话知道文件的存在并可以拿到";识别/解析归角色 LLM 决策,不编排)。
21221
- * 降级不静默:下载失败 / 超 20MB → 注入说明进对话;typing 窗口覆盖下载(M7)。
20779
+ * 🔴 **本函数没有任何 iLink 概念** —— 这是 B1 的判据。任何 `context_token` /
20780
+ * `item_list` / `message_type` 出现在这里,就说明抽缝没抽干净。
21222
20781
  *
21223
- * 🔴 **P0-3(2026-09-27):路由改由机器级订阅表决定**(本函数的 `root` 入参退为
21224
- * **回退目标**)。两档语义刻意分开(方案 §3.1):**未命中 ⇒ 不投递 + 留痕**(静默档);
21225
- * **目标不存在(CCC / 角色)⇒ 响亮报错**(`console.error` + 机器级留痕)。两者都
21226
- * **不是**"投给默认角色"。
20782
+ * @param preset 机器级模式:目标已解析好 ⇒ **不再自行路由**,且**不看本 CCC 的 `enabled`**
21227
20783
  */
21228
- async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
21229
- const fromUserId = msg.from_user_id;
20784
+ async function handleInboundEvent(ctx, root, accountId, io, event, preset) {
20785
+ const fromUserId = event.userId;
21230
20786
  if (!fromUserId) return;
21231
- const text = extractWeixinText(msg);
21232
- const mediaRefs = extractWeixinMedia(msg);
20787
+ const text = event.text;
20788
+ const mediaRefs = event.media;
21233
20789
  const settings = readWeixinSettings(root);
21234
20790
  let resolved;
21235
20791
  if (preset) resolved = preset.target;
@@ -21249,12 +20805,10 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
21249
20805
  const { trajectory, profile } = resolved;
21250
20806
  root = resolved.root;
21251
20807
  if (!text && mediaRefs.length === 0) {
21252
- if (hasVoiceItem(msg)) await sendTextMessage({
21253
- baseUrl: cred.baseUrl,
21254
- token: cred.token,
20808
+ if (event.hasVoice) await io.sendText({
21255
20809
  toUserId: fromUserId,
21256
20810
  text: "(抱歉,暂时无法解析这条语音消息,请尝试发送文字)",
21257
- contextToken: msg.context_token
20811
+ replyRef: event.replyRef
21258
20812
  }).catch(() => {});
21259
20813
  return;
21260
20814
  }
@@ -21278,25 +20832,22 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
21278
20832
  rebuild: true
21279
20833
  }
21280
20834
  }, hc?.defaultModel, sessionId);
21281
- if (!ref.resumed) await sendTextMessage({
21282
- baseUrl: cred.baseUrl,
21283
- token: cred.token,
20835
+ if (!ref.resumed) await io.sendText({
21284
20836
  toUserId: fromUserId,
21285
20837
  text: "(新的对话已开始)",
21286
- contextToken: msg.context_token
20838
+ replyRef: event.replyRef
21287
20839
  }).catch(() => {});
21288
- await sendTypingStart(cred, accountId, fromUserId, msg.context_token);
20840
+ await io.typingStart?.({
20841
+ toUserId: fromUserId,
20842
+ replyRef: event.replyRef
20843
+ });
21289
20844
  try {
21290
20845
  const mediaNotes = [];
21291
20846
  const degradedNotes = [];
21292
20847
  const hookMedia = [];
21293
20848
  for (const mediaRef of mediaRefs) {
21294
- const mediaType = mediaRef.kind === "image" ? "image_item" : "file_item";
21295
20849
  const label = mediaRef.kind === "image" ? "一张图片" : `文件 ${mediaRef.fileName ?? "(未命名)"}`;
21296
- const result = await downloadMedia({
21297
- item: mediaRef.item,
21298
- mediaType
21299
- });
20850
+ const result = await mediaRef.fetch();
21300
20851
  if (!result) {
21301
20852
  hookMedia.push({
21302
20853
  kind: mediaRef.kind,
@@ -21316,7 +20867,7 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
21316
20867
  try {
21317
20868
  const inboundDir = weixinInboundDir(root, fromUserId);
21318
20869
  mkdirSync(inboundDir, { recursive: true });
21319
- const fname = mediaRef.kind === "image" ? `img_${Date.now()}_${randomBytes(4).toString("hex")}.${sniffImageExt(result.data)}` : sanitizeFileName$1(result.fileName ?? "") || `file_${Date.now()}_${randomBytes(4).toString("hex")}`;
20870
+ const fname = mediaRef.kind === "image" ? `img_${Date.now()}_${randomBytes(4).toString("hex")}.${sniffImageExt(result.data)}` : sanitizeFileName$1(result.fileName ?? mediaRef.fileName ?? "") || `file_${Date.now()}_${randomBytes(4).toString("hex")}`;
21320
20871
  const abs = join(inboundDir, fname);
21321
20872
  writeFileSync(abs, result.data);
21322
20873
  const rel = relative(root, abs);
@@ -21330,7 +20881,7 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
21330
20881
  kind: mediaRef.kind,
21331
20882
  relPath: null
21332
20883
  });
21333
- degradedNotes.push(`(媒体保存失败,请重试)`);
20884
+ degradedNotes.push("(媒体保存失败,请重试)");
21334
20885
  }
21335
20886
  }
21336
20887
  const parts = [];
@@ -21349,7 +20900,7 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
21349
20900
  userId: fromUserId,
21350
20901
  role: trajectory
21351
20902
  });
21352
- parts.push(weixinManualOutputMarker(root, accountId, fromUserId));
20903
+ parts.push(manualOutputMarker(accountId, fromUserId));
21353
20904
  } else forgetManualOutputSession(sessionId);
21354
20905
  const question = parts.join("\n");
21355
20906
  const hookRel = settings.hook;
@@ -21362,7 +20913,7 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
21362
20913
  text,
21363
20914
  media: hookMedia
21364
20915
  })).catch((err) => {
21365
- console.log(`[serenity-hooks] weixin hook incoming error: ${err instanceof Error ? err.message : String(err)}`);
20916
+ console.log(`[serenity-hooks] im hook incoming error: ${err instanceof Error ? err.message : String(err)}`);
21366
20917
  });
21367
20918
  const answer = (await askSkiff(ctx, ref.agent, question)).answer ?? "";
21368
20919
  if (manualOutput) {
@@ -21373,14 +20924,12 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
21373
20924
  answer
21374
20925
  })) return;
21375
20926
  const reply = markdownToPlainText(stripThink(answer));
21376
- await sendTextMessage({
21377
- baseUrl: cred.baseUrl,
21378
- token: cred.token,
20927
+ await io.sendText({
21379
20928
  toUserId: fromUserId,
21380
20929
  text: reply,
21381
- contextToken: msg.context_token
20930
+ replyRef: event.replyRef
21382
20931
  });
21383
- console.warn(`[serenity-hooks] ⚠ weixin 输出兜底:agent 本轮未发送(闸门打回用尽)→ 桥转发最终文本(session=${sessionId}, trajectory=${trajectory})`);
20932
+ console.warn(`[serenity-hooks] ⚠ im 输出兜底:agent 本轮未发送(闸门打回用尽)→ 桥转发最终文本(session=${sessionId}, trajectory=${trajectory})`);
21384
20933
  if (hookRel) invokeWeixinHook(root, hookRel, buildOutgoingHookEvent({
21385
20934
  cccRoot: root,
21386
20935
  accountId,
@@ -21390,18 +20939,16 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
21390
20939
  reply,
21391
20940
  source: "reply-fallback"
21392
20941
  })).catch((err) => {
21393
- console.log(`[serenity-hooks] weixin hook outgoing(reply-fallback) error: ${err instanceof Error ? err.message : String(err)}`);
20942
+ console.log(`[serenity-hooks] im hook outgoing(reply-fallback) error: ${err instanceof Error ? err.message : String(err)}`);
21394
20943
  });
21395
20944
  return;
21396
20945
  }
21397
20946
  if (answer === "") return;
21398
20947
  const reply = markdownToPlainText(stripThink(answer));
21399
- await sendTextMessage({
21400
- baseUrl: cred.baseUrl,
21401
- token: cred.token,
20948
+ await io.sendText({
21402
20949
  toUserId: fromUserId,
21403
20950
  text: reply,
21404
- contextToken: msg.context_token
20951
+ replyRef: event.replyRef
21405
20952
  });
21406
20953
  if (hookRel) invokeWeixinHook(root, hookRel, buildOutgoingHookEvent({
21407
20954
  cccRoot: root,
@@ -21411,28 +20958,369 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
21411
20958
  role: trajectory,
21412
20959
  reply
21413
20960
  })).catch((err) => {
21414
- console.log(`[serenity-hooks] weixin hook outgoing error: ${err instanceof Error ? err.message : String(err)}`);
20961
+ console.log(`[serenity-hooks] im hook outgoing error: ${err instanceof Error ? err.message : String(err)}`);
21415
20962
  });
21416
20963
  } finally {
21417
- await sendTypingStop(cred, accountId, fromUserId);
20964
+ await io.typingStop?.({ toUserId: fromUserId });
21418
20965
  }
21419
20966
  } catch (err) {
21420
20967
  const msg = err instanceof Error ? err.message : String(err);
21421
- console.log(`[serenity-hooks] weixin-bridge error (ccc=${root}): ${msg}`);
20968
+ console.log(`[serenity-hooks] im-bridge error (ccc=${root}): ${msg}`);
21422
20969
  const stack = err instanceof Error ? err.stack : void 0;
21423
- if (stack) console.log(`[serenity-hooks] weixin-bridge stack:\n${stack.slice(0, 1500)}`);
20970
+ if (stack) console.log(`[serenity-hooks] im-bridge stack:\n${stack.slice(0, 1500)}`);
20971
+ }
20972
+ }
20973
+ /**
20974
+ * 手动输出模式的**机制标记**(v1.30.10 引入;v1.30.16 按「ACC 管机制、CCC 管措辞」重构)。
20975
+ *
20976
+ * 边界(R↓):ACC 只提供**机制与数据**——"桥不转发最终文本"这个事实 + 三个已填好的参数值。
20977
+ * **纪律措辞归 CCC**:怎么写、必须怎么做、后果是什么,全部写在轨迹的 `PROMPT.md` 里。
20978
+ */
20979
+ function manualOutputMarker(accountId, userId) {
20980
+ return ["[serenity:weixin-manual-output]", `im-bridge({channel:"weixin", action:"send", account:"${accountId}", user:"${userId}", text:"<回复>"})`].join("\n");
20981
+ }
20982
+ /**
20983
+ * 手动输出模式的**兜底判定**(纯函数,v1.30.17——把决策矩阵从装配里抽出来,可独立测试)。
20984
+ *
20985
+ * 四个条件同时成立才兜底:
20986
+ * ① CCC 显式开启 `fallbackOnNoSend`(缺省 false = 严格静默)
20987
+ * ② 机械闸门**已装配**(未装配时无法判定"是否发送过" → 宁可静默也不冒重复发送风险)
20988
+ * ③ 本轮**没有**成功发送(agent 自己发过 → 输出权归 agent,桥不插话)
20989
+ * ④ 有最终文本可转发(空文本无意义)
20990
+ */
20991
+ function manualOutputFallbackNeeded(input) {
20992
+ return input.fallbackOnNoSend === true && input.guardActive && !input.sentThisTurn && input.answer !== "";
20993
+ }
20994
+ //#endregion
20995
+ //#region src/im/feishu-account.ts
20996
+ /** 去重集合上限(飞书重推窗口有限;限量防内存无界增长) */
20997
+ const SEEN_LIMIT = 2e3;
20998
+ /** 已处理过的 message_id(超时重推去重;按账号隔离) */
20999
+ const seenByAccount = /* @__PURE__ */ new Map();
21000
+ /** 取 SDK(未注入 ⇒ 动态 import 真包) */
21001
+ async function loadFeishuSdk() {
21002
+ return await import("./lib-TxscFKeo.js").then((m) => /* @__PURE__ */ __toESM(m.default, 1));
21003
+ }
21004
+ /**
21005
+ * 账号 → 长连接客户端(`ImAccountLoop` 的形状是面板既有契约,**不加字段** ——
21006
+ * 加字段会逼面板与 `im-bridge status` 认识一个通道特有的东西)。
21007
+ * 用途:`stopped` 置位后能真正 `close()` 掉连接(只置标志位而不关连接 = 假停止)。
21008
+ */
21009
+ const wsByAccount = /* @__PURE__ */ new Map();
21010
+ function seenSet(accountId) {
21011
+ let s = seenByAccount.get(accountId);
21012
+ if (!s) {
21013
+ s = /* @__PURE__ */ new Set();
21014
+ seenByAccount.set(accountId, s);
21015
+ }
21016
+ return s;
21017
+ }
21018
+ /**
21019
+ * 飞书凭据就绪判据:**appId 与 appSecret 都非空**。
21020
+ * (微信只需 token;飞书是双要素,缺任一起不了长连接。)
21021
+ */
21022
+ function feishuCredentialReady(cred) {
21023
+ const appId = typeof cred.appId === "string" ? cred.appId.trim() : "";
21024
+ const secret = typeof cred.token === "string" ? cred.token.trim() : "";
21025
+ return appId !== "" && secret !== "";
21026
+ }
21027
+ /**
21028
+ * 从飞书 `im.message.receive_v1` 事件里取出**中立入站事件**。
21029
+ *
21030
+ * 🔴 这是 P3 唯一的"形状翻译"处,与 `bridge.ts` 里 iLink 的那一处对称。
21031
+ * ⚠️ 用户标识用 **open_id**(不是 chat_id):路由表是"人 → 轨迹",
21032
+ * 而 chat_id 在群里会变、在单聊里也与人不是一一对应。
21033
+ * @returns 无可用发送者/消息体 ⇒ `null`(调用方丢弃并**出声**,不静默)
21034
+ */
21035
+ function feishuEventToInbound(data) {
21036
+ const sender = data.sender;
21037
+ const message = data.message;
21038
+ const userId = sender?.sender_id?.open_id;
21039
+ if (!userId || !message?.message_id) return null;
21040
+ let text = null;
21041
+ if (message.message_type === "text" && typeof message.content === "string") try {
21042
+ const parsed = JSON.parse(message.content);
21043
+ text = (typeof parsed.text === "string" ? parsed.text.trim() : "").replace(/@_user_\d+/g, "").trim();
21044
+ if (text === "") text = null;
21045
+ } catch {
21046
+ text = null;
21047
+ }
21048
+ return {
21049
+ userId,
21050
+ text,
21051
+ media: [],
21052
+ hasVoice: false,
21053
+ replyRef: void 0
21054
+ };
21055
+ }
21056
+ /**
21057
+ * 造出站动作面(飞书)。
21058
+ *
21059
+ * 🔴 `content` 必须是 **JSON 字符串**:`JSON.stringify({ text })` —— 这是官方形态,
21060
+ * 直接给裸文本会被服务端拒绝(且错误码不直观)。
21061
+ */
21062
+ function feishuIo(client) {
21063
+ const c = client;
21064
+ return { sendText: async ({ toUserId, text }) => {
21065
+ const create = c.im?.v1?.message?.create;
21066
+ if (typeof create !== "function") throw new Error("飞书 SDK 缺少 im.v1.message.create");
21067
+ await create({
21068
+ params: { receive_id_type: "open_id" },
21069
+ data: {
21070
+ receive_id: toUserId,
21071
+ msg_type: "text",
21072
+ content: JSON.stringify({ text: markdownToPlainText(text) })
21073
+ }
21074
+ });
21075
+ } };
21076
+ }
21077
+ /**
21078
+ * 单账号的飞书长连接循环。
21079
+ *
21080
+ * 与微信的**关键结构差别**:微信是 `while` 轮询,飞书是**事件回调**(长连接推)。
21081
+ * 所以这里的"循环"其实是"建连一次,然后等回调";`stopped` 靠 `close()` 兑现。
21082
+ */
21083
+ async function runFeishuAccountLoop(input, loop) {
21084
+ const cred = input.cred;
21085
+ const appId = cred.appId ?? "";
21086
+ const appSecret = cred.token ?? "";
21087
+ if (!appId || !appSecret) {
21088
+ loop.lastError = "飞书凭据不全(缺 appId 或 appSecret)";
21089
+ return;
21090
+ }
21091
+ const Lark = await loadFeishuSdk();
21092
+ const client = new Lark.Client({
21093
+ appId,
21094
+ appSecret
21095
+ });
21096
+ const dispatcher = new Lark.EventDispatcher({});
21097
+ const queue = [];
21098
+ let draining = false;
21099
+ const drain = async () => {
21100
+ if (draining) return;
21101
+ draining = true;
21102
+ try {
21103
+ while (queue.length > 0 && !loop.stopped) {
21104
+ const item = queue.shift();
21105
+ loop.lastPollAt = Date.now();
21106
+ try {
21107
+ const event = feishuEventToInbound(item.data);
21108
+ if (!event) {
21109
+ loop.lastError = "收到飞书事件但取不出发送者/消息体";
21110
+ continue;
21111
+ }
21112
+ const msgId = item.data.message?.message_id ?? "";
21113
+ if (msgId !== "") {
21114
+ const seen = seenSet(input.accountId);
21115
+ if (seen.has(msgId)) continue;
21116
+ seen.add(msgId);
21117
+ if (seen.size > SEEN_LIMIT) {
21118
+ const oldest = seen.values().next().value;
21119
+ if (oldest !== void 0) seen.delete(oldest);
21120
+ }
21121
+ }
21122
+ const ctx = input.ctx;
21123
+ const io = feishuIo(client);
21124
+ if (input.machine) {
21125
+ const target = await resolveInboundTarget(ctx, {
21126
+ account: input.accountId,
21127
+ user: event.userId,
21128
+ cccFallback: null
21129
+ });
21130
+ if (!target) continue;
21131
+ await handleInboundEvent(ctx, target.root, input.accountId, io, event, { target });
21132
+ } else await handleInboundEvent(ctx, input.root, input.accountId, io, event);
21133
+ } catch (err) {
21134
+ loop.lastError = err instanceof Error ? err.message : String(err);
21135
+ }
21136
+ }
21137
+ } finally {
21138
+ draining = false;
21139
+ }
21140
+ };
21141
+ dispatcher.register({ "im.message.receive_v1": (data) => {
21142
+ queue.push({ data });
21143
+ drain();
21144
+ } });
21145
+ const ws = new Lark.WSClient({
21146
+ appId,
21147
+ appSecret
21148
+ });
21149
+ wsByAccount.set(input.accountId, ws);
21150
+ try {
21151
+ await ws.start({ eventDispatcher: dispatcher });
21152
+ } catch (err) {
21153
+ loop.lastError = `飞书长连接建立失败: ${err instanceof Error ? err.message : String(err)}`;
21154
+ }
21155
+ }
21156
+ /** 飞书账号实现(登记名 = `feishu`;与配置里的 `channel` 取值一致) */
21157
+ const feishuAccountImpl = {
21158
+ channel: "feishu",
21159
+ isReady: feishuCredentialReady,
21160
+ notReadyReason: () => "**缺 appId 或 appSecret**(未绑定)",
21161
+ startLoop(input) {
21162
+ const loop = {
21163
+ stopped: false,
21164
+ lastPollAt: 0
21165
+ };
21166
+ runFeishuAccountLoop(input, loop);
21167
+ return loop;
21168
+ }
21169
+ };
21170
+ /**
21171
+ * 注册飞书实现(`bridge.ts` 在**模块求值时**调用)。
21172
+ *
21173
+ * ⚠️ 与微信同款教训(续 220 实测 12 例红):注册必须在**模块求值**时发生,
21174
+ * 不能等 `apply` —— 测试与面板会**直接**调 `syncCccBridge` / `syncHumanChannel`。
21175
+ */
21176
+ function installFeishuAccountImpl() {
21177
+ registerImAccountImpl(feishuAccountImpl);
21178
+ }
21179
+ //#endregion
21180
+ //#region src/im/bridge.ts
21181
+ var bridge_exports = /* @__PURE__ */ __exportAll({
21182
+ handleIncoming: () => handleIncoming,
21183
+ handleMachineIncoming: () => handleMachineIncoming,
21184
+ registerWeixinBridge: () => registerWeixinBridge,
21185
+ resolveInboundTarget: () => resolveInboundTarget,
21186
+ resolveWeixinAccount: () => resolveWeixinAccount,
21187
+ sendProactiveText: () => sendProactiveText,
21188
+ stopAllBridges: () => stopAllBridges,
21189
+ stopCccBridge: () => stopCccBridge,
21190
+ syncCccBridge: () => syncCccBridge,
21191
+ syncHumanChannel: () => syncHumanChannel,
21192
+ weixinBridgeStatus: () => weixinBridgeStatus
21193
+ });
21194
+ const bridges = /* @__PURE__ */ new Map();
21195
+ /**
21196
+ * 🔴 **机器级 poller 的"根"哨兵**(P0-3 A)—— 它**不是一个目录**。
21197
+ *
21198
+ * 机器级 poller **不属于任何 CCC**(这正是"上移 ACC 层"的含义:谁轮询不再由 CCC 数量决定)。
21199
+ * 用途有二:① `runAccountLoop` 的 `root` 形参在机器级模式下不被使用,传它以示"无根";
21200
+ * ② 归属表(`im/inbound.ts`)按 root 区分拥有者 ⇒ 机器级用这个哨兵值,
21201
+ * 与任何**真实** CCC 根都不会撞(真根一律是绝对路径)。
21202
+ */
21203
+ const MACHINE_SCOPE = "<machine>";
21204
+ /** 机器级 poller(账号 id → 循环)。与 `bridges` **互斥**:同一时刻只有一种模式在跑。 */
21205
+ const machineLoops = /* @__PURE__ */ new Map();
21206
+ /** typing_ticket 缓存(每 (accountId, fromUserId);getconfig 一次后续复用——对齐参考实现 typingTicketCache) */
21207
+ const typingTicketByUser = /* @__PURE__ */ new Map();
21208
+ function typingCacheKey(accountId, fromUserId) {
21209
+ return `${accountId}|${fromUserId}`;
21210
+ }
21211
+ /** 处理开始:getconfig(无缓存时)→ sendtyping status=1(微信侧显示"正在输入...")。
21212
+ * 任何失败静默(typing 不影响主流程——对齐参考实现 typingCallbacks try/catch 吞错)。 */
21213
+ async function sendTypingStart(cred, accountId, fromUserId, contextToken) {
21214
+ try {
21215
+ const key = typingCacheKey(accountId, fromUserId);
21216
+ let ticket = typingTicketByUser.get(key);
21217
+ if (!ticket) {
21218
+ ticket = (await getConfig({
21219
+ baseUrl: cred.baseUrl,
21220
+ token: cred.token,
21221
+ ilinkUserId: fromUserId,
21222
+ contextToken: contextToken ?? ""
21223
+ })).typing_ticket;
21224
+ if (ticket) typingTicketByUser.set(key, ticket);
21225
+ }
21226
+ if (!ticket) return;
21227
+ await sendTyping({
21228
+ baseUrl: cred.baseUrl,
21229
+ token: cred.token,
21230
+ ilinkUserId: fromUserId,
21231
+ typingTicket: ticket,
21232
+ status: TypingStatus.TYPING
21233
+ });
21234
+ } catch (err) {
21235
+ console.log(`[serenity-hooks] weixin-bridge typing start skipped: ${err instanceof Error ? err.message : String(err)}`);
21236
+ }
21237
+ }
21238
+ /** 处理结束(含异常路径 finally):sendtyping status=0;缓存保留(ticket 可复用)。 */
21239
+ async function sendTypingStop(cred, accountId, fromUserId) {
21240
+ try {
21241
+ const ticket = typingTicketByUser.get(typingCacheKey(accountId, fromUserId));
21242
+ if (!ticket) return;
21243
+ await sendTyping({
21244
+ baseUrl: cred.baseUrl,
21245
+ token: cred.token,
21246
+ ilinkUserId: fromUserId,
21247
+ typingTicket: ticket,
21248
+ status: TypingStatus.CANCEL
21249
+ });
21250
+ } catch (err) {
21251
+ console.log(`[serenity-hooks] weixin-bridge typing stop skipped: ${err instanceof Error ? err.message : String(err)}`);
21424
21252
  }
21425
21253
  }
21426
21254
  /**
21255
+ * 🔴 **2026-10-19(P2)`runAccountLoop` 已迁出本文件** —— 逐字搬到
21256
+ * `im/weixin-account.ts`,成为 `ImAccountImpl` 的一个实现(微信)。
21257
+ *
21258
+ * 为什么搬:装配处原先把渠道写死在这里(`channel !== 'weixin'` ⇒ 跳过)。
21259
+ * owner 定向「飞书和微信一样,只不过是 account 方实现不同」⇒ 起循环这个动作
21260
+ * 归**账号自己的实现**,装配处只按 `channel` 查派发表。
21261
+ * ⚠️ 依赖(`getUpdates` / `handleIncoming` / `handleMachineIncoming`)通过
21262
+ * `installWeixinAccountImpl` **注入**,避免 `bridge ⇄ weixin-account` 互相 import 成环。
21263
+ */
21264
+ /**
21265
+ * 处理单条**微信**消息:把 iLink 形状的 `msg` 归一成**中立入站事件**,
21266
+ * 再交给通道中立的 `handleInboundEvent`(`im/inbound-event.ts`)走主流程。
21267
+ *
21268
+ * ## 🔴 2026-10-19(B1):本函数已退化为**薄适配器**,主流程不在本文件
21269
+ *
21270
+ * 原因:`handleIncoming` 原先同时承担两件不同的事 —— ① "iLink 消息长什么样"
21271
+ * ② "一条已到达的消息该投到哪、怎么答、怎么记账"。接第二家 IM 时只有 ① 需要新写,
21272
+ * 而 ② 应该**被两家共用**。两者混在一个函数里 ⇒ 飞书接进来只能复制一整份主流程。
21273
+ *
21274
+ * ⇒ 主流程搬到 `im/inbound-event.ts`,本函数只做**形状翻译**:
21275
+ * `msg`(iLink)+ `cred`(iLink 凭据)→ `ImInboundEvent` + `ImAccountIo`。
21276
+ *
21277
+ * ## 为什么保留这个签名(而不是让调用方直接调新的主流程)
21278
+ *
21279
+ * 全仓约 50 处测试按 iLink 形状驱动本函数,而 B1 的验收判据是「**微信行为逐字不变**」
21280
+ * (用例数守恒 + 真机一条微信)。保留签名 ⇒ 那 50 处**逐字不动**,判据可复算。
21281
+ *
21282
+ * 会话语义(不变):会话 id 由**轨迹编码**派生 ⇒ 同一条轨迹恒同一条 dsh 会话;
21283
+ * 进程内已有 → 延续;无 → 固定 id resume-or-create。
21284
+ */
21285
+ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
21286
+ const fromUserId = msg.from_user_id;
21287
+ if (!fromUserId) return;
21288
+ await handleInboundEvent(ctx, root, accountId, {
21289
+ sendText: async ({ toUserId, text, replyRef }) => {
21290
+ await sendTextMessage({
21291
+ baseUrl: cred.baseUrl,
21292
+ token: cred.token,
21293
+ toUserId,
21294
+ text,
21295
+ contextToken: replyRef
21296
+ });
21297
+ },
21298
+ typingStart: ({ toUserId, replyRef }) => sendTypingStart(cred, accountId, toUserId, replyRef),
21299
+ typingStop: ({ toUserId }) => sendTypingStop(cred, accountId, toUserId)
21300
+ }, {
21301
+ userId: fromUserId,
21302
+ text: extractWeixinText(msg),
21303
+ media: extractWeixinMedia(msg).map((ref) => ({
21304
+ kind: ref.kind,
21305
+ fileName: ref.fileName,
21306
+ fetch: () => downloadMedia({
21307
+ item: ref.item,
21308
+ mediaType: ref.kind === "image" ? "image_item" : "file_item"
21309
+ }).then((r) => r ? {
21310
+ data: r.data,
21311
+ fileName: r.fileName
21312
+ } : null)
21313
+ })),
21314
+ hasVoice: hasVoiceItem(msg),
21315
+ replyRef: msg.context_token
21316
+ }, preset);
21317
+ }
21318
+ /**
21427
21319
  * **机器级 poller 的单条消息入口**(P0-3 A)。
21428
21320
  *
21429
21321
  * 与 `handleIncoming` 的唯一差别:**目标由机器级订阅表逐条解析,且没有 CCC 级回退**
21430
21322
  * (方案 §3.1:机器级模式下**未命中 = 不投递**,不是"投给默认角色")。
21431
- * 解析出来后把**目标根**当 `root` 交给 `handleIncoming`(并带上 `preset` ⇒ 它不再自行路由、
21432
- * 也不看目标 CCC 的 `weixin.enabled`)。
21433
- *
21434
- * 🔴 为什么"起 poller"与"路由"必须同批(本件即 P0-2 的后半):机器级 poller 收到消息时
21435
- * **不知道它属于哪个 CCC** —— 那个答案是订阅表给的。没有订阅表 ⇒ **能收不能投**。
21323
+ * 解析出来后把**目标根**当 `root` 交给 `handleIncoming`(并带上 `preset`)。
21436
21324
  *
21437
21325
  * 🔵 **导出供测试直接驱动**(与 `handleIncoming` 同款:它俩都是"单条消息的入口",
21438
21326
  * 生产路径 `runAccountLoop` 只是循环壳)。
@@ -21449,6 +21337,35 @@ async function handleMachineIncoming(ctx, accountId, cred, msg) {
21449
21337
  await handleIncoming(ctx, target.root, accountId, cred, msg, { target });
21450
21338
  }
21451
21339
  /**
21340
+ * **按渠道读凭据**(P3 起的单一入口)。
21341
+ *
21342
+ * 为什么必须抽成一个函数:CCC 级桥(`syncCccBridge`)与机器级 poller(`syncMachinePollers`)
21343
+ * 是**两条装配路径**,原先各自写着"怎么从 localstore 取微信 token"。接第二家渠道时若两边各写一份,
21344
+ * 必然分叉(一家改了另一家忘改,且**没有任何机械信号**)。
21345
+ *
21346
+ * 两种取法的**真实差别**(不是风格差异):
21347
+ * - **CCC 级**:凭据在 localstore(`WEIXIN_<A>_TOKEN` / `FEISHU_<A>_APPID|SECRET`)。
21348
+ * - **机器级**:凭据在插件设置行(`account.token`)——那是宿主设置面,不经 localstore。
21349
+ *
21350
+ * ⇒ 本函数只负责**把"渠道"翻译成"该读哪套字段"**,返回统一形状 `ImAccountCredential`。
21351
+ * 微信的 appId 恒 undefined;飞书把 appSecret 装进 `token`(让两边共用同一个就绪判据位置)。
21352
+ */
21353
+ function readImCredentialByChannel(root, accountId, channel) {
21354
+ if (channel === "feishu") {
21355
+ const f = readFeishuCredential(root, accountId);
21356
+ return f ? {
21357
+ appId: f.appId,
21358
+ token: f.appSecret
21359
+ } : null;
21360
+ }
21361
+ const w = readWeixinCredential(root, accountId);
21362
+ return w ? {
21363
+ token: w.token,
21364
+ baseUrl: w.baseUrl,
21365
+ userId: w.userId
21366
+ } : null;
21367
+ }
21368
+ /**
21452
21369
  * 启动/重建某 CCC 的桥:读配置 → 对每个 enabled + 有凭据的账号启动轮询循环。
21453
21370
  * 已存在(配置变化热重建)→ 先停旧循环再启动新的。
21454
21371
  *
@@ -21481,7 +21398,7 @@ function syncCccBridge(ctx, root) {
21481
21398
  }
21482
21399
  continue;
21483
21400
  }
21484
- const cred = readWeixinCredential(root, account.accountId);
21401
+ const cred = readImCredentialByChannel(root, account.accountId, channel);
21485
21402
  if (!cred) continue;
21486
21403
  if (!impl.isReady(cred)) continue;
21487
21404
  implByAccount.set(account.accountId, impl);
@@ -21489,7 +21406,7 @@ function syncCccBridge(ctx, root) {
21489
21406
  requests.push({
21490
21407
  root,
21491
21408
  accountId: account.accountId,
21492
- identity: accountIdentity(root, account.accountId, cred.token)
21409
+ identity: accountIdentity(root, account.accountId, cred.token ?? "")
21493
21410
  });
21494
21411
  }
21495
21412
  const plan = planPollerClaims(requests, pollerOwners());
@@ -21579,10 +21496,12 @@ function syncMachinePollers(ctx, hc) {
21579
21496
  continue;
21580
21497
  }
21581
21498
  const token = (account.token ?? "").trim();
21499
+ const appIdRaw = account.appId;
21582
21500
  const cred = {
21583
21501
  token,
21584
21502
  baseUrl: "",
21585
- userId: account.userId
21503
+ userId: account.userId,
21504
+ ...channel === "feishu" && typeof appIdRaw === "string" ? { appId: appIdRaw.trim() } : {}
21586
21505
  };
21587
21506
  if (!impl.isReady(cred)) {
21588
21507
  console.warn(`[serenity-hooks] ⚠ human-channel: 账号 ${id} ${impl.notReadyReason?.(cred) ?? "**凭据不全**"} ⇒ 不起 poller`);
@@ -21642,7 +21561,7 @@ function syncHumanChannel(ctx) {
21642
21561
  (async () => {
21643
21562
  try {
21644
21563
  stopMachineLoops();
21645
- const { listCccs } = await import("./ccc-roots-jZYR2GVE.js").then((n) => n.a);
21564
+ const { listCccs } = await import("./ccc-roots-BhoA7G-0.js").then((n) => n.a);
21646
21565
  for (const entry of await listCccs(ctx)) syncCccBridge(ctx, entry.root);
21647
21566
  } catch {}
21648
21567
  })();
@@ -21782,6 +21701,14 @@ installWeixinAccountImpl({
21782
21701
  handleIncoming,
21783
21702
  handleMachineIncoming
21784
21703
  });
21704
+ /**
21705
+ * 🆕 **P3 装配:飞书实现登记进同一张派发表**(同样是模块求值时)。
21706
+ *
21707
+ * 飞书不需要注入依赖 —— 它的收(`WSClient`)与发(`im.v1.message.create`)都在 SDK 里,
21708
+ * 主流程则复用 `im/inbound-event.ts`。⇒ 这一行就是"微信与飞书成为并列实现"的**最后一块**:
21709
+ * 派发表有了两个条目,装配处一行没改(它只按 `channel` 查表)。
21710
+ */
21711
+ installFeishuAccountImpl();
21785
21712
  //#endregion
21786
21713
  //#region src/im/send.ts
21787
21714
  /** 请求体上限(文本消息远小于此;防滥用) */
@@ -21898,7 +21825,7 @@ function matchCcc(input, candidates) {
21898
21825
  }
21899
21826
  /** 组装候选列表(`ccc-roots.listCccs` 投影 + `.serenity` 名;动态 import 保持本模块静态依赖轻量) */
21900
21827
  async function collectCandidates(ctx) {
21901
- const { listCccs } = await import("./ccc-roots-jZYR2GVE.js").then((n) => n.a);
21828
+ const { listCccs } = await import("./ccc-roots-BhoA7G-0.js").then((n) => n.a);
21902
21829
  const entries = await listCccs(ctx, { defaultRoot: process.cwd() });
21903
21830
  const seen = /* @__PURE__ */ new Set();
21904
21831
  const out = [];
@@ -24741,10 +24668,6 @@ const Config = z.object({
24741
24668
  opencodeSkills: z.boolean().default(true),
24742
24669
  gateway: z.object({ enabled: z.boolean().default(false) }),
24743
24670
  rebuild: z.object({ thresholdK: z.number().min(50).max(4e3).default(400) }),
24744
- skiff: z.object({
24745
- enabled: z.boolean().default(false),
24746
- debugPort: z.number().min(1024).max(65535).default(SKIFF_DEBUG_PORT)
24747
- }),
24748
24671
  webFetch: z.object({ enabled: z.boolean().default(true) }),
24749
24672
  opencodeProvider: z.object({ autoConfigure: z.boolean().default(true) }),
24750
24673
  visionPatch: z.object({ enabled: z.boolean().default(true) }),
@@ -24757,8 +24680,6 @@ const Config = z.object({
24757
24680
  }),
24758
24681
  gatewayEnabled: z.boolean().volatile(),
24759
24682
  rebuildThresholdK: z.number().min(50).max(4e3).volatile(),
24760
- skiffEnabled: z.boolean().volatile(),
24761
- skiffDebugPort: z.number().min(1024).max(65535).volatile(),
24762
24683
  croEnabled: z.boolean().volatile(),
24763
24684
  diagramEnabled: z.boolean().volatile()
24764
24685
  });
@@ -24825,7 +24746,6 @@ function apply(ctx, config) {
24825
24746
  if (config.env) registerEnv(ctx);
24826
24747
  if (config.opencodeSkills) registerOpencodeSkills(ctx);
24827
24748
  registerBootstrap(ctx);
24828
- registerSkiff(ctx);
24829
24749
  registerCroTurnTracking(ctx);
24830
24750
  registerWakeScheduler(ctx);
24831
24751
  registerWeixinBridge(ctx);
@@ -24887,139 +24807,5 @@ function registerDiagramTool(ctx) {
24887
24807
  } catch {}
24888
24808
  sync();
24889
24809
  }
24890
- /**
24891
- * F4 Skiff 调试服务装配:启停 = 人工(设置面板 Skiff 区块开关,settings 持久化)。
24892
- * settings-changed 事件触发同步(skiffEnabled 开 → 启动调试服务;关 → 停止)。
24893
- * 角色配置(skiff.roles)从当前 CCC 根读取(含 skiff.roles 的 CCC 优先,进程 cwd 次之)。
24894
- *
24895
- * v1.30.13(S142 诊断 D1):**CCC root 解析失败不再"永不重试"**。旧实现只在 apply 时
24896
- * 同步一次——此刻通常还没有 live 会话,进程 cwd(服务启动目录)也不在 CCC 内 →
24897
- * `resolveSkiffRoot` 返回 null → 打印一行警告后**永不重试**(实证:重启日志
24898
- * `✗ Skiff 调试服务未启动:无法定位 CCC root`,`ss -ltn` 无 3099;同批 ACP 因容忍
24899
- * `root ?? undefined` 而正常启动 —— ⚠️ **该对照已随面 E 退役而失效**(2026-10-15),
24900
- * 但本条记录的**病因与修法不受影响**(Skiff 侧仍是"首次未定位 → 曾永不重试")。
24901
- *
24902
- * v1.30.14(重启日志实证后的收敛):**并发启动守卫** + 首次未定位降级为信息级日志。
24903
- * - 实证(v1.30.13 重启日志):定时触发与"会话就绪"事件几乎同时到达 → 两次 sync 都
24904
- * 看到 `started === false` → **两次 start**(第一次绑定成功、第二次 `EADDRINUSE` 刷错误日志)。
24905
- * → 加 `starting` 在飞标志(启动 Promise settle 前不再重复发起)。
24906
- * - 首次未定位**不是失败**(apply 时无 live 会话是常态)→ 用 `console.log` 说明"等待 live 会话"。
24907
- *
24908
- * v1.35(C2,S142 2026-09-15 Q5):**退避重试定时器退场**。原形态是 ① 启动同步
24909
- * ② 退避重试(1s/3s/8s/20s/40s) ③ 会话就绪事件再触发。C2 把 CCC 发现面归一后,
24910
- * ② 的病灶("apply 时刻解析不到根")改由 **`resolveSkiffRoot` 第 ④ 档(ccc-roots 的
24911
- * **并集**枚举:工作区注册表 ∪ 持久化会话 ∪ live 会话)** 从源头消掉——持久来源不依赖
24912
- * 任何 live 会话,故 apply 时刻就能解析到,不再需要"等一会儿再试"。
24913
- *
24914
- * 🔒 **必须保留**(它们不是"重试",删了会引入新缺陷):
24915
- * - **反应式再触发**(`agent/created` / `session/created` → sync):宿主重启后
24916
- * "恢复"旧会话不触发 `session/created`,但**开始对话**会触发 `agent/created`
24917
- * (v0.1.7 前名 `agent/session-start`)——这是唯一能把"进程起来时还解析不到、稍后才有会话"
24918
- * 这条路径接上的通道。
24919
- * - **`starting` 在飞标志**:防同拍双次 start → `EADDRINUSE`。
24920
- * (C4 块 A 备注:面宿主亦按面名合并在飞启动,是第二道保险;**本标志仍保留**——
24921
- * 它管的是"解析 root 这段装配流程"的在飞语义,且调试服务的启动替身在测试里被 mock,
24922
- * 面宿主的 active 表在那种场景下为空,不能当作 `started` 的替代。)
24923
- * - **`informedRoot` 信息级日志**(只报一次,不刷屏)、`serenity/settings-changed` 监听、
24924
- * 启动时那次 `sync()`。
24925
- */
24926
- function registerSkiff(ctx) {
24927
- let started = false;
24928
- let starting = false;
24929
- let informedRoot = false;
24930
- /** 已解析到的根(成功后缓存,避免每次 sync 重复解析) */
24931
- let resolvedRoot = null;
24932
- const sync = async () => {
24933
- const s = readSimpleSettings();
24934
- if (s.skiffEnabled) {
24935
- if (started || starting) return;
24936
- starting = true;
24937
- try {
24938
- const root = resolvedRoot ?? await resolveSkiffRoot(ctx);
24939
- if (!root) {
24940
- if (!informedRoot) {
24941
- informedRoot = true;
24942
- console.log("[serenity-hooks] Skiff 调试服务等待 CCC root(此刻无已知 CCC;会话就绪即试)");
24943
- }
24944
- return;
24945
- }
24946
- resolvedRoot = root;
24947
- const webPort = readWebPort(ctx);
24948
- await startSkiffDebugServer(ctx, root, s.skiffDebugPort, webPort);
24949
- started = true;
24950
- } catch (err) {
24951
- console.error(`[serenity-hooks] ✗ Skiff 调试服务启动失败: ${String(err?.message ?? err)}`);
24952
- } finally {
24953
- starting = false;
24954
- }
24955
- return;
24956
- }
24957
- if (started) {
24958
- stopSkiffDebugServer();
24959
- started = false;
24960
- }
24961
- resolvedRoot = null;
24962
- informedRoot = false;
24963
- };
24964
- try {
24965
- ctx.on("serenity/settings-changed", () => {
24966
- sync();
24967
- });
24968
- } catch {}
24969
- for (const eventName of ["agent/created", "session/created"]) try {
24970
- ctx.on(eventName, () => {
24971
- if (!started) sync();
24972
- });
24973
- } catch {}
24974
- sync();
24975
- }
24976
- /**
24977
- * 解析 Skiff 调试服务绑定的 CCC 根(v1.25.2 用户指出:skiff 必须绑定 CCC):
24978
- * ① live 会话中**配置了 skiff.roles 的 CCC 优先**(用户认知中的绑定目标)
24979
- * ② 回退进程 cwd 上溯 .serenity
24980
- * ③ 再回退任一 live 会话的 CCC
24981
- * ④ **(v1.35 / C2 新增)ccc-roots 枚举出的 CCC——仅在唯一候选时采用**
24982
- *
24983
- * 第 ④ 档为什么存在:①②③ 全部依赖 live 会话或进程 cwd;宿主重启、浏览器尚未重连时
24984
- * 三者可能同时落空,而 `ccc-roots.listCccs` 的**持久来源**(工作区注册表 / 持久化会话)
24985
- * 不依赖 live 会话 ⇒ 这正是"apply 时刻就能解析到根"的那条路,也是退避重试定时器
24986
- * 得以退场的原因。
24987
- *
24988
- * ④ 的收敛条件(**不猜**):只在候选**唯一**时采用;多个候选 → 返回 `null`
24989
- * (留着让 ①②③ 的语义与人类的显式选择决定,不代替人拍板)。
24990
- *
24991
- * ⚠️ ①②③ 的**逐条优先级不变**;④ 只追加在末尾。
24992
- */
24993
- async function resolveSkiffRoot(ctx) {
24994
- const liveRoots = [];
24995
- try {
24996
- const sessions = hostSessions(ctx);
24997
- for (const s of sessions?.list?.() ?? []) {
24998
- const cwd = s?.header?.cwd;
24999
- if (typeof cwd === "string") {
25000
- const r = cccRootForCwd(cwd);
25001
- if (r && !liveRoots.includes(r)) liveRoots.push(r);
25002
- }
25003
- }
25004
- } catch {}
25005
- for (const r of liveRoots) if (listSkiffProfiles(r).length > 0) return r;
25006
- const fromCwd = cccRootForCwd(process.cwd());
25007
- if (fromCwd) return fromCwd;
25008
- if (liveRoots[0]) return liveRoots[0];
25009
- try {
25010
- const entries = await listCccs(ctx);
25011
- if (entries.length === 1) return entries[0].root;
25012
- } catch {}
25013
- return null;
25014
- }
25015
- /** 主 WebUI 端口(WebUI 链接;webServer 未装配回退集中端口表的 main 面端口) */
25016
- function readWebPort(ctx) {
25017
- try {
25018
- const ws = hostWebServer(ctx);
25019
- return typeof ws?.port === "number" ? ws.port : MAIN_WEB_PORT;
25020
- } catch {
25021
- return MAIN_WEB_PORT;
25022
- }
25023
- }
25024
24810
  //#endregion
25025
24811
  export { Config, apply, humanChannelAccountSchema, humanChannelSubscriptionSchema, inject, name };