@shgroup/dsh-serenity-hooks 1.85.0 → 1.86.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.
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-serenity-hooks",
3
- "version": "1.85.0",
3
+ "version": "1.86.0",
4
4
  "main": "lib/index.js",
5
5
  "description": "宁静号 ACC harness(Native Cordis 插件):给 DSH 装一个「AI 工作区」——14 个工具(container_fs/cognitive_trajectory/dashboard/container_git/msm/praxis/ontology/serenity-skill-creator/cheap-worker/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫)+ 认知轨迹日志与原地重建 + 网页登录入口/IM 桥/认知轨迹唤醒注册表。轨迹声明 = 轨迹目录下的 trajectory.json(约束面 + 旧标识 uid + CRO 机器绑定 cro.host;旧名 trajectory-constraint.json / skiff.json 仍在读取族里);轨迹提示词 = 轨迹目录下的 PROMPT.md(一条轨迹「是谁、按什么章程工作」,所有轨迹通用,追加在容器层与质量层之后,不替换它们)。适配 DSH 0.1.7-rc.1 / 0.2.0-rc.1 / 0.2.1-rc.1(三档,deepseek-ai/deepseek-harness)。",
6
6
  "engines": {
@@ -227,7 +227,7 @@ async function listCccs(ctx, opts = {}) {
227
227
  if (typeof defaultRoot === "string" && defaultRoot !== "" && !c.roots.includes(defaultRoot)) c.roots.unshift(defaultRoot);
228
228
  let rolesOf = null;
229
229
  if (opts.withRoles === true) {
230
- const { listTrajectoryConstraints } = await import("./profile-DnLSb7yv.js").then((n) => n.i);
230
+ const { listTrajectoryConstraints } = await import("./profile-vHheCJiU.js").then((n) => n.a);
231
231
  rolesOf = (root) => {
232
232
  try {
233
233
  return listTrajectoryConstraints(root).map((p) => p.code);
@@ -87,6 +87,39 @@ export interface ImChannel {
87
87
  }>;
88
88
  /** 通道健康快照(`status` 动作)——必须可 JSON 化(工具返回值契约) */
89
89
  status(root: string): ImJson;
90
+ /**
91
+ * 列出**与这条 CCC 有过往来的对端用户**(`contacts` 动作,2026-10-22 新增)。
92
+ *
93
+ * 🔴 **它存在的理由**(owner 原话:「配路由需要的是账户到轨迹,轨迹 CCC 知道,
94
+ * 账户 CCC 不知道,需要有个能力让 CCC 查询用户配好的账户」):
95
+ * 路由要写 `{ user: "<通道用户 id>", trajectory: "<轨迹编码>" }`。轨迹编码 CCC 知道,
96
+ * **但"对方是谁"它不知道** —— 那个 id 只存在于聊天记录里。
97
+ * ⇒ 没有本动作,CCC 配路由时只能去翻日志或猜。
98
+ *
99
+ * ⚠️ **可选**:某通道若没有可扫的记录来源(例如未实现记录落盘),不实现即可,
100
+ * 工具会如实返回 `ACTION_UNSUPPORTED` —— 比返回空数组好(空数组会被读成"确实没有人")。
101
+ */
102
+ listContacts?(root: string): ImContact[];
103
+ }
104
+ /**
105
+ * 一个"有过往来的对端"(`contacts` 动作的条目)。
106
+ *
107
+ * 字段刻意**全部可 JSON 化**(工具返回值契约),且不引用内部类型。
108
+ */
109
+ export interface ImContact {
110
+ /** 该对端属于哪个渠道(`weixin` / `feishu`) */
111
+ channel: string;
112
+ /** 对端在该通道内的稳定 id(微信 `xxx@im.wechat` / 飞书 `ou_…`) */
113
+ userId: string;
114
+ /** 收发过它的账号(本地唯一键,如 `wechat-1`)—— 多账号 CCC 靠它区分 */
115
+ accountIds: string[];
116
+ /** 当前路由到的轨迹编码;未配路由 ⇒ `null` */
117
+ route: string | null;
118
+ /** 首次/最近出现时间(ISO8601;取不到为 `null`) */
119
+ firstSeen: string | null;
120
+ lastSeen: string | null;
121
+ /** 见过的消息条数 */
122
+ messages: number;
90
123
  }
91
124
  /** 注册通道(index.ts apply 调用;重复 id 覆盖——测试与热重载友好) */
92
125
  export declare function registerImChannel(channel: ImChannel): void;
package/lib/im/hook.d.ts CHANGED
@@ -27,6 +27,19 @@ interface WeixinHookEventBase {
27
27
  ts: number;
28
28
  /** 触发 CCC 根(多 CCC 一进程可区分) */
29
29
  cccRoot: string;
30
+ /**
31
+ * 渠道 id(`weixin` / `feishu`)。
32
+ *
33
+ * 🔴 2026-10-22 新增(owner:「一个 CCC 绑定多个账户很常见,聊天记录落盘要完善」)。
34
+ * 此前**没有这个字段** ⇒ 三个后果:① 飞书事件落进同一份 jsonl 后**无法区分渠道**
35
+ * (只能靠 `ou_` / `@im.wechat` 的形状猜 = D167 违反:认族不认名);② 一个 CCC 配多个
36
+ * 账号时**不知是哪个渠道**收发的(`accountId` 是本地命名习惯,不携带渠道信息);
37
+ * ③ 按渠道分组统计在字段层就做不到。
38
+ *
39
+ * ⚠️ **向后兼容**:存量事件没有它 ⇒ 读侧按 `accountId → im.accounts[].channel`
40
+ * 回填,仍取不到则按 `weixin`(那是本字段存在之前唯一的渠道)。
41
+ */
42
+ channel: string;
30
43
  /** 接收账号(incoming)/ 发送账号(outgoing) */
31
44
  accountId: string;
32
45
  /** 对端用户(from_user_id,形如 userA@im.wechat) */
@@ -71,6 +84,8 @@ type WeixinHookEvent = WeixinHookIncomingEvent | WeixinHookOutgoingEvent;
71
84
  /** incoming 事件构造输入(bridge 提供实参) */
72
85
  interface IncomingHookInput {
73
86
  cccRoot: string;
87
+ /** 渠道 id(`weixin` / `feishu`);缺省 `weixin`(存量调用点零改动) */
88
+ channel?: string;
74
89
  accountId: string;
75
90
  userId: string;
76
91
  sessionId: string;
@@ -81,6 +96,8 @@ interface IncomingHookInput {
81
96
  /** outgoing 事件构造输入(bridge 提供实参) */
82
97
  interface OutgoingHookInput {
83
98
  cccRoot: string;
99
+ /** 渠道 id(`weixin` / `feishu`);缺省 `weixin` */
100
+ channel?: string;
84
101
  accountId: string;
85
102
  userId: string;
86
103
  sessionId: string;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * im/log-scan.ts — 读聊天记录落盘,汇总出「与谁有过往来」(`contacts` 的数据源)
3
+ *
4
+ * ## 这个文件解决什么
5
+ *
6
+ * owner 2026-10-22:「配路由需要的是**账户到轨迹**,轨迹 CCC 知道,**账户 CCC 不知道**,
7
+ * 需要有个能力让 CCC 查询用户配好的账户」。这里的"账户"= **通道用户**(对方是谁),
8
+ * 不是 IM 账号(我们自己登录的身份,`status` 早就能查)。
9
+ *
10
+ * 那个 id(微信 `xxx@im.wechat` / 飞书 `ou_…`)不在任何配置文件里 ——
11
+ * 它**只在聊天记录里出现过**。⇒ 本文件把记录读成联系人表。
12
+ *
13
+ * ## 🔴 两条硬边界
14
+ *
15
+ * 1. **只读**:本文件不写任何东西。真相源仍是那些 jsonl(append-only 事件流),
16
+ * 本文件只是它的一次投影 —— ⛔ 不产生第二真相源。
17
+ * 2. **不猜渠道**:事件的 `channel` 字段为准;缺它(存量事件)⇒ 按
18
+ * `accountId → im.accounts[].channel` 回填,仍取不到才落 `weixin`
19
+ * (那是该字段存在之前唯一的渠道)。⛔ **绝不按 userId 的形状猜**
20
+ * (`ou_` 前缀 / `@im.wechat` 后缀都是命名习惯,不是判据 —— D167 认族不认名)。
21
+ */
22
+ /** 日志根目录族(**新旧两代并存**:`_im-logs/<channel>/` 与存量 `_weixin-logs/`) */
23
+ export declare const IM_LOG_DIR_NEW = "_im-logs";
24
+ export declare const IM_LOG_DIR_LEGACY = "_weixin-logs";
25
+ /** 汇总结果(可 JSON 化) */
26
+ export interface ScannedContact {
27
+ channel: string;
28
+ userId: string;
29
+ accountIds: string[];
30
+ /** 事件里记录的路由目标(`role` 字段;它是事件发生当时的轨迹编码,未必等于**当前**路由) */
31
+ roleAtEvent: string | null;
32
+ firstSeen: number | null;
33
+ lastSeen: number | null;
34
+ messages: number;
35
+ }
36
+ /** 扫描结果(含**扫描域**——否定性结论必须附它) */
37
+ export interface ScanResult {
38
+ contacts: ScannedContact[];
39
+ /** 实际读过的文件(相对 CCC 根) */
40
+ sources: string[];
41
+ /** 读失败的文件及其原因(不静默) */
42
+ errors: string[];
43
+ }
44
+ /** 某 CCC 下**存在**的日志目录(相对根),新代在前 */
45
+ export declare function imLogRoots(root: string): string[];
46
+ /**
47
+ * 扫描本 CCC 的全部聊天记录,按 `(channel, userId)` 汇总。
48
+ *
49
+ * @param root CCC 根
50
+ * @param accountChannel 存量事件缺 `channel` 时的回填表(`accountId → channel`)
51
+ * @returns 汇总结果 + **扫描域** + 读取错误(三者都不可省)
52
+ */
53
+ export declare function scanImContacts(root: string, accountChannel?: Record<string, string>): ScanResult;
package/lib/im/route.d.ts CHANGED
@@ -64,8 +64,15 @@ export declare function readImSettings(root: string, paths?: string[]): ImSettin
64
64
  * ⚠️ 2026-10-19 起它是 {@link readImSettings} 的**薄别名**(签名与返回形状不变):
65
65
  * 旧调用点无需改动,但**配置来源已经统一**——`im` 段优先、`weixin` 段回退。
66
66
  * 保留名字是为了让既有调用点与测试**逐字不动**(P1 是纯重构,读数必须不变)。
67
+ *
68
+ * 🔴 2026-10-22:**返回类型由 `WeixinSettings` 改为 `ImSettings`**。
69
+ * 此前实现返回的就是 `readImSettings(...)`(已是归一形态:`accounts[].channel` 必填),
70
+ * 却把它断言成旧类型 —— 于是类型层"看不见"`channel` 字段,
71
+ * 调用方写 `a.channel` 会报 TS2339,而**运行时它一直在那儿**。
72
+ * ⇒ 那是**类型声明在撒谎**,不是调用方写错。改成真实类型后,多账户/多渠道的读侧才敢用它。
73
+ * ⚠️ 附带效应:`WeixinSettings` 的旧形状调用方若用到旧字段,会在此处暴露(好事,早暴露)。
67
74
  */
68
- export declare function readWeixinSettings(root: string, paths?: string[]): WeixinSettings;
75
+ export declare function readWeixinSettings(root: string, paths?: string[]): ImSettings;
69
76
  /** 账号凭据(localstore credential scope;baseUrl 缺省 = 官方默认) */
70
77
  export interface WeixinAccountCredential {
71
78
  token: string;
package/lib/index.js CHANGED
@@ -1,14 +1,14 @@
1
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-VCJ3aYJA.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-BI_4UVcj.js";
3
3
  import { _ as resolveInside, a as findSerenityRoot$1, c as matchBlacklist, d as readCccName$1, f as readCheapWorkerConfig, h as readSafeModeHiddenTools, i as findGitRoot, l as pathInside, m as readExclusiveTools, n as SAFE_MODE_MARKER, o as isSafeModeOn, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-CyYJL4F2.js";
4
- import { A as sessionEvents, B as isoLocal, C as listSessions, E as parseTrajectoryCode, F as trajectoryDescOf, H as localHuman, I as useSession, L as TRAJECTORY_FILENAME, M as setActiveSessionInfo, N as showSession, O as readTrajectoryUid, P as summarize, R as TRAJECTORY_LEGACY_FILENAMES, S as listActiveTrajectories, T as parseSessionContextFromEvents, U as localIdStamp, V as localFileStamp, _ as createSession, a as readTrajectoryConstraintAt, b as getActiveSessionInfo, c as trajectoryConstraintForSession, f as COGNITIVE_TRAJECTORY_ACTIONS, g as clearActiveSessionInfo, h as archiveSession, j as sessionsRoot, k as resolveSessionByTitle, m as UID_FILENAME, n as isTrajectoryConstrained, o as readTrajectoryConstraintByCode, r as listTrajectoryConstraints, s as replacesDefaultIdentity, t as TRAJECTORY_CONSTRAINT_FILENAME, v as extractSessionMdPathFromText, w as namingTitleFor, y as findSession, z as readTrajectoryFile } from "./profile-DnLSb7yv.js";
4
+ import { A as resolveSessionByTitle, B as readTrajectoryFile, C as listActiveTrajectories, D as parseTrajectoryCode, E as parseSessionContextFromEvents, F as summarize, H as localFileStamp, I as trajectoryDescOf, L as useSession, M as sessionsRoot, N as setActiveSessionInfo, P as showSession, R as TRAJECTORY_FILENAME, T as namingTitleFor, U as localHuman, V as isoLocal, W as localIdStamp, _ as clearActiveSessionInfo, b as findSession, c as replacesDefaultIdentity, g as archiveSession, h as UID_FILENAME, i as listTrajectoryConstraints, j as sessionEvents, k as readTrajectoryUid, l as trajectoryConstraintForSession, n as findDirNameByCode, o as readTrajectoryConstraintAt, p as COGNITIVE_TRAJECTORY_ACTIONS, r as isTrajectoryConstrained, s as readTrajectoryConstraintByCode, t as TRAJECTORY_CONSTRAINT_FILENAME, v as createSession, w as listSessions, x as getActiveSessionInfo, y as extractSessionMdPathFromText, z as TRAJECTORY_LEGACY_FILENAMES } from "./profile-vHheCJiU.js";
5
5
  import { n as checkLocalstoreGitCompliance, o as readStore, s as runLocalStore, t as LOCALSTORE_SCOPES } from "./service-DDmHSglV.js";
6
- import { _ as resolveConstraintSystemPrompt, a as listBoundSessionIds, d as constraintCognitiveTrajectory, f as constraintSystemPromptSource, g as profileWriteWhitelist, h as profileToolWhitelist, i as carrierState, l as TRAJECTORY_CONSTRAINT_HINT, n as PLUGIN_SOURCE, o as readLastBound, r as appendBound, s as resolveSessionCognitiveTrajectoryLabel, t as ACC_MESSAGE_KIND, v as ACC_VERSION, y as findPackageRoot } from "./message-source-D7dVNDIh.js";
7
- import { A as buildResourceIndex, C as verifyPassword, D as recordToolUsage, E as verifyTotpCode, F as hasTrajectoryPrompt, M as isSafeSkillName, N as truncateContent, O as REFERENCE_DIR, P as TRAJECTORY_PROMPT_FILENAME, S as toWire, T as MECHANISM_PORTS, _ as registerEntrySkillSectionGlobal, a as trajectoryConstraintFor, b as projectKnownWorkspaces, c as trajectoryWorkspaceMdPath, d as CHEAP_WORKER_GUIDE, f as providerAvailabilityError, g as registerEntrySkillSection, h as registerCognitiveTrajectorySkillSection, i as getTrajectoryAgent, j as buildResourceNameSuffix, k as SKILL_ONTOLOGY_FILE, l as unregisterTrajectorySession, m as splitModel, n as createTrajectoryAgent, o as trajectoryConstraintMsmGate, p as requireWhitelistedModel, r as ensureWorkspacePromptSection, s as trajectoryConstraintParticipates, t as askTrajectory, u as workspaceCognitiveTrajectoryLine, v as applyWirePatch, w as FACE_PORTS, x as readAdvancedSettings, y as migrateLegacyLocalstore } from "./session-I6SM2GT6.js";
6
+ import { _ as resolveConstraintSystemPrompt, a as listBoundSessionIds, d as constraintCognitiveTrajectory, f as constraintSystemPromptSource, g as profileWriteWhitelist, h as profileToolWhitelist, i as carrierState, l as TRAJECTORY_CONSTRAINT_HINT, n as PLUGIN_SOURCE, o as readLastBound, r as appendBound, s as resolveSessionCognitiveTrajectoryLabel, t as ACC_MESSAGE_KIND, v as ACC_VERSION, y as findPackageRoot } from "./message-source-DRkQwfgh.js";
7
+ import { A as buildResourceIndex, C as verifyPassword, D as recordToolUsage, E as verifyTotpCode, F as hasTrajectoryPrompt, M as isSafeSkillName, N as truncateContent, O as REFERENCE_DIR, P as TRAJECTORY_PROMPT_FILENAME, S as toWire, T as MECHANISM_PORTS, _ as registerEntrySkillSectionGlobal, a as trajectoryConstraintFor, b as projectKnownWorkspaces, c as trajectoryWorkspaceMdPath, d as CHEAP_WORKER_GUIDE, f as providerAvailabilityError, g as registerEntrySkillSection, h as registerCognitiveTrajectorySkillSection, i as getTrajectoryAgent, j as buildResourceNameSuffix, k as SKILL_ONTOLOGY_FILE, l as unregisterTrajectorySession, m as splitModel, n as createTrajectoryAgent, o as trajectoryConstraintMsmGate, p as requireWhitelistedModel, r as ensureWorkspacePromptSection, s as trajectoryConstraintParticipates, t as askTrajectory, u as workspaceCognitiveTrajectoryLine, v as applyWirePatch, w as FACE_PORTS, x as readAdvancedSettings, y as migrateLegacyLocalstore } from "./session-DZXSLLEx.js";
8
8
  import { n as hasSessionLogById, r as sessionsRootDir } from "./cleanup-C-92qnDs.js";
9
- import { n as readSimpleSettings, r as registerSettingsSection } from "./settings-4ag1jtR0.js";
9
+ import { n as readSimpleSettings, r as registerSettingsSection } from "./settings-H7TVDayZ.js";
10
+ import { a as readFeishuCredential, d as weixinSessionIdForTrajectory, i as matchWeixinRoute, l as sanitizeFileName$1, n as extractWeixinText, o as readWeixinCredential, r as hasVoiceItem, s as readWeixinSettings, t as extractWeixinMedia, u as weixinInboundDir } from "./route-GtYJdR06.js";
10
11
  import { REGISTERED_TOOLS } from "./invariant.js";
11
- import { a as readFeishuCredential, d as weixinSessionIdForTrajectory, i as matchWeixinRoute, l as sanitizeFileName$1, n as extractWeixinText, o as readWeixinCredential, r as hasVoiceItem, s as readWeixinSettings, t as extractWeixinMedia, u as weixinInboundDir } from "./route-Dr0SFD8A.js";
12
12
  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";
13
13
  import { a as registerImAccountImpl, i as normalizeAccountChannel, n as getImAccountImpl, r as imAccountChannels } from "./accounts-BHQbakUh.js";
14
14
  import { createRequire } from "node:module";
@@ -11048,6 +11048,152 @@ const TRAJECTORY_SELF_GUIDE = [
11048
11048
  ⚠️ 以上四项都是**读取当前真实状态**,不是"保存成功"的回执。保存成功 ≠ 生效(本仓反复踩过)。`
11049
11049
  ].join("\n");
11050
11050
  //#endregion
11051
+ //#region src/tools/im-docs.ts
11052
+ /**
11053
+ * tools/im-docs.ts — 「IM 桥怎么用、怎么配路由」的**手册正文**(2026-10-22)
11054
+ *
11055
+ * ## 为什么需要这一章(owner 原话)
11056
+ *
11057
+ * 「我在测试飞书,发现 ACC 没提供 IM 桥使用说明和账户查询方法,**导致 CCC 没办法自己配路由**,
11058
+ * 这个要完善下」+「配路由需要的是**账户到轨迹**,轨迹 CCC 知道,**账户 CCC 不知道**,
11059
+ * 需要有个能力让 CCC 查询用户配好的账户」。
11060
+ *
11061
+ * ⇒ 两件事被同时指出来:**没有说明面**(只能猜配置形状)+ **没有查询面**(拿不到对方 id)。
11062
+ * 后者已由 `im-bridge action=contacts` 兑现;本章补前者。
11063
+ *
11064
+ * ## 🔴 写这一章时守的两条纪律
11065
+ *
11066
+ * 1. **只描述已经建成的东西**(D179):本章出现的每个字段、每个动作,都必须在实现里存在。
11067
+ * 写"打算做"的功能进手册 = 教用户按图施工然后把自己锁死。
11068
+ * 2. **指针从读者真正打开的那个工具发出**(D178):读这一章的人是 **CCC 的 agent**,
11069
+ * 它手边常用的入口是 `im-bridge`(工具描述已指向本章)。⛔ 不能只藏在 container_admin 里。
11070
+ */
11071
+ const IM_BRIDGE_GUIDE = `# IM 桥(im-bridge)—— 怎么用、怎么配、怎么查
11072
+
11073
+ ## 一、它是什么
11074
+
11075
+ 一个 CCC 通过 IM 渠道(微信 / 飞书)跟家人收发消息的能力。
11076
+ **只作用于本容器**——没有"目标容器"参数(跨 CCC 发送不在能力面)。
11077
+
11078
+ 工具在**未配置任何 IM 渠道时根本不可见**(不是"看得到但被拒")。
11079
+ ⇒ 你看到这个工具,说明本 CCC 已配了至少一个渠道。
11080
+
11081
+ ## 二、五个动作
11082
+
11083
+ | 动作 | 用途 | 关键参数 |
11084
+ |---|---|---|
11085
+ | \`send\` | 发一条文本 | \`user\`(对方)+ \`text\` |
11086
+ | \`send-file\` | 发本容器内的一个文件 | \`user\` + \`file\`(**CCC 根相对路径**)+ 可选 \`caption\` |
11087
+ | \`users\` | 已配的**别名**表 | — |
11088
+ | \`status\` | 渠道健康 + **账号** + **当前路由**(含每条路由的目标是否存在) | — |
11089
+ | \`contacts\` | **跟本容器有过往来的人**(含对方 id)+ 未配路由的 / 配了却没往来的 | — |
11090
+
11091
+ ⚠️ 渠道不一定支持全部动作(例如飞书目前不支持 \`send-file\`,会如实返回 \`ACTION_UNSUPPORTED\`)。
11092
+ **如实返回"不支持"比假装支持好**——后者会得到一条"发出去但对方收不到"的静默故障。
11093
+
11094
+ ## 三、🔴 配路由:先 \`contacts\`,再写,再 \`status\` 自检
11095
+
11096
+ 路由的形状(\`.opencode/serenity.json\` 的 \`im.routes\`):
11097
+
11098
+ \`\`\`json
11099
+ {
11100
+ "im": {
11101
+ "enabled": true,
11102
+ "accounts": [
11103
+ { "accountId": "wechat-1", "channel": "weixin", "enabled": true },
11104
+ { "accountId": "feishu-1", "channel": "feishu", "enabled": true }
11105
+ ],
11106
+ "routes": [
11107
+ { "user": "o9cq…@im.wechat", "trajectory": "zhaocai-xingzai" },
11108
+ { "user": "ou_xxxxxxxx", "trajectory": "llm-intel" }
11109
+ ]
11110
+ }
11111
+ }
11112
+ \`\`\`
11113
+
11114
+ - \`user\` = **对方在该渠道内的 id**(不是我们的账号,也不是别名)
11115
+ · 微信:形如 \`xxx@im.wechat\`
11116
+ · 飞书:形如 \`ou_…\`(open_id)
11117
+ - \`trajectory\` = **认知轨迹编码**(目录名里那一段,如 \`S142\` / \`llm-intel\`)
11118
+ - \`*\` 可作为 \`user\` 的**通配兜底**(exact 优先)
11119
+
11120
+ ### 那个 \`user\` id 从哪里来 —— 这是本节存在的原因
11121
+
11122
+ **你不知道对方 id,而它也不在任何配置文件里。** 它只出现在聊天记录中。
11123
+ ⇒ 用 \`im-bridge({channel:"weixin", action:"contacts"})\`,它会给出:
11124
+
11125
+ \`\`\`json
11126
+ {
11127
+ "contacts": [
11128
+ { "channel": "weixin", "userId": "o9cq…@im.wechat", "accountIds": ["wechat-1"],
11129
+ "route": null, "firstSeen": "2026-09-04T…", "lastSeen": "2026-10-22T…", "messages": 1284 }
11130
+ ],
11131
+ "unrouted": ["o9cq…@im.wechat"],
11132
+ "orphanRoutes": ["ou_typo_never_seen"],
11133
+ "sources": ["AGENT_SESSIONS/_im-logs/weixin/2026-10-22.jsonl", "…"]
11134
+ }
11135
+ \`\`\`
11136
+
11137
+ 三行输出**各有用途,缺一行这个动作就只剩"列名字"**:
11138
+
11139
+ | 字段 | 含义 | 你要做什么 |
11140
+ |---|---|---|
11141
+ | \`contacts[].userId\` | 对方 id | **复制它**填进 \`routes[].user\` |
11142
+ | \`unrouted\` | ⚠️ 有往来、但**没配路由** | 这些人的消息现在**无处可去** ⇒ 这就是该配的 |
11143
+ | \`orphanRoutes\` | ❓ 配了路由、但记录里**从没出现过** | 大概率 **id 写错**(或从未有人发过) |
11144
+ | \`sources\` | 实际读过的文件 | **否定性结论的可信度上限** = 这个列表;它空着就别下"没人"的结论 |
11145
+
11146
+ ### 写完怎么自检
11147
+
11148
+ \`im-bridge({channel:"weixin", action:"status"})\` 的 \`routes[]\` 每条都带 **\`targetExists\`**:
11149
+
11150
+ - \`true\` ⇒ 该轨迹目录真实存在,路由**有机会**生效
11151
+ - \`false\` ⇒ **轨迹编码写错了**(或那条轨迹已被归档/删除)
11152
+
11153
+ 🔴 这一格是"配了不生效"的**唯一机械信号**:写错一个编码**不会有任何报错**,
11154
+ 消息进来找不到目标就静默不投递,你只会看到"没回音"。
11155
+
11156
+ ## 四、多账号 / 多渠道
11157
+
11158
+ 一个 CCC 绑多个账号是常态(owner 2026-10-22:「一个 CCC 绑定多个账户很常见」)。
11159
+
11160
+ - **账号**在 \`im.accounts[]\`,每条自带 \`channel\`(缺省 \`weixin\`)
11161
+ - **路由是通道无关的**(\`user → trajectory\`);同一轨迹可以被多个渠道的对端指向
11162
+ - \`contacts\` 的每条带 \`accountIds\`(该对端被哪些账号收过)——**多账号靠它区分**
11163
+ - 发消息时可用 \`account\` 参数指定用哪个账号(缺省:第一个启用且已绑定的)
11164
+
11165
+ ## 五、聊天记录落在哪
11166
+
11167
+ \`\`\`
11168
+ AGENT_SESSIONS/_im-logs/<channel>/<YYYY-MM-DD>.jsonl
11169
+ \`\`\`
11170
+
11171
+ - 按**渠道**分目录(\`weixin\` / \`feishu\`),按**本地自然天**分文件,逐行 JSON 追加
11172
+ - 老路径 \`AGENT_SESSIONS/_weixin-logs/\` 是**同一族的前代**(2026-10-22 之前的微信记录);
11173
+ 读侧**两个都认**,⛔ 但**只往新路径写**
11174
+ - 每条事件带 \`channel\` 字段 ⇒ 飞书事件不会再混进微信目录(此前会,目录名会说谎)
11175
+ - 媒体的持久副本在 \`_im-logs/<channel>/media/<日期>/\`
11176
+
11177
+ ⚠️ **记录归 CCC**(\`im.hook\` 指向本容器的脚本),ACC 不认识存储格式 —— 这层分离是有意的。
11178
+
11179
+ ## 六、出错时
11180
+
11181
+ 工具返回**稳定错误码 + 可行动提示**,常见的:
11182
+
11183
+ | 码 | 意思 | 怎么改 |
11184
+ |---|---|---|
11185
+ | \`CHANNEL_NOT_CONFIGURED\` | 本 CCC 没启用该渠道 | 看提示里的**那一半**(总开关 vs 缺账号,两者改法不同) |
11186
+ | \`ALIAS_UNKNOWN\` | \`user\` 既不是已配别名、也不是合法的裸 id | 先 \`action=users\` 看别名,或用 \`contacts\` 拿裸 id |
11187
+ | \`ACTION_UNSUPPORTED\` | 该渠道没实现这个动作 | 换动作;⛔ 不要重试 |
11188
+ | \`FILE_ESCAPE\` | \`file\` 跑出了 CCC 根 | 用容器内相对路径 |
11189
+
11190
+ ## 七、边界(明确不做)
11191
+
11192
+ - ⛔ 跨 CCC 发送(只作用于本容器)
11193
+ - ⛔ 修改配置(\`contacts\` 只读;加路由要人/CCC 自己写 \`serenity.json\`)
11194
+ - ⛔ 判断"对方读没读"——记录只证明**已发出**
11195
+ `;
11196
+ //#endregion
11051
11197
  //#region src/tools/container-admin.ts
11052
11198
  /**
11053
11199
  * container-admin.ts — container_admin(v1.30:机务舱——容器管理面统一入口)
@@ -11198,6 +11344,11 @@ const DOCS_CHAPTERS = [
11198
11344
  id: "trajectory-constraint",
11199
11345
  summary: "轨迹约束(只讲限制面):trajectory.json 的双白名单 / 模型 / 纪律参与项 / IM 路由",
11200
11346
  render: () => TRAJECTORY_CONSTRAINT_GUIDE
11347
+ },
11348
+ {
11349
+ id: "im-bridge",
11350
+ summary: "IM 桥怎么用、怎么配路由、怎么查对方 id —— send/send-file/users/status/contacts 五个动作 + 路由三步法(contacts → 写 → status 自检)",
11351
+ render: () => IM_BRIDGE_GUIDE
11201
11352
  }
11202
11353
  ];
11203
11354
  /**
@@ -11208,7 +11359,7 @@ const DOCS_CHAPTERS = [
11208
11359
  function createContainerAdminTool(ctx) {
11209
11360
  return defineTool({
11210
11361
  name: "container_admin",
11211
- 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). TRAJECTORY SELF-DOCS: a cognitive trajectory describing ITSELF — charter (PROMPT.md) / resident declaration / self-restriction (whitelists) ⇒ config docs trajectory-self; the restriction side only ⇒ config docs trajectory-constraint. Execution of registered MSMs belongs to the msm tool; this tool manages the container.",
11362
+ 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). TRAJECTORY SELF-DOCS: a cognitive trajectory describing ITSELF — charter (PROMPT.md) / resident declaration / self-restriction (whitelists) ⇒ config docs trajectory-self; the restriction side only ⇒ config docs trajectory-constraint. IM BRIDGE: how to send / configure routes / look up the other party's channel user id — send / send-file / users / status / contacts + the three-step route recipe ⇒ config docs im-bridge. Execution of registered MSMs belongs to the msm tool; this tool manages the container.",
11212
11363
  parameters: {
11213
11364
  domain: {
11214
11365
  type: "string",
@@ -11321,6 +11472,138 @@ function createContainerAdminTool(ctx) {
11321
11472
  });
11322
11473
  }
11323
11474
  //#endregion
11475
+ //#region src/im/log-scan.ts
11476
+ /**
11477
+ * im/log-scan.ts — 读聊天记录落盘,汇总出「与谁有过往来」(`contacts` 的数据源)
11478
+ *
11479
+ * ## 这个文件解决什么
11480
+ *
11481
+ * owner 2026-10-22:「配路由需要的是**账户到轨迹**,轨迹 CCC 知道,**账户 CCC 不知道**,
11482
+ * 需要有个能力让 CCC 查询用户配好的账户」。这里的"账户"= **通道用户**(对方是谁),
11483
+ * 不是 IM 账号(我们自己登录的身份,`status` 早就能查)。
11484
+ *
11485
+ * 那个 id(微信 `xxx@im.wechat` / 飞书 `ou_…`)不在任何配置文件里 ——
11486
+ * 它**只在聊天记录里出现过**。⇒ 本文件把记录读成联系人表。
11487
+ *
11488
+ * ## 🔴 两条硬边界
11489
+ *
11490
+ * 1. **只读**:本文件不写任何东西。真相源仍是那些 jsonl(append-only 事件流),
11491
+ * 本文件只是它的一次投影 —— ⛔ 不产生第二真相源。
11492
+ * 2. **不猜渠道**:事件的 `channel` 字段为准;缺它(存量事件)⇒ 按
11493
+ * `accountId → im.accounts[].channel` 回填,仍取不到才落 `weixin`
11494
+ * (那是该字段存在之前唯一的渠道)。⛔ **绝不按 userId 的形状猜**
11495
+ * (`ou_` 前缀 / `@im.wechat` 后缀都是命名习惯,不是判据 —— D167 认族不认名)。
11496
+ */
11497
+ /** 日志根目录族(**新旧两代并存**:`_im-logs/<channel>/` 与存量 `_weixin-logs/`) */
11498
+ const IM_LOG_DIR_NEW = "_im-logs";
11499
+ const IM_LOG_DIR_LEGACY = "_weixin-logs";
11500
+ /** 某 CCC 下**存在**的日志目录(相对根),新代在前 */
11501
+ function imLogRoots(root) {
11502
+ const out = [];
11503
+ const newBase = join(root, "AGENT_SESSIONS", IM_LOG_DIR_NEW);
11504
+ const legacy = join(root, "AGENT_SESSIONS", IM_LOG_DIR_LEGACY);
11505
+ if (existsSync(newBase)) out.push(join("AGENT_SESSIONS", IM_LOG_DIR_NEW));
11506
+ if (existsSync(legacy)) out.push(join("AGENT_SESSIONS", IM_LOG_DIR_LEGACY));
11507
+ return out;
11508
+ }
11509
+ /** 收集某个日志根下所有 `*.jsonl`(含 `_im-logs/<channel>/` 一层;按文件名排序) */
11510
+ function jsonlFilesIn(root, rel) {
11511
+ const abs = join(root, rel);
11512
+ const out = [];
11513
+ let entries = [];
11514
+ try {
11515
+ entries = readdirSync(abs, { withFileTypes: true }).map((d) => d.name);
11516
+ } catch {
11517
+ return out;
11518
+ }
11519
+ for (const name of entries.sort()) {
11520
+ if (name.startsWith(".")) continue;
11521
+ if (name.endsWith(".jsonl")) {
11522
+ out.push(join(rel, name));
11523
+ continue;
11524
+ }
11525
+ if (rel === join("AGENT_SESSIONS", "_im-logs")) {
11526
+ const sub = join(rel, name);
11527
+ try {
11528
+ for (const child of readdirSync(join(root, sub)).sort()) if (child.endsWith(".jsonl")) out.push(join(sub, child));
11529
+ } catch {}
11530
+ }
11531
+ }
11532
+ return out;
11533
+ }
11534
+ /** 从文件名 / 所在目录猜渠道(**只作为回填的最后一级**,不是判据) */
11535
+ function channelFromPath(rel) {
11536
+ const parts = rel.split("/");
11537
+ const idx = parts.indexOf(IM_LOG_DIR_NEW);
11538
+ const candidate = idx >= 0 && parts.length > idx + 2 ? parts[idx + 1] : void 0;
11539
+ return candidate !== void 0 && candidate !== "" ? candidate : null;
11540
+ }
11541
+ /**
11542
+ * 扫描本 CCC 的全部聊天记录,按 `(channel, userId)` 汇总。
11543
+ *
11544
+ * @param root CCC 根
11545
+ * @param accountChannel 存量事件缺 `channel` 时的回填表(`accountId → channel`)
11546
+ * @returns 汇总结果 + **扫描域** + 读取错误(三者都不可省)
11547
+ */
11548
+ function scanImContacts(root, accountChannel = {}) {
11549
+ const byKey = /* @__PURE__ */ new Map();
11550
+ const sources = [];
11551
+ const errors = [];
11552
+ for (const rel of imLogRoots(root)) for (const fileRel of jsonlFilesIn(root, rel)) {
11553
+ sources.push(fileRel);
11554
+ let text = "";
11555
+ try {
11556
+ text = readFileSync(join(root, fileRel), "utf8");
11557
+ } catch (err) {
11558
+ errors.push(`${fileRel}: ${err instanceof Error ? err.message : String(err)}`);
11559
+ continue;
11560
+ }
11561
+ const pathChannel = channelFromPath(fileRel);
11562
+ for (const line of text.split("\n")) {
11563
+ const trimmed = line.trim();
11564
+ if (trimmed === "") continue;
11565
+ let ev;
11566
+ try {
11567
+ ev = JSON.parse(trimmed);
11568
+ } catch {
11569
+ errors.push(`${fileRel}: 有 1 行不是合法 JSON(已跳过)`);
11570
+ continue;
11571
+ }
11572
+ const userId = typeof ev.userId === "string" ? ev.userId.trim() : "";
11573
+ if (userId === "") continue;
11574
+ const accountId = typeof ev.accountId === "string" ? ev.accountId.trim() : "";
11575
+ const channel = (typeof ev.channel === "string" && ev.channel.trim() !== "" ? ev.channel.trim() : null) ?? pathChannel ?? (accountId !== "" ? accountChannel[accountId] ?? null : null) ?? "weixin";
11576
+ const key = `${channel}\u0000${userId}`;
11577
+ let entry = byKey.get(key);
11578
+ if (!entry) {
11579
+ entry = {
11580
+ channel,
11581
+ userId,
11582
+ accountIds: [],
11583
+ roleAtEvent: null,
11584
+ firstSeen: null,
11585
+ lastSeen: null,
11586
+ messages: 0
11587
+ };
11588
+ byKey.set(key, entry);
11589
+ }
11590
+ if (accountId !== "" && !entry.accountIds.includes(accountId)) entry.accountIds.push(accountId);
11591
+ if (typeof ev.role === "string" && ev.role.trim() !== "") entry.roleAtEvent = ev.role.trim();
11592
+ const ts = typeof ev.ts === "number" && Number.isFinite(ev.ts) ? ev.ts : null;
11593
+ if (ts !== null) {
11594
+ if (entry.firstSeen === null || ts < entry.firstSeen) entry.firstSeen = ts;
11595
+ if (entry.lastSeen === null || ts > entry.lastSeen) entry.lastSeen = ts;
11596
+ }
11597
+ entry.messages += 1;
11598
+ }
11599
+ }
11600
+ return {
11601
+ contacts: [...byKey.values()].sort((a, b) => (b.lastSeen ?? 0) - (a.lastSeen ?? 0)),
11602
+ sources,
11603
+ errors
11604
+ };
11605
+ }
11606
+ //#endregion
11324
11607
  //#region src/im/channels.ts
11325
11608
  /**
11326
11609
  * im/channels.ts — IM 发送能力层(v1.31.0,S142 用户洞察)
@@ -11464,6 +11747,39 @@ async function runImBridge(root, req) {
11464
11747
  action,
11465
11748
  detail: { users: channel.listUsers(root) }
11466
11749
  };
11750
+ if (action === "contacts") {
11751
+ if (!channel.listContacts) return fail("ACTION_UNSUPPORTED", `channel "${channelId}" has no record source for contacts`);
11752
+ const settings = readWeixinSettings(root);
11753
+ const accountChannel = {};
11754
+ for (const account of settings.accounts ?? []) if (typeof account.accountId === "string" && account.accountId !== "") accountChannel[account.accountId] = typeof account.channel === "string" && account.channel !== "" ? account.channel : "weixin";
11755
+ const scanned = scanImContacts(root, accountChannel);
11756
+ const mine = scanned.contacts.filter((c) => c.channel === channelId);
11757
+ /** 当前路由表(`user → trajectory`);同一 user 多条时后者覆盖 */
11758
+ const routeMap = /* @__PURE__ */ new Map();
11759
+ for (const route of settings.routes ?? []) if (typeof route.user === "string" && route.user !== "") routeMap.set(route.user, typeof route.trajectory === "string" ? route.trajectory : "");
11760
+ const contacts = mine.map((c) => ({
11761
+ channel: c.channel,
11762
+ userId: c.userId,
11763
+ accountIds: c.accountIds,
11764
+ route: routeMap.get(c.userId) ?? null,
11765
+ firstSeen: c.firstSeen === null ? null : new Date(c.firstSeen).toISOString(),
11766
+ lastSeen: c.lastSeen === null ? null : new Date(c.lastSeen).toISOString(),
11767
+ messages: c.messages
11768
+ }));
11769
+ const seenUsers = new Set(mine.map((c) => c.userId));
11770
+ return {
11771
+ ok: true,
11772
+ channel: channelId,
11773
+ action,
11774
+ detail: {
11775
+ contacts,
11776
+ unrouted: mine.filter((c) => !routeMap.has(c.userId)).map((c) => c.userId),
11777
+ orphanRoutes: [...routeMap.keys()].filter((user) => !seenUsers.has(user)),
11778
+ sources: scanned.sources,
11779
+ ...scanned.errors.length > 0 ? { errors: scanned.errors } : {}
11780
+ }
11781
+ };
11782
+ }
11467
11783
  if (action === "status") return {
11468
11784
  ok: true,
11469
11785
  channel: channelId,
@@ -13352,11 +13668,65 @@ function accMessage(root, configPaths, entrySkillMaxChars) {
13352
13668
  source: PLUGIN_SOURCE
13353
13669
  });
13354
13670
  }
13355
- /** 每 agent 是否已注入过(进程内存态) */
13671
+ /**
13672
+ * 每 agent 是否已注入过 —— **只作为本进程内的快路径**,⛔ 不是判据。
13673
+ *
13674
+ * 🔴🔴 2026-10-22(owner 报「send-now 创建的会话消息注入两次」的取证结论):
13675
+ * 此前**它就是判据**,于是 `pre-step` 那道门拿它回答"**这条会话**注入过没有"——
13676
+ * 而它实际回答的是"**本进程**注入过没有"。进程一换(`restart-web` / 看门狗拉起 /
13677
+ * 崩溃重启)它就清空 ⇒ 老会话被冷载入时 `pre-step` 判"没注入过" ⇒ **再注入一遍**。
13678
+ *
13679
+ * 实测铁证(同一会话两条 `[ACC] … active` 横幅,相隔 18.6 小时):
13680
+ * 只有**跨过重启**的会话有两条,没跨的只有一条 ⇒ 重复的量 = 跨了几个进程。
13681
+ *
13682
+ * ⇒ 判据改为 {@link hasAccBanner}(读**会话历史**)。本 Set 降级为"同进程免重复扫"的优化:
13683
+ * 命中它就直接返回(省一次扫),⛔ 它为空**不代表**没注入过。
13684
+ */
13356
13685
  const injected = /* @__PURE__ */ new Set();
13357
13686
  function agentKey(agent) {
13358
13687
  return agent.session.id ?? "global";
13359
13688
  }
13689
+ /**
13690
+ * ACC 身份横幅在**会话历史**里的识别特征(结构判据,不依赖措辞顺序)。
13691
+ *
13692
+ * 🔴 为什么认 `source.kind` + 正文前缀两件:
13693
+ * - 只认正文 ⇒ 将来文案一改就静默失效(D151 同族:判据不能长在措辞上);
13694
+ * - 只认 `source.kind` ⇒ 本插件注入的**所有**消息都算(`skill-catalog` 块、假首轮横幅…)
13695
+ * ⇒ 会把"别的注入出现过"误当成"身份横幅出现过",导致**永不再注入**。
13696
+ * ⇒ 两者与:由本插件发出 **且** 正文以 ACC 横幅首词开头。
13697
+ */
13698
+ const ACC_BANNER_PREFIX = "[ACC] Serenity cognitive container active";
13699
+ /**
13700
+ * 这条会话的历史里**是否已经有** ACC 身份横幅(= 判据的唯一权威)。
13701
+ *
13702
+ * 🔴 这是本条修复的核心:**"注入过没有"是会话的属性,不是进程的属性。**
13703
+ * 会话历史随会话走(落盘、重启后仍在),进程内状态随进程死。
13704
+ *
13705
+ * ⚠️ 读不到 events(受限宿主 / 投影未就绪)⇒ 返回 `false`(退回"注入一次")。
13706
+ * 这是**偏保守**的选择:多注入一条横幅(可见、可清理)远好于
13707
+ * **永远不再注入**(陈旧会话里 ACC 身份彻底缺失,且零信号)。
13708
+ *
13709
+ * @param session 目标会话(`sessionEvents` 能读的形状)
13710
+ */
13711
+ function hasAccBanner(session) {
13712
+ return sessionEvents(session).some((event) => {
13713
+ const e = event;
13714
+ if (e.type !== "user/message") return false;
13715
+ if (e.data?.source?.kind !== PLUGIN_SOURCE.kind) return false;
13716
+ return textOfUserMessage(e.data)?.trimStart().startsWith(ACC_BANNER_PREFIX) === true;
13717
+ });
13718
+ }
13719
+ /** 从 `user/message` 的 content 块数组里拼出纯文本(无 text 块 ⇒ undefined) */
13720
+ function textOfUserMessage(data) {
13721
+ const content = data?.content;
13722
+ if (!Array.isArray(content)) return void 0;
13723
+ const parts = [];
13724
+ for (const block of content) {
13725
+ const b = block;
13726
+ if (b?.type === "text" && typeof b.text === "string") parts.push(b.text);
13727
+ }
13728
+ return parts.length === 0 ? void 0 : parts.join("");
13729
+ }
13360
13730
  function agentScope(agent) {
13361
13731
  return agent.session.id ?? "default";
13362
13732
  }
@@ -13456,7 +13826,10 @@ function registerContext(ctx, opts = {}) {
13456
13826
  }
13457
13827
  }
13458
13828
  } catch {}
13459
- if (injected.has(key)) return;
13829
+ if (injected.has(key) || hasAccBanner(agent.session)) {
13830
+ injected.add(key);
13831
+ return;
13832
+ }
13460
13833
  injected.add(key);
13461
13834
  agent.inject(accMessage(root, configPaths, entrySkillMaxChars));
13462
13835
  syncSafeModeRestriction(agent, root);
@@ -13512,7 +13885,11 @@ function registerContext(ctx, opts = {}) {
13512
13885
  registerEntrySkillSection(agent, root);
13513
13886
  registerCognitiveTrajectorySkillSection(agent, root);
13514
13887
  }
13515
- if (!root || injected.has(key) || downstream.kind !== "enter") return downstream;
13888
+ if (!root || downstream.kind !== "enter") return downstream;
13889
+ if (injected.has(key) || hasAccBanner(agent.session)) {
13890
+ injected.add(key);
13891
+ return downstream;
13892
+ }
13516
13893
  injected.add(key);
13517
13894
  return {
13518
13895
  kind: "enter",
@@ -14101,11 +14478,11 @@ function registerStatusApi(ctx, opts = {}) {
14101
14478
  sendJson$2(res, 400, { error: chk.error });
14102
14479
  return;
14103
14480
  }
14104
- const { readWeixinSettings } = await import("./route-Dr0SFD8A.js").then((n) => n.c);
14481
+ const { readWeixinSettings } = await import("./route-GtYJdR06.js").then((n) => n.c);
14105
14482
  const { weixinBridgeStatus } = await Promise.resolve().then(() => bridge_exports);
14106
14483
  const settings = readWeixinSettings(chk.root);
14107
14484
  const bridge = weixinBridgeStatus().find((b) => b.ccc === chk.root);
14108
- const { imCredentialBound } = await import("./route-Dr0SFD8A.js").then((n) => n.c);
14485
+ const { imCredentialBound } = await import("./route-GtYJdR06.js").then((n) => n.c);
14109
14486
  const { normalizeAccountChannel } = await import("./accounts-BHQbakUh.js").then((n) => n.t);
14110
14487
  const accounts = (settings.accounts ?? []).map((a) => {
14111
14488
  const channel = normalizeAccountChannel(a.channel);
@@ -14142,7 +14519,7 @@ function registerStatusApi(ctx, opts = {}) {
14142
14519
  const { fetchQRCode } = await import("./ilink-BMk2w2KY.js").then((n) => n.a);
14143
14520
  const { randomUUID } = await import("node:crypto");
14144
14521
  for (const [key, v] of weixinLogins) if (Date.now() - v.startedAt > WEIXIN_LOGIN_TTL_MS) weixinLogins.delete(key);
14145
- const { readWeixinSettings } = await import("./route-Dr0SFD8A.js").then((n) => n.c);
14522
+ const { readWeixinSettings } = await import("./route-GtYJdR06.js").then((n) => n.c);
14146
14523
  const qr = await fetchQRCode({ botType: readWeixinSettings(root).botType ?? void 0 });
14147
14524
  const loginKey = randomUUID();
14148
14525
  weixinLogins.set(loginKey, {
@@ -14163,7 +14540,7 @@ function registerStatusApi(ctx, opts = {}) {
14163
14540
  sendJson$2(res, 400, { error: "missing accountId" });
14164
14541
  return;
14165
14542
  }
14166
- const { removeWeixinAccount } = await import("./route-Dr0SFD8A.js").then((n) => n.c);
14543
+ const { removeWeixinAccount } = await import("./route-GtYJdR06.js").then((n) => n.c);
14167
14544
  const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
14168
14545
  removeWeixinAccount(root, body.accountId);
14169
14546
  syncCccBridge(ctx, root);
@@ -14189,8 +14566,8 @@ function registerStatusApi(ctx, opts = {}) {
14189
14566
  trajectory: code
14190
14567
  });
14191
14568
  }
14192
- const { saveWeixinRoutes } = await import("./route-Dr0SFD8A.js").then((n) => n.c);
14193
- const { findDirNameByCode } = await import("./profile-DnLSb7yv.js").then((n) => n.i);
14569
+ const { saveWeixinRoutes } = await import("./route-GtYJdR06.js").then((n) => n.c);
14570
+ const { findDirNameByCode } = await import("./profile-vHheCJiU.js").then((n) => n.a);
14194
14571
  for (const r of routes) if (findDirNameByCode(root, r.trajectory) === null) {
14195
14572
  sendJson$2(res, 400, { error: `unknown trajectory: ${r.trajectory} (no such trajectory code in ${root})` });
14196
14573
  return;
@@ -14201,7 +14578,7 @@ function registerStatusApi(ctx, opts = {}) {
14201
14578
  }
14202
14579
  if (body.action === "set-enabled") {
14203
14580
  const enabled = body.enabled === true;
14204
- const { readWeixinSettings, setWeixinEnabled } = await import("./route-Dr0SFD8A.js").then((n) => n.c);
14581
+ const { readWeixinSettings, setWeixinEnabled } = await import("./route-GtYJdR06.js").then((n) => n.c);
14205
14582
  const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
14206
14583
  const settings = readWeixinSettings(root);
14207
14584
  if (enabled && (settings.accounts ?? []).length === 0) {
@@ -14224,7 +14601,7 @@ function registerStatusApi(ctx, opts = {}) {
14224
14601
  sendJson$2(res, 400, { error: "appId 与 appSecret 都必须填(只填一个等于没绑)" });
14225
14602
  return;
14226
14603
  }
14227
- const { readWeixinSettings, upsertWeixinAccount, writeFeishuCredential, nextFeishuAccountId } = await import("./route-Dr0SFD8A.js").then((n) => n.c);
14604
+ const { readWeixinSettings, upsertWeixinAccount, writeFeishuCredential, nextFeishuAccountId } = await import("./route-GtYJdR06.js").then((n) => n.c);
14228
14605
  const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
14229
14606
  const before = readWeixinSettings(root);
14230
14607
  const accountId = (body.accountId ?? "").trim() !== "" ? body.accountId.trim() : nextFeishuAccountId(before);
@@ -14274,7 +14651,7 @@ function registerStatusApi(ctx, opts = {}) {
14274
14651
  return;
14275
14652
  }
14276
14653
  const { pollQRStatus } = await import("./ilink-BMk2w2KY.js").then((n) => n.a);
14277
- const { readWeixinSettings } = await import("./route-Dr0SFD8A.js").then((n) => n.c);
14654
+ const { readWeixinSettings } = await import("./route-GtYJdR06.js").then((n) => n.c);
14278
14655
  const settings = readWeixinSettings(login.root);
14279
14656
  const status = await pollQRStatus({
14280
14657
  baseUrl: void 0,
@@ -14289,7 +14666,7 @@ function registerStatusApi(ctx, opts = {}) {
14289
14666
  });
14290
14667
  return;
14291
14668
  }
14292
- const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./route-Dr0SFD8A.js").then((n) => n.c);
14669
+ const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./route-GtYJdR06.js").then((n) => n.c);
14293
14670
  const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
14294
14671
  const accountId = nextWeixinAccountId(settings);
14295
14672
  upsertWeixinAccount(login.root, {
@@ -18825,12 +19202,15 @@ function appendHumanChannelAudit(event) {
18825
19202
  const WEIXIN_HOOK_TIMEOUT_MS = 15e3;
18826
19203
  /** hook stderr/stdout 日志截断(防脚本刷屏) */
18827
19204
  const HOOK_LOG_MAX = 500;
19205
+ /** 存量调用点未传 `channel` 时的缺省值(本字段存在之前唯一的渠道) */
19206
+ const DEFAULT_HOOK_CHANNEL = "weixin";
18828
19207
  /** 构造 incoming 事件对象(纯函数,可测) */
18829
19208
  function buildIncomingHookEvent(input) {
18830
19209
  return {
18831
19210
  event: "incoming",
18832
19211
  ts: Date.now(),
18833
19212
  cccRoot: input.cccRoot,
19213
+ channel: input.channel ?? DEFAULT_HOOK_CHANNEL,
18834
19214
  accountId: input.accountId,
18835
19215
  userId: input.userId,
18836
19216
  sessionId: input.sessionId,
@@ -18847,6 +19227,7 @@ function buildOutgoingHookEvent(input) {
18847
19227
  event: "outgoing",
18848
19228
  ts: Date.now(),
18849
19229
  cccRoot: input.cccRoot,
19230
+ channel: input.channel ?? DEFAULT_HOOK_CHANNEL,
18850
19231
  accountId: input.accountId,
18851
19232
  userId: input.userId,
18852
19233
  sessionId: input.sessionId,
@@ -19229,7 +19610,7 @@ const MEDIA_MAX_BYTES = 20971520;
19229
19610
  */
19230
19611
  async function resolveCccRootByAlias(ctx, alias) {
19231
19612
  try {
19232
- const { listCccs } = await import("./ccc-roots-VCJ3aYJA.js").then((n) => n.a);
19613
+ const { listCccs } = await import("./ccc-roots-BI_4UVcj.js").then((n) => n.a);
19233
19614
  const entries = await listCccs(ctx);
19234
19615
  const byName = entries.find((e) => e.name === alias);
19235
19616
  if (byName) return byName.root;
@@ -19297,7 +19678,7 @@ async function resolveInboundTarget(ctx, input) {
19297
19678
  });
19298
19679
  return null;
19299
19680
  }
19300
- const { findDirNameByCode } = await import("./profile-DnLSb7yv.js").then((n) => n.i);
19681
+ const { findDirNameByCode } = await import("./profile-vHheCJiU.js").then((n) => n.a);
19301
19682
  if (findDirNameByCode(root, target.role) === null) {
19302
19683
  const detail = `目标 CCC "${target.ccc}" 内没有编码为 "${target.role}" 的认知轨迹`;
19303
19684
  console.error(`[serenity-hooks] ✗ human-channel: ${detail}(account=${input.account} user=${input.user})—— 该消息**未投递**,请改订阅表里的目标编码`);
@@ -19526,6 +19907,7 @@ async function handleInboundEvent(ctx, root, accountId, io, event, preset) {
19526
19907
  const hookRel = settings.hook;
19527
19908
  if (hookRel) invokeWeixinHook(root, hookRel, buildIncomingHookEvent({
19528
19909
  cccRoot: root,
19910
+ channel,
19529
19911
  accountId,
19530
19912
  userId: fromUserId,
19531
19913
  sessionId,
@@ -19552,6 +19934,7 @@ async function handleInboundEvent(ctx, root, accountId, io, event, preset) {
19552
19934
  console.warn(`[serenity-hooks] ⚠ im 输出兜底:agent 本轮未发送(闸门打回用尽)→ 桥转发最终文本(session=${sessionId}, trajectory=${trajectory})`);
19553
19935
  if (hookRel) invokeWeixinHook(root, hookRel, buildOutgoingHookEvent({
19554
19936
  cccRoot: root,
19937
+ channel,
19555
19938
  accountId,
19556
19939
  userId: fromUserId,
19557
19940
  sessionId,
@@ -19572,6 +19955,7 @@ async function handleInboundEvent(ctx, root, accountId, io, event, preset) {
19572
19955
  });
19573
19956
  if (hookRel) invokeWeixinHook(root, hookRel, buildOutgoingHookEvent({
19574
19957
  cccRoot: root,
19958
+ channel,
19575
19959
  accountId,
19576
19960
  userId: fromUserId,
19577
19961
  sessionId,
@@ -20247,7 +20631,7 @@ function syncHumanChannel(ctx) {
20247
20631
  (async () => {
20248
20632
  try {
20249
20633
  stopMachineLoops();
20250
- const { listCccs } = await import("./ccc-roots-VCJ3aYJA.js").then((n) => n.a);
20634
+ const { listCccs } = await import("./ccc-roots-BI_4UVcj.js").then((n) => n.a);
20251
20635
  for (const entry of await listCccs(ctx)) syncCccBridge(ctx, entry.root);
20252
20636
  } catch {}
20253
20637
  })();
@@ -20344,6 +20728,7 @@ async function sendProactiveText(input) {
20344
20728
  }
20345
20729
  if (settings.hook) invokeWeixinHook(root, settings.hook, buildOutgoingHookEvent({
20346
20730
  cccRoot: root,
20731
+ channel: "weixin",
20347
20732
  accountId,
20348
20733
  userId: toUserId,
20349
20734
  sessionId,
@@ -20538,7 +20923,7 @@ function matchCcc(input, candidates) {
20538
20923
  }
20539
20924
  /** 组装候选列表(`ccc-roots.listCccs` 投影 + `.serenity` 名;动态 import 保持本模块静态依赖轻量) */
20540
20925
  async function collectCandidates(ctx) {
20541
- const { listCccs } = await import("./ccc-roots-VCJ3aYJA.js").then((n) => n.a);
20926
+ const { listCccs } = await import("./ccc-roots-BI_4UVcj.js").then((n) => n.a);
20542
20927
  const entries = await listCccs(ctx, { defaultRoot: process.cwd() });
20543
20928
  const seen = /* @__PURE__ */ new Set();
20544
20929
  const out = [];
@@ -21753,6 +22138,24 @@ function credentialValue$1(root, key) {
21753
22138
  function channelError$1(code, error, remediation) {
21754
22139
  return /* @__PURE__ */ new Error(`${code}: ${error}${remediation ? ` — ${remediation}` : ""}`);
21755
22140
  }
22141
+ /**
22142
+ * 路由目标轨迹**是否存在**(`status` 的自检列)。
22143
+ *
22144
+ * 🔴 复用既有判据 `findDirNameByCode`(它扫活跃区并按编码解析)—— ⛔ 不在这里另写一套目录扫描,
22145
+ * 两份判据会漂(一处认归档、另一处不认,就会出现"这里说有、投递时说没有")。
22146
+ *
22147
+ * ⚠️ 解析失败(`findDirNameByCode` 在**重码**时会抛)⇒ 返回 `false` 但**不吞掉原因**:
22148
+ * 重码本身也是"这个路由不可信"的一种形态,`status` 里会表现为 `targetExists:false`,
22149
+ * 配合 `cognitive_trajectory list` 可定位。不额外造一个错误码(键名已经够直白)。
22150
+ */
22151
+ function trajectoryDirExists(root, code) {
22152
+ if (typeof code !== "string" || code.trim() === "") return false;
22153
+ try {
22154
+ return findDirNameByCode(root, code.trim()) !== null;
22155
+ } catch {
22156
+ return false;
22157
+ }
22158
+ }
21756
22159
  const weixinChannel = {
21757
22160
  id: "weixin",
21758
22161
  isEnabled(root) {
@@ -21831,6 +22234,7 @@ const weixinChannel = {
21831
22234
  const trajectory = matchWeixinRoute(settings.routes ?? [], input.userId) ?? "";
21832
22235
  invokeWeixinHook(input.root, settings.hook, buildOutgoingHookEvent({
21833
22236
  cccRoot: input.root,
22237
+ channel: "weixin",
21834
22238
  accountId: account.accountId,
21835
22239
  userId: input.userId,
21836
22240
  sessionId: weixinSessionIdForTrajectory(trajectory !== "" ? trajectory : sanitizeFileName$1(input.userId) || "unknown"),
@@ -21862,10 +22266,14 @@ const weixinChannel = {
21862
22266
  lastPollAt: account.lastPollAt,
21863
22267
  ...account.lastError ? { lastError: account.lastError } : {}
21864
22268
  })));
21865
- const routes = (settings.routes ?? []).map((route) => ({
21866
- user: route.user,
21867
- role: route.trajectory
21868
- }));
22269
+ const routes = (settings.routes ?? []).map((route) => {
22270
+ const code = route.trajectory;
22271
+ return {
22272
+ user: route.user,
22273
+ role: code,
22274
+ targetExists: trajectoryDirExists(root, code)
22275
+ };
22276
+ });
21869
22277
  return {
21870
22278
  enabled: settings.enabled === true,
21871
22279
  autoReplyWithLastMessage: settings.autoReplyWithLastMessage !== false,
@@ -21906,12 +22314,17 @@ const weixinChannel = {
21906
22314
  * ⇒ 校验**值的形状**(飞书 open_id 以 `ou_` 开头):错配在**解析处**就失败,
21907
22315
  * 错误码是 `ALIAS_UNKNOWN` + 可行动提示。
21908
22316
  *
21909
- * ## 🔴 为什么不 fire 记录 hook
22317
+ * ## 🔴 记录 hook:曾经不 fire,2026-10-22 起 fire(A83 已裁)
21910
22318
  *
21911
- * `settings.hook` 的契约与消费方都是**微信的**:CCC 的 `scripts/weixin-message-hook.ts`
21912
- * 把事件追加进 `AGENT_SESSIONS/_weixin-logs/<日期>.jsonl`。飞书事件若流进去,
21913
- * 那个目录就**同时装两种渠道的数据而名字只说一种**(D151/D179 同族:名字开始撒谎)。
21914
- * ⇒ 本期**不落记录**,并把它登记为待裁(A83)。工具描述里按渠道写清这件事。
22319
+ * **当时的理由**:`settings.hook` 的契约与消费方都是**微信的**,CCC 的
22320
+ * `scripts/weixin-message-hook.ts` 把事件追加进 `AGENT_SESSIONS/_weixin-logs/<日期>.jsonl`。
22321
+ * 飞书事件若流进去,那个目录就**同时装两种渠道的数据而名字只说一种**(D151/D179 同族)。
22322
+ *
22323
+ * **为什么现在可以 fire**:同批给 hook 事件加了 **`channel` 字段**(owner 2026-10-22:
22324
+ * 「一个 CCC 绑定多个账户很常见,聊天记录落盘要完善」)⇒ CCC 侧 hook 按渠道分流到
22325
+ * `_im-logs/<channel>/`,**目录名不再需要撒谎**。
22326
+ * ⇒ 所以这不是"决定记录",而是"**让记录不再有害**之后才记录"。
22327
+ * ⚠️ 顺序有意义:**先有渠道字段,再有记录**。反过来做就会先造出一批分不清渠道的数据。
21915
22328
  */
21916
22329
  /**
21917
22330
  * 飞书 open_id 的形状前缀(官方形态:`ou_` + 一串)。
@@ -22066,6 +22479,18 @@ const feishuChannel = {
22066
22479
  const settings = readWeixinSettings(input.root);
22067
22480
  const trajectory = matchWeixinRoute(settings.routes ?? [], input.userId) ?? "";
22068
22481
  const sessionId = weixinSessionIdForTrajectory(trajectory !== "" ? trajectory : sanitizeFileName$1(input.userId) || "unknown");
22482
+ if (settings.hook) invokeWeixinHook(input.root, settings.hook, buildOutgoingHookEvent({
22483
+ cccRoot: input.root,
22484
+ channel: "feishu",
22485
+ accountId: account.accountId,
22486
+ userId: input.userId,
22487
+ sessionId,
22488
+ role: trajectory,
22489
+ reply: markdownToPlainText(input.text),
22490
+ source: "proactive"
22491
+ })).catch((err) => {
22492
+ console.log(`[serenity-hooks] im hook outgoing(feishu) error: ${err instanceof Error ? err.message : String(err)}`);
22493
+ });
22069
22494
  return {
22070
22495
  accountId: account.accountId,
22071
22496
  userId: input.userId,
@@ -22119,7 +22544,8 @@ const ACTIONS = [
22119
22544
  "send",
22120
22545
  "send-file",
22121
22546
  "users",
22122
- "status"
22547
+ "status",
22548
+ "contacts"
22123
22549
  ];
22124
22550
  function renderText(value) {
22125
22551
  return [{
@@ -22134,7 +22560,7 @@ function renderText(value) {
22134
22560
  function createImBridgeTool() {
22135
22561
  return defineTool({
22136
22562
  name: "im-bridge",
22137
- description: `Send messages to family members through an IM channel configured for THIS container. This tool only acts on the current container (no target container parameter). It is hidden entirely when this container has no IM channel configured. Channels: ${imChannelIds().join(", ") || "(none registered)"}. Actions: send (text) / send-file (a file inside this container) / users (list configured recipients) / status (channel health). Not every channel supports every action (e.g. send-file may return ACTION_UNSUPPORTED). user accepts a configured alias (e.g. user-a, user-b) or a raw channel user id. Failures return a stable code plus a remediation hint.`,
22563
+ description: `Send messages to family members through an IM channel configured for THIS container. This tool only acts on the current container (no target container parameter). It is hidden entirely when this container has no IM channel configured. Channels: ${imChannelIds().join(", ") || "(none registered)"}. Actions: send (text) / send-file (a file inside this container) / users (configured aliases) / status (channel health, accounts + current routes) / contacts (people this container has exchanged messages with — use this to find the \`user\` id when adding a route, since that id exists only in the chat logs). Not every channel supports every action (e.g. send-file may return ACTION_UNSUPPORTED). user accepts a configured alias or a raw channel user id. Docs: container_admin config docs im-bridge (how to add a route, where the other party's id comes from). Failures return a stable code plus a remediation hint.`,
22138
22564
  parameters: {
22139
22565
  channel: {
22140
22566
  type: "string",
@@ -22145,7 +22571,7 @@ function createImBridgeTool() {
22145
22571
  type: "string",
22146
22572
  enum: [...ACTIONS],
22147
22573
  required: true,
22148
- description: "send / send-file / users / status"
22574
+ description: "send / send-file / users / status / contacts"
22149
22575
  },
22150
22576
  user: {
22151
22577
  type: "string",
@@ -1,6 +1,6 @@
1
- import { r as cccRootForCwd } from "./ccc-roots-VCJ3aYJA.js";
1
+ import { r as cccRootForCwd } from "./ccc-roots-BI_4UVcj.js";
2
2
  import { _ as resolveInside, g as readUtf8 } from "./ccc-CyYJL4F2.js";
3
- import { A as sessionEvents, E as parseTrajectoryCode, b as getActiveSessionInfo, j as sessionsRoot, x as getLastActiveSessionInfo } from "./profile-DnLSb7yv.js";
3
+ import { D as parseTrajectoryCode, M as sessionsRoot, S as getLastActiveSessionInfo, j as sessionEvents, x as getActiveSessionInfo } from "./profile-vHheCJiU.js";
4
4
  import { dirname, join } from "node:path";
5
5
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
6
6
  import { fileURLToPath } from "node:url";
@@ -1222,4 +1222,4 @@ function replacesDefaultIdentity(profile) {
1222
1222
  return hasFile || hasInline;
1223
1223
  }
1224
1224
  //#endregion
1225
- export { sessionEvents as A, isoLocal as B, listSessions as C, readActiveSessionMd as D, parseTrajectoryCode as E, trajectoryDescOf as F, localHuman as H, useSession as I, TRAJECTORY_FILENAME as L, setActiveSessionInfo as M, showSession as N, readTrajectoryUid as O, summarize as P, TRAJECTORY_LEGACY_FILENAMES as R, listActiveTrajectories as S, parseSessionContextFromEvents as T, localIdStamp as U, localFileStamp as V, createSession as _, readTrajectoryConstraintAt as a, getActiveSessionInfo as b, trajectoryConstraintForSession as c, unregisterTrajectorySession as d, COGNITIVE_TRAJECTORY_ACTIONS as f, clearActiveSessionInfo as g, archiveSession as h, profile_exports as i, sessionsRoot as j, resolveSessionByTitle as k, registerTrajectorySession as l, UID_FILENAME as m, isTrajectoryConstrained as n, readTrajectoryConstraintByCode as o, DEFAULT_SESSION_SCOPE as p, listTrajectoryConstraints as r, replacesDefaultIdentity as s, TRAJECTORY_CONSTRAINT_FILENAME as t, trajectoryDirNameForSession as u, extractSessionMdPathFromText as v, namingTitleFor as w, getLastActiveSessionInfo as x, findSession as y, readTrajectoryFile as z };
1225
+ export { resolveSessionByTitle as A, readTrajectoryFile as B, listActiveTrajectories as C, parseTrajectoryCode as D, parseSessionContextFromEvents as E, summarize as F, localFileStamp as H, trajectoryDescOf as I, useSession as L, sessionsRoot as M, setActiveSessionInfo as N, readActiveSessionMd as O, showSession as P, TRAJECTORY_FILENAME as R, getLastActiveSessionInfo as S, namingTitleFor as T, localHuman as U, isoLocal as V, localIdStamp as W, clearActiveSessionInfo as _, profile_exports as a, findSession as b, replacesDefaultIdentity as c, trajectoryDirNameForSession as d, unregisterTrajectorySession as f, archiveSession as g, UID_FILENAME as h, listTrajectoryConstraints as i, sessionEvents as j, readTrajectoryUid as k, trajectoryConstraintForSession as l, DEFAULT_SESSION_SCOPE as m, findDirNameByCode as n, readTrajectoryConstraintAt as o, COGNITIVE_TRAJECTORY_ACTIONS as p, isTrajectoryConstrained as r, readTrajectoryConstraintByCode as s, TRAJECTORY_CONSTRAINT_FILENAME as t, registerTrajectorySession as u, createSession as v, listSessions as w, getActiveSessionInfo as x, extractSessionMdPathFromText as y, TRAJECTORY_LEGACY_FILENAMES as z };
@@ -172,6 +172,13 @@ function normalizeImSection(w, d) {
172
172
  * ⚠️ 2026-10-19 起它是 {@link readImSettings} 的**薄别名**(签名与返回形状不变):
173
173
  * 旧调用点无需改动,但**配置来源已经统一**——`im` 段优先、`weixin` 段回退。
174
174
  * 保留名字是为了让既有调用点与测试**逐字不动**(P1 是纯重构,读数必须不变)。
175
+ *
176
+ * 🔴 2026-10-22:**返回类型由 `WeixinSettings` 改为 `ImSettings`**。
177
+ * 此前实现返回的就是 `readImSettings(...)`(已是归一形态:`accounts[].channel` 必填),
178
+ * 却把它断言成旧类型 —— 于是类型层"看不见"`channel` 字段,
179
+ * 调用方写 `a.channel` 会报 TS2339,而**运行时它一直在那儿**。
180
+ * ⇒ 那是**类型声明在撒谎**,不是调用方写错。改成真实类型后,多账户/多渠道的读侧才敢用它。
181
+ * ⚠️ 附带效应:`WeixinSettings` 的旧形状调用方若用到旧字段,会在此处暴露(好事,早暴露)。
175
182
  */
176
183
  function readWeixinSettings(root, paths = DEFAULT_SERENITY_CONFIG_PATHS) {
177
184
  return readImSettings(root, paths);
@@ -21,6 +21,19 @@ export declare function accIdentityText(root: string, configPaths?: string[], en
21
21
  * 不再重复注入同一内容(token 双倍浪费,见 S134 注入方案梳理)。
22
22
  */
23
23
  export declare function accMessage(root: string, configPaths: string[], entrySkillMaxChars: number): UserMessage;
24
+ /**
25
+ * 这条会话的历史里**是否已经有** ACC 身份横幅(= 判据的唯一权威)。
26
+ *
27
+ * 🔴 这是本条修复的核心:**"注入过没有"是会话的属性,不是进程的属性。**
28
+ * 会话历史随会话走(落盘、重启后仍在),进程内状态随进程死。
29
+ *
30
+ * ⚠️ 读不到 events(受限宿主 / 投影未就绪)⇒ 返回 `false`(退回"注入一次")。
31
+ * 这是**偏保守**的选择:多注入一条横幅(可见、可清理)远好于
32
+ * **永远不再注入**(陈旧会话里 ACC 身份彻底缺失,且零信号)。
33
+ *
34
+ * @param session 目标会话(`sessionEvents` 能读的形状)
35
+ */
36
+ export declare function hasAccBanner(session: unknown): boolean;
24
37
  /**
25
38
  * 重启恢复的根会话判定(S134 需求):
26
39
  * 只有"conversation 根会话"才自动恢复最近激活的宁静号会话——
@@ -1,8 +1,8 @@
1
- import { n as agentCwdFor, r as cccRootForCwd, s as hostAgents } from "./ccc-roots-VCJ3aYJA.js";
1
+ import { n as agentCwdFor, r as cccRootForCwd, s as hostAgents } from "./ccc-roots-BI_4UVcj.js";
2
2
  import { g as readUtf8, o as isSafeModeOn, p as readCognitiveTrajectorySkills, u as readBlacklist } from "./ccc-CyYJL4F2.js";
3
- import { B as isoLocal, D as readActiveSessionMd, a as readTrajectoryConstraintAt, c as trajectoryConstraintForSession, d as unregisterTrajectorySession$1, j as sessionsRoot, l as registerTrajectorySession$1, p as DEFAULT_SESSION_SCOPE, u as trajectoryDirNameForSession } from "./profile-DnLSb7yv.js";
3
+ import { M as sessionsRoot, O as readActiveSessionMd, V as isoLocal, d as trajectoryDirNameForSession, f as unregisterTrajectorySession$1, l as trajectoryConstraintForSession, m as DEFAULT_SESSION_SCOPE, o as readTrajectoryConstraintAt, u as registerTrajectorySession$1 } from "./profile-vHheCJiU.js";
4
4
  import { a as readGitTrack, i as localstorePath } from "./service-DDmHSglV.js";
5
- import { d as constraintCognitiveTrajectory, m as profileMsmWhitelist, n as PLUGIN_SOURCE, o as readLastBound, p as createProfilePromptReader, u as buildTrajectoryBasePrompt, v as ACC_VERSION } from "./message-source-D7dVNDIh.js";
5
+ import { d as constraintCognitiveTrajectory, m as profileMsmWhitelist, n as PLUGIN_SOURCE, o as readLastBound, p as createProfilePromptReader, u as buildTrajectoryBasePrompt, v as ACC_VERSION } from "./message-source-DRkQwfgh.js";
6
6
  import { basename, dirname, join, resolve } from "node:path";
7
7
  import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
8
8
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
@@ -1,4 +1,4 @@
1
- import { f as hostSettings } from "./ccc-roots-VCJ3aYJA.js";
1
+ import { f as hostSettings } from "./ccc-roots-BI_4UVcj.js";
2
2
  //#region src/surface/settings.ts
3
3
  /**
4
4
  * 设置面板命名空间 = **profile 条目 id**(v0.1.7 起;与旧 `settings.yaml` 段名逐字相同)。
@@ -0,0 +1,20 @@
1
+ /**
2
+ * tools/im-docs.ts — 「IM 桥怎么用、怎么配路由」的**手册正文**(2026-10-22)
3
+ *
4
+ * ## 为什么需要这一章(owner 原话)
5
+ *
6
+ * 「我在测试飞书,发现 ACC 没提供 IM 桥使用说明和账户查询方法,**导致 CCC 没办法自己配路由**,
7
+ * 这个要完善下」+「配路由需要的是**账户到轨迹**,轨迹 CCC 知道,**账户 CCC 不知道**,
8
+ * 需要有个能力让 CCC 查询用户配好的账户」。
9
+ *
10
+ * ⇒ 两件事被同时指出来:**没有说明面**(只能猜配置形状)+ **没有查询面**(拿不到对方 id)。
11
+ * 后者已由 `im-bridge action=contacts` 兑现;本章补前者。
12
+ *
13
+ * ## 🔴 写这一章时守的两条纪律
14
+ *
15
+ * 1. **只描述已经建成的东西**(D179):本章出现的每个字段、每个动作,都必须在实现里存在。
16
+ * 写"打算做"的功能进手册 = 教用户按图施工然后把自己锁死。
17
+ * 2. **指针从读者真正打开的那个工具发出**(D178):读这一章的人是 **CCC 的 agent**,
18
+ * 它手边常用的入口是 `im-bridge`(工具描述已指向本章)。⛔ 不能只藏在 container_admin 里。
19
+ */
20
+ export declare const IM_BRIDGE_GUIDE = "# IM \u6865\uFF08im-bridge\uFF09\u2014\u2014 \u600E\u4E48\u7528\u3001\u600E\u4E48\u914D\u3001\u600E\u4E48\u67E5\n\n## \u4E00\u3001\u5B83\u662F\u4EC0\u4E48\n\n\u4E00\u4E2A CCC \u901A\u8FC7 IM \u6E20\u9053\uFF08\u5FAE\u4FE1 / \u98DE\u4E66\uFF09\u8DDF\u5BB6\u4EBA\u6536\u53D1\u6D88\u606F\u7684\u80FD\u529B\u3002\n**\u53EA\u4F5C\u7528\u4E8E\u672C\u5BB9\u5668**\u2014\u2014\u6CA1\u6709\"\u76EE\u6807\u5BB9\u5668\"\u53C2\u6570\uFF08\u8DE8 CCC \u53D1\u9001\u4E0D\u5728\u80FD\u529B\u9762\uFF09\u3002\n\n\u5DE5\u5177\u5728**\u672A\u914D\u7F6E\u4EFB\u4F55 IM \u6E20\u9053\u65F6\u6839\u672C\u4E0D\u53EF\u89C1**\uFF08\u4E0D\u662F\"\u770B\u5F97\u5230\u4F46\u88AB\u62D2\"\uFF09\u3002\n\u21D2 \u4F60\u770B\u5230\u8FD9\u4E2A\u5DE5\u5177\uFF0C\u8BF4\u660E\u672C CCC \u5DF2\u914D\u4E86\u81F3\u5C11\u4E00\u4E2A\u6E20\u9053\u3002\n\n## \u4E8C\u3001\u4E94\u4E2A\u52A8\u4F5C\n\n| \u52A8\u4F5C | \u7528\u9014 | \u5173\u952E\u53C2\u6570 |\n|---|---|---|\n| `send` | \u53D1\u4E00\u6761\u6587\u672C | `user`\uFF08\u5BF9\u65B9\uFF09\uFF0B `text` |\n| `send-file` | \u53D1\u672C\u5BB9\u5668\u5185\u7684\u4E00\u4E2A\u6587\u4EF6 | `user` \uFF0B `file`\uFF08**CCC \u6839\u76F8\u5BF9\u8DEF\u5F84**\uFF09\uFF0B \u53EF\u9009 `caption` |\n| `users` | \u5DF2\u914D\u7684**\u522B\u540D**\u8868 | \u2014 |\n| `status` | \u6E20\u9053\u5065\u5EB7 \uFF0B **\u8D26\u53F7** \uFF0B **\u5F53\u524D\u8DEF\u7531**\uFF08\u542B\u6BCF\u6761\u8DEF\u7531\u7684\u76EE\u6807\u662F\u5426\u5B58\u5728\uFF09 | \u2014 |\n| `contacts` | **\u8DDF\u672C\u5BB9\u5668\u6709\u8FC7\u5F80\u6765\u7684\u4EBA**\uFF08\u542B\u5BF9\u65B9 id\uFF09\uFF0B \u672A\u914D\u8DEF\u7531\u7684 \uFF0F \u914D\u4E86\u5374\u6CA1\u5F80\u6765\u7684 | \u2014 |\n\n\u26A0\uFE0F \u6E20\u9053\u4E0D\u4E00\u5B9A\u652F\u6301\u5168\u90E8\u52A8\u4F5C\uFF08\u4F8B\u5982\u98DE\u4E66\u76EE\u524D\u4E0D\u652F\u6301 `send-file`\uFF0C\u4F1A\u5982\u5B9E\u8FD4\u56DE `ACTION_UNSUPPORTED`\uFF09\u3002\n**\u5982\u5B9E\u8FD4\u56DE\"\u4E0D\u652F\u6301\"\u6BD4\u5047\u88C5\u652F\u6301\u597D**\u2014\u2014\u540E\u8005\u4F1A\u5F97\u5230\u4E00\u6761\"\u53D1\u51FA\u53BB\u4F46\u5BF9\u65B9\u6536\u4E0D\u5230\"\u7684\u9759\u9ED8\u6545\u969C\u3002\n\n## \u4E09\u3001\uD83D\uDD34 \u914D\u8DEF\u7531\uFF1A\u5148 `contacts`\uFF0C\u518D\u5199\uFF0C\u518D `status` \u81EA\u68C0\n\n\u8DEF\u7531\u7684\u5F62\u72B6\uFF08`.opencode/serenity.json` \u7684 `im.routes`\uFF09\uFF1A\n\n```json\n{\n \"im\": {\n \"enabled\": true,\n \"accounts\": [\n { \"accountId\": \"wechat-1\", \"channel\": \"weixin\", \"enabled\": true },\n { \"accountId\": \"feishu-1\", \"channel\": \"feishu\", \"enabled\": true }\n ],\n \"routes\": [\n { \"user\": \"o9cq\u2026@im.wechat\", \"trajectory\": \"zhaocai-xingzai\" },\n { \"user\": \"ou_xxxxxxxx\", \"trajectory\": \"llm-intel\" }\n ]\n }\n}\n```\n\n- `user` = **\u5BF9\u65B9\u5728\u8BE5\u6E20\u9053\u5185\u7684 id**\uFF08\u4E0D\u662F\u6211\u4EEC\u7684\u8D26\u53F7\uFF0C\u4E5F\u4E0D\u662F\u522B\u540D\uFF09\n \u00B7 \u5FAE\u4FE1\uFF1A\u5F62\u5982 `xxx@im.wechat`\n \u00B7 \u98DE\u4E66\uFF1A\u5F62\u5982 `ou_\u2026`\uFF08open_id\uFF09\n- `trajectory` = **\u8BA4\u77E5\u8F68\u8FF9\u7F16\u7801**\uFF08\u76EE\u5F55\u540D\u91CC\u90A3\u4E00\u6BB5\uFF0C\u5982 `S142` / `llm-intel`\uFF09\n- `*` \u53EF\u4F5C\u4E3A `user` \u7684**\u901A\u914D\u515C\u5E95**\uFF08exact \u4F18\u5148\uFF09\n\n### \u90A3\u4E2A `user` id \u4ECE\u54EA\u91CC\u6765 \u2014\u2014 \u8FD9\u662F\u672C\u8282\u5B58\u5728\u7684\u539F\u56E0\n\n**\u4F60\u4E0D\u77E5\u9053\u5BF9\u65B9 id\uFF0C\u800C\u5B83\u4E5F\u4E0D\u5728\u4EFB\u4F55\u914D\u7F6E\u6587\u4EF6\u91CC\u3002** \u5B83\u53EA\u51FA\u73B0\u5728\u804A\u5929\u8BB0\u5F55\u4E2D\u3002\n\u21D2 \u7528 `im-bridge({channel:\"weixin\", action:\"contacts\"})`\uFF0C\u5B83\u4F1A\u7ED9\u51FA\uFF1A\n\n```json\n{\n \"contacts\": [\n { \"channel\": \"weixin\", \"userId\": \"o9cq\u2026@im.wechat\", \"accountIds\": [\"wechat-1\"],\n \"route\": null, \"firstSeen\": \"2026-09-04T\u2026\", \"lastSeen\": \"2026-10-22T\u2026\", \"messages\": 1284 }\n ],\n \"unrouted\": [\"o9cq\u2026@im.wechat\"],\n \"orphanRoutes\": [\"ou_typo_never_seen\"],\n \"sources\": [\"AGENT_SESSIONS/_im-logs/weixin/2026-10-22.jsonl\", \"\u2026\"]\n}\n```\n\n\u4E09\u884C\u8F93\u51FA**\u5404\u6709\u7528\u9014\uFF0C\u7F3A\u4E00\u884C\u8FD9\u4E2A\u52A8\u4F5C\u5C31\u53EA\u5269\"\u5217\u540D\u5B57\"**\uFF1A\n\n| \u5B57\u6BB5 | \u542B\u4E49 | \u4F60\u8981\u505A\u4EC0\u4E48 |\n|---|---|---|\n| `contacts[].userId` | \u5BF9\u65B9 id | **\u590D\u5236\u5B83**\u586B\u8FDB `routes[].user` |\n| `unrouted` | \u26A0\uFE0F \u6709\u5F80\u6765\u3001\u4F46**\u6CA1\u914D\u8DEF\u7531** | \u8FD9\u4E9B\u4EBA\u7684\u6D88\u606F\u73B0\u5728**\u65E0\u5904\u53EF\u53BB** \u21D2 \u8FD9\u5C31\u662F\u8BE5\u914D\u7684 |\n| `orphanRoutes` | \u2753 \u914D\u4E86\u8DEF\u7531\u3001\u4F46\u8BB0\u5F55\u91CC**\u4ECE\u6CA1\u51FA\u73B0\u8FC7** | \u5927\u6982\u7387 **id \u5199\u9519**\uFF08\u6216\u4ECE\u672A\u6709\u4EBA\u53D1\u8FC7\uFF09 |\n| `sources` | \u5B9E\u9645\u8BFB\u8FC7\u7684\u6587\u4EF6 | **\u5426\u5B9A\u6027\u7ED3\u8BBA\u7684\u53EF\u4FE1\u5EA6\u4E0A\u9650** = \u8FD9\u4E2A\u5217\u8868\uFF1B\u5B83\u7A7A\u7740\u5C31\u522B\u4E0B\"\u6CA1\u4EBA\"\u7684\u7ED3\u8BBA |\n\n### \u5199\u5B8C\u600E\u4E48\u81EA\u68C0\n\n`im-bridge({channel:\"weixin\", action:\"status\"})` \u7684 `routes[]` \u6BCF\u6761\u90FD\u5E26 **`targetExists`**\uFF1A\n\n- `true` \u21D2 \u8BE5\u8F68\u8FF9\u76EE\u5F55\u771F\u5B9E\u5B58\u5728\uFF0C\u8DEF\u7531**\u6709\u673A\u4F1A**\u751F\u6548\n- `false` \u21D2 **\u8F68\u8FF9\u7F16\u7801\u5199\u9519\u4E86**\uFF08\u6216\u90A3\u6761\u8F68\u8FF9\u5DF2\u88AB\u5F52\u6863/\u5220\u9664\uFF09\n\n\uD83D\uDD34 \u8FD9\u4E00\u683C\u662F\"\u914D\u4E86\u4E0D\u751F\u6548\"\u7684**\u552F\u4E00\u673A\u68B0\u4FE1\u53F7**\uFF1A\u5199\u9519\u4E00\u4E2A\u7F16\u7801**\u4E0D\u4F1A\u6709\u4EFB\u4F55\u62A5\u9519**\uFF0C\n\u6D88\u606F\u8FDB\u6765\u627E\u4E0D\u5230\u76EE\u6807\u5C31\u9759\u9ED8\u4E0D\u6295\u9012\uFF0C\u4F60\u53EA\u4F1A\u770B\u5230\"\u6CA1\u56DE\u97F3\"\u3002\n\n## \u56DB\u3001\u591A\u8D26\u53F7 / \u591A\u6E20\u9053\n\n\u4E00\u4E2A CCC \u7ED1\u591A\u4E2A\u8D26\u53F7\u662F\u5E38\u6001\uFF08owner 2026-10-22\uFF1A\u300C\u4E00\u4E2A CCC \u7ED1\u5B9A\u591A\u4E2A\u8D26\u6237\u5F88\u5E38\u89C1\u300D\uFF09\u3002\n\n- **\u8D26\u53F7**\u5728 `im.accounts[]`\uFF0C\u6BCF\u6761\u81EA\u5E26 `channel`\uFF08\u7F3A\u7701 `weixin`\uFF09\n- **\u8DEF\u7531\u662F\u901A\u9053\u65E0\u5173\u7684**\uFF08`user \u2192 trajectory`\uFF09\uFF1B\u540C\u4E00\u8F68\u8FF9\u53EF\u4EE5\u88AB\u591A\u4E2A\u6E20\u9053\u7684\u5BF9\u7AEF\u6307\u5411\n- `contacts` \u7684\u6BCF\u6761\u5E26 `accountIds`\uFF08\u8BE5\u5BF9\u7AEF\u88AB\u54EA\u4E9B\u8D26\u53F7\u6536\u8FC7\uFF09\u2014\u2014**\u591A\u8D26\u53F7\u9760\u5B83\u533A\u5206**\n- \u53D1\u6D88\u606F\u65F6\u53EF\u7528 `account` \u53C2\u6570\u6307\u5B9A\u7528\u54EA\u4E2A\u8D26\u53F7\uFF08\u7F3A\u7701\uFF1A\u7B2C\u4E00\u4E2A\u542F\u7528\u4E14\u5DF2\u7ED1\u5B9A\u7684\uFF09\n\n## \u4E94\u3001\u804A\u5929\u8BB0\u5F55\u843D\u5728\u54EA\n\n```\nAGENT_SESSIONS/_im-logs/<channel>/<YYYY-MM-DD>.jsonl\n```\n\n- \u6309**\u6E20\u9053**\u5206\u76EE\u5F55\uFF08`weixin` / `feishu`\uFF09\uFF0C\u6309**\u672C\u5730\u81EA\u7136\u5929**\u5206\u6587\u4EF6\uFF0C\u9010\u884C JSON \u8FFD\u52A0\n- \u8001\u8DEF\u5F84 `AGENT_SESSIONS/_weixin-logs/` \u662F**\u540C\u4E00\u65CF\u7684\u524D\u4EE3**\uFF082026-10-22 \u4E4B\u524D\u7684\u5FAE\u4FE1\u8BB0\u5F55\uFF09\uFF1B\n \u8BFB\u4FA7**\u4E24\u4E2A\u90FD\u8BA4**\uFF0C\u26D4 \u4F46**\u53EA\u5F80\u65B0\u8DEF\u5F84\u5199**\n- \u6BCF\u6761\u4E8B\u4EF6\u5E26 `channel` \u5B57\u6BB5 \u21D2 \u98DE\u4E66\u4E8B\u4EF6\u4E0D\u4F1A\u518D\u6DF7\u8FDB\u5FAE\u4FE1\u76EE\u5F55\uFF08\u6B64\u524D\u4F1A\uFF0C\u76EE\u5F55\u540D\u4F1A\u8BF4\u8C0E\uFF09\n- \u5A92\u4F53\u7684\u6301\u4E45\u526F\u672C\u5728 `_im-logs/<channel>/media/<\u65E5\u671F>/`\n\n\u26A0\uFE0F **\u8BB0\u5F55\u5F52 CCC**\uFF08`im.hook` \u6307\u5411\u672C\u5BB9\u5668\u7684\u811A\u672C\uFF09\uFF0CACC \u4E0D\u8BA4\u8BC6\u5B58\u50A8\u683C\u5F0F \u2014\u2014 \u8FD9\u5C42\u5206\u79BB\u662F\u6709\u610F\u7684\u3002\n\n## \u516D\u3001\u51FA\u9519\u65F6\n\n\u5DE5\u5177\u8FD4\u56DE**\u7A33\u5B9A\u9519\u8BEF\u7801 + \u53EF\u884C\u52A8\u63D0\u793A**\uFF0C\u5E38\u89C1\u7684\uFF1A\n\n| \u7801 | \u610F\u601D | \u600E\u4E48\u6539 |\n|---|---|---|\n| `CHANNEL_NOT_CONFIGURED` | \u672C CCC \u6CA1\u542F\u7528\u8BE5\u6E20\u9053 | \u770B\u63D0\u793A\u91CC\u7684**\u90A3\u4E00\u534A**\uFF08\u603B\u5F00\u5173 vs \u7F3A\u8D26\u53F7\uFF0C\u4E24\u8005\u6539\u6CD5\u4E0D\u540C\uFF09 |\n| `ALIAS_UNKNOWN` | `user` \u65E2\u4E0D\u662F\u5DF2\u914D\u522B\u540D\u3001\u4E5F\u4E0D\u662F\u5408\u6CD5\u7684\u88F8 id | \u5148 `action=users` \u770B\u522B\u540D\uFF0C\u6216\u7528 `contacts` \u62FF\u88F8 id |\n| `ACTION_UNSUPPORTED` | \u8BE5\u6E20\u9053\u6CA1\u5B9E\u73B0\u8FD9\u4E2A\u52A8\u4F5C | \u6362\u52A8\u4F5C\uFF1B\u26D4 \u4E0D\u8981\u91CD\u8BD5 |\n| `FILE_ESCAPE` | `file` \u8DD1\u51FA\u4E86 CCC \u6839 | \u7528\u5BB9\u5668\u5185\u76F8\u5BF9\u8DEF\u5F84 |\n\n## \u4E03\u3001\u8FB9\u754C\uFF08\u660E\u786E\u4E0D\u505A\uFF09\n\n- \u26D4 \u8DE8 CCC \u53D1\u9001\uFF08\u53EA\u4F5C\u7528\u4E8E\u672C\u5BB9\u5668\uFF09\n- \u26D4 \u4FEE\u6539\u914D\u7F6E\uFF08`contacts` \u53EA\u8BFB\uFF1B\u52A0\u8DEF\u7531\u8981\u4EBA/CCC \u81EA\u5DF1\u5199 `serenity.json`\uFF09\n- \u26D4 \u5224\u65AD\"\u5BF9\u65B9\u8BFB\u6CA1\u8BFB\"\u2014\u2014\u8BB0\u5F55\u53EA\u8BC1\u660E**\u5DF2\u53D1\u51FA**\n";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.85.0",
3
+ "version": "1.86.0",
4
4
  "description": "宁静号 ACC harness(Native Cordis 插件)——给 DeepSeek Harness 装一个「AI 工作区」:14 个工具(container_fs/cognitive_trajectory/dashboard/container_git/msm/praxis/ontology/serenity-skill-creator/cheap-worker/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫/对外输出守卫)+ 认知轨迹日志与原地重建 + 网页登录入口/IM 桥/认知轨迹唤醒注册表。轨迹声明 = 轨迹目录下的 trajectory.json(约束面 + 旧标识 uid + CRO 机器绑定 cro.host;旧名 trajectory-constraint.json / skiff.json 仍在读取族里);轨迹提示词 = 轨迹目录下的 PROMPT.md(一条轨迹「是谁、按什么章程工作」,所有轨迹通用,追加在容器层与质量层之后,不替换它们)。适配 DSH 0.1.7-rc.1 / 0.2.0-rc.1 / 0.2.1-rc.1(三档)。",
5
5
  "license": "MIT",
6
6
  "repository": {