@shgroup/dsh-serenity-hooks 1.72.0 → 1.74.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 (61) hide show
  1. package/README.md +12 -11
  2. package/dsh.plugin.json +2 -2
  3. package/lib/accounts-BHQbakUh.js +33 -0
  4. package/lib/{ccc-Dkoa2oW3.js → ccc-CyYJL4F2.js} +9 -9
  5. package/lib/{ccc-roots-jZYR2GVE.js → ccc-roots-B2t_aM8L.js} +5 -5
  6. package/lib/client.js +165 -58
  7. package/lib/{wake/cro.d.ts → cro/core.d.ts} +3 -3
  8. package/lib/{wake/cro-guide.d.ts → cro/guide.d.ts} +3 -3
  9. package/lib/{wake/cro-log.d.ts → cro/log.d.ts} +2 -2
  10. package/lib/{wake/cro-turns.d.ts → cro/turns.d.ts} +2 -2
  11. package/lib/guard/seam.d.ts +4 -4
  12. package/lib/host/effect.d.ts +1 -1
  13. package/lib/{ilink-DEpmE3yi.js → ilink-BMk2w2KY.js} +1 -1
  14. package/lib/im/bridge.d.ts +36 -65
  15. package/lib/im/endpoint.d.ts +3 -3
  16. package/lib/im/feishu-account.d.ts +92 -0
  17. package/lib/im/guard.d.ts +1 -1
  18. package/lib/im/hook.d.ts +1 -1
  19. package/lib/im/inbound-event.d.ts +162 -0
  20. package/lib/im/route.d.ts +38 -3
  21. package/lib/index.d.ts +5 -27
  22. package/lib/index.js +1089 -1208
  23. package/lib/kernel/ccc-roots.d.ts +4 -4
  24. package/lib/kernel/ccc.d.ts +19 -19
  25. package/lib/kernel/ports.d.ts +3 -5
  26. package/lib/lib-TxscFKeo.js +119355 -0
  27. package/lib/{message-source-kVoNK0C5.js → message-source-DCpoRHP3.js} +2 -2
  28. package/lib/patches/opencode-provider.d.ts +1 -1
  29. package/lib/{profile-DyMJ_bfr.js → profile-BVt84mbi.js} +68 -65
  30. package/lib/rolldown-runtime-B4iAMlE-.js +35 -0
  31. package/lib/{route-f7JlinIt.js → route-COOFLTpU.js} +61 -8
  32. package/lib/seams/guards.d.ts +14 -14
  33. package/lib/seams/keeper.d.ts +1 -1
  34. package/lib/seams/lifecycle.d.ts +2 -2
  35. package/lib/{settings-lt1i3OZv.js → settings-D8hlqxua.js} +49 -84
  36. package/lib/skills/resource-index.d.ts +1 -1
  37. package/lib/surface/faces.d.ts +1 -1
  38. package/lib/surface/http.d.ts +1 -1
  39. package/lib/surface/settings.d.ts +7 -13
  40. package/lib/surface/text.d.ts +72 -0
  41. package/lib/surface/usage.d.ts +2 -2
  42. package/lib/tools/config-docs.d.ts +53 -0
  43. package/lib/tools/container-admin.d.ts +19 -12
  44. package/lib/tools/msm/service.d.ts +7 -0
  45. package/lib/tools/trajectory-constraint-admin.d.ts +30 -0
  46. package/lib/{role/worker/idle.d.ts → trajectory/carrier.d.ts} +12 -2
  47. package/lib/trajectory/core.d.ts +1 -1
  48. package/lib/{role/worker → trajectory/delegate}/core.d.ts +7 -1
  49. package/lib/{role/worker → trajectory/delegate}/preset.d.ts +9 -2
  50. package/lib/trajectory/policy/profile.d.ts +122 -0
  51. package/lib/trajectory/policy/rules.d.ts +98 -0
  52. package/lib/{role/skiff/registry.d.ts → trajectory/policy/window.d.ts} +19 -13
  53. package/lib/trajectory/prompt.d.ts +12 -12
  54. package/lib/trajectory/rebuild.d.ts +2 -2
  55. package/lib/{role/skiff/core.d.ts → trajectory/session.d.ts} +58 -49
  56. package/package.json +3 -2
  57. package/lib/role/skiff/config.d.ts +0 -79
  58. package/lib/role/skiff/debug.d.ts +0 -88
  59. package/lib/role/skiff/profile.d.ts +0 -115
  60. package/lib/rolldown-runtime-8H4AJuhK.js +0 -14
  61. package/lib/tools/skiff-admin.d.ts +0 -30
package/lib/index.js CHANGED
@@ -1,12 +1,13 @@
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-B2t_aM8L.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-CyYJL4F2.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 readTrajectoryConstraintAt, c as registerTrajectorySession$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 trajectoryDirNameForSession, m as archiveSession, n as isTrajectoryConstrained, o as readTrajectoryConstraintByCode, p as UID_FILENAME, r as listTrajectoryConstraints, s as trajectoryConstraintForSession, t as TRAJECTORY_CONSTRAINT_FILENAME, u as unregisterTrajectorySession$1, v as findSession, w as parseSessionContextFromEvents, x as listActiveTrajectories, y as getActiveSessionInfo } from "./profile-BVt84mbi.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-COOFLTpU.js";
6
+ import { a as buildTrajectoryBasePrompt, c as createProfilePromptReader, d as resolveConstraintSystemPrompt, f as ACC_VERSION, i as TRAJECTORY_CONSTRAINT_HINT, l as profileMsmWhitelist, n as readSimpleSettings, o as constraintCognitiveTrajectory, p as findPackageRoot, r as registerSettingsSection, s as constraintSystemPromptSource, u as profileToolWhitelist } from "./settings-D8hlqxua.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-DCpoRHP3.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
+ import { a as registerImAccountImpl, i as normalizeAccountChannel, n as getImAccountImpl, r as imAccountChannels } from "./accounts-BHQbakUh.js";
10
11
  import { createRequire } from "node:module";
11
12
  import z from "@deepseek-ai/schemastery";
12
13
  import { defineTool } from "@deepseek-ai/dsh-tools";
@@ -977,11 +978,11 @@ const gitTool = defineTool({
977
978
  *
978
979
  * 为什么单独一个模块(R↓):地址的读者是 `msm-ops.buildMsmEnv`(把地址注入 MSM 子进程 env),
979
980
  * 写者才是 `weixin-send-api`(监听器启停)。若 msm-ops 直接 import weixin-send-api,
980
- * 静态依赖链会拖进整条微信桥栈(weixin-bridge → skiff-core → `@deepseek-ai/dsh-llm`)——
981
- * 后果实测:`acc-extras` / `ops` / `skiff-admin` 三个测试套件因解析不到宿主包而**整文件加载失败**
981
+ * 静态依赖链会拖进整条微信桥栈(weixin-bridge → trajectory-session → `@deepseek-ai/dsh-llm`)——
982
+ * 后果实测:`acc-extras` / `ops` / `trajectory-constraint-admin` 三个测试套件因解析不到宿主包而**整文件加载失败**
982
983
  * (它们只用到 MSM 注册表逻辑)。叶模块把「读」与「写」解耦:读者零依赖,写者自己承担重量。
983
984
  *
984
- * 同款先例:`host/effect.ts`(拆卸登记)、`role/worker/idle.ts`(等待结算)。
985
+ * 同款先例:`host/effect.ts`(拆卸登记)、`trajectory/carrier.ts`(等待结算)。
985
986
  */
986
987
  let endpoint = null;
987
988
  /** 设置当前入口地址(weixin-send-api 启停时调用;null = 未启动) */
@@ -1083,18 +1084,19 @@ Directory of ACC capabilities — each area lists where to go for details (guide
1083
1084
  acc_msm 已并入 msm(执行+发现)与 container_admin(管理)——msm("<name>", ["<args>"]);inspect=true 查用法
1084
1085
  cheap-worker(杂工 agent 编排——guide 子命令:cheap-worker guide)
1085
1086
  专属工具(**默认对所有 CCC 隐藏**,只有在自己 exclusiveTools 里点名的 CCC 可见)→ acc-diag(运行态诊断)
1086
- ④ 角色与对外面 → container_admin role(Skiff 认知子集角色:guide/validate/apply/list——skiff_admin 已并入)
1087
- 3099 Skiff 调试问答页(人工调试面)。外部程序化接入用官方 dsh --profile acp
1087
+ ④ 轨迹约束与对外面 → container_admin config(轨迹约束四动作 guide/validate/apply/list——**原 role 域已并入 config**,旧名作过渡别名仍可用;说明类配置走**两层菜单** config docs 目录 / config docs <章节> 详情)
1088
+ 外部程序化接入用官方 dsh --profile acp
1088
1089
  + 现成 ACP 客户端;本插件不提供 ACP 面
1089
1090
  ⑤ 自主与接入 → cognitive_trajectory send-now(跨轨迹的**即时**递送,**不排队**:live 目标在跑就当场注入当前轮、空闲则立即起轮;非 live 冷载入——等效于直接 wake;**有同步回执**)
1090
1091
  cognitive_trajectory send-later(跨轨迹的"未来时刻 + 一条 message";fire-and-forget,无回执无回收)
1091
1092
  weixin 微信桥(配置/扫码/路由/消息 hook——CCC 侧 weixin-doctor MSM:msm("weixin-doctor", ["guide"]))
1092
- ⑥ CCC 配置总览 → container_admin msm ccc-config(9 段:cheap-worker/sessionKeeper/localstore/hooks/safeMode/skiff/cognitiveTrajectory/weixin/**exclusiveTools**)
1093
+ ⑥ CCC 配置总览 → container_admin config docs config(按章详情——9 段全表)
1094
+ 或 container_admin msm ccc-config(同一份正文的旧入口,保留)
1093
1095
  ⑦ 注册表与安全 → mech-registry.json 由 container_admin msm register/deregister 管理(写保护——不可直接编辑)
1094
1096
  dashboard health 输出 registry 完整性检查;损坏恢复指引见 health registry.issues
1095
1097
  CCC 级开关:exclusiveTools(声明专属工具)/ safeMode.blacklist(写路径黑名单)
1096
1098
  / safeMode.hiddenTools(**安全模式开启期间**额外隐藏的工具,v1.51.4)
1097
- CCC 级 skill 供给:cognitiveTrajectory.skills(v1.44.0——**本 CCC 每条轨迹都带**,含 skiff 会话;与轨迹自己的 frontmatter 声明取并集)
1099
+ CCC 级 skill 供给:cognitiveTrajectory.skills(v1.44.0——**本 CCC 每条轨迹都带**,含 轨迹约束会话;与轨迹自己的 frontmatter 声明取并集)
1098
1100
 
1099
1101
  每区:一句话定位 + 详细入口(工具名 / guide 子命令)。详情永远以对应工具的 guide/ccc-config 为单一真相源。
1100
1102
  `;
@@ -1151,7 +1153,12 @@ msm("<name>", ["--list"]) — list all MSMs
1151
1153
  msm("<name>", ["--schema", "<n>"]) — view a specific MSM's parameter schema
1152
1154
  msm("<name>", ["--format=json"]) — JSON output mode (remaining args passed through losslessly)
1153
1155
  `;
1154
- /** CCC 配置参考(对齐 osp ccc-config action) */
1156
+ /**
1157
+ * CCC 配置参考(对齐 osp ccc-config action)。
1158
+ *
1159
+ * 🔴 **2026-10-19 起导出**:`container_admin config docs config` 的两层菜单把它作为
1160
+ * 「config」章正文返回。**不另写一份**——同一段说明有第二份副本就会漂(本轨反复踩过)。
1161
+ */
1155
1162
  const CCC_CONFIG_REFERENCE = `═══ CCC Configuration Reference ═══
1156
1163
 
1157
1164
  CCC-level features are configured in .opencode/serenity.json.
@@ -1227,15 +1234,15 @@ safe-mode 由 WebUI 开关控制(写 .serenity-safe-on 标记);黑名单
1227
1234
  Config:
1228
1235
  { "safeMode": { "blacklist": [".secrets/"], "hiddenTools": ["workflow", "job_kill"] } }
1229
1236
 
1230
- ── 6. skiff.roles(F4 认知子集角色)──
1231
- Skiff 角色 = 全知全能 cognitive trajectory 的任意子集(CCC 定义)。每角色独立模型 + 双白名单
1237
+ ── 6. 轨迹约束.roles(F4 认知子集角色)──
1238
+ 轨迹约束角色 = 全知全能 cognitive trajectory 的任意子集(CCC 定义)。每角色独立模型 + 双白名单
1232
1239
  (MSM 白名单 msms[] 与非 MSM 工具白名单 tools[],白名单外全隐藏;skill 加载恒可用)。
1233
1240
  cognitive trajectory 纪律子集(session/keeper/rebuild 参与项)默认全关 = 完全独立。
1234
1241
  systemPrompt 内联 或 systemPromptFile(.md 引用,推荐)——角色会话的完整人格/边界提示词。
1235
- validate 校验 / apply 生效 / list 查看:container_admin role <guide|validate|apply|list>(skiff_admin 已并入)。
1242
+ validate 校验 / apply 生效 / list 查看:container_admin config <guide|validate|apply|list>(原 role 域已并入 config,旧名作过渡别名仍可用)。
1236
1243
 
1237
1244
  Config:
1238
- { "skiff": { "roles": {
1245
+ { "轨迹约束": { "roles": {
1239
1246
  "qa": {
1240
1247
  "model": "provider/model",
1241
1248
  "msms": ["web-search", "vlm-describe"],
@@ -1255,12 +1262,12 @@ CCC 的一条 cognitive trajectory = 持久身体(SESSION.md)+ 时间轴(A
1255
1262
  运行态诊断(ACC 负责人专用):acc-diag(① live 运行态 / ①b 唤醒时钟 / ③ 唤醒注册表)。
1256
1263
 
1257
1264
  ▸ cognitiveTrajectory.skills(v1.44.0,**CCC 级 skill 供给**;owner 令 2026-09-20「要求实现我的诉求……
1258
- 注入是动态注入就行;create 先不触发吧;**skiff 也支持**」):
1265
+ 注入是动态注入就行;create 先不触发吧;**轨迹约束也支持**」):
1259
1266
  **本 CCC 的每条轨迹都带这些 skill**——声明的 skill 全文在该轨迹**被绑定期间**注入系统提示词
1260
1267
  (**动态注入**:每请求重新求值、不缓存 ⇒ 改配置或改 frontmatter **即时生效**,无需重新 use、无需重启)。
1261
1268
  两条声明来源**并集**(不是二选一):本键 **cognitiveTrajectory.skills**(容器给的底座,**在前**)
1262
1269
  + 轨迹自己 SESSION.md 顶部 frontmatter 的 skills:(这条轨迹额外的,**追加**;同名去重)。
1263
- ✅ **含 skiff 角色会话**(旁路的是 ACC 身份与纪律;skill 是"工作资料",照给)。
1270
+ ✅ **含 轨迹约束角色会话**(旁路的是 ACC 身份与纪律;skill 是"工作资料",照给)。
1264
1271
  守卫:名字先过安全校验才用于拼路径(防穿越)|**合并后**统一 32 KB 上限截断|找不到响亮写 [缺失]。
1265
1272
  ⚠️ 与 create **无关**(create 刻意不夺绑定,U6)⇒ 注入的门是"**存在绑定**"(cognitive_trajectory use)。
1266
1273
  失败语义:未配置 / 字段缺失 / 配置损坏 ⇒ **空数组**(= 无 CCC 级供给;轨迹级声明照旧生效)。
@@ -1268,7 +1275,7 @@ CCC 的一条 cognitive trajectory = 持久身体(SESSION.md)+ 时间轴(A
1268
1275
 
1269
1276
  Config:
1270
1277
  { "cognitiveTrajectory": {
1271
- "skills": ["acc-session", "cce"], // v1.44.0:本 CCC 的每条轨迹都带(含 skiff 会话)
1278
+ "skills": ["acc-session", "cce"], // v1.44.0:本 CCC 的每条轨迹都带(含 轨迹约束会话)
1272
1279
  "autopilot": { // 原 autopilotTrajectory 段,字段不变(旧键仍回退读)
1273
1280
  "enabled": true,
1274
1281
  "intervalHours": 2,
@@ -1282,7 +1289,7 @@ CCC 的一条 cognitive trajectory = 持久身体(SESSION.md)+ 时间轴(A
1282
1289
  CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC 独立对接微信桥。
1283
1290
  账号/路由/开关在此文件;**bot_token 凭据在 CCC localstore credential scope**
1284
1291
  (扫码绑定后自动写入,永不进 git 明文面)。
1285
- 路由 user → role:exact 优先,* 通配兜底;role 必须 ∈ 该 CCC skiff.roles。
1292
+ 路由 user → role:exact 优先,* 通配兜底;role 必须 ∈ 该 CCC 轨迹约束.roles。
1286
1293
  面板(WebUI 设置 → 微信桥)可扫码绑定/移除账号/编辑路由;msm("weixin-doctor", ["status"|"diag"|"verify"|"guide"]) 排查 + 大而全指南。凭据主动查看:localstore get WEIXIN_<ACCOUNT>_TOKEN。
1287
1294
  ▸ 主动发送(v1.31.0:**ACC 工具 im-bridge**):调
1288
1295
  im-bridge({channel:"weixin", action:"send", user:"<别名|id>", text:"<文本>"}) 给用户发消息——
@@ -1290,7 +1297,7 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
1290
1297
  成功后触发 outgoing hook(source="proactive")。文件用 action:"send-file" + file:"<CCC 内路径>"
1291
1298
  (逃逸/缺失/超 20MB 拒绝)+ 可选 caption。**可见性**:本 CCC 配置了 IM 通道
1292
1299
  (weixin.enabled=true)才出现在模型工具清单里;未配置 → 由 guards 逐 agent
1293
- tools.restrict({deny:['im-bridge']}) 移除(不是"看得到但被拒")。skiff 角色另需在
1300
+ tools.restrict({deny:['im-bridge']}) 移除(不是"看得到但被拒")。轨迹约束角色另需在
1294
1301
  roles.<role>.tools 白名单列出 im-bridge。
1295
1302
  历史(v1.30.9~v1.30.x):CCC 侧 MSM weixin-send(走 loopback HTTP 入口 3082)——
1296
1303
  v1.31.0 已退役(脚本删除 + 注册表注销);3082 入口保留给**非 agent 调用者**。
@@ -1834,7 +1841,7 @@ async function runMsmAsync(root, args) {
1834
1841
  * **轨迹提示词** = 一条认知轨迹的**可编辑章程声明**,由 ACC 按固定顺序装配进系统提示词。
1835
1842
  * 三个关键词各是一条判据:
1836
1843
  * - **可编辑**:住在 CCC(盘上),改它**不发版**(D23 归属判据的直接推论)
1837
- * - **声明**:它声明**身份/章程/方法论**,**不声明能力**(能力面归 `skiff.json` 白名单与工具 schema)
1844
+ * - **声明**:它声明**身份/章程/方法论**,**不声明能力**(能力面归 `trajectory-constraint.json` 白名单与工具 schema)
1838
1845
  * - **固定顺序**:装配顺序由 ACC 定死(见 `seams/system-prompt.ts` 的 ⑧ 位)
1839
1846
  *
1840
1847
  * ## 为什么取代「人格」
@@ -1849,13 +1856,13 @@ async function runMsmAsync(root, args) {
1849
1856
  *
1850
1857
  * - **跟着轨迹走**:`archive` 时整个目录搬走,章程不会掉队(旧形态 `.opencode/skiff/<name>.md`
1851
1858
  * 与轨迹**分居两处** —— 多条轨迹共用一份文件正是那个后果)。
1852
- * - **每条轨迹都能有**:不只是 skiff。普通轨迹同样需要"我是谁、按什么章程"。
1853
- * - 与 `SESSION.md`(身体:延续/日志)、`skiff.json`(约束面:能力)三件并列,
1859
+ * - **每条轨迹都能有**:不只是 轨迹约束。普通轨迹同样需要"我是谁、按什么章程"。
1860
+ * - 与 `SESSION.md`(身体:延续/日志)、`trajectory-constraint.json`(约束面:能力)三件并列,
1854
1861
  * 各管一件事,不合并(合并会让"改能力"与"改身份"互相牵连)。
1855
1862
  *
1856
1863
  * ## 兼容(不静默坏)
1857
1864
  *
1858
- * `PROMPT.md` 不存在时**不报错**(普通轨迹常态)。skiff 的旧字段 `systemPromptFile`
1865
+ * `PROMPT.md` 不存在时**不报错**(普通轨迹常态)。轨迹约束的旧字段 `systemPromptFile`
1859
1866
  * 在兼容期仍被读取——优先级见 {@link resolveTrajectoryPrompt}。
1860
1867
  */
1861
1868
  /** 轨迹提示词的固定文件名(**判据就是它存在**——不是目录名前缀,也不是配置里的一行) */
@@ -1867,9 +1874,9 @@ function trajectoryPromptPath(root, dirName) {
1867
1874
  /**
1868
1875
  * 该轨迹**有没有**轨迹提示词(单一判据)。
1869
1876
  *
1870
- * 用途:兼容期的"让位"判定 —— 若已有 `PROMPT.md`,skiff 旧字段 `systemPromptFile`
1871
- * 必须**让位**,否则同一份章程会在两处各注入一遍(第 ⑧ 段 + `serenity-skiff` 段)。
1872
- * 🔴 判据只有这一处:别在 skiff 那边自己拼路径再判(两处会漂)。
1877
+ * 用途:兼容期的"让位"判定 —— 若已有 `PROMPT.md`,轨迹约束 旧字段 `systemPromptFile`
1878
+ * 必须**让位**,否则同一份章程会在两处各注入一遍(第 ⑧ 段 + `serenity-轨迹约束` 段)。
1879
+ * 🔴 判据只有这一处:别在 轨迹约束那边自己拼路径再判(两处会漂)。
1873
1880
  */
1874
1881
  function hasTrajectoryPrompt(root, dirName) {
1875
1882
  try {
@@ -1958,7 +1965,7 @@ function trajectoryPromptBlock(text) {
1958
1965
  ].join("\n");
1959
1966
  }
1960
1967
  //#endregion
1961
- //#region src/role/worker/idle.ts
1968
+ //#region src/trajectory/carrier.ts
1962
1969
  /**
1963
1970
  * 等待 agent 空闲或销毁。
1964
1971
  * @param ctx 宿主插件上下文(事件通道来源;**结构化访问**,故类型为 `unknown`,见文件头 I2 段)
@@ -2105,7 +2112,7 @@ function truncateContent(content, maxChars) {
2105
2112
  * 六条硬性规则(逐条对应设计 §4.2,改这里先改那里):
2106
2113
  * ① **只枚举,不注入正文** —— 索引里**绝不**出现任何参考文件的正文;
2107
2114
  * ② **确定性排序** = 相对路径的 **UTF-8 字节序**(🔴 **不用 mtime**:mtime 随检出漂移,
2108
- * 会让每请求的注入文本抖动;对照 `src/wake/cro.ts:101` 那份**落盘快照**用 {name,bytes,mtime} ——
2115
+ * 会让每请求的注入文本抖动;对照 `src/cro/core.ts:101` 那份**落盘快照**用 {name,bytes,mtime} ——
2109
2116
  * **同一件事两处的取舍相反,那个差异是判据不是巧合**);
2110
2117
  * ③ **有界 + 响亮** —— 条数/字符各有上限,超出**必须显式写出还剩多少项**,**绝不静默截断**;
2111
2118
  * ④ **摘要可复现** —— 取该文件首个 `#` 标题(剥 markdown 记号);取不到 ⇒ 留空,**不编**;
@@ -2600,7 +2607,7 @@ function buildCognitiveTrajectorySkillsSection(root, mdPath, maxChars = COGNITIV
2600
2607
  * ⇒ 本模块让每个 CCC 留下一份**纯计数**账本:**用了哪个、多少次、第一次和最近一次是什么时候**。
2601
2608
  * 用途 = 清理/精简的**依据**(没有人用的东西与天天在用的东西,不再靠猜)。
2602
2609
  *
2603
- * ## 🔴 铁律(与 `wake/cro-log.ts` 同族)
2610
+ * ## 🔴 铁律(与 `cro/log.ts` 同族)
2604
2611
  *
2605
2612
  * **本模块每个导出函数都不抛错**:读盘 / 解析 / 写盘失败一律返回结构化失败,
2606
2613
  * 由调用方只记一行日志(或什么都不做)⇒ **统计的问题绝不影响工具执行与既有链路**。
@@ -2616,7 +2623,7 @@ function buildCognitiveTrajectorySkillsSection(root, mdPath, maxChars = COGNITIV
2616
2623
  *
2617
2624
  * 宿主会被重启(本容器实测 24h 内 3 次)⇒ 计数**不能只活在内存里**:
2618
2625
  * 每次写盘 = **读旧文件 → 在旧值上自增 → 原子写**(`tmp + rename`,与 `wake/registry.ts` /
2619
- * `wake/cro-log.ts` 同款形态)⇒ **进程重启后计数自然延续**,无需额外的 flush/落盘时机。
2626
+ * `cro/log.ts` 同款形态)⇒ **进程重启后计数自然延续**,无需额外的 flush/落盘时机。
2620
2627
  *
2621
2628
  * ## 口径(两个字段,勿合并)
2622
2629
  *
@@ -2899,6 +2906,48 @@ function verifyTotpCode(secretBase32, code, nowSeconds = nowEpochSeconds()) {
2899
2906
  return null;
2900
2907
  }
2901
2908
  //#endregion
2909
+ //#region src/kernel/ports.ts
2910
+ /**
2911
+ * 集中端口表。
2912
+ * A=宿主主面(插件不监听)/ B=网关(可配,默认关)/ C=微信发送(默认开,仅 loopback)
2913
+ * / D=轨迹约束调试(默认关)。
2914
+ */
2915
+ const FACE_PORTS = {
2916
+ main: {
2917
+ port: 3080,
2918
+ host: null,
2919
+ defaultEnabled: true,
2920
+ meaning: "宿主 DSH 主 WebUI + 插件 /serenity/* 路由面(**插件不起此 listener**,端口自宿主读)"
2921
+ },
2922
+ gateway: {
2923
+ port: 3081,
2924
+ host: "0.0.0.0",
2925
+ defaultEnabled: false,
2926
+ meaning: "双端口网关(外部登录 + 全量反代宿主主面;host/port 走 plugin 全局配置)"
2927
+ },
2928
+ weixinSend: {
2929
+ port: 3082,
2930
+ host: "127.0.0.1",
2931
+ defaultEnabled: true,
2932
+ meaning: "微信主动发送面(容器外进程 → POST /send;与 3080 显式解耦,不经网关)"
2933
+ }
2934
+ };
2935
+ FACE_PORTS.main.port;
2936
+ /** 网关端口默认值(面 B) */
2937
+ const GATEWAY_PORT = FACE_PORTS.gateway.port;
2938
+ /** 微信主动发送端口默认值(面 C) */
2939
+ const WEIXIN_SEND_PORT = FACE_PORTS.weixinSend.port;
2940
+ /**
2941
+ * **机制端口**词表(output-guard 用:agent 在**用户可见输出**里提及内部端口即打回)。
2942
+ *
2943
+ * 由 {@link FACE_PORTS} 机械派生 ⇒ 与面清单**永不漂移**。
2944
+ * ⚠️ 修复记录(C4 块 B,2026-09-15):旧常量硬写 `['3080','3081','3099','3100']`
2945
+ * ——**漏 3082**(微信发送面,默认开)⇒ 守卫拦不住"3082"这类提及。现由表派生,四面齐全。
2946
+ * ⚠️ 面变更记录(2026-10-15):**面 E(3100 ACP/问答)已整体退役**(owner 裁「砍掉外部问答」)
2947
+ * ⇒ 本词表随之少一项,**无需另改**(这正是"派生而非手抄"的收益)。
2948
+ */
2949
+ const MECHANISM_PORTS = Object.keys(FACE_PORTS).map((id) => String(FACE_PORTS[id].port));
2950
+ //#endregion
2902
2951
  //#region src/kernel/config.ts
2903
2952
  /**
2904
2953
  * kernel/config.ts — plugin 全局配置读写(结构化,`~/.dsh/serenity-hooks.json`)
@@ -3221,7 +3270,7 @@ function toolsBlock() {
3221
3270
  " cheap-worker — delegate work to a worker agent on the CCC-configured model; one serial child per task, returns its final text. `jobs=[...]` fans out in parallel (cap 10, results keep your order). No loop, no completion code, no progress file. The model is NOT selectable per call (read from the CCC config); a worker's tool set excludes cheap-worker itself",
3222
3271
  " serenity-skill-creator — creator for serenity-skills (this container's enhanced skill standard): guide (authoring/optimization method — read first) / create <name> (scaffold SKILL.md + skill.ttl + references/, base-chain import path computed) / check <name> (A1 structural + A2 base-chain; three states: modeled-ok / plain-skill / modeled-but-check-could-not-run) / review <name> (C tier: axiom-vs-text compliance question list, half-mechanical) / index <name> (preview the second-layer resource list the model will see) / status (which skills are modeled). A skill is \"modeled\" iff it has a file named exactly skill.ttl next to SKILL.md; a plain skill is NOT an anomaly. Only `create` writes, and it refuses an existing directory",
3223
3272
  " localstore — ACC local credential/config storage (CCC-root localstore.json; git policy localstore.gitTrack default deny)",
3224
- " container_admin — container administration (the maintenance bay): role (Skiff roles: guide/validate/apply/list) / msm (register/deregister/check/guide/catalog/ccc-config) / config (view)",
3273
+ " container_admin — container administration (the maintenance bay): role (轨迹约束 roles: guide/validate/apply/list) / msm (register/deregister/check/guide/catalog/ccc-config) / config (view)",
3225
3274
  "",
3226
3275
  "MSM call (registered CCC MSMs, deterministic Mech & Semi-Mech):",
3227
3276
  " Execute: msm(\"<name>\", [\"<arg1>\", \"<arg2>\"]) — run a registered MSM directly (partial name returns matching candidates)",
@@ -3721,7 +3770,7 @@ function registerEntrySkillSection(agent, root) {
3721
3770
  *
3722
3771
  * **声明来源(两条,并集)**:
3723
3772
  * · **CCC 级**(v1.44.0,owner 令)= `.opencode/serenity.json` 的 `cognitiveTrajectory.skills`
3724
- * ——**本 CCC 的每条轨迹都带**(含 skiff 角色会话),是容器给的底座;
3773
+ * ——**本 CCC 的每条轨迹都带**(含 轨迹约束角色会话),是容器给的底座;
3725
3774
  * · **轨迹级** = 该会话绑定的轨迹(`readLastBound(session).mdPath` = 它的 `SESSION.md`)
3726
3775
  * 顶部 frontmatter 的 `skills:`——这条轨迹额外的。
3727
3776
  * 合并规则与顺序见 `cognitive-trajectory-skills.mergeSkillNames`(CCC 级在前,同名去重)。
@@ -3768,13 +3817,19 @@ function registerCognitiveTrajectorySkillSection(agent, root) {
3768
3817
  }
3769
3818
  }
3770
3819
  //#endregion
3771
- //#region src/role/worker/core.ts
3820
+ //#region src/trajectory/delegate/core.ts
3772
3821
  /**
3773
- * role/worker/core.ts — cheap-worker(杂工)纯逻辑层(零 DSH 依赖,可独立单测)
3822
+ * trajectory/delegate/core.ts — cheap-worker(杂工)纯逻辑层(零 DSH 依赖,可独立单测)
3774
3823
  *
3775
3824
  * 🆕 2026-10-14(owner 令「删除后台运行能力」):**进度文件 / stop token / 轮次循环**整族已退场 ——
3776
3825
  * 它们**只**服务于 background 模式。本模块现存 = 前台委派真正需要的三件:
3777
3826
  * 模型串解析 / 白名单校验 / provider 可用性判读(+ 使用指引文本)。
3827
+ *
3828
+ * ## S14(2026-10-19,轨迹中心重构):此前位于 `role/worker/` 下(该目录已消失)
3829
+ *
3830
+ * 落点 = `trajectory/delegate/`。判据(判读表 §3.1):它是**轨迹 × 轨迹**那种合法形态 ——
3831
+ * 主语是「主轨迹把活交给子 agent」,既不是某一条轨迹的切面,也不是容器宪法。
3832
+ * ⚠️ 它**不属于** `policy/`:policy 管"这条轨迹允许什么",本模块管"一条轨迹怎么把活派出去"。
3778
3833
  */
3779
3834
  /** 解析 model 字符串(provider/model)→ {provider, model};无 / 视为 model-only */
3780
3835
  function splitModel(model) {
@@ -3910,42 +3965,38 @@ Before calling cheap-worker, load eap (acc-eap skill) and design the "scale-up c
3910
3965
  (and "output" for whatever partial text was produced).
3911
3966
  `;
3912
3967
  //#endregion
3913
- //#region src/role/skiff/core.ts
3914
- /** `provider/model` → agentOptions(由 worker/core 提供,保持单一实现) */
3915
- const skiffAgents = /* @__PURE__ */ new Map();
3916
- /** 查 sessionId 的会话绑定(轨迹 + ccc;无 → null)——追问校验 */
3917
- function skiffSessionInfo(sessionId) {
3918
- return skiffSessionInfo$1(sessionId);
3919
- }
3968
+ //#region src/trajectory/session.ts
3969
+ /** `provider/model` → agentOptions(由 delegate/core 提供,保持单一实现) */
3970
+ const trajectoryAgents = /* @__PURE__ */ new Map();
3920
3971
  /** 查 sessionId 的活体 agent(进程内会话延续用;未注册/已清理 → undefined) */
3921
- function getSkiffAgent(sessionId) {
3922
- return skiffAgents.get(sessionId);
3972
+ function getTrajectoryAgent(sessionId) {
3973
+ return trajectoryAgents.get(sessionId);
3923
3974
  }
3924
- /** 该会话当前绑定的 skiff 约束面(不是 skiff → null)。**守门与提示词的唯一查询入口。** */
3925
- function skiffProfileFor(root, sessionId) {
3975
+ /** 该会话当前绑定的 轨迹约束面(不是 轨迹约束 → null)。**守门与提示词的唯一查询入口。** */
3976
+ function trajectoryConstraintFor(root, sessionId) {
3926
3977
  if (sessionId === void 0) return null;
3927
- return skiffProfileForSession(root, sessionId, skiffTrajectoryDirNameFor(sessionId));
3978
+ return trajectoryConstraintForSession(root, sessionId, trajectoryDirNameForSession(sessionId));
3928
3979
  }
3929
- function registerSkiffSession(sessionId, trajectoryDirName, ccc, agent) {
3930
- skiffAgents.set(sessionId, agent);
3931
- registerSkiffSession$1(sessionId, trajectoryDirName, ccc);
3980
+ function registerTrajectorySession(sessionId, trajectoryDirName, ccc, agent) {
3981
+ trajectoryAgents.set(sessionId, agent);
3982
+ registerTrajectorySession$1(sessionId, trajectoryDirName, ccc);
3932
3983
  }
3933
- function unregisterSkiffSession(sessionId) {
3934
- skiffAgents.delete(sessionId);
3935
- unregisterSkiffSession$1(sessionId);
3984
+ function unregisterTrajectorySession(sessionId) {
3985
+ trajectoryAgents.delete(sessionId);
3986
+ unregisterTrajectorySession$1(sessionId);
3936
3987
  }
3937
3988
  /**
3938
- * 该 skiff 会话的**工作台路径** = 它自己轨迹的 SESSION.md。
3989
+ * 该 轨迹约束会话的**工作台路径** = 它自己轨迹的 SESSION.md。
3939
3990
  *
3940
- * 🔴 与旧版的差别:旧版会在这里**自动新建**一个 SESSION(`ensureSkiffSession`);
3941
- * 新版**没有新建这个动作** —— 轨迹就是工作台,`skiff.json` 与 `SESSION.md` 同处一个目录。
3991
+ * 🔴 与旧版的差别:旧版会在这里**自动新建**一个 SESSION(`ensureTrajectorySession`);
3992
+ * 新版**没有新建这个动作** —— 轨迹就是工作台,`trajectory-constraint.json` 与 `SESSION.md` 同处一个目录。
3942
3993
  * ⇒ 语义变成纯粹的"查询:我自己的身体在哪"。
3943
3994
  *
3944
- * @returns SESSION.md 绝对路径;不是 skiff / 文件不在 → null
3995
+ * @returns SESSION.md 绝对路径;不是 轨迹约束 / 文件不在 → null
3945
3996
  */
3946
- function skiffWorkspaceMdPath(root, sessionId) {
3997
+ function trajectoryWorkspaceMdPath(root, sessionId) {
3947
3998
  if (sessionId === void 0) return null;
3948
- const dirName = skiffTrajectoryDirNameFor(sessionId);
3999
+ const dirName = trajectoryDirNameForSession(sessionId);
3949
4000
  if (dirName === null) return null;
3950
4001
  const md = join(sessionsRoot(root), dirName, "SESSION.md");
3951
4002
  return existsSync(md) ? md : null;
@@ -3953,12 +4004,12 @@ function skiffWorkspaceMdPath(root, sessionId) {
3953
4004
  /**
3954
4005
  * 系统提示词中的**工作台纪律块**(S142 用户:只给路径 LLM 不会主动用)。
3955
4006
  *
3956
- * SESSION = 工作台 —— skiff 与主舱同一套机制(**零特调**,用户拍板):
4007
+ * SESSION = 工作台 —— 轨迹约束与主舱同一套机制(**零特调**,用户拍板):
3957
4008
  * SESSION.md 是持久记忆载体、rebuild 自动从本 SESSION 续接。
3958
4009
  * 注入 agent 系统提示词(用户对话面不可见)。
3959
4010
  *
3960
4011
  * v1.30.13(S142 诊断 D5):写入纪律**不点名具体工具** —— 旧文案写死 "with write/edit",
3961
- * 而某个 skiff 的工具白名单里根本没有 write/edit ⇒ 指令指向它没有的工具
4012
+ * 而某个 轨迹约束的工具白名单里根本没有 write/edit ⇒ 指令指向它没有的工具
3962
4013
  * (注入纪律与能力面矛盾,模型只能猜)。现文案改为「用本轨迹被授权的写入通道」。
3963
4014
  */
3964
4015
  function workspaceCognitiveTrajectoryLine(mdPath) {
@@ -3986,10 +4037,10 @@ const workspaceSectionAgents = /* @__PURE__ */ new WeakSet();
3986
4037
  /**
3987
4038
  * 把**工作台纪律块**挂到 agent 的系统提示词段(单一注入点)。
3988
4039
  *
3989
- * 用户原话(S142,2026-09-08):"微信桥的注入机制,每个用户消息都会注入,skiff 本身也会注入,
3990
- * 这样就重复,能否微信桥情况下,注入内容直接取 skiff 的,这样不用配两遍"。
4040
+ * 用户原话(S142,2026-09-08):"微信桥的注入机制,每个用户消息都会注入,轨迹约束本身也会注入,
4041
+ * 这样就重复,能否微信桥情况下,注入内容直接取 轨迹约束的,这样不用配两遍"。
3991
4042
  *
3992
- * 现形态:工作台行**只**经本函数挂系统提示词段(name `serenity-skiff-workspace`,
4043
+ * 现形态:工作台行**只**经本函数挂系统提示词段(name `serenity-轨迹约束-workspace`,
3993
4044
  * 动态 `text()` 每轮按 scope 读当前活跃 mdPath)。
3994
4045
  *
3995
4046
  * @returns true = 段已在位;false = 该 agent 挂不上(调用方降级为 question 前缀注入)
@@ -4001,39 +4052,39 @@ function ensureWorkspacePromptSection(agent, scope, root) {
4001
4052
  if (typeof api?.section !== "function") return false;
4002
4053
  try {
4003
4054
  api.section({
4004
- name: "serenity-skiff-workspace",
4055
+ name: "serenity-轨迹约束-workspace",
4005
4056
  order: -55,
4006
4057
  text: () => {
4007
- const mdPath = skiffWorkspaceMdPath(root, scope);
4058
+ const mdPath = trajectoryWorkspaceMdPath(root, scope);
4008
4059
  return mdPath ? workspaceCognitiveTrajectoryLine(mdPath) : "";
4009
4060
  }
4010
4061
  });
4011
4062
  workspaceSectionAgents.add(agent);
4012
4063
  return true;
4013
4064
  } catch (err) {
4014
- console.warn(`[serenity-hooks] skiff 工作台提示词段注册失败(回退 question 注入): ${String(err?.message ?? err)}`);
4065
+ console.warn(`[serenity-hooks] 轨迹约束 工作台提示词段注册失败(回退 question 注入): ${String(err?.message ?? err)}`);
4015
4066
  return false;
4016
4067
  }
4017
4068
  }
4018
- /** skiff agent 挂载的 DSH preset(平台工具面由 preset 决定;白名单再按轨迹过滤) */
4019
- const SKIFF_PRESET = "standard";
4069
+ /** 轨迹约束 agent 挂载的 DSH preset(平台工具面由 preset 决定;白名单再按轨迹过滤) */
4070
+ const TRAJECTORY_PRESET = "standard";
4020
4071
  /**
4021
- * 创建(或复用)一个 skiff 会话。
4072
+ * 创建(或复用)一个 轨迹约束会话。
4022
4073
  *
4023
4074
  * **固定 sessionId 的 resume-or-create**(v1.27.2):指定 sessionId 且磁盘已有持久化 log →
4024
4075
  * `ctx.agents.resume`(DSH 持久化语义:sessionId 即身份,历史 + turn 续号延续);
4025
4076
  * 无 log(首次)→ `ctx.agents.create`。不指定 → 随机 id 恒 create(调试页路径)。
4026
4077
  *
4027
- * @param profile 该 skiff 的约束面(来自 `skiff.json`)
4078
+ * @param profile 该 轨迹约束的约束面(来自 `trajectory-constraint.json`)
4028
4079
  * @param sessionId 指定会话 id(可选;微信桥等外部面用固定 id 实现"轨迹 ↔ 会话"长期映射)
4029
4080
  */
4030
- async function createSkiffAgent(ctx, root, profile, defaultModel, sessionId) {
4031
- if (!ctx.agents) throw new Error("skiff: ctx.agents unavailable");
4081
+ async function createTrajectoryAgent(ctx, root, profile, defaultModel, sessionId) {
4082
+ if (!ctx.agents) throw new Error("轨迹约束: ctx.agents unavailable");
4032
4083
  const model = profile.model?.trim() || defaultModel || "";
4033
- const id = sessionId ?? `skiff-${profile.code}-${randomUUID()}`;
4084
+ const id = sessionId ?? `轨迹约束-${profile.code}-${randomUUID()}`;
4034
4085
  const setup = async (agentCtx) => {
4035
4086
  try {
4036
- await agentCtx.get("agentPresets")?.mount?.(agentCtx, SKIFF_PRESET);
4087
+ await agentCtx.get("agentPresets")?.mount?.(agentCtx, TRAJECTORY_PRESET);
4037
4088
  } catch {}
4038
4089
  };
4039
4090
  const handle = await createOrResumeAgent(ctx, id, root, model, setup, sessionId !== void 0);
@@ -4043,7 +4094,7 @@ async function createSkiffAgent(ctx, root, profile, defaultModel, sessionId) {
4043
4094
  const livePrompt = () => {
4044
4095
  let live = profile;
4045
4096
  try {
4046
- const fresh = readSkiffProfileAt(join(sessionsRoot(root), profile.dirName), root);
4097
+ const fresh = readTrajectoryConstraintAt(join(sessionsRoot(root), profile.dirName), root);
4047
4098
  if (fresh) live = fresh;
4048
4099
  } catch {}
4049
4100
  const wantPath = live.systemPromptFile?.trim();
@@ -4056,23 +4107,23 @@ async function createSkiffAgent(ctx, root, profile, defaultModel, sessionId) {
4056
4107
  livePrompt();
4057
4108
  try {
4058
4109
  agent.ctx.systemPrompt.section({
4059
- name: "serenity-skiff",
4110
+ name: "serenity-轨迹约束",
4060
4111
  order: -60,
4061
4112
  text: () => {
4062
4113
  const legacy = hasTrajectoryPrompt(root, profile.dirName) ? "" : livePrompt();
4063
- return [buildSkiffBasePrompt(profile.code), legacy].filter(Boolean).join("\n");
4114
+ return [buildTrajectoryBasePrompt(profile.code), legacy].filter(Boolean).join("\n");
4064
4115
  }
4065
4116
  });
4066
4117
  } catch (err) {
4067
- console.warn(`[serenity-hooks] skiff 系统提示词注册失败: ${String(err?.message ?? err)}`);
4118
+ console.warn(`[serenity-hooks] 轨迹约束 系统提示词注册失败: ${String(err?.message ?? err)}`);
4068
4119
  }
4069
4120
  if (agent?.session) try {
4070
4121
  registerCognitiveTrajectorySkillSection(agent, root);
4071
4122
  } catch (err) {
4072
- console.warn(`[serenity-hooks] skiff 轨迹 skill 注入注册失败(不影响 agent 创建): ${String(err?.message ?? err)}`);
4123
+ console.warn(`[serenity-hooks] 轨迹约束轨迹 skill 注入注册失败(不影响 agent 创建): ${String(err?.message ?? err)}`);
4073
4124
  }
4074
- if (skiffWorkspaceMdPath(root, id) !== null) ensureWorkspacePromptSection(agent, id, root);
4075
- registerSkiffSession(id, profile.dirName, root, agent);
4125
+ if (trajectoryWorkspaceMdPath(root, id) !== null) ensureWorkspacePromptSection(agent, id, root);
4126
+ registerTrajectorySession(id, profile.dirName, root, agent);
4076
4127
  return {
4077
4128
  handle,
4078
4129
  agent,
@@ -4111,7 +4162,7 @@ async function createOrResumeAgent(ctx, id, root, model, setup, fixedId) {
4111
4162
  sessionId: id,
4112
4163
  meta: {
4113
4164
  cwd: root,
4114
- agentPreset: SKIFF_PRESET
4165
+ agentPreset: TRAJECTORY_PRESET
4115
4166
  },
4116
4167
  setup,
4117
4168
  ...model ? { agentOptions: splitModel(model) } : {}
@@ -4126,7 +4177,7 @@ async function createOrResumeAgent(ctx, id, root, model, setup, fixedId) {
4126
4177
  sessionId: id,
4127
4178
  meta: {
4128
4179
  cwd: root,
4129
- agentPreset: SKIFF_PRESET
4180
+ agentPreset: TRAJECTORY_PRESET
4130
4181
  },
4131
4182
  setup,
4132
4183
  ...model ? { agentOptions: splitModel(model) } : {}
@@ -4147,14 +4198,14 @@ async function createOrResumeAgent(ctx, id, root, model, setup, fixedId) {
4147
4198
  if (liveNow) return liveReuseRef(liveNow, setup);
4148
4199
  const msg = err instanceof Error ? err.message : String(err);
4149
4200
  const stack = err instanceof Error ? err.stack ?? "" : "";
4150
- console.log(`[serenity-hooks] skiff resume 失败降级 create (id=${id}): ${msg}`);
4151
- if (stack) console.log(`[serenity-hooks] skiff resume stack:\n${stack.slice(0, 1200)}`);
4201
+ console.log(`[serenity-hooks] 轨迹约束 resume 失败降级 create (id=${id}): ${msg}`);
4202
+ if (stack) console.log(`[serenity-hooks] 轨迹约束 resume stack:\n${stack.slice(0, 1200)}`);
4152
4203
  return {
4153
4204
  ...await ctx.agents.create({
4154
4205
  sessionId: id,
4155
4206
  meta: {
4156
4207
  cwd: root,
4157
- agentPreset: SKIFF_PRESET
4208
+ agentPreset: TRAJECTORY_PRESET
4158
4209
  },
4159
4210
  setup,
4160
4211
  ...model ? { agentOptions: splitModel(model) } : {}
@@ -4218,7 +4269,7 @@ async function waitForTurnAssistant(ctx, agent, beforeAssistantCount, opts = {})
4218
4269
  *
4219
4270
  * @param options.timeoutMs 本轮等待上限(测试注入;生产用缺省)
4220
4271
  */
4221
- async function askSkiff(ctx, agent, question, options) {
4272
+ async function askTrajectory(ctx, agent, question, options) {
4222
4273
  const beforeAssistant = countAssistantMessages(agent.session);
4223
4274
  agent.followup(createUserMessage({
4224
4275
  content: [{
@@ -4233,29 +4284,29 @@ async function askSkiff(ctx, agent, question, options) {
4233
4284
  };
4234
4285
  }
4235
4286
  /**
4236
- * skiff 会话的轨迹纪律参与判定:**不是 skiff 恒 true**(正常参与);
4237
- * skiff 按 `skiff.json` 的 `cognitiveTrajectory` 子集决定。
4287
+ * 轨迹约束会话的轨迹纪律参与判定:**不是 轨迹约束恒 true**(正常参与);
4288
+ * 轨迹约束 按 `trajectory-constraint.json` 的 `cognitiveTrajectory` 子集决定。
4238
4289
  *
4239
- * 注册表缺失(进程刚重启、绑定还没读)⇒ 由 `skiffProfileForSession` 回落持久绑定;
4240
- * 两者都不中 ⇒ 判"不是 skiff" ⇒ 恒 true(正常参与)。**fail-open 是有意的**:
4290
+ * 注册表缺失(进程刚重启、绑定还没读)⇒ 由 `trajectoryConstraintForSession` 回落持久绑定;
4291
+ * 两者都不中 ⇒ 判"不是 轨迹约束" ⇒ 恒 true(正常参与)。**fail-open 是有意的**:
4241
4292
  * 纪律参与项的失败不该让会话失去 keeper/rebuild(那是能力倒退,比多跑一次更糟)。
4242
4293
  */
4243
- function skiffCognitiveTrajectoryEnabled(root, sessionId, key) {
4294
+ function trajectoryConstraintParticipates(root, sessionId, key) {
4244
4295
  if (sessionId === void 0) return true;
4245
- const profile = skiffProfileFor(root, sessionId);
4296
+ const profile = trajectoryConstraintFor(root, sessionId);
4246
4297
  if (profile === null) return true;
4247
- return profileCognitiveTrajectory(profile)[key];
4298
+ return constraintCognitiveTrajectory(profile)[key];
4248
4299
  }
4249
4300
  /**
4250
- * `msm` 工具的 skiff 门控:非 skiff 恒放行;skiff 会话——
4301
+ * `msm` 工具的 轨迹约束门控:非 轨迹约束恒放行;轨迹约束会话——
4251
4302
  * exec 非白名单 MSM 拒绝(**不列名单**)、register/deregister 必拒、
4252
4303
  * list 白名单过滤、check/guide/ccc-config 只读放行。
4253
4304
  */
4254
- function skiffMsmGate(root, sessionId, action, name) {
4305
+ function trajectoryConstraintMsmGate(root, sessionId, action, name) {
4255
4306
  if (sessionId === void 0) return {};
4256
- const profile = skiffProfileFor(root, sessionId);
4307
+ const profile = trajectoryConstraintFor(root, sessionId);
4257
4308
  if (profile === null) return {};
4258
- if (action === "register" || action === "deregister") return { reject: "register/deregister is not allowed in skiff sessions" };
4309
+ if (action === "register" || action === "deregister") return { reject: "register/deregister is not allowed in 轨迹约束 sessions" };
4259
4310
  if (action === "exec") {
4260
4311
  if (!name || !profile.msms.includes(name)) return { reject: "MSM not allowed" };
4261
4312
  return {};
@@ -4342,7 +4393,7 @@ const msmTool = defineTool({
4342
4393
  if (!root) throw new Error(NO_CCC_FROM_AGENT_CWD);
4343
4394
  const sessionId = agentSessionId$1(exec);
4344
4395
  const name = args.name;
4345
- const gate = skiffMsmGate(root, sessionId, name ? "exec" : "list", name);
4396
+ const gate = trajectoryConstraintMsmGate(root, sessionId, name ? "exec" : "list", name);
4346
4397
  if (gate.reject) throw new Error(gate.reject);
4347
4398
  if (!name || name.trim() === "") return gate.whitelist ? "MSMs allowed in this role:\n" + [...gate.whitelist].join("\n") : buildMsmIndex(root);
4348
4399
  const clean = name.trim();
@@ -10043,9 +10094,9 @@ function listWakes(root, opts = {}) {
10043
10094
  };
10044
10095
  }
10045
10096
  //#endregion
10046
- //#region src/wake/cro.ts
10097
+ //#region src/cro/core.ts
10047
10098
  /**
10048
- * wake/cro.ts — CRO(Continuous Re-Occurrence · 持续再发生)机制层(S142 §7.8,2026-09-19)
10099
+ * cro/core.ts — CRO(Continuous Re-Occurrence · 持续再发生)机制层(S142 §7.8,2026-09-19)
10049
10100
  *
10050
10101
  * ## 这东西用来干什么(白话)
10051
10102
  *
@@ -10375,7 +10426,7 @@ function parseCroHostBinding(raw) {
10375
10426
  * `AGENT_SESSIONS/` 下每个目录查一次 `<目录>/continuous-re-occurrence.ts`。
10376
10427
  * · **一次 readdir + 每个目录一次 existsSync**:N 条轨迹的代价是 O(N) 次 `stat`,
10377
10428
  * 每 5min 一次 —— 与既有 `listSessions` 同量级,可接受。
10378
- * · 跳过 **`_` 前缀**(`_archived` / `_skiff-logs` / `_weixin-logs` = 系统与日志目录)
10429
+ * · 跳过 **`_` 前缀**(`_archived` / `_轨迹约束-logs` / `_weixin-logs` = 系统与日志目录)
10379
10430
  * 与 **`.` 前缀**(隐藏)——它们**不是轨迹**,且扫它们纯属浪费。
10380
10431
  * · 🔴 **本函数绝不抛错**(返回 `[]`)——它跑在调度器 tick 内,任何异常都可能影响既有链路
10381
10432
  * (设计 §5 铁律)。目录读不到(不存在 / 权限)⇒ `[]` = "没有轨迹启用 CRO",语义正确。
@@ -10669,9 +10720,9 @@ function registerDisposer(ctx, label, cleanup) {
10669
10720
  }
10670
10721
  }
10671
10722
  //#endregion
10672
- //#region src/wake/cro-turns.ts
10723
+ //#region src/cro/turns.ts
10673
10724
  /**
10674
- * wake/cro-turns.ts — 「这条轨迹是否正在跑轮次」的追踪(设计 §3.2,2026-09-19 S142)
10725
+ * cro/turns.ts — 「这条轨迹是否正在跑轮次」的追踪(设计 §3.2,2026-09-19 S142)
10675
10726
  *
10676
10727
  * ## 为什么需要这个模块(CRO 唯一需要 ACC 新增的状态)
10677
10728
  *
@@ -10797,7 +10848,7 @@ function runningCarriersOf(boundSessionIds, nowMs, ttlMs = TURN_TTL_MS) {
10797
10848
  * 🔴 **本处顺带更正 §2.7 的一处分档**:本文件原被列在"剩余 3 个属**真依赖**档"里,
10798
10849
  * 但**逐符号查证后与那个判断不符** ——
10799
10850
  * · `Context`:`ctx` 只被**结构化访问**(原码本就写着 `ctx as unknown as { on: … }`)
10800
- * ⇒ 该注解未提供任何类型保证(与 `role/worker/idle.ts` 同档)。
10851
+ * ⇒ 该注解未提供任何类型保证(与 `trajectory/carrier.ts` 同档)。
10801
10852
  * · `Agent`:**导入后从未使用** —— 事件 payload 一律按 `{ agent?: unknown }` 解构,
10802
10853
  * 再交给 `sessionIdOf(...)`(它收 `unknown`)。⇒ **纯粹是死 import**。
10803
10854
  * ⇒ **判据(本件再证一次,且比上一件更硬)**:分档**必须逐符号看"它有没有参与类型检查"**,
@@ -10845,9 +10896,9 @@ function registerCroTurnTracking(ctx) {
10845
10896
  });
10846
10897
  }
10847
10898
  //#endregion
10848
- //#region src/wake/cro-log.ts
10899
+ //#region src/cro/log.ts
10849
10900
  /**
10850
- * wake/cro-log.ts — CRO 唤起日志(**ACC 侧**,每轨迹一份固定名文件,2026-09-20 S142 §7.16)
10901
+ * cro/log.ts — CRO 唤起日志(**ACC 侧**,每轨迹一份固定名文件,2026-09-20 S142 §7.16)
10851
10902
  *
10852
10903
  * ## 所有者令(本模块的存在理由)
10853
10904
  *
@@ -10883,7 +10934,7 @@ function registerCroTurnTracking(ctx) {
10883
10934
  * ⇒ 清理与写入**是同一笔操作** ⇒ **结构上不存在"忘了清"的可能**(比"另设一个每 tick 的清理步"更硬:
10884
10935
  * 那多一个"可能忘了跑"的面)。窗宽是**常量**,**无配置键、无人工清理动作**。
10885
10936
  *
10886
- * ## 🔴 铁律(与 `wake/cro.ts` 同族)
10937
+ * ## 🔴 铁律(与 `cro/core.ts` 同族)
10887
10938
  *
10888
10939
  * **本模块的每个导出函数都不抛错**:写盘 / 裁剪 / 解析失败一律返回结构化失败,
10889
10940
  * 由调用方只记一行 tick 日志 ⇒ **CRO 流水的问题绝不影响投递与既有链路**(设计 §5)。
@@ -12006,9 +12057,9 @@ function registerWakeScheduler(ctx) {
12006
12057
  registerDisposer(ctx, "认知轨迹唤醒调度器", () => clock.dispose());
12007
12058
  }
12008
12059
  //#endregion
12009
- //#region src/wake/cro-guide.ts
12060
+ //#region src/cro/guide.ts
12010
12061
  /**
12011
- * wake/cro-guide.ts — CRO 编写指南(**ACC 内置**,S142 §7.8,2026-09-19)
12062
+ * cro/guide.ts — CRO 编写指南(**ACC 内置**,S142 §7.8,2026-09-19)
12012
12063
  *
12013
12064
  * ## 为什么指南在 ACC 而不在 CCC(所有者令)
12014
12065
  *
@@ -12771,20 +12822,20 @@ const localstoreTool = defineTool({
12771
12822
  }
12772
12823
  });
12773
12824
  //#endregion
12774
- //#region src/tools/skiff-admin.ts
12775
- /** Skiff 定义教程:概念 / `skiff.json` schema / 双白名单 / 轨迹纪律 / 微信路由 */
12776
- const SKIFF_GUIDE = `═══ Skiff Guide (2026-10-19 redesigned) ═══
12825
+ //#region src/tools/trajectory-constraint-admin.ts
12826
+ /** 轨迹约束 定义教程:概念 / `trajectory-constraint.json` schema / 双白名单 / 轨迹纪律 / 微信路由 */
12827
+ const TRAJECTORY_CONSTRAINT_GUIDE = `═══ 轨迹约束 Guide (2026-10-19 redesigned) ═══
12777
12828
 
12778
- **A skiff IS a cognitive trajectory — the one whose directory contains \`${SKIFF_PROFILE_FILENAME}\`.**
12829
+ **A 轨迹约束 IS a cognitive trajectory — the one whose directory contains \`${TRAJECTORY_CONSTRAINT_FILENAME}\`.**
12779
12830
  That is the whole rule. No role table, no name registry: put the file there and the trajectory
12780
- becomes a skiff; delete it and it goes back to being an ordinary trajectory (takes effect at once).
12831
+ becomes a 轨迹约束; delete it and it goes back to being an ordinary trajectory (takes effect at once).
12781
12832
 
12782
12833
  AGENT_SESSIONS/<YYYY-MM-DD--<code>--<desc>>/
12783
12834
  ├─ SESSION.md ← its persistent body (this IS the workspace — nothing is auto-created)
12784
- ├─ ${SKIFF_PROFILE_FILENAME} ← its constraints (this file is what makes it a skiff)
12835
+ ├─ ${TRAJECTORY_CONSTRAINT_FILENAME} ← its constraints (this file is what makes it a 轨迹约束)
12785
12836
  └─ TRAJECTORY-UID ← legacy, read-only (superseded by the code)
12786
12837
 
12787
- ── ${SKIFF_PROFILE_FILENAME} schema ──
12838
+ ── ${TRAJECTORY_CONSTRAINT_FILENAME} schema ──
12788
12839
  {
12789
12840
  "model": "provider/model", // optional; falls back to cheap-worker.defaultModel
12790
12841
  "tools": ["read","grep","msm",...], // non-MSM toolbar whitelist — anything not listed is REMOVED from the schema
@@ -12795,35 +12846,34 @@ becomes a skiff; delete it and it goes back to being an ordinary trajectory (tak
12795
12846
  }
12796
12847
 
12797
12848
  ── Two whitelists, both default-deny ──
12798
- - \`tools: []\` + non-empty \`msms\` = pure-MSM skiff (the usual knowledge-QA shape)
12849
+ - \`tools: []\` + non-empty \`msms\` = pure-MSM 轨迹约束 (the usual knowledge-QA shape)
12799
12850
  - non-empty \`msms\` ⇒ the \`msm\` tool is added automatically (the only entry point for MSMs)
12800
12851
  - anything outside both lists is invisible in the schema, not merely rejected at call time
12801
12852
 
12802
12853
  ── cognitiveTrajectory: why the defaults are TRUE ──
12803
- The workspace of a skiff is its own SESSION.md. Setting these to false would cut it off from
12854
+ The workspace of a 轨迹约束 is its own SESSION.md. Setting these to false would cut it off from
12804
12855
  its own body, so \`session\`/\`keeper\`/\`rebuild\` all default to true. Set false explicitly only
12805
12856
  when you deliberately want that participation off.
12806
12857
 
12807
12858
  ── WeChat routing: user → trajectory code ──
12808
12859
  \`weixin.routes\`: \`[{ "user": "<wx id or *>", "trajectory": "<code>" }]\`
12809
12860
  A route pointing at a code with no such trajectory is a hard error (the message is NOT delivered).
12810
- A trajectory WITHOUT \`${SKIFF_PROFILE_FILENAME}\` is still a legal target — it is just an ordinary trajectory.
12861
+ A trajectory WITHOUT \`${TRAJECTORY_CONSTRAINT_FILENAME}\` is still a legal target — it is just an ordinary trajectory.
12811
12862
 
12812
12863
  ── Operating it ──
12813
- Debug page: Settings → Serenity → enable the Skiff block → http://127.0.0.1:<debugPort>
12814
- Everything is read live: saving \`${SKIFF_PROFILE_FILENAME}\` takes effect on the next turn, no restart.
12864
+ Everything is read live: saving \`${TRAJECTORY_CONSTRAINT_FILENAME}\` takes effect on the next turn, no restart.
12815
12865
  (The prompt FILE hot-reloads by mtime; the JSON itself is re-read every turn.)
12816
- After editing, run container_admin role apply for an explicit validation + confirmation pass.
12817
- Skill loading is always available to skiff sessions (skills are not whitelisted — they are read-only knowledge).`;
12818
- /** 校验当前 CCC 的 skiff 约束面:msms 均已注册 / model ∈ cheap-worker.models / 提示词非空 */
12819
- function validateSkiffConfig(root) {
12820
- const profiles = listSkiffProfiles(root);
12866
+ After editing, run container_admin config apply for an explicit validation + confirmation pass.
12867
+ Skill loading is always available to 轨迹约束 sessions (skills are not whitelisted — they are read-only knowledge).`;
12868
+ /** 校验当前 CCC 的 轨迹约束面:msms 均已注册 / model ∈ cheap-worker.models / 提示词非空 */
12869
+ function validateTrajectoryConstraint(root) {
12870
+ const profiles = listTrajectoryConstraints(root);
12821
12871
  const issues = [];
12822
12872
  if (profiles.length === 0) return {
12823
12873
  ok: true,
12824
12874
  issues: [],
12825
- roleCount: 0,
12826
- note: `no skiff trajectories in this CCC (no trajectory has ${SKIFF_PROFILE_FILENAME} — zero impact)`
12875
+ constraintCount: 0,
12876
+ note: `no 轨迹约束 trajectories in this CCC (no trajectory has ${TRAJECTORY_CONSTRAINT_FILENAME} — zero impact)`
12827
12877
  };
12828
12878
  const registered = new Set(loadMsmEntries(root).map((e) => e.name));
12829
12879
  const hc = readCheapWorkerConfig(root);
@@ -12831,7 +12881,7 @@ function validateSkiffConfig(root) {
12831
12881
  for (const p of profiles) {
12832
12882
  if (!(hasTrajectoryPrompt(root, p.dirName) || (() => {
12833
12883
  try {
12834
- return resolveProfileSystemPrompt(root, p).trim() !== "";
12884
+ return resolveConstraintSystemPrompt(root, p).trim() !== "";
12835
12885
  } catch (err) {
12836
12886
  issues.push(`trajectory "${p.code}": ${String(err?.message ?? err)}`);
12837
12887
  return true;
@@ -12843,15 +12893,15 @@ function validateSkiffConfig(root) {
12843
12893
  return {
12844
12894
  ok: issues.length === 0,
12845
12895
  issues,
12846
- roleCount: profiles.length
12896
+ constraintCount: profiles.length
12847
12897
  };
12848
12898
  }
12849
- /** 列出当前 CCC 的 skiff 轨迹(编码 / 模型 / msms / tools / 纪律摘要 / 提示词来源) */
12850
- function listSkiffRoles(root) {
12851
- const profiles = listSkiffProfiles(root);
12899
+ /** 列出当前 CCC 的轨迹约束(编码 / 模型 / msms / tools / 纪律摘要 / 提示词来源) */
12900
+ function listTrajectoryConstraintSummary(root) {
12901
+ const profiles = listTrajectoryConstraints(root);
12852
12902
  if (profiles.length === 0) return {
12853
12903
  roles: [],
12854
- note: `no skiff trajectories in this CCC (no trajectory has ${SKIFF_PROFILE_FILENAME})`
12904
+ note: `no 轨迹约束 trajectories in this CCC (no trajectory has ${TRAJECTORY_CONSTRAINT_FILENAME})`
12855
12905
  };
12856
12906
  const items = profiles.map((p) => ({
12857
12907
  name: p.code,
@@ -12859,8 +12909,8 @@ function listSkiffRoles(root) {
12859
12909
  model: p.model ?? "(cheap-worker default)",
12860
12910
  msms: p.msms,
12861
12911
  tools: p.tools,
12862
- cognitiveTrajectory: profileCognitiveTrajectory(p),
12863
- promptSource: profileSystemPromptSource(p),
12912
+ cognitiveTrajectory: constraintCognitiveTrajectory(p),
12913
+ promptSource: constraintSystemPromptSource(p, hasTrajectoryPrompt(root, p.dirName)),
12864
12914
  ...p.systemPromptFile ? { systemPromptFile: p.systemPromptFile } : {}
12865
12915
  }));
12866
12916
  return {
@@ -12869,51 +12919,91 @@ function listSkiffRoles(root) {
12869
12919
  };
12870
12920
  }
12871
12921
  /**
12872
- * 应用当前 skiff 配置(S142 用户:改了 json 应该有个生效机制)。
12922
+ * 应用当前 轨迹约束 配置(S142 用户:改了 json 应该有个生效机制)。
12873
12923
  *
12874
12924
  * 语义:约束面是**每轮实时读取**的 ⇒ apply 不是"推送",而是**显式校验 + 应用确认**:
12875
12925
  * ① 校验合法(复用 validate:msms 注册 / model 白名单 / 提示词非空)
12876
12926
  * ② 非法 → 报告问题清单,不应用(提示修复后重跑 apply)
12877
12927
  * ③ 合法 → 应用确认:绑定 CCC + 轨迹清单 + 说明(下一轮即生效)
12878
12928
  */
12879
- function applySkiffConfig(root) {
12880
- const validation = validateSkiffConfig(root);
12881
- const profiles = listSkiffProfiles(root);
12929
+ function applyTrajectoryConstraint(root) {
12930
+ const validation = validateTrajectoryConstraint(root);
12931
+ const profiles = listTrajectoryConstraints(root);
12882
12932
  if (!validation.ok) return {
12883
12933
  applied: false,
12884
12934
  cccRoot: root,
12885
- roleCount: profiles.length,
12935
+ constraintCount: profiles.length,
12886
12936
  issues: validation.issues,
12887
- hint: "fix the issues above, then run container_admin role apply again"
12937
+ hint: "fix the issues above, then run container_admin config apply again"
12888
12938
  };
12889
12939
  return {
12890
12940
  applied: true,
12891
12941
  cccRoot: root,
12892
- roleCount: profiles.length,
12942
+ constraintCount: profiles.length,
12893
12943
  roles: profiles.map((p) => p.code),
12894
- note: `constraints are read live from each trajectory's ${SKIFF_PROFILE_FILENAME} — they take effect on the next turn (no restart); ${SKIFF_PROFILE_HINT}`
12944
+ note: `constraints are read live from each trajectory's ${TRAJECTORY_CONSTRAINT_FILENAME} — they take effect on the next turn (no restart); ${TRAJECTORY_CONSTRAINT_HINT}`
12895
12945
  };
12896
12946
  }
12897
12947
  //#endregion
12948
+ //#region src/tools/config-docs.ts
12949
+ /**
12950
+ * 渲染**目录**(第一层)。
12951
+ *
12952
+ * 🔴 只调 `summary`,**绝不调 `render`** —— 目录若触发正文,就退化成"整份长文"。
12953
+ * 这条不是性能优化,是**判据**:它保证 `docs` 的输出大小与章数成正比,与正文长度无关。
12954
+ */
12955
+ function renderDocsIndex(chapters) {
12956
+ const lines = [
12957
+ "═══ CCC 配置说明 · 目录 ═══",
12958
+ "",
12959
+ "说明类内容已超过单次返回的合适篇幅 ⇒ 先读目录,再按章取详情。",
12960
+ "",
12961
+ "用法:container_admin config docs <章节>",
12962
+ ""
12963
+ ];
12964
+ for (const ch of chapters) lines.push(` ${ch.id.padEnd(14)}${ch.summary}`);
12965
+ lines.push("");
12966
+ lines.push("相关:container_admin msm catalog(工具/能力目录——与本章录互指,不互相复制)");
12967
+ return lines.join("\n");
12968
+ }
12969
+ /**
12970
+ * 解析一个章节参数(第二层)。
12971
+ *
12972
+ * 不存在 ⇒ 返回 `null`,由调用方给出**带候选清单**的拒绝 ——
12973
+ * "不知道这个章节" 与 "这一章是空的" 必须可区分(否则用户以为功能没有)。
12974
+ */
12975
+ function findDocsChapter(chapters, id) {
12976
+ const want = id.trim().toLowerCase();
12977
+ if (want === "") return null;
12978
+ return chapters.find((c) => c.id.toLowerCase() === want) ?? null;
12979
+ }
12980
+ //#endregion
12898
12981
  //#region src/tools/container-admin.ts
12899
12982
  /**
12900
12983
  * container-admin.ts — container_admin(v1.30:机务舱——容器管理面统一入口)
12901
12984
  *
12902
- * 合并自:
12903
- * · skiff_admin(F4 Skiff 认知子集角色:guide/validate/apply/list)
12904
- * · acc_msm 管理面(register/deregister/check/guide/catalog/ccc-config)
12905
- * —— msm 工具只管执行+发现;一切「管理容器」的操作归本工具
12985
+ * ## 域结构(2026-10-19 owner 两条令后:**两域**)
12906
12986
  *
12907
- * 语义(用户拍板"skiff_admin 及系列 admin 合并成 container_admin"):
12987
+ * | 域 | 管什么 |
12988
+ * |---|---|
12989
+ * | `config` | **本 CCC 的全部配置面**:说明目录/按章详情 + 轨迹约束(原 `role` 域,已并入) |
12990
+ * | `msm` | MSM 注册表管理 + 手册(register/deregister/check/guide/catalog/ccc-config) |
12991
+ *
12992
+ * 🔴 **`role` 域已并入 `config`**(owner 2026-10-19 裁「1 并入」)。为什么不是三个平行域:
12993
+ * `role` 管的本来就是"某条轨迹的约束面"——**一组配置**,与 `config` 是同一抽象层的两半。
12994
+ * 分着放的代价是:用户在 `config` 里看不到"轨迹怎么约束",得先知道 `role` 这个词存在。
12995
+ * ⚠️ **旧 `role` 域仍被接受**(过渡别名,指向同一实现)——存量 CCC 的文档/脚本里到处是
12996
+ * `container_admin role validate`(实测本仓 30 处),硬切会让它们全部变成"未知域"。
12997
+ * 别名**只保留读取路径**:schema 的 `enum` 只列新名,文案只教新名。
12998
+ *
12999
+ * ## 三个动作名为什么是这三个(P3 的实现形态)
13000
+ * - `config docs`(无参)→ **使用说明目录**(第一层;只索引不复制)
13001
+ * - `config docs <chapter>` → **使用说明详情**(第二层;按章获得)
13002
+ * - `config view` → 配置**现值**(原来是唯一动作;现在是"看当前填了什么")
13003
+ *
13004
+ * 归属(D23):**机制在 ACC**(有哪几章、怎么路由、怎么校验),**内容在 CCC**(填什么)。
13005
+ * 语义(用户拍板"轨迹约束_admin 及系列 admin 合并成 container_admin"):
12908
13006
  * container = CCC(具体认知容器);admin = 管理操作。
12909
- * 类比 The Manifest(清单维护——工具注册表)+ Crew(角色管理)+ 机务(配置/手册)。
12910
- *
12911
- * 参数极简(LLM 直觉):domain(管理域)+ action(该域子命令)+ 少量业务参数。
12912
- *
12913
- * v1.34.1(⑥ C6a):由 `export const containerAdminTool` 改为**工厂**
12914
- * `createContainerAdminTool(ctx)`——原为 autopilot 域传进程内运行态事实(全局闸 / live 会话 /
12915
- * agent 可解析性)。该域已于 2026-09-15 随 ACC 侧 autopilot 整段退场删除(所有者裁决 (a)):
12916
- * `ctx` 现**无消费者**,工厂形态保留只是为了让装配点签名不再变动(见 index.ts 注册处)。
12917
13007
  */
12918
13008
  function agentSessionId(exec) {
12919
13009
  return exec.agent?.session?.id ?? "";
@@ -12924,9 +13014,13 @@ function renderText$1(value) {
12924
13014
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
12925
13015
  }];
12926
13016
  }
13017
+ /** 过渡别名表:旧域 → 新域(只读兼容;文案不教它) */
13018
+ const DOMAIN_ALIASES = { role: "config" };
12927
13019
  /** 合法 domain 名(拒绝文案与 schema enum 同源——不得各写一份) */
12928
13020
  const DOMAIN_NAMES = Object.keys({
12929
- role: [
13021
+ config: [
13022
+ "docs",
13023
+ "view",
12930
13024
  "guide",
12931
13025
  "validate",
12932
13026
  "apply",
@@ -12939,23 +13033,43 @@ const DOMAIN_NAMES = Object.keys({
12939
13033
  "guide",
12940
13034
  "catalog",
12941
13035
  "ccc-config"
12942
- ],
12943
- config: ["view"]
13036
+ ]
12944
13037
  });
13038
+ /**
13039
+ * 说明目录的章节(P3 两层菜单的**唯一注册处**)。
13040
+ *
13041
+ * 🔴 加一章 = 在这里加一项(`render` 惰性求值)。**正文不复制**:
13042
+ * 每章的真相源就在它自己的函数里(`CCC_CONFIG_REFERENCE` / `TRAJECTORY_CONSTRAINT_GUIDE`)——
13043
+ * 本表只做**切分与路由**,避免"同一段说明有第二份会漂的副本"。
13044
+ */
13045
+ const DOCS_CHAPTERS = [{
13046
+ id: "config",
13047
+ summary: "CCC 配置全表——9 段各是什么、怎么填、填错会怎样(.opencode/serenity.json)",
13048
+ render: () => CCC_CONFIG_REFERENCE
13049
+ }, {
13050
+ id: "trajectory-constraint",
13051
+ summary: "轨迹约束:一条轨迹怎么被约束(trajectory-constraint.json 的 schema + 双白名单)",
13052
+ render: () => TRAJECTORY_CONSTRAINT_GUIDE
13053
+ }];
12945
13054
  function createContainerAdminTool(ctx) {
12946
13055
  return defineTool({
12947
13056
  name: "container_admin",
12948
- description: "Container administration (the ship's maintenance bay — one entry for managing the CCC): role — Skiff cognitive-subset roles (guide/validate/apply/list); msm — MSM registry management (register/deregister/check quality DC-M1~M4) + manuals (guide dev manual / catalog ACC capability directory / ccc-config CCC configuration reference); config — view CCC configuration. Execution of registered MSMs belongs to the msm tool; this tool manages the container.",
13057
+ description: "Container administration (the ship's maintenance bay — one entry for managing the CCC): config — CCC configuration: docs (usage-manual index) / docs <chapter> (one chapter in full) / view (current values) + 轨迹约束 (guide/validate/apply/list); msm — MSM registry management (register/deregister/check quality DC-M1~M4) + manuals (guide dev manual / catalog ACC capability directory / ccc-config CCC configuration reference). Execution of registered MSMs belongs to the msm tool; this tool manages the container.",
12949
13058
  parameters: {
12950
13059
  domain: {
12951
13060
  type: "string",
12952
13061
  enum: DOMAIN_NAMES,
12953
13062
  required: true,
12954
- description: "Management domain: role (Skiff roles) / msm (MSM registry + manuals) / config (CCC config view)"
13063
+ description: "Management domain: config (CCC configuration + 轨迹约束) / msm (MSM registry + manuals)"
12955
13064
  },
12956
13065
  action: {
12957
13066
  type: "string",
12958
- description: "Domain subcommand (see domain descriptions; e.g. role: validate/apply/list/guide; msm: register/deregister/check/guide/catalog/ccc-config)"
13067
+ description: "Domain subcommand. config: docs / view / guide / validate / apply / list; msm: register / deregister / check / guide / catalog / ccc-config"
13068
+ },
13069
+ /** 第二层菜单参数:`config docs <chapter>`(缺省 = 目录) */
13070
+ chapter: {
13071
+ type: "string",
13072
+ description: "config docs only: chapter id (omit for the index; e.g. config / trajectory-constraint)"
12959
13073
  },
12960
13074
  name: {
12961
13075
  type: "string",
@@ -12994,18 +13108,35 @@ function createContainerAdminTool(ctx) {
12994
13108
  const root = cccRootForExec(exec);
12995
13109
  if (!root) throw new Error(NO_CCC_FROM_AGENT_CWD);
12996
13110
  const sessionId = agentSessionId(exec);
12997
- const domain = args.domain;
13111
+ const rawDomain = String(args.domain ?? "");
13112
+ const domain = DOMAIN_ALIASES[rawDomain] ?? rawDomain;
12998
13113
  const action = args.action ?? "";
13114
+ if (!DOMAIN_NAMES.includes(domain)) throw new Error(`container_admin requires domain: ${DOMAIN_NAMES.join(" | ")}`);
12999
13115
  if (domain === "msm" && (action === "register" || action === "deregister")) {
13000
- const gate = skiffMsmGate(root, sessionId, action, args.name);
13116
+ const gate = trajectoryConstraintMsmGate(root, sessionId, action, args.name);
13001
13117
  if (gate.reject) throw new Error(gate.reject);
13002
13118
  }
13003
- if (domain === "role") switch (action) {
13004
- case "guide": return { guide: SKIFF_GUIDE };
13005
- case "validate": return validateSkiffConfig(root);
13006
- case "apply": return applySkiffConfig(root);
13007
- case "list": return listSkiffRoles(root);
13008
- default: throw new Error(`container_admin role requires action: guide | validate | apply | list`);
13119
+ if (domain === "config") switch (action) {
13120
+ case "docs": {
13121
+ const chapter = args.chapter?.trim() ?? "";
13122
+ if (chapter === "") return {
13123
+ docs: renderDocsIndex(DOCS_CHAPTERS),
13124
+ chapters: DOCS_CHAPTERS.map((c) => c.id)
13125
+ };
13126
+ const found = findDocsChapter(DOCS_CHAPTERS, chapter);
13127
+ if (found === null) throw new Error(`container_admin config docs: unknown chapter "${chapter}". Available: ${DOCS_CHAPTERS.map((c) => c.id).join(" | ")} (omit the chapter for the index)`);
13128
+ return {
13129
+ chapter: found.id,
13130
+ docs: found.render()
13131
+ };
13132
+ }
13133
+ case "view":
13134
+ case "": return { config: runMsm(root, { action: "ccc-config" }) };
13135
+ case "guide": return { guide: TRAJECTORY_CONSTRAINT_GUIDE };
13136
+ case "validate": return validateTrajectoryConstraint(root);
13137
+ case "apply": return applyTrajectoryConstraint(root);
13138
+ case "list": return listTrajectoryConstraintSummary(root);
13139
+ default: throw new Error(`container_admin config requires action: docs | view | guide | validate | apply | list`);
13009
13140
  }
13010
13141
  if (domain === "msm") {
13011
13142
  const mgmtActions = [
@@ -13028,7 +13159,6 @@ function createContainerAdminTool(ctx) {
13028
13159
  usage: args.usage
13029
13160
  });
13030
13161
  }
13031
- if (domain === "config") return { config: runMsm(root, { action: "ccc-config" }) };
13032
13162
  throw new Error(`container_admin requires domain: ${DOMAIN_NAMES.join(" | ")}`);
13033
13163
  }
13034
13164
  });
@@ -13280,7 +13410,7 @@ const SENSITIVE_CREDENTIAL_FILES = /* @__PURE__ */ new Set(["localstore.json"]);
13280
13410
  * MSM 注册表写保护(需求⑤b S142 用户拍板:"msm注册的文件需要写保护起来,避免CCC意外写坏搞崩自己"):
13281
13411
  * mech-registry.json 是 CCC 执行层地基(坏 → loadMsmEntries 抛 → container_admin/output-guard 全崩,
13282
13412
  * register 也崩 → 自锁)。**写 deny、读 allow**(与 localstore 读 deny 语义区分 R6——注册表是
13283
- * 结构核心不是秘密,需被 output-guard/skiff-admin 读取建 MSM 词表)。
13413
+ * 结构核心不是秘密,需被 output-guard/trajectory-constraint-admin 读取建 MSM 词表)。
13284
13414
  *
13285
13415
  * 单级化(需求⑤a)后**唯一合法注册表 = cccName 聚合档**
13286
13416
  * `.opencode/skills/<cccName>/references/mech-registry.json`(cccName = .serenity 首行)。
@@ -13318,15 +13448,15 @@ function isWriteTool(toolName, action) {
13318
13448
  */
13319
13449
  function decideGuard(input) {
13320
13450
  const { root, toolName, safeModeOn, blacklist, pathArg, action } = input;
13321
- if (input.skiffProfile !== void 0 && input.skiffProfile !== null) {
13322
- const profile = input.skiffProfile;
13451
+ if (input.trajectoryConstraint !== void 0 && input.trajectoryConstraint !== null) {
13452
+ const profile = input.trajectoryConstraint;
13323
13453
  if (toolName === "skill") return { kind: "allow" };
13324
13454
  if (profile.msms.includes(toolName)) return {
13325
13455
  deny: `"${toolName}" is a registered MSM, not a direct tool in this trajectory — call it through the msm tool: msm("${toolName}", ["<args>"]) (pass "--help" or inspect=true for usage)`,
13326
13456
  kind: "deny"
13327
13457
  };
13328
13458
  if (!profileToolWhitelist(profile).has(toolName)) return {
13329
- deny: "tool not allowed in this skiff trajectory",
13459
+ deny: "tool not allowed in this 轨迹约束 trajectory",
13330
13460
  kind: "deny"
13331
13461
  };
13332
13462
  }
@@ -13611,17 +13741,17 @@ const exclusiveRestrictions = /* @__PURE__ */ new Map();
13611
13741
  * 每次 pre-step 都重算白名单,只有**算出来的结果变了**才重新发 restrict
13612
13742
  * —— 配置不变就不重复发(restrict 会重建 schema,没必要每步都做)。
13613
13743
  */
13614
- const skiffRoleRestrictions = /* @__PURE__ */ new Map();
13744
+ const trajectoryConstraintRestrictions = /* @__PURE__ */ new Map();
13615
13745
  /**
13616
- * 同步 **skiff 会话的工具面**:把白名单之外的工具**从 schema 移除**(不是"看得到但被拒")。
13746
+ * 同步 **轨迹约束会话的工具面**:把白名单之外的工具**从 schema 移除**(不是"看得到但被拒")。
13617
13747
  *
13618
13748
  * ## 为什么必须有这条(owner 2026-10-19:「工具清单也要热加载」)
13619
13749
  *
13620
- * 在它之前,skiff 白名单**只由运行时守卫拦截**(`decideGuard` 的 skiff 分支):
13750
+ * 在它之前,轨迹约束白名单**只由运行时守卫拦截**(`decideGuard` 的 轨迹约束 分支):
13621
13751
  * 工具全部出现在模型 schema 里,调用时才被打回。⇒ 两个真实后果:
13622
13752
  * ① **模型看得见它不该有的工具** —— 它会去试,试了才撞墙(一轮白烧,且错误文案泛化,
13623
13753
  * 看不出"是不在清单里"还是"临时性拒绝");
13624
- * ② **配置变动在 schema 上永远不生效** —— 改 `skiff.json` 的 `tools` 只影响"调用是否放行"。
13754
+ * ② **配置变动在 schema 上永远不生效** —— 改 `trajectory-constraint.json` 的 `tools` 只影响"调用是否放行"。
13625
13755
  *
13626
13756
  * ## 判据与降级
13627
13757
  *
@@ -13629,24 +13759,24 @@ const skiffRoleRestrictions = /* @__PURE__ */ new Map();
13629
13759
  * 不去动宿主的工具 —— 宿主的工具由各自的条件同步管)。
13630
13760
  * - ⚠️ **未注册的名字会让整次 restrict 失败**(宿主契约逐字 "unknown names … fail"),
13631
13761
  * 而 `diagram` 是按开关注册的 ⇒ 与 `hideAllFor` 同款**两档尝试**(先全覆盖,失败退常驻段)。
13632
- * - 读不到约束面(不是 skiff / 配置删了)⇒ **撤销既有隐藏**(回到"无约束"):
13633
- * ⛔ 不能只是 `return` —— 那会把上一步的隐藏**永久留在**一个已不再是 skiff 的会话上
13634
- * (这正是 owner 说的"删掉 `skiff.json` 就该立刻恢复")。
13762
+ * - 读不到约束面(不是 轨迹约束 / 配置删了)⇒ **撤销既有隐藏**(回到"无约束"):
13763
+ * ⛔ 不能只是 `return` —— 那会把上一步的隐藏**永久留在**一个已不再是 轨迹约束的会话上
13764
+ * (这正是 owner 说的"删掉 `trajectory-constraint.json` 就该立刻恢复")。
13635
13765
  * - 幂等:算出来的 deny 签名不变就跳过(不每步重建 schema)。
13636
13766
  *
13637
13767
  * @param agent 目标会话
13638
13768
  * @param root CCC 根
13639
13769
  */
13640
- function syncSkiffRoleToolVisibility(agent, root) {
13770
+ function syncTrajectoryConstraintToolVisibility(agent, root) {
13641
13771
  const sessionId = agent.session.id ?? "global";
13642
- const profile = skiffProfileForSession(root, sessionId);
13643
- const existing = skiffRoleRestrictions.get(sessionId);
13772
+ const profile = trajectoryConstraintForSession(root, sessionId);
13773
+ const existing = trajectoryConstraintRestrictions.get(sessionId);
13644
13774
  if (profile === null) {
13645
13775
  if (existing !== void 0) {
13646
13776
  try {
13647
13777
  existing.dispose();
13648
13778
  } catch {}
13649
- skiffRoleRestrictions.delete(sessionId);
13779
+ trajectoryConstraintRestrictions.delete(sessionId);
13650
13780
  }
13651
13781
  return;
13652
13782
  }
@@ -13662,7 +13792,7 @@ function syncSkiffRoleToolVisibility(agent, root) {
13662
13792
  try {
13663
13793
  existing.dispose();
13664
13794
  } catch {}
13665
- skiffRoleRestrictions.delete(sessionId);
13795
+ trajectoryConstraintRestrictions.delete(sessionId);
13666
13796
  }
13667
13797
  return;
13668
13798
  }
@@ -13671,10 +13801,10 @@ function syncSkiffRoleToolVisibility(agent, root) {
13671
13801
  try {
13672
13802
  existing.dispose();
13673
13803
  } catch {}
13674
- skiffRoleRestrictions.delete(sessionId);
13804
+ trajectoryConstraintRestrictions.delete(sessionId);
13675
13805
  }
13676
13806
  const dispose = agent.ctx.tools.restrict({ deny: [...deny] });
13677
- skiffRoleRestrictions.set(sessionId, {
13807
+ trajectoryConstraintRestrictions.set(sessionId, {
13678
13808
  signature,
13679
13809
  deny,
13680
13810
  dispose
@@ -13686,13 +13816,13 @@ function syncSkiffRoleToolVisibility(agent, root) {
13686
13816
  }
13687
13817
  }
13688
13818
  /** 会话销毁清理(lifecycle 调用) */
13689
- function forgetSkiffRoleToolVisibility(sessionId) {
13690
- const existing = skiffRoleRestrictions.get(sessionId);
13819
+ function forgetTrajectoryConstraintToolVisibility(sessionId) {
13820
+ const existing = trajectoryConstraintRestrictions.get(sessionId);
13691
13821
  if (!existing) return;
13692
13822
  try {
13693
13823
  existing.dispose();
13694
13824
  } catch {}
13695
- skiffRoleRestrictions.delete(sessionId);
13825
+ trajectoryConstraintRestrictions.delete(sessionId);
13696
13826
  }
13697
13827
  /**
13698
13828
  * 同步专属工具可见性(v1.33)。
@@ -13887,7 +14017,7 @@ function registerGuards(ctx, opts = {}) {
13887
14017
  const pathArg = extractPathArg(exec);
13888
14018
  const action = extractAction(exec);
13889
14019
  const sessionId = exec?.agent?.session?.id;
13890
- const skiffProfile = skiffProfileForSession(root, sessionId);
14020
+ const trajectoryConstraint = trajectoryConstraintForSession(root, sessionId);
13891
14021
  return decideGuard({
13892
14022
  root,
13893
14023
  toolName: exec.name,
@@ -13895,7 +14025,7 @@ function registerGuards(ctx, opts = {}) {
13895
14025
  blacklist,
13896
14026
  pathArg,
13897
14027
  action,
13898
- skiffProfile
14028
+ trajectoryConstraint
13899
14029
  });
13900
14030
  };
13901
14031
  ctx.on("tools/pre-execute", async (exec, next) => {
@@ -14132,7 +14262,7 @@ function createEpochPromotion(promoteEvents, requiredSignals = 1, maxRoundsFallb
14132
14262
  const loopSid = session.id;
14133
14263
  const loopCwd = session.header?.cwd ?? process.cwd();
14134
14264
  const loopRoot = cccRootForCwd(loopCwd);
14135
- if (typeof loopSid === "string" && (loopSid.startsWith("cheap-worker-") || loopRoot !== null && isSkiffSession(loopRoot, loopSid))) return {
14265
+ if (typeof loopSid === "string" && (loopSid.startsWith("cheap-worker-") || loopRoot !== null && isTrajectoryConstrained(loopRoot, loopSid))) return {
14136
14266
  boundary: -1,
14137
14267
  promoted: true
14138
14268
  };
@@ -14251,7 +14381,7 @@ function registerBootstrap(ctx) {
14251
14381
  const depth = session?.header?.delegationDepth ?? 0;
14252
14382
  const sid = typeof session?.id === "string" ? session.id : void 0;
14253
14383
  if (sid === void 0) return;
14254
- if (sid.startsWith("cheap-worker-") || isSkiffSession(root, sid)) return;
14384
+ if (sid.startsWith("cheap-worker-") || isTrajectoryConstrained(root, sid)) return;
14255
14385
  if (depth === 0) {
14256
14386
  if (hasUserMessageHistory(session)) return;
14257
14387
  } else if (anchoredSessions.has(sid)) return;
@@ -14565,7 +14695,7 @@ function forgetLogbookCompactionState(scope) {
14565
14695
  * 解析**活跃** SESSION.md 绝对路径(缓存 60s)。
14566
14696
  *
14567
14697
  * 候选链(刻意比 rebuild 的完整链短——这里每个工具调用都要跑,贵候选不划算):
14568
- * ① `getActiveSessionInfo(scope).mdPath`(`cognitive_trajectory use` / skiff 绑定 / 重启恢复都会写)
14698
+ * ① `getActiveSessionInfo(scope).mdPath`(`cognitive_trajectory use` / 轨迹约束 绑定 / 重启恢复都会写)
14569
14699
  * ② `readLastBound(session).mdPath`(`.bindings.json` 权威绑定——重启后内存空时兜底)
14570
14700
  * 两者都拿不到 → null(**不猜**:宁可不提醒,也不去猜别的轨迹——对齐 v1.30.13 D4 的教训)。
14571
14701
  */
@@ -14694,10 +14824,10 @@ function registerKeeper(ctx, opts = {}) {
14694
14824
  const root = cccRootForCwd(cwd);
14695
14825
  if (!root) return next();
14696
14826
  const sessionId = exec.agent?.session?.id;
14697
- const skiffKeeper = skiffCognitiveTrajectoryEnabled(root, sessionId, "keeper");
14698
- const skiffRebuild = skiffCognitiveTrajectoryEnabled(root, sessionId, "rebuild");
14827
+ const 轨迹约束Keeper = trajectoryConstraintParticipates(root, sessionId, "keeper");
14828
+ const 轨迹约束Rebuild = trajectoryConstraintParticipates(root, sessionId, "rebuild");
14699
14829
  const tracker = trackerFor(exec);
14700
- const shouldRemind = skiffKeeper ? tracker.step(exec.name) : false;
14830
+ const shouldRemind = 轨迹约束Keeper ? tracker.step(exec.name) : false;
14701
14831
  const downstream = await next();
14702
14832
  try {
14703
14833
  recordToolUsage(root, exec.name, exec.arguments);
@@ -14710,7 +14840,7 @@ function registerKeeper(ctx, opts = {}) {
14710
14840
  text: reminderText(code)
14711
14841
  });
14712
14842
  }
14713
- if (skiffRebuild) {
14843
+ if (轨迹约束Rebuild) {
14714
14844
  const session = exec.agent?.session;
14715
14845
  if (session) {
14716
14846
  const pressure = readContextPressure(ctx, session);
@@ -14730,7 +14860,7 @@ function registerKeeper(ctx, opts = {}) {
14730
14860
  }
14731
14861
  }
14732
14862
  }
14733
- if (skiffCognitiveTrajectoryEnabled(root, sessionId, "session")) {
14863
+ if (trajectoryConstraintParticipates(root, sessionId, "session")) {
14734
14864
  const limitKB = readSessionMdMaxKB(root, opts.configPaths);
14735
14865
  if (limitKB > 0) {
14736
14866
  const scope = sessionId ?? "global";
@@ -14753,7 +14883,7 @@ function registerKeeper(ctx, opts = {}) {
14753
14883
  } else compactionStates.delete(scope);
14754
14884
  }
14755
14885
  }
14756
- if (skiffCognitiveTrajectoryEnabled(root, sessionId, "session")) {
14886
+ if (trajectoryConstraintParticipates(root, sessionId, "session")) {
14757
14887
  const scope = sessionId ?? "global";
14758
14888
  const session = exec.agent?.session;
14759
14889
  const mdPath = resolveActiveSessionMdPath(root, scope, session);
@@ -14861,7 +14991,7 @@ function shouldAutoRestore(agent, root) {
14861
14991
  if (session.header?.parentSession) return false;
14862
14992
  if (session.id?.startsWith("cheap-worker-")) return false;
14863
14993
  const cccRoot = root !== void 0 ? root : cccRootForCwd(session.header?.cwd ?? process.cwd());
14864
- if (cccRoot && isSkiffSession(cccRoot, session.id)) return false;
14994
+ if (cccRoot && isTrajectoryConstrained(cccRoot, session.id)) return false;
14865
14995
  return true;
14866
14996
  }
14867
14997
  /**
@@ -14884,7 +15014,7 @@ function registerContext(ctx, opts = {}) {
14884
15014
  } catch {}
14885
15015
  if (!root) return;
14886
15016
  const key = agentKey(agent);
14887
- if (isSkiffSession(root, key)) return;
15017
+ if (isTrajectoryConstrained(root, key)) return;
14888
15018
  registerEntrySkillSection(agent, root);
14889
15019
  registerCognitiveTrajectorySkillSection(agent, root);
14890
15020
  const scope = agentScope(agent);
@@ -14968,9 +15098,9 @@ function registerContext(ctx, opts = {}) {
14968
15098
  const key = agentKey(agent);
14969
15099
  const downstream = await next();
14970
15100
  if (root) try {
14971
- syncSkiffRoleToolVisibility(agent, root);
15101
+ syncTrajectoryConstraintToolVisibility(agent, root);
14972
15102
  } catch {}
14973
- if (root && isSkiffSession(root, key)) return downstream;
15103
+ if (root && isTrajectoryConstrained(root, key)) return downstream;
14974
15104
  try {
14975
15105
  syncCccToolVisibility(agent, root);
14976
15106
  } catch {}
@@ -15019,7 +15149,7 @@ function registerCompactRetention(ctx, opts = {}) {
15019
15149
  const cwd = agent.session?.header?.cwd ?? process.cwd();
15020
15150
  const root = cccRootForCwd(cwd);
15021
15151
  if (!root) return;
15022
- if (isSkiffSession(root, String(session.id ?? ""))) return;
15152
+ if (isTrajectoryConstrained(root, String(session.id ?? ""))) return;
15023
15153
  try {
15024
15154
  agent.inject(accMessage(root, configPaths, entrySkillMaxChars));
15025
15155
  } catch {}
@@ -15312,7 +15442,7 @@ function saveFileToTmp(root, fileName, data) {
15312
15442
  writeFileSync(join(dir, filename), bytes);
15313
15443
  return `${FILE_UPLOAD_DIR}/${filename}`;
15314
15444
  }
15315
- function readBody$3(req, maxBytes) {
15445
+ function readBody$2(req, maxBytes) {
15316
15446
  return new Promise((resolve, reject) => {
15317
15447
  let data = "";
15318
15448
  req.on("data", (chunk) => {
@@ -15326,7 +15456,7 @@ function readBody$3(req, maxBytes) {
15326
15456
  req.on("error", reject);
15327
15457
  });
15328
15458
  }
15329
- function sendJson$3(res, code, body) {
15459
+ function sendJson$2(res, code, body) {
15330
15460
  const payload = JSON.stringify(body);
15331
15461
  res.writeHead(code, {
15332
15462
  "content-type": "application/json; charset=utf-8",
@@ -15351,7 +15481,7 @@ function senderIsWebUi(req) {
15351
15481
  */
15352
15482
  function requireWebUi(req, res, message) {
15353
15483
  if (senderIsWebUi(req)) return true;
15354
- sendJson$3(res, 403, { error: message });
15484
+ sendJson$2(res, 403, { error: message });
15355
15485
  return false;
15356
15486
  }
15357
15487
  /**
@@ -15360,7 +15490,7 @@ function requireWebUi(req, res, message) {
15360
15490
  *
15361
15491
  * 🔴🔴 **2026-10-18 修缺陷(owner 报「在别的 CCC 点开显示的还是 home-serenity 的信息」)**:
15362
15492
  * 旧实现第 ③ 步是「**遍历 live sessions 找第一个 CCC 会话的 cwd**」—— 那是**跨工作区猜测**:
15363
- * 只要 sessionId 缺失或那条会话查不到(ACP/skiff 会话、尚未 materialize 的会话、远程访问),
15493
+ * 只要 sessionId 缺失或那条会话查不到(ACP/轨迹约束会话、尚未 materialize 的会话、远程访问),
15364
15494
  * 它就会**静默返回另一个 CCC 的路径**,于是面板把**别人的轨迹/技能/安全模式**当成你的显示出来。
15365
15495
  * 这比"显示未激活"危险得多 —— 用户会据此做判断(以为自己的 safe-mode 开着、以为那是自己的轨迹)。
15366
15496
  *
@@ -15409,11 +15539,11 @@ function registerStatusApi(ctx, opts = {}) {
15409
15539
  handler: async (req, res) => {
15410
15540
  try {
15411
15541
  if (req.method !== "POST") {
15412
- sendJson$3(res, 405, { error: "method not allowed" });
15542
+ sendJson$2(res, 405, { error: "method not allowed" });
15413
15543
  return;
15414
15544
  }
15415
15545
  if (!requireWebUi(req, res, "图片落盘仅限 WebUI(client 专用)")) return;
15416
- const raw = await readBody$3(req, 20971520);
15546
+ const raw = await readBody$2(req, 20971520);
15417
15547
  const body = JSON.parse(raw);
15418
15548
  const workspace = resolveWorkspace(ctx, {
15419
15549
  sessionId: body.sessionId,
@@ -15421,12 +15551,12 @@ function registerStatusApi(ctx, opts = {}) {
15421
15551
  });
15422
15552
  const root = cccRootForCwd(workspace);
15423
15553
  if (!root) {
15424
- sendJson$3(res, 404, { error: `no CCC found from workspace: ${workspace}` });
15554
+ sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
15425
15555
  return;
15426
15556
  }
15427
- sendJson$3(res, 200, { path: saveImageToTmp(root, body.mediaType ?? "", body.data ?? "") });
15557
+ sendJson$2(res, 200, { path: saveImageToTmp(root, body.mediaType ?? "", body.data ?? "") });
15428
15558
  } catch (err) {
15429
- sendJson$3(res, 400, { error: err.message ?? String(err) });
15559
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
15430
15560
  }
15431
15561
  }
15432
15562
  });
@@ -15436,11 +15566,11 @@ function registerStatusApi(ctx, opts = {}) {
15436
15566
  handler: async (req, res) => {
15437
15567
  try {
15438
15568
  if (req.method !== "POST") {
15439
- sendJson$3(res, 405, { error: "method not allowed" });
15569
+ sendJson$2(res, 405, { error: "method not allowed" });
15440
15570
  return;
15441
15571
  }
15442
15572
  if (!requireWebUi(req, res, "文件落盘仅限 WebUI(client 专用)")) return;
15443
- const raw = await readBody$3(req, 20971520);
15573
+ const raw = await readBody$2(req, 20971520);
15444
15574
  const body = JSON.parse(raw);
15445
15575
  const workspace = resolveWorkspace(ctx, {
15446
15576
  sessionId: body.sessionId,
@@ -15448,12 +15578,12 @@ function registerStatusApi(ctx, opts = {}) {
15448
15578
  });
15449
15579
  const root = cccRootForCwd(workspace);
15450
15580
  if (!root) {
15451
- sendJson$3(res, 404, { error: `no CCC found from workspace: ${workspace}` });
15581
+ sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
15452
15582
  return;
15453
15583
  }
15454
- sendJson$3(res, 200, { path: saveFileToTmp(root, body.name ?? "", body.data ?? "") });
15584
+ sendJson$2(res, 200, { path: saveFileToTmp(root, body.name ?? "", body.data ?? "") });
15455
15585
  } catch (err) {
15456
- sendJson$3(res, 400, { error: err.message ?? String(err) });
15586
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
15457
15587
  }
15458
15588
  }
15459
15589
  });
@@ -15469,7 +15599,7 @@ function registerStatusApi(ctx, opts = {}) {
15469
15599
  workspace: url.searchParams.get("workspace") ?? void 0
15470
15600
  }), configPaths);
15471
15601
  const runtime = ctx.get("codeRuntime");
15472
- sendJson$3(res, 200, {
15602
+ sendJson$2(res, 200, {
15473
15603
  ...status,
15474
15604
  ...runtime === void 0 ? { codeRuntime: null } : { codeRuntime: {
15475
15605
  language: runtime.language,
@@ -15480,7 +15610,7 @@ function registerStatusApi(ctx, opts = {}) {
15480
15610
  }
15481
15611
  if (req.method === "POST") {
15482
15612
  if (!requireWebUi(req, res, "safe-mode 切换仅限 WebUI(agent 不可自行开关)")) return;
15483
- const raw = await readBody$3(req, 65536);
15613
+ const raw = await readBody$2(req, 65536);
15484
15614
  const body = JSON.parse(raw);
15485
15615
  const workspace = resolveWorkspace(ctx, {
15486
15616
  sessionId: body.sessionId,
@@ -15488,19 +15618,19 @@ function registerStatusApi(ctx, opts = {}) {
15488
15618
  });
15489
15619
  const root = cccRootForCwd(workspace);
15490
15620
  if (!root) {
15491
- sendJson$3(res, 404, { error: `no CCC found from workspace: ${workspace}` });
15621
+ sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
15492
15622
  return;
15493
15623
  }
15494
15624
  const result = setSafeMode(root, body.on === true);
15495
- sendJson$3(res, 200, {
15625
+ sendJson$2(res, 200, {
15496
15626
  ...getStatus(workspace, configPaths),
15497
15627
  ...result
15498
15628
  });
15499
15629
  return;
15500
15630
  }
15501
- sendJson$3(res, 405, { error: "method not allowed" });
15631
+ sendJson$2(res, 405, { error: "method not allowed" });
15502
15632
  } catch (err) {
15503
- sendJson$3(res, 400, { error: err.message ?? String(err) });
15633
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
15504
15634
  }
15505
15635
  }
15506
15636
  });
@@ -15516,24 +15646,24 @@ function registerStatusApi(ctx, opts = {}) {
15516
15646
  try {
15517
15647
  known = projectKnownWorkspaces(hostService(ctx, "workspaceRegistry")?.list?.() ?? [], settings.gateway.workspaces);
15518
15648
  } catch {}
15519
- sendJson$3(res, 200, {
15649
+ sendJson$2(res, 200, {
15520
15650
  config: toWire(settings),
15521
15651
  knownWorkspaces: known
15522
15652
  });
15523
15653
  return;
15524
15654
  }
15525
15655
  if (req.method === "PUT") {
15526
- const raw = await readBody$3(req, 131072);
15656
+ const raw = await readBody$2(req, 131072);
15527
15657
  const saved = applyWirePatch(JSON.parse(raw).config ?? {});
15528
15658
  try {
15529
15659
  ctx.emit?.("serenity/config-updated");
15530
15660
  } catch {}
15531
- sendJson$3(res, 200, { config: toWire(saved) });
15661
+ sendJson$2(res, 200, { config: toWire(saved) });
15532
15662
  return;
15533
15663
  }
15534
- sendJson$3(res, 405, { error: "method not allowed" });
15664
+ sendJson$2(res, 405, { error: "method not allowed" });
15535
15665
  } catch (err) {
15536
- sendJson$3(res, 400, { error: err.message ?? String(err) });
15666
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
15537
15667
  }
15538
15668
  }
15539
15669
  });
@@ -15543,7 +15673,7 @@ function registerStatusApi(ctx, opts = {}) {
15543
15673
  handler: async (req, res) => {
15544
15674
  try {
15545
15675
  if (req.method !== "GET") {
15546
- sendJson$3(res, 405, { error: "method not allowed" });
15676
+ sendJson$2(res, 405, { error: "method not allowed" });
15547
15677
  return;
15548
15678
  }
15549
15679
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
@@ -15552,12 +15682,12 @@ function registerStatusApi(ctx, opts = {}) {
15552
15682
  workspace: url.searchParams.get("workspace") ?? void 0
15553
15683
  });
15554
15684
  const root = cccRootForCwd(workspace) ?? "";
15555
- sendJson$3(res, 200, { cccs: await listCccs(ctx, {
15685
+ sendJson$2(res, 200, { cccs: await listCccs(ctx, {
15556
15686
  defaultRoot: root,
15557
15687
  withRoles: true
15558
15688
  }) });
15559
15689
  } catch (err) {
15560
- sendJson$3(res, 400, { error: err.message ?? String(err) });
15690
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
15561
15691
  }
15562
15692
  }
15563
15693
  });
@@ -15571,21 +15701,26 @@ function registerStatusApi(ctx, opts = {}) {
15571
15701
  if (req.method === "GET") {
15572
15702
  const chk = requireCcc(url.searchParams.get("ccc") ?? void 0);
15573
15703
  if (!chk.ok) {
15574
- sendJson$3(res, 400, { error: chk.error });
15704
+ sendJson$2(res, 400, { error: chk.error });
15575
15705
  return;
15576
15706
  }
15577
- const { readWeixinSettings } = await import("./route-f7JlinIt.js").then((n) => n.s);
15707
+ const { readWeixinSettings } = await import("./route-COOFLTpU.js").then((n) => n.c);
15578
15708
  const { weixinBridgeStatus } = await Promise.resolve().then(() => bridge_exports);
15579
15709
  const settings = readWeixinSettings(chk.root);
15580
15710
  const bridge = weixinBridgeStatus().find((b) => b.ccc === chk.root);
15581
- const { readWeixinCredential } = await import("./route-f7JlinIt.js").then((n) => n.s);
15582
- const accounts = (settings.accounts ?? []).map((a) => ({
15583
- accountId: a.accountId,
15584
- name: a.name ?? void 0,
15585
- enabled: a.enabled !== false,
15586
- bound: readWeixinCredential(chk.root, a.accountId) !== null
15587
- }));
15588
- sendJson$3(res, 200, {
15711
+ const { imCredentialBound } = await import("./route-COOFLTpU.js").then((n) => n.c);
15712
+ const { normalizeAccountChannel } = await import("./accounts-BHQbakUh.js").then((n) => n.t);
15713
+ const accounts = (settings.accounts ?? []).map((a) => {
15714
+ const channel = normalizeAccountChannel(a.channel);
15715
+ return {
15716
+ accountId: a.accountId,
15717
+ name: a.name ?? void 0,
15718
+ enabled: a.enabled !== false,
15719
+ channel,
15720
+ bound: imCredentialBound(chk.root, a.accountId, channel)
15721
+ };
15722
+ });
15723
+ sendJson$2(res, 200, {
15589
15724
  enabled: settings.enabled === true,
15590
15725
  botType: settings.botType ?? void 0,
15591
15726
  accounts,
@@ -15595,22 +15730,22 @@ function registerStatusApi(ctx, opts = {}) {
15595
15730
  return;
15596
15731
  }
15597
15732
  if (req.method !== "POST") {
15598
- sendJson$3(res, 405, { error: "method not allowed" });
15733
+ sendJson$2(res, 405, { error: "method not allowed" });
15599
15734
  return;
15600
15735
  }
15601
- const raw = await readBody$3(req, 131072);
15736
+ const raw = await readBody$2(req, 131072);
15602
15737
  const body = JSON.parse(raw);
15603
15738
  const chk = requireCcc(body.ccc);
15604
15739
  if (!chk.ok) {
15605
- sendJson$3(res, 400, { error: chk.error });
15740
+ sendJson$2(res, 400, { error: chk.error });
15606
15741
  return;
15607
15742
  }
15608
15743
  const root = chk.root;
15609
15744
  if (body.action === "login-start") {
15610
- const { fetchQRCode } = await import("./ilink-DEpmE3yi.js").then((n) => n.a);
15745
+ const { fetchQRCode } = await import("./ilink-BMk2w2KY.js").then((n) => n.a);
15611
15746
  const { randomUUID } = await import("node:crypto");
15612
15747
  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);
15748
+ const { readWeixinSettings } = await import("./route-COOFLTpU.js").then((n) => n.c);
15614
15749
  const qr = await fetchQRCode({ botType: readWeixinSettings(root).botType ?? void 0 });
15615
15750
  const loginKey = randomUUID();
15616
15751
  weixinLogins.set(loginKey, {
@@ -15619,7 +15754,7 @@ function registerStatusApi(ctx, opts = {}) {
15619
15754
  startedAt: Date.now(),
15620
15755
  polling: false
15621
15756
  });
15622
- sendJson$3(res, 200, {
15757
+ sendJson$2(res, 200, {
15623
15758
  qrcode: qr.qrcode,
15624
15759
  qrcode_img_content: qr.qrcode_img_content,
15625
15760
  loginKey
@@ -15628,20 +15763,20 @@ function registerStatusApi(ctx, opts = {}) {
15628
15763
  }
15629
15764
  if (body.action === "remove-account") {
15630
15765
  if (!body.accountId) {
15631
- sendJson$3(res, 400, { error: "missing accountId" });
15766
+ sendJson$2(res, 400, { error: "missing accountId" });
15632
15767
  return;
15633
15768
  }
15634
- const { removeWeixinAccount } = await import("./route-f7JlinIt.js").then((n) => n.s);
15769
+ const { removeWeixinAccount } = await import("./route-COOFLTpU.js").then((n) => n.c);
15635
15770
  const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
15636
15771
  removeWeixinAccount(root, body.accountId);
15637
15772
  syncCccBridge(ctx, root);
15638
- sendJson$3(res, 200, { removed: body.accountId });
15773
+ sendJson$2(res, 200, { removed: body.accountId });
15639
15774
  return;
15640
15775
  }
15641
15776
  if (body.action === "save-routes") {
15642
15777
  const raw = body.routes ?? [];
15643
15778
  if (!Array.isArray(raw)) {
15644
- sendJson$3(res, 400, { error: "invalid routes (expected [{user, trajectory}, ...])" });
15779
+ sendJson$2(res, 400, { error: "invalid routes (expected [{user, trajectory}, ...])" });
15645
15780
  return;
15646
15781
  }
15647
15782
  const routes = [];
@@ -15649,7 +15784,7 @@ function registerStatusApi(ctx, opts = {}) {
15649
15784
  const user = typeof r?.user === "string" ? r.user : "";
15650
15785
  const code = typeof r?.trajectory === "string" && r.trajectory !== "" ? r.trajectory : typeof r?.role === "string" ? r.role : "";
15651
15786
  if (user === "" || code === "") {
15652
- sendJson$3(res, 400, { error: "invalid routes (each row needs non-empty user and trajectory)" });
15787
+ sendJson$2(res, 400, { error: "invalid routes (each row needs non-empty user and trajectory)" });
15653
15788
  return;
15654
15789
  }
15655
15790
  routes.push({
@@ -15657,23 +15792,23 @@ function registerStatusApi(ctx, opts = {}) {
15657
15792
  trajectory: code
15658
15793
  });
15659
15794
  }
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);
15795
+ const { saveWeixinRoutes } = await import("./route-COOFLTpU.js").then((n) => n.c);
15796
+ const { findDirNameByCode } = await import("./profile-BVt84mbi.js").then((n) => n.i);
15662
15797
  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})` });
15798
+ sendJson$2(res, 400, { error: `unknown trajectory: ${r.trajectory} (no such trajectory code in ${root})` });
15664
15799
  return;
15665
15800
  }
15666
15801
  saveWeixinRoutes(root, routes);
15667
- sendJson$3(res, 200, { saved: routes.length });
15802
+ sendJson$2(res, 200, { saved: routes.length });
15668
15803
  return;
15669
15804
  }
15670
15805
  if (body.action === "set-enabled") {
15671
15806
  const enabled = body.enabled === true;
15672
- const { readWeixinSettings, setWeixinEnabled } = await import("./route-f7JlinIt.js").then((n) => n.s);
15807
+ const { readWeixinSettings, setWeixinEnabled } = await import("./route-COOFLTpU.js").then((n) => n.c);
15673
15808
  const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
15674
15809
  const settings = readWeixinSettings(root);
15675
15810
  if (enabled && (settings.accounts ?? []).length === 0) {
15676
- sendJson$3(res, 400, { error: "启用前请先扫码绑定至少一个账号" });
15811
+ sendJson$2(res, 400, { error: "启用前请先扫码绑定至少一个账号" });
15677
15812
  return;
15678
15813
  }
15679
15814
  setWeixinEnabled(root, enabled);
@@ -15682,12 +15817,41 @@ function registerStatusApi(ctx, opts = {}) {
15682
15817
  const { stopCccBridge } = await Promise.resolve().then(() => bridge_exports);
15683
15818
  stopCccBridge(root);
15684
15819
  }
15685
- sendJson$3(res, 200, { enabled });
15820
+ sendJson$2(res, 200, { enabled });
15686
15821
  return;
15687
15822
  }
15688
- sendJson$3(res, 400, { error: `unsupported action: ${body.action ?? ""}` });
15823
+ if (body.action === "add-feishu-account") {
15824
+ const appId = (body.appId ?? "").trim();
15825
+ const appSecret = (body.appSecret ?? "").trim();
15826
+ if (appId === "" || appSecret === "") {
15827
+ sendJson$2(res, 400, { error: "appId 与 appSecret 都必须填(只填一个等于没绑)" });
15828
+ return;
15829
+ }
15830
+ const { readWeixinSettings, upsertWeixinAccount, writeFeishuCredential, nextFeishuAccountId } = await import("./route-COOFLTpU.js").then((n) => n.c);
15831
+ const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
15832
+ const before = readWeixinSettings(root);
15833
+ const accountId = (body.accountId ?? "").trim() !== "" ? body.accountId.trim() : nextFeishuAccountId(before);
15834
+ upsertWeixinAccount(root, {
15835
+ accountId,
15836
+ name: (body.name ?? "").trim() !== "" ? body.name.trim() : void 0,
15837
+ channel: "feishu",
15838
+ enabled: true
15839
+ });
15840
+ writeFeishuCredential(root, accountId, {
15841
+ appId,
15842
+ appSecret
15843
+ });
15844
+ if (before.enabled) syncCccBridge(ctx, root);
15845
+ sendJson$2(res, 200, {
15846
+ accountId,
15847
+ channel: "feishu",
15848
+ saved: true
15849
+ });
15850
+ return;
15851
+ }
15852
+ sendJson$2(res, 400, { error: `unsupported action: ${body.action ?? ""}` });
15689
15853
  } catch (err) {
15690
- sendJson$3(res, 400, { error: err.message ?? String(err) });
15854
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
15691
15855
  }
15692
15856
  }
15693
15857
  });
@@ -15698,22 +15862,22 @@ function registerStatusApi(ctx, opts = {}) {
15698
15862
  try {
15699
15863
  if (!requireWebUi(req, res, "微信桥配置仅限 WebUI(client 专用)")) return;
15700
15864
  if (req.method !== "GET") {
15701
- sendJson$3(res, 405, { error: "method not allowed" });
15865
+ sendJson$2(res, 405, { error: "method not allowed" });
15702
15866
  return;
15703
15867
  }
15704
15868
  const key = new URL(req.url ?? "/", "http://127.0.0.1").searchParams.get("key") ?? "";
15705
15869
  const login = weixinLogins.get(key);
15706
15870
  if (!login) {
15707
- sendJson$3(res, 404, { error: "login not found or expired" });
15871
+ sendJson$2(res, 404, { error: "login not found or expired" });
15708
15872
  return;
15709
15873
  }
15710
15874
  if (Date.now() - login.startedAt > WEIXIN_LOGIN_TTL_MS) {
15711
15875
  weixinLogins.delete(key);
15712
- sendJson$3(res, 200, { status: "expired" });
15876
+ sendJson$2(res, 200, { status: "expired" });
15713
15877
  return;
15714
15878
  }
15715
- const { pollQRStatus } = await import("./ilink-DEpmE3yi.js").then((n) => n.a);
15716
- const { readWeixinSettings } = await import("./route-f7JlinIt.js").then((n) => n.s);
15879
+ const { pollQRStatus } = await import("./ilink-BMk2w2KY.js").then((n) => n.a);
15880
+ const { readWeixinSettings } = await import("./route-COOFLTpU.js").then((n) => n.c);
15717
15881
  const settings = readWeixinSettings(login.root);
15718
15882
  const status = await pollQRStatus({
15719
15883
  baseUrl: void 0,
@@ -15722,13 +15886,13 @@ function registerStatusApi(ctx, opts = {}) {
15722
15886
  switch (status.status) {
15723
15887
  case "confirmed": {
15724
15888
  if (!status.bot_token) {
15725
- sendJson$3(res, 200, {
15889
+ sendJson$2(res, 200, {
15726
15890
  status: "error",
15727
15891
  error: "confirmed but no bot_token"
15728
15892
  });
15729
15893
  return;
15730
15894
  }
15731
- const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./route-f7JlinIt.js").then((n) => n.s);
15895
+ const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./route-COOFLTpU.js").then((n) => n.c);
15732
15896
  const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
15733
15897
  const accountId = nextWeixinAccountId(settings);
15734
15898
  upsertWeixinAccount(login.root, {
@@ -15743,7 +15907,7 @@ function registerStatusApi(ctx, opts = {}) {
15743
15907
  });
15744
15908
  syncCccBridge(ctx, login.root);
15745
15909
  weixinLogins.delete(key);
15746
- sendJson$3(res, 200, {
15910
+ sendJson$2(res, 200, {
15747
15911
  status: "confirmed",
15748
15912
  accountId,
15749
15913
  tokenSaved: true
@@ -15752,14 +15916,14 @@ function registerStatusApi(ctx, opts = {}) {
15752
15916
  }
15753
15917
  case "expired":
15754
15918
  weixinLogins.delete(key);
15755
- sendJson$3(res, 200, { status: "expired" });
15919
+ sendJson$2(res, 200, { status: "expired" });
15756
15920
  return;
15757
15921
  default:
15758
- sendJson$3(res, 200, { status: status.status ?? "wait" });
15922
+ sendJson$2(res, 200, { status: status.status ?? "wait" });
15759
15923
  return;
15760
15924
  }
15761
15925
  } catch (err) {
15762
- sendJson$3(res, 400, { error: err.message ?? String(err) });
15926
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
15763
15927
  }
15764
15928
  }
15765
15929
  });
@@ -15781,7 +15945,7 @@ function registerStatusApi(ctx, opts = {}) {
15781
15945
  const root = sessionsRootDir();
15782
15946
  if (req.method === "GET") {
15783
15947
  const { candidates } = performCleanup(root, cutoffDaysAgo(days), liveIds, { dryRun: true });
15784
- sendJson$3(res, 200, {
15948
+ sendJson$2(res, 200, {
15785
15949
  root,
15786
15950
  olderThanDays: days,
15787
15951
  dryRun: true,
@@ -15795,12 +15959,12 @@ function registerStatusApi(ctx, opts = {}) {
15795
15959
  return;
15796
15960
  }
15797
15961
  if (req.method !== "POST") {
15798
- sendJson$3(res, 405, { error: "method not allowed" });
15962
+ sendJson$2(res, 405, { error: "method not allowed" });
15799
15963
  return;
15800
15964
  }
15801
15965
  if (!requireWebUi(req, res, "会话清理仅限 WebUI(client 专用)")) return;
15802
15966
  const { result } = performCleanup(root, cutoffDaysAgo(days), liveIds);
15803
- sendJson$3(res, 200, {
15967
+ sendJson$2(res, 200, {
15804
15968
  root,
15805
15969
  olderThanDays: days,
15806
15970
  dryRun: false,
@@ -15808,7 +15972,7 @@ function registerStatusApi(ctx, opts = {}) {
15808
15972
  errors: result.errors
15809
15973
  });
15810
15974
  } catch (err) {
15811
- sendJson$3(res, 400, { error: err.message ?? String(err) });
15975
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
15812
15976
  }
15813
15977
  }
15814
15978
  });
@@ -15987,7 +16151,7 @@ function loadVoyageHtml(assetPath = voyageAssetPath()) {
15987
16151
  return readFileSync(assetPath, "utf-8");
15988
16152
  }
15989
16153
  /** 发 JSON(错误分支用;与 surface/http.ts 的 sendJson 同形,但本模块不依赖它,保持零耦合) */
15990
- function sendJson$2(res, status, body) {
16154
+ function sendJson$1(res, status, body) {
15991
16155
  res.statusCode = status;
15992
16156
  res.setHeader("content-type", "application/json; charset=utf-8");
15993
16157
  res.end(JSON.stringify(body));
@@ -16005,14 +16169,14 @@ function registerVoyageApi(ctx, opts = {}) {
16005
16169
  path: VOYAGE_PATH,
16006
16170
  handler: (req, res) => {
16007
16171
  if (req.method !== "GET") {
16008
- sendJson$2(res, 405, { error: "method not allowed" });
16172
+ sendJson$1(res, 405, { error: "method not allowed" });
16009
16173
  return;
16010
16174
  }
16011
16175
  let html;
16012
16176
  try {
16013
16177
  html = loadVoyageHtml(opts.assetPath ?? voyageAssetPath());
16014
16178
  } catch (err) {
16015
- sendJson$2(res, 500, { error: `voyage asset unavailable: ${err?.message ?? String(err)}` });
16179
+ sendJson$1(res, 500, { error: `voyage asset unavailable: ${err?.message ?? String(err)}` });
16016
16180
  return;
16017
16181
  }
16018
16182
  let root = null;
@@ -16170,7 +16334,7 @@ const HOST_SERVICES = [
16170
16334
  name: "get",
16171
16335
  kind: "function"
16172
16336
  }],
16173
- impact: "skiff/cheap-worker 无法创建或复用 agent",
16337
+ impact: "轨迹约束/cheap-worker 无法创建或复用 agent",
16174
16338
  required: true
16175
16339
  },
16176
16340
  {
@@ -16593,7 +16757,7 @@ function summarizeHostContract(report) {
16593
16757
  *
16594
16758
  * ## 现状 → 本模块要收掉什么(取证:`…/acc-c4c5-current-state.md` §①/§③ 3.1)
16595
16759
  *
16596
- * 插件自己 spawn 了**三个** node:http 监听器(B 3081 网关 / C 3082 微信发送 / D 3099 Skiff 调试)。
16760
+ * 插件自己 spawn 了**三个** node:http 监听器(B 3081 网关 / C 3082 微信发送 / D 3099 轨迹约束调试)。
16597
16761
  * ⚠️ **2026-10-15 变更**:原为四个(含 **E 3100 ACP+问答**),E 已随 owner「砍掉外部问答」令**整面退役**
16598
16762
  * ⇒ 仍保有面 B/C/D 三个;本段保留"当年是四个"的记述,因为下方"两套拆卸"的病史由它解释。
16599
16763
  * 此前四处各写一份同款样板:`createServer` + `server.once('error')` +
@@ -16650,10 +16814,6 @@ var FaceStartCancelled = class extends Error {
16650
16814
  this.name = "FaceStartCancelled";
16651
16815
  }
16652
16816
  };
16653
- /** 面是否在监听 */
16654
- function faceActive(name) {
16655
- return faces.has(name);
16656
- }
16657
16817
  /** 面的实际监听端口(未在监听 → null) */
16658
16818
  function facePort(name) {
16659
16819
  return faces.get(name)?.handle.port ?? null;
@@ -17240,7 +17400,7 @@ function createDshCookieProvider(connection, authority) {
17240
17400
  /** 面名(C4 块 A:listener 生命周期归 `face-host`) */
17241
17401
  const FACE_GATEWAY = "gateway";
17242
17402
  /** 读取请求体(≤ maxBytes;超限 reject) */
17243
- function readBody$2(req, maxBytes) {
17403
+ function readBody$1(req, maxBytes) {
17244
17404
  return new Promise((resolve, reject) => {
17245
17405
  let data = "";
17246
17406
  req.on("data", (chunk) => {
@@ -17367,7 +17527,7 @@ async function startGateway(config, getAccounts, getDshCookie) {
17367
17527
  const session = authed(req);
17368
17528
  if (session !== void 0) {
17369
17529
  if (req.method === "POST" && isWorkspaceCreatePath(url.pathname)) {
17370
- readBody$2(req, 131072).then((body) => {
17530
+ readBody$1(req, 131072).then((body) => {
17371
17531
  const { rpcId, path } = parseWorkspaceCreateBody(body);
17372
17532
  if (!allowWorkspaceCreate) {
17373
17533
  res.writeHead(403, { "content-type": "application/json" });
@@ -17768,9 +17928,9 @@ function sessionNameFromMdPath(mdPath) {
17768
17928
  * → ②b **权威绑定**(`AGENT_SESSIONS/.bindings.json`,本会话持久绑定)→ ③ surface 首条锚点
17769
17929
  * (events 异常时兜底)。
17770
17930
  *
17771
- * v1.30.13(S142 用户"微信桥 + skiff 会话锚定要准确",诊断 D4):**移除**原 ④「全局最新未完成
17931
+ * v1.30.13(S142 用户"微信桥 + 轨迹约束会话锚定要准确",诊断 D4):**移除**原 ④「全局最新未完成
17772
17932
  * 会话」(`findLatestActiveSessionMd`)。理由(R↓):④ 是**跨轨迹**猜测——候选①②③全空时,
17773
- * 它会把 rebuild 静默接到 AGENT_SESSIONS 里最新的另一个会话(临时 skiff 会话 persistent=false、
17933
+ * 它会把 rebuild 静默接到 AGENT_SESSIONS 里最新的另一个会话(临时 轨迹约束会话 persistent=false、
17774
17934
  * 重启后首条消息前、新会话首条消息都命中该形态),而"接错轨迹"比重建失败危险得多
17775
17935
  * (错误轨迹被继续写入 + 正确轨迹静默丢失)。现在全部候选失败 → 返回 null → 调用方
17776
17936
  * 报错引导用户显式 `cognitive_trajectory use`(响亮失败优于静默接错)。
@@ -19687,234 +19847,16 @@ f.parseInline;
19687
19847
  b.parse;
19688
19848
  x.lex;
19689
19849
  //#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
- }
19850
+ //#region src/surface/text.ts
19900
19851
  /**
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 安全文本
19852
+ * 剥离 `<think>…</think>` 块(v1.27.1,微信桥用户反馈:"过微信桥回复给用户的消息,
19853
+ * 要去掉 think 标签,用户不应看到")。复用 extractThinkBlocks 状态机(v1.26.8,
19854
+ * 弃正则)——提取后**只保留正文**(占位符替换为空,think 内容丢弃),
19855
+ * 未闭合 think 优雅截断。适用于纯文本输出面(微信等不支持 think 折叠的通道)。
19915
19856
  */
19916
- function jscSafeJsonText(jsonText) {
19917
- return jsonText.replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029").replace(/\uFEFF/g, "\\uFEFF");
19857
+ function stripThink(raw) {
19858
+ const { body } = extractThinkBlocks(raw);
19859
+ return body.replace(/\u0001T(\d+)\u0001/g, "");
19918
19860
  }
19919
19861
  /**
19920
19862
  * 提取 `<think>…</think>` 块(v1.26.8:**状态机扫描,弃正则**——用户批评"老用正则不是个办法")。
@@ -19992,140 +19934,6 @@ function matchCloseThink(s, contentStart) {
19992
19934
  }
19993
19935
  return -1;
19994
19936
  }
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
19937
  //#endregion
20130
19938
  //#region src/guard/filter.ts
20131
19939
  /**
@@ -20168,9 +19976,9 @@ const MECHANISM_WORDS = [
20168
19976
  "拦截缝",
20169
19977
  "装配层",
20170
19978
  "acp-core",
20171
- "skiff-core",
20172
- "skiff-registry",
20173
- "skiff-role",
19979
+ "trajectory-session",
19980
+ "trajectory-window",
19981
+ "trajectory-constraint",
20174
19982
  "output-guard"
20175
19983
  ];
20176
19984
  /**
@@ -20270,20 +20078,20 @@ const rebukeStates = /* @__PURE__ */ new Map();
20270
20078
  /**
20271
20079
  * 外部面会话判定(v1.26.3 用户拍板:仅外部面检测,本地维护会话豁免)。
20272
20080
  *
20273
- * 🔴 **2026-10-19 判据更换**:skiff 那一条从「sessionId 前缀 `skiff-`」改为
20274
- * **「该会话绑定的轨迹有 `skiff.json`」**(owner 重做令)。前缀判据在新模型下是
20275
- * **错的**——skiff 的身份是轨迹,不再由 id 前缀表达;继续用前缀会漏掉所有
20276
- * "挂在 skiff 轨迹上、但 id 不带前缀"的会话(典型 = 微信桥按轨迹派生的会话 id)。
20081
+ * 🔴 **2026-10-19 判据更换**:轨迹约束那一条从「sessionId 前缀 `轨迹约束-`」改为
20082
+ * **「该会话绑定的轨迹有 `trajectory-constraint.json`」**(owner 重做令)。前缀判据在新模型下是
20083
+ * **错的**——轨迹约束的身份是轨迹,不再由 id 前缀表达;继续用前缀会漏掉所有
20084
+ * "挂在 轨迹约束轨迹上、但 id 不带前缀"的会话(典型 = 微信桥按轨迹派生的会话 id)。
20277
20085
  *
20278
- * ⚠️ `rebuild-` 前缀保留(那是另一件事:重建会话同属对外面,与 skiff 无关);
20086
+ * ⚠️ `rebuild-` 前缀保留(那是另一件事:重建会话同属对外面,与 轨迹约束 无关);
20279
20087
  * `acp-` 分支保留但**已是死判据**(无生产者;面 E 已于 2026-10-15 退役)——
20280
20088
  * 理由与"为何不擅自删"见文件头注释。
20281
20089
  */
20282
20090
  function isExternalFaceSession(sessionId, root) {
20283
20091
  if (!sessionId) return false;
20284
- return isSkiffSession(root, sessionId) || sessionId.startsWith("acp-") || sessionId.startsWith("rebuild-");
20092
+ return isTrajectoryConstrained(root, sessionId) || sessionId.startsWith("acp-") || sessionId.startsWith("rebuild-");
20285
20093
  }
20286
- /** 读会话最后一个 assistant/message 文本(skiff-core 同款)
20094
+ /** 读会话最后一个 assistant/message 文本(trajectory-session 同款)
20287
20095
  * **v1.63.3 起唯一的导出理由消失**:原导出是给 `unattended-seam.ts` 复用的(D77),
20288
20096
  * 该文件已随无人值守功能整条退役 ⇒ 现只有本模块自己用(`isLeak` 的调用点)。
20289
20097
  * ⚠️ 保持 `export` 无害(它有测试覆盖,且将来"同一读法只留一份"的纪律仍成立),
@@ -20597,188 +20405,63 @@ function appendHumanChannelAudit(event) {
20597
20405
  })}\n`, "utf-8");
20598
20406
  } catch {}
20599
20407
  }
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
- }
20408
+ //#endregion
20409
+ //#region src/im/hook.ts
20642
20410
  /**
20643
- * 工具调用是否为「成功的微信发送」。
20411
+ * im/hook.ts — 微信桥消息记录 hook(F4c-3 扩展,v1.27.13 用户需求)
20644
20412
  *
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 通道,逐步退役)
20413
+ * 需求:微信桥发生的所有消息记录,支持 hook——允许 CCC 通过自行编写 hook
20414
+ * 脚本进行**持久性保存**(存哪/存成什么/是否入库,全归 CCC 决定——ACC 不绑定存储)。
20415
+ *
20416
+ * 设计(docs/weixin-message-hook-design.md v0.1,三项拍板 H1~H6):
20417
+ * - **双向触发**(H1):incoming(用户→bot,媒体落盘后)+ outgoing(bot→用户,发送成功后)
20418
+ * - **脚本 + stdin JSON**(H2):serenity.json weixin.hook = CCC 根相对脚本路径;
20419
+ * 每次消息事件 spawn 一次,事件 JSON 单行经 stdin 传入(bun 优先 / node 兜底,biasProvider 先例)
20420
+ * - **旁路容忍**(H3):异步 fire-and-forget + 超时 kill + 失败仅日志——微信桥可靠性不受影响
20421
+ * - **不含会话凭据**(H5):事件只有记录所需字段(无 context_token / bot_token——最小暴露面)
20422
+ *
20423
+ * 定位:Mech 纯逻辑(事件构造纯函数 + 执行器确定性),可独立单测。
20648
20424
  */
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;
20425
+ /** hook 执行超时(旁路记录足够;超时 kill 防挂死脚本) */
20426
+ const WEIXIN_HOOK_TIMEOUT_MS = 15e3;
20427
+ /** hook stderr/stdout 日志截断(防脚本刷屏) */
20428
+ const HOOK_LOG_MAX = 500;
20429
+ /** 构造 incoming 事件对象(纯函数,可测) */
20430
+ function buildIncomingHookEvent(input) {
20431
+ return {
20432
+ event: "incoming",
20433
+ ts: Date.now(),
20434
+ cccRoot: input.cccRoot,
20435
+ accountId: input.accountId,
20436
+ userId: input.userId,
20437
+ sessionId: input.sessionId,
20438
+ role: input.role,
20439
+ message: {
20440
+ text: input.text,
20441
+ media: input.media
20442
+ }
20443
+ };
20660
20444
  }
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");
20445
+ /** 构造 outgoing 事件对象(纯函数,可测) */
20446
+ function buildOutgoingHookEvent(input) {
20447
+ return {
20448
+ event: "outgoing",
20449
+ ts: Date.now(),
20450
+ cccRoot: input.cccRoot,
20451
+ accountId: input.accountId,
20452
+ userId: input.userId,
20453
+ sessionId: input.sessionId,
20454
+ role: input.role,
20455
+ reply: input.reply,
20456
+ ...input.source && input.source !== "reply" ? { source: input.source } : {},
20457
+ ...input.file ? { file: input.file } : {}
20458
+ };
20668
20459
  }
20669
20460
  /**
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
- //#endregion
20726
- //#region src/im/hook.ts
20727
- /**
20728
- * im/hook.ts — 微信桥消息记录 hook(F4c-3 扩展,v1.27.13 用户需求)
20729
- *
20730
- * 需求:微信桥发生的所有消息记录,支持 hook——允许 CCC 通过自行编写 hook
20731
- * 脚本进行**持久性保存**(存哪/存成什么/是否入库,全归 CCC 决定——ACC 不绑定存储)。
20732
- *
20733
- * 设计(docs/weixin-message-hook-design.md v0.1,三项拍板 H1~H6):
20734
- * - **双向触发**(H1):incoming(用户→bot,媒体落盘后)+ outgoing(bot→用户,发送成功后)
20735
- * - **脚本 + stdin JSON**(H2):serenity.json weixin.hook = CCC 根相对脚本路径;
20736
- * 每次消息事件 spawn 一次,事件 JSON 单行经 stdin 传入(bun 优先 / node 兜底,biasProvider 先例)
20737
- * - **旁路容忍**(H3):异步 fire-and-forget + 超时 kill + 失败仅日志——微信桥可靠性不受影响
20738
- * - **不含会话凭据**(H5):事件只有记录所需字段(无 context_token / bot_token——最小暴露面)
20739
- *
20740
- * 定位:Mech 纯逻辑(事件构造纯函数 + 执行器确定性),可独立单测。
20741
- */
20742
- /** hook 执行超时(旁路记录足够;超时 kill 防挂死脚本) */
20743
- const WEIXIN_HOOK_TIMEOUT_MS = 15e3;
20744
- /** hook stderr/stdout 日志截断(防脚本刷屏) */
20745
- const HOOK_LOG_MAX = 500;
20746
- /** 构造 incoming 事件对象(纯函数,可测) */
20747
- function buildIncomingHookEvent(input) {
20748
- return {
20749
- event: "incoming",
20750
- ts: Date.now(),
20751
- cccRoot: input.cccRoot,
20752
- accountId: input.accountId,
20753
- userId: input.userId,
20754
- sessionId: input.sessionId,
20755
- role: input.role,
20756
- message: {
20757
- text: input.text,
20758
- media: input.media
20759
- }
20760
- };
20761
- }
20762
- /** 构造 outgoing 事件对象(纯函数,可测) */
20763
- function buildOutgoingHookEvent(input) {
20764
- return {
20765
- event: "outgoing",
20766
- ts: Date.now(),
20767
- cccRoot: input.cccRoot,
20768
- accountId: input.accountId,
20769
- userId: input.userId,
20770
- sessionId: input.sessionId,
20771
- role: input.role,
20772
- reply: input.reply,
20773
- ...input.source && input.source !== "reply" ? { source: input.source } : {},
20774
- ...input.file ? { file: input.file } : {}
20775
- };
20776
- }
20777
- /**
20778
- * 执行一次 hook:spawn CCC 根脚本,事件 JSON 单行经 stdin 传入。
20779
- * 旁路容忍(H3):超时 kill / 非 0 退出 / spawn 失败 → 仅日志返回 {ok:false},绝不抛(不阻断微信桥)。
20780
- * 路径逃逸校验:脚本必须解析在 CCC 根内(resolveInside——resolveInside 抛错也吞为 {ok:false})。
20781
- * 脚本缺失 → {ok:false}(配置了但未实现——日志提示;不视为异常)。
20461
+ * 执行一次 hook:spawn CCC 根脚本,事件 JSON 单行经 stdin 传入。
20462
+ * 旁路容忍(H3):超时 kill / 非 0 退出 / spawn 失败 → 仅日志返回 {ok:false},绝不抛(不阻断微信桥)。
20463
+ * 路径逃逸校验:脚本必须解析在 CCC 根内(resolveInside——resolveInside 抛错也吞为 {ok:false})。
20464
+ * 脚本缺失 → {ok:false}(配置了但未实现——日志提示;不视为异常)。
20782
20465
  */
20783
20466
  async function runWeixinHook(root, hookRel, event, timeoutMs = WEIXIN_HOOK_TIMEOUT_MS) {
20784
20467
  let scriptAbs;
@@ -20882,31 +20565,6 @@ function invokeWeixinHook(root, hookRel, event) {
20882
20565
  return activeRunner(root, hookRel, event);
20883
20566
  }
20884
20567
  //#endregion
20885
- //#region src/im/accounts.ts
20886
- const impls = /* @__PURE__ */ new Map();
20887
- /** 注册账号实现(`index.ts` 装配时调用;重复 channel 覆盖——测试与热重载友好) */
20888
- function registerImAccountImpl(impl) {
20889
- impls.set(impl.channel, impl);
20890
- }
20891
- /** 取实现(未注册 ⇒ `null`,由调用方**响亮出声**,不要静默跳过) */
20892
- function getImAccountImpl(channel) {
20893
- return impls.get(channel) ?? null;
20894
- }
20895
- /** 已注册的实现名(诊断与错误文案用;顺序 = 注册顺序) */
20896
- function imAccountChannels() {
20897
- return [...impls.keys()];
20898
- }
20899
- /**
20900
- * 账号行的渠道归一(**单一处**)。
20901
- *
20902
- * 缺省 `weixin`:老配置平移到 `im` 段时不必补这个字段——owner 的"统一配置"
20903
- * 要能直接吃旧数据(与 `route.ts` 的 `normalizeImSection` 保持同一口径)。
20904
- */
20905
- function normalizeAccountChannel(channel) {
20906
- const v = (channel ?? "").trim();
20907
- return v === "" ? "weixin" : v;
20908
- }
20909
- //#endregion
20910
20568
  //#region src/im/weixin-account.ts
20911
20569
  /** 轮询间隔(getupdates 失败后重试;成功 = 立即续,长轮询本身 hold 35s) */
20912
20570
  const POLL_RETRY_MS = 3e3;
@@ -20980,138 +20638,145 @@ function installWeixinAccountImpl(d) {
20980
20638
  setWeixinLoopDeps(d);
20981
20639
  registerImAccountImpl(weixinAccountImpl);
20982
20640
  }
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();
20641
+ const manualSessions = /* @__PURE__ */ new Map();
20999
20642
  /**
21000
- * 🔴 **机器级 poller 的"根"哨兵**(P0-3 A)—— 它**不是一个目录**。
21001
- *
21002
- * 机器级 poller **不属于任何 CCC**(这正是"上移 ACC 层"的含义:谁轮询不再由 CCC 数量决定)。
21003
- * 用途有二:① `runAccountLoop` 的 `root` 形参在机器级模式下不被使用,传它以示"无根";
21004
- * ② 归属表(`im/inbound.ts`)按 root 区分拥有者 ⇒ 机器级用这个哨兵值,
21005
- * 与任何**真实** CCC 根都不会撞(真根一律是绝对路径)。
20643
+ * 本轮是否已成功发送(**由桥在每轮开始时清空** `clearSentThisTurn`;闸门只置位不清空——
20644
+ * v1.30.17 起桥要在 `askTrajectory` 返回后读它决定是否兜底,故 turn-stopping 不再结算清空)。
21006
20645
  */
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}`;
20646
+ const sentThisTurn = /* @__PURE__ */ new Set();
20647
+ /** 连续打回计数(成功发送即清零) */
20648
+ const rebukeCounts = /* @__PURE__ */ new Map();
20649
+ /** 闸门是否装配成功(v1.30.17:桥据此判断能否信任"未发送"这一判定——未装配时不兜底,防重复发送) */
20650
+ let guardActive = false;
20651
+ /** 桥:登记手动模式会话(每轮 incoming 都调用,幂等覆盖) */
20652
+ function noteManualOutputSession(sessionId, session) {
20653
+ if (typeof sessionId !== "string" || sessionId === "") return;
20654
+ manualSessions.set(sessionId, session);
21016
20655
  }
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
- }
20656
+ /** 桥:非手动模式 / 会话失效 → 注销(闸门随之失效,零干预) */
20657
+ function forgetManualOutputSession(sessionId) {
20658
+ manualSessions.delete(sessionId);
20659
+ sentThisTurn.delete(sessionId);
20660
+ rebukeCounts.delete(sessionId);
21043
20661
  }
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
- }
20662
+ /** 本轮是否已成功发送(测试/诊断用;桥在每轮结束时读它决定是否兜底) */
20663
+ function hasSentThisTurn(sessionId) {
20664
+ return sentThisTurn.has(sessionId);
21059
20665
  }
21060
20666
  /**
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 成环。
20667
+ * 桥:**每轮开始时**清空本会话的"已发送"标记(turn 边界由桥掌握——v1.30.17)。
20668
+ * 闸门侧不再于 turn-stopping 结算清空,否则桥读不到本轮结果、无法兜底。
21069
20669
  */
20670
+ function clearSentThisTurn(sessionId) {
20671
+ sentThisTurn.delete(sessionId);
20672
+ }
20673
+ /** 闸门是否装配成功(未装配 → 桥不做兜底:宁可静默也不冒重复发送的风险) */
20674
+ function isWeixinOutputGuardActive() {
20675
+ return guardActive;
20676
+ }
20677
+ /** 从 agent / 会话对象取会话 id(形状宽容) */
20678
+ function sessionIdOf$1(value) {
20679
+ const v = value;
20680
+ const id = v?.session?.id ?? v?.id;
20681
+ return typeof id === "string" && id !== "" ? id : null;
20682
+ }
21070
20683
  /**
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 管内容"的归属二分。
20684
+ * 工具调用是否为「成功的微信发送」。
21082
20685
  *
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
- * ```
20686
+ * v1.31.0 起有两种合法形态(标记与闸门同时认,迁移期并存):
20687
+ * - `im-bridge({channel:"weixin", action:"send"|"send-file", …})` —— ACC 工具(推荐)
20688
+ * - `msm("weixin-send", ["send"|"send-file", …])` —— CCC MSM(v1.30.x 通道,逐步退役)
21088
20689
  */
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");
20690
+ function isSuccessfulWeixinSend(exec, result) {
20691
+ const e = exec;
20692
+ if (result?.isError === true) return false;
20693
+ const args = e?.arguments;
20694
+ if (e?.name === "im-bridge") {
20695
+ if ((typeof args?.channel === "string" ? args.channel : "") !== "weixin") return false;
20696
+ const action = typeof args?.action === "string" ? args.action : "";
20697
+ return action === "send" || action === "send-file";
20698
+ }
20699
+ if (e?.name === "msm") return args?.name === "weixin-send";
20700
+ return false;
20701
+ }
20702
+ /** 打回消息(机制事实 + 标记;怎么做由 CCC 角色提示词定义) */
20703
+ function buildManualOutputRebuke(session) {
20704
+ return [
20705
+ "[serenity:weixin-manual-output] no send detected in this turn — the user has received NOTHING.",
20706
+ `ccc=${session.root} account=${session.accountId} user=${session.userId}`,
20707
+ `im-bridge({channel:"weixin", action:"send", account:"${session.accountId}", user:"${session.userId}", text:"<回复>"})`
20708
+ ].join("\n");
21091
20709
  }
21092
20710
  /**
21093
- * 手动输出模式的**兜底判定**(纯函数,v1.30.17——把决策矩阵从装配里抽出来,可独立测试)。
21094
- *
21095
- * 四个条件同时成立才兜底:
21096
- * ① CCC 显式开启 `fallbackOnNoSend`(缺省 false = 严格静默,向后兼容)
21097
- * ② 机械闸门**已装配**(未装配时无法判定"是否发送过" → 宁可静默也不冒重复发送风险)
21098
- * ③ 本轮**没有**成功发送(agent 自己发过 → 输出权归 agent,桥不插话)
21099
- * ④ 有最终文本可转发(空文本无意义)
20711
+ * 装配闸门(index.ts apply 调用)。
20712
+ * 只对**已登记的手动模式会话**生效——其他会话(主舱/其他 轨迹约束会话)零干预。
21100
20713
  */
21101
- function manualOutputFallbackNeeded(input) {
21102
- return input.fallbackOnNoSend === true && input.guardActive && !input.sentThisTurn && input.answer !== "";
20714
+ function registerWeixinOutputGuard(ctx) {
20715
+ try {
20716
+ ctx.on("tools/post-execute", async (exec, result, next) => {
20717
+ const downstream = await next();
20718
+ try {
20719
+ const sessionId = sessionIdOf$1(exec?.agent);
20720
+ if (sessionId && manualSessions.has(sessionId) && isSuccessfulWeixinSend(exec, result)) {
20721
+ sentThisTurn.add(sessionId);
20722
+ rebukeCounts.delete(sessionId);
20723
+ }
20724
+ } catch {}
20725
+ return downstream;
20726
+ });
20727
+ } catch {
20728
+ console.warn("[serenity-hooks] ✗ weixin 输出闸门未装配:tools/post-execute 不可用");
20729
+ return;
20730
+ }
20731
+ try {
20732
+ ctx.on("agent/turn-stopping", (payload) => {
20733
+ const agent = payload?.agent;
20734
+ if (!agent) return;
20735
+ const sessionId = sessionIdOf$1(agent);
20736
+ if (!sessionId) return;
20737
+ const session = manualSessions.get(sessionId);
20738
+ if (!session) return;
20739
+ if (sentThisTurn.has(sessionId)) return;
20740
+ const count = (rebukeCounts.get(sessionId) ?? 0) + 1;
20741
+ if (count > 2) {
20742
+ rebukeCounts.delete(sessionId);
20743
+ console.warn(`[serenity-hooks] ✗ weixin 输出闸门:连续 2 次打回后仍未发送(session=${sessionId}, role=${session.role})——本轮用户将收不到 agent 自己发的消息`);
20744
+ return;
20745
+ }
20746
+ rebukeCounts.set(sessionId, count);
20747
+ try {
20748
+ agent.steer(createUserMessage({
20749
+ content: [{
20750
+ type: "text",
20751
+ text: buildManualOutputRebuke(session)
20752
+ }],
20753
+ source: PLUGIN_SOURCE
20754
+ }));
20755
+ console.log(`[serenity-hooks] weixin 输出闸门:打回 ${count}/2(session=${sessionId}, turn=${payload.turn ?? "?"})`);
20756
+ } catch (error) {
20757
+ console.warn(`[serenity-hooks] ✗ weixin 输出闸门 steer 失败: ${String(error?.message ?? error)}`);
20758
+ }
20759
+ });
20760
+ } catch {
20761
+ console.warn("[serenity-hooks] ✗ weixin 输出闸门未装配:agent/turn-stopping 不可用");
20762
+ return;
20763
+ }
20764
+ guardActive = true;
21103
20765
  }
20766
+ //#endregion
20767
+ //#region src/im/inbound-event.ts
20768
+ /** 媒体大小上限(M6:20MB,对齐 vlm-describe;超限降级告知不落盘) */
20769
+ const MEDIA_MAX_BYTES = 20971520;
21104
20770
  /**
21105
20771
  * 订阅表里的 `ccc` **别名 → CCC 根**(P0-3)。
21106
20772
  *
21107
- * 别名取值与面板下拉**同源**(方案 §2.3:`listCccs({withRoles:true})` 的下拉)⇒
21108
- * 先按 **`CccEntry.name`**(= CCC 根目录名,`basename(root)`)匹配;再退一步接受**绝对路径**
21109
- * (手写配置时人会直接粘路径)。两条都试 ⇒ 两种写法都可用,且**不引入第二张名字表**。
21110
- * @returns 命中的 CCC 根;枚举里没有 ⇒ `null`(调用方**响亮报错**,不静默丢弃)
20773
+ * 别名取值与面板下拉**同源** ⇒ 先按 `CccEntry.name`(= CCC 根目录名)匹配;
20774
+ * 再退一步接受**绝对路径**(手写配置时人会直接粘路径)。两条都试
20775
+ * ⇒ 两种写法都可用,且**不引入第二张名字表**。
21111
20776
  */
21112
20777
  async function resolveCccRootByAlias(ctx, alias) {
21113
20778
  try {
21114
- const { listCccs } = await import("./ccc-roots-jZYR2GVE.js").then((n) => n.a);
20779
+ const { listCccs } = await import("./ccc-roots-B2t_aM8L.js").then((n) => n.a);
21115
20780
  const entries = await listCccs(ctx);
21116
20781
  const byName = entries.find((e) => e.name === alias);
21117
20782
  if (byName) return byName.root;
@@ -21128,11 +20793,9 @@ async function resolveCccRootByAlias(ctx, alias) {
21128
20793
  * 判据**,差别只有一格 —— **有没有 CCC 级回退**(`cccFallback`)。写两遍必然分叉。
21129
20794
  *
21130
20795
  * 三档(方案 §3.1,**刻意分档**):
21131
- * - **未命中** ⇒ 不投递 + **留痕**(`console.warn`,静默档:这是"表里没有你",不是错误)
21132
- * - **表残缺** ⇒ **响亮出声**(`console.error`;且**独立于是否命中**)
21133
- * - **目标不存在**(CCC / **轨迹**)⇒ **响亮报错**(`console.error`;不静默丢弃)
21134
- *
21135
- * @param input.cccFallback 过渡期回退(方案 Q5);**机器级模式传 `null`** ⇒ 未命中即不投递
20796
+ * - **未命中** ⇒ 不投递 + **留痕**(静默档:这是"表里没有你",不是错误)
20797
+ * - **表残缺** ⇒ **响亮出声**(且**独立于是否命中**)
20798
+ * - **目标不存在**(CCC / **轨迹**)⇒ **响亮报错**(不静默丢弃)
21136
20799
  */
21137
20800
  async function resolveInboundTarget(ctx, input) {
21138
20801
  const route = planInboundRoute({
@@ -21181,7 +20844,7 @@ async function resolveInboundTarget(ctx, input) {
21181
20844
  });
21182
20845
  return null;
21183
20846
  }
21184
- const { findDirNameByCode } = await import("./profile-DyMJ_bfr.js").then((n) => n.i);
20847
+ const { findDirNameByCode } = await import("./profile-BVt84mbi.js").then((n) => n.i);
21185
20848
  if (findDirNameByCode(root, target.role) === null) {
21186
20849
  const detail = `目标 CCC "${target.ccc}" 内没有编码为 "${target.role}" 的认知轨迹`;
21187
20850
  console.error(`[serenity-hooks] ✗ human-channel: ${detail}(account=${input.account} user=${input.user})—— 该消息**未投递**,请改订阅表里的目标编码`);
@@ -21195,7 +20858,7 @@ async function resolveInboundTarget(ctx, input) {
21195
20858
  });
21196
20859
  return null;
21197
20860
  }
21198
- const profile = readSkiffProfileByCode(root, target.role);
20861
+ const profile = readTrajectoryConstraintByCode(root, target.role);
21199
20862
  return {
21200
20863
  root,
21201
20864
  trajectory: target.role,
@@ -21203,33 +20866,21 @@ async function resolveInboundTarget(ctx, input) {
21203
20866
  };
21204
20867
  }
21205
20868
  /**
21206
- * 处理单条微信消息:路由 → skiff 会话(固定 id 创建/延续)→ 提问 → 回复回写。
20869
+ * **一条入站消息的主流程**(通道中立)。
21207
20870
  *
21208
- * 会话语义:`weixinSessionIdFor(fromUserId)` 固定可重建——同用户长期同一会话;
21209
- * 进程内已有(getSkiffAgent 命中)→ 延续;无(首次/进程重启)→ createSkiffAgent
21210
- * 固定 id resume-or-create(v1.27.2):磁盘已有持久化 log → resume(历史延续,
21211
- * 重启后记忆保留——真正的"同用户长期延续");无 log(首次)→ create。
21212
- * "新的对话已开始"通知仅真正首次(create)时发送;resume/进程内延续不发。
20871
+ * 职责:路由 → 固定会话 id 创建/延续 → 提问 → 回复(或被闸门拦下后兜底)。
20872
+ * 语料(媒体)由事件自带 `fetch()`;出站走的 `io`。
21213
20873
  *
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)。
20874
+ * 🔴 **本函数没有任何 iLink 概念** —— 这是 B1 的判据。任何 `context_token` /
20875
+ * `item_list` / `message_type` 出现在这里,就说明抽缝没抽干净。
21222
20876
  *
21223
- * 🔴 **P0-3(2026-09-27):路由改由机器级订阅表决定**(本函数的 `root` 入参退为
21224
- * **回退目标**)。两档语义刻意分开(方案 §3.1):**未命中 ⇒ 不投递 + 留痕**(静默档);
21225
- * **目标不存在(CCC / 角色)⇒ 响亮报错**(`console.error` + 机器级留痕)。两者都
21226
- * **不是**"投给默认角色"。
20877
+ * @param preset 机器级模式:目标已解析好 ⇒ **不再自行路由**,且**不看本 CCC 的 `enabled`**
21227
20878
  */
21228
- async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
21229
- const fromUserId = msg.from_user_id;
20879
+ async function handleInboundEvent(ctx, root, accountId, io, event, preset) {
20880
+ const fromUserId = event.userId;
21230
20881
  if (!fromUserId) return;
21231
- const text = extractWeixinText(msg);
21232
- const mediaRefs = extractWeixinMedia(msg);
20882
+ const text = event.text;
20883
+ const mediaRefs = event.media;
21233
20884
  const settings = readWeixinSettings(root);
21234
20885
  let resolved;
21235
20886
  if (preset) resolved = preset.target;
@@ -21249,24 +20900,22 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
21249
20900
  const { trajectory, profile } = resolved;
21250
20901
  root = resolved.root;
21251
20902
  if (!text && mediaRefs.length === 0) {
21252
- if (hasVoiceItem(msg)) await sendTextMessage({
21253
- baseUrl: cred.baseUrl,
21254
- token: cred.token,
20903
+ if (event.hasVoice) await io.sendText({
21255
20904
  toUserId: fromUserId,
21256
20905
  text: "(抱歉,暂时无法解析这条语音消息,请尝试发送文字)",
21257
- contextToken: msg.context_token
20906
+ replyRef: event.replyRef
21258
20907
  }).catch(() => {});
21259
20908
  return;
21260
20909
  }
21261
20910
  try {
21262
20911
  const sessionId = weixinSessionIdForTrajectory(trajectory);
21263
- const existing = getSkiffAgent(sessionId);
20912
+ const existing = getTrajectoryAgent(sessionId);
21264
20913
  const hc = readCheapWorkerConfig(root);
21265
20914
  const ref = existing ? {
21266
20915
  agent: existing,
21267
20916
  sessionId,
21268
20917
  resumed: true
21269
- } : await createSkiffAgent(ctx, root, profile ?? {
20918
+ } : await createTrajectoryAgent(ctx, root, profile ?? {
21270
20919
  dirName: trajectory,
21271
20920
  code: trajectory,
21272
20921
  path: "",
@@ -21278,25 +20927,22 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
21278
20927
  rebuild: true
21279
20928
  }
21280
20929
  }, hc?.defaultModel, sessionId);
21281
- if (!ref.resumed) await sendTextMessage({
21282
- baseUrl: cred.baseUrl,
21283
- token: cred.token,
20930
+ if (!ref.resumed) await io.sendText({
21284
20931
  toUserId: fromUserId,
21285
20932
  text: "(新的对话已开始)",
21286
- contextToken: msg.context_token
20933
+ replyRef: event.replyRef
21287
20934
  }).catch(() => {});
21288
- await sendTypingStart(cred, accountId, fromUserId, msg.context_token);
20935
+ await io.typingStart?.({
20936
+ toUserId: fromUserId,
20937
+ replyRef: event.replyRef
20938
+ });
21289
20939
  try {
21290
20940
  const mediaNotes = [];
21291
20941
  const degradedNotes = [];
21292
20942
  const hookMedia = [];
21293
20943
  for (const mediaRef of mediaRefs) {
21294
- const mediaType = mediaRef.kind === "image" ? "image_item" : "file_item";
21295
20944
  const label = mediaRef.kind === "image" ? "一张图片" : `文件 ${mediaRef.fileName ?? "(未命名)"}`;
21296
- const result = await downloadMedia({
21297
- item: mediaRef.item,
21298
- mediaType
21299
- });
20945
+ const result = await mediaRef.fetch();
21300
20946
  if (!result) {
21301
20947
  hookMedia.push({
21302
20948
  kind: mediaRef.kind,
@@ -21316,7 +20962,7 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
21316
20962
  try {
21317
20963
  const inboundDir = weixinInboundDir(root, fromUserId);
21318
20964
  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")}`;
20965
+ 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
20966
  const abs = join(inboundDir, fname);
21321
20967
  writeFileSync(abs, result.data);
21322
20968
  const rel = relative(root, abs);
@@ -21330,11 +20976,11 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
21330
20976
  kind: mediaRef.kind,
21331
20977
  relPath: null
21332
20978
  });
21333
- degradedNotes.push(`(媒体保存失败,请重试)`);
20979
+ degradedNotes.push("(媒体保存失败,请重试)");
21334
20980
  }
21335
20981
  }
21336
20982
  const parts = [];
21337
- const workspaceMd = skiffWorkspaceMdPath(root, sessionId) ?? getActiveSessionInfo(sessionId)?.mdPath ?? null;
20983
+ const workspaceMd = trajectoryWorkspaceMdPath(root, sessionId) ?? getActiveSessionInfo(sessionId)?.mdPath ?? null;
21338
20984
  if (workspaceMd) {
21339
20985
  if (!ensureWorkspacePromptSection(ref.agent, sessionId, root)) parts.push(workspaceCognitiveTrajectoryLine(workspaceMd));
21340
20986
  }
@@ -21349,7 +20995,7 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
21349
20995
  userId: fromUserId,
21350
20996
  role: trajectory
21351
20997
  });
21352
- parts.push(weixinManualOutputMarker(root, accountId, fromUserId));
20998
+ parts.push(manualOutputMarker(accountId, fromUserId));
21353
20999
  } else forgetManualOutputSession(sessionId);
21354
21000
  const question = parts.join("\n");
21355
21001
  const hookRel = settings.hook;
@@ -21362,9 +21008,9 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
21362
21008
  text,
21363
21009
  media: hookMedia
21364
21010
  })).catch((err) => {
21365
- console.log(`[serenity-hooks] weixin hook incoming error: ${err instanceof Error ? err.message : String(err)}`);
21011
+ console.log(`[serenity-hooks] im hook incoming error: ${err instanceof Error ? err.message : String(err)}`);
21366
21012
  });
21367
- const answer = (await askSkiff(ctx, ref.agent, question)).answer ?? "";
21013
+ const answer = (await askTrajectory(ctx, ref.agent, question)).answer ?? "";
21368
21014
  if (manualOutput) {
21369
21015
  if (!manualOutputFallbackNeeded({
21370
21016
  fallbackOnNoSend: settings.fallbackOnNoSend,
@@ -21373,14 +21019,12 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
21373
21019
  answer
21374
21020
  })) return;
21375
21021
  const reply = markdownToPlainText(stripThink(answer));
21376
- await sendTextMessage({
21377
- baseUrl: cred.baseUrl,
21378
- token: cred.token,
21022
+ await io.sendText({
21379
21023
  toUserId: fromUserId,
21380
21024
  text: reply,
21381
- contextToken: msg.context_token
21025
+ replyRef: event.replyRef
21382
21026
  });
21383
- console.warn(`[serenity-hooks] ⚠ weixin 输出兜底:agent 本轮未发送(闸门打回用尽)→ 桥转发最终文本(session=${sessionId}, trajectory=${trajectory})`);
21027
+ console.warn(`[serenity-hooks] ⚠ im 输出兜底:agent 本轮未发送(闸门打回用尽)→ 桥转发最终文本(session=${sessionId}, trajectory=${trajectory})`);
21384
21028
  if (hookRel) invokeWeixinHook(root, hookRel, buildOutgoingHookEvent({
21385
21029
  cccRoot: root,
21386
21030
  accountId,
@@ -21390,18 +21034,16 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
21390
21034
  reply,
21391
21035
  source: "reply-fallback"
21392
21036
  })).catch((err) => {
21393
- console.log(`[serenity-hooks] weixin hook outgoing(reply-fallback) error: ${err instanceof Error ? err.message : String(err)}`);
21037
+ console.log(`[serenity-hooks] im hook outgoing(reply-fallback) error: ${err instanceof Error ? err.message : String(err)}`);
21394
21038
  });
21395
21039
  return;
21396
21040
  }
21397
21041
  if (answer === "") return;
21398
21042
  const reply = markdownToPlainText(stripThink(answer));
21399
- await sendTextMessage({
21400
- baseUrl: cred.baseUrl,
21401
- token: cred.token,
21043
+ await io.sendText({
21402
21044
  toUserId: fromUserId,
21403
21045
  text: reply,
21404
- contextToken: msg.context_token
21046
+ replyRef: event.replyRef
21405
21047
  });
21406
21048
  if (hookRel) invokeWeixinHook(root, hookRel, buildOutgoingHookEvent({
21407
21049
  cccRoot: root,
@@ -21411,28 +21053,369 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
21411
21053
  role: trajectory,
21412
21054
  reply
21413
21055
  })).catch((err) => {
21414
- console.log(`[serenity-hooks] weixin hook outgoing error: ${err instanceof Error ? err.message : String(err)}`);
21056
+ console.log(`[serenity-hooks] im hook outgoing error: ${err instanceof Error ? err.message : String(err)}`);
21415
21057
  });
21416
21058
  } finally {
21417
- await sendTypingStop(cred, accountId, fromUserId);
21059
+ await io.typingStop?.({ toUserId: fromUserId });
21418
21060
  }
21419
21061
  } catch (err) {
21420
21062
  const msg = err instanceof Error ? err.message : String(err);
21421
- console.log(`[serenity-hooks] weixin-bridge error (ccc=${root}): ${msg}`);
21063
+ console.log(`[serenity-hooks] im-bridge error (ccc=${root}): ${msg}`);
21422
21064
  const stack = err instanceof Error ? err.stack : void 0;
21423
- if (stack) console.log(`[serenity-hooks] weixin-bridge stack:\n${stack.slice(0, 1500)}`);
21065
+ if (stack) console.log(`[serenity-hooks] im-bridge stack:\n${stack.slice(0, 1500)}`);
21066
+ }
21067
+ }
21068
+ /**
21069
+ * 手动输出模式的**机制标记**(v1.30.10 引入;v1.30.16 按「ACC 管机制、CCC 管措辞」重构)。
21070
+ *
21071
+ * 边界(R↓):ACC 只提供**机制与数据**——"桥不转发最终文本"这个事实 + 三个已填好的参数值。
21072
+ * **纪律措辞归 CCC**:怎么写、必须怎么做、后果是什么,全部写在轨迹的 `PROMPT.md` 里。
21073
+ */
21074
+ function manualOutputMarker(accountId, userId) {
21075
+ return ["[serenity:weixin-manual-output]", `im-bridge({channel:"weixin", action:"send", account:"${accountId}", user:"${userId}", text:"<回复>"})`].join("\n");
21076
+ }
21077
+ /**
21078
+ * 手动输出模式的**兜底判定**(纯函数,v1.30.17——把决策矩阵从装配里抽出来,可独立测试)。
21079
+ *
21080
+ * 四个条件同时成立才兜底:
21081
+ * ① CCC 显式开启 `fallbackOnNoSend`(缺省 false = 严格静默)
21082
+ * ② 机械闸门**已装配**(未装配时无法判定"是否发送过" → 宁可静默也不冒重复发送风险)
21083
+ * ③ 本轮**没有**成功发送(agent 自己发过 → 输出权归 agent,桥不插话)
21084
+ * ④ 有最终文本可转发(空文本无意义)
21085
+ */
21086
+ function manualOutputFallbackNeeded(input) {
21087
+ return input.fallbackOnNoSend === true && input.guardActive && !input.sentThisTurn && input.answer !== "";
21088
+ }
21089
+ //#endregion
21090
+ //#region src/im/feishu-account.ts
21091
+ /** 去重集合上限(飞书重推窗口有限;限量防内存无界增长) */
21092
+ const SEEN_LIMIT = 2e3;
21093
+ /** 已处理过的 message_id(超时重推去重;按账号隔离) */
21094
+ const seenByAccount = /* @__PURE__ */ new Map();
21095
+ /** 取 SDK(未注入 ⇒ 动态 import 真包) */
21096
+ async function loadFeishuSdk() {
21097
+ return await import("./lib-TxscFKeo.js").then((m) => /* @__PURE__ */ __toESM(m.default, 1));
21098
+ }
21099
+ /**
21100
+ * 账号 → 长连接客户端(`ImAccountLoop` 的形状是面板既有契约,**不加字段** ——
21101
+ * 加字段会逼面板与 `im-bridge status` 认识一个通道特有的东西)。
21102
+ * 用途:`stopped` 置位后能真正 `close()` 掉连接(只置标志位而不关连接 = 假停止)。
21103
+ */
21104
+ const wsByAccount = /* @__PURE__ */ new Map();
21105
+ function seenSet(accountId) {
21106
+ let s = seenByAccount.get(accountId);
21107
+ if (!s) {
21108
+ s = /* @__PURE__ */ new Set();
21109
+ seenByAccount.set(accountId, s);
21110
+ }
21111
+ return s;
21112
+ }
21113
+ /**
21114
+ * 飞书凭据就绪判据:**appId 与 appSecret 都非空**。
21115
+ * (微信只需 token;飞书是双要素,缺任一起不了长连接。)
21116
+ */
21117
+ function feishuCredentialReady(cred) {
21118
+ const appId = typeof cred.appId === "string" ? cred.appId.trim() : "";
21119
+ const secret = typeof cred.token === "string" ? cred.token.trim() : "";
21120
+ return appId !== "" && secret !== "";
21121
+ }
21122
+ /**
21123
+ * 从飞书 `im.message.receive_v1` 事件里取出**中立入站事件**。
21124
+ *
21125
+ * 🔴 这是 P3 唯一的"形状翻译"处,与 `bridge.ts` 里 iLink 的那一处对称。
21126
+ * ⚠️ 用户标识用 **open_id**(不是 chat_id):路由表是"人 → 轨迹",
21127
+ * 而 chat_id 在群里会变、在单聊里也与人不是一一对应。
21128
+ * @returns 无可用发送者/消息体 ⇒ `null`(调用方丢弃并**出声**,不静默)
21129
+ */
21130
+ function feishuEventToInbound(data) {
21131
+ const sender = data.sender;
21132
+ const message = data.message;
21133
+ const userId = sender?.sender_id?.open_id;
21134
+ if (!userId || !message?.message_id) return null;
21135
+ let text = null;
21136
+ if (message.message_type === "text" && typeof message.content === "string") try {
21137
+ const parsed = JSON.parse(message.content);
21138
+ text = (typeof parsed.text === "string" ? parsed.text.trim() : "").replace(/@_user_\d+/g, "").trim();
21139
+ if (text === "") text = null;
21140
+ } catch {
21141
+ text = null;
21142
+ }
21143
+ return {
21144
+ userId,
21145
+ text,
21146
+ media: [],
21147
+ hasVoice: false,
21148
+ replyRef: void 0
21149
+ };
21150
+ }
21151
+ /**
21152
+ * 造出站动作面(飞书)。
21153
+ *
21154
+ * 🔴 `content` 必须是 **JSON 字符串**:`JSON.stringify({ text })` —— 这是官方形态,
21155
+ * 直接给裸文本会被服务端拒绝(且错误码不直观)。
21156
+ */
21157
+ function feishuIo(client) {
21158
+ const c = client;
21159
+ return { sendText: async ({ toUserId, text }) => {
21160
+ const create = c.im?.v1?.message?.create;
21161
+ if (typeof create !== "function") throw new Error("飞书 SDK 缺少 im.v1.message.create");
21162
+ await create({
21163
+ params: { receive_id_type: "open_id" },
21164
+ data: {
21165
+ receive_id: toUserId,
21166
+ msg_type: "text",
21167
+ content: JSON.stringify({ text: markdownToPlainText(text) })
21168
+ }
21169
+ });
21170
+ } };
21171
+ }
21172
+ /**
21173
+ * 单账号的飞书长连接循环。
21174
+ *
21175
+ * 与微信的**关键结构差别**:微信是 `while` 轮询,飞书是**事件回调**(长连接推)。
21176
+ * 所以这里的"循环"其实是"建连一次,然后等回调";`stopped` 靠 `close()` 兑现。
21177
+ */
21178
+ async function runFeishuAccountLoop(input, loop) {
21179
+ const cred = input.cred;
21180
+ const appId = cred.appId ?? "";
21181
+ const appSecret = cred.token ?? "";
21182
+ if (!appId || !appSecret) {
21183
+ loop.lastError = "飞书凭据不全(缺 appId 或 appSecret)";
21184
+ return;
21185
+ }
21186
+ const Lark = await loadFeishuSdk();
21187
+ const client = new Lark.Client({
21188
+ appId,
21189
+ appSecret
21190
+ });
21191
+ const dispatcher = new Lark.EventDispatcher({});
21192
+ const queue = [];
21193
+ let draining = false;
21194
+ const drain = async () => {
21195
+ if (draining) return;
21196
+ draining = true;
21197
+ try {
21198
+ while (queue.length > 0 && !loop.stopped) {
21199
+ const item = queue.shift();
21200
+ loop.lastPollAt = Date.now();
21201
+ try {
21202
+ const event = feishuEventToInbound(item.data);
21203
+ if (!event) {
21204
+ loop.lastError = "收到飞书事件但取不出发送者/消息体";
21205
+ continue;
21206
+ }
21207
+ const msgId = item.data.message?.message_id ?? "";
21208
+ if (msgId !== "") {
21209
+ const seen = seenSet(input.accountId);
21210
+ if (seen.has(msgId)) continue;
21211
+ seen.add(msgId);
21212
+ if (seen.size > SEEN_LIMIT) {
21213
+ const oldest = seen.values().next().value;
21214
+ if (oldest !== void 0) seen.delete(oldest);
21215
+ }
21216
+ }
21217
+ const ctx = input.ctx;
21218
+ const io = feishuIo(client);
21219
+ if (input.machine) {
21220
+ const target = await resolveInboundTarget(ctx, {
21221
+ account: input.accountId,
21222
+ user: event.userId,
21223
+ cccFallback: null
21224
+ });
21225
+ if (!target) continue;
21226
+ await handleInboundEvent(ctx, target.root, input.accountId, io, event, { target });
21227
+ } else await handleInboundEvent(ctx, input.root, input.accountId, io, event);
21228
+ } catch (err) {
21229
+ loop.lastError = err instanceof Error ? err.message : String(err);
21230
+ }
21231
+ }
21232
+ } finally {
21233
+ draining = false;
21234
+ }
21235
+ };
21236
+ dispatcher.register({ "im.message.receive_v1": (data) => {
21237
+ queue.push({ data });
21238
+ drain();
21239
+ } });
21240
+ const ws = new Lark.WSClient({
21241
+ appId,
21242
+ appSecret
21243
+ });
21244
+ wsByAccount.set(input.accountId, ws);
21245
+ try {
21246
+ await ws.start({ eventDispatcher: dispatcher });
21247
+ } catch (err) {
21248
+ loop.lastError = `飞书长连接建立失败: ${err instanceof Error ? err.message : String(err)}`;
21249
+ }
21250
+ }
21251
+ /** 飞书账号实现(登记名 = `feishu`;与配置里的 `channel` 取值一致) */
21252
+ const feishuAccountImpl = {
21253
+ channel: "feishu",
21254
+ isReady: feishuCredentialReady,
21255
+ notReadyReason: () => "**缺 appId 或 appSecret**(未绑定)",
21256
+ startLoop(input) {
21257
+ const loop = {
21258
+ stopped: false,
21259
+ lastPollAt: 0
21260
+ };
21261
+ runFeishuAccountLoop(input, loop);
21262
+ return loop;
21263
+ }
21264
+ };
21265
+ /**
21266
+ * 注册飞书实现(`bridge.ts` 在**模块求值时**调用)。
21267
+ *
21268
+ * ⚠️ 与微信同款教训(续 220 实测 12 例红):注册必须在**模块求值**时发生,
21269
+ * 不能等 `apply` —— 测试与面板会**直接**调 `syncCccBridge` / `syncHumanChannel`。
21270
+ */
21271
+ function installFeishuAccountImpl() {
21272
+ registerImAccountImpl(feishuAccountImpl);
21273
+ }
21274
+ //#endregion
21275
+ //#region src/im/bridge.ts
21276
+ var bridge_exports = /* @__PURE__ */ __exportAll({
21277
+ handleIncoming: () => handleIncoming,
21278
+ handleMachineIncoming: () => handleMachineIncoming,
21279
+ registerWeixinBridge: () => registerWeixinBridge,
21280
+ resolveInboundTarget: () => resolveInboundTarget,
21281
+ resolveWeixinAccount: () => resolveWeixinAccount,
21282
+ sendProactiveText: () => sendProactiveText,
21283
+ stopAllBridges: () => stopAllBridges,
21284
+ stopCccBridge: () => stopCccBridge,
21285
+ syncCccBridge: () => syncCccBridge,
21286
+ syncHumanChannel: () => syncHumanChannel,
21287
+ weixinBridgeStatus: () => weixinBridgeStatus
21288
+ });
21289
+ const bridges = /* @__PURE__ */ new Map();
21290
+ /**
21291
+ * 🔴 **机器级 poller 的"根"哨兵**(P0-3 A)—— 它**不是一个目录**。
21292
+ *
21293
+ * 机器级 poller **不属于任何 CCC**(这正是"上移 ACC 层"的含义:谁轮询不再由 CCC 数量决定)。
21294
+ * 用途有二:① `runAccountLoop` 的 `root` 形参在机器级模式下不被使用,传它以示"无根";
21295
+ * ② 归属表(`im/inbound.ts`)按 root 区分拥有者 ⇒ 机器级用这个哨兵值,
21296
+ * 与任何**真实** CCC 根都不会撞(真根一律是绝对路径)。
21297
+ */
21298
+ const MACHINE_SCOPE = "<machine>";
21299
+ /** 机器级 poller(账号 id → 循环)。与 `bridges` **互斥**:同一时刻只有一种模式在跑。 */
21300
+ const machineLoops = /* @__PURE__ */ new Map();
21301
+ /** typing_ticket 缓存(每 (accountId, fromUserId);getconfig 一次后续复用——对齐参考实现 typingTicketCache) */
21302
+ const typingTicketByUser = /* @__PURE__ */ new Map();
21303
+ function typingCacheKey(accountId, fromUserId) {
21304
+ return `${accountId}|${fromUserId}`;
21305
+ }
21306
+ /** 处理开始:getconfig(无缓存时)→ sendtyping status=1(微信侧显示"正在输入...")。
21307
+ * 任何失败静默(typing 不影响主流程——对齐参考实现 typingCallbacks try/catch 吞错)。 */
21308
+ async function sendTypingStart(cred, accountId, fromUserId, contextToken) {
21309
+ try {
21310
+ const key = typingCacheKey(accountId, fromUserId);
21311
+ let ticket = typingTicketByUser.get(key);
21312
+ if (!ticket) {
21313
+ ticket = (await getConfig({
21314
+ baseUrl: cred.baseUrl,
21315
+ token: cred.token,
21316
+ ilinkUserId: fromUserId,
21317
+ contextToken: contextToken ?? ""
21318
+ })).typing_ticket;
21319
+ if (ticket) typingTicketByUser.set(key, ticket);
21320
+ }
21321
+ if (!ticket) return;
21322
+ await sendTyping({
21323
+ baseUrl: cred.baseUrl,
21324
+ token: cred.token,
21325
+ ilinkUserId: fromUserId,
21326
+ typingTicket: ticket,
21327
+ status: TypingStatus.TYPING
21328
+ });
21329
+ } catch (err) {
21330
+ console.log(`[serenity-hooks] weixin-bridge typing start skipped: ${err instanceof Error ? err.message : String(err)}`);
21331
+ }
21332
+ }
21333
+ /** 处理结束(含异常路径 finally):sendtyping status=0;缓存保留(ticket 可复用)。 */
21334
+ async function sendTypingStop(cred, accountId, fromUserId) {
21335
+ try {
21336
+ const ticket = typingTicketByUser.get(typingCacheKey(accountId, fromUserId));
21337
+ if (!ticket) return;
21338
+ await sendTyping({
21339
+ baseUrl: cred.baseUrl,
21340
+ token: cred.token,
21341
+ ilinkUserId: fromUserId,
21342
+ typingTicket: ticket,
21343
+ status: TypingStatus.CANCEL
21344
+ });
21345
+ } catch (err) {
21346
+ console.log(`[serenity-hooks] weixin-bridge typing stop skipped: ${err instanceof Error ? err.message : String(err)}`);
21424
21347
  }
21425
21348
  }
21426
21349
  /**
21350
+ * 🔴 **2026-10-19(P2)`runAccountLoop` 已迁出本文件** —— 逐字搬到
21351
+ * `im/weixin-account.ts`,成为 `ImAccountImpl` 的一个实现(微信)。
21352
+ *
21353
+ * 为什么搬:装配处原先把渠道写死在这里(`channel !== 'weixin'` ⇒ 跳过)。
21354
+ * owner 定向「飞书和微信一样,只不过是 account 方实现不同」⇒ 起循环这个动作
21355
+ * 归**账号自己的实现**,装配处只按 `channel` 查派发表。
21356
+ * ⚠️ 依赖(`getUpdates` / `handleIncoming` / `handleMachineIncoming`)通过
21357
+ * `installWeixinAccountImpl` **注入**,避免 `bridge ⇄ weixin-account` 互相 import 成环。
21358
+ */
21359
+ /**
21360
+ * 处理单条**微信**消息:把 iLink 形状的 `msg` 归一成**中立入站事件**,
21361
+ * 再交给通道中立的 `handleInboundEvent`(`im/inbound-event.ts`)走主流程。
21362
+ *
21363
+ * ## 🔴 2026-10-19(B1):本函数已退化为**薄适配器**,主流程不在本文件
21364
+ *
21365
+ * 原因:`handleIncoming` 原先同时承担两件不同的事 —— ① "iLink 消息长什么样"
21366
+ * ② "一条已到达的消息该投到哪、怎么答、怎么记账"。接第二家 IM 时只有 ① 需要新写,
21367
+ * 而 ② 应该**被两家共用**。两者混在一个函数里 ⇒ 飞书接进来只能复制一整份主流程。
21368
+ *
21369
+ * ⇒ 主流程搬到 `im/inbound-event.ts`,本函数只做**形状翻译**:
21370
+ * `msg`(iLink)+ `cred`(iLink 凭据)→ `ImInboundEvent` + `ImAccountIo`。
21371
+ *
21372
+ * ## 为什么保留这个签名(而不是让调用方直接调新的主流程)
21373
+ *
21374
+ * 全仓约 50 处测试按 iLink 形状驱动本函数,而 B1 的验收判据是「**微信行为逐字不变**」
21375
+ * (用例数守恒 + 真机一条微信)。保留签名 ⇒ 那 50 处**逐字不动**,判据可复算。
21376
+ *
21377
+ * 会话语义(不变):会话 id 由**轨迹编码**派生 ⇒ 同一条轨迹恒同一条 dsh 会话;
21378
+ * 进程内已有 → 延续;无 → 固定 id resume-or-create。
21379
+ */
21380
+ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
21381
+ const fromUserId = msg.from_user_id;
21382
+ if (!fromUserId) return;
21383
+ await handleInboundEvent(ctx, root, accountId, {
21384
+ sendText: async ({ toUserId, text, replyRef }) => {
21385
+ await sendTextMessage({
21386
+ baseUrl: cred.baseUrl,
21387
+ token: cred.token,
21388
+ toUserId,
21389
+ text,
21390
+ contextToken: replyRef
21391
+ });
21392
+ },
21393
+ typingStart: ({ toUserId, replyRef }) => sendTypingStart(cred, accountId, toUserId, replyRef),
21394
+ typingStop: ({ toUserId }) => sendTypingStop(cred, accountId, toUserId)
21395
+ }, {
21396
+ userId: fromUserId,
21397
+ text: extractWeixinText(msg),
21398
+ media: extractWeixinMedia(msg).map((ref) => ({
21399
+ kind: ref.kind,
21400
+ fileName: ref.fileName,
21401
+ fetch: () => downloadMedia({
21402
+ item: ref.item,
21403
+ mediaType: ref.kind === "image" ? "image_item" : "file_item"
21404
+ }).then((r) => r ? {
21405
+ data: r.data,
21406
+ fileName: r.fileName
21407
+ } : null)
21408
+ })),
21409
+ hasVoice: hasVoiceItem(msg),
21410
+ replyRef: msg.context_token
21411
+ }, preset);
21412
+ }
21413
+ /**
21427
21414
  * **机器级 poller 的单条消息入口**(P0-3 A)。
21428
21415
  *
21429
21416
  * 与 `handleIncoming` 的唯一差别:**目标由机器级订阅表逐条解析,且没有 CCC 级回退**
21430
21417
  * (方案 §3.1:机器级模式下**未命中 = 不投递**,不是"投给默认角色")。
21431
- * 解析出来后把**目标根**当 `root` 交给 `handleIncoming`(并带上 `preset` ⇒ 它不再自行路由、
21432
- * 也不看目标 CCC 的 `weixin.enabled`)。
21433
- *
21434
- * 🔴 为什么"起 poller"与"路由"必须同批(本件即 P0-2 的后半):机器级 poller 收到消息时
21435
- * **不知道它属于哪个 CCC** —— 那个答案是订阅表给的。没有订阅表 ⇒ **能收不能投**。
21418
+ * 解析出来后把**目标根**当 `root` 交给 `handleIncoming`(并带上 `preset`)。
21436
21419
  *
21437
21420
  * 🔵 **导出供测试直接驱动**(与 `handleIncoming` 同款:它俩都是"单条消息的入口",
21438
21421
  * 生产路径 `runAccountLoop` 只是循环壳)。
@@ -21449,6 +21432,35 @@ async function handleMachineIncoming(ctx, accountId, cred, msg) {
21449
21432
  await handleIncoming(ctx, target.root, accountId, cred, msg, { target });
21450
21433
  }
21451
21434
  /**
21435
+ * **按渠道读凭据**(P3 起的单一入口)。
21436
+ *
21437
+ * 为什么必须抽成一个函数:CCC 级桥(`syncCccBridge`)与机器级 poller(`syncMachinePollers`)
21438
+ * 是**两条装配路径**,原先各自写着"怎么从 localstore 取微信 token"。接第二家渠道时若两边各写一份,
21439
+ * 必然分叉(一家改了另一家忘改,且**没有任何机械信号**)。
21440
+ *
21441
+ * 两种取法的**真实差别**(不是风格差异):
21442
+ * - **CCC 级**:凭据在 localstore(`WEIXIN_<A>_TOKEN` / `FEISHU_<A>_APPID|SECRET`)。
21443
+ * - **机器级**:凭据在插件设置行(`account.token`)——那是宿主设置面,不经 localstore。
21444
+ *
21445
+ * ⇒ 本函数只负责**把"渠道"翻译成"该读哪套字段"**,返回统一形状 `ImAccountCredential`。
21446
+ * 微信的 appId 恒 undefined;飞书把 appSecret 装进 `token`(让两边共用同一个就绪判据位置)。
21447
+ */
21448
+ function readImCredentialByChannel(root, accountId, channel) {
21449
+ if (channel === "feishu") {
21450
+ const f = readFeishuCredential(root, accountId);
21451
+ return f ? {
21452
+ appId: f.appId,
21453
+ token: f.appSecret
21454
+ } : null;
21455
+ }
21456
+ const w = readWeixinCredential(root, accountId);
21457
+ return w ? {
21458
+ token: w.token,
21459
+ baseUrl: w.baseUrl,
21460
+ userId: w.userId
21461
+ } : null;
21462
+ }
21463
+ /**
21452
21464
  * 启动/重建某 CCC 的桥:读配置 → 对每个 enabled + 有凭据的账号启动轮询循环。
21453
21465
  * 已存在(配置变化热重建)→ 先停旧循环再启动新的。
21454
21466
  *
@@ -21481,7 +21493,7 @@ function syncCccBridge(ctx, root) {
21481
21493
  }
21482
21494
  continue;
21483
21495
  }
21484
- const cred = readWeixinCredential(root, account.accountId);
21496
+ const cred = readImCredentialByChannel(root, account.accountId, channel);
21485
21497
  if (!cred) continue;
21486
21498
  if (!impl.isReady(cred)) continue;
21487
21499
  implByAccount.set(account.accountId, impl);
@@ -21489,7 +21501,7 @@ function syncCccBridge(ctx, root) {
21489
21501
  requests.push({
21490
21502
  root,
21491
21503
  accountId: account.accountId,
21492
- identity: accountIdentity(root, account.accountId, cred.token)
21504
+ identity: accountIdentity(root, account.accountId, cred.token ?? "")
21493
21505
  });
21494
21506
  }
21495
21507
  const plan = planPollerClaims(requests, pollerOwners());
@@ -21579,10 +21591,12 @@ function syncMachinePollers(ctx, hc) {
21579
21591
  continue;
21580
21592
  }
21581
21593
  const token = (account.token ?? "").trim();
21594
+ const appIdRaw = account.appId;
21582
21595
  const cred = {
21583
21596
  token,
21584
21597
  baseUrl: "",
21585
- userId: account.userId
21598
+ userId: account.userId,
21599
+ ...channel === "feishu" && typeof appIdRaw === "string" ? { appId: appIdRaw.trim() } : {}
21586
21600
  };
21587
21601
  if (!impl.isReady(cred)) {
21588
21602
  console.warn(`[serenity-hooks] ⚠ human-channel: 账号 ${id} ${impl.notReadyReason?.(cred) ?? "**凭据不全**"} ⇒ 不起 poller`);
@@ -21642,7 +21656,7 @@ function syncHumanChannel(ctx) {
21642
21656
  (async () => {
21643
21657
  try {
21644
21658
  stopMachineLoops();
21645
- const { listCccs } = await import("./ccc-roots-jZYR2GVE.js").then((n) => n.a);
21659
+ const { listCccs } = await import("./ccc-roots-B2t_aM8L.js").then((n) => n.a);
21646
21660
  for (const entry of await listCccs(ctx)) syncCccBridge(ctx, entry.root);
21647
21661
  } catch {}
21648
21662
  })();
@@ -21782,6 +21796,14 @@ installWeixinAccountImpl({
21782
21796
  handleIncoming,
21783
21797
  handleMachineIncoming
21784
21798
  });
21799
+ /**
21800
+ * 🆕 **P3 装配:飞书实现登记进同一张派发表**(同样是模块求值时)。
21801
+ *
21802
+ * 飞书不需要注入依赖 —— 它的收(`WSClient`)与发(`im.v1.message.create`)都在 SDK 里,
21803
+ * 主流程则复用 `im/inbound-event.ts`。⇒ 这一行就是"微信与飞书成为并列实现"的**最后一块**:
21804
+ * 派发表有了两个条目,装配处一行没改(它只按 `channel` 查表)。
21805
+ */
21806
+ installFeishuAccountImpl();
21785
21807
  //#endregion
21786
21808
  //#region src/im/send.ts
21787
21809
  /** 请求体上限(文本消息远小于此;防滥用) */
@@ -21898,7 +21920,7 @@ function matchCcc(input, candidates) {
21898
21920
  }
21899
21921
  /** 组装候选列表(`ccc-roots.listCccs` 投影 + `.serenity` 名;动态 import 保持本模块静态依赖轻量) */
21900
21922
  async function collectCandidates(ctx) {
21901
- const { listCccs } = await import("./ccc-roots-jZYR2GVE.js").then((n) => n.a);
21923
+ const { listCccs } = await import("./ccc-roots-B2t_aM8L.js").then((n) => n.a);
21902
21924
  const entries = await listCccs(ctx, { defaultRoot: process.cwd() });
21903
21925
  const seen = /* @__PURE__ */ new Set();
21904
21926
  const out = [];
@@ -22118,7 +22140,7 @@ function sessionIdOf(value) {
22118
22140
  /** 单会话内存态清理(幂等;无对应状态即 no-op) */
22119
22141
  function cleanupSessionState(sessionId) {
22120
22142
  try {
22121
- unregisterSkiffSession(sessionId);
22143
+ unregisterTrajectorySession(sessionId);
22122
22144
  } catch {}
22123
22145
  try {
22124
22146
  clearActiveSessionInfo(sessionId);
@@ -22139,7 +22161,7 @@ function cleanupSessionState(sessionId) {
22139
22161
  forgetHostScheduleToolHiding(sessionId);
22140
22162
  } catch {}
22141
22163
  try {
22142
- forgetSkiffRoleToolVisibility(sessionId);
22164
+ forgetTrajectoryConstraintToolVisibility(sessionId);
22143
22165
  } catch {}
22144
22166
  try {
22145
22167
  forgetLogbookCompactionState(sessionId);
@@ -22170,7 +22192,7 @@ function registerLifecycle(ctx) {
22170
22192
  stopAllBridges();
22171
22193
  } catch {}
22172
22194
  };
22173
- registerDisposer(ctx, "self-started resources (gateway/weixin/skiff-debug faces + weixin bridges)", disposeAll);
22195
+ registerDisposer(ctx, "self-started resources (gateway/weixin/trajectory-debug faces + weixin bridges)", disposeAll);
22174
22196
  }
22175
22197
  //#endregion
22176
22198
  //#region src/patches/web-fetch.ts
@@ -22764,7 +22786,7 @@ async function applyOpencodeAutoConfigOnce(ctx, env) {
22764
22786
  *
22765
22787
  * 为什么要重试:`llm-pi-ai` 只在**它自己的** `ctx.inject(['settings'])` 回调里注册命名空间,
22766
22788
  * 与本插件 apply **无先后保证**(两者都依赖 settings 服务)。首次读取拿到 undefined 是常态而非错误
22767
- * (与 skiff 的 CCC root 重试同款问题、同款处置:v1.30.13 实证"只在 apply 时试一次 → 永不重试")。
22789
+ * (与 轨迹约束的 CCC root 重试同款问题、同款处置:v1.30.13 实证"只在 apply 时试一次 → 永不重试")。
22768
22790
  */
22769
22791
  const NAMESPACE_RETRY_DELAYS_MS$1 = [
22770
22792
  1e3,
@@ -22776,7 +22798,7 @@ const NAMESPACE_RETRY_DELAYS_MS$1 = [
22776
22798
  /**
22777
22799
  * 装配 opencode 路由自动配置。
22778
22800
  *
22779
- * 触发面(三层,与 skiff 同款;R↓:单点触发在同装载竞态下必然漏):
22801
+ * 触发面(三层,与 轨迹约束 同款;R↓:单点触发在同装载竞态下必然漏):
22780
22802
  * ① apply 时立即试一次
22781
22803
  * ② 命名空间尚未注册 → 退避重试(1s/3s/8s/20s/40s)
22782
22804
  * ③ `settings/updated` → 再试(用户热改 settings.yaml 加路由时自愈;自身写入触发的复评是幂等空转)
@@ -24371,7 +24393,7 @@ function renderDsl(model) {
24371
24393
  * 🔴 **为什么 PNG 是必需的**:宿主的对话内图片通路**白名单只有光栅格式**
24372
24394
  * (`image/png|jpeg|webp|gif`,见 `dsh-attachment/types.d.ts`)⇒ **SVG 进不去**。
24373
24395
  *
24374
- * 🔴 **开关**:由 `index.ts` 门控(**关时不注册** ⇒ 工具根本不进模型工具清单),同 skiff 口径。
24396
+ * 🔴 **开关**:由 `index.ts` 门控(**关时不注册** ⇒ 工具根本不进模型工具清单),同 轨迹约束 口径。
24375
24397
  *
24376
24398
  * ## 🔴 两条**宿主契约**(2026-09-29 现场取证;第 ① 条是本文件第一版写错的地方)
24377
24399
  *
@@ -24741,10 +24763,6 @@ const Config = z.object({
24741
24763
  opencodeSkills: z.boolean().default(true),
24742
24764
  gateway: z.object({ enabled: z.boolean().default(false) }),
24743
24765
  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
24766
  webFetch: z.object({ enabled: z.boolean().default(true) }),
24749
24767
  opencodeProvider: z.object({ autoConfigure: z.boolean().default(true) }),
24750
24768
  visionPatch: z.object({ enabled: z.boolean().default(true) }),
@@ -24757,8 +24775,6 @@ const Config = z.object({
24757
24775
  }),
24758
24776
  gatewayEnabled: z.boolean().volatile(),
24759
24777
  rebuildThresholdK: z.number().min(50).max(4e3).volatile(),
24760
- skiffEnabled: z.boolean().volatile(),
24761
- skiffDebugPort: z.number().min(1024).max(65535).volatile(),
24762
24778
  croEnabled: z.boolean().volatile(),
24763
24779
  diagramEnabled: z.boolean().volatile()
24764
24780
  });
@@ -24825,7 +24841,6 @@ function apply(ctx, config) {
24825
24841
  if (config.env) registerEnv(ctx);
24826
24842
  if (config.opencodeSkills) registerOpencodeSkills(ctx);
24827
24843
  registerBootstrap(ctx);
24828
- registerSkiff(ctx);
24829
24844
  registerCroTurnTracking(ctx);
24830
24845
  registerWakeScheduler(ctx);
24831
24846
  registerWeixinBridge(ctx);
@@ -24849,7 +24864,7 @@ function apply(ctx, config) {
24849
24864
  * ① 听宿主的**真事件** `settings/document-updated`(ns = 我们那张设置页)——顺带说明为什么不用
24850
24865
  * 我们自己的 `serenity/settings-changed`:那个事件**全仓没有任何 `emit` 点**(只有 `ctx.on` 侧
24851
24866
  * 在用)=**死通道**,照着它接线等于接了个永远不响的铃;
24852
- * ② 兜底再挂 `agent/created` / `session/created`(与 skiff 同款做法,那两条在实测里确实会 fire)
24867
+ * ② 兜底再挂 `agent/created` / `session/created`(与 轨迹约束 同款做法,那两条在实测里确实会 fire)
24853
24868
  * —— 宿主事件面若换名,至少还有这一条能自愈;
24854
24869
  * ③ 启动时同步一次(apply 时按当前配置定初始态)。
24855
24870
  * 🔴 **关的时候要注销**(不是"留着但不响应"):与"关时不注册"同一口径 —— 工具不出现在模型清单里。
@@ -24887,139 +24902,5 @@ function registerDiagramTool(ctx) {
24887
24902
  } catch {}
24888
24903
  sync();
24889
24904
  }
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
24905
  //#endregion
25025
24906
  export { Config, apply, humanChannelAccountSchema, humanChannelSubscriptionSchema, inject, name };