@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.
package/lib/index.js CHANGED
@@ -1,4 +1,9 @@
1
- import { a as findSerenityRoot, c as matchBlacklist, d as readHandymanConfig, f as readUtf8, i as findGitRoot, l as pathInside, n as SAFE_MODE_MARKER, o as isSafeModeOn, p as resolveInside, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-CfDrlfA7.js";
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
+ import { a as findSerenityRoot, c as matchBlacklist, d as readHandymanConfig, i as findGitRoot, l as pathInside, n as SAFE_MODE_MARKER, o as isSafeModeOn, p as resolveInside, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-CfDrlfA7.js";
3
+ import { a as resolveRoleSystemPrompt, i as readSkiffRoles, l as systemPromptSource, r as isSkiffSessionId, s as roleToolWhitelist } from "./skiff-role-Dw7UKCUm.js";
4
+ import { _ as writeFailedStatus, a as skiffSessionInfo, c as unregisterSkiffSession, d as handymanProgressPaths, f as listActiveHandymen, g as splitModel, h as requireWhitelistedModel, i as skiffMsmGate, l as HANDYMAN_GUIDE, m as readProgress, n as createSkiffAgent, o as skiffSessionSnapshot, p as newStopToken, r as getSkiffAgent, s as skiffTrajectoryEnabled, t as askSkiff, u as buildRoundPrompt, v as writeProgress, y as skiffRoleFor } from "./skiff-core-BewZSbVH.js";
5
+ import { a as weixinSessionIdFor, c as checkLocalstoreGitCompliance, d as runLocalStore, i as readWeixinSettings, l as localstorePath, n as matchWeixinRoute, r as readWeixinCredential, s as LOCALSTORE_SCOPES, t as extractWeixinText, u as readGitTrack } from "./weixin-route-CHCWBvLZ.js";
6
+ import { n as sendTextMessage, t as getUpdates } from "./weixin-api-b8HLmcPE.js";
2
7
  import z from "@deepseek-ai/schemastery";
3
8
  import { defineTool } from "@deepseek-ai/dsh-tools";
4
9
  import { appendFileSync, chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
@@ -12,18 +17,6 @@ import { createUserMessage } from "@deepseek-ai/dsh-llm";
12
17
  import { createHmac, randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
13
18
  import { deriveEventMessage } from "@deepseek-ai/dsh-session";
14
19
  import { createServer, request } from "node:http";
15
- //#region \0rolldown/runtime.js
16
- var __defProp = Object.defineProperty;
17
- var __exportAll = (all, no_symbols) => {
18
- let target = {};
19
- for (var name in all) __defProp(target, name, {
20
- get: all[name],
21
- enumerable: true
22
- });
23
- if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
24
- return target;
25
- };
26
- //#endregion
27
20
  //#region src/fs-ops.ts
28
21
  /**
29
22
  * fs-ops.ts — cc_fs 纯操作层(零 DSH 依赖,可独立单测)
@@ -484,141 +477,6 @@ const ACC_VERSION = (() => {
484
477
  }
485
478
  })();
486
479
  //#endregion
487
- //#region src/skiff-role.ts
488
- /**
489
- * skiff-role.ts — Skiff(F4,v1.25.0 实验性)角色层纯逻辑(零 DSH 依赖,可独立单测)
490
- *
491
- * 概念(S142 用户拍板):Skiff = 完整宁静号 trajectory(在宁静号内全知全能)的
492
- * **任意子集**——CCC 通过角色配置定义:能力面(tools 非 MSM 工具白名单 + msms
493
- * MSM 白名单,双白名单独立,白名单外全隐藏)+ 轨迹纪律面(trajectory 子集)+
494
- * 系统提示词(CCC 完整定义,dsp 只给基础部分)。
495
- *
496
- * 实验性质:未配置任何角色 → Skiff 完全零影响(无监听、无 agent 创建、guard 无规则)。
497
- */
498
- /** Skiff agent 会话 id 前缀(agents.create 生成;seams 旁路/白名单判定用) */
499
- const SKIFF_SESSION_PREFIX = "skiff-";
500
- /** 判定 sessionId 是否为 Skiff 会话(仿 handyman- 前缀排除模式) */
501
- function isSkiffSessionId(sessionId) {
502
- return typeof sessionId === "string" && sessionId.startsWith("skiff-");
503
- }
504
- /**
505
- * 读取 CCC 的 Skiff 角色配置(.opencode/serenity.json skiff.roles)。
506
- * @returns 名 → 角色配置 的 Map;未配置(无 skiff 段/空 roles)返回空 Map(Skiff 未启用)
507
- */
508
- function readSkiffRoles(root, paths = DEFAULT_SERENITY_CONFIG_PATHS) {
509
- const out = /* @__PURE__ */ new Map();
510
- try {
511
- const roles = loadSerenityConfig(root, paths).skiff?.roles;
512
- if (!roles || typeof roles !== "object") return out;
513
- for (const [name, role] of Object.entries(roles)) {
514
- if (!role || typeof role !== "object") continue;
515
- if (name.trim() === "") continue;
516
- out.set(name.trim(), {
517
- model: typeof role.model === "string" ? role.model : void 0,
518
- msms: Array.isArray(role.msms) ? role.msms.filter((m) => typeof m === "string") : void 0,
519
- tools: Array.isArray(role.tools) ? role.tools.filter((t) => typeof t === "string") : void 0,
520
- trajectory: role.trajectory && typeof role.trajectory === "object" ? {
521
- session: role.trajectory.session === true,
522
- keeper: role.trajectory.keeper === true,
523
- rebuild: role.trajectory.rebuild === true
524
- } : void 0,
525
- systemPrompt: typeof role.systemPrompt === "string" ? role.systemPrompt : void 0,
526
- systemPromptFile: typeof role.systemPromptFile === "string" ? role.systemPromptFile : void 0
527
- });
528
- }
529
- } catch {}
530
- return out;
531
- }
532
- function trajectorySubset(role) {
533
- return {
534
- session: role?.trajectory?.session === true,
535
- keeper: role?.trajectory?.keeper === true,
536
- rebuild: role?.trajectory?.rebuild === true
537
- };
538
- }
539
- /** 角色可用工具面(白名单并集):tools + acc_msm(msms 非空时作为 MSM 通道自动可用) */
540
- function roleToolWhitelist(role) {
541
- const out = /* @__PURE__ */ new Set();
542
- for (const t of role?.tools ?? []) out.add(t);
543
- if ((role?.msms?.length ?? 0) > 0) out.add("acc_msm");
544
- return out;
545
- }
546
- /** 角色允许的 MSM 白名单(acc_msm exec 校验 / msm_list 过滤用;独立于 tools 白名单) */
547
- function roleMsmWhitelist(role) {
548
- return new Set(role?.msms ?? []);
549
- }
550
- /**
551
- * 解析角色的系统提示词全文(v1.25.10,S142 用户:超长提示词 JSON 内嵌不可读):
552
- * ① `systemPromptFile` 存在 → 读取文件内容(**推荐配置方法**;相对 CCC 根,
553
- * 路径逃逸拒绝(resolveInside)+ BOM 剥除(readUtf8)+ 存在性校验)
554
- * ② 否则 → 内嵌 `systemPrompt`(兼容旧配置)
555
- * ③ 都无 → 空字符串
556
- * 文件缺失/逃逸 → 抛错(调用方 catch 降级 + validate 报 issue)。
557
- * 懒读取:readSkiffRoles 不读文件(guards/seams 每次工具调用查询的热路径零 IO),
558
- * 仅在本函数(创建 agent / validate / list 时)读取。
559
- */
560
- function resolveRoleSystemPrompt(root, role) {
561
- if (!role) return "";
562
- const file = role.systemPromptFile?.trim();
563
- if (file) {
564
- const abs = resolveInside(root, file);
565
- if (!existsSync(abs)) throw new Error(`skiff role "${role.systemPrompt ?? "(unnamed)"}": systemPromptFile "${file}" not found (resolved: ${abs})`);
566
- return readUtf8(abs).trim();
567
- }
568
- return role.systemPrompt ?? "";
569
- }
570
- /** 角色系统提示词来源(validate/list 展示用) */
571
- function systemPromptSource(role) {
572
- if (role?.systemPromptFile?.trim()) return "file";
573
- if (role?.systemPrompt?.trim()) return "inline";
574
- return "none";
575
- }
576
- /**
577
- * Skiff 基础提示词(dsp 只给这部分;CCC 的 systemPrompt 段由调用方拼接):
578
- * 身份 + 可用 MSM/工具清单 + 调用协议 + 边界声明。动态生成(清单来自角色白名单)。
579
- */
580
- function buildSkiffBasePrompt(roleName, role) {
581
- const msms = role?.msms ?? [];
582
- const tools = role?.tools ?? [];
583
- const lines = [
584
- "=== Serenity Skiff ===",
585
- `Role: ${roleName} (defined by this CCC)`,
586
- "You interact with this CCC ONLY through the exposed surface below:"
587
- ];
588
- if (msms.length > 0) lines.push(` MSMs: ${msms.join(", ")} (call acc_msm exec <name> [args...]; pass --help as the first arg for usage)`);
589
- else lines.push(" MSMs: (none)");
590
- lines.push(` Tools: ${tools.length > 0 ? tools.join(", ") : "(none)"}`);
591
- lines.push("No other tools are available. Your capability boundary is this surface.");
592
- lines.push("");
593
- lines.push("---");
594
- lines.push("");
595
- return lines.join("\n");
596
- }
597
- //#endregion
598
- //#region src/skiff-registry.ts
599
- const skiffSessions = /* @__PURE__ */ new Map();
600
- /** 查 sessionId 的 Skiff 角色名(无 → null)——向后兼容(guards/seams 依赖) */
601
- function skiffRoleFor$1(sessionId) {
602
- return skiffSessions.get(sessionId)?.role ?? null;
603
- }
604
- /** 查 sessionId 的完整绑定(role + ccc;无 → null)——v1.25.10 会话追问校验 */
605
- function skiffSessionInfo$1(sessionId) {
606
- return skiffSessions.get(sessionId) ?? null;
607
- }
608
- function registerSkiffSession$1(sessionId, role, ccc) {
609
- skiffSessions.set(sessionId, {
610
- role,
611
- ccc
612
- });
613
- }
614
- function unregisterSkiffSession$1(sessionId) {
615
- skiffSessions.delete(sessionId);
616
- }
617
- /** 测试/调试:注册表快照(sessionId → {role, ccc}) */
618
- function skiffSessionSnapshot$1() {
619
- return new Map(skiffSessions);
620
- }
621
- //#endregion
622
480
  //#region src/seams/guards.ts
623
481
  /**
624
482
  * guards.ts — 拦截缝:安全模式 + 路径守卫(P3 语义的机械层)
@@ -675,8 +533,9 @@ function decideGuard(input) {
675
533
  const { root, toolName, safeModeOn, blacklist, pathArg, action } = input;
676
534
  if (input.skiffSessionId !== void 0 && isSkiffSessionId(input.skiffSessionId)) {
677
535
  if (toolName === "skill") return { kind: "allow" };
678
- const roleName = input.skiffSessionId ? skiffRoleFor$1(input.skiffSessionId) : null;
679
- if (!roleToolWhitelist(roleName ? readSkiffRoles(root).get(roleName) : void 0).has(toolName)) return {
536
+ const roleName = input.skiffSessionId ? skiffRoleFor(input.skiffSessionId) : null;
537
+ const role = roleName ? readSkiffRoles(root).get(roleName) : void 0;
538
+ if (!roleToolWhitelist(role).has(toolName)) return {
680
539
  deny: "tool not allowed in this skiff role",
681
540
  kind: "deny"
682
541
  };
@@ -1025,278 +884,6 @@ const kitTool = defineTool({
1025
884
  }
1026
885
  });
1027
886
  //#endregion
1028
- //#region src/localstore-ops.ts
1029
- /**
1030
- * localstore-ops.ts — localstore 纯逻辑层(零 DSH 依赖,可独立单测)
1031
- *
1032
- * S134 重设计(v1.16.7):存储从 ~/.serenity/ 迁到 **CCC 根根目录 localstore.json**
1033
- * (JSON 格式——方便 MSM 直接 read + JSON.parse 读取,零解析依赖)。
1034
- *
1035
- * git 提交策略(可靠机制 × 用户自由):
1036
- * - 配置:.opencode/serenity.json `localstore.gitTrack`: "allow"(可提交)| "deny"(禁提交)
1037
- * - **缺省 deny(没配就是不提交)**;且 deny 的保证**不依赖 dsh 运行**——
1038
- * 写入时自动确保 .gitignore 含 localstore.json(物理保证:即使 dsh 不在、
1039
- * 用户手动 git commit 也不会误提交),cc_git 检查为第二道防线(拒绝 + 提示)
1040
- * - allow:放行(文件可提交,用户自行管理 .gitignore)
1041
- *
1042
- * 存储结构(JSON 顶层分节):
1043
- * { "credentials": { "HOME_GITLAB_TOKEN": "xxx" }, "<config节>": { "<key>": "v" } }
1044
- * credentials 为保留节(credential 命名空间,key 大写蛇形);其余节归 config 命名空间
1045
- * (path = section.key,如 handyman.models)。
1046
- */
1047
- const LOCALSTORE_SCOPES = ["credential", "config"];
1048
- /** 存储文件名(CCC 根根目录) */
1049
- const LOCALSTORE_FILENAME = "localstore.json";
1050
- /** 凭据保留节名(JSON 顶层) */
1051
- const CREDENTIALS_SECTION = "credentials";
1052
- /** 凭据 key 规范:大写蛇形,如 HOME_GITLAB_TOKEN */
1053
- const CREDENTIAL_KEY_RE = /^[A-Z][A-Z0-9_]*$/;
1054
- /** 配置节名规范:小写字母数字连字符 */
1055
- const CONFIG_SECTION_RE = /^[a-z][a-z0-9-]*$/;
1056
- /** 配置 key 规范(节内):小驼峰,如 defaultModel */
1057
- const CONFIG_KEY_RE = /^[a-z][a-zA-Z0-9_]*$/;
1058
- /** 存储文件绝对路径(CCC 根根目录) */
1059
- function localstorePath(root) {
1060
- return join(root, LOCALSTORE_FILENAME);
1061
- }
1062
- /** git 策略:serenity.json localstore.gitTrack,缺省 deny(不提交) */
1063
- function readGitTrack(root, paths = DEFAULT_SERENITY_CONFIG_PATHS) {
1064
- return loadSerenityConfig(root, paths).localstore?.gitTrack === "allow" ? "allow" : "deny";
1065
- }
1066
- /** .gitignore 是否已覆盖 localstore 文件(非空非注释行含文件名即算) */
1067
- function isLocalstoreGitignored(root) {
1068
- const gi = join(root, ".gitignore");
1069
- if (!existsSync(gi)) return false;
1070
- return readFileSync(gi, "utf-8").split("\n").some((l) => {
1071
- const t = l.trim();
1072
- return t !== "" && !t.startsWith("#") && t.includes("localstore.json");
1073
- });
1074
- }
1075
- /**
1076
- * 确保 .gitignore 含 localstore 文件(deny 时的物理保证,不依赖 dsh 运行):
1077
- * 写入 localstore 时调用——deny(含缺省)且 .gitignore 未覆盖 → 自动追加一行。
1078
- * allow → 不写入(放行,文件可提交)。
1079
- */
1080
- function ensureLocalstoreGitignored(root) {
1081
- if (readGitTrack(root) === "allow") return { status: "allow" };
1082
- if (isLocalstoreGitignored(root)) return { status: "ignored" };
1083
- const gi = join(root, ".gitignore");
1084
- const existing = existsSync(gi) ? readFileSync(gi, "utf-8") : "";
1085
- const line = `${LOCALSTORE_FILENAME} # ACC localstore (localstore.gitTrack defaults to deny — do not commit)`;
1086
- writeFileSync(gi, (existing.endsWith("\n") || existing === "" ? existing : existing + "\n") + line + "\n", "utf-8");
1087
- return { status: "appended" };
1088
- }
1089
- /**
1090
- * cc_git 联动检查(第二道防线):文件存在 && deny && .gitignore 未覆盖 → 不通过。
1091
- * 调用方(cc_git commit)据此拒绝提交;status 可输出 warning。
1092
- */
1093
- function checkLocalstoreGitCompliance(root) {
1094
- if (!existsSync(localstorePath(root))) return { ok: true };
1095
- if (readGitTrack(root) === "allow") return { ok: true };
1096
- if (isLocalstoreGitignored(root)) return { ok: true };
1097
- return {
1098
- ok: false,
1099
- 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)`
1100
- };
1101
- }
1102
- /** 读取全文件(顶层分节);文件不存在/坏 JSON 返回空 */
1103
- function readAll(root) {
1104
- const p = localstorePath(root);
1105
- if (!existsSync(p)) return {};
1106
- try {
1107
- const v = JSON.parse(readFileSync(p, "utf-8").replace(/^\uFEFF/, ""));
1108
- if (v && typeof v === "object" && !Array.isArray(v)) return v;
1109
- return {};
1110
- } catch {
1111
- return {};
1112
- }
1113
- }
1114
- /** 写回全文件(2 空格缩进 + 尾换行,方便 MSM 直接读取) */
1115
- function writeAll(root, data) {
1116
- writeFileSync(localstorePath(root), JSON.stringify(data, null, 2) + "\n", "utf-8");
1117
- }
1118
- /** 读取命名空间全部条目:credential → 扁平;config → 分节(剔除 credentials 保留节) */
1119
- function readStore(root, scope) {
1120
- const all = readAll(root);
1121
- if (scope === "credential") return all["credentials"] ?? {};
1122
- const { [CREDENTIALS_SECTION]: _cred, ...rest } = all;
1123
- return rest;
1124
- }
1125
- /** 校验凭据 key 合法(大写蛇形);不合法抛错 */
1126
- function assertCredentialKey(key) {
1127
- 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)`);
1128
- }
1129
- /** 校验 config 路径(节.key);不合法抛错 */
1130
- function assertConfigPath(path) {
1131
- const idx = path.indexOf(".");
1132
- if (idx <= 0 || idx === path.length - 1) throw new Error(`config path "${path}" must be section.key (e.g. handyman.models)`);
1133
- const section = path.slice(0, idx);
1134
- const key = path.slice(idx + 1);
1135
- if (!CONFIG_SECTION_RE.test(section)) throw new Error(`config section "${section}" must match ^[a-z][a-z0-9-]*$`);
1136
- if (!CONFIG_KEY_RE.test(key)) throw new Error(`config key "${key}" must match lowerCamel ^[a-z][a-zA-Z0-9_]*$ (e.g. defaultModel)`);
1137
- return {
1138
- section,
1139
- key
1140
- };
1141
- }
1142
- /** 写入单个条目(自动建文件;deny 默认时同步确保 .gitignore 物理保证) */
1143
- function writeEntry(root, scope, name, value) {
1144
- const all = readAll(root);
1145
- if (scope === "credential") {
1146
- assertCredentialKey(name);
1147
- all[CREDENTIALS_SECTION] ??= {};
1148
- all[CREDENTIALS_SECTION][name] = value;
1149
- } else {
1150
- const { section, key } = assertConfigPath(name);
1151
- all[section] ??= {};
1152
- all[section][key] = value;
1153
- }
1154
- writeAll(root, all);
1155
- ensureLocalstoreGitignored(root);
1156
- }
1157
- /** 删除单个条目(不存在返回 false);空节自动移除 */
1158
- function unsetEntry(root, scope, name) {
1159
- const all = readAll(root);
1160
- if (scope === "credential") {
1161
- assertCredentialKey(name);
1162
- const sec = all[CREDENTIALS_SECTION];
1163
- if (!sec || !(name in sec)) return false;
1164
- delete sec[name];
1165
- if (Object.keys(sec).length === 0) delete all[CREDENTIALS_SECTION];
1166
- } else {
1167
- const { section, key } = assertConfigPath(name);
1168
- const sec = all[section];
1169
- if (!sec || !(key in sec)) return false;
1170
- delete sec[key];
1171
- if (Object.keys(sec).length === 0) delete all[section];
1172
- }
1173
- writeAll(root, all);
1174
- return true;
1175
- }
1176
- /** 读取单个条目值;不存在返回 null */
1177
- function getEntry(root, scope, name) {
1178
- const all = readAll(root);
1179
- if (scope === "credential") {
1180
- assertCredentialKey(name);
1181
- return all["credentials"]?.[name] ?? null;
1182
- }
1183
- const { section, key } = assertConfigPath(name);
1184
- return all[section]?.[key] ?? null;
1185
- }
1186
- /** 列出 key(凭据只返回 key 名,不返回值) */
1187
- function listKeys(root, scope) {
1188
- const data = readStore(root, scope);
1189
- if (scope === "credential") return Object.keys(data);
1190
- return Object.entries(data).flatMap(([section, entries]) => Object.keys(entries).map((key) => `${section}.${key}`));
1191
- }
1192
- /**
1193
- * doc 说明文本:输出存储位置/格式/key 规范/git 策略/读写方法。
1194
- * agent 据此可直接用 fs 工具(read/write)自己读写凭据/配置。
1195
- */
1196
- function docText(root) {
1197
- const path = localstorePath(root);
1198
- return [
1199
- "# localstore — ACC local credential/config storage (standard, S134 redesign)",
1200
- "",
1201
- "One tool manages two namespaces: credential (credentials) + config (preferences).",
1202
- `Stored at the CCC root in ${path} (JSON format; MSMs can read + JSON.parse directly).`,
1203
- "",
1204
- "## Git commit policy",
1205
- "- Config: .opencode/serenity.json `localstore.gitTrack`: `\"allow\"` (may commit) | `\"deny\"` (must not commit; .dsh fallback)",
1206
- "- **Default deny (unset = not committed)**; when deny, writes automatically ensure .gitignore contains localstore.json",
1207
- " (physical guarantee, independent of dsh runtime); cc_git commit checks and refuses",
1208
- "- To commit: set `\"localstore\": { \"gitTrack\": \"allow\" }` and remove localstore.json from .gitignore",
1209
- "",
1210
- "## Format (JSON top-level sections)",
1211
- "```json",
1212
- "{",
1213
- " \"credentials\": {",
1214
- " \"HOME_GITLAB_TOKEN\": \"xxx\"",
1215
- " },",
1216
- " \"handyman\": {",
1217
- " \"models\": [\"minimax-cn-coding-plan/MiniMax-M3\"]",
1218
- " }",
1219
- "}",
1220
- "```",
1221
- "- credentials is a reserved section (credentials, UPPER_SNAKE keys); other sections = config (path = section.key)",
1222
- "",
1223
- "## Key conventions",
1224
- "- credential key: ^[A-Z][A-Z0-9_]*$ (e.g. HOME_GITLAB_TOKEN)",
1225
- "- config path: section.key (section ^[a-z][a-z0-9-]*$, key lowerCamel ^[a-z][a-zA-Z0-9_]*$, e.g. handyman.models)",
1226
- "",
1227
- "## Reading (agents may use fs tools directly)",
1228
- `- Read ${path} with the read tool → JSON.parse`,
1229
- "- Or use localstore get <name> [--scope credential|config]",
1230
- "",
1231
- "## Writing",
1232
- "- Recommended: localstore set <name> <value> [--scope ...] (auto-creates file / preserves other entries / syncs .gitignore)",
1233
- "- Or modify the file directly with write/edit, keeping the JSON valid",
1234
- "",
1235
- "## Security boundary",
1236
- "- list/show return only key names for credentials, never values",
1237
- "- Credential values should be used internally by the agent; never write them into conversation or logs",
1238
- "- Default deny: the file is not committed to git (.gitignore physical guarantee + cc_git check fallback)",
1239
- ""
1240
- ].join("\n");
1241
- }
1242
- /** 运行 localstore 操作(纯逻辑;返回 JSON 值供工具 render) */
1243
- function runLocalStore(root, args) {
1244
- const scope = args.scope === "config" ? "config" : "credential";
1245
- switch (args.action) {
1246
- case "list": return {
1247
- scope,
1248
- keys: listKeys(root, scope)
1249
- };
1250
- case "get": {
1251
- if (!args.name) throw new Error("get requires name");
1252
- const value = getEntry(root, scope, args.name);
1253
- if (value === null) throw new Error(`not found: ${args.name} (scope=${scope})`);
1254
- return {
1255
- scope,
1256
- name: args.name,
1257
- value,
1258
- source: scope
1259
- };
1260
- }
1261
- case "set": {
1262
- if (!args.name) throw new Error("set requires name");
1263
- if (args.value === void 0) throw new Error("set requires value");
1264
- writeEntry(root, scope, args.name, args.value);
1265
- const git = checkLocalstoreGitCompliance(root);
1266
- return {
1267
- scope,
1268
- name: args.name,
1269
- set: true,
1270
- path: localstorePath(root),
1271
- gitTrack: readGitTrack(root),
1272
- gitOk: git.ok,
1273
- git: git.reason ? { warning: git.reason } : null
1274
- };
1275
- }
1276
- case "unset": {
1277
- if (!args.name) throw new Error("unset requires name");
1278
- const removed = unsetEntry(root, scope, args.name);
1279
- return {
1280
- scope,
1281
- name: args.name,
1282
- removed
1283
- };
1284
- }
1285
- case "show": {
1286
- if (!args.name) throw new Error("show requires name");
1287
- const exists = getEntry(root, scope, args.name) !== null;
1288
- return {
1289
- scope,
1290
- name: args.name,
1291
- exists,
1292
- path: localstorePath(root)
1293
- };
1294
- }
1295
- case "doc": return { doc: docText(root) };
1296
- default: throw new Error(`Unknown subcommand: ${args.action} (available: list/get/set/unset/show/doc)`);
1297
- }
1298
- }
1299
- //#endregion
1300
887
  //#region src/git-ops.ts
1301
888
  /**
1302
889
  * git-ops.ts — cc_git 纯操作层(零 DSH 依赖)
@@ -2091,397 +1678,6 @@ async function runMsmAsync(root, args) {
2091
1678
  }
2092
1679
  }
2093
1680
  //#endregion
2094
- //#region src/handyman-ops.ts
2095
- /**
2096
- * handyman-ops.ts — handyman(杂工)纯逻辑层(零 DSH 依赖,可独立单测)
2097
- *
2098
- * v1.24.0:loop(牛马)→ handyman(杂工)重命名。语义对齐 osp loop:
2099
- * 进度文件(handyman-<label>.md/.json)、续跑、轮次 prompt 结构、stop token。
2100
- * 不兼容旧 loop- 进度文件(用户拍板:仅新 handyman- 前缀)。
2101
- */
2102
- /** label 脱敏(Windows 审计问题 17):非法字符 → '-',去尾点/空格,限长(按码点截断,修复代理对切散 U+FFFD) */
2103
- function sanitizeLabel(label) {
2104
- return [...label.replace(/[<>:"/\\|?*\u0000-\u001f]/g, "-").replace(/[ .]+$/g, "")].slice(0, 50).join("");
2105
- }
2106
- function handymanProgressPaths(root, label) {
2107
- const dir = join(root, "AGENT_SESSIONS");
2108
- const safe = sanitizeLabel(label);
2109
- return {
2110
- md: join(dir, `handyman-${safe}.md`),
2111
- json: join(dir, `handyman-${safe}.json`)
2112
- };
2113
- }
2114
- /** 读取进度(续跑);无文件返回 round 0 */
2115
- function readProgress(root, label) {
2116
- const { json } = handymanProgressPaths(root, label);
2117
- if (!existsSync(json)) return null;
2118
- try {
2119
- return JSON.parse(readFileSync(json, "utf-8"));
2120
- } catch {
2121
- return null;
2122
- }
2123
- }
2124
- function writeProgress(root, label, p) {
2125
- const { md, json } = handymanProgressPaths(root, label);
2126
- mkdirSync(join(root, "AGENT_SESSIONS"), { recursive: true });
2127
- writeFileSync(json, JSON.stringify({
2128
- ...p,
2129
- status: p.status ?? "running",
2130
- updated: (/* @__PURE__ */ new Date()).toISOString()
2131
- }, null, 2) + "\n", "utf-8");
2132
- const lines = [
2133
- `# handyman: ${label}`,
2134
- `- Model: ${p.model}`,
2135
- `- Round: ${p.round}`,
2136
- `- Done: ${p.done}`,
2137
- "",
2138
- `## Latest response`,
2139
- "",
2140
- p.lastResponse,
2141
- ""
2142
- ];
2143
- writeFileSync(md, lines.join("\n"), "utf-8");
2144
- }
2145
- /** 失败状态落盘(对齐 osp writeFailedStatus:done=true / status=failed / errorCode) */
2146
- function writeFailedStatus(root, label, info) {
2147
- const { json } = handymanProgressPaths(root, label);
2148
- mkdirSync(join(root, "AGENT_SESSIONS"), { recursive: true });
2149
- const prev = readProgress(root, label);
2150
- writeFileSync(json, JSON.stringify({
2151
- round: prev?.round ?? 0,
2152
- done: true,
2153
- label,
2154
- model: prev?.model ?? "",
2155
- status: "failed",
2156
- errorCode: info.errorCode,
2157
- errorMessage: info.errorMessage,
2158
- updated: (/* @__PURE__ */ new Date()).toISOString(),
2159
- lastResponse: prev?.lastResponse ?? ""
2160
- }, null, 2) + "\n", "utf-8");
2161
- }
2162
- function newStopToken() {
2163
- return `SERENITY_HANDYMAN_DONE_${randomBytes(8).toString("hex")}`;
2164
- }
2165
- /** 解析 model 字符串(provider/model)→ {provider, model};无 / 视为 model-only */
2166
- function splitModel(model) {
2167
- const idx = model.indexOf("/");
2168
- if (idx < 0) return {
2169
- provider: void 0,
2170
- model
2171
- };
2172
- return {
2173
- provider: model.slice(0, idx),
2174
- model: model.slice(idx + 1)
2175
- };
2176
- }
2177
- /** 校验模型在白名单内;不在 → 抛错(用户拍板:只能使用 CCC 配置的模型) */
2178
- function requireWhitelistedModel(model, models) {
2179
- if (!models.includes(model)) throw new Error(`handyman: model "${model}" is not in the CCC whitelist. Configure .opencode/serenity.json "handyman.models" with one of: ${models.join(", ")}`);
2180
- }
2181
- /** 轮次 prompt(对齐老 loop 结构:回顾进度 → 自由工作 → 汇报;S134 EAP 化:固定详尽) */
2182
- function buildRoundPrompt(opts) {
2183
- const { root, session, label, round, stopToken, progress, task } = opts;
2184
- const resumeNote = progress && progress.round > 0 ? `Previous round (round ${progress.round}) completed: ${progress.lastResponse.slice(0, 300)}\nAlways continue from where you left off; never redo completed work.` : "This is the first round.";
2185
- return `# ${label} — handyman round ${round}
2186
-
2187
- CCC root: ${root}
2188
- ${session ? `Work session: ${session} (progress recorded in AGENT_SESSIONS/${session}/SESSION.md)` : ""}
2189
- ${task ? `Task: ${task}` : `Task: follow the work corresponding to label "${label}" (if a work session exists, read SESSION.md first to clarify the goal)`}
2190
- ${resumeNote}
2191
-
2192
- ## Work rules (fixed every round, must follow)
2193
- 1. Work freely within this round: read files, modify code, execute commands — use every means to advance the task.
2194
- 2. If this task is **reading/curating or text-writing work** (extracting from files, summarizing, writing docs, generating text, etc.),
2195
- first load eap (acc-eap skill) and organize output per the EAP standard:
2196
- - E↑ Explicit: entities/variables clearly defined, relationships with direction and cardinality, boundaries drawn, no ambiguous words
2197
- - R↓ Reconstructable: key conclusions record sources and reasoning, rebuildable by later agents
2198
- - S↑ Stable: output structure regenerates repeatably, no reliance on implicit context
2199
- 3. Reports must be concrete and verifiable — no filler.
2200
-
2201
- ## Per-round report (fixed format, answer each item)
2202
- 1. What was done this round (concrete)
2203
- 2. Next-step plan
2204
- 3. Whether the task is complete (if complete, output only ${stopToken})
2205
-
2206
- If the task is complete, output only ${stopToken}.`;
2207
- }
2208
- /**
2209
- * handyman 规模化使用指引(guide 子命令输出;S134 继承 + v1.24.0 更新):
2210
- * 使用 handyman 前必须先加载 eap 设计方案;并行策略(jobs 编排);提示词规范(详尽固定 EAP);
2211
- * 阅读/文字编写类 handyman 内部也加载 eap。
2212
- */
2213
- const HANDYMAN_GUIDE = `# handyman — Scale-Up Usage Guide (guide)
2214
-
2215
- ## ⚠️ Before using: load eap and design the plan
2216
- Before calling handyman, load eap (acc-eap skill) and design the "scale-up handyman plan" based on the EAP framework.
2217
-
2218
- ### 1. Task decomposition (E↑ Explicit)
2219
- - Split large tasks into explicit subtasks: each subtask defines goal / input / boundaries (what to do, what not to do) / acceptance criteria
2220
- - Make dependencies explicit: dependent tasks run serially, independent ones can run in parallel
2221
-
2222
- ### 2. Prompt design (handyman's task parameter)
2223
- - task must be detailed, fixed, and EAP-compliant: clear goal, drawn boundaries, decidable acceptance criteria
2224
- - Anti-example "handle this file" — ambiguous; good example "read <path>, extract all rows of the「关键决策」table,
2225
- output a JSON array (fields id/conclusion/evidence), do not modify the original file"
2226
- - Reading/curating or text-writing work (extracting from files, summarizing, writing docs, generating text, etc.):
2227
- the handyman-internal agent is also required to load eap and organize output per the EAP standard
2228
-
2229
- ### 3. Model whitelist (CCC-configured, mandatory)
2230
- - handyman only uses models listed in .opencode/serenity.json "handyman.models" — never arbitrary models
2231
- - Recursive subagents inside a handyman inherit the handyman's model automatically (DSH native)
2232
- - Keep the subagent tool instance free of a fixed agentOptions, or model inheritance breaks
2233
-
2234
- ### 4. Parallel strategy (jobs orchestration, workflow capability)
2235
- - Independent subtasks can run in parallel via handyman(jobs=[...]): each job gets its own label + task + stop token + progress file
2236
- - Concurrency safety guaranteed: unique sessionId (handyman-<label>-<uuid>), progress files isolated per label
2237
- (AGENT_SESSIONS/handyman-<label>.json) — same label resumes, different labels never interfere
2238
- - Parallel cap: handyman.maxParallel (default 10 — cheap models are cheap)
2239
- - Aggregation: after each parallel job produces progress, the main agent merges (or spawns one aggregation handyman)
2240
- - For programmable pipeline/phase orchestration at scale, use the platform's workflow tool instead
2241
-
2242
- ## Completion criteria (osp loop standard)
2243
- - The only completion condition = the handyman-internal agent echoes this round's random verification code (stop token); dialogue round cap (default 100, osp fail-safe, forced stop beyond the cap, resumable)
2244
- - Automatic restart on abnormal agent stop (≤100 restarts, anti-infinite-loop)
2245
-
2246
- ## Waiting UI
2247
- - The WebUI session-header Serenity detail card shows running handymen's progress (label / round / last response), one line per parallel job, ~3s refresh
2248
- `;
2249
- /** 列出 AGENT_SESSIONS/handyman-*.json 的全部进度(按 updated 倒序;坏文件跳过) */
2250
- function listActiveHandymen(root) {
2251
- const dir = join(root, "AGENT_SESSIONS");
2252
- if (!existsSync(dir)) return [];
2253
- const out = [];
2254
- for (const entry of readdirSync(dir)) {
2255
- if (!entry.startsWith("handyman-") || !entry.endsWith(".json")) continue;
2256
- try {
2257
- const data = JSON.parse(readFileSync(join(dir, entry), "utf-8"));
2258
- if (typeof data.label !== "string" || typeof data.round !== "number") continue;
2259
- out.push({
2260
- label: data.label,
2261
- round: data.round,
2262
- done: data.done === true,
2263
- model: typeof data.model === "string" ? data.model : "",
2264
- updated: typeof data.updated === "string" ? data.updated : "",
2265
- lastResponse: typeof data.lastResponse === "string" ? data.lastResponse : ""
2266
- });
2267
- } catch {}
2268
- }
2269
- out.sort((a, b) => a.updated < b.updated ? 1 : -1);
2270
- return out;
2271
- }
2272
- //#endregion
2273
- //#region src/skiff-core.ts
2274
- const PLUGIN_SOURCE$5 = {
2275
- kind: "plugin",
2276
- plugin: "dsh-serenity-hooks"
2277
- };
2278
- const skiffAgents = /* @__PURE__ */ new Map();
2279
- /** 查 sessionId 的 Skiff 角色名(无 → null) */
2280
- function skiffRoleFor(sessionId) {
2281
- return skiffRoleFor$1(sessionId);
2282
- }
2283
- /** 查 sessionId 的会话绑定(role + ccc;无 → null)——v1.25.10 追问校验 */
2284
- function skiffSessionInfo(sessionId) {
2285
- return skiffSessionInfo$1(sessionId);
2286
- }
2287
- /** 查 sessionId 的活体 agent(进程内会话延续用;未注册/已清理 → undefined) */
2288
- function getSkiffAgent(sessionId) {
2289
- return skiffAgents.get(sessionId);
2290
- }
2291
- function registerSkiffSession(sessionId, role, ccc, agent) {
2292
- skiffAgents.set(sessionId, agent);
2293
- registerSkiffSession$1(sessionId, role, ccc);
2294
- }
2295
- function unregisterSkiffSession(sessionId) {
2296
- skiffAgents.delete(sessionId);
2297
- unregisterSkiffSession$1(sessionId);
2298
- }
2299
- /** 测试/调试:注册表快照(sessionId → {role} 兼容展示) */
2300
- function skiffSessionSnapshot() {
2301
- const out = /* @__PURE__ */ new Map();
2302
- for (const [id, b] of skiffSessionSnapshot$1()) out.set(id, {
2303
- role: b.role,
2304
- ccc: b.ccc
2305
- });
2306
- return out;
2307
- }
2308
- /** Skiff agent 挂载的 DSH preset(v1.25.3 修复:read/grep/glob 等平台工具由 preset 决定工具面;
2309
- * handyman 经 composeFrom 继承父、skiff 无父上下文——直接挂 DSH 默认 standard preset;
2310
- * guard 角色白名单再按角色过滤可见/可用面——白名单外工具仍 deny) */
2311
- const SKIFF_PRESET = "standard";
2312
- /**
2313
- * 创建 Skiff agent:标准 DSH agent + cwd=CCC root + 角色模型 +
2314
- * standard preset(平台工具面)+ scoped 系统提示词(基础提示词 + CCC 定义段,全替换 ACC 默认注入)。
2315
- */
2316
- async function createSkiffAgent(ctx, root, roleName, role, defaultModel) {
2317
- if (!ctx.agents) throw new Error("skiff: ctx.agents unavailable");
2318
- const model = role.model?.trim() || defaultModel || "";
2319
- const sessionId = `${SKIFF_SESSION_PREFIX}${roleName}-${randomUUID()}`;
2320
- const handle = await ctx.agents.create({
2321
- sessionId,
2322
- meta: {
2323
- cwd: root,
2324
- agentPreset: SKIFF_PRESET
2325
- },
2326
- setup: async (agentCtx) => {
2327
- try {
2328
- await agentCtx.get("agentPresets")?.mount?.(agentCtx, SKIFF_PRESET);
2329
- } catch {}
2330
- },
2331
- ...model ? { agentOptions: splitModel(model) } : {}
2332
- });
2333
- const agent = handle.agent;
2334
- let cccPrompt = "";
2335
- try {
2336
- cccPrompt = resolveRoleSystemPrompt(root, role);
2337
- } catch (err) {
2338
- console.warn(`[serenity-hooks] skiff 角色 "${roleName}" 系统提示词解析失败(回退仅基础段): ${String(err?.message ?? err)}`);
2339
- }
2340
- try {
2341
- agent.ctx.systemPrompt.section({
2342
- name: "serenity-skiff",
2343
- order: -60,
2344
- text: () => [buildSkiffBasePrompt(roleName, role), cccPrompt].filter(Boolean).join("\n")
2345
- });
2346
- } catch (err) {
2347
- console.warn(`[serenity-hooks] skiff 系统提示词注册失败: ${String(err?.message ?? err)}`);
2348
- }
2349
- registerSkiffSession(sessionId, roleName, root, agent);
2350
- return {
2351
- handle,
2352
- agent,
2353
- sessionId
2354
- };
2355
- }
2356
- /** 等待 agent 空闲(agent/status → idle);无超时(agent 工作多久等多久,handyman 同款) */
2357
- function waitIdle$1(ctx, agent) {
2358
- return new Promise((resolve) => {
2359
- let settled = false;
2360
- let dispose = () => {};
2361
- const finish = () => {
2362
- if (settled) return;
2363
- settled = true;
2364
- dispose();
2365
- resolve();
2366
- };
2367
- dispose = ctx.on("agent/status", (payload) => {
2368
- if (payload.agent === agent && payload.status === "idle") finish();
2369
- });
2370
- });
2371
- }
2372
- /** 读会话最后一个 assistant/message 文本(handyman 同款) */
2373
- function lastAssistantText$2(agent) {
2374
- const events = agent.session.events;
2375
- for (let i = events.length - 1; i >= 0; i--) {
2376
- const e = events[i];
2377
- if (e && e.type === "assistant/message") {
2378
- const text = (e.data?.message?.content ?? e.data?.content ?? []).filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
2379
- if (text) return text;
2380
- }
2381
- }
2382
- return "";
2383
- }
2384
- /** events → 可读轨迹(user/assistant 文本 + 工具调用 + 工具结果;单条解析失败跳过) */
2385
- function eventsToTrajectory(events) {
2386
- const out = [];
2387
- for (const raw of events) try {
2388
- const ev = raw;
2389
- if (ev.type === "user/message") {
2390
- const text = extractText(ev.data);
2391
- if (text) out.push({
2392
- role: "user",
2393
- text
2394
- });
2395
- } else if (ev.type === "assistant/message") {
2396
- const d = ev.data;
2397
- const text = (d?.message?.content ?? d?.content ?? []).filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
2398
- const calls = d?.message?.tool_calls ?? [];
2399
- if (text) out.push({
2400
- role: "assistant",
2401
- text
2402
- });
2403
- for (const c of calls ?? []) {
2404
- const args = typeof c.arguments === "string" ? c.arguments : JSON.stringify(c.arguments ?? {});
2405
- out.push({
2406
- role: "assistant",
2407
- text: `→ ${c.name ?? "(tool)"} ${truncate(args, 300)}`,
2408
- tool: c.name
2409
- });
2410
- }
2411
- } else if (ev.type === "tool/result") {
2412
- const outText = extractText(ev.data);
2413
- if (outText) out.push({
2414
- role: "tool",
2415
- text: truncate(outText, 500),
2416
- tool: String(ev.data?.name ?? "")
2417
- });
2418
- }
2419
- } catch {}
2420
- return out;
2421
- }
2422
- function extractText(data) {
2423
- const content = data?.content;
2424
- if (!Array.isArray(content)) return "";
2425
- return content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
2426
- }
2427
- function truncate(s, n) {
2428
- return s.length > n ? `${s.slice(0, n)}…` : s;
2429
- }
2430
- /**
2431
- * 提问一轮:followup → 等 idle → 读答案 + 轨迹。
2432
- * @param eventsStart 轨迹起点:显式传 0 = 全量轨迹(会话追问时页面重绘完整时间线,
2433
- * v1.25.10 用户拍板);不传(undefined)= 本轮增量(followup 前 events 之后)
2434
- * @param options.includeTrajectory 是否计算轨迹(v1.26.10:**3100 对外只提供问答**——
2435
- * 公开问答页 / ACP JSON-RPC 不返回 trajectory,传 false 跳过计算;3099 调试页默认 true 保留)
2436
- */
2437
- async function askSkiff(ctx, agent, question, eventsStart, options) {
2438
- const before = eventsStart === void 0 ? agent.session.events.length : eventsStart;
2439
- agent.followup(createUserMessage({
2440
- content: [{
2441
- type: "text",
2442
- text: question
2443
- }],
2444
- source: PLUGIN_SOURCE$5
2445
- }));
2446
- await waitIdle$1(ctx, agent);
2447
- const answer = lastAssistantText$2(agent);
2448
- const trajectory = options?.includeTrajectory === false ? [] : eventsToTrajectory(agent.session.events.slice(before));
2449
- return {
2450
- answer,
2451
- sessionId: String(agent.session.id ?? ""),
2452
- trajectory
2453
- };
2454
- }
2455
- /**
2456
- * Skiff 会话的轨迹纪律参与判定:非 skiff 会话恒 true(正常参与);
2457
- * skiff 会话按角色 trajectory 子集(session/keeper/rebuild)决定;
2458
- * 注册表缺失(进程重启遗留等)→ 保守旁路(false,完全独立)。
2459
- */
2460
- function skiffTrajectoryEnabled(root, sessionId, key) {
2461
- if (!isSkiffSessionId(sessionId)) return true;
2462
- const roleName = sessionId ? skiffRoleFor(sessionId) : null;
2463
- if (!roleName) return false;
2464
- return trajectorySubset(readSkiffRoles(root).get(roleName))[key];
2465
- }
2466
- /**
2467
- * acc_msm 的 Skiff 门控:非 skiff 会话恒放行;skiff 会话——
2468
- * exec 非白名单 MSM 拒绝(不列名单)、register/deregister 必拒、
2469
- * list 白名单过滤、check/guide/ccc-config 只读放行。
2470
- */
2471
- function skiffMsmGate(root, sessionId, action, name) {
2472
- if (!isSkiffSessionId(sessionId)) return {};
2473
- const roleName = sessionId ? skiffRoleFor(sessionId) : null;
2474
- const role = roleName ? readSkiffRoles(root).get(roleName) : void 0;
2475
- if (!roleName || !role) return { reject: "MSM not allowed in this skiff session" };
2476
- if (action === "register" || action === "deregister") return { reject: "register/deregister is not allowed in skiff sessions" };
2477
- if (action === "exec") {
2478
- if (!name || !(role.msms ?? []).includes(name)) return { reject: "MSM not allowed" };
2479
- return {};
2480
- }
2481
- if (action === "list") return { whitelist: roleMsmWhitelist(role) };
2482
- return {};
2483
- }
2484
- //#endregion
2485
1681
  //#region src/tools/msm.ts
2486
1682
  /**
2487
1683
  * msm.ts — acc_msm 真实 DSH 工具定义(defineTool)
@@ -2549,7 +1745,8 @@ const msmTool = defineTool({
2549
1745
  async execute(args, exec) {
2550
1746
  const root = findSerenityRoot(agentCwd$7(exec));
2551
1747
  if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
2552
- const gate = skiffMsmGate(root, agentSessionId$1(exec), args.action, args.name);
1748
+ const sessionId = agentSessionId$1(exec);
1749
+ const gate = skiffMsmGate(root, sessionId, args.action, args.name);
2553
1750
  if (gate.reject) throw new Error(gate.reject);
2554
1751
  if (args.action === "list" && gate.whitelist) {
2555
1752
  const out = runMsm(root, args);
@@ -7100,14 +6297,14 @@ async function fetchBiasContent(root, providerRel) {
7100
6297
  error: "偏见内容提供者脚本无法运行(bun 与 node 均不可用)"
7101
6298
  };
7102
6299
  }
7103
- /** 唤起消息(三段式:身份锚定 / 先验偏见[自生动机+偏见内容] / 任务)——注入前台会话,用户可见 */
6300
+ /** 唤起消息(四段式:轨迹焦点[CCC 定义,稳定] / 身份锚定 / 先验偏见[自生动机+偏见内容] / 任务)——注入前台会话,用户可见 */
7104
6301
  function buildWakeMessage(opts) {
7105
- const lines = [
7106
- `[自主轨迹唤起] — 距上次轨迹活动已满 ${opts.intervalHours} 小时,自动继续。`,
7107
- "",
7108
- `身份锚定:继续 ${opts.sessionName} 的 trajectory(SESSION.md: ${opts.mdPath})。`,
7109
- "先验偏见:"
7110
- ];
6302
+ const lines = [];
6303
+ if (opts.topPrompt) {
6304
+ lines.push(`[轨迹焦点] ${opts.topPrompt}`);
6305
+ lines.push("");
6306
+ }
6307
+ lines.push(`[自主轨迹唤起] — 距上次轨迹活动已满 ${opts.intervalHours} 小时,自动继续。`, "", `身份锚定:继续 ${opts.sessionName} 的 trajectory(SESSION.md: ${opts.mdPath})。`, "先验偏见:");
7111
6308
  if (opts.motivation) lines.push(` · 自生动机:${opts.motivation}`);
7112
6309
  if (opts.biasContent) lines.push(` · 偏见内容:${opts.biasContent}`);
7113
6310
  if (!opts.motivation && !opts.biasContent) lines.push(" · (无——本轮纯自主探索)");
@@ -7164,6 +6361,7 @@ async function performAutoTrajectoryWake(ctx, root, settings, opts = {}) {
7164
6361
  sessionName: basename(dirname(mdPath)),
7165
6362
  mdPath,
7166
6363
  intervalHours: Math.max(1, settings.intervalHours ?? 12),
6364
+ topPrompt: settings.topPrompt?.trim() || null,
7167
6365
  motivation,
7168
6366
  biasContent: biasRes.text
7169
6367
  });
@@ -7321,6 +6519,7 @@ function getAutoTrajectoryStatus(root) {
7321
6519
  enabled: cfg?.enabled ?? false,
7322
6520
  intervalHours: Math.max(1, cfg?.intervalHours ?? 12),
7323
6521
  biasProvider: cfg?.biasProvider?.trim() || "autotrajectory-bias.ts",
6522
+ topPrompt: cfg?.topPrompt?.trim() || null,
7324
6523
  session: cfg?.session ?? null,
7325
6524
  avoidWakeHours: {
7326
6525
  start: cfg?.avoidWakeHours?.start ?? DEFAULT_AVOID_HOURS.start,
@@ -7497,7 +6696,7 @@ function renderDiagLive(r) {
7497
6696
  function createAutoTrajectoryExpTool(ctx) {
7498
6697
  return defineTool({
7499
6698
  name: "autotrajectory-exp",
7500
- description: "自主轨迹实验(Self-Sustaining Trajectory)一站式管理——实验提案 v1.26.14,默认关。无参/action=all:全报告(背景摘要 + 就绪检查 + 状态 + 下一步)——CCC agent 看一次即完整理解实验并知道怎么开始;init:初始化辅助(写配置 + 生成偏见提供者脚本模板);random:运行偏见提供者脚本输出当前偏见内容;diag:唤起条件链诊断(--ccc <path> 指定 CCC,无参递归扫描 /home/yh 两层——覆盖任意实验 CCC 位置;逐条件输出 + 阻断点 + 修复建议);diag-live:**进程内诊断**(v1.26.14——live 会话清单/标题/agent 定位/面板解析目标;排查\"面板检测不到实验 CCC\");doc:实验定义全文;check/status/guide:单项。实验是 CCC 的自选动作——dsp 只提供工具与知识,不自动安装任何东西。",
6699
+ description: "自主轨迹实验(Self-Sustaining Trajectory)一站式管理——实验提案 v1.26.14,默认关。无参/action=all:全报告(背景摘要 + 就绪检查 + 状态 + 下一步)——CCC agent 看一次即完整理解实验并知道怎么开始;init:初始化辅助(写配置 + 生成偏见提供者脚本模板);random:运行偏见提供者脚本输出当前偏见内容;diag:唤起条件链诊断(--ccc <path> 指定 CCC,无参递归扫描 /home/yh 两层——覆盖任意实验 CCC 位置;逐条件输出 + 阻断点 + 修复建议);diag-live:**进程内诊断**(v1.26.14——live 会话清单/标题/agent 定位/面板解析目标;排查\"面板检测不到实验 CCC\");doc:实验定义全文;check/status/guide:单项。**topPrompt(v1.26.17,轨迹焦点)**:CCC 定义 autotrajectory 时自己填写本轨迹核心焦点(顶层提示词),每次唤起最先注入——稳定焦点锚定防漂移(实验观察:无焦点多轮唤起轨迹腐化),与偏见内容(每轮随机探索)互补。实验是 CCC 的自选动作——dsp 只提供工具与知识,不自动安装任何东西。",
7501
6700
  parameters: { action: {
7502
6701
  type: "string",
7503
6702
  enum: [...AUTO_TRAJECTORY_EXP_ACTIONS],
@@ -8853,6 +8052,7 @@ const CONFIG_PATH = "/serenity/config";
8853
8052
  const CCCS_PATH = "/serenity/cccs";
8854
8053
  const PUBLIC_ASK_PATH = "/serenity/public-ask";
8855
8054
  const AUTOTRAJECTORY_PATH = "/serenity/autotrajectory";
8055
+ const WEIXIN_PATH = "/serenity/weixin";
8856
8056
  /** 图片落盘目录(CCC 根相对;S142 图片自动识别基础设施——粘贴图片落盘供 agent 经 CCC vlm MSM 自主处理) */
8857
8057
  const IMAGE_UPLOAD_DIR = "_tmp/images_from_user";
8858
8058
  const IMAGE_MEDIA_TYPES = /* @__PURE__ */ new Set([
@@ -9269,6 +8469,221 @@ function registerStatusApi(ctx, opts = {}) {
9269
8469
  }
9270
8470
  }
9271
8471
  });
8472
+ ctx.webServer.register({
8473
+ kind: "exact",
8474
+ path: WEIXIN_PATH,
8475
+ handler: async (req, res) => {
8476
+ try {
8477
+ if (req.headers["x-serenity-ui"] !== "1") {
8478
+ sendJson$2(res, 403, { error: "微信桥配置仅限 WebUI(client 专用)" });
8479
+ return;
8480
+ }
8481
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
8482
+ if (req.method === "GET") {
8483
+ const chk = requireCcc(url.searchParams.get("ccc") ?? void 0);
8484
+ if (!chk.ok) {
8485
+ sendJson$2(res, 400, { error: chk.error });
8486
+ return;
8487
+ }
8488
+ const { readWeixinSettings } = await import("./weixin-route-CHCWBvLZ.js").then((n) => n.o);
8489
+ const { weixinBridgeStatus } = await Promise.resolve().then(() => weixin_bridge_exports);
8490
+ const settings = readWeixinSettings(chk.root);
8491
+ const bridge = weixinBridgeStatus().find((b) => b.ccc === chk.root);
8492
+ const { readWeixinCredential } = await import("./weixin-route-CHCWBvLZ.js").then((n) => n.o);
8493
+ const accounts = (settings.accounts ?? []).map((a) => ({
8494
+ accountId: a.accountId,
8495
+ name: a.name ?? void 0,
8496
+ enabled: a.enabled !== false,
8497
+ bound: readWeixinCredential(chk.root, a.accountId) !== null
8498
+ }));
8499
+ sendJson$2(res, 200, {
8500
+ enabled: settings.enabled === true,
8501
+ botType: settings.botType ?? void 0,
8502
+ accounts,
8503
+ routes: settings.routes ?? [],
8504
+ bridge: bridge?.accounts ?? []
8505
+ });
8506
+ return;
8507
+ }
8508
+ if (req.method !== "POST") {
8509
+ sendJson$2(res, 405, { error: "method not allowed" });
8510
+ return;
8511
+ }
8512
+ const raw = await readBody$3(req, 131072);
8513
+ const body = JSON.parse(raw);
8514
+ const chk = requireCcc(body.ccc);
8515
+ if (!chk.ok) {
8516
+ sendJson$2(res, 400, { error: chk.error });
8517
+ return;
8518
+ }
8519
+ const root = chk.root;
8520
+ if (body.action === "login-start") {
8521
+ const { fetchQRCode } = await import("./weixin-api-b8HLmcPE.js").then((n) => n.r);
8522
+ const { randomUUID } = await import("node:crypto");
8523
+ for (const [key, v] of weixinLogins) if (Date.now() - v.startedAt > WEIXIN_LOGIN_TTL_MS) weixinLogins.delete(key);
8524
+ const { readWeixinSettings } = await import("./weixin-route-CHCWBvLZ.js").then((n) => n.o);
8525
+ const qr = await fetchQRCode({ botType: readWeixinSettings(root).botType ?? void 0 });
8526
+ const loginKey = randomUUID();
8527
+ weixinLogins.set(loginKey, {
8528
+ root,
8529
+ qrcode: qr.qrcode,
8530
+ startedAt: Date.now(),
8531
+ polling: false
8532
+ });
8533
+ sendJson$2(res, 200, {
8534
+ qrcode: qr.qrcode,
8535
+ qrcode_img_content: qr.qrcode_img_content,
8536
+ loginKey
8537
+ });
8538
+ return;
8539
+ }
8540
+ if (body.action === "remove-account") {
8541
+ if (!body.accountId) {
8542
+ sendJson$2(res, 400, { error: "missing accountId" });
8543
+ return;
8544
+ }
8545
+ const { removeWeixinAccount } = await import("./weixin-route-CHCWBvLZ.js").then((n) => n.o);
8546
+ const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
8547
+ removeWeixinAccount(root, body.accountId);
8548
+ syncCccBridge(ctx, root);
8549
+ sendJson$2(res, 200, { removed: body.accountId });
8550
+ return;
8551
+ }
8552
+ if (body.action === "save-routes") {
8553
+ const routes = body.routes ?? [];
8554
+ if (!Array.isArray(routes) || routes.some((r) => typeof r?.user !== "string" || typeof r?.role !== "string" || r.user === "" || r.role === "")) {
8555
+ sendJson$2(res, 400, { error: "invalid routes (expected [{user, role}, ...])" });
8556
+ return;
8557
+ }
8558
+ const { saveWeixinRoutes } = await import("./weixin-route-CHCWBvLZ.js").then((n) => n.o);
8559
+ const { readSkiffRoles } = await import("./skiff-role-Dw7UKCUm.js").then((n) => n.c);
8560
+ const roles = readSkiffRoles(root);
8561
+ for (const r of routes) if (!roles.has(r.role)) {
8562
+ sendJson$2(res, 400, { error: `unknown role: ${r.role} (not in ${root} skiff.roles)` });
8563
+ return;
8564
+ }
8565
+ saveWeixinRoutes(root, routes);
8566
+ sendJson$2(res, 200, { saved: routes.length });
8567
+ return;
8568
+ }
8569
+ if (body.action === "set-enabled") {
8570
+ const enabled = body.enabled === true;
8571
+ const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-CHCWBvLZ.js").then((n) => n.o);
8572
+ const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
8573
+ const settings = readWeixinSettings(root);
8574
+ if (enabled && (settings.accounts ?? []).length === 0) {
8575
+ sendJson$2(res, 400, { error: "启用前请先扫码绑定至少一个账号" });
8576
+ return;
8577
+ }
8578
+ setWeixinEnabled(root, enabled);
8579
+ if (enabled) syncCccBridge(ctx, root);
8580
+ else {
8581
+ const { stopCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
8582
+ stopCccBridge(root);
8583
+ }
8584
+ sendJson$2(res, 200, { enabled });
8585
+ return;
8586
+ }
8587
+ sendJson$2(res, 400, { error: `unsupported action: ${body.action ?? ""}` });
8588
+ } catch (err) {
8589
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
8590
+ }
8591
+ }
8592
+ });
8593
+ ctx.webServer.register({
8594
+ kind: "exact",
8595
+ path: `${WEIXIN_PATH}/login`,
8596
+ handler: async (req, res) => {
8597
+ try {
8598
+ if (req.headers["x-serenity-ui"] !== "1") {
8599
+ sendJson$2(res, 403, { error: "微信桥配置仅限 WebUI(client 专用)" });
8600
+ return;
8601
+ }
8602
+ if (req.method !== "GET") {
8603
+ sendJson$2(res, 405, { error: "method not allowed" });
8604
+ return;
8605
+ }
8606
+ const key = new URL(req.url ?? "/", "http://127.0.0.1").searchParams.get("key") ?? "";
8607
+ const login = weixinLogins.get(key);
8608
+ if (!login) {
8609
+ sendJson$2(res, 404, { error: "login not found or expired" });
8610
+ return;
8611
+ }
8612
+ if (Date.now() - login.startedAt > WEIXIN_LOGIN_TTL_MS) {
8613
+ weixinLogins.delete(key);
8614
+ sendJson$2(res, 200, { status: "expired" });
8615
+ return;
8616
+ }
8617
+ const { pollQRStatus } = await import("./weixin-api-b8HLmcPE.js").then((n) => n.r);
8618
+ const { readWeixinSettings } = await import("./weixin-route-CHCWBvLZ.js").then((n) => n.o);
8619
+ const settings = readWeixinSettings(login.root);
8620
+ const status = await pollQRStatus({
8621
+ baseUrl: void 0,
8622
+ qrcode: login.qrcode
8623
+ });
8624
+ switch (status.status) {
8625
+ case "confirmed": {
8626
+ if (!status.bot_token) {
8627
+ sendJson$2(res, 200, {
8628
+ status: "error",
8629
+ error: "confirmed but no bot_token"
8630
+ });
8631
+ return;
8632
+ }
8633
+ const { upsertWeixinAccount, writeWeixinCredential } = await import("./weixin-route-CHCWBvLZ.js").then((n) => n.o);
8634
+ const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
8635
+ const accountId = `wechat-${(settings.accounts?.length ?? 0) + 1}`;
8636
+ upsertWeixinAccount(login.root, {
8637
+ accountId,
8638
+ name: `微信 ${accountId}`,
8639
+ enabled: true
8640
+ });
8641
+ writeWeixinCredential(login.root, accountId, {
8642
+ token: status.bot_token,
8643
+ baseUrl: status.baseurl ?? "",
8644
+ userId: status.ilink_user_id
8645
+ });
8646
+ syncCccBridge(ctx, login.root);
8647
+ weixinLogins.delete(key);
8648
+ sendJson$2(res, 200, {
8649
+ status: "confirmed",
8650
+ accountId,
8651
+ tokenSaved: true
8652
+ });
8653
+ return;
8654
+ }
8655
+ case "expired":
8656
+ weixinLogins.delete(key);
8657
+ sendJson$2(res, 200, { status: "expired" });
8658
+ return;
8659
+ default:
8660
+ sendJson$2(res, 200, { status: status.status ?? "wait" });
8661
+ return;
8662
+ }
8663
+ } catch (err) {
8664
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
8665
+ }
8666
+ }
8667
+ });
8668
+ }
8669
+ /** 进行中的扫码登录(loginKey → 状态;进程内,5min 有效) */
8670
+ const weixinLogins = /* @__PURE__ */ new Map();
8671
+ const WEIXIN_LOGIN_TTL_MS = 3e5;
8672
+ /** 校验微信桥面板操作的头/参数(x-serenity-ui + ccc 必填) */
8673
+ function requireCcc(root) {
8674
+ if (!root || root.trim() === "") return {
8675
+ ok: false,
8676
+ error: "missing ccc param"
8677
+ };
8678
+ const serenityRoot = findSerenityRoot(root);
8679
+ if (!serenityRoot) return {
8680
+ ok: false,
8681
+ error: `no CCC found from: ${root}`
8682
+ };
8683
+ return {
8684
+ ok: true,
8685
+ root: serenityRoot
8686
+ };
9272
8687
  }
9273
8688
  //#endregion
9274
8689
  //#region src/seams/env.ts
@@ -12932,7 +12347,10 @@ async function handleAskParsed(ctx, ccc, body, res, ip) {
12932
12347
  continued = true;
12933
12348
  }
12934
12349
  }
12935
- if (!agent) agent = (await createSkiffAgent(ctx, root, effectiveRole, effectiveCfg, readHandymanConfig(root)?.defaultModel)).agent;
12350
+ if (!agent) {
12351
+ const hc = readHandymanConfig(root);
12352
+ agent = (await createSkiffAgent(ctx, root, effectiveRole, effectiveCfg, hc?.defaultModel)).agent;
12353
+ }
12936
12354
  const result = await askSkiff(ctx, agent, question, void 0, { includeTrajectory: false });
12937
12355
  sendJson(res, 200, {
12938
12356
  answer: result.answer,
@@ -13269,6 +12687,161 @@ qInput.focus()
13269
12687
  </html>`;
13270
12688
  }
13271
12689
  //#endregion
12690
+ //#region src/weixin-bridge.ts
12691
+ var weixin_bridge_exports = /* @__PURE__ */ __exportAll({
12692
+ handleIncoming: () => handleIncoming,
12693
+ registerWeixinBridge: () => registerWeixinBridge,
12694
+ stopCccBridge: () => stopCccBridge,
12695
+ syncCccBridge: () => syncCccBridge,
12696
+ weixinBridgeStatus: () => weixinBridgeStatus
12697
+ });
12698
+ const bridges = /* @__PURE__ */ new Map();
12699
+ /** 轮询间隔(getupdates 失败后重试延迟;成功 = 立即续轮询) */
12700
+ const POLL_RETRY_MS = 3e3;
12701
+ /** 单账号轮询循环:getupdates 长轮询 → 逐消息分发 → 回复回写 */
12702
+ async function runAccountLoop(ctx, root, accountId, cred, loop) {
12703
+ let buf = "";
12704
+ while (!loop.stopped) {
12705
+ loop.lastPollAt = Date.now();
12706
+ try {
12707
+ const resp = await getUpdates({
12708
+ baseUrl: cred.baseUrl,
12709
+ token: cred.token,
12710
+ getUpdatesBuf: buf,
12711
+ timeoutMs: 35e3
12712
+ });
12713
+ buf = resp.get_updates_buf ?? buf;
12714
+ for (const msg of resp.msgs ?? []) {
12715
+ if (loop.stopped) break;
12716
+ if (msg.message_type !== 1) continue;
12717
+ await handleIncoming(ctx, root, accountId, cred, msg);
12718
+ }
12719
+ } catch (err) {
12720
+ loop.lastError = err instanceof Error ? err.message : String(err);
12721
+ await new Promise((r) => setTimeout(r, POLL_RETRY_MS));
12722
+ }
12723
+ }
12724
+ }
12725
+ /**
12726
+ * 处理单条微信消息:路由 → skiff 会话(固定 id 创建/延续)→ 提问 → 回复回写。
12727
+ *
12728
+ * 会话语义:`weixinSessionIdFor(fromUserId)` 固定可重建——同用户长期同一会话;
12729
+ * 进程内已有(getSkiffAgent 命中)→ 延续;无(首次/进程重启)→ createSkiffAgent
12730
+ * 以固定 id 新建(角色不变,记忆从新开始——重启后自动重建,与 3100 问答页同语义)。
12731
+ */
12732
+ async function handleIncoming(ctx, root, _accountId, cred, msg) {
12733
+ const fromUserId = msg.from_user_id;
12734
+ if (!fromUserId) return;
12735
+ const text = extractWeixinText(msg);
12736
+ if (!text) return;
12737
+ const settings = readWeixinSettings(root);
12738
+ if (!settings.enabled) return;
12739
+ const roleName = matchWeixinRoute(settings.routes ?? [], fromUserId);
12740
+ if (!roleName) return;
12741
+ try {
12742
+ const role = readSkiffRoles(root).get(roleName);
12743
+ if (!role) {
12744
+ console.log(`[serenity-hooks] weixin-bridge: 路由命中角色 "${roleName}" 但该 CCC 未定义(检查 skiff.roles)`);
12745
+ return;
12746
+ }
12747
+ const sessionId = weixinSessionIdFor(fromUserId);
12748
+ const existing = getSkiffAgent(sessionId);
12749
+ const recreated = !existing;
12750
+ const hc = readHandymanConfig(root);
12751
+ const ref = existing ? {
12752
+ agent: existing,
12753
+ sessionId
12754
+ } : await createSkiffAgent(ctx, root, roleName, role, hc?.defaultModel, sessionId);
12755
+ if (recreated) await sendTextMessage({
12756
+ baseUrl: cred.baseUrl,
12757
+ token: cred.token,
12758
+ toUserId: fromUserId,
12759
+ text: "(新的对话已开始)",
12760
+ contextToken: msg.context_token
12761
+ }).catch(() => {});
12762
+ const answer = (await askSkiff(ctx, ref.agent, text, void 0, { includeTrajectory: false })).answer ?? "";
12763
+ if (answer === "") return;
12764
+ await sendTextMessage({
12765
+ baseUrl: cred.baseUrl,
12766
+ token: cred.token,
12767
+ toUserId: fromUserId,
12768
+ text: answer,
12769
+ contextToken: msg.context_token
12770
+ });
12771
+ } catch (err) {
12772
+ console.log(`[serenity-hooks] weixin-bridge error (ccc=${root}): ${err instanceof Error ? err.message : String(err)}`);
12773
+ }
12774
+ }
12775
+ /**
12776
+ * 启动/重建某 CCC 的桥:读配置 → 对每个 enabled + 有凭据的账号启动轮询循环。
12777
+ * 已存在(配置变化热重建)→ 先停旧循环再启动新的。
12778
+ */
12779
+ function syncCccBridge(ctx, root) {
12780
+ const existing = bridges.get(root);
12781
+ if (existing) {
12782
+ for (const loop of existing.loops.values()) loop.stopped = true;
12783
+ bridges.delete(root);
12784
+ }
12785
+ const settings = readWeixinSettings(root);
12786
+ if (!settings.enabled) return;
12787
+ const bridge = {
12788
+ root,
12789
+ loops: /* @__PURE__ */ new Map()
12790
+ };
12791
+ for (const account of settings.accounts ?? []) {
12792
+ if (account.enabled === false) continue;
12793
+ const cred = readWeixinCredential(root, account.accountId);
12794
+ if (!cred) continue;
12795
+ const loop = {
12796
+ stopped: false,
12797
+ lastPollAt: 0
12798
+ };
12799
+ bridge.loops.set(account.accountId, loop);
12800
+ runAccountLoop(ctx, root, account.accountId, cred, loop);
12801
+ }
12802
+ if (bridge.loops.size > 0) {
12803
+ bridges.set(root, bridge);
12804
+ console.log(`[serenity-hooks] ✓ weixin-bridge: ccc=${root} accounts=${[...bridge.loops.keys()].join(",")}`);
12805
+ }
12806
+ }
12807
+ /** 停止某 CCC 的桥(移除账号/禁用时) */
12808
+ function stopCccBridge(root) {
12809
+ const bridge = bridges.get(root);
12810
+ if (!bridge) return;
12811
+ for (const loop of bridge.loops.values()) loop.stopped = true;
12812
+ bridges.delete(root);
12813
+ }
12814
+ /** 桥状态快照(面板数据源):每 CCC → 每账号 → 轮询健康 */
12815
+ function weixinBridgeStatus() {
12816
+ const out = [];
12817
+ for (const [root, bridge] of bridges) out.push({
12818
+ ccc: root,
12819
+ accounts: [...bridge.loops.entries()].map(([accountId, loop]) => ({
12820
+ accountId,
12821
+ lastPollAt: loop.lastPollAt,
12822
+ ...loop.lastError ? { lastError: loop.lastError } : {}
12823
+ }))
12824
+ });
12825
+ return out;
12826
+ }
12827
+ /**
12828
+ * 装配(index.ts 调用):扫描 live CCC 启动桥 + 监听会话变化。
12829
+ * 事件驱动(对齐 autotrajectory v1.26.15):live 会话出现 → 同步该 CCC 桥。
12830
+ */
12831
+ function registerWeixinBridge(ctx) {
12832
+ const syncFromLive = () => {
12833
+ try {
12834
+ const cwds = (ctx.sessions?.list?.() ?? []).map((s) => s.header?.cwd ?? "").filter(Boolean);
12835
+ const roots = [...new Set(cwds.map((c) => findSerenityRoot(c))).values()].filter((r) => r !== null);
12836
+ for (const root of roots) syncCccBridge(ctx, root);
12837
+ } catch {}
12838
+ };
12839
+ syncFromLive();
12840
+ try {
12841
+ ctx.on("session/created", () => syncFromLive());
12842
+ } catch {}
12843
+ }
12844
+ //#endregion
13272
12845
  //#region src/index.ts
13273
12846
  const name = "dsh-serenity-hooks";
13274
12847
  /** 主动调用的服务;其余(agent 事件)随 harness 装配必然存在 */
@@ -13351,6 +12924,7 @@ function apply(ctx, config) {
13351
12924
  registerSkiff(ctx);
13352
12925
  registerAcp(ctx);
13353
12926
  registerAutoTrajectory(ctx);
12927
+ registerWeixinBridge(ctx);
13354
12928
  }
13355
12929
  /**
13356
12930
  * F4 Skiff 调试服务装配:启停 = 人工(设置面板 Skiff 区块开关,settings 持久化)。