@shgroup/dsh-serenity-hooks 1.77.0 → 1.78.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.
@@ -1,7 +1,243 @@
1
- import { r as cccRootForCwd } from "./ccc-roots-rwgzt_0F.js";
2
- import { A as sessionsRoot, T as parseTrajectoryCode, b as getLastActiveSessionInfo, k as sessionEvents, y as getActiveSessionInfo } from "./profile-DplLGGqo.js";
1
+ import { r as cccRootForCwd } from "./ccc-roots-D3ty1C8P.js";
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-C7BuyZK7.js";
3
4
  import { dirname, join } from "node:path";
4
- import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
5
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
6
+ import { fileURLToPath } from "node:url";
7
+ //#region src/kernel/constants.ts
8
+ /** 常量(纯模块,零 DSH 依赖) */
9
+ /** 本包名 —— 上溯定位包根时的**身份核对**(同名目录家的邻居不会误命中) */
10
+ const PACKAGE_NAME = "@shgroup/dsh-serenity-hooks";
11
+ /** 上溯层数上限(源码态最深 `src/<域>/<子域>/` / 发布态 `lib/` ⇒ 8 层足够,且**保证有界**) */
12
+ const MAX_ROOT_ASCENT = 8;
13
+ /**
14
+ * 包根 = 含**本包** `package.json` 的目录。
15
+ *
16
+ * 🔴 为什么不写 `join(here, '..')`:本模块自身的位置**随目录重构变化**
17
+ * (曾 `src/`,现 `src/kernel/`),而发布态是从打包入口 `lib/index.js` 反推 ——
18
+ * 两种布局的深度不同,写死层数**必然在其中一个布局下静默错位**。
19
+ * 实测代价:`src/` → `src/kernel/` 之后,`join(here,'..')` 指到 `src/`(无 package.json)
20
+ * ⇒ `ACC_VERSION` 落进 catch 分支变 `0.0.0`,**没有任何门禁报警**。
21
+ *
22
+ * ⇒ 判据改成**内容**(找带本包名的 package.json)而不是**层数**。
23
+ *
24
+ * @param start 起点目录(缺省 = 本模块所在目录;测试可注入)
25
+ */
26
+ function findPackageRoot(start) {
27
+ let dir = start ?? dirname(fileURLToPath(import.meta.url));
28
+ for (let i = 0; i < MAX_ROOT_ASCENT; i += 1) {
29
+ try {
30
+ if (JSON.parse(readFileSync(join(dir, "package.json"), "utf-8")).name === PACKAGE_NAME) return dir;
31
+ } catch {}
32
+ const parent = dirname(dir);
33
+ if (parent === dir) return null;
34
+ dir = parent;
35
+ }
36
+ return null;
37
+ }
38
+ /**
39
+ * ACC 版本:自动从 package.json 读取(单一真相源,消除与 CHANGELOG 的漂移)。
40
+ * 发布时只需改 package.json 的 version。
41
+ */
42
+ const ACC_VERSION = (() => {
43
+ try {
44
+ const root = findPackageRoot();
45
+ if (root === null) return "0.0.0";
46
+ return JSON.parse(readFileSync(join(root, "package.json"), "utf-8")).version ?? "0.0.0";
47
+ } catch {
48
+ return "0.0.0";
49
+ }
50
+ })();
51
+ //#endregion
52
+ //#region src/trajectory/policy/rules.ts
53
+ /**
54
+ * trajectory/policy/rules.ts — 轨迹约束面的**纯逻辑助手**(零 DSH 依赖,可独立单测)
55
+ *
56
+ * ## 2026-10-19 重做后的职责边界(R↓,别再混起来)
57
+ *
58
+ * | 模块 | 管什么 |
59
+ * |---|---|
60
+ * | `./profile.ts` | **判据 + 读盘**("这是不是 轨迹约束"、"约束面是什么") |
61
+ * | `./rules.ts`(本文件) | **从约束面派生出的纯函数**(白名单集合、提示词读取器、基础提示词文本) |
62
+ * | `../session-create.ts` | 会话创建/复用、提问、守门查询 |
63
+ *
64
+ * ⇒ 本文件**不再读 `serenity.json`**:`轨迹约束.roles` 整段已退役(owner 2026-10-19 重做)。
65
+ * 它只接收一个已解析好的 `TrajectoryConstraint`,因此**不涉及任何 IO**(除提示词文件,那是刻意懒读的)。
66
+ *
67
+ * ## S12(2026-10-19,轨迹中心重构)
68
+ *
69
+ * 原名 `role/轨迹约束/config.ts`。改名理由(框架 §6.3「角色不是一个概念,是一段历史」):
70
+ * 它派生的是**某条轨迹的约束面**,与"配置读取"无关(本文件零配置读取,见上一段)
71
+ * ⇒ `config` 这个名字描述的是**它曾经的样子**,`rules` 描述它**现在做的事**。
72
+ */
73
+ /** 白名单推导 ────────────────────────────────────────────────────────────── */
74
+ /** 提示词来源提示串(`apply` 回执与面板共用;避免两处措辞漂) */
75
+ const TRAJECTORY_CONSTRAINT_HINT = "systemPromptFile hot-reloads by mtime; the rest of trajectory-constraint.json is re-read every turn";
76
+ /**
77
+ * 该轨迹会话**可见**的工具面(tools ∪ `msm`)。
78
+ *
79
+ * 🔴 **返回值 `null` 有专门含义(D181,owner 2026-10-20 裁「不写当然不收窄」)**:
80
+ *
81
+ * | `tools` | `msms` | 返回 | 语义 |
82
+ * |---|---|---|---|
83
+ * | 缺省 | 缺省 | **`null`** | **不收窄任何工具面**(这条轨迹要的是别的设定,如只写 `resident`) |
84
+ * | `[]` | 缺省 | `{'msm'?}` 空/仅 msm | **全隐藏**(除恒可用的 `skill`) |
85
+ * | `['a']` | … | `{'a', …}` | 只给这些 |
86
+ *
87
+ * ⛔ **调用方不许把 `null` 当空集处理** —— 那正是 S196 被锁死的形态(D179(b))。
88
+ * 判据是**字段在不在**,不是数组空不空。
89
+ *
90
+ * 🔴 `msm` 为什么自动加入:`msms` 非空意味着这个轨迹 **有 MSM 通道**,
91
+ * 而 MSM 的唯一合法调用入口是 `msm` 工具(单入口纪律)——不给它 `msm` 就等于
92
+ * 声明了一堆调不动的能力。⚠️ 这是**并集**,不是覆盖:`tools` 里显式写的照旧生效。
93
+ *
94
+ * @returns 白名单集合;**`null` = 不收窄**(调用方须显式判 `null`)
95
+ */
96
+ function profileToolWhitelist(profile) {
97
+ if (!profile) return null;
98
+ if (profile.tools === void 0 && profile.msms === void 0) return null;
99
+ const out = /* @__PURE__ */ new Set();
100
+ for (const t of profile.tools ?? []) out.add(t);
101
+ if ((profile.msms?.length ?? 0) > 0) out.add("msm");
102
+ return out;
103
+ }
104
+ /** 该轨迹会话可 exec 的 MSM 白名单(`msm` 工具内部再过滤;独立于工具白名单) */
105
+ function profileMsmWhitelist(profile) {
106
+ return new Set(profile?.msms ?? []);
107
+ }
108
+ /** 轨迹纪律参与项(`profile` 缺省已在 `readTrajectoryConstraintAt` 归一为全 true) */
109
+ function constraintCognitiveTrajectory(profile) {
110
+ return {
111
+ session: profile?.cognitiveTrajectory.session === true,
112
+ keeper: profile?.cognitiveTrajectory.keeper === true,
113
+ rebuild: profile?.cognitiveTrajectory.rebuild === true
114
+ };
115
+ }
116
+ /**
117
+ * 解析约束面声明的**系统提示词全文**(一次性读取;`create` / `validate` / `list` 用)。
118
+ *
119
+ * 顺序:`systemPromptFile`(**推荐**:超长提示词在 JSON 里内嵌不可读)→ 内嵌 `systemPrompt` → 空串。
120
+ * 文件缺失/逃逸 ⇒ **抛错**(调用方 catch 降级;`validate` 据此报 issue)。
121
+ *
122
+ * ⚠️ 热路径**不用**它(那会用每次工具调用去读盘)——热路径用 `createProfilePromptReader`。
123
+ */
124
+ function resolveConstraintSystemPrompt(root, profile) {
125
+ if (!profile) return "";
126
+ const file = profile.systemPromptFile?.trim();
127
+ if (file) {
128
+ const abs = resolveInside(root, file);
129
+ if (!existsSync(abs)) throw new Error(`轨迹约束 "${profile.code}": systemPromptFile "${file}" not found (resolved: ${abs})`);
130
+ return readUtf8(abs).trim();
131
+ }
132
+ return profile.systemPrompt ?? "";
133
+ }
134
+ /**
135
+ * 提示词来源(`list`/`validate` 展示用)。
136
+ *
137
+ * 🔴 **2026-10-19 修(T4 的漏网面)**:v1.72.0 起提示词有两代来源——**新形态**
138
+ * 是轨迹目录里的 `PROMPT.md`(`trajectory/prompt.ts`,**优先**),旧形态才是
139
+ * `systemPromptFile` / `systemPrompt`。本函数原先只认旧形态 ⇒ 两条只写了 `PROMPT.md`
140
+ * 的轨迹在 `container_admin config list` 里恒显示 `promptSource: 'none'`。
141
+ *
142
+ * 那是**读数器过时**,不是配置缺失:`validate` 侧早已按两条来源判(见
143
+ * `trajectory-constraint-admin.ts` 的 `hasTrajectoryPromptOrLegacy`),而 `list` 侧漏了。
144
+ * ⇒ 返 **四种**值,把"有章程但来源是新形态"这一格单独说出来。
145
+ *
146
+ * @param hasPromptFile 该轨迹目录下**是否真有** `PROMPT.md`(调用方查,本函数不做 IO)
147
+ */
148
+ function constraintSystemPromptSource(profile, hasPromptFile = false) {
149
+ if (hasPromptFile) return "trajectory-file";
150
+ if (profile?.systemPromptFile?.trim()) return "file";
151
+ if (profile?.systemPrompt?.trim()) return "inline";
152
+ return "none";
153
+ }
154
+ /**
155
+ * 提示词**读取器**(长期热更新;改文件即生效,无需重启)。
156
+ *
157
+ * 为什么返回函数而不是字符串(R↓,实证代价):旧实现在创建时把提示词**快照**进系统提示词段闭包
158
+ * ⇒ CCC 改了提示词文件必须重启 dsh web 才生效(实测:改完 18KB 提示词,live agent 仍用旧文本)。
159
+ * 现形态由系统提示词段的 `text()` **每轮调用**本读取器:
160
+ * - `systemPromptFile`:按 **mtime + size** 缓存重读(命中缓存零 IO;未变不解析)
161
+ * - 内嵌 `systemPrompt`:静态(改 JSON 仍需重载配置,这与文件热更的差别是**必然**的 —— 文件有 mtime,JSON 没有)
162
+ * - 读取失败:返回**上次成功内容**(⛔ 绝不返回空——空提示词会让角色失去人格与纪律),并按标签去重告警一次
163
+ *
164
+ * @param root CCC 根
165
+ * @param profile 约束面
166
+ */
167
+ function createProfilePromptReader(root, profile) {
168
+ const file = profile?.systemPromptFile?.trim();
169
+ if (!file) {
170
+ const inline = profile?.systemPrompt ?? "";
171
+ return () => inline;
172
+ }
173
+ let abs = null;
174
+ try {
175
+ abs = resolveInside(root, file);
176
+ } catch (err) {
177
+ console.warn(`[serenity-hooks] ✗ 轨迹约束提示词路径非法(${file}): ${String(err?.message ?? err)}`);
178
+ }
179
+ if (abs === null) return () => "";
180
+ const absPath = abs;
181
+ let cachedMtime = -1;
182
+ let cachedSize = -1;
183
+ let cachedText = "";
184
+ let warned = false;
185
+ return () => {
186
+ try {
187
+ const st = statSync(absPath);
188
+ if (st.mtimeMs === cachedMtime && st.size === cachedSize) return cachedText;
189
+ const text = readUtf8(absPath).trim();
190
+ const isReload = cachedMtime !== -1;
191
+ cachedMtime = st.mtimeMs;
192
+ cachedSize = st.size;
193
+ cachedText = text;
194
+ warned = false;
195
+ if (isReload) console.info(`[serenity-hooks] ↻ 轨迹约束提示词已热重载(${file},${text.length} 字符)`);
196
+ return text;
197
+ } catch (err) {
198
+ if (!warned) {
199
+ warned = true;
200
+ console.warn(`[serenity-hooks] ✗ 轨迹约束提示词读取失败(${file})——沿用上次成功内容: ${String(err?.message ?? err)}`);
201
+ }
202
+ return cachedText;
203
+ }
204
+ };
205
+ }
206
+ /**
207
+ * 轨迹约束 基础提示词(dsp 只给这部分;CCC 的 `systemPrompt` 段由调用方拼接)。
208
+ *
209
+ * ## 🔴 为什么**不再枚举**清单(owner 2026-10-19 令)
210
+ *
211
+ * 旧版在这里抄了两份清单(`MSMs: a, b, c` / `Tools: read, grep, …`)。那是**第二真相源**,
212
+ * 而真相源本来就在模型自己手里:
213
+ * · **工具清单** = 这一轮实际注册给它的 tool schema —— 模型已经看得见,抄一遍只会漂
214
+ * · **MSM 清单** = `msm()` 无参调用返回的目录 —— 已按白名单过滤,且**每次实时读配置**
215
+ *
216
+ * ⚠️ **漂移不是理论风险,是已发生的缺陷**(2026-10-19):角色配置早就长到 8 个工具,
217
+ * 而这个函数是从**创建时冻结的快照**求值的 ⇒ 提示词里一直写着旧的 4 个
218
+ * ⇒ 模型照着提示词办事,**它自己以为没有的能力就不去调** ⇒ 症状是"工具用不了",
219
+ * 而守卫那边(每调用实时读配置)其实是放行的。两个真相源打架,输的总是提示词。
220
+ *
221
+ * ⇒ 现在只声明**边界与发现路径**,不抄内容:清单在哪,怎么取,越界会怎样。
222
+ *
223
+ * @param code 轨迹编码(轨迹约束的身份 —— 不是"角色名",那个概念已退役)
224
+ */
225
+ function buildTrajectoryBasePrompt(code) {
226
+ return [
227
+ "=== Serenity 轨迹约束 ===",
228
+ `Cognitive trajectory: ${code} (this is the trajectory you ARE, not a role you play)`,
229
+ "Your capability boundary is enforced mechanically by this CCC — anything outside it is rejected at the call site, not by your judgment.",
230
+ "Discover your surface, do not memorize it (both are generated from the live config on every turn):",
231
+ " · Your tools are exactly the tool schemas you see in this session — if a tool is not in your schema, it is not available.",
232
+ " · Your MSMs are listed by calling msm() with no arguments — that directory is already filtered to this trajectory.",
233
+ " · Usage for any MSM: msm(\"<name>\", [], inspect=true) or pass \"--help\" as the first argument.",
234
+ "Do not infer unavailability from anything you remember or from an earlier list: this surface changes when the CCC configuration changes, and it takes effect without restarting.",
235
+ "",
236
+ "---",
237
+ ""
238
+ ].join("\n");
239
+ }
240
+ //#endregion
5
241
  //#region src/trajectory/bind-file.ts
6
242
  /**
7
243
  * trajectory/bind-file.ts — **当前载体绑定 = 认知轨迹目录里的自我交代**(判决 D172,owner 2026-10-20)
@@ -485,4 +721,4 @@ const ACC_MESSAGE_KIND = "plugin:dsh-serenity-hooks";
485
721
  /** ACC 注入消息的统一来源值(各缝共用;需要 `form` 时按 `ContextFormed` 另加字段)。 */
486
722
  const PLUGIN_SOURCE = { kind: ACC_MESSAGE_KIND };
487
723
  //#endregion
488
- export { listBoundSessionIds as a, setBindingStore as c, carrierState as i, PLUGIN_SOURCE as n, readLastBound as o, appendBound as r, resolveSessionCognitiveTrajectoryLabel as s, ACC_MESSAGE_KIND as t };
724
+ export { ACC_VERSION as _, listBoundSessionIds as a, setBindingStore as c, constraintCognitiveTrajectory as d, constraintSystemPromptSource as f, resolveConstraintSystemPrompt as g, profileToolWhitelist as h, carrierState as i, TRAJECTORY_CONSTRAINT_HINT as l, profileMsmWhitelist as m, PLUGIN_SOURCE as n, readLastBound as o, createProfilePromptReader as p, appendBound as r, resolveSessionCognitiveTrajectoryLabel as s, ACC_MESSAGE_KIND as t, buildTrajectoryBasePrompt as u, findPackageRoot as v };
@@ -243,7 +243,8 @@ const COGNITIVE_TRAJECTORY_ACTIONS = [
243
243
  "uid",
244
244
  "send-now",
245
245
  "send-later",
246
- "cro-guide"
246
+ "cro-guide",
247
+ "resident"
247
248
  ];
248
249
  /**
249
250
  * 读取 Session 事件序列(v1.28.1 适配 0.1.2-rc.1 补齐):rc.1 起官方 Session 类
@@ -949,6 +950,7 @@ var profile_exports = /* @__PURE__ */ __exportAll({
949
950
  listTrajectoryConstraints: () => listTrajectoryConstraints,
950
951
  readTrajectoryConstraintAt: () => readTrajectoryConstraintAt,
951
952
  readTrajectoryConstraintByCode: () => readTrajectoryConstraintByCode,
953
+ replacesDefaultIdentity: () => replacesDefaultIdentity,
952
954
  trajectoryConstraintForSession: () => trajectoryConstraintForSession
953
955
  });
954
956
  /**
@@ -1011,7 +1013,20 @@ function readTrajectoryConstraintAt(trajectoryDir, root) {
1011
1013
  }
1012
1014
  const obj = raw;
1013
1015
  const file = found;
1014
- const strList = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.trim() !== "").map((x) => x.trim()) : [];
1016
+ /**
1017
+ * 白名单字段的**两态**读取(D181,owner 2026-10-20 裁「不写当然不收窄」):
1018
+ *
1019
+ * | 文件里 | 返回 | 语义 |
1020
+ * |---|---|---|
1021
+ * | 字段**不存在** | `undefined` | **不收窄**(这条轨迹要的是别的设定,比如只写 `resident`) |
1022
+ * | `[]`(显式空数组) | `[]` | **全隐藏**(除恒可用的 `skill`)—— 用户明说了"我一个都不给" |
1023
+ * | `["a","b"]` | `["a","b"]` | 只给这些 |
1024
+ *
1025
+ * 🔴 **为什么不能归一成 `[]`**:那会把"我没提这件事"读成"我要禁止一切",
1026
+ * 于是「只想声明常驻」的人会被静默锁死(S196 实测踩到,见 D179(b))。
1027
+ * 判据是**字段在不在**,不是数组空不空。
1028
+ */
1029
+ const strListMaybe = (v) => v === void 0 ? void 0 : Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.trim() !== "").map((x) => x.trim()) : void 0;
1015
1030
  /** 布尔缺省 **true**(R↓ 见接口:工作台就是这条轨迹自己的身体) */
1016
1031
  const boolOrTrue = (v) => typeof v === "boolean" ? v : true;
1017
1032
  const ct = obj.cognitiveTrajectory && typeof obj.cognitiveTrajectory === "object" ? obj.cognitiveTrajectory : {};
@@ -1025,13 +1040,22 @@ function readTrajectoryConstraintAt(trajectoryDir, root) {
1025
1040
  } catch (err) {
1026
1041
  console.warn(`[serenity-hooks] ✗ ${TRAJECTORY_CONSTRAINT_FILENAME} 的 systemPromptFile 越界,已忽略(其余约束仍生效): ${file} — ${String(err?.message ?? err)}`);
1027
1042
  }
1043
+ const residentRaw = obj.resident;
1044
+ let resident;
1045
+ if (residentRaw && typeof residentRaw === "object" && !Array.isArray(residentRaw)) {
1046
+ const serves = residentRaw.serves;
1047
+ resident = typeof serves === "string" && serves.trim() !== "" ? { serves: serves.trim() } : {};
1048
+ }
1049
+ const tools = strListMaybe(obj.tools);
1050
+ const msms = strListMaybe(obj.msms);
1028
1051
  return {
1029
1052
  dirName,
1030
1053
  code,
1031
1054
  path: file,
1032
1055
  ...model !== void 0 ? { model } : {},
1033
- tools: strList(obj.tools),
1034
- msms: strList(obj.msms),
1056
+ ...tools !== void 0 ? { tools } : {},
1057
+ ...msms !== void 0 ? { msms } : {},
1058
+ ...resident !== void 0 ? { resident } : {},
1035
1059
  cognitiveTrajectory: {
1036
1060
  session: boolOrTrue(ct.session),
1037
1061
  keeper: boolOrTrue(ct.keeper),
@@ -1103,9 +1127,43 @@ function registeredDirName(sessionId) {
1103
1127
  *
1104
1128
  * 存在的理由:调用点很多只要一个"是/不是"(seams 的旁路判定),
1105
1129
  * 让它们各自写 `!== null` 会把判据散开;**判据只能有一个入口**。
1130
+ *
1131
+ * 🔴 **D183(owner 2026-10-20):这里的 `true` 只表示"有声明",不再等于"该旁路默认行为"。**
1132
+ * 要判断后者请用 `replacesDefaultIdentity()` —— 两者**刻意分开**,因为:
1133
+ * 「我声明过」与「我声明了什么」是两件事,而旁路 ACC 身份只需要后者成立。
1106
1134
  */
1107
1135
  function isTrajectoryConstrained(root, sessionId) {
1108
1136
  return trajectoryConstraintForSession(root, sessionId) !== null;
1109
1137
  }
1138
+ /**
1139
+ * 这条声明**是否真的取代了该轨迹的默认身份**(= 该不该旁路 ACC 身份/首轮/恢复等默认行为)。
1140
+ *
1141
+ * ## 为什么需要它(D183,owner 2026-10-20 逐字)
1142
+ *
1143
+ * > 「这个 `trajectory.json` 中**所有 key 都是可选的;不写就是不设定 不应影响默认行为**」
1144
+ *
1145
+ * 旧判据是**文件存在** ⇒ 只写一句 `resident`(对外用途自述)就会连带丢掉
1146
+ * ACC 身份播种、首轮锚定、自动恢复、compact 重注入 —— **完全超出用户预期**。
1147
+ *
1148
+ * ## 判据(哪些 key 真的改"这条轨迹是谁")
1149
+ *
1150
+ * | key | 取代默认身份? | 理由 |
1151
+ * |---|---|---|
1152
+ * | `tools` / `msms` | **否** | 它们收窄**能力**,但"我是谁、我按什么章程工作"没变 ⇒ 仍该拿 ACC 身份 |
1153
+ * | `model` | **否** | 换的只是**执行的模型**,不是身份 |
1154
+ * | `resident` | **否** | 纯对外自述,D173 明说"不碰任何默认行为" |
1155
+ * | `cro.host` | **否** | 管"哪台机器跑 CRO 程序",与身份无关 |
1156
+ * | `cognitiveTrajectory` | **否** | 管"参与哪些纪律",纪律是叠加不是取代 |
1157
+ * | `systemPromptFile` / `systemPrompt` | **是** | 它自带**章程** ⇒ 用户的提示词整段取代 ACC 的 |
1158
+ * | `PROMPT.md`(目录里) | **否** | 它**追加**在 ACC 身份之外(v1.72.0 起的机制),不取代 |
1159
+ *
1160
+ * @returns `true` ⇒ 该轨迹自带章程,应旁路 ACC 身份类默认行为;否则 `false`
1161
+ */
1162
+ function replacesDefaultIdentity(profile) {
1163
+ if (!profile) return false;
1164
+ const hasFile = (profile.systemPromptFile ?? "").trim() !== "";
1165
+ const hasInline = (profile.systemPrompt ?? "").trim() !== "";
1166
+ return hasFile || hasInline;
1167
+ }
1110
1168
  //#endregion
1111
- export { sessionsRoot as A, localFileStamp as B, namingTitleFor as C, readTrajectoryUid as D, readActiveSessionMd as E, useSession as F, localIdStamp as H, TRAJECTORY_FILENAME as I, TRAJECTORY_LEGACY_FILENAMES as L, showSession as M, summarize as N, resolveSessionByTitle as O, trajectoryDescOf as P, readTrajectoryFile as R, listSessions as S, parseTrajectoryCode as T, localHuman as V, extractSessionMdPathFromText as _, readTrajectoryConstraintAt as a, getLastActiveSessionInfo as b, registerTrajectorySession as c, COGNITIVE_TRAJECTORY_ACTIONS as d, DEFAULT_SESSION_SCOPE as f, createSession as g, clearActiveSessionInfo as h, profile_exports as i, setActiveSessionInfo as j, sessionEvents as k, trajectoryDirNameForSession as l, archiveSession as m, isTrajectoryConstrained as n, readTrajectoryConstraintByCode as o, UID_FILENAME as p, listTrajectoryConstraints as r, trajectoryConstraintForSession as s, TRAJECTORY_CONSTRAINT_FILENAME as t, unregisterTrajectorySession as u, findSession as v, parseSessionContextFromEvents as w, listActiveTrajectories as x, getActiveSessionInfo as y, isoLocal as z };
1169
+ 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 };
@@ -1,286 +1,9 @@
1
1
  import { n as __exportAll } from "./rolldown-runtime-B4iAMlE-.js";
2
2
  import { s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS } from "./ccc-CyYJL4F2.js";
3
+ import { c as unsetEntry, l as writeEntry, r as getEntry } from "./service-DDmHSglV.js";
3
4
  import { join, resolve } from "node:path";
4
5
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
6
  import { createHash } from "node:crypto";
6
- //#region src/tools/localstore/service.ts
7
- /**
8
- * tools/localstore/service.ts — localstore 纯逻辑层(零 DSH 依赖,可独立单测)
9
- *
10
- * S134 重设计(v1.16.7):存储从 ~/.serenity/ 迁到 **CCC 根根目录 localstore.json**
11
- * (JSON 格式——方便 MSM 直接 read + JSON.parse 读取,零解析依赖)。
12
- *
13
- * git 提交策略(可靠机制 × 用户自由):
14
- * - 配置:.opencode/serenity.json `localstore.gitTrack`: "allow"(可提交)| "deny"(禁提交)
15
- * - **缺省 deny(没配就是不提交)**;且 deny 的保证**不依赖 dsh 运行**——
16
- * 写入时自动确保 .gitignore 含 localstore.json(物理保证:即使 dsh 不在、
17
- * 用户手动 git commit 也不会误提交),container_git 检查为第二道防线(拒绝 + 提示)
18
- * - allow:放行(文件可提交,用户自行管理 .gitignore)
19
- *
20
- * 存储结构(JSON 顶层分节):
21
- * { "credentials": { "HOME_GITLAB_TOKEN": "xxx" }, "<config节>": { "<key>": "v" } }
22
- * credentials 为保留节(credential 命名空间,key 大写蛇形);其余节归 config 命名空间
23
- * (path = section.key,如 cheap-worker.models)。
24
- */
25
- const LOCALSTORE_SCOPES = ["credential", "config"];
26
- /** 存储文件名(CCC 根根目录) */
27
- const LOCALSTORE_FILENAME = "localstore.json";
28
- /** 凭据保留节名(JSON 顶层) */
29
- const CREDENTIALS_SECTION = "credentials";
30
- /** 凭据 key 规范:大写蛇形,如 HOME_GITLAB_TOKEN */
31
- const CREDENTIAL_KEY_RE = /^[A-Z][A-Z0-9_]*$/;
32
- /** 配置节名规范:小写字母数字连字符 */
33
- const CONFIG_SECTION_RE = /^[a-z][a-z0-9-]*$/;
34
- /** 配置 key 规范(节内):小驼峰,如 defaultModel */
35
- const CONFIG_KEY_RE = /^[a-z][a-zA-Z0-9_]*$/;
36
- /** 存储文件绝对路径(CCC 根根目录) */
37
- function localstorePath(root) {
38
- return join(root, LOCALSTORE_FILENAME);
39
- }
40
- /** git 策略:serenity.json localstore.gitTrack,缺省 deny(不提交) */
41
- function readGitTrack(root, paths = DEFAULT_SERENITY_CONFIG_PATHS) {
42
- return loadSerenityConfig(root, paths).localstore?.gitTrack === "allow" ? "allow" : "deny";
43
- }
44
- /** .gitignore 是否已覆盖 localstore 文件(非空非注释行含文件名即算) */
45
- function isLocalstoreGitignored(root) {
46
- const gi = join(root, ".gitignore");
47
- if (!existsSync(gi)) return false;
48
- return readFileSync(gi, "utf-8").split("\n").some((l) => {
49
- const t = l.trim();
50
- return t !== "" && !t.startsWith("#") && t.includes("localstore.json");
51
- });
52
- }
53
- /**
54
- * 确保 .gitignore 含 localstore 文件(deny 时的物理保证,不依赖 dsh 运行):
55
- * 写入 localstore 时调用——deny(含缺省)且 .gitignore 未覆盖 → 自动追加一行。
56
- * allow → 不写入(放行,文件可提交)。
57
- */
58
- function ensureLocalstoreGitignored(root) {
59
- if (readGitTrack(root) === "allow") return { status: "allow" };
60
- if (isLocalstoreGitignored(root)) return { status: "ignored" };
61
- const gi = join(root, ".gitignore");
62
- const existing = existsSync(gi) ? readFileSync(gi, "utf-8") : "";
63
- const line = `${LOCALSTORE_FILENAME} # ACC localstore (localstore.gitTrack defaults to deny — do not commit)`;
64
- writeFileSync(gi, (existing.endsWith("\n") || existing === "" ? existing : existing + "\n") + line + "\n", "utf-8");
65
- return { status: "appended" };
66
- }
67
- /**
68
- * container_git 联动检查(第二道防线):文件存在 && deny && .gitignore 未覆盖 → 不通过。
69
- * 调用方(container_git commit)据此拒绝提交;status 可输出 warning。
70
- */
71
- function checkLocalstoreGitCompliance(root) {
72
- if (!existsSync(localstorePath(root))) return { ok: true };
73
- if (readGitTrack(root) === "allow") return { ok: true };
74
- if (isLocalstoreGitignored(root)) return { ok: true };
75
- return {
76
- ok: false,
77
- reason: `localstore.json must not be committed (localstore.gitTrack=deny default) but .gitignore does not cover it — add ${LOCALSTORE_FILENAME} to .gitignore (or set localstore.gitTrack=allow to explicitly permit)`
78
- };
79
- }
80
- /** 已告警的坏 localstore 路径(进程级去重;F-07:凭据文件损坏不得静默) */
81
- const warnedBrokenStores = /* @__PURE__ */ new Set();
82
- /** 读取全文件(顶层分节);文件不存在 → 空;**坏 JSON → 空 + 响亮告警**(每路径一次) */
83
- function readAll(root) {
84
- const p = localstorePath(root);
85
- if (!existsSync(p)) return {};
86
- try {
87
- const v = JSON.parse(readFileSync(p, "utf-8").replace(/^\uFEFF/, ""));
88
- if (v && typeof v === "object" && !Array.isArray(v)) return v;
89
- return {};
90
- } catch (err) {
91
- if (!warnedBrokenStores.has(p)) {
92
- warnedBrokenStores.add(p);
93
- console.warn(`[serenity-hooks] ✗ localstore 解析失败,已按空存储继续(其中凭据/配置对工具表现为"未设置",请修 JSON): ${p} — ${String(err?.message ?? err)}`);
94
- }
95
- return {};
96
- }
97
- }
98
- /** 写回全文件(2 空格缩进 + 尾换行,方便 MSM 直接读取) */
99
- function writeAll(root, data) {
100
- writeFileSync(localstorePath(root), JSON.stringify(data, null, 2) + "\n", "utf-8");
101
- }
102
- /** 读取命名空间全部条目:credential → 扁平;config → 分节(剔除 credentials 保留节) */
103
- function readStore(root, scope) {
104
- const all = readAll(root);
105
- if (scope === "credential") return all[CREDENTIALS_SECTION] ?? {};
106
- const { [CREDENTIALS_SECTION]: _cred, ...rest } = all;
107
- return rest;
108
- }
109
- /** 校验凭据 key 合法(大写蛇形);不合法抛错 */
110
- function assertCredentialKey(key) {
111
- if (!CREDENTIAL_KEY_RE.test(key)) throw new Error(`credential key "${key}" must match UPPER_SNAKE ^[A-Z][A-Z0-9_]*$ (e.g. HOME_GITLAB_TOKEN)`);
112
- }
113
- /** 校验 config 路径(节.key);不合法抛错 */
114
- function assertConfigPath(path) {
115
- const idx = path.indexOf(".");
116
- if (idx <= 0 || idx === path.length - 1) throw new Error(`config path "${path}" must be section.key (e.g. cheap-worker.models)`);
117
- const section = path.slice(0, idx);
118
- const key = path.slice(idx + 1);
119
- if (!CONFIG_SECTION_RE.test(section)) throw new Error(`config section "${section}" must match ^[a-z][a-z0-9-]*$`);
120
- if (!CONFIG_KEY_RE.test(key)) throw new Error(`config key "${key}" must match lowerCamel ^[a-z][a-zA-Z0-9_]*$ (e.g. defaultModel)`);
121
- return {
122
- section,
123
- key
124
- };
125
- }
126
- /** 写入单个条目(自动建文件;deny 默认时同步确保 .gitignore 物理保证) */
127
- function writeEntry(root, scope, name, value) {
128
- const all = readAll(root);
129
- if (scope === "credential") {
130
- assertCredentialKey(name);
131
- all[CREDENTIALS_SECTION] ??= {};
132
- all[CREDENTIALS_SECTION][name] = value;
133
- } else {
134
- const { section, key } = assertConfigPath(name);
135
- all[section] ??= {};
136
- all[section][key] = value;
137
- }
138
- writeAll(root, all);
139
- ensureLocalstoreGitignored(root);
140
- }
141
- /** 删除单个条目(不存在返回 false);空节自动移除 */
142
- function unsetEntry(root, scope, name) {
143
- const all = readAll(root);
144
- if (scope === "credential") {
145
- assertCredentialKey(name);
146
- const sec = all[CREDENTIALS_SECTION];
147
- if (!sec || !(name in sec)) return false;
148
- delete sec[name];
149
- if (Object.keys(sec).length === 0) delete all[CREDENTIALS_SECTION];
150
- } else {
151
- const { section, key } = assertConfigPath(name);
152
- const sec = all[section];
153
- if (!sec || !(key in sec)) return false;
154
- delete sec[key];
155
- if (Object.keys(sec).length === 0) delete all[section];
156
- }
157
- writeAll(root, all);
158
- return true;
159
- }
160
- /** 读取单个条目值;不存在返回 null */
161
- function getEntry(root, scope, name) {
162
- const all = readAll(root);
163
- if (scope === "credential") {
164
- assertCredentialKey(name);
165
- return all[CREDENTIALS_SECTION]?.[name] ?? null;
166
- }
167
- const { section, key } = assertConfigPath(name);
168
- return all[section]?.[key] ?? null;
169
- }
170
- /** 列出 key(凭据只返回 key 名,不返回值) */
171
- function listKeys(root, scope) {
172
- const data = readStore(root, scope);
173
- if (scope === "credential") return Object.keys(data);
174
- return Object.entries(data).flatMap(([section, entries]) => Object.keys(entries).map((key) => `${section}.${key}`));
175
- }
176
- /**
177
- * doc 说明文本:输出存储位置/格式/key 规范/git 策略/读写方法。
178
- * agent 据此可直接用 fs 工具(read/write)自己读写凭据/配置。
179
- */
180
- function docText(root) {
181
- const path = localstorePath(root);
182
- return [
183
- "# localstore — ACC local credential/config storage (standard, S134 redesign)",
184
- "",
185
- "One tool manages two namespaces: credential (credentials) + config (preferences).",
186
- `Stored at the CCC root in ${path} (JSON format; MSMs can read + JSON.parse directly).`,
187
- "",
188
- "## Git commit policy",
189
- "- Config: .opencode/serenity.json `localstore.gitTrack`: `\"allow\"` (may commit) | `\"deny\"` (must not commit; .dsh fallback)",
190
- "- **Default deny (unset = not committed)**; when deny, writes automatically ensure .gitignore contains localstore.json",
191
- " (physical guarantee, independent of dsh runtime); container_git commit checks and refuses",
192
- "- To commit: set `\"localstore\": { \"gitTrack\": \"allow\" }` and remove localstore.json from .gitignore",
193
- "",
194
- "## Format (JSON top-level sections)",
195
- "```json",
196
- "{",
197
- " \"credentials\": {",
198
- " \"HOME_GITLAB_TOKEN\": \"xxx\"",
199
- " },",
200
- " \"cheap-worker\": {",
201
- " \"models\": [\"minimax-cn-coding-plan/MiniMax-M3\"]",
202
- " }",
203
- "}",
204
- "```",
205
- "- credentials is a reserved section (credentials, UPPER_SNAKE keys); other sections = config (path = section.key)",
206
- "",
207
- "## Key conventions",
208
- "- credential key: ^[A-Z][A-Z0-9_]*$ (e.g. HOME_GITLAB_TOKEN)",
209
- "- config path: section.key (section ^[a-z][a-z0-9-]*$, key lowerCamel ^[a-z][a-zA-Z0-9_]*$, e.g. cheap-worker.models)",
210
- "",
211
- "## Reading (agents may use fs tools directly)",
212
- `- Read ${path} with the read tool → JSON.parse`,
213
- "- Or use localstore get <name> [--scope credential|config]",
214
- "",
215
- "## Writing",
216
- "- Recommended: localstore set <name> <value> [--scope ...] (auto-creates file / preserves other entries / syncs .gitignore)",
217
- "- Or modify the file directly with write/edit, keeping the JSON valid",
218
- "",
219
- "## Security boundary",
220
- "- list/show return only key names for credentials, never values",
221
- "- Credential values should be used internally by the agent; never write them into conversation or logs",
222
- "- Default deny: the file is not committed to git (.gitignore physical guarantee + container_git check fallback)",
223
- ""
224
- ].join("\n");
225
- }
226
- /** 运行 localstore 操作(纯逻辑;返回 JSON 值供工具 render) */
227
- function runLocalStore(root, args) {
228
- const scope = args.scope === "config" ? "config" : "credential";
229
- switch (args.action) {
230
- case "list": return {
231
- scope,
232
- keys: listKeys(root, scope)
233
- };
234
- case "get": {
235
- if (!args.name) throw new Error("get requires name");
236
- const value = getEntry(root, scope, args.name);
237
- if (value === null) throw new Error(`not found: ${args.name} (scope=${scope})`);
238
- return {
239
- scope,
240
- name: args.name,
241
- value,
242
- source: scope
243
- };
244
- }
245
- case "set": {
246
- if (!args.name) throw new Error("set requires name");
247
- if (args.value === void 0) throw new Error("set requires value");
248
- writeEntry(root, scope, args.name, args.value);
249
- const git = checkLocalstoreGitCompliance(root);
250
- return {
251
- scope,
252
- name: args.name,
253
- set: true,
254
- path: localstorePath(root),
255
- gitTrack: readGitTrack(root),
256
- gitOk: git.ok,
257
- git: git.reason ? { warning: git.reason } : null
258
- };
259
- }
260
- case "unset": {
261
- if (!args.name) throw new Error("unset requires name");
262
- const removed = unsetEntry(root, scope, args.name);
263
- return {
264
- scope,
265
- name: args.name,
266
- removed
267
- };
268
- }
269
- case "show": {
270
- if (!args.name) throw new Error("show requires name");
271
- const exists = getEntry(root, scope, args.name) !== null;
272
- return {
273
- scope,
274
- name: args.name,
275
- exists,
276
- path: localstorePath(root)
277
- };
278
- }
279
- case "doc": return { doc: docText(root) };
280
- default: throw new Error(`Unknown subcommand: ${args.action} (available: list/get/set/unset/show/doc)`);
281
- }
282
- }
283
- //#endregion
284
7
  //#region src/im/route.ts
285
8
  /**
286
9
  * im/route.ts — 微信桥 CCC 配置层纯逻辑(F4c-3,v1.27.0 实验性)
@@ -693,4 +416,4 @@ function setWeixinEnabled(root, enabled) {
693
416
  return next;
694
417
  }
695
418
  //#endregion
696
- export { runLocalStore as _, readFeishuCredential as a, route_exports as c, weixinSessionIdForTrajectory as d, LOCALSTORE_SCOPES as f, readStore as g, readGitTrack as h, matchWeixinRoute as i, sanitizeFileName as l, localstorePath as m, extractWeixinText as n, readWeixinCredential as o, checkLocalstoreGitCompliance as p, hasVoiceItem as r, readWeixinSettings as s, extractWeixinMedia as t, weixinInboundDir as u };
419
+ export { readFeishuCredential as a, route_exports as c, weixinSessionIdForTrajectory as d, matchWeixinRoute as i, sanitizeFileName as l, extractWeixinText as n, readWeixinCredential as o, hasVoiceItem as r, readWeixinSettings as s, extractWeixinMedia as t, weixinInboundDir as u };