@shgroup/dsh-serenity-hooks 1.26.16 → 1.27.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.
@@ -0,0 +1,473 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
+ import { s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS } from "./ccc-CfDrlfA7.js";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { join, resolve } from "node:path";
5
+ import { createHash } from "node:crypto";
6
+ //#region src/localstore-ops.ts
7
+ /**
8
+ * localstore-ops.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 也不会误提交),cc_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,如 handyman.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
+ * cc_git 联动检查(第二道防线):文件存在 && deny && .gitignore 未覆盖 → 不通过。
69
+ * 调用方(cc_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
+ /** 读取全文件(顶层分节);文件不存在/坏 JSON 返回空 */
81
+ function readAll(root) {
82
+ const p = localstorePath(root);
83
+ if (!existsSync(p)) return {};
84
+ try {
85
+ const v = JSON.parse(readFileSync(p, "utf-8").replace(/^\uFEFF/, ""));
86
+ if (v && typeof v === "object" && !Array.isArray(v)) return v;
87
+ return {};
88
+ } catch {
89
+ return {};
90
+ }
91
+ }
92
+ /** 写回全文件(2 空格缩进 + 尾换行,方便 MSM 直接读取) */
93
+ function writeAll(root, data) {
94
+ writeFileSync(localstorePath(root), JSON.stringify(data, null, 2) + "\n", "utf-8");
95
+ }
96
+ /** 读取命名空间全部条目:credential → 扁平;config → 分节(剔除 credentials 保留节) */
97
+ function readStore(root, scope) {
98
+ const all = readAll(root);
99
+ if (scope === "credential") return all["credentials"] ?? {};
100
+ const { [CREDENTIALS_SECTION]: _cred, ...rest } = all;
101
+ return rest;
102
+ }
103
+ /** 校验凭据 key 合法(大写蛇形);不合法抛错 */
104
+ function assertCredentialKey(key) {
105
+ 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)`);
106
+ }
107
+ /** 校验 config 路径(节.key);不合法抛错 */
108
+ function assertConfigPath(path) {
109
+ const idx = path.indexOf(".");
110
+ if (idx <= 0 || idx === path.length - 1) throw new Error(`config path "${path}" must be section.key (e.g. handyman.models)`);
111
+ const section = path.slice(0, idx);
112
+ const key = path.slice(idx + 1);
113
+ if (!CONFIG_SECTION_RE.test(section)) throw new Error(`config section "${section}" must match ^[a-z][a-z0-9-]*$`);
114
+ if (!CONFIG_KEY_RE.test(key)) throw new Error(`config key "${key}" must match lowerCamel ^[a-z][a-zA-Z0-9_]*$ (e.g. defaultModel)`);
115
+ return {
116
+ section,
117
+ key
118
+ };
119
+ }
120
+ /** 写入单个条目(自动建文件;deny 默认时同步确保 .gitignore 物理保证) */
121
+ function writeEntry(root, scope, name, value) {
122
+ const all = readAll(root);
123
+ if (scope === "credential") {
124
+ assertCredentialKey(name);
125
+ all[CREDENTIALS_SECTION] ??= {};
126
+ all[CREDENTIALS_SECTION][name] = value;
127
+ } else {
128
+ const { section, key } = assertConfigPath(name);
129
+ all[section] ??= {};
130
+ all[section][key] = value;
131
+ }
132
+ writeAll(root, all);
133
+ ensureLocalstoreGitignored(root);
134
+ }
135
+ /** 删除单个条目(不存在返回 false);空节自动移除 */
136
+ function unsetEntry(root, scope, name) {
137
+ const all = readAll(root);
138
+ if (scope === "credential") {
139
+ assertCredentialKey(name);
140
+ const sec = all[CREDENTIALS_SECTION];
141
+ if (!sec || !(name in sec)) return false;
142
+ delete sec[name];
143
+ if (Object.keys(sec).length === 0) delete all[CREDENTIALS_SECTION];
144
+ } else {
145
+ const { section, key } = assertConfigPath(name);
146
+ const sec = all[section];
147
+ if (!sec || !(key in sec)) return false;
148
+ delete sec[key];
149
+ if (Object.keys(sec).length === 0) delete all[section];
150
+ }
151
+ writeAll(root, all);
152
+ return true;
153
+ }
154
+ /** 读取单个条目值;不存在返回 null */
155
+ function getEntry(root, scope, name) {
156
+ const all = readAll(root);
157
+ if (scope === "credential") {
158
+ assertCredentialKey(name);
159
+ return all["credentials"]?.[name] ?? null;
160
+ }
161
+ const { section, key } = assertConfigPath(name);
162
+ return all[section]?.[key] ?? null;
163
+ }
164
+ /** 列出 key(凭据只返回 key 名,不返回值) */
165
+ function listKeys(root, scope) {
166
+ const data = readStore(root, scope);
167
+ if (scope === "credential") return Object.keys(data);
168
+ return Object.entries(data).flatMap(([section, entries]) => Object.keys(entries).map((key) => `${section}.${key}`));
169
+ }
170
+ /**
171
+ * doc 说明文本:输出存储位置/格式/key 规范/git 策略/读写方法。
172
+ * agent 据此可直接用 fs 工具(read/write)自己读写凭据/配置。
173
+ */
174
+ function docText(root) {
175
+ const path = localstorePath(root);
176
+ return [
177
+ "# localstore — ACC local credential/config storage (standard, S134 redesign)",
178
+ "",
179
+ "One tool manages two namespaces: credential (credentials) + config (preferences).",
180
+ `Stored at the CCC root in ${path} (JSON format; MSMs can read + JSON.parse directly).`,
181
+ "",
182
+ "## Git commit policy",
183
+ "- Config: .opencode/serenity.json `localstore.gitTrack`: `\"allow\"` (may commit) | `\"deny\"` (must not commit; .dsh fallback)",
184
+ "- **Default deny (unset = not committed)**; when deny, writes automatically ensure .gitignore contains localstore.json",
185
+ " (physical guarantee, independent of dsh runtime); cc_git commit checks and refuses",
186
+ "- To commit: set `\"localstore\": { \"gitTrack\": \"allow\" }` and remove localstore.json from .gitignore",
187
+ "",
188
+ "## Format (JSON top-level sections)",
189
+ "```json",
190
+ "{",
191
+ " \"credentials\": {",
192
+ " \"HOME_GITLAB_TOKEN\": \"xxx\"",
193
+ " },",
194
+ " \"handyman\": {",
195
+ " \"models\": [\"minimax-cn-coding-plan/MiniMax-M3\"]",
196
+ " }",
197
+ "}",
198
+ "```",
199
+ "- credentials is a reserved section (credentials, UPPER_SNAKE keys); other sections = config (path = section.key)",
200
+ "",
201
+ "## Key conventions",
202
+ "- credential key: ^[A-Z][A-Z0-9_]*$ (e.g. HOME_GITLAB_TOKEN)",
203
+ "- config path: section.key (section ^[a-z][a-z0-9-]*$, key lowerCamel ^[a-z][a-zA-Z0-9_]*$, e.g. handyman.models)",
204
+ "",
205
+ "## Reading (agents may use fs tools directly)",
206
+ `- Read ${path} with the read tool → JSON.parse`,
207
+ "- Or use localstore get <name> [--scope credential|config]",
208
+ "",
209
+ "## Writing",
210
+ "- Recommended: localstore set <name> <value> [--scope ...] (auto-creates file / preserves other entries / syncs .gitignore)",
211
+ "- Or modify the file directly with write/edit, keeping the JSON valid",
212
+ "",
213
+ "## Security boundary",
214
+ "- list/show return only key names for credentials, never values",
215
+ "- Credential values should be used internally by the agent; never write them into conversation or logs",
216
+ "- Default deny: the file is not committed to git (.gitignore physical guarantee + cc_git check fallback)",
217
+ ""
218
+ ].join("\n");
219
+ }
220
+ /** 运行 localstore 操作(纯逻辑;返回 JSON 值供工具 render) */
221
+ function runLocalStore(root, args) {
222
+ const scope = args.scope === "config" ? "config" : "credential";
223
+ switch (args.action) {
224
+ case "list": return {
225
+ scope,
226
+ keys: listKeys(root, scope)
227
+ };
228
+ case "get": {
229
+ if (!args.name) throw new Error("get requires name");
230
+ const value = getEntry(root, scope, args.name);
231
+ if (value === null) throw new Error(`not found: ${args.name} (scope=${scope})`);
232
+ return {
233
+ scope,
234
+ name: args.name,
235
+ value,
236
+ source: scope
237
+ };
238
+ }
239
+ case "set": {
240
+ if (!args.name) throw new Error("set requires name");
241
+ if (args.value === void 0) throw new Error("set requires value");
242
+ writeEntry(root, scope, args.name, args.value);
243
+ const git = checkLocalstoreGitCompliance(root);
244
+ return {
245
+ scope,
246
+ name: args.name,
247
+ set: true,
248
+ path: localstorePath(root),
249
+ gitTrack: readGitTrack(root),
250
+ gitOk: git.ok,
251
+ git: git.reason ? { warning: git.reason } : null
252
+ };
253
+ }
254
+ case "unset": {
255
+ if (!args.name) throw new Error("unset requires name");
256
+ const removed = unsetEntry(root, scope, args.name);
257
+ return {
258
+ scope,
259
+ name: args.name,
260
+ removed
261
+ };
262
+ }
263
+ case "show": {
264
+ if (!args.name) throw new Error("show requires name");
265
+ const exists = getEntry(root, scope, args.name) !== null;
266
+ return {
267
+ scope,
268
+ name: args.name,
269
+ exists,
270
+ path: localstorePath(root)
271
+ };
272
+ }
273
+ case "doc": return { doc: docText(root) };
274
+ default: throw new Error(`Unknown subcommand: ${args.action} (available: list/get/set/unset/show/doc)`);
275
+ }
276
+ }
277
+ //#endregion
278
+ //#region src/weixin-route.ts
279
+ /**
280
+ * weixin-route.ts — 微信桥 CCC 配置层纯逻辑(F4c-3,v1.27.0 实验性)
281
+ *
282
+ * 职责(零 DSH 依赖,可独立单测):
283
+ * - 读取 CCC 的 weixin 配置(.opencode/serenity.json weixin 段——结构/路由/开关)
284
+ * - 读取/写入 CCC localstore 的账号凭据(weixin.accounts.<accountId>.token/.baseUrl/.userId)
285
+ * - 微信用户 → skiff 会话 id 映射(固定可重建:skiff-weixin-<sha256(userid)>.slice(16))
286
+ * - 路由匹配(exact user → 通配 * 兜底)
287
+ *
288
+ * 配置归属(S142 用户拍板):**CCC 级**——serenity.json 结构 + localstore 凭据分离;
289
+ * 不绑定具体 role(用户自选路由目标)。
290
+ */
291
+ var weixin_route_exports = /* @__PURE__ */ __exportAll({
292
+ WEIXIN_SESSION_PREFIX: () => WEIXIN_SESSION_PREFIX,
293
+ clearWeixinCredential: () => clearWeixinCredential,
294
+ extractWeixinText: () => extractWeixinText,
295
+ matchWeixinRoute: () => matchWeixinRoute,
296
+ readWeixinCredential: () => readWeixinCredential,
297
+ readWeixinSettings: () => readWeixinSettings,
298
+ removeWeixinAccount: () => removeWeixinAccount,
299
+ saveWeixinRoutes: () => saveWeixinRoutes,
300
+ setWeixinEnabled: () => setWeixinEnabled,
301
+ upsertWeixinAccount: () => upsertWeixinAccount,
302
+ weixinSessionIdFor: () => weixinSessionIdFor,
303
+ writeWeixinCredential: () => writeWeixinCredential
304
+ });
305
+ /** localstore 凭据键前缀(**credential scope**——token 是凭据,走 UPPER_SNAKE 键;
306
+ * 格式 WEIXIN_<ACCOUNT_ID>_TOKEN / _BASEURL / _USERID(accountId 归一为大写蛇形)) */
307
+ const LS_PREFIX = "WEIXIN_";
308
+ /** accountId → UPPER_SNAKE 片段(wechat-1 → WECHAT_1) */
309
+ function accountKeyPart(accountId) {
310
+ return accountId.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
311
+ }
312
+ /** 微信桥 skiff 会话 id 前缀(seams 旁路判定;对外面纯净——守卫识别外部面) */
313
+ const WEIXIN_SESSION_PREFIX = "skiff-weixin-";
314
+ /** 微信用户 → 固定会话 id(同用户长期同一会话,记忆延续;多用户天然隔离) */
315
+ function weixinSessionIdFor(fromUserId) {
316
+ const digest = createHash("sha256").update(fromUserId).digest("hex").slice(0, 16);
317
+ return `${WEIXIN_SESSION_PREFIX}${digest}`;
318
+ }
319
+ /**
320
+ * 读取 CCC 微信桥配置(未配置/未启用 → 归一默认)。
321
+ * @returns 归一化 WeixinSettings(accounts/routes 数组清洗;空段 → 空数组)
322
+ */
323
+ function readWeixinSettings(root, paths = DEFAULT_SERENITY_CONFIG_PATHS) {
324
+ try {
325
+ const w = loadSerenityConfig(root, paths).weixin;
326
+ if (!w || typeof w !== "object") return {
327
+ enabled: false,
328
+ accounts: [],
329
+ routes: []
330
+ };
331
+ return {
332
+ enabled: w.enabled === true,
333
+ botType: typeof w.botType === "string" && w.botType !== "" ? w.botType : void 0,
334
+ accounts: Array.isArray(w.accounts) ? w.accounts.filter((a) => typeof a === "object" && a !== null && typeof a.accountId === "string" && a.accountId !== "").map((a) => ({
335
+ accountId: a.accountId,
336
+ name: typeof a.name === "string" ? a.name : void 0,
337
+ enabled: a.enabled !== false
338
+ })) : [],
339
+ routes: Array.isArray(w.routes) ? w.routes.filter((r) => typeof r === "object" && r !== null && typeof r.user === "string" && r.user !== "" && typeof r.role === "string" && r.role !== "") : []
340
+ };
341
+ } catch {
342
+ return {
343
+ enabled: false,
344
+ accounts: [],
345
+ routes: []
346
+ };
347
+ }
348
+ }
349
+ /** localstore 凭据键(credential scope: WEIXIN_<ACCOUNT_ID>_<FIELD>) */
350
+ function lsKey(accountId, field) {
351
+ return `${LS_PREFIX}${accountKeyPart(accountId)}_${field}`;
352
+ }
353
+ /** 读取账号凭据(token 缺失 → null——账号未绑定) */
354
+ function readWeixinCredential(root, accountId) {
355
+ const token = getEntry(root, "credential", lsKey(accountId, "TOKEN"));
356
+ if (!token) return null;
357
+ const baseUrl = getEntry(root, "credential", lsKey(accountId, "BASEURL"));
358
+ const userId = getEntry(root, "credential", lsKey(accountId, "USERID"));
359
+ return {
360
+ token,
361
+ baseUrl: baseUrl ?? "",
362
+ userId: userId ?? void 0
363
+ };
364
+ }
365
+ /** 写入账号凭据(扫码 confirmed 后;token 必写,baseUrl/userId 可选) */
366
+ function writeWeixinCredential(root, accountId, cred) {
367
+ writeEntry(root, "credential", lsKey(accountId, "TOKEN"), cred.token);
368
+ if (cred.baseUrl) writeEntry(root, "credential", lsKey(accountId, "BASEURL"), cred.baseUrl);
369
+ if (cred.userId) writeEntry(root, "credential", lsKey(accountId, "USERID"), cred.userId);
370
+ }
371
+ /** 清除账号凭据(移除账号时) */
372
+ function clearWeixinCredential(root, accountId) {
373
+ unsetEntry(root, "credential", lsKey(accountId, "TOKEN"));
374
+ unsetEntry(root, "credential", lsKey(accountId, "BASEURL"));
375
+ unsetEntry(root, "credential", lsKey(accountId, "USERID"));
376
+ }
377
+ /**
378
+ * 路由匹配:exact user 优先,通配 `*` 兜底。
379
+ * @returns 命中的 role 名;未命中 → null(无路由 → 消息不回复/忽略)
380
+ */
381
+ function matchWeixinRoute(routes, fromUserId) {
382
+ if (!Array.isArray(routes) || routes.length === 0) return null;
383
+ for (const r of routes) if (r.user === fromUserId) return r.role;
384
+ for (const r of routes) if (r.user === "*") return r.role;
385
+ return null;
386
+ }
387
+ /**
388
+ * 提取消息文本(item_list 首条 text_item;非文本消息 → null——P1 只处理文本)。
389
+ * @returns 纯文本;无文本 → null
390
+ */
391
+ function extractWeixinText(msg) {
392
+ const items = msg.item_list;
393
+ if (!Array.isArray(items) || items.length === 0) return null;
394
+ for (const item of items) if (item.type === 1 && typeof item.text_item?.text === "string") {
395
+ const text = item.text_item.text.trim();
396
+ return text === "" ? null : text;
397
+ }
398
+ return null;
399
+ }
400
+ /**
401
+ * 读取 serenity.json 文件(原始;不存在 → 空对象)。
402
+ * 注意:loadSerenityConfig 返回解析对象但无法写回——此处直接文件读写。
403
+ */
404
+ function readRawConfig(root, paths = DEFAULT_SERENITY_CONFIG_PATHS) {
405
+ for (const candidate of paths) {
406
+ const p = resolve(root, candidate);
407
+ if (existsSync(p)) try {
408
+ const v = JSON.parse(readFileSync(p, "utf-8").replace(/^\uFEFF/, ""));
409
+ if (v && typeof v === "object" && !Array.isArray(v)) return v;
410
+ } catch {
411
+ return {};
412
+ }
413
+ }
414
+ return {};
415
+ }
416
+ /** 写入 serenity.json(保留既有字段;weixin 段整体替换) */
417
+ function writeWeixinSection(root, weixin, paths = DEFAULT_SERENITY_CONFIG_PATHS) {
418
+ const target = resolve(root, paths[0] ?? ".opencode/serenity.json");
419
+ const raw = readRawConfig(root, paths);
420
+ raw.weixin = weixin;
421
+ const dir = resolve(target, "..");
422
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
423
+ writeFileSync(target, JSON.stringify(raw, null, 2) + "\n", "utf-8");
424
+ }
425
+ /** 添加/更新账号(serenity.json 结构部分;凭据单独写 localstore) */
426
+ function upsertWeixinAccount(root, account) {
427
+ const settings = readWeixinSettings(root);
428
+ const accounts = settings.accounts ?? [];
429
+ const idx = accounts.findIndex((a) => a.accountId === account.accountId);
430
+ if (idx >= 0) accounts[idx] = {
431
+ ...accounts[idx],
432
+ ...account
433
+ };
434
+ else accounts.push(account);
435
+ const next = {
436
+ ...settings,
437
+ accounts
438
+ };
439
+ writeWeixinSection(root, next);
440
+ return next;
441
+ }
442
+ /** 移除账号(serenity.json 结构 + localstore 凭据;同时停桥由调用方 syncCccBridge) */
443
+ function removeWeixinAccount(root, accountId) {
444
+ const settings = readWeixinSettings(root);
445
+ const accounts = (settings.accounts ?? []).filter((a) => a.accountId !== accountId);
446
+ const next = {
447
+ ...settings,
448
+ accounts
449
+ };
450
+ writeWeixinSection(root, next);
451
+ clearWeixinCredential(root, accountId);
452
+ return next;
453
+ }
454
+ /** 保存路由表(整体替换) */
455
+ function saveWeixinRoutes(root, routes) {
456
+ const next = {
457
+ ...readWeixinSettings(root),
458
+ routes
459
+ };
460
+ writeWeixinSection(root, next);
461
+ return next;
462
+ }
463
+ /** 切换总开关 */
464
+ function setWeixinEnabled(root, enabled) {
465
+ const next = {
466
+ ...readWeixinSettings(root),
467
+ enabled
468
+ };
469
+ writeWeixinSection(root, next);
470
+ return next;
471
+ }
472
+ //#endregion
473
+ export { weixinSessionIdFor as a, checkLocalstoreGitCompliance as c, runLocalStore as d, readWeixinSettings as i, localstorePath as l, matchWeixinRoute as n, weixin_route_exports as o, readWeixinCredential as r, LOCALSTORE_SCOPES as s, extractWeixinText as t, readGitTrack as u };
@@ -0,0 +1,61 @@
1
+ /**
2
+ * weixin-route.ts — 微信桥 CCC 配置层纯逻辑(F4c-3,v1.27.0 实验性)
3
+ *
4
+ * 职责(零 DSH 依赖,可独立单测):
5
+ * - 读取 CCC 的 weixin 配置(.opencode/serenity.json weixin 段——结构/路由/开关)
6
+ * - 读取/写入 CCC localstore 的账号凭据(weixin.accounts.<accountId>.token/.baseUrl/.userId)
7
+ * - 微信用户 → skiff 会话 id 映射(固定可重建:skiff-weixin-<sha256(userid)>.slice(16))
8
+ * - 路由匹配(exact user → 通配 * 兜底)
9
+ *
10
+ * 配置归属(S142 用户拍板):**CCC 级**——serenity.json 结构 + localstore 凭据分离;
11
+ * 不绑定具体 role(用户自选路由目标)。
12
+ */
13
+ import { type WeixinSettings, type WeixinRouteConfig, type WeixinAccountConfig } from './ccc.js';
14
+ /** 微信桥 skiff 会话 id 前缀(seams 旁路判定;对外面纯净——守卫识别外部面) */
15
+ export declare const WEIXIN_SESSION_PREFIX = "skiff-weixin-";
16
+ /** 判定 sessionId 是否为微信桥会话(外部面——输出守卫/轨迹隐藏生效) */
17
+ export declare function isWeixinSessionId(sessionId: string | undefined): boolean;
18
+ /** 微信用户 → 固定会话 id(同用户长期同一会话,记忆延续;多用户天然隔离) */
19
+ export declare function weixinSessionIdFor(fromUserId: string): string;
20
+ /**
21
+ * 读取 CCC 微信桥配置(未配置/未启用 → 归一默认)。
22
+ * @returns 归一化 WeixinSettings(accounts/routes 数组清洗;空段 → 空数组)
23
+ */
24
+ export declare function readWeixinSettings(root: string, paths?: string[]): WeixinSettings;
25
+ /** 账号凭据(localstore credential scope;baseUrl 缺省 = 官方默认) */
26
+ export interface WeixinAccountCredential {
27
+ token: string;
28
+ baseUrl: string;
29
+ userId?: string;
30
+ }
31
+ /** 读取账号凭据(token 缺失 → null——账号未绑定) */
32
+ export declare function readWeixinCredential(root: string, accountId: string): WeixinAccountCredential | null;
33
+ /** 写入账号凭据(扫码 confirmed 后;token 必写,baseUrl/userId 可选) */
34
+ export declare function writeWeixinCredential(root: string, accountId: string, cred: WeixinAccountCredential): void;
35
+ /** 清除账号凭据(移除账号时) */
36
+ export declare function clearWeixinCredential(root: string, accountId: string): void;
37
+ /**
38
+ * 路由匹配:exact user 优先,通配 `*` 兜底。
39
+ * @returns 命中的 role 名;未命中 → null(无路由 → 消息不回复/忽略)
40
+ */
41
+ export declare function matchWeixinRoute(routes: WeixinRouteConfig[], fromUserId: string): string | null;
42
+ /**
43
+ * 提取消息文本(item_list 首条 text_item;非文本消息 → null——P1 只处理文本)。
44
+ * @returns 纯文本;无文本 → null
45
+ */
46
+ export declare function extractWeixinText(msg: {
47
+ item_list?: Array<{
48
+ type?: number;
49
+ text_item?: {
50
+ text?: string;
51
+ };
52
+ }>;
53
+ }): string | null;
54
+ /** 添加/更新账号(serenity.json 结构部分;凭据单独写 localstore) */
55
+ export declare function upsertWeixinAccount(root: string, account: WeixinAccountConfig): WeixinSettings;
56
+ /** 移除账号(serenity.json 结构 + localstore 凭据;同时停桥由调用方 syncCccBridge) */
57
+ export declare function removeWeixinAccount(root: string, accountId: string): WeixinSettings;
58
+ /** 保存路由表(整体替换) */
59
+ export declare function saveWeixinRoutes(root: string, routes: WeixinRouteConfig[]): WeixinSettings;
60
+ /** 切换总开关 */
61
+ export declare function setWeixinEnabled(root: string, enabled: boolean): WeixinSettings;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.26.16",
3
+ "version": "1.27.0",
4
4
  "description": "宁静号 ACC harness — Native Cordis 插件(DeepSeek Harness 运行时)。真实 DSH 工具注册(cc_fs/session/acc_msm 等 9 工具)+ 拦截缝机械约束(safe-mode/路径守卫/会话落盘)+ 系统提示词注入(ACC/CCE/Constraints/SKILL/Session 五块)。适配 DSH 公开版(deepseek-ai/deepseek-harness 0.1.0-rc)。",
5
5
  "license": "MIT",
6
6
  "repository": {