@shgroup/dsh-serenity-hooks 1.27.12 → 1.28.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/README.md +7 -5
- package/dsh.plugin.json +3 -2
- package/lib/{ccc-CfDrlfA7.js → ccc-CX48YNUL.js} +19 -1
- package/lib/ccc.d.ts +16 -0
- package/lib/client.js +18 -22
- package/lib/config-ops.d.ts +8 -10
- package/lib/index.d.ts +1 -1
- package/lib/index.js +695 -120
- package/lib/invariant.d.ts +1 -1
- package/lib/invariant.js +2 -1
- package/lib/kit-ops.d.ts +27 -0
- package/lib/msm-ops.d.ts +17 -3
- package/lib/rebuild.d.ts +5 -0
- package/lib/seams/guards.d.ts +14 -0
- package/lib/seams/keeper.d.ts +4 -1
- package/lib/seams/system-prompt.d.ts +15 -4
- package/lib/{settings-section-PzeHStWP.js → settings-section-DnX9TJxt.js} +3 -3
- package/lib/settings-section.d.ts +5 -5
- package/lib/{skiff-debug-BRlc17GU.js → skiff-debug-DIf_Uenr.js} +2 -2
- package/lib/{skiff-role-Dw7UKCUm.js → skiff-role-BTdBHyOQ.js} +1 -1
- package/lib/tools/session.d.ts +23 -6
- package/lib/{weixin-api-DAugVJ-f.js → weixin-api-OUCuw0Ws.js} +1 -1
- package/lib/weixin-hook.d.ts +103 -0
- package/lib/{weixin-route-EVu56Ytp.js → weixin-route-DFkRf9ou.js} +2 -1
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
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,
|
|
3
|
-
import { a as resolveRoleSystemPrompt, i as readSkiffRoles, l as systemPromptSource, r as isSkiffSessionId, s as roleToolWhitelist } from "./skiff-role-
|
|
4
|
-
import { C as splitModel, E as skiffRoleFor, S as requireWhitelistedModel, T as writeProgress, _ as buildRoundPrompt, a as startSkiffDebugServer, b as newStopToken, c as askSkiff, d as skiffMsmGate, f as skiffSessionInfo, g as HANDYMAN_GUIDE, h as unregisterSkiffSession, l as createSkiffAgent, m as skiffTrajectoryEnabled, n as jscSafeJsonText, o as stopSkiffDebugServer, p as skiffSessionSnapshot, r as renderSkiffMarkdown, s as stripThink, t as discoverCccs, u as getSkiffAgent, v as handymanProgressPaths, w as writeFailedStatus, x as readProgress, y as listActiveHandymen } from "./skiff-debug-
|
|
5
|
-
import { a as readWeixinCredential, c as weixinInboundDir, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, h as runLocalStore, i as matchWeixinRoute, l as weixinSessionIdFor, m as readGitTrack, n as extractWeixinText, o as readWeixinSettings, p as localstorePath, r as hasVoiceItem, s as sanitizeFileName$1, t as extractWeixinMedia } from "./weixin-route-
|
|
6
|
-
import { n as registerSettingsSection, t as readSimpleSettings } from "./settings-section-
|
|
7
|
-
import { a as
|
|
2
|
+
import { a as findSerenityRoot, c as matchBlacklist, d as readCccName$2, f as readHandymanConfig, i as findGitRoot, l as pathInside, m as resolveInside, n as SAFE_MODE_MARKER, o as isSafeModeOn, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-CX48YNUL.js";
|
|
3
|
+
import { a as resolveRoleSystemPrompt, i as readSkiffRoles, l as systemPromptSource, r as isSkiffSessionId, s as roleToolWhitelist } from "./skiff-role-BTdBHyOQ.js";
|
|
4
|
+
import { C as splitModel, E as skiffRoleFor, S as requireWhitelistedModel, T as writeProgress, _ as buildRoundPrompt, a as startSkiffDebugServer, b as newStopToken, c as askSkiff, d as skiffMsmGate, f as skiffSessionInfo, g as HANDYMAN_GUIDE, h as unregisterSkiffSession, l as createSkiffAgent, m as skiffTrajectoryEnabled, n as jscSafeJsonText, o as stopSkiffDebugServer, p as skiffSessionSnapshot, r as renderSkiffMarkdown, s as stripThink, t as discoverCccs, u as getSkiffAgent, v as handymanProgressPaths, w as writeFailedStatus, x as readProgress, y as listActiveHandymen } from "./skiff-debug-DIf_Uenr.js";
|
|
5
|
+
import { a as readWeixinCredential, c as weixinInboundDir, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, h as runLocalStore, i as matchWeixinRoute, l as weixinSessionIdFor, m as readGitTrack, n as extractWeixinText, o as readWeixinSettings, p as localstorePath, r as hasVoiceItem, s as sanitizeFileName$1, t as extractWeixinMedia } from "./weixin-route-DFkRf9ou.js";
|
|
6
|
+
import { n as registerSettingsSection, t as readSimpleSettings } from "./settings-section-DnX9TJxt.js";
|
|
7
|
+
import { a as markdownToPlainText, c as sniffImageExt, i as getUpdates, n as downloadMedia, o as sendTextMessage, r as getConfig, s as sendTyping, t as TypingStatus } from "./weixin-api-OUCuw0Ws.js";
|
|
8
8
|
import z from "@deepseek-ai/schemastery";
|
|
9
9
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
10
10
|
import { appendFileSync, chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
|
|
@@ -81,12 +81,43 @@ function getFileInfo(absPath, name) {
|
|
|
81
81
|
function safeRel(root, abs) {
|
|
82
82
|
return relative(root, abs) || ".";
|
|
83
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* 受保护注册表聚合档的路径集合(review P2-2:精确文件保护可被 `rm -r 父目录` / `mv 父目录`
|
|
86
|
+
* 绕过——删掉 references/ 目录等于删掉注册表。保护对象 = 聚合档文件 + 其全部祖先目录)。
|
|
87
|
+
* @returns null = 无 cccName(无法定位 → 不保护);否则 { fileRel, ancestorDirs } 相对根的正斜杠 rel
|
|
88
|
+
*/
|
|
89
|
+
function protectedRegistryTargets(root) {
|
|
90
|
+
const cccName = readCccName$2(root);
|
|
91
|
+
if (!cccName) return null;
|
|
92
|
+
const fileRel = `.opencode/skills/${cccName}/references/mech-registry.json`;
|
|
93
|
+
const segs = fileRel.split("/");
|
|
94
|
+
const ancestorDirs = [];
|
|
95
|
+
if (segs.length >= 3) ancestorDirs.push(segs.slice(0, segs.length - 1).join("/"));
|
|
96
|
+
return {
|
|
97
|
+
fileRel,
|
|
98
|
+
ancestorDirs
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function isProtectedRegistryTarget(root, relCi) {
|
|
102
|
+
const protectedTargets = protectedRegistryTargets(root);
|
|
103
|
+
if (!protectedTargets) return null;
|
|
104
|
+
const lower = (s) => process.platform === "win32" ? s.toLowerCase() : s;
|
|
105
|
+
if (relCi === lower(protectedTargets.fileRel)) return { hit: "file" };
|
|
106
|
+
for (const ancestor of protectedTargets.ancestorDirs) if (relCi === lower(ancestor)) return { hit: "ancestor" };
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
function assertNotProtectedRegistry(root, absPath, targetLabel) {
|
|
110
|
+
const rel = relative(root, absPath).split("\\").join("/");
|
|
111
|
+
const lower = (s) => process.platform === "win32" ? s.toLowerCase() : s;
|
|
112
|
+
const hit = isProtectedRegistryTarget(root, lower(rel));
|
|
113
|
+
if (hit === null) return;
|
|
114
|
+
if (hit.hit === "file") throw new Error(`cc-fs: refusing to directly modify mech-registry.json — use acc_msm register/deregister instead`);
|
|
115
|
+
throw new Error(`cc-fs: refusing to ${/rm|delete|remove/i.test(targetLabel) ? "remove" : "move"} "${rel}" — it is an ancestor of the ACC-managed mech-registry.json (${protectedRegistryTargets(root)?.fileRel}); the registry is managed by acc_msm register/deregister`);
|
|
116
|
+
}
|
|
84
117
|
function validateWritePath(root, target) {
|
|
85
118
|
const absPath = target.startsWith("/") ? resolve(target) : resolveInside(root, target);
|
|
86
119
|
if (!pathInside(resolve(root), absPath)) throw new Error(`cc-fs: path "${target}" resolves to "${absPath}" which is outside serenity root "${root}"`);
|
|
87
|
-
|
|
88
|
-
const relCi = process.platform === "win32" ? rel.toLowerCase() : rel;
|
|
89
|
-
if (relCi.endsWith("/mech-registry.json") && /(^|\/)(opencode|\.opencode)\/skills\//.test(relCi)) throw new Error(`cc-fs: refusing to directly modify mech-registry.json — use acc_msm register/deregister instead`);
|
|
120
|
+
assertNotProtectedRegistry(root, absPath, `modify ${target}`);
|
|
90
121
|
if (existsSync(absPath)) try {
|
|
91
122
|
const real = realpathSync(absPath);
|
|
92
123
|
if (!pathInside(resolve(root), real)) throw new Error(`cc-fs: path "${target}" resolves via symlink to "${real}" outside serenity root "${root}"`);
|
|
@@ -187,7 +218,13 @@ function runCcFs(root, args) {
|
|
|
187
218
|
const recursive = args.recursive ?? false;
|
|
188
219
|
const results = [];
|
|
189
220
|
for (const target of targets) {
|
|
190
|
-
|
|
221
|
+
let absPath;
|
|
222
|
+
try {
|
|
223
|
+
absPath = validateWritePath(root, target);
|
|
224
|
+
} catch (e) {
|
|
225
|
+
results.push(`[SKIP] ${e.message}`);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
191
228
|
if (!existsSync(absPath)) {
|
|
192
229
|
results.push(`[SKIP] not found: ${target}`);
|
|
193
230
|
continue;
|
|
@@ -518,6 +555,34 @@ const CC_FS_WRITE_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
518
555
|
* 含 API keys/tokens/SSH 密码,即使 prompt 纪律失败也无值可吐(机械保证)。
|
|
519
556
|
*/
|
|
520
557
|
const SENSITIVE_CREDENTIAL_FILES = /* @__PURE__ */ new Set(["localstore.json"]);
|
|
558
|
+
/**
|
|
559
|
+
* MSM 注册表写保护(需求⑤b S142 用户拍板:"msm注册的文件需要写保护起来,避免CCC意外写坏搞崩自己"):
|
|
560
|
+
* mech-registry.json 是 CCC 执行层地基(坏 → loadMsmEntries 抛 → acc_msm/skiff/output-guard 全崩,
|
|
561
|
+
* register 也崩 → 自锁)。**写 deny、读 allow**(与 localstore 读 deny 语义区分 R6——注册表是
|
|
562
|
+
* 结构核心不是秘密,需被 output-guard/skiff-admin 读取建 MSM 词表)。
|
|
563
|
+
*
|
|
564
|
+
* 单级化(需求⑤a)后**唯一合法注册表 = cccName 聚合档**
|
|
565
|
+
* `.opencode/skills/<cccName>/references/mech-registry.json`(cccName = .serenity 首行)。
|
|
566
|
+
* 历史 root 级 `mech-registry.json` 与各 skill 分散注册表已废弃:**不再保护**
|
|
567
|
+
* (review P1:保护一个永不被读的文件 = 死锁——MSM 既不可见又不可删/迁移)。
|
|
568
|
+
* 写工具命中聚合档 → deny(唯一合法写通道 = acc_msm register/deregister 内部 writeRegistry,
|
|
569
|
+
* 走工具实现不经 pre-execute → 天然豁免);读工具放行。
|
|
570
|
+
*/
|
|
571
|
+
function isProtectedRegistryRel(root, rel) {
|
|
572
|
+
const lower = (s) => process.platform === "win32" ? s.toLowerCase() : s;
|
|
573
|
+
const relCi = lower(rel);
|
|
574
|
+
try {
|
|
575
|
+
const marker = resolve(root, ".serenity");
|
|
576
|
+
if (existsSync(marker)) {
|
|
577
|
+
const line = readCccName$2(root);
|
|
578
|
+
if (line) {
|
|
579
|
+
if (relCi === lower(`.opencode/skills/${line}/references/mech-registry.json`)) return true;
|
|
580
|
+
if (relCi === lower(`.opencode/skills/${line}/references`)) return true;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
} catch {}
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
521
586
|
/** 判定工具是否为写类:普通工具按名;cc_fs 复合工具按子命令 action */
|
|
522
587
|
function isWriteTool(toolName, action) {
|
|
523
588
|
if (!WRITE_TOOLS.has(toolName)) return false;
|
|
@@ -561,6 +626,10 @@ function decideGuard(input) {
|
|
|
561
626
|
deny: `CCC governance file "${rel}" is reserved for the user — agent must not write`,
|
|
562
627
|
kind: "deny"
|
|
563
628
|
};
|
|
629
|
+
if (isProtectedRegistryRel(root, rel)) return {
|
|
630
|
+
deny: rel.endsWith("/mech-registry.json") ? `mech-registry.json is ACC-managed (${rel}) — use acc_msm register/deregister instead of writing it directly` : `"${rel}" is an ancestor of the ACC-managed mech-registry.json — removing/moving it would destroy the registry; the registry is managed by acc_msm register/deregister`,
|
|
631
|
+
kind: "deny"
|
|
632
|
+
};
|
|
564
633
|
const hit = matchBlacklist(rel, blacklist);
|
|
565
634
|
if (hit) return {
|
|
566
635
|
deny: hit.message ?? `blacklist blocked: "${pathArg}" matches rule "${hit.pattern}"`,
|
|
@@ -780,14 +849,83 @@ const KIT_ACTIONS = [
|
|
|
780
849
|
"time",
|
|
781
850
|
"wait"
|
|
782
851
|
];
|
|
783
|
-
/** CCC
|
|
852
|
+
/** CCC 名(review P2-2:统一收口到 ccc.ts readCccName——跳 # 注释/空行首非空行;此处保留 null 容错) */
|
|
784
853
|
function readCccName$1(root) {
|
|
785
|
-
|
|
854
|
+
return root ? readCccName$2(root) : null;
|
|
855
|
+
}
|
|
856
|
+
function checkRegistryHealth(root) {
|
|
857
|
+
const cccName = readCccName$1(root);
|
|
858
|
+
if (!cccName) return {
|
|
859
|
+
path: null,
|
|
860
|
+
ok: true,
|
|
861
|
+
present: false,
|
|
862
|
+
issues: []
|
|
863
|
+
};
|
|
864
|
+
const rel = `.opencode/skills/${cccName}/references/mech-registry.json`;
|
|
865
|
+
const abs = resolve(root, rel);
|
|
866
|
+
if (!existsSync(abs)) return {
|
|
867
|
+
path: rel,
|
|
868
|
+
ok: true,
|
|
869
|
+
present: false,
|
|
870
|
+
issues: []
|
|
871
|
+
};
|
|
872
|
+
const issues = [];
|
|
873
|
+
let raw = "";
|
|
786
874
|
try {
|
|
787
|
-
|
|
788
|
-
} catch {
|
|
789
|
-
return
|
|
875
|
+
raw = readFileSync(abs, "utf-8");
|
|
876
|
+
} catch (err) {
|
|
877
|
+
return {
|
|
878
|
+
path: rel,
|
|
879
|
+
ok: false,
|
|
880
|
+
present: true,
|
|
881
|
+
issues: [`registry unreadable: ${String(err?.message ?? err)}`]
|
|
882
|
+
};
|
|
790
883
|
}
|
|
884
|
+
let data;
|
|
885
|
+
try {
|
|
886
|
+
data = JSON.parse(raw.replace(/^\uFEFF/, ""));
|
|
887
|
+
} catch (err) {
|
|
888
|
+
return {
|
|
889
|
+
path: rel,
|
|
890
|
+
ok: false,
|
|
891
|
+
present: true,
|
|
892
|
+
issues: [`registry JSON is broken: ${String(err?.message ?? err)}`, `Fix: restore from git — register/deregister auto-commits the registry (git checkout -- ${rel}; or git restore ${rel})`]
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
const entries = Array.isArray(data) ? data : data?.entries;
|
|
896
|
+
if (!Array.isArray(entries)) return {
|
|
897
|
+
path: rel,
|
|
898
|
+
ok: false,
|
|
899
|
+
present: true,
|
|
900
|
+
issues: ["registry top-level is neither an array nor a v1 wrapper with entries[]", `Fix: restore from git (git checkout -- ${rel}; or git restore ${rel})`]
|
|
901
|
+
};
|
|
902
|
+
const names = /* @__PURE__ */ new Set();
|
|
903
|
+
for (let i = 0; i < entries.length; i++) {
|
|
904
|
+
const e = entries[i];
|
|
905
|
+
if (typeof e !== "object" || e === null) {
|
|
906
|
+
issues.push(`entry[${i}] is not an object`);
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
if (typeof e.name !== "string" || e.name === "") issues.push(`entry[${i}]: name missing or not a string`);
|
|
910
|
+
if (typeof e.path !== "string" || e.path === "") issues.push(`entry[${i}] (${String(e.name ?? "?")}): path missing or not a string`);
|
|
911
|
+
if (e.skill !== void 0 && typeof e.skill !== "string") issues.push(`entry[${i}] (${String(e.name ?? "?")}): skill not a string`);
|
|
912
|
+
if (e.category !== void 0 && typeof e.category !== "string") issues.push(`entry[${i}] (${String(e.name ?? "?")}): category not a string`);
|
|
913
|
+
if (typeof e.name === "string" && e.name !== "") {
|
|
914
|
+
if (names.has(e.name)) issues.push(`duplicate MSM name: "${e.name}" (entry[${i}]) — loadMsmEntries dedups by name, ambiguity`);
|
|
915
|
+
names.add(e.name);
|
|
916
|
+
}
|
|
917
|
+
if (typeof e.path === "string" && e.path !== "") {
|
|
918
|
+
const scriptAbs = resolve(root, e.path);
|
|
919
|
+
if (!pathInside(resolve(root), scriptAbs)) issues.push(`entry[${i}] (${String(e.name ?? "?")}): path "${e.path}" escapes CCC root`);
|
|
920
|
+
else if (!existsSync(scriptAbs)) issues.push(`entry[${i}] (${String(e.name ?? "?")}): script not found at "${e.path}"`);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
return {
|
|
924
|
+
path: rel,
|
|
925
|
+
ok: issues.length === 0,
|
|
926
|
+
present: true,
|
|
927
|
+
issues
|
|
928
|
+
};
|
|
791
929
|
}
|
|
792
930
|
async function runKit(root, args) {
|
|
793
931
|
switch (args.action) {
|
|
@@ -825,6 +963,7 @@ async function runKit(root, args) {
|
|
|
825
963
|
}
|
|
826
964
|
}
|
|
827
965
|
};
|
|
966
|
+
if (root) report.registry = checkRegistryHealth(root);
|
|
828
967
|
if (root) report.config = loadSerenityConfig(root);
|
|
829
968
|
report.accVersion = ACC_VERSION;
|
|
830
969
|
report.dshVersion = readDshVersion();
|
|
@@ -863,7 +1002,7 @@ function renderText$11(value) {
|
|
|
863
1002
|
}
|
|
864
1003
|
const kitTool = defineTool({
|
|
865
1004
|
name: "acc_kit",
|
|
866
|
-
description: "ACC general-purpose utility kit: health (CCC three-principle check P1/P2/config, healthy/degraded
|
|
1005
|
+
description: "ACC general-purpose utility kit: health (CCC three-principle check P1/P2/config + MSM registry integrity report, healthy/degraded) / time (now_iso/now_local/epoch_ms) / wait (wait N seconds, default 1). Routine self-check before entering a CCC.",
|
|
867
1006
|
parameters: {
|
|
868
1007
|
action: {
|
|
869
1008
|
type: "string",
|
|
@@ -1150,13 +1289,9 @@ function bunExecutablePath() {
|
|
|
1150
1289
|
bunExeCache = null;
|
|
1151
1290
|
return null;
|
|
1152
1291
|
}
|
|
1153
|
-
/** CCC 名:从 .serenity
|
|
1292
|
+
/** CCC 名:从 .serenity 解析(review P2-2 统一收口到 ccc.ts readCccName——跳 # 注释/空行首非空行;此处保留导出名兼容内部调用) */
|
|
1154
1293
|
function readCccName(root) {
|
|
1155
|
-
|
|
1156
|
-
return readFileSync(resolve(root, ".serenity"), "utf-8").trim().split("\n")[0]?.trim() || null;
|
|
1157
|
-
} catch {
|
|
1158
|
-
return null;
|
|
1159
|
-
}
|
|
1294
|
+
return readCccName$2(root);
|
|
1160
1295
|
}
|
|
1161
1296
|
/**
|
|
1162
1297
|
* path-arg 逃逸校验:根内 + symlink 防御(对齐 osp validatePathArgsFromTokens)。
|
|
@@ -1194,8 +1329,34 @@ const MSM_ACTIONS = [
|
|
|
1194
1329
|
"deregister",
|
|
1195
1330
|
"check",
|
|
1196
1331
|
"guide",
|
|
1197
|
-
"ccc-config"
|
|
1332
|
+
"ccc-config",
|
|
1333
|
+
"catalog"
|
|
1198
1334
|
];
|
|
1335
|
+
/**
|
|
1336
|
+
* ACC 能力目录(需求④ S142 用户拍板:"acc 配置各自做 guide 越来越多,整合成目录式使用指南")。
|
|
1337
|
+
* 目录在前、详情各归各——**单一真相源**:本目录只索引"去哪个工具/子命令看详情",不复制详情
|
|
1338
|
+
* (避免 skiff_admin/weixin-doctor 等 guide 全文重复 → 多真相源 → 失同步)。
|
|
1339
|
+
* 后续新增功能只需在此加一行(低熵可演进)。
|
|
1340
|
+
*/
|
|
1341
|
+
const ACC_CATALOG = `═══ ACC Usage Catalog ═══
|
|
1342
|
+
Directory of ACC capabilities — each area lists where to go for details (guides live with their feature; this catalog only points).
|
|
1343
|
+
|
|
1344
|
+
① 会话与轨迹 → session tool(list/show/create/use/close/health/qa/archive/summary/hook-develop-guide)
|
|
1345
|
+
session_rebuild(超限重建;阈值 K 见 ccc-config / 设置面板重建阈值K)
|
|
1346
|
+
② 认知质量框架 → eap(E↑/R↓/S↑)/ neat(Neat 协议)/ cce(认知连续性工程)——渐进披露,无参即全文
|
|
1347
|
+
③ 工具与执行 → cc_fs(文件系统 15 子命令)/ cc_git(git)/ acc_kit(health 含注册表检查/time/wait)
|
|
1348
|
+
acc_msm(MSM 框架:本工具 list/exec/register/deregister/check/guide/ccc-config/catalog)
|
|
1349
|
+
handyman(杂工 agent 编排——guide 子命令:acc_msm exec handyman guide)
|
|
1350
|
+
④ 角色与对外面 → skiff_admin(Skiff 认知子集角色:guide/validate/apply/list——acc_msm exec skiff_admin guide)
|
|
1351
|
+
ACP(程序化 JSON-RPC 3100)/ Skiff 问答页(公网 ask)——面板「外部能力」组
|
|
1352
|
+
⑤ 自主与接入 → autopilot-trajectory(Autopilot 一站式:all/init/random/diag/doc/check/status/guide)
|
|
1353
|
+
weixin 微信桥(配置/扫码/路由/消息 hook——CCC 侧 weixin-doctor MSM:acc_msm exec weixin-doctor guide)
|
|
1354
|
+
⑥ CCC 配置总览 → acc_msm ccc-config(8 段:handyman/sessionKeeper/localstore/hooks/safeMode/skiff/autopilotTrajectory/weixin)
|
|
1355
|
+
⑦ 注册表与安全 → mech-registry.json 由 acc_msm register/deregister 管理(写保护——不可直接编辑)
|
|
1356
|
+
acc_kit health 输出 registry 完整性检查;损坏恢复指引见 health registry.issues
|
|
1357
|
+
|
|
1358
|
+
每区:一句话定位 + 详细入口(工具名 / guide 子命令)。详情永远以对应工具的 guide/ccc-config 为单一真相源。
|
|
1359
|
+
`;
|
|
1199
1360
|
const MSM_GUIDE = `MSM Development Manual (Mech & Semi-Mech framework)
|
|
1200
1361
|
|
|
1201
1362
|
## What it is
|
|
@@ -1293,6 +1454,85 @@ safe-mode 由 WebUI 开关控制(写 .serenity-safe-on 标记);黑名单
|
|
|
1293
1454
|
|
|
1294
1455
|
Config:
|
|
1295
1456
|
{ "safeMode": { "blacklist": [".secrets/"] } }
|
|
1457
|
+
|
|
1458
|
+
── 6. skiff.roles(F4 认知子集角色)──
|
|
1459
|
+
Skiff 角色 = 全知全能 trajectory 的任意子集(CCC 定义)。每角色独立模型 + 双白名单
|
|
1460
|
+
(MSM 白名单 msms[] 与非 MSM 工具白名单 tools[],白名单外全隐藏;skill 加载恒可用)。
|
|
1461
|
+
trajectory 纪律子集(session/keeper/rebuild 参与项)默认全关 = 完全独立。
|
|
1462
|
+
systemPrompt 内联 或 systemPromptFile(.md 引用,推荐)——角色会话的完整人格/边界提示词。
|
|
1463
|
+
validate 校验 / apply 生效 / list 查看:acc_msm exec skiff_admin <guide|validate|apply|list>。
|
|
1464
|
+
|
|
1465
|
+
Config:
|
|
1466
|
+
{ "skiff": { "roles": {
|
|
1467
|
+
"qa": {
|
|
1468
|
+
"model": "provider/model",
|
|
1469
|
+
"msms": ["web-search", "vlm-describe"],
|
|
1470
|
+
"tools": ["read", "grep", "glob"],
|
|
1471
|
+
"systemPromptFile": ".opencode/skiff/qa.md"
|
|
1472
|
+
} } } }
|
|
1473
|
+
|
|
1474
|
+
── 7. autopilotTrajectory(自动巡航轨迹)──
|
|
1475
|
+
CCC 定义的一条自主 trajectory——时钟到点自动唤起(前台注入,用户可见可介入)。
|
|
1476
|
+
未配置或 enabled=false → 完全不启动(零资源占用)。多 CCC 独立:每 CCC 自己的配置。
|
|
1477
|
+
唤起消息四段式:轨迹焦点 topPrompt(最先注入,稳定锚)→ 身份锚定 → 先验偏见
|
|
1478
|
+
(CCC 根脚本 biasProvider 输出)→ 任务。目标会话 session 必填(目录须带 --auto 后缀)。
|
|
1479
|
+
诊断/状态/立即唤起:acc_msm exec autopilot-trajectory <all|check|status|diag>。
|
|
1480
|
+
|
|
1481
|
+
Config:
|
|
1482
|
+
{ "autopilotTrajectory": {
|
|
1483
|
+
"enabled": true,
|
|
1484
|
+
"intervalHours": 2,
|
|
1485
|
+
"session": "S151",
|
|
1486
|
+
"biasProvider": "autopilot-bias.ts",
|
|
1487
|
+
"topPrompt": "本轨迹核心目标/纪律/质量要求(CCC 自填,防漂移)",
|
|
1488
|
+
"avoidWakeHours": { "start": 8, "end": 18 }
|
|
1489
|
+
} }
|
|
1490
|
+
|
|
1491
|
+
── 8. weixin(微信桥 F4c-3,含消息记录 hook)──
|
|
1492
|
+
CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC 独立对接微信桥。
|
|
1493
|
+
账号/路由/开关在此文件;**bot_token 凭据在 CCC localstore credential scope**
|
|
1494
|
+
(扫码绑定后自动写入,永不进 git 明文面)。
|
|
1495
|
+
路由 user → role:exact 优先,* 通配兜底;role 必须 ∈ 该 CCC skiff.roles。
|
|
1496
|
+
面板(WebUI 设置 → 微信桥)可扫码绑定/移除账号/编辑路由;acc_msm exec weixin-doctor
|
|
1497
|
+
<status|diag|verify> 排查。凭据主动查看:localstore get WEIXIN_<ACCOUNT>_TOKEN。
|
|
1498
|
+
|
|
1499
|
+
Config:
|
|
1500
|
+
{ "weixin": {
|
|
1501
|
+
"enabled": true,
|
|
1502
|
+
"hook": "scripts/weixin-message-hook.ts",
|
|
1503
|
+
"accounts": [{ "accountId": "wechat-1", "name": "家庭助手", "enabled": true }],
|
|
1504
|
+
"routes": [{ "user": "*", "role": "zhaocai" }]
|
|
1505
|
+
} }
|
|
1506
|
+
|
|
1507
|
+
▸ weixin.hook(消息记录 hook,v1.27.13):
|
|
1508
|
+
微信桥每收/发一条消息触发一次 CCC 自写脚本,由 CCC 自行持久化保存(存哪/存成什么
|
|
1509
|
+
/是否入库全归 CCC——ACC 不绑定存储)。未配置 hook → 零变化(不触发)。
|
|
1510
|
+
|
|
1511
|
+
触发(双向):
|
|
1512
|
+
incoming = 用户 → bot:路由命中后、媒体落盘后触发(文本含语音转写;媒体带落盘 relPath)
|
|
1513
|
+
outgoing = bot → 用户:回复发送成功后触发(reply = 用户实际收到的纯文本,已剥离 think)
|
|
1514
|
+
|
|
1515
|
+
脚本约定(事件 JSON 单行经 stdin 传入;bun 优先 node 兜底):
|
|
1516
|
+
#!/usr/bin/env bun 或 node script.js —— 读 process.stdin 整段 JSON.parse
|
|
1517
|
+
const ev = JSON.parse(await new Response(process.stdin).text())
|
|
1518
|
+
ev.event === 'incoming' | 'outgoing'
|
|
1519
|
+
|
|
1520
|
+
事件 schema(**不含任何会话凭据**——context_token/bot_token/token/aes_key 均不出现):
|
|
1521
|
+
{ "event": "incoming", "ts": 1788359941488, "cccRoot": "/path/ccc",
|
|
1522
|
+
"accountId": "wechat-1", "userId": "u1@im.wechat",
|
|
1523
|
+
"sessionId": "skiff-weixin-xxx", "role": "zhaocai",
|
|
1524
|
+
"message": { "text": "你好", "media": [{ "kind": "image", "relPath": "_tmp/weixin-inbound/<hash>/img_x.jpg" }] } }
|
|
1525
|
+
{ "event": "outgoing", "ts": ..., "cccRoot": ..., "accountId": ...,
|
|
1526
|
+
"userId": ..., "sessionId": ..., "role": ...,
|
|
1527
|
+
"reply": "已记录(纯文本)" }
|
|
1528
|
+
|
|
1529
|
+
示例脚本(追加到按日文件——持久化归 CCC 自选:文件/DB/远端均可):
|
|
1530
|
+
const fs = require('node:fs'); const p = '/path/ccc/AGENT_SESSIONS/_weixin-log.jsonl';
|
|
1531
|
+
fs.appendFileSync(p, JSON.stringify(ev) + '\\n');
|
|
1532
|
+
|
|
1533
|
+
执行语义(旁路容忍):异步 fire-and-forget + 15s 超时 kill + 失败仅日志——
|
|
1534
|
+
微信桥消息处理/回复不受 hook 影响;脚本须在 CCC 根内(路径逃逸拒绝)。
|
|
1535
|
+
媒体 relPath 指向 _tmp/weixin-inbound/(临时目录)——需持久保存媒体文件请自行 copy。
|
|
1296
1536
|
`;
|
|
1297
1537
|
function parseRegistry(raw) {
|
|
1298
1538
|
const data = JSON.parse(raw.replace(/^\uFEFF/, ""));
|
|
@@ -1301,16 +1541,18 @@ function parseRegistry(raw) {
|
|
|
1301
1541
|
if (!Array.isArray(entries)) throw new Error("invalid registry: missing entries[]");
|
|
1302
1542
|
return entries;
|
|
1303
1543
|
}
|
|
1544
|
+
/**
|
|
1545
|
+
* 注册表单级化(需求⑤a S142 用户拍板:注册表只有一级——对齐 osp cccName 单聚合档):
|
|
1546
|
+
* 所有 entry 集中写入 `.opencode/skills/<cccName>/references/mech-registry.json`
|
|
1547
|
+
* (cccName = .serenity 首行,即顶层入口 skill 目录),skill 只作 entry 字段。
|
|
1548
|
+
* 不再扫描/写入各 skill 目录的分散注册表(历史形态 v1.14~v1.27,register --skill
|
|
1549
|
+
* 曾写入各自 skill 目录 —— 多真相源,废弃)。
|
|
1550
|
+
*/
|
|
1304
1551
|
function findRegistries(root) {
|
|
1305
|
-
const
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
if (existsSync(p)) out.push(p);
|
|
1310
|
-
}
|
|
1311
|
-
const rootRegistry = join(root, "mech-registry.json");
|
|
1312
|
-
if (existsSync(rootRegistry)) out.push(rootRegistry);
|
|
1313
|
-
return out;
|
|
1552
|
+
const cccName = readCccName(root);
|
|
1553
|
+
if (!cccName) return [];
|
|
1554
|
+
const aggregate = join(root, ".opencode", "skills", cccName, "references", "mech-registry.json");
|
|
1555
|
+
return existsSync(aggregate) ? [aggregate] : [];
|
|
1314
1556
|
}
|
|
1315
1557
|
function loadMsmEntries(root) {
|
|
1316
1558
|
const byName = /* @__PURE__ */ new Map();
|
|
@@ -1332,8 +1574,13 @@ function scanSkillScripts(root) {
|
|
|
1332
1574
|
}
|
|
1333
1575
|
return out.sort();
|
|
1334
1576
|
}
|
|
1335
|
-
|
|
1336
|
-
|
|
1577
|
+
/**
|
|
1578
|
+
* 注册表写入路径(需求⑤a 单级化):永远返回 cccName 聚合档。
|
|
1579
|
+
* skill 参数保留签名(兼容调用方)但只进 entry 字段,不决定写入位置。
|
|
1580
|
+
*/
|
|
1581
|
+
function registryPathFor(root, _skill) {
|
|
1582
|
+
const cccName = readCccName(root) ?? "unknown";
|
|
1583
|
+
return join(root, ".opencode", "skills", cccName, "references", "mech-registry.json");
|
|
1337
1584
|
}
|
|
1338
1585
|
function writeRegistry(path, entries, isV1Wrapped = true) {
|
|
1339
1586
|
mkdirSync(dirname(path), { recursive: true });
|
|
@@ -1359,6 +1606,7 @@ function runMsm(root, args) {
|
|
|
1359
1606
|
return `${header}\n` + lines.join("\n");
|
|
1360
1607
|
}
|
|
1361
1608
|
case "guide": return { guide: MSM_GUIDE };
|
|
1609
|
+
case "catalog": return { catalog: ACC_CATALOG };
|
|
1362
1610
|
case "exec": {
|
|
1363
1611
|
const { entry, businessArgs, fmtJson, hasHelp, protocol } = prepareExec(root, args);
|
|
1364
1612
|
const p = protocolResult(protocol);
|
|
@@ -1404,7 +1652,7 @@ function runMsm(root, args) {
|
|
|
1404
1652
|
if (loadMsmEntries(root).some((e) => e.name === name)) throw new Error(`MSM already registered: "${name}"`);
|
|
1405
1653
|
const regPath = registryPathFor(root, skill);
|
|
1406
1654
|
const raw = existsSync(regPath) ? readFileSync(regPath, "utf-8").replace(/^\uFEFF/, "") : "";
|
|
1407
|
-
const isV1Wrapped = raw
|
|
1655
|
+
const isV1Wrapped = raw === "" ? true : !Array.isArray(JSON.parse(raw));
|
|
1408
1656
|
const entries = existsSync(regPath) ? parseRegistry(readFileSync(regPath, "utf-8")) : [];
|
|
1409
1657
|
let flags;
|
|
1410
1658
|
if (args.flags) try {
|
|
@@ -1697,13 +1945,13 @@ function renderText$9(value) {
|
|
|
1697
1945
|
}
|
|
1698
1946
|
const msmTool = defineTool({
|
|
1699
1947
|
name: "acc_msm",
|
|
1700
|
-
description: "MSM (Mech & Semi-Mech) framework: list lists registered MSMs (header+flags display); exec executes (600s timeout, path-escape + symlink blocking, injects SERENITY_ROOT/CCC/VERSION env, appends --help TIP on failure; first arg --list/--schema/--format=json is protocol); register/deregister manage the registry (path inside root + script exists + globally unique validation, auto git precision commit); check quality checks DC-M1~M4; guide development manual; ccc-config CCC config reference (handyman.models/sessionKeeper.threshold/localstore.gitTrack/hooks.autoRestoreSession). Reuses the CCC mech-registry.json.",
|
|
1948
|
+
description: "MSM (Mech & Semi-Mech) framework: list lists registered MSMs (header+flags display); exec executes (600s timeout, path-escape + symlink blocking, injects SERENITY_ROOT/CCC/VERSION env, appends --help TIP on failure; first arg --list/--schema/--format=json is protocol); register/deregister manage the registry (path inside root + script exists + globally unique validation, auto git precision commit); check quality checks DC-M1~M4; guide development manual; catalog ACC usage directory (all capability areas — points to each feature guide, single source of truth); ccc-config CCC config reference (all sections: handyman.models / sessionKeeper.threshold / localstore.gitTrack / hooks.autoRestoreSession / safeMode.blacklist / skiff.roles / autopilotTrajectory / weixin incl. message hook full guide). Reuses the CCC mech-registry.json.",
|
|
1701
1949
|
parameters: {
|
|
1702
1950
|
action: {
|
|
1703
1951
|
type: "string",
|
|
1704
1952
|
enum: [...MSM_ACTIONS],
|
|
1705
1953
|
required: true,
|
|
1706
|
-
description: "Subcommand: list/exec/register/deregister/check/guide/ccc-config"
|
|
1954
|
+
description: "Subcommand: list/exec/register/deregister/check/guide/catalog/ccc-config"
|
|
1707
1955
|
},
|
|
1708
1956
|
name: {
|
|
1709
1957
|
type: "string",
|
|
@@ -2936,26 +3184,45 @@ function agentScope$2(exec) {
|
|
|
2936
3184
|
return exec.agent?.session?.id ?? "default";
|
|
2937
3185
|
}
|
|
2938
3186
|
/**
|
|
2939
|
-
*
|
|
3187
|
+
* 清洗 + 截断会话概括(需求② S142 用户拍板:编号日期后加 ≤20 字内容概括)。
|
|
3188
|
+
* 规则(服务端统一,不信任 LLM 输入):
|
|
3189
|
+
* - 去控制字符/换行/回车/制表(防标题注入/多行污染)
|
|
3190
|
+
* - trim(去首尾空白)
|
|
3191
|
+
* - 截断 ≤20 字符(按 Unicode 码点——中英混排统一;emoji 等代理对按码点保留)
|
|
3192
|
+
* - 去 `/`(防标题被误读为路径分隔)
|
|
3193
|
+
* @returns 清洗后的概括(空输入 → 空串;调用方决定是否允许空)
|
|
3194
|
+
*/
|
|
3195
|
+
function sanitizeSessionSummary(summary) {
|
|
3196
|
+
return [...summary.replace(/[\u0000-\u001f\u007f]/g, "").replace(/\//g, "").trim()].slice(0, 20).join("");
|
|
3197
|
+
}
|
|
3198
|
+
/**
|
|
3199
|
+
* 从激活会话派生命名标题(v1.22.9 格式修正 + 需求② 概括):
|
|
2940
3200
|
* F3 原始需求是 **`S###-日期`**(如 `S143-2026-08-26`)——从 `sessionId` 派生,
|
|
2941
3201
|
* 而非完整目录名(`2026-08-24--S142--...` 超长 + 中文,不符合用户拍板格式)。
|
|
3202
|
+
* 需求②(S142 用户拍板):编号日期后加 ≤20 字内容概括 → `S###-YYYY-MM-DD-<概括>`
|
|
3203
|
+
* ——概括来自显式 summary 参数(服务端截断/清洗,可靠不靠猜);编号日期仍固定派生。
|
|
2942
3204
|
* 无 S### 编号(issue 会话等)→ 回退目录名。
|
|
2943
|
-
* @
|
|
3205
|
+
* @param active 激活会话信息(sessionId + dirName)
|
|
3206
|
+
* @param summary 内容概括(≤20 字,服务端清洗截断;空 → 不带概括的 `S###-日期`)
|
|
3207
|
+
* @returns `S143-2026-08-26-概括` / `S143-2026-08-26` / 原目录名
|
|
2944
3208
|
*/
|
|
2945
|
-
function namingTitleFor(active) {
|
|
3209
|
+
function namingTitleFor(active, summary) {
|
|
2946
3210
|
const sid = active.sessionId;
|
|
2947
3211
|
if (typeof sid === "string" && /^S\d+$/.test(sid)) {
|
|
2948
3212
|
const date = active.dirName.match(/^(\d{4}-\d{2}-\d{2})--/)?.[1] ?? "";
|
|
2949
|
-
|
|
2950
|
-
|
|
3213
|
+
const cleaned = summary ? sanitizeSessionSummary(summary) : "";
|
|
3214
|
+
const base = date ? `${sid}-${date}` : sid;
|
|
3215
|
+
return cleaned ? `${base}-${cleaned}` : base;
|
|
2951
3216
|
}
|
|
2952
3217
|
return active.dirName;
|
|
2953
3218
|
}
|
|
2954
3219
|
/**
|
|
2955
|
-
* use 激活宁静号会话后,把当前 dsh 会话重命名为命名标题(`S
|
|
3220
|
+
* use 激活宁静号会话后,把当前 dsh 会话重命名为命名标题(`S###-日期[-概括]`)。
|
|
2956
3221
|
* v1.27.1:**永远开启**(不再有 naming.enabled 门控)——仅 sessionTitle 服务
|
|
2957
3222
|
* 存在性守卫;失败不静默——返回结果对象而非 null(v1.22.9),调用方决定可见性。
|
|
2958
3223
|
*
|
|
3224
|
+
* 需求②(S142 用户拍板):summary 参数(≤20 字概括)→ 标题带概括;编号日期固定派生。
|
|
3225
|
+
*
|
|
2959
3226
|
* v1.23.2 修复(this 绑定):第三参从**解构的裸 rename 函数**改为**整个
|
|
2960
3227
|
* sessionTitle 服务对象**——内部以 `titles.rename(session, title)` **方法调用**
|
|
2961
3228
|
* (this = titles 服务实例)。旧实现调用点 `const rename = titles.rename` 解构
|
|
@@ -2964,7 +3231,7 @@ function namingTitleFor(active) {
|
|
|
2964
3231
|
* 与 v1.20.2/1.20.3 图片落盘同款解构丢 this bug)。
|
|
2965
3232
|
* @returns { title, ok } 或 { ok:false, reason }(未执行/失败均返回对象)
|
|
2966
3233
|
*/
|
|
2967
|
-
function renameDshSessionOnUse(deps, session, titles, active) {
|
|
3234
|
+
function renameDshSessionOnUse(deps, session, titles, active, summary) {
|
|
2968
3235
|
if (!deps.sessionTitleAvailable) return {
|
|
2969
3236
|
ok: false,
|
|
2970
3237
|
reason: "sessionTitle service unavailable"
|
|
@@ -2973,7 +3240,7 @@ function renameDshSessionOnUse(deps, session, titles, active) {
|
|
|
2973
3240
|
ok: false,
|
|
2974
3241
|
reason: "sessionTitle service unavailable"
|
|
2975
3242
|
};
|
|
2976
|
-
const title = namingTitleFor(active);
|
|
3243
|
+
const title = namingTitleFor(active, summary);
|
|
2977
3244
|
try {
|
|
2978
3245
|
titles.rename(session, title);
|
|
2979
3246
|
return {
|
|
@@ -3001,12 +3268,13 @@ function activeInfoFromCreate(result) {
|
|
|
3001
3268
|
}
|
|
3002
3269
|
/**
|
|
3003
3270
|
* 把当前 dsh 会话重命名为指定 SESSION 的命名标题(use/create 共用;v1.25.11)。
|
|
3271
|
+
* 需求②:summary 参数(≤20 字概括)透传——标题带概括(编号日期固定派生)。
|
|
3004
3272
|
* 门控/失败可见性与 renameDshSessionOnUse 一致(不静默:成功 log / 失败 warn)。
|
|
3005
3273
|
* 调用点(use 分支原内联逻辑提取,create 分支复用):
|
|
3006
3274
|
* - use:激活后从 activeStore 取 info
|
|
3007
3275
|
* - create:createSession 结果经 activeInfoFromCreate 构造 info
|
|
3008
3276
|
*/
|
|
3009
|
-
function renameDshSessionForActive(ctx, exec, info) {
|
|
3277
|
+
function renameDshSessionForActive(ctx, exec, info, summary) {
|
|
3010
3278
|
try {
|
|
3011
3279
|
const titles = ctx.get?.("sessionTitle");
|
|
3012
3280
|
const dshSession = exec.agent?.session;
|
|
@@ -3014,7 +3282,7 @@ function renameDshSessionForActive(ctx, exec, info) {
|
|
|
3014
3282
|
console.warn(`[serenity-hooks] dsh 会话重命名未执行: 缺少 agent session(info: ${info.sessionId})`);
|
|
3015
3283
|
return;
|
|
3016
3284
|
}
|
|
3017
|
-
const result = renameDshSessionOnUse({ sessionTitleAvailable: true }, dshSession, titles, info);
|
|
3285
|
+
const result = renameDshSessionOnUse({ sessionTitleAvailable: true }, dshSession, titles, info, summary);
|
|
3018
3286
|
if (result.ok) console.log(`[serenity-hooks] dsh 会话已重命名: ${String(dshSession.id ?? "?")} → ${result.title}`);
|
|
3019
3287
|
else console.warn(`[serenity-hooks] dsh 会话重命名未执行: ${result.reason}`);
|
|
3020
3288
|
} catch (err) {
|
|
@@ -3120,7 +3388,7 @@ function getHookDevelopGuide(hasSessionTool) {
|
|
|
3120
3388
|
function createSessionTool(ctx) {
|
|
3121
3389
|
return defineTool({
|
|
3122
3390
|
name: "session",
|
|
3123
|
-
description: "Full work-session lifecycle (AGENT_SESSIONS/, home-session convention). list/show/create/use/close/health/qa/archive/summary/hook-develop-guide. create requires --desc <desc> [--goal] or --issue <ticket> (exactly one); close requires --confirm; use activates the session for the current dsh conversation (in-memory + events restore, isolated per dsh session).",
|
|
3391
|
+
description: "Full work-session lifecycle (AGENT_SESSIONS/, home-session convention). list/show/create/use/close/health/qa/archive/summary/hook-develop-guide. create requires --desc <desc> [--goal] or --issue <ticket> (exactly one) plus --summary (≤20 chars, content summary — required, except --dry-run preview and --issue sessions which are exempt); close requires --confirm; use activates the session for the current dsh conversation (in-memory + events restore, isolated per dsh session) and requires --summary (≤20 chars). The summary is appended to the dsh session title (S###-YYYY-MM-DD-<summary>); the S### id and date stay server-derived.",
|
|
3124
3392
|
parameters: {
|
|
3125
3393
|
action: {
|
|
3126
3394
|
type: "string",
|
|
@@ -3144,6 +3412,10 @@ function createSessionTool(ctx) {
|
|
|
3144
3412
|
type: "string",
|
|
3145
3413
|
description: "create one-sentence goal (optional)"
|
|
3146
3414
|
},
|
|
3415
|
+
summary: {
|
|
3416
|
+
type: "string",
|
|
3417
|
+
description: "content summary ≤20 chars (REQUIRED for use and create — create exempts --dry-run preview and --issue sessions) — appended to the dsh session title as S###-YYYY-MM-DD-<summary>; the S### id and date stay server-derived; sanitized/truncated server-side"
|
|
3418
|
+
},
|
|
3147
3419
|
confirm: {
|
|
3148
3420
|
type: "boolean",
|
|
3149
3421
|
description: "close must be true (prevents accidental close)"
|
|
@@ -3171,16 +3443,19 @@ function createSessionTool(ctx) {
|
|
|
3171
3443
|
if (!args.name) throw new Error("show requires name (S### or directory name)");
|
|
3172
3444
|
return showSession(root, args.name) + extHint;
|
|
3173
3445
|
case "create": {
|
|
3446
|
+
const isDryRun = args.dryRun ?? false;
|
|
3447
|
+
const isIssueSession = typeof args.issue === "string" && args.issue !== "";
|
|
3448
|
+
if (!isDryRun && !isIssueSession && (!args.summary || args.summary.trim() === "")) throw new Error("create requires --summary <content summary ≤20 chars> (appended to the dsh session title; the S### id and date stay server-derived)");
|
|
3174
3449
|
const result = createSession({
|
|
3175
3450
|
root,
|
|
3176
3451
|
desc: args.desc,
|
|
3177
3452
|
issue: args.issue,
|
|
3178
3453
|
goal: args.goal,
|
|
3179
|
-
dryRun:
|
|
3454
|
+
dryRun: isDryRun
|
|
3180
3455
|
});
|
|
3181
3456
|
let message = result.message;
|
|
3182
|
-
if (!
|
|
3183
|
-
if (!
|
|
3457
|
+
if (!isDryRun) renameDshSessionForActive(ctx, exec, activeInfoFromCreate(result), args.summary ?? args.issue);
|
|
3458
|
+
if (!isDryRun && cccHooks.includes("create-transform")) try {
|
|
3184
3459
|
const hookResult = await runMsmAsync(root, {
|
|
3185
3460
|
action: "exec",
|
|
3186
3461
|
name: "session-tool",
|
|
@@ -3195,10 +3470,11 @@ function createSessionTool(ctx) {
|
|
|
3195
3470
|
}
|
|
3196
3471
|
case "use": {
|
|
3197
3472
|
if (!args.name) throw new Error("use requires name (S### or directory name)");
|
|
3473
|
+
if (!args.summary || args.summary.trim() === "") throw new Error("use requires --summary <content summary ≤20 chars> (appended to the dsh session title as S###-YYYY-MM-DD-<summary>; id and date stay server-derived)");
|
|
3198
3474
|
const scope = agentScope$2(exec);
|
|
3199
3475
|
const active = useSession(root, args.name, scope);
|
|
3200
3476
|
const info = getActiveSessionInfo(scope);
|
|
3201
|
-
if (info) renameDshSessionForActive(ctx, exec, info);
|
|
3477
|
+
if (info) renameDshSessionForActive(ctx, exec, info, args.summary);
|
|
3202
3478
|
return active;
|
|
3203
3479
|
}
|
|
3204
3480
|
case "close":
|
|
@@ -3598,6 +3874,32 @@ function resolveSessionMdPath(root, scope, session) {
|
|
|
3598
3874
|
const pendingRebuilds = /* @__PURE__ */ new Map();
|
|
3599
3875
|
/** 陈旧队列存活时长(毫秒):超过则丢弃(turn 异常结束/agent 崩溃时防残留误清空) */
|
|
3600
3876
|
const PENDING_TTL_MS = 6e5;
|
|
3877
|
+
let diagState = {
|
|
3878
|
+
lastTs: "",
|
|
3879
|
+
lastSessionId: "",
|
|
3880
|
+
lastEvent: "",
|
|
3881
|
+
queueCount: 0,
|
|
3882
|
+
rebuiltCount: 0,
|
|
3883
|
+
droppedCount: 0,
|
|
3884
|
+
failedCount: 0
|
|
3885
|
+
};
|
|
3886
|
+
function writeRebuildDiag(root, entry) {
|
|
3887
|
+
try {
|
|
3888
|
+
const dir = join(root, "AGENT_SESSIONS");
|
|
3889
|
+
mkdirSync(dir, { recursive: true });
|
|
3890
|
+
const file = join(dir, ".rebuild-diag.json");
|
|
3891
|
+
const s = diagState;
|
|
3892
|
+
if (entry.event === "queued") s.queueCount += 1;
|
|
3893
|
+
if (entry.event === "rebuilt") s.rebuiltCount += 1;
|
|
3894
|
+
if (entry.event === "ttl-dropped") s.droppedCount += 1;
|
|
3895
|
+
if (entry.event === "failed") s.failedCount += 1;
|
|
3896
|
+
s.lastTs = (/* @__PURE__ */ new Date()).toISOString();
|
|
3897
|
+
s.lastSessionId = entry.sessionId;
|
|
3898
|
+
s.lastEvent = entry.event;
|
|
3899
|
+
s.detail = entry.detail;
|
|
3900
|
+
writeFileSync(file, JSON.stringify(s, null, 2) + "\n", "utf-8");
|
|
3901
|
+
} catch {}
|
|
3902
|
+
}
|
|
3601
3903
|
/**
|
|
3602
3904
|
* 排队一次重建(v1.22.4 定稿语义第一步):
|
|
3603
3905
|
* ① 门控校验(rebuild.enabled + 会话定位)
|
|
@@ -3607,16 +3909,22 @@ const PENDING_TTL_MS = 6e5;
|
|
|
3607
3909
|
*/
|
|
3608
3910
|
async function queueRebuild(ctx, opts) {
|
|
3609
3911
|
if (!readSimpleSettings().rebuildEnabled) throw new Error("session_rebuild is disabled (rebuild.enabled=false — enable it in the dsh settings panel)");
|
|
3610
|
-
const { root, note, dshSessionId } = opts;
|
|
3912
|
+
const { root, note, summary, dshSessionId } = opts;
|
|
3611
3913
|
const session = ctx.sessions?.get?.(dshSessionId);
|
|
3612
3914
|
if (!session) throw new Error(`Unable to locate dsh session ${dshSessionId} (session may be closed)`);
|
|
3613
3915
|
const mdPath = resolveSessionMdPath(root, dshSessionId, session);
|
|
3614
|
-
if (!mdPath) throw new Error("Unable to determine the active SESSION.md — no session context found in this conversation. Run \"session use <S
|
|
3916
|
+
if (!mdPath) throw new Error("Unable to determine the active SESSION.md — no session context found in this conversation. Run \"session use <S###> --summary <内容概括 ≤20 字>\" first to activate the trajectory to resume, then retry session_rebuild.");
|
|
3615
3917
|
const anchor = buildRebuildAnchor(root, getActiveSessionInfo(dshSessionId)?.sessionId ?? sessionNameFromMdPath(mdPath), mdPath);
|
|
3616
3918
|
pendingRebuilds.set(dshSessionId, {
|
|
3617
3919
|
anchor,
|
|
3920
|
+
summary,
|
|
3921
|
+
mdPath,
|
|
3618
3922
|
queuedAt: Date.now()
|
|
3619
3923
|
});
|
|
3924
|
+
writeRebuildDiag(root, {
|
|
3925
|
+
sessionId: dshSessionId,
|
|
3926
|
+
event: "queued"
|
|
3927
|
+
});
|
|
3620
3928
|
return {
|
|
3621
3929
|
queued: true,
|
|
3622
3930
|
anchor,
|
|
@@ -3677,6 +3985,33 @@ function performRebuild(session, pending, meter) {
|
|
|
3677
3985
|
return true;
|
|
3678
3986
|
}
|
|
3679
3987
|
/**
|
|
3988
|
+
* 重建后重命名 dsh 会话标题(需求② S142 用户拍板:rebuild 后标题带新阶段概括)。
|
|
3989
|
+
* 标题 = S###-YYYY-MM-DD-<summary>——S### 与日期从持久轨迹目录名派生(不信任 LLM),
|
|
3990
|
+
* 概括来自 queueRebuild 的 summary 参数(服务端清洗截断)。
|
|
3991
|
+
* 失败仅 warn 不阻断重建主流程(标题美观非关键路径)。
|
|
3992
|
+
*/
|
|
3993
|
+
function renameAfterRebuild(ctx, agent, pending) {
|
|
3994
|
+
try {
|
|
3995
|
+
const mdPath = pending.mdPath;
|
|
3996
|
+
const dirName = basename(dirname(mdPath));
|
|
3997
|
+
const idMatch = dirName.match(/--S(\d{3,})--/);
|
|
3998
|
+
const title = namingTitleFor({
|
|
3999
|
+
sessionId: idMatch ? `S${idMatch[1]}` : dirName.replace(/^\d{4}-\d{2}-\d{2}--/, ""),
|
|
4000
|
+
dirName,
|
|
4001
|
+
mdPath
|
|
4002
|
+
}, pending.summary);
|
|
4003
|
+
const titles = ctx.get?.("sessionTitle");
|
|
4004
|
+
if (!titles || typeof titles.rename !== "function") {
|
|
4005
|
+
console.warn("[serenity-hooks] rebuild 后重命名跳过: sessionTitle 服务不可用");
|
|
4006
|
+
return;
|
|
4007
|
+
}
|
|
4008
|
+
titles.rename(agent.session, title);
|
|
4009
|
+
console.log(`[serenity-hooks] rebuild 后会话重命名: ${agent.id} → ${title}`);
|
|
4010
|
+
} catch (error) {
|
|
4011
|
+
console.warn(`[serenity-hooks] rebuild 后重命名失败(不阻断): ${String(error?.message ?? error)}`);
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
4014
|
+
/**
|
|
3680
4015
|
* 注册 turn-stopping 钩子(index.ts apply 调用):
|
|
3681
4016
|
* agent 每轮 turn 结束前(serial)检查 pending 队列——有该会话的重建请求 → 执行清空
|
|
3682
4017
|
* 并 **steer 自动继续**(v1.22.5:next-step 非空 → turn 不 break → 模型自动读取
|
|
@@ -3692,6 +4027,11 @@ function registerRebuildTurnHook(ctx) {
|
|
|
3692
4027
|
if (pending === void 0) return;
|
|
3693
4028
|
if (Date.now() - pending.queuedAt > PENDING_TTL_MS) {
|
|
3694
4029
|
pendingRebuilds.delete(id);
|
|
4030
|
+
writeRebuildDiag(resolveSerenityRootFor(agent), {
|
|
4031
|
+
sessionId: id,
|
|
4032
|
+
event: "ttl-dropped",
|
|
4033
|
+
detail: `queuedAt=${new Date(pending.queuedAt).toISOString()} older than ${PENDING_TTL_MS / 6e4}min TTL — turn did not end in time`
|
|
4034
|
+
});
|
|
3695
4035
|
return;
|
|
3696
4036
|
}
|
|
3697
4037
|
pendingRebuilds.delete(id);
|
|
@@ -3699,6 +4039,7 @@ function registerRebuildTurnHook(ctx) {
|
|
|
3699
4039
|
const tokenMeter = ctx.get?.("tokenMeter");
|
|
3700
4040
|
const meter = tokenMeter && typeof tokenMeter.estimateMessage === "function" ? { estimateMessage: (m) => tokenMeter.estimateMessage(m) } : void 0;
|
|
3701
4041
|
if (performRebuild(agent.session, pending, meter)) {
|
|
4042
|
+
renameAfterRebuild(ctx, agent, pending);
|
|
3702
4043
|
agent.steer(createUserMessage({
|
|
3703
4044
|
content: [{
|
|
3704
4045
|
type: "text",
|
|
@@ -3706,13 +4047,31 @@ function registerRebuildTurnHook(ctx) {
|
|
|
3706
4047
|
}],
|
|
3707
4048
|
source: PLUGIN_SOURCE$4
|
|
3708
4049
|
}));
|
|
4050
|
+
writeRebuildDiag(resolveSerenityRootFor(agent), {
|
|
4051
|
+
sessionId: id,
|
|
4052
|
+
event: "rebuilt"
|
|
4053
|
+
});
|
|
3709
4054
|
console.log(`[serenity-hooks] session_rebuild executed with auto-continue (turn ${payload.turn ?? "?"} ended): ${id}`);
|
|
3710
|
-
}
|
|
4055
|
+
} else writeRebuildDiag(resolveSerenityRootFor(agent), {
|
|
4056
|
+
sessionId: id,
|
|
4057
|
+
event: "empty-surface"
|
|
4058
|
+
});
|
|
3711
4059
|
} catch (error) {
|
|
3712
|
-
|
|
4060
|
+
const msg = String(error?.message ?? error);
|
|
4061
|
+
writeRebuildDiag(resolveSerenityRootFor(agent), {
|
|
4062
|
+
sessionId: id,
|
|
4063
|
+
event: "failed",
|
|
4064
|
+
detail: msg
|
|
4065
|
+
});
|
|
4066
|
+
console.warn(`[serenity-hooks] session_rebuild failed: ${msg}`);
|
|
3713
4067
|
}
|
|
3714
4068
|
});
|
|
3715
4069
|
}
|
|
4070
|
+
/** 从 agent 会话 cwd 解析 CCC 根(诊断落盘用);无则回退进程 cwd */
|
|
4071
|
+
function resolveSerenityRootFor(agent) {
|
|
4072
|
+
const cwd = agent.session?.header?.cwd;
|
|
4073
|
+
return findSerenityRoot(cwd ?? process.cwd()) ?? process.cwd();
|
|
4074
|
+
}
|
|
3716
4075
|
//#endregion
|
|
3717
4076
|
//#region src/tools/rebuild.ts
|
|
3718
4077
|
/**
|
|
@@ -3747,10 +4106,16 @@ function createRebuildTool(ctx) {
|
|
|
3747
4106
|
return defineTool({
|
|
3748
4107
|
name: "session_rebuild",
|
|
3749
4108
|
description: "Trajectory-tracker overflow rebuild (Ship of Theseus): this session is the rebuildable carrier of a trajectory — the current conversation is discarded and rebuilt in place at the end of this turn with the anchor \"continue the work of {SESSION name}\" (first-anchor protocol body included), then auto-continues. SESSION.md (the trajectory's persistent body) stays in place; identity continues from it. Use when you receive a [TRAJECTORY] reminder (context above threshold), at a natural pause point. After triggering, resume from SESSION.md when this turn ends.",
|
|
3750
|
-
parameters: {
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
4109
|
+
parameters: {
|
|
4110
|
+
note: {
|
|
4111
|
+
type: "string",
|
|
4112
|
+
description: "Optional: one-sentence rebuild background note (for the rebuilt self)"
|
|
4113
|
+
},
|
|
4114
|
+
summary: {
|
|
4115
|
+
type: "string",
|
|
4116
|
+
description: "REQUIRED: content summary ≤20 chars of the next work phase — the dsh session title is renamed to S###-YYYY-MM-DD-<summary> after the rebuild (the rebuilt phase gets a fresh summary); sanitized/truncated server-side"
|
|
4117
|
+
}
|
|
4118
|
+
},
|
|
3754
4119
|
output: {
|
|
3755
4120
|
schema: { type: "json" },
|
|
3756
4121
|
render: (args, value) => renderText$3(value)
|
|
@@ -3760,9 +4125,11 @@ function createRebuildTool(ctx) {
|
|
|
3760
4125
|
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
3761
4126
|
const dshSessionId = agentSessionId(exec);
|
|
3762
4127
|
if (!dshSessionId) throw new Error("Unable to determine the current dsh session id");
|
|
4128
|
+
if (!args.summary || args.summary.trim() === "") throw new Error("session_rebuild requires --summary <content summary ≤20 chars> (the dsh session title is renamed after rebuild to S###-YYYY-MM-DD-<summary>)");
|
|
3763
4129
|
const result = await queueRebuild(ctx, {
|
|
3764
4130
|
root,
|
|
3765
4131
|
note: args.note,
|
|
4132
|
+
summary: args.summary,
|
|
3766
4133
|
agentCwd: agentCwd$4(exec),
|
|
3767
4134
|
dshSessionId
|
|
3768
4135
|
});
|
|
@@ -4744,12 +5111,13 @@ function reminderText(code, score) {
|
|
|
4744
5111
|
*
|
|
4745
5112
|
* escalated=true(v1.23.3):连续多轮超阈值仍未 rebuild → 升级强制语气
|
|
4746
5113
|
* (STOP and rebuild now,持续注入直到调用 session_rebuild)。
|
|
5114
|
+
*
|
|
5115
|
+
* 需求①(S142 用户拍板):百分比比例 → K 数值——tokensK = 实际占用(千 token),
|
|
5116
|
+
* thresholdK = 配置阈值(千 token);文案 `Context usage at NNNK (threshold NNNK)`。
|
|
4747
5117
|
*/
|
|
4748
|
-
function rebuildReminderText(
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
if (escalated) return `[TRAJECTORY-ESCALATED] Context usage at ${pct}% (threshold ${thr}%) — you have been reminded repeatedly and have NOT called session_rebuild. This is now mandatory: STOP at the current task step, preserve valuable cognition into the CCC skills (or write a new-skill proposal into SESSION.md), then call the session_rebuild tool immediately. The conversation will be cleared and rebuilt in place; SESSION.md is the persistent trajectory and stays in place — identity continues from it. Do not continue working without rebuilding; this reminder persists until you call session_rebuild.`;
|
|
4752
|
-
return `[TRAJECTORY] Context usage at ${pct}% (threshold ${thr}%). This session is the rebuildable carrier of the trajectory: SESSION.md is the persistent body, this conversation is only a temporary work copy. Before rebuilding: if this conversation produced valuable cognition, revise the relevant existing skill of this CCC (structure it with eap); if a new skill is warranted, write a short proposal into SESSION.md for the user to review — do not create it yourself. ACT NOW: at the next natural pause (end of the current task step), call the session_rebuild tool to clear and rebuild this conversation — the current copy is discarded, identity continues from SESSION.md. If you are in the middle of an unbreakable step, continue it, then rebuild at its end. Do not ignore this; rebuild is the expected action, not an option.`;
|
|
5118
|
+
function rebuildReminderText(tokensK, thresholdK, escalated = false) {
|
|
5119
|
+
if (escalated) return `[TRAJECTORY-ESCALATED] Context usage at ${Math.round(tokensK)}K (threshold ${Math.round(thresholdK)}K) — you have been reminded repeatedly and have NOT called session_rebuild. This is now mandatory: STOP at the current task step, preserve valuable cognition into the CCC skills (or write a new-skill proposal into SESSION.md), then call the session_rebuild tool immediately, passing --summary "<content summary ≤20 chars>" (required; the dsh session title is renamed to S###-YYYY-MM-DD-<summary> after rebuild). The conversation will be cleared and rebuilt in place; SESSION.md is the persistent trajectory and stays in place — identity continues from it. Do not continue working without rebuilding; this reminder persists until you call session_rebuild.`;
|
|
5120
|
+
return `[TRAJECTORY] Context usage at ${Math.round(tokensK)}K (threshold ${Math.round(thresholdK)}K). This session is the rebuildable carrier of the trajectory: SESSION.md is the persistent body, this conversation is only a temporary work copy. Before rebuilding: if this conversation produced valuable cognition, revise the relevant existing skill of this CCC (structure it with eap); if a new skill is warranted, write a short proposal into SESSION.md for the user to review — do not create it yourself. ACT NOW: at the next natural pause (end of the current task step), call the session_rebuild tool — passing --summary "<content summary ≤20 chars>" describing the next work phase (required; the dsh session title is renamed to S###-YYYY-MM-DD-<summary> after rebuild) — to clear and rebuild this conversation: the current copy is discarded, identity continues from SESSION.md. If you are in the middle of an unbreakable step, continue it, then rebuild at its end. Do not ignore this; rebuild is the expected action, not an option.`;
|
|
4753
5121
|
}
|
|
4754
5122
|
/** 读取会话 contextPressure 投影(sessionProjections 可选服务;未装配返回 null) */
|
|
4755
5123
|
function readContextPressure(ctx, session) {
|
|
@@ -4810,17 +5178,17 @@ function registerKeeper(ctx, opts = {}) {
|
|
|
4810
5178
|
const session = exec.agent?.session;
|
|
4811
5179
|
if (session) {
|
|
4812
5180
|
const pressure = readContextPressure(ctx, session);
|
|
4813
|
-
if (pressure && pressure.
|
|
4814
|
-
const
|
|
4815
|
-
const
|
|
4816
|
-
if (
|
|
5181
|
+
if (pressure && pressure.projectedTokens > 0) {
|
|
5182
|
+
const tokensK = pressure.projectedTokens / 1e3;
|
|
5183
|
+
const thresholdK = readSimpleSettings().rebuildThresholdK;
|
|
5184
|
+
if (pressure.projectedTokens >= thresholdK * 1e3) {
|
|
4817
5185
|
const key = exec.agent?.session?.id ?? "global";
|
|
4818
5186
|
const st = rebuildReminderStates.get(key) ?? { consecutive: 0 };
|
|
4819
5187
|
st.consecutive += 1;
|
|
4820
5188
|
const escalated = st.consecutive >= REBUILD_ESCALATE_AFTER;
|
|
4821
5189
|
blocks.push({
|
|
4822
5190
|
type: "text",
|
|
4823
|
-
text: rebuildReminderText(
|
|
5191
|
+
text: rebuildReminderText(tokensK, thresholdK, escalated)
|
|
4824
5192
|
});
|
|
4825
5193
|
rebuildReminderStates.set(key, st);
|
|
4826
5194
|
}
|
|
@@ -5001,10 +5369,6 @@ function defaultAdvancedSettings() {
|
|
|
5001
5369
|
allowWorkspaceCreate: true,
|
|
5002
5370
|
totpEnabled: false
|
|
5003
5371
|
},
|
|
5004
|
-
rebuild: {
|
|
5005
|
-
enabled: true,
|
|
5006
|
-
thresholdRatio: .9
|
|
5007
|
-
},
|
|
5008
5372
|
persona: {
|
|
5009
5373
|
mode: "",
|
|
5010
5374
|
overrideText: ""
|
|
@@ -5060,7 +5424,6 @@ function mergeWithDefaults(raw) {
|
|
|
5060
5424
|
if (raw === null || typeof raw !== "object") return def;
|
|
5061
5425
|
const o = raw;
|
|
5062
5426
|
const gateway = o.gateway ?? {};
|
|
5063
|
-
const rebuild = o.rebuild ?? {};
|
|
5064
5427
|
const persona = o.persona ?? {};
|
|
5065
5428
|
const publicAsk = o.publicAsk ?? {};
|
|
5066
5429
|
return {
|
|
@@ -5074,10 +5437,6 @@ function mergeWithDefaults(raw) {
|
|
|
5074
5437
|
allowWorkspaceCreate: typeof gateway.allowWorkspaceCreate === "boolean" ? gateway.allowWorkspaceCreate : def.gateway.allowWorkspaceCreate,
|
|
5075
5438
|
totpEnabled: typeof gateway.totpEnabled === "boolean" ? gateway.totpEnabled : def.gateway.totpEnabled
|
|
5076
5439
|
},
|
|
5077
|
-
rebuild: {
|
|
5078
|
-
enabled: typeof rebuild.enabled === "boolean" ? rebuild.enabled : def.rebuild.enabled,
|
|
5079
|
-
thresholdRatio: typeof rebuild.thresholdRatio === "number" ? rebuild.thresholdRatio : def.rebuild.thresholdRatio
|
|
5080
|
-
},
|
|
5081
5440
|
persona: {
|
|
5082
5441
|
mode: typeof persona.mode === "string" ? persona.mode : def.persona.mode,
|
|
5083
5442
|
overrideText: typeof persona.overrideText === "string" ? persona.overrideText : def.persona.overrideText
|
|
@@ -5103,7 +5462,6 @@ function writeAdvancedSettings(settings) {
|
|
|
5103
5462
|
function updateAdvancedSettings(patch) {
|
|
5104
5463
|
const current = readAdvancedSettings();
|
|
5105
5464
|
const gw = patch.gateway;
|
|
5106
|
-
const rb = patch.rebuild;
|
|
5107
5465
|
const ps = patch.persona;
|
|
5108
5466
|
const next = {
|
|
5109
5467
|
gateway: gw !== void 0 ? {
|
|
@@ -5116,10 +5474,6 @@ function updateAdvancedSettings(patch) {
|
|
|
5116
5474
|
allowWorkspaceCreate: typeof gw.allowWorkspaceCreate === "boolean" ? gw.allowWorkspaceCreate : current.gateway.allowWorkspaceCreate,
|
|
5117
5475
|
totpEnabled: typeof gw.totpEnabled === "boolean" ? gw.totpEnabled : current.gateway.totpEnabled
|
|
5118
5476
|
} : current.gateway,
|
|
5119
|
-
rebuild: rb !== void 0 ? {
|
|
5120
|
-
enabled: typeof rb.enabled === "boolean" ? rb.enabled : current.rebuild.enabled,
|
|
5121
|
-
thresholdRatio: typeof rb.thresholdRatio === "number" && rb.thresholdRatio > 0 && rb.thresholdRatio <= 1 ? rb.thresholdRatio : current.rebuild.thresholdRatio
|
|
5122
|
-
} : current.rebuild,
|
|
5123
5477
|
persona: ps !== void 0 ? {
|
|
5124
5478
|
mode: typeof ps.mode === "string" ? ps.mode : current.persona.mode,
|
|
5125
5479
|
overrideText: typeof ps.overrideText === "string" ? ps.overrideText : current.persona.overrideText
|
|
@@ -5244,7 +5598,6 @@ function toWire(settings) {
|
|
|
5244
5598
|
allowWorkspaceCreate: settings.gateway.allowWorkspaceCreate,
|
|
5245
5599
|
totpEnabled: settings.gateway.totpEnabled
|
|
5246
5600
|
},
|
|
5247
|
-
rebuild: settings.rebuild,
|
|
5248
5601
|
persona: settings.persona,
|
|
5249
5602
|
publicAsk: { allowed: [...settings.publicAsk.allowed] }
|
|
5250
5603
|
};
|
|
@@ -5297,12 +5650,6 @@ function applyWirePatch(wire) {
|
|
|
5297
5650
|
}
|
|
5298
5651
|
patch.gateway = gwPatch;
|
|
5299
5652
|
}
|
|
5300
|
-
if (wire.rebuild !== void 0) {
|
|
5301
|
-
const rbPatch = { ...current.rebuild };
|
|
5302
|
-
if (typeof wire.rebuild.enabled === "boolean") rbPatch.enabled = wire.rebuild.enabled;
|
|
5303
|
-
if (typeof wire.rebuild.thresholdRatio === "number" && wire.rebuild.thresholdRatio > 0 && wire.rebuild.thresholdRatio <= 1) rbPatch.thresholdRatio = wire.rebuild.thresholdRatio;
|
|
5304
|
-
patch.rebuild = rbPatch;
|
|
5305
|
-
}
|
|
5306
5653
|
if (wire.persona !== void 0) {
|
|
5307
5654
|
const psPatch = { ...current.persona };
|
|
5308
5655
|
if (typeof wire.persona.mode === "string") psPatch.mode = wire.persona.mode;
|
|
@@ -5338,8 +5685,8 @@ const HIDDEN_LINES = /安全模式|safe-mode|\.serenity-safe-on/;
|
|
|
5338
5685
|
function sanitizeSkillContent(content) {
|
|
5339
5686
|
return content.split("\n").filter((line) => !HIDDEN_LINES.test(line)).join("\n");
|
|
5340
5687
|
}
|
|
5341
|
-
/** 1) ACC
|
|
5342
|
-
function
|
|
5688
|
+
/** 1) 身份块:ACC 身份 + CCC 名 + 平台工具说明(需求③ S142:工具清单移出为独立 toolsBlock 放装配末尾——身份先行、工具参考殿后) */
|
|
5689
|
+
function identityBlock(root) {
|
|
5343
5690
|
const cccName = basename(root);
|
|
5344
5691
|
return [
|
|
5345
5692
|
"",
|
|
@@ -5349,26 +5696,53 @@ function accBlock(root) {
|
|
|
5349
5696
|
"",
|
|
5350
5697
|
"You are running inside a Concrete Cognitive Container (CCC) —",
|
|
5351
5698
|
"the runtime instance of an Abstract Cognitive Container (ACC).",
|
|
5699
|
+
"The ACC (this plugin) is a cognitive container harness: it provides",
|
|
5700
|
+
"deterministic tools, mechanical constraints, and session continuity.",
|
|
5701
|
+
"The complete built-in tool list is at the end of this prompt under the",
|
|
5702
|
+
"\"Serenity Tools\" heading — read it before using any ACC tool.",
|
|
5703
|
+
"",
|
|
5704
|
+
" ℹ️ Use relative paths from the CCC root for CCC-internal file operations (read/write/edit/glob/grep etc.), e.g. AGENT_SESSIONS/2026-08-14--S134--x/SESSION.md; Root / absolute SESSION.md paths are identifiers only, not tool arguments",
|
|
5705
|
+
"",
|
|
5706
|
+
"The DSH platform tools remain available too (read/write/edit/glob/grep/web_search/ask_user_question/subagent/workflow/goal and more) — the ACC tools are the serenity-native layer, not the only tools.",
|
|
5707
|
+
"",
|
|
5708
|
+
"Additional MSMs registered by this CCC are available — call acc_msm list to discover them.",
|
|
5709
|
+
""
|
|
5710
|
+
].join("\n");
|
|
5711
|
+
}
|
|
5712
|
+
/**
|
|
5713
|
+
* 工具清单块(需求③ S142 用户拍板:工具列表移装配末尾 + MSM 调用示例):
|
|
5714
|
+
* 原 accBlock 内嵌 13 行工具清单 → 独立成块放装配末尾(SKILL 后、Session 前)。
|
|
5715
|
+
* 身份先行(认知轨迹开头不被 13 行清单干扰)、工具参考殿后(需要时再看)。
|
|
5716
|
+
* 附带 MSM 调用示例(用户拍板:顶层提示词加调用方式说明,避免偶发调用错误——
|
|
5717
|
+
* 模型对 acc_msm 参数面/协议 flag 理解不稳)。
|
|
5718
|
+
*/
|
|
5719
|
+
function toolsBlock() {
|
|
5720
|
+
return [
|
|
5721
|
+
"",
|
|
5722
|
+
"=== Serenity Tools ===",
|
|
5352
5723
|
"The ACC (this plugin) provides the following built-in tools:",
|
|
5353
5724
|
"",
|
|
5354
5725
|
" cc_fs — CCC filesystem operations (root/resolve/exists/list/tree/relative/mkdir/rm/mv/cp/touch/append/reveal/info/find)",
|
|
5355
|
-
" session — session lifecycle (list/show/create/health/qa/archive/summary)",
|
|
5356
|
-
" acc_kit — ACC utility kit (health: CCC three principles / time: now / wait: wait N seconds)",
|
|
5726
|
+
" session — session lifecycle (list/show/create/use/close/health/qa/archive/summary/hook-develop-guide)",
|
|
5727
|
+
" acc_kit — ACC utility kit (health: CCC three principles + MSM registry integrity report / time: now / wait: wait N seconds)",
|
|
5357
5728
|
" cc_git — git operations (status/commit/push/log)",
|
|
5358
|
-
" acc_msm — MSM framework (list/exec/register/deregister/check/guide)",
|
|
5729
|
+
" acc_msm — MSM framework (list/exec/register/deregister/check/guide/catalog/ccc-config)",
|
|
5359
5730
|
" eap — return the full EAP cognitive quality framework",
|
|
5360
5731
|
" neat — return the full Neat design collaboration protocol",
|
|
5361
5732
|
" cce — return the full Cognitive Continuity Engineering framework",
|
|
5362
|
-
" handyman — delegate a do-everything worker agent (CCC-whitelisted model) to run synchronously in rounds until done
|
|
5733
|
+
" handyman — delegate a do-everything worker agent (CCC-whitelisted model) to run synchronously in rounds until done; jobs=[] orchestrates parallel work",
|
|
5363
5734
|
" session_rebuild — rebuild this conversation in place from SESSION.md when the trajectory-tracker trips",
|
|
5364
5735
|
" localstore — ACC local credential/config storage (CCC-root localstore.json, JSON format; git policy localstore.gitTrack default deny); doc subcommand outputs the spec",
|
|
5365
|
-
" skiff_admin — Skiff (F4, experimental): CCC cognitive-subset roles — guide (definition tutorial) / validate (config check) / list (role summary)",
|
|
5736
|
+
" skiff_admin — Skiff (F4, experimental): CCC cognitive-subset roles — guide (definition tutorial) / validate (config check) / apply (validate + confirm live) / list (role summary)",
|
|
5737
|
+
" autopilot-trajectory — Autopilot Trajectory one-stop management (all/init/random/diag/doc/check/status/guide)",
|
|
5366
5738
|
"",
|
|
5367
|
-
"
|
|
5368
|
-
"",
|
|
5369
|
-
"The DSH platform tools remain available too (read/write/edit/glob/grep/web_search/ask_user_question/subagent/workflow/goal and more) — the ACC tools above are the serenity-native layer, not the only tools.",
|
|
5739
|
+
"First-time in a CCC? Run acc_msm catalog — ACC usage directory (capability areas + where each guide lives).",
|
|
5370
5740
|
"",
|
|
5371
|
-
"
|
|
5741
|
+
"MSM call protocol (registered CCC MSMs, deterministic Mech & Semi-Mech):",
|
|
5742
|
+
" 1. Discover: acc_msm list — list all registered MSMs",
|
|
5743
|
+
" 2. Inspect usage: acc_msm exec <name> --schema 1 — print one MSM's usage/flags (protocol flag, no execution)",
|
|
5744
|
+
" 3. Execute: acc_msm exec <name> <args...> — run the MSM with business args (first arg may be --list/--schema/--format=json)",
|
|
5745
|
+
" Register new MSMs with acc_msm register; deregister with acc_msm deregister (registry is ACC-managed — never edit mech-registry.json directly).",
|
|
5372
5746
|
""
|
|
5373
5747
|
].join("\n");
|
|
5374
5748
|
}
|
|
@@ -5687,8 +6061,11 @@ function sessionBlock(root, scope = DEFAULT_SESSION_SCOPE) {
|
|
|
5687
6061
|
/**
|
|
5688
6062
|
* 完整系统提示词注入文本(v1.19.8 结构精简,S142 重建视角 R↓):
|
|
5689
6063
|
* 身份(ACC)→ 世界模型(Metaphor)→ 信念/边界(Principles)→ 时间约束(CCE)
|
|
5690
|
-
* → 质量(EAP)→ 状态(SafeMode/Localstore)→ CCC 上下文(SKILL)→ 会话(Session)。
|
|
5691
|
-
* 认知展开顺序:我是谁 → 我所在的世界 → 为什么 → 如何一致 → 产物标准 → 当前状态 →
|
|
6064
|
+
* → 质量(EAP)→ 状态(SafeMode/Localstore)→ CCC 上下文(SKILL)→ 工具参考(Tools)→ 会话(Session)。
|
|
6065
|
+
* 认知展开顺序:我是谁 → 我所在的世界 → 为什么 → 如何一致 → 产物标准 → 当前状态 → 上下文 → 工具参考 → 会话。
|
|
6066
|
+
*
|
|
6067
|
+
* 需求③(S142 用户拍板):工具清单从 accBlock 移出为独立 toolsBlock,
|
|
6068
|
+
* 放 SKILL 后 Session 前(工具参考殿后,身份先行不被 13 行清单干扰)。
|
|
5692
6069
|
*
|
|
5693
6070
|
* v1.23.1 persona(彩蛋):persona.mode 配置 → EAP 块替换为 Persona 块(输出约束),
|
|
5694
6071
|
* Principles 剥离 MSM 原则段(指令遵循约束)——用户文本承接两处风格;本体论/关系段/
|
|
@@ -5698,7 +6075,7 @@ function serenitySystemPrompt(root, scope = DEFAULT_SESSION_SCOPE) {
|
|
|
5698
6075
|
const persona = readPersonaSettings();
|
|
5699
6076
|
const personaOn = persona.mode !== "" && persona.overrideText.trim() !== "";
|
|
5700
6077
|
const parts = [
|
|
5701
|
-
|
|
6078
|
+
identityBlock(root),
|
|
5702
6079
|
metaphorBlock(),
|
|
5703
6080
|
principlesBlock(root, personaOn),
|
|
5704
6081
|
cceBlock(),
|
|
@@ -5708,6 +6085,7 @@ function serenitySystemPrompt(root, scope = DEFAULT_SESSION_SCOPE) {
|
|
|
5708
6085
|
if (state) parts.push(state);
|
|
5709
6086
|
const skill = entrySkillSectionText(root);
|
|
5710
6087
|
if (skill) parts.push(skill);
|
|
6088
|
+
parts.push(toolsBlock());
|
|
5711
6089
|
const session = sessionBlock(root, scope);
|
|
5712
6090
|
if (session) parts.push(session);
|
|
5713
6091
|
return parts.join("\n\n");
|
|
@@ -6290,7 +6668,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6290
6668
|
workspace: url.searchParams.get("workspace") ?? void 0
|
|
6291
6669
|
});
|
|
6292
6670
|
const root = findSerenityRoot(workspace) ?? "";
|
|
6293
|
-
const { discoverCccs } = await import("./skiff-debug-
|
|
6671
|
+
const { discoverCccs } = await import("./skiff-debug-DIf_Uenr.js").then((n) => n.i);
|
|
6294
6672
|
sendJson$1(res, 200, { cccs: await discoverCccs(ctx, root) });
|
|
6295
6673
|
} catch (err) {
|
|
6296
6674
|
sendJson$1(res, 400, { error: err.message ?? String(err) });
|
|
@@ -6320,7 +6698,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6320
6698
|
return;
|
|
6321
6699
|
}
|
|
6322
6700
|
const settings = readAdvancedSettings();
|
|
6323
|
-
const { readSimpleSettings } = await import("./settings-section-
|
|
6701
|
+
const { readSimpleSettings } = await import("./settings-section-DnX9TJxt.js").then((n) => n.r);
|
|
6324
6702
|
const simple = readSimpleSettings();
|
|
6325
6703
|
const allowed = settings.publicAsk.allowed;
|
|
6326
6704
|
const port = simple.acpHttpPort ?? 3100;
|
|
@@ -6416,11 +6794,11 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6416
6794
|
sendJson$1(res, 400, { error: chk.error });
|
|
6417
6795
|
return;
|
|
6418
6796
|
}
|
|
6419
|
-
const { readWeixinSettings } = await import("./weixin-route-
|
|
6797
|
+
const { readWeixinSettings } = await import("./weixin-route-DFkRf9ou.js").then((n) => n.u);
|
|
6420
6798
|
const { weixinBridgeStatus } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
6421
6799
|
const settings = readWeixinSettings(chk.root);
|
|
6422
6800
|
const bridge = weixinBridgeStatus().find((b) => b.ccc === chk.root);
|
|
6423
|
-
const { readWeixinCredential } = await import("./weixin-route-
|
|
6801
|
+
const { readWeixinCredential } = await import("./weixin-route-DFkRf9ou.js").then((n) => n.u);
|
|
6424
6802
|
const accounts = (settings.accounts ?? []).map((a) => ({
|
|
6425
6803
|
accountId: a.accountId,
|
|
6426
6804
|
name: a.name ?? void 0,
|
|
@@ -6449,10 +6827,10 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6449
6827
|
}
|
|
6450
6828
|
const root = chk.root;
|
|
6451
6829
|
if (body.action === "login-start") {
|
|
6452
|
-
const { fetchQRCode } = await import("./weixin-api-
|
|
6830
|
+
const { fetchQRCode } = await import("./weixin-api-OUCuw0Ws.js").then((n) => n.l);
|
|
6453
6831
|
const { randomUUID } = await import("node:crypto");
|
|
6454
6832
|
for (const [key, v] of weixinLogins) if (Date.now() - v.startedAt > WEIXIN_LOGIN_TTL_MS) weixinLogins.delete(key);
|
|
6455
|
-
const { readWeixinSettings } = await import("./weixin-route-
|
|
6833
|
+
const { readWeixinSettings } = await import("./weixin-route-DFkRf9ou.js").then((n) => n.u);
|
|
6456
6834
|
const qr = await fetchQRCode({ botType: readWeixinSettings(root).botType ?? void 0 });
|
|
6457
6835
|
const loginKey = randomUUID();
|
|
6458
6836
|
weixinLogins.set(loginKey, {
|
|
@@ -6473,7 +6851,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6473
6851
|
sendJson$1(res, 400, { error: "missing accountId" });
|
|
6474
6852
|
return;
|
|
6475
6853
|
}
|
|
6476
|
-
const { removeWeixinAccount } = await import("./weixin-route-
|
|
6854
|
+
const { removeWeixinAccount } = await import("./weixin-route-DFkRf9ou.js").then((n) => n.u);
|
|
6477
6855
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
6478
6856
|
removeWeixinAccount(root, body.accountId);
|
|
6479
6857
|
syncCccBridge(ctx, root);
|
|
@@ -6486,8 +6864,8 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6486
6864
|
sendJson$1(res, 400, { error: "invalid routes (expected [{user, role}, ...])" });
|
|
6487
6865
|
return;
|
|
6488
6866
|
}
|
|
6489
|
-
const { saveWeixinRoutes } = await import("./weixin-route-
|
|
6490
|
-
const { readSkiffRoles } = await import("./skiff-role-
|
|
6867
|
+
const { saveWeixinRoutes } = await import("./weixin-route-DFkRf9ou.js").then((n) => n.u);
|
|
6868
|
+
const { readSkiffRoles } = await import("./skiff-role-BTdBHyOQ.js").then((n) => n.c);
|
|
6491
6869
|
const roles = readSkiffRoles(root);
|
|
6492
6870
|
for (const r of routes) if (!roles.has(r.role)) {
|
|
6493
6871
|
sendJson$1(res, 400, { error: `unknown role: ${r.role} (not in ${root} skiff.roles)` });
|
|
@@ -6499,7 +6877,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6499
6877
|
}
|
|
6500
6878
|
if (body.action === "set-enabled") {
|
|
6501
6879
|
const enabled = body.enabled === true;
|
|
6502
|
-
const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-
|
|
6880
|
+
const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-DFkRf9ou.js").then((n) => n.u);
|
|
6503
6881
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
6504
6882
|
const settings = readWeixinSettings(root);
|
|
6505
6883
|
if (enabled && (settings.accounts ?? []).length === 0) {
|
|
@@ -6545,8 +6923,8 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6545
6923
|
sendJson$1(res, 200, { status: "expired" });
|
|
6546
6924
|
return;
|
|
6547
6925
|
}
|
|
6548
|
-
const { pollQRStatus } = await import("./weixin-api-
|
|
6549
|
-
const { readWeixinSettings } = await import("./weixin-route-
|
|
6926
|
+
const { pollQRStatus } = await import("./weixin-api-OUCuw0Ws.js").then((n) => n.l);
|
|
6927
|
+
const { readWeixinSettings } = await import("./weixin-route-DFkRf9ou.js").then((n) => n.u);
|
|
6550
6928
|
const settings = readWeixinSettings(login.root);
|
|
6551
6929
|
const status = await pollQRStatus({
|
|
6552
6930
|
baseUrl: void 0,
|
|
@@ -6561,7 +6939,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6561
6939
|
});
|
|
6562
6940
|
return;
|
|
6563
6941
|
}
|
|
6564
|
-
const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-
|
|
6942
|
+
const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-DFkRf9ou.js").then((n) => n.u);
|
|
6565
6943
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
6566
6944
|
const accountId = nextWeixinAccountId(settings);
|
|
6567
6945
|
upsertWeixinAccount(login.root, {
|
|
@@ -8488,6 +8866,163 @@ qInput.focus()
|
|
|
8488
8866
|
</html>`;
|
|
8489
8867
|
}
|
|
8490
8868
|
//#endregion
|
|
8869
|
+
//#region src/weixin-hook.ts
|
|
8870
|
+
/**
|
|
8871
|
+
* weixin-hook.ts — 微信桥消息记录 hook(F4c-3 扩展,v1.27.13 用户需求)
|
|
8872
|
+
*
|
|
8873
|
+
* 需求:微信桥发生的所有消息记录,支持 hook——允许 CCC 通过自行编写 hook
|
|
8874
|
+
* 脚本进行**持久性保存**(存哪/存成什么/是否入库,全归 CCC 决定——ACC 不绑定存储)。
|
|
8875
|
+
*
|
|
8876
|
+
* 设计(docs/weixin-message-hook-design.md v0.1,三项拍板 H1~H6):
|
|
8877
|
+
* - **双向触发**(H1):incoming(用户→bot,媒体落盘后)+ outgoing(bot→用户,发送成功后)
|
|
8878
|
+
* - **脚本 + stdin JSON**(H2):serenity.json weixin.hook = CCC 根相对脚本路径;
|
|
8879
|
+
* 每次消息事件 spawn 一次,事件 JSON 单行经 stdin 传入(bun 优先 / node 兜底,biasProvider 先例)
|
|
8880
|
+
* - **旁路容忍**(H3):异步 fire-and-forget + 超时 kill + 失败仅日志——微信桥可靠性不受影响
|
|
8881
|
+
* - **不含会话凭据**(H5):事件只有记录所需字段(无 context_token / bot_token——最小暴露面)
|
|
8882
|
+
*
|
|
8883
|
+
* 定位:Mech 纯逻辑(事件构造纯函数 + 执行器确定性),可独立单测。
|
|
8884
|
+
*/
|
|
8885
|
+
/** hook 执行超时(旁路记录足够;超时 kill 防挂死脚本) */
|
|
8886
|
+
const WEIXIN_HOOK_TIMEOUT_MS = 15e3;
|
|
8887
|
+
/** hook stderr/stdout 日志截断(防脚本刷屏) */
|
|
8888
|
+
const HOOK_LOG_MAX = 500;
|
|
8889
|
+
/** 构造 incoming 事件对象(纯函数,可测) */
|
|
8890
|
+
function buildIncomingHookEvent(input) {
|
|
8891
|
+
return {
|
|
8892
|
+
event: "incoming",
|
|
8893
|
+
ts: Date.now(),
|
|
8894
|
+
cccRoot: input.cccRoot,
|
|
8895
|
+
accountId: input.accountId,
|
|
8896
|
+
userId: input.userId,
|
|
8897
|
+
sessionId: input.sessionId,
|
|
8898
|
+
role: input.role,
|
|
8899
|
+
message: {
|
|
8900
|
+
text: input.text,
|
|
8901
|
+
media: input.media
|
|
8902
|
+
}
|
|
8903
|
+
};
|
|
8904
|
+
}
|
|
8905
|
+
/** 构造 outgoing 事件对象(纯函数,可测) */
|
|
8906
|
+
function buildOutgoingHookEvent(input) {
|
|
8907
|
+
return {
|
|
8908
|
+
event: "outgoing",
|
|
8909
|
+
ts: Date.now(),
|
|
8910
|
+
cccRoot: input.cccRoot,
|
|
8911
|
+
accountId: input.accountId,
|
|
8912
|
+
userId: input.userId,
|
|
8913
|
+
sessionId: input.sessionId,
|
|
8914
|
+
role: input.role,
|
|
8915
|
+
reply: input.reply
|
|
8916
|
+
};
|
|
8917
|
+
}
|
|
8918
|
+
/**
|
|
8919
|
+
* 执行一次 hook:spawn CCC 根脚本,事件 JSON 单行经 stdin 传入。
|
|
8920
|
+
* 旁路容忍(H3):超时 kill / 非 0 退出 / spawn 失败 → 仅日志返回 {ok:false},绝不抛(不阻断微信桥)。
|
|
8921
|
+
* 路径逃逸校验:脚本必须解析在 CCC 根内(resolveInside——resolveInside 抛错也吞为 {ok:false})。
|
|
8922
|
+
* 脚本缺失 → {ok:false}(配置了但未实现——日志提示;不视为异常)。
|
|
8923
|
+
*/
|
|
8924
|
+
async function runWeixinHook(root, hookRel, event, timeoutMs = WEIXIN_HOOK_TIMEOUT_MS) {
|
|
8925
|
+
let scriptAbs;
|
|
8926
|
+
try {
|
|
8927
|
+
scriptAbs = resolveInside(root, hookRel);
|
|
8928
|
+
} catch {
|
|
8929
|
+
return {
|
|
8930
|
+
ok: false,
|
|
8931
|
+
detail: `weixin hook 路径逃逸(须在 CCC 根内): ${hookRel}`
|
|
8932
|
+
};
|
|
8933
|
+
}
|
|
8934
|
+
if (!existsSync(scriptAbs)) {
|
|
8935
|
+
console.log(`[serenity-hooks] weixin hook 脚本缺失(配置了 weixin.hook 但文件不存在): ${hookRel}`);
|
|
8936
|
+
return {
|
|
8937
|
+
ok: false,
|
|
8938
|
+
detail: `hook 脚本不存在: ${hookRel}`
|
|
8939
|
+
};
|
|
8940
|
+
}
|
|
8941
|
+
const json = JSON.stringify(event);
|
|
8942
|
+
const runners = [["bun", [scriptAbs]], [process.execPath, [scriptAbs]]];
|
|
8943
|
+
for (const [cmd, args] of runners) try {
|
|
8944
|
+
const res = await runOnce(cmd, args, json, timeoutMs);
|
|
8945
|
+
if (!res.ok && res.code === "ENOENT") continue;
|
|
8946
|
+
return res;
|
|
8947
|
+
} catch {
|
|
8948
|
+
continue;
|
|
8949
|
+
}
|
|
8950
|
+
return {
|
|
8951
|
+
ok: false,
|
|
8952
|
+
detail: "hook 执行失败(bun 与 node 均不可用)"
|
|
8953
|
+
};
|
|
8954
|
+
}
|
|
8955
|
+
/** 单次 spawn 执行(Promise 化 + 超时 kill + 输出截断) */
|
|
8956
|
+
function runOnce(cmd, args, json, timeoutMs) {
|
|
8957
|
+
return new Promise((resolvePromise) => {
|
|
8958
|
+
let settled = false;
|
|
8959
|
+
let stdout = "";
|
|
8960
|
+
let stderr = "";
|
|
8961
|
+
let child = null;
|
|
8962
|
+
try {
|
|
8963
|
+
child = spawn(cmd, args, { stdio: [
|
|
8964
|
+
"pipe",
|
|
8965
|
+
"pipe",
|
|
8966
|
+
"pipe"
|
|
8967
|
+
] });
|
|
8968
|
+
} catch (err) {
|
|
8969
|
+
return resolvePromise({
|
|
8970
|
+
ok: false,
|
|
8971
|
+
code: "SPAWN_ERR",
|
|
8972
|
+
detail: String(err)
|
|
8973
|
+
});
|
|
8974
|
+
}
|
|
8975
|
+
const timer = setTimeout(() => {
|
|
8976
|
+
if (settled) return;
|
|
8977
|
+
settled = true;
|
|
8978
|
+
child?.kill("SIGKILL");
|
|
8979
|
+
resolvePromise({
|
|
8980
|
+
ok: false,
|
|
8981
|
+
detail: `hook 执行超时(>${Math.round(timeoutMs / 1e3)}s,已 kill)`
|
|
8982
|
+
});
|
|
8983
|
+
}, timeoutMs);
|
|
8984
|
+
child.stdin?.on("error", () => {});
|
|
8985
|
+
child.stdin?.write(json);
|
|
8986
|
+
child.stdin?.end();
|
|
8987
|
+
child.stdout?.on("data", (d) => {
|
|
8988
|
+
stdout = (stdout + d.toString()).slice(0, HOOK_LOG_MAX);
|
|
8989
|
+
});
|
|
8990
|
+
child.stderr?.on("data", (d) => {
|
|
8991
|
+
stderr = (stderr + d.toString()).slice(0, HOOK_LOG_MAX);
|
|
8992
|
+
});
|
|
8993
|
+
child.on("error", (err) => {
|
|
8994
|
+
if (settled) return;
|
|
8995
|
+
settled = true;
|
|
8996
|
+
clearTimeout(timer);
|
|
8997
|
+
resolvePromise({
|
|
8998
|
+
ok: false,
|
|
8999
|
+
code: err.code,
|
|
9000
|
+
detail: String(err.message ?? err)
|
|
9001
|
+
});
|
|
9002
|
+
});
|
|
9003
|
+
child.on("close", (code) => {
|
|
9004
|
+
if (settled) return;
|
|
9005
|
+
settled = true;
|
|
9006
|
+
clearTimeout(timer);
|
|
9007
|
+
if (code === 0) {
|
|
9008
|
+
if (stderr.trim() !== "") console.log(`[serenity-hooks] weixin hook stderr: ${stderr.trim()}`);
|
|
9009
|
+
resolvePromise({ ok: true });
|
|
9010
|
+
} else {
|
|
9011
|
+
console.log(`[serenity-hooks] weixin hook exit=${code}${stderr ? ` stderr: ${stderr.trim()}` : ""}`);
|
|
9012
|
+
resolvePromise({
|
|
9013
|
+
ok: false,
|
|
9014
|
+
detail: `hook exit=${code}`
|
|
9015
|
+
});
|
|
9016
|
+
}
|
|
9017
|
+
});
|
|
9018
|
+
});
|
|
9019
|
+
}
|
|
9020
|
+
let activeRunner = runWeixinHook;
|
|
9021
|
+
/** bridge 调用入口(转发 activeRunner——默认真实 spawn;测试可注入捕获) */
|
|
9022
|
+
function invokeWeixinHook(root, hookRel, event) {
|
|
9023
|
+
return activeRunner(root, hookRel, event);
|
|
9024
|
+
}
|
|
9025
|
+
//#endregion
|
|
8491
9026
|
//#region src/weixin-bridge.ts
|
|
8492
9027
|
var weixin_bridge_exports = /* @__PURE__ */ __exportAll({
|
|
8493
9028
|
handleIncoming: () => handleIncoming,
|
|
@@ -8635,6 +9170,7 @@ async function handleIncoming(ctx, root, accountId, cred, msg) {
|
|
|
8635
9170
|
try {
|
|
8636
9171
|
const mediaNotes = [];
|
|
8637
9172
|
const degradedNotes = [];
|
|
9173
|
+
const hookMedia = [];
|
|
8638
9174
|
for (const mediaRef of mediaRefs) {
|
|
8639
9175
|
const mediaType = mediaRef.kind === "image" ? "image_item" : "file_item";
|
|
8640
9176
|
const label = mediaRef.kind === "image" ? "一张图片" : `文件 ${mediaRef.fileName ?? "(未命名)"}`;
|
|
@@ -8643,10 +9179,18 @@ async function handleIncoming(ctx, root, accountId, cred, msg) {
|
|
|
8643
9179
|
mediaType
|
|
8644
9180
|
});
|
|
8645
9181
|
if (!result) {
|
|
9182
|
+
hookMedia.push({
|
|
9183
|
+
kind: mediaRef.kind,
|
|
9184
|
+
relPath: null
|
|
9185
|
+
});
|
|
8646
9186
|
degradedNotes.push(`(用户发送了${label},但下载失败——可请用户重发)`);
|
|
8647
9187
|
continue;
|
|
8648
9188
|
}
|
|
8649
9189
|
if (result.data.length > MEDIA_MAX_BYTES) {
|
|
9190
|
+
hookMedia.push({
|
|
9191
|
+
kind: mediaRef.kind,
|
|
9192
|
+
relPath: null
|
|
9193
|
+
});
|
|
8650
9194
|
degradedNotes.push(`(用户发送了${label},但超过 20MB 大小限制)`);
|
|
8651
9195
|
continue;
|
|
8652
9196
|
}
|
|
@@ -8657,8 +9201,16 @@ async function handleIncoming(ctx, root, accountId, cred, msg) {
|
|
|
8657
9201
|
const abs = join(inboundDir, fname);
|
|
8658
9202
|
writeFileSync(abs, result.data);
|
|
8659
9203
|
const rel = relative(root, abs);
|
|
9204
|
+
hookMedia.push({
|
|
9205
|
+
kind: mediaRef.kind,
|
|
9206
|
+
relPath: rel
|
|
9207
|
+
});
|
|
8660
9208
|
mediaNotes.push(`(用户发送了${mediaRef.kind === "image" ? "一张图片" : `文件 ${fname}`},已保存到 ${rel})`);
|
|
8661
9209
|
} catch {
|
|
9210
|
+
hookMedia.push({
|
|
9211
|
+
kind: mediaRef.kind,
|
|
9212
|
+
relPath: null
|
|
9213
|
+
});
|
|
8662
9214
|
degradedNotes.push(`(媒体保存失败,请重试)`);
|
|
8663
9215
|
}
|
|
8664
9216
|
}
|
|
@@ -8666,15 +9218,38 @@ async function handleIncoming(ctx, root, accountId, cred, msg) {
|
|
|
8666
9218
|
if (text) parts.push(text);
|
|
8667
9219
|
parts.push(...mediaNotes, ...degradedNotes);
|
|
8668
9220
|
const question = parts.join("\n");
|
|
9221
|
+
const hookRel = settings.hook;
|
|
9222
|
+
if (hookRel) invokeWeixinHook(root, hookRel, buildIncomingHookEvent({
|
|
9223
|
+
cccRoot: root,
|
|
9224
|
+
accountId,
|
|
9225
|
+
userId: fromUserId,
|
|
9226
|
+
sessionId,
|
|
9227
|
+
role: roleName,
|
|
9228
|
+
text,
|
|
9229
|
+
media: hookMedia
|
|
9230
|
+
})).catch((err) => {
|
|
9231
|
+
console.log(`[serenity-hooks] weixin hook incoming error: ${err instanceof Error ? err.message : String(err)}`);
|
|
9232
|
+
});
|
|
8669
9233
|
const answer = (await askSkiff(ctx, ref.agent, question, void 0, { includeTrajectory: false })).answer ?? "";
|
|
8670
9234
|
if (answer === "") return;
|
|
9235
|
+
const reply = markdownToPlainText(stripThink(answer));
|
|
8671
9236
|
await sendTextMessage({
|
|
8672
9237
|
baseUrl: cred.baseUrl,
|
|
8673
9238
|
token: cred.token,
|
|
8674
9239
|
toUserId: fromUserId,
|
|
8675
|
-
text:
|
|
9240
|
+
text: reply,
|
|
8676
9241
|
contextToken: msg.context_token
|
|
8677
9242
|
});
|
|
9243
|
+
if (hookRel) invokeWeixinHook(root, hookRel, buildOutgoingHookEvent({
|
|
9244
|
+
cccRoot: root,
|
|
9245
|
+
accountId,
|
|
9246
|
+
userId: fromUserId,
|
|
9247
|
+
sessionId,
|
|
9248
|
+
role: roleName,
|
|
9249
|
+
reply
|
|
9250
|
+
})).catch((err) => {
|
|
9251
|
+
console.log(`[serenity-hooks] weixin hook outgoing error: ${err instanceof Error ? err.message : String(err)}`);
|
|
9252
|
+
});
|
|
8678
9253
|
} finally {
|
|
8679
9254
|
await sendTypingStop(cred, accountId, fromUserId);
|
|
8680
9255
|
}
|
|
@@ -8784,7 +9359,7 @@ const Config = z.object({
|
|
|
8784
9359
|
gateway: z.object({ enabled: z.boolean().default(false) }),
|
|
8785
9360
|
rebuild: z.object({
|
|
8786
9361
|
enabled: z.boolean().default(true),
|
|
8787
|
-
|
|
9362
|
+
thresholdK: z.number().min(50).max(4e3).default(400)
|
|
8788
9363
|
}),
|
|
8789
9364
|
skiff: z.object({
|
|
8790
9365
|
enabled: z.boolean().default(false),
|