@shgroup/dsh-serenity-hooks 1.26.14 → 1.26.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dsh.plugin.json +1 -1
- package/lib/acp-core.d.ts +100 -0
- package/lib/acp-http.d.ts +29 -0
- package/lib/api.d.ts +47 -0
- package/lib/autotrajectory.d.ts +186 -0
- package/lib/ccc-CfDrlfA7.js +141 -0
- package/lib/ccc.d.ts +154 -0
- package/lib/config-ops.d.ts +166 -0
- package/lib/constants.d.ts +8 -0
- package/lib/fs-ops.d.ts +36 -0
- package/lib/gateway-auth.d.ts +89 -0
- package/lib/gateway-proxy.d.ts +39 -0
- package/lib/gateway.d.ts +80 -0
- package/lib/git-ops.d.ts +28 -0
- package/lib/handyman-ops.d.ts +68 -0
- package/lib/handyman-preset-inherit.d.ts +34 -0
- package/lib/index.d.ts +68 -0
- package/lib/index.js +2172 -1992
- package/lib/invariant.d.ts +29 -0
- package/lib/json.d.ts +9 -0
- package/lib/kit-ops.d.ts +23 -0
- package/lib/localstore-ops.d.ts +85 -0
- package/lib/msm-ops.d.ts +78 -0
- package/lib/output-guard-seam.d.ts +21 -0
- package/lib/output-guard.d.ts +68 -0
- package/lib/rebuild.d.ts +118 -0
- package/lib/seams/bootstrap.d.ts +83 -0
- package/lib/seams/compact.d.ts +31 -0
- package/lib/seams/context.d.ts +48 -0
- package/lib/seams/env.d.ts +16 -0
- package/lib/seams/guards.d.ts +72 -0
- package/lib/seams/keeper.d.ts +61 -0
- package/lib/seams/opencode-skills.d.ts +14 -0
- package/lib/seams/system-prompt.d.ts +113 -0
- package/lib/session-ops.d.ts +127 -0
- package/lib/settings-section.d.ts +113 -0
- package/lib/skiff-core.d.ts +78 -0
- package/lib/skiff-debug.d.ts +86 -0
- package/lib/skiff-registry.d.ts +25 -0
- package/lib/skiff-role.d.ts +50 -0
- package/lib/skills/opencode-scan.d.ts +27 -0
- package/lib/skills-discovery.d.ts +25 -0
- package/lib/status.d.ts +38 -0
- package/lib/tools/autotrajectory-exp.d.ts +24 -0
- package/lib/tools/cc-fs.d.ts +7 -0
- package/lib/tools/cce.d.ts +9 -0
- package/lib/tools/eap.d.ts +7 -0
- package/lib/tools/git.d.ts +4 -0
- package/lib/tools/handyman.d.ts +43 -0
- package/lib/tools/kit.d.ts +4 -0
- package/lib/tools/localstore.d.ts +8 -0
- package/lib/tools/msm.d.ts +4 -0
- package/lib/tools/neat.d.ts +5 -0
- package/lib/tools/rebuild.d.ts +19 -0
- package/lib/tools/session.d.ts +78 -0
- package/lib/tools/skiff-admin.d.ts +25 -0
- package/lib/totp.d.ts +40 -0
- package/package.json +6 -4
package/lib/index.js
CHANGED
|
@@ -6949,1598 +6949,2098 @@ const skiffAdminTool = defineTool({
|
|
|
6949
6949
|
}
|
|
6950
6950
|
});
|
|
6951
6951
|
//#endregion
|
|
6952
|
-
//#region src/
|
|
6952
|
+
//#region src/autotrajectory.ts
|
|
6953
|
+
var autotrajectory_exports = /* @__PURE__ */ __exportAll({
|
|
6954
|
+
AUTO_DIR_SUFFIX: () => AUTO_DIR_SUFFIX,
|
|
6955
|
+
DEFAULT_AVOID_HOURS: () => DEFAULT_AVOID_HOURS,
|
|
6956
|
+
DEFAULT_BIAS_PROVIDER: () => DEFAULT_BIAS_PROVIDER,
|
|
6957
|
+
MOTIVATION_MARKER: () => MOTIVATION_MARKER,
|
|
6958
|
+
TICK_MS: () => TICK_MS,
|
|
6959
|
+
beijingHour: () => beijingHour,
|
|
6960
|
+
buildWakeMessage: () => buildWakeMessage,
|
|
6961
|
+
diagLive: () => diagLive,
|
|
6962
|
+
fetchBiasContent: () => fetchBiasContent,
|
|
6963
|
+
getAutoTrajectoryStatus: () => getAutoTrajectoryStatus,
|
|
6964
|
+
inAllowedWakeWindow: () => inAllowedWakeWindow,
|
|
6965
|
+
isAutoTrajectorySession: () => isAutoTrajectorySession,
|
|
6966
|
+
listLiveSessions: () => listLiveSessions,
|
|
6967
|
+
performAutoTrajectoryWake: () => performAutoTrajectoryWake,
|
|
6968
|
+
readAutoTrajectorySettings: () => readAutoTrajectorySettings,
|
|
6969
|
+
readSelfGeneratedMotivation: () => readSelfGeneratedMotivation,
|
|
6970
|
+
registerAutoTrajectory: () => registerAutoTrajectory,
|
|
6971
|
+
resolveAutoTrajectoryCcc: () => resolveAutoTrajectoryCcc,
|
|
6972
|
+
resolveTargetMd: () => resolveTargetMd,
|
|
6973
|
+
shouldWake: () => shouldWake
|
|
6974
|
+
});
|
|
6975
|
+
const PLUGIN_SOURCE$3 = {
|
|
6976
|
+
kind: "plugin",
|
|
6977
|
+
plugin: "dsh-serenity-hooks"
|
|
6978
|
+
};
|
|
6979
|
+
/** 偏见内容提供者脚本缺省名(CCC 根目录下;用户拍板:tool 直接运行,缺失报错要求实现) */
|
|
6980
|
+
const DEFAULT_BIAS_PROVIDER = "autotrajectory-bias.ts";
|
|
6981
|
+
/** 自主轨迹会话目录后缀标志(用户拍板:后缀) */
|
|
6982
|
+
const AUTO_DIR_SUFFIX = "--auto";
|
|
6983
|
+
/** 调度 tick 周期(10min——验证形态"一个就够") */
|
|
6984
|
+
const TICK_MS = 6e5;
|
|
6985
|
+
/** SESSION.md 内「下一轮动机」段标记(自生偏见载体——轨迹预设自己的未来) */
|
|
6986
|
+
const MOTIVATION_MARKER = "下一轮动机";
|
|
6987
|
+
/** 缺省避开的高峰时段(北京时间 [start, end) 不唤起——用量峰谷省钱) */
|
|
6988
|
+
const DEFAULT_AVOID_HOURS = {
|
|
6989
|
+
start: 8,
|
|
6990
|
+
end: 18
|
|
6991
|
+
};
|
|
6992
|
+
function readAutoTrajectorySettings(root) {
|
|
6993
|
+
return loadSerenityConfig(root).autotrajectory ?? null;
|
|
6994
|
+
}
|
|
6953
6995
|
/**
|
|
6954
|
-
*
|
|
6955
|
-
*
|
|
6956
|
-
* 定位:dsp **只提供工具与知识**,不向 CCC 自动安装任何东西(实验可能失败,不污染 CCC)——
|
|
6957
|
-
* 实验是 CCC 的自选动作:agent 调本工具(doc/全报告)即懂实验,init/random/check 辅助,
|
|
6958
|
-
* 实际执行(写配置/写偏见脚本/标记会话)由 CCC 自己决定、自己用现有工具完成。
|
|
6959
|
-
*
|
|
6960
|
-
* 实现:薄封装——exec 包内静态脚本(npm files 含 experiments/),脚本是单一真相源。
|
|
6961
|
-
* 环境注入 SERENITY_ROOT(当前 CCC 根)供脚本定位;bun 优先(可直跑 TS)。
|
|
6996
|
+
* 北京时间(UTC+8)当前小时——不依赖服务器时区(服务器可能 UTC/本地任意)。
|
|
6962
6997
|
*/
|
|
6998
|
+
function beijingHour(nowMs) {
|
|
6999
|
+
return Math.floor((nowMs + 288e5) % 864e5 / 36e5);
|
|
7000
|
+
}
|
|
6963
7001
|
/**
|
|
6964
|
-
*
|
|
6965
|
-
*
|
|
6966
|
-
* vitest 源码直跑时指向 src/tools/x.ts(src/tools → 包根 2 层)——逐级上溯查找,
|
|
6967
|
-
* 两种布局都稳(找到 experiments/autotrajectory/scripts/autotrajectory-exp.ts 即止)。
|
|
7002
|
+
* 唤起窗口判定:北京时间 [avoidStart, avoidEnd) 内不唤起(缺省 8~18)。
|
|
7003
|
+
* 单段避开(start<=end):窗口 = [0,start) ∪ [end,24);跨零点避开(start>end):窗口 = [end,start)。
|
|
6968
7004
|
*/
|
|
6969
|
-
function
|
|
6970
|
-
|
|
6971
|
-
|
|
6972
|
-
|
|
6973
|
-
|
|
6974
|
-
|
|
6975
|
-
if (parent === cur) return null;
|
|
6976
|
-
cur = parent;
|
|
6977
|
-
}
|
|
7005
|
+
function inAllowedWakeWindow(nowMs, avoid) {
|
|
7006
|
+
const start = avoid?.start ?? DEFAULT_AVOID_HOURS.start;
|
|
7007
|
+
const end = avoid?.end ?? DEFAULT_AVOID_HOURS.end;
|
|
7008
|
+
const h = beijingHour(nowMs);
|
|
7009
|
+
if (start <= end) return h < start || h >= end;
|
|
7010
|
+
return h >= end && h < start;
|
|
6978
7011
|
}
|
|
6979
|
-
/**
|
|
6980
|
-
|
|
6981
|
-
|
|
6982
|
-
"all",
|
|
6983
|
-
"init",
|
|
6984
|
-
"random",
|
|
6985
|
-
"diag",
|
|
6986
|
-
"doc",
|
|
6987
|
-
"check",
|
|
6988
|
-
"status",
|
|
6989
|
-
"guide"
|
|
6990
|
-
];
|
|
6991
|
-
function agentCwd$1(exec) {
|
|
6992
|
-
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
7012
|
+
/** 标志位判定:SESSION.md 所在目录名以 --auto 结尾 → 自主轨迹形态 */
|
|
7013
|
+
function isAutoTrajectorySession(mdPath) {
|
|
7014
|
+
return basename(dirname(mdPath)).endsWith(AUTO_DIR_SUFFIX);
|
|
6993
7015
|
}
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
|
|
6998
|
-
|
|
7016
|
+
/**
|
|
7017
|
+
* 目标会话定位:**必须配置 cfg.session**(S###/目录名)——CCC 日常有多条 trajectory 在跑,
|
|
7018
|
+
* 绝不默认唤起(缺省最近活跃会误伤其他正在运行的轨迹;用户拍板:必须配置才生效)。
|
|
7019
|
+
* 未配置 session 或未命中 → null(不唤起)。
|
|
7020
|
+
*/
|
|
7021
|
+
function resolveTargetMd(root, cfg) {
|
|
7022
|
+
if (!cfg?.session) return null;
|
|
7023
|
+
const found = findSession(sessionsRoot(root), cfg.session);
|
|
7024
|
+
if (!found) return null;
|
|
7025
|
+
const md = join(found.path, "SESSION.md");
|
|
7026
|
+
return existsSync(md) ? md : null;
|
|
6999
7027
|
}
|
|
7000
|
-
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
+
/**
|
|
7029
|
+
* 唤起条件(纯逻辑,可测):enabled + 未在运行 + 目录标志 + mtime 超间隔 + 窗口允许。
|
|
7030
|
+
*/
|
|
7031
|
+
function shouldWake(settings, mdPath, nowMs, running) {
|
|
7032
|
+
if (!settings?.enabled || running) return false;
|
|
7033
|
+
if (!mdPath || !isAutoTrajectorySession(mdPath)) return false;
|
|
7034
|
+
if (!inAllowedWakeWindow(nowMs, settings.avoidWakeHours)) return false;
|
|
7035
|
+
try {
|
|
7036
|
+
const mtime = statSync(mdPath).mtimeMs;
|
|
7037
|
+
const hours = Math.max(1, settings.intervalHours ?? 12);
|
|
7038
|
+
return nowMs - mtime >= hours * 36e5;
|
|
7039
|
+
} catch {
|
|
7040
|
+
return false;
|
|
7041
|
+
}
|
|
7042
|
+
}
|
|
7043
|
+
/** 自生动机读取:SESSION.md「下一轮动机」段内容(到下一个二级标题或文件尾;无 → null) */
|
|
7044
|
+
function readSelfGeneratedMotivation(mdPath) {
|
|
7045
|
+
try {
|
|
7046
|
+
const content = readFileSync(mdPath, "utf-8");
|
|
7047
|
+
const idx = content.indexOf(MOTIVATION_MARKER);
|
|
7048
|
+
if (idx < 0) return null;
|
|
7049
|
+
const rest = content.slice(idx + 5);
|
|
7050
|
+
const nextHeading = rest.search(/\n## /);
|
|
7051
|
+
return (nextHeading >= 0 ? rest.slice(0, nextHeading) : rest).trim() || null;
|
|
7052
|
+
} catch {
|
|
7053
|
+
return null;
|
|
7054
|
+
}
|
|
7055
|
+
}
|
|
7056
|
+
/**
|
|
7057
|
+
* 偏见内容:直接运行 CCC 根目录下偏见提供者脚本(biasProvider,缺省 autotrajectory-bias.ts)→ stdout。
|
|
7058
|
+
* 脚本缺失 → 返回 { text: null, error: 提示实现 }(唤起侧报错要求实现,不静默跳过)。
|
|
7059
|
+
* 路径逃逸校验(resolveInside);bun 优先,node 兜底;600s 超时。
|
|
7060
|
+
*/
|
|
7061
|
+
async function fetchBiasContent(root, providerRel) {
|
|
7062
|
+
let scriptAbs;
|
|
7063
|
+
try {
|
|
7064
|
+
scriptAbs = resolveInside(root, providerRel);
|
|
7065
|
+
} catch {
|
|
7066
|
+
return {
|
|
7067
|
+
text: null,
|
|
7068
|
+
error: `biasProvider 路径逃逸(须在 CCC 根内): ${providerRel}`
|
|
7069
|
+
};
|
|
7070
|
+
}
|
|
7071
|
+
if (!existsSync(scriptAbs)) return {
|
|
7072
|
+
text: null,
|
|
7073
|
+
error: `请在 CCC 根目录实现偏见内容提供者脚本: ${providerRel}(stdout 输出偏见内容一行;acc_msm exec autotrajectory-exp init 可生成模板)`
|
|
7074
|
+
};
|
|
7075
|
+
const runs = [["bun", [scriptAbs]], [process.execPath, [scriptAbs]]];
|
|
7076
|
+
for (const [cmd, args] of runs) try {
|
|
7077
|
+
const r = spawnSync(cmd, args, {
|
|
7028
7078
|
encoding: "utf-8",
|
|
7029
7079
|
timeout: 6e5,
|
|
7030
|
-
env,
|
|
7031
7080
|
stdio: [
|
|
7032
7081
|
"ignore",
|
|
7033
7082
|
"pipe",
|
|
7034
7083
|
"pipe"
|
|
7035
7084
|
]
|
|
7036
7085
|
});
|
|
7037
|
-
if (r.status === 0) {
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
}
|
|
7041
|
-
if (r.error?.code === "ENOENT")
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7086
|
+
if (r.status === 0) return {
|
|
7087
|
+
text: (r.stdout ?? "").trim() || null,
|
|
7088
|
+
error: null
|
|
7089
|
+
};
|
|
7090
|
+
if (r.error?.code === "ENOENT") continue;
|
|
7091
|
+
return {
|
|
7092
|
+
text: null,
|
|
7093
|
+
error: `偏见内容提供者脚本执行失败(exit ${r.status ?? "?"}): ${r.stderr?.trim() || r.stdout?.trim() || ""}`
|
|
7094
|
+
};
|
|
7095
|
+
} catch {
|
|
7096
|
+
continue;
|
|
7047
7097
|
}
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
write: 3,
|
|
7053
|
-
edit: 3,
|
|
7054
|
-
str_replace_editor: 3,
|
|
7055
|
-
task: 10,
|
|
7056
|
-
read: 1,
|
|
7057
|
-
grep: 1,
|
|
7058
|
-
glob: 1,
|
|
7059
|
-
skill: 1,
|
|
7060
|
-
acc_msm: 1,
|
|
7061
|
-
cc_fs: 1
|
|
7062
|
-
};
|
|
7063
|
-
function scoreTool(toolName) {
|
|
7064
|
-
return SCORES[toolName] ?? 0;
|
|
7098
|
+
return {
|
|
7099
|
+
text: null,
|
|
7100
|
+
error: "偏见内容提供者脚本无法运行(bun 与 node 均不可用)"
|
|
7101
|
+
};
|
|
7065
7102
|
}
|
|
7066
|
-
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
}
|
|
7076
|
-
|
|
7077
|
-
|
|
7078
|
-
|
|
7079
|
-
if (this.lastTs > 0) this.score += Math.floor((now - this.lastTs) / 6e4);
|
|
7080
|
-
this.lastTs = now;
|
|
7081
|
-
this.score += scoreTool(toolName);
|
|
7082
|
-
return this.score >= this.threshold;
|
|
7083
|
-
}
|
|
7084
|
-
/** 生成确认码并清零积分 */
|
|
7085
|
-
ack() {
|
|
7086
|
-
this.counter += 1;
|
|
7087
|
-
this.score = 0;
|
|
7088
|
-
this.lastTs = 0;
|
|
7089
|
-
return `K${this.counter}`;
|
|
7090
|
-
}
|
|
7091
|
-
get currentScore() {
|
|
7092
|
-
return this.score;
|
|
7093
|
-
}
|
|
7094
|
-
};
|
|
7095
|
-
function reminderText(code, score) {
|
|
7096
|
-
return `[TRAJECTORY-STEWARD] Score threshold reached (${score}). Please acknowledge with [TRAJECTORY-STEWARD-recorded-${code}] once progress is synced to the working session (acc-session show). No need to interrupt your work — just acknowledge inline and keep going.`;
|
|
7103
|
+
/** 唤起消息(三段式:身份锚定 / 先验偏见[自生动机+偏见内容] / 任务)——注入前台会话,用户可见 */
|
|
7104
|
+
function buildWakeMessage(opts) {
|
|
7105
|
+
const lines = [
|
|
7106
|
+
`[自主轨迹唤起] — 距上次轨迹活动已满 ${opts.intervalHours} 小时,自动继续。`,
|
|
7107
|
+
"",
|
|
7108
|
+
`身份锚定:继续 ${opts.sessionName} 的 trajectory(SESSION.md: ${opts.mdPath})。`,
|
|
7109
|
+
"先验偏见:"
|
|
7110
|
+
];
|
|
7111
|
+
if (opts.motivation) lines.push(` · 自生动机:${opts.motivation}`);
|
|
7112
|
+
if (opts.biasContent) lines.push(` · 偏见内容:${opts.biasContent}`);
|
|
7113
|
+
if (!opts.motivation && !opts.biasContent) lines.push(" · (无——本轮纯自主探索)");
|
|
7114
|
+
lines.push("", "任务:执行一轮自主认知(探索/反事实检验),把产出写入 SESSION.md", "「自主探索日志」段,并预写「下一轮动机」段。完成后自然结束。");
|
|
7115
|
+
return lines.join("\n");
|
|
7097
7116
|
}
|
|
7098
|
-
|
|
7099
|
-
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
|
|
7103
|
-
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
/** 读取会话 contextPressure 投影(sessionProjections 可选服务;未装配返回 null) */
|
|
7118
|
-
function readContextPressure(ctx, session) {
|
|
7119
|
-
try {
|
|
7120
|
-
const projections = ctx.get?.("sessionProjections");
|
|
7121
|
-
if (!projections) return null;
|
|
7122
|
-
const pressure = (projections.snapshot?.(session))?.values?.contextPressure;
|
|
7123
|
-
if (!pressure || typeof pressure.projectedTokens !== "number") return null;
|
|
7124
|
-
return {
|
|
7125
|
-
projectedTokens: pressure.projectedTokens,
|
|
7126
|
-
contextWindow: pressure.contextWindow
|
|
7117
|
+
async function performAutoTrajectoryWake(ctx, root, settings, opts = {}) {
|
|
7118
|
+
if (!settings?.enabled) return {
|
|
7119
|
+
ok: false,
|
|
7120
|
+
detail: "autotrajectory 未启用(enabled=false)"
|
|
7121
|
+
};
|
|
7122
|
+
const mdPath = resolveTargetMd(root, settings);
|
|
7123
|
+
if (!mdPath) return {
|
|
7124
|
+
ok: false,
|
|
7125
|
+
detail: "目标会话未命中(session 未配置或 AGENT_SESSIONS 无匹配)"
|
|
7126
|
+
};
|
|
7127
|
+
if (!isAutoTrajectorySession(mdPath)) return {
|
|
7128
|
+
ok: false,
|
|
7129
|
+
detail: "目标会话目录无 --auto 标志(AGENT_SESSIONS/<date>--<desc>--auto/)"
|
|
7130
|
+
};
|
|
7131
|
+
if (!opts.force) {
|
|
7132
|
+
const now = Date.now();
|
|
7133
|
+
if (!inAllowedWakeWindow(now, settings.avoidWakeHours)) return {
|
|
7134
|
+
ok: false,
|
|
7135
|
+
detail: "当前在北京高峰避开窗口内(不唤起)"
|
|
7127
7136
|
};
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
}
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
const rebuildReminderStates = /* @__PURE__ */ new Map();
|
|
7141
|
-
function registerKeeper(ctx, opts = {}) {
|
|
7142
|
-
const defaultThreshold = opts.defaultThreshold ?? 150;
|
|
7143
|
-
const trackerFor = (exec) => {
|
|
7144
|
-
const key = exec.agent?.session?.id ?? "global";
|
|
7145
|
-
let t = trackers.get(key);
|
|
7146
|
-
if (!t) {
|
|
7147
|
-
const root = findSerenityRoot(exec.agent?.session?.header?.cwd ?? process.cwd());
|
|
7148
|
-
t = new KeeperTracker(root ? loadSerenityConfig(root, opts.configPaths).sessionKeeper?.threshold ?? defaultThreshold : defaultThreshold);
|
|
7149
|
-
trackers.set(key, t);
|
|
7137
|
+
try {
|
|
7138
|
+
const mtime = statSync(mdPath).mtimeMs;
|
|
7139
|
+
const hours = Math.max(1, settings.intervalHours ?? 12);
|
|
7140
|
+
if (now - mtime < hours * 36e5) return {
|
|
7141
|
+
ok: false,
|
|
7142
|
+
detail: `距上次轨迹活动不足 ${hours}h(等待中)`
|
|
7143
|
+
};
|
|
7144
|
+
} catch {
|
|
7145
|
+
return {
|
|
7146
|
+
ok: false,
|
|
7147
|
+
detail: "SESSION.md 读取失败(statSync)"
|
|
7148
|
+
};
|
|
7150
7149
|
}
|
|
7151
|
-
|
|
7150
|
+
}
|
|
7151
|
+
const agent = resolveTargetAgent(ctx, mdPath);
|
|
7152
|
+
if (!agent) return {
|
|
7153
|
+
ok: false,
|
|
7154
|
+
detail: `目标会话 agent 不可得——${diagnoseTargetUnavailable(ctx, mdPath) ?? "目标会话未打开或命名未生效"}`
|
|
7152
7155
|
};
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
const code = tracker.ack();
|
|
7167
|
-
blocks.push({
|
|
7168
|
-
type: "text",
|
|
7169
|
-
text: reminderText(code, tracker.currentScore)
|
|
7170
|
-
});
|
|
7171
|
-
}
|
|
7172
|
-
if (skiffRebuild && readSimpleSettings().rebuildEnabled) {
|
|
7173
|
-
const session = exec.agent?.session;
|
|
7174
|
-
if (session) {
|
|
7175
|
-
const pressure = readContextPressure(ctx, session);
|
|
7176
|
-
if (pressure && pressure.contextWindow && pressure.contextWindow > 0) {
|
|
7177
|
-
const ratio = pressure.projectedTokens / pressure.contextWindow;
|
|
7178
|
-
const threshold = readSimpleSettings().rebuildThreshold;
|
|
7179
|
-
if (ratio >= threshold) {
|
|
7180
|
-
const key = exec.agent?.session?.id ?? "global";
|
|
7181
|
-
const st = rebuildReminderStates.get(key) ?? { consecutive: 0 };
|
|
7182
|
-
st.consecutive += 1;
|
|
7183
|
-
const escalated = st.consecutive >= REBUILD_ESCALATE_AFTER;
|
|
7184
|
-
blocks.push({
|
|
7185
|
-
type: "text",
|
|
7186
|
-
text: rebuildReminderText(ratio, threshold, escalated)
|
|
7187
|
-
});
|
|
7188
|
-
rebuildReminderStates.set(key, st);
|
|
7189
|
-
}
|
|
7190
|
-
}
|
|
7191
|
-
}
|
|
7192
|
-
}
|
|
7193
|
-
if (blocks.length === 0) return downstream;
|
|
7194
|
-
const reminderAll = createUserMessage({
|
|
7195
|
-
content: blocks,
|
|
7196
|
-
source: PLUGIN_SOURCE$3
|
|
7197
|
-
});
|
|
7198
|
-
if (downstream.kind === "block") return {
|
|
7199
|
-
kind: "block",
|
|
7200
|
-
feedback: downstream.feedback,
|
|
7201
|
-
additionalContexts: [reminderAll, ...downstream.additionalContexts ?? []]
|
|
7202
|
-
};
|
|
7203
|
-
return {
|
|
7204
|
-
...downstream,
|
|
7205
|
-
additionalContexts: [reminderAll, ...downstream.additionalContexts ?? []]
|
|
7206
|
-
};
|
|
7156
|
+
const provider = settings.biasProvider?.trim() || "autotrajectory-bias.ts";
|
|
7157
|
+
const biasRes = await fetchBiasContent(root, provider);
|
|
7158
|
+
if (biasRes.error) return {
|
|
7159
|
+
ok: false,
|
|
7160
|
+
detail: `偏见内容缺失:${biasRes.error}`
|
|
7161
|
+
};
|
|
7162
|
+
const motivation = readSelfGeneratedMotivation(mdPath);
|
|
7163
|
+
const message = buildWakeMessage({
|
|
7164
|
+
sessionName: basename(dirname(mdPath)),
|
|
7165
|
+
mdPath,
|
|
7166
|
+
intervalHours: Math.max(1, settings.intervalHours ?? 12),
|
|
7167
|
+
motivation,
|
|
7168
|
+
biasContent: biasRes.text
|
|
7207
7169
|
});
|
|
7170
|
+
agent.steer(createUserMessage({
|
|
7171
|
+
content: [{
|
|
7172
|
+
type: "text",
|
|
7173
|
+
text: message
|
|
7174
|
+
}],
|
|
7175
|
+
source: PLUGIN_SOURCE$3
|
|
7176
|
+
}));
|
|
7177
|
+
return {
|
|
7178
|
+
ok: true,
|
|
7179
|
+
detail: `已唤起 ${basename(dirname(mdPath))}(偏见提供者 ${provider})`
|
|
7180
|
+
};
|
|
7208
7181
|
}
|
|
7209
|
-
//#endregion
|
|
7210
|
-
//#region src/skills-discovery.ts
|
|
7211
7182
|
/**
|
|
7212
|
-
*
|
|
7183
|
+
* 装配(index.ts apply 调用)。时钟唤起(v1.26.14 修复——用户"手动唤起正常,时钟唤起不工作"):
|
|
7213
7184
|
*
|
|
7214
|
-
*
|
|
7215
|
-
*
|
|
7216
|
-
*
|
|
7217
|
-
* 1. `.dsh/entry-skill` 指针文件(内容 = skill 名)—— 兼容旧约定
|
|
7218
|
-
* 2. `.opencode/skills/*-serenity/SKILL.md` —— 自动扫描该 CCC 的顶层入口
|
|
7219
|
-
* (home-serenity / tg-serenity / pangu-serenity …,命名模式 `*-serenity`)
|
|
7220
|
-
* 3. `.dsh/skills/*-serenity/SKILL.md` —— 自动扫描 ACC/harness 入口(acc-serenity 等)
|
|
7221
|
-
* 按名去重;顺序 = 记号文件 → 指针 → opencode 入口 → dsh 入口。
|
|
7185
|
+
* 根因:旧实现启动时一次性 `resolveAutoTrajectoryRoot(ctx)`——web 进程启动时 live 会话
|
|
7186
|
+
* 往往为空(用户尚未打开实验 CCC 会话)→ root=null → settings=null → **定时器根本不启动**;
|
|
7187
|
+
* 手动唤起走 HTTP 端点(点击时重新解析 root)→ 正常。
|
|
7222
7188
|
*
|
|
7223
|
-
*
|
|
7189
|
+
* 修复:
|
|
7190
|
+
* ① **动态解析**:每次 tick 重新解析 root + settings(不绑定启动时值)——live 会话变化即跟上;
|
|
7191
|
+
* ② **事件驱动启动**:监听 `session/created`(live 会话出现)→ 启动定时器(进程启动时若
|
|
7192
|
+
* 已有实验 CCC 会话则立即启动);
|
|
7193
|
+
* ③ **优先实验 CCC**:resolveAutoTrajectoryCcc(配置了 autotrajectory 的 live CCC)优先于
|
|
7194
|
+
* 进程 cwd/任一 live(多 CCC 同实例时时钟唤起绑定实验 CCC,而非当前维护会话 CCC)。
|
|
7195
|
+
*
|
|
7196
|
+
* 零资源占用语义保留:未配置/未启用 → tick 内直接 return(定时器存在但每 10min 一次空检查,
|
|
7197
|
+
* unref 不阻塞进程退出)。
|
|
7224
7198
|
*/
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7199
|
+
function registerAutoTrajectory(ctx) {
|
|
7200
|
+
let timer = null;
|
|
7201
|
+
let running = false;
|
|
7202
|
+
/** 解析当前应绑定的实验 CCC:配置了 autotrajectory 的 live CCC 优先,回退进程 cwd/任一 live */
|
|
7203
|
+
const resolveRoot = () => resolveAutoTrajectoryCcc(ctx) ?? resolveAutoTrajectoryRoot(ctx);
|
|
7204
|
+
const tick = () => {
|
|
7205
|
+
if (running) return;
|
|
7206
|
+
const root = resolveRoot();
|
|
7207
|
+
const settings = root ? readAutoTrajectorySettings(root) : null;
|
|
7208
|
+
if (!settings?.enabled) return;
|
|
7209
|
+
if (!shouldWake(settings, resolveTargetMd(root, settings), Date.now(), running)) return;
|
|
7210
|
+
running = true;
|
|
7211
|
+
(async () => {
|
|
7212
|
+
try {
|
|
7213
|
+
const res = await performAutoTrajectoryWake(ctx, root, settings, { force: false });
|
|
7214
|
+
if (res.ok) console.log(`[serenity-hooks] ✓ 自主轨迹唤起(${res.detail})`);
|
|
7215
|
+
else console.warn(`[serenity-hooks] ✗ 自主轨迹唤起跳过:${res.detail}`);
|
|
7216
|
+
} catch (err) {
|
|
7217
|
+
console.warn(`[serenity-hooks] ✗ 自主轨迹唤起失败: ${String(err?.message ?? err)}`);
|
|
7218
|
+
} finally {
|
|
7219
|
+
running = false;
|
|
7220
|
+
}
|
|
7221
|
+
})();
|
|
7222
|
+
};
|
|
7223
|
+
const startTimer = () => {
|
|
7224
|
+
if (timer) return;
|
|
7225
|
+
const root = resolveRoot();
|
|
7226
|
+
const settings = root ? readAutoTrajectorySettings(root) : null;
|
|
7227
|
+
if (!settings?.enabled) return;
|
|
7228
|
+
timer = setInterval(tick, TICK_MS);
|
|
7229
|
+
timer.unref();
|
|
7230
|
+
console.log(`[serenity-hooks] ✓ 自主轨迹定时器启动(${root},intervalHours=${settings.intervalHours ?? 12})`);
|
|
7231
|
+
tick();
|
|
7232
|
+
};
|
|
7233
|
+
startTimer();
|
|
7234
|
+
try {
|
|
7235
|
+
ctx.on("session/created", () => startTimer());
|
|
7236
|
+
} catch {}
|
|
7234
7237
|
}
|
|
7235
|
-
/**
|
|
7236
|
-
function
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7238
|
+
/** 目标 agent:从 SESSION.md 反向定位 dsh 会话(标题 F3 命名 S###-日期 匹配 + cwd 归属校验);不可得 → null */
|
|
7239
|
+
function resolveTargetAgent(ctx, mdPath) {
|
|
7240
|
+
const idMatch = basename(dirname(mdPath)).match(/--S(\d{3,})--/);
|
|
7241
|
+
const sid = idMatch ? `S${idMatch[1]}` : null;
|
|
7242
|
+
const targetRoot = findSerenityRoot(mdPath);
|
|
7243
|
+
try {
|
|
7244
|
+
const sessions = ctx.sessions;
|
|
7245
|
+
for (const s of sessions?.list?.() ?? []) {
|
|
7246
|
+
const cwd = s?.header?.cwd ?? "";
|
|
7247
|
+
if (targetRoot) {
|
|
7248
|
+
if (findSerenityRoot(cwd) !== targetRoot) continue;
|
|
7249
|
+
} else if (cwd !== "" && !mdPath.startsWith(cwd.endsWith("/") ? cwd : cwd + "/")) continue;
|
|
7250
|
+
const title = readSessionTitle(s);
|
|
7251
|
+
if (sid && title && (title === sid || title.startsWith(`${sid}-`))) {
|
|
7252
|
+
const agent = ctx.agents?.get?.(s.id ?? "");
|
|
7253
|
+
if (agent) return agent;
|
|
7254
|
+
}
|
|
7255
|
+
}
|
|
7256
|
+
} catch {}
|
|
7257
|
+
return null;
|
|
7240
7258
|
}
|
|
7241
|
-
/**
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
|
|
7259
|
+
/**
|
|
7260
|
+
* 从 dsh 会话 log 读取标题(latest-wins `session/title` 事件)——
|
|
7261
|
+
* **标题不在 sessions.list() 条目上**(wire/对象均无 title 字段;F3 命名经
|
|
7262
|
+
* sessionTitle.rename 写进 session log),必须从 events 提取(rebuild 同款读取模式)。
|
|
7263
|
+
*/
|
|
7264
|
+
function readSessionTitle(session) {
|
|
7265
|
+
try {
|
|
7266
|
+
const events = session?.events;
|
|
7267
|
+
if (!Array.isArray(events)) return null;
|
|
7268
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
7269
|
+
const e = events[i];
|
|
7270
|
+
if (e?.type === "session/title" && typeof e.data?.title === "string" && e.data.title.trim() !== "") return e.data.title.trim();
|
|
7271
|
+
}
|
|
7272
|
+
} catch {}
|
|
7248
7273
|
return null;
|
|
7249
7274
|
}
|
|
7250
|
-
/**
|
|
7251
|
-
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
|
|
7275
|
+
/**
|
|
7276
|
+
* 诊断:目标会话 agent 为何不可得(供 performAutoTrajectoryWake 失败信息——
|
|
7277
|
+
* 区分"live 会话里无匹配" vs "匹配但 agent 未加载")。返回诊断文本(无则 null)。
|
|
7278
|
+
*/
|
|
7279
|
+
function diagnoseTargetUnavailable(ctx, mdPath) {
|
|
7280
|
+
const idMatch = basename(dirname(mdPath)).match(/--S(\d{3,})--/);
|
|
7281
|
+
const sid = idMatch ? `S${idMatch[1]}` : null;
|
|
7282
|
+
const targetRoot = findSerenityRoot(mdPath);
|
|
7283
|
+
const sameCccTitles = [];
|
|
7284
|
+
const agentMissing = { sid: false };
|
|
7285
|
+
try {
|
|
7286
|
+
const sessions = ctx.sessions;
|
|
7287
|
+
for (const s of sessions?.list?.() ?? []) {
|
|
7288
|
+
const cwd = s?.header?.cwd ?? "";
|
|
7289
|
+
if (targetRoot) {
|
|
7290
|
+
if (findSerenityRoot(cwd) !== targetRoot) continue;
|
|
7291
|
+
} else if (cwd !== "" && !mdPath.startsWith(cwd.endsWith("/") ? cwd : cwd + "/")) continue;
|
|
7292
|
+
const title = readSessionTitle(s);
|
|
7293
|
+
if (title) sameCccTitles.push(title);
|
|
7294
|
+
if (sid && title && (title === sid || title.startsWith(`${sid}-`))) {
|
|
7295
|
+
if (!ctx.agents?.get?.(s.id ?? "")) agentMissing.sid = true;
|
|
7296
|
+
}
|
|
7271
7297
|
}
|
|
7272
|
-
}
|
|
7273
|
-
|
|
7274
|
-
if (
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
|
|
7298
|
+
} catch {}
|
|
7299
|
+
if (agentMissing.sid) return `会话 ${sid} 已打开但 agent 未加载(会话可能刚创建/正在恢复——稍后重试)`;
|
|
7300
|
+
if (sameCccTitles.length > 0) return `目标 CCC 内 live 会话标题: [${sameCccTitles.join(", ")}]——均不匹配 ${sid}(目标会话未在 WebUI 打开,或命名未生效)`;
|
|
7301
|
+
return `目标 CCC 内无 live 会话(先在 WebUI 打开 ${sid} 会话后重试)`;
|
|
7302
|
+
}
|
|
7303
|
+
/** 自主轨迹绑定的 CCC 根:进程 cwd 上溯 .serenity 优先,回退任一 live 会话 root */
|
|
7304
|
+
function resolveAutoTrajectoryRoot(ctx) {
|
|
7305
|
+
const fromCwd = findSerenityRoot(process.cwd());
|
|
7306
|
+
if (fromCwd) return fromCwd;
|
|
7307
|
+
try {
|
|
7308
|
+
const sessions = ctx.sessions;
|
|
7309
|
+
for (const s of sessions?.list?.() ?? []) {
|
|
7310
|
+
const r = findSerenityRoot(s?.header?.cwd ?? "");
|
|
7311
|
+
if (r) return r;
|
|
7279
7312
|
}
|
|
7280
|
-
}
|
|
7281
|
-
|
|
7282
|
-
for (const dir of scanSerenityDirs(resolve(root, ".dsh", "skills"))) add(join(dir, "SKILL.md"), "dsh");
|
|
7283
|
-
return out;
|
|
7313
|
+
} catch {}
|
|
7314
|
+
return null;
|
|
7284
7315
|
}
|
|
7285
|
-
function
|
|
7286
|
-
|
|
7287
|
-
|
|
7316
|
+
function getAutoTrajectoryStatus(root) {
|
|
7317
|
+
const cfg = readAutoTrajectorySettings(root);
|
|
7318
|
+
const now = Date.now();
|
|
7319
|
+
const base = {
|
|
7320
|
+
configured: cfg !== null,
|
|
7321
|
+
enabled: cfg?.enabled ?? false,
|
|
7322
|
+
intervalHours: Math.max(1, cfg?.intervalHours ?? 12),
|
|
7323
|
+
biasProvider: cfg?.biasProvider?.trim() || "autotrajectory-bias.ts",
|
|
7324
|
+
session: cfg?.session ?? null,
|
|
7325
|
+
avoidWakeHours: {
|
|
7326
|
+
start: cfg?.avoidWakeHours?.start ?? DEFAULT_AVOID_HOURS.start,
|
|
7327
|
+
end: cfg?.avoidWakeHours?.end ?? DEFAULT_AVOID_HOURS.end
|
|
7328
|
+
},
|
|
7329
|
+
beijingHour: beijingHour(now),
|
|
7330
|
+
windowAllowed: inAllowedWakeWindow(now, cfg?.avoidWakeHours)
|
|
7331
|
+
};
|
|
7332
|
+
if (!cfg) return {
|
|
7333
|
+
...base,
|
|
7334
|
+
target: null
|
|
7335
|
+
};
|
|
7336
|
+
const mdPath = resolveTargetMd(root, cfg);
|
|
7337
|
+
if (!mdPath) return {
|
|
7338
|
+
...base,
|
|
7339
|
+
target: null
|
|
7340
|
+
};
|
|
7341
|
+
let idleHours = 0;
|
|
7342
|
+
try {
|
|
7343
|
+
idleHours = (now - statSync(mdPath).mtimeMs) / 36e5;
|
|
7344
|
+
} catch {}
|
|
7345
|
+
return {
|
|
7346
|
+
...base,
|
|
7347
|
+
target: {
|
|
7348
|
+
dirName: basename(dirname(mdPath)),
|
|
7349
|
+
autoFlag: isAutoTrajectorySession(mdPath),
|
|
7350
|
+
idleHours: Math.max(0, idleHours),
|
|
7351
|
+
wakeable: shouldWake(cfg, mdPath, now, false)
|
|
7352
|
+
}
|
|
7353
|
+
};
|
|
7288
7354
|
}
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
function base32Decode(input) {
|
|
7292
|
-
const clean = input.toUpperCase().replace(/[\s=]/g, "");
|
|
7293
|
-
if (clean.length === 0) throw new Error("empty base32");
|
|
7355
|
+
/** 遍历 live 会话(sessions.list())+ 补标题(events session/title latest-wins)+ ccc 归属 */
|
|
7356
|
+
function listLiveSessions(ctx) {
|
|
7294
7357
|
const out = [];
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
7358
|
+
try {
|
|
7359
|
+
const sessions = ctx.sessions;
|
|
7360
|
+
for (const s of sessions?.list?.() ?? []) {
|
|
7361
|
+
const cwd = s?.header?.cwd ?? null;
|
|
7362
|
+
out.push({
|
|
7363
|
+
id: s?.id ?? "",
|
|
7364
|
+
cwd,
|
|
7365
|
+
cccRoot: cwd ? findSerenityRoot(cwd) : null,
|
|
7366
|
+
title: readSessionTitle(s)
|
|
7367
|
+
});
|
|
7305
7368
|
}
|
|
7306
|
-
}
|
|
7307
|
-
return
|
|
7308
|
-
}
|
|
7309
|
-
/** 一个时步的 TOTP code(6 位,前导零保留)。counter = floor(epochSeconds / step) */
|
|
7310
|
-
function totpCode(secretBase32, counter) {
|
|
7311
|
-
const key = base32Decode(secretBase32);
|
|
7312
|
-
const msg = Buffer.alloc(8);
|
|
7313
|
-
msg.writeBigUInt64BE(BigInt(counter));
|
|
7314
|
-
const digest = createHmac("sha1", Buffer.from(key)).update(msg).digest();
|
|
7315
|
-
const offset = digest[digest.length - 1] & 15;
|
|
7316
|
-
const bin = (digest[offset] & 127) << 24 | digest[offset + 1] << 16 | digest[offset + 2] << 8 | digest[offset + 3];
|
|
7317
|
-
return String(bin % 10 ** 6).padStart(6, "0");
|
|
7318
|
-
}
|
|
7319
|
-
/** 当前 epoch 秒 */
|
|
7320
|
-
function nowEpochSeconds() {
|
|
7321
|
-
return Math.floor(Date.now() / 1e3);
|
|
7369
|
+
} catch {}
|
|
7370
|
+
return out;
|
|
7322
7371
|
}
|
|
7323
7372
|
/**
|
|
7324
|
-
*
|
|
7325
|
-
*
|
|
7326
|
-
*
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
const
|
|
7331
|
-
|
|
7332
|
-
|
|
7373
|
+
* 面板解析:GET /serenity/autotrajectory 无 workspace 参数时——
|
|
7374
|
+
* **优先「配置了 autotrajectory 的 live CCC」**(用户实验状态所在;v1.26.14 修复:
|
|
7375
|
+
* 原 resolveWorkspace 解析到第一个 live 会话(如 home-serenity)→ 面板显示"未配置",
|
|
7376
|
+
* 用户实验 CCC(pangu)检测不到)。优先级:已启用 → 已配置(未启用)→ null。
|
|
7377
|
+
*/
|
|
7378
|
+
function resolveAutoTrajectoryCcc(ctx) {
|
|
7379
|
+
const enabled = [];
|
|
7380
|
+
const configured = [];
|
|
7381
|
+
for (const s of listLiveSessions(ctx)) {
|
|
7382
|
+
if (!s.cccRoot) continue;
|
|
7383
|
+
const cfg = readAutoTrajectorySettings(s.cccRoot);
|
|
7384
|
+
if (!cfg) continue;
|
|
7385
|
+
if (cfg.enabled) {
|
|
7386
|
+
if (!enabled.includes(s.cccRoot)) enabled.push(s.cccRoot);
|
|
7387
|
+
} else if (!configured.includes(s.cccRoot)) configured.push(s.cccRoot);
|
|
7388
|
+
}
|
|
7389
|
+
return enabled[0] ?? configured[0] ?? null;
|
|
7390
|
+
}
|
|
7391
|
+
function diagLive(ctx) {
|
|
7392
|
+
const liveSessions = listLiveSessions(ctx);
|
|
7393
|
+
const processCwd = process.cwd();
|
|
7394
|
+
const processCcc = findSerenityRoot(processCwd);
|
|
7395
|
+
const panelResolved = resolveAutoTrajectoryCcc(ctx);
|
|
7396
|
+
const autotrajectoryCccs = [];
|
|
7397
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7398
|
+
for (const s of liveSessions) {
|
|
7399
|
+
if (!s.cccRoot || seen.has(s.cccRoot)) continue;
|
|
7400
|
+
const cfg = readAutoTrajectorySettings(s.cccRoot);
|
|
7401
|
+
if (!cfg) continue;
|
|
7402
|
+
seen.add(s.cccRoot);
|
|
7403
|
+
const status = getAutoTrajectoryStatus(s.cccRoot);
|
|
7404
|
+
const mdPath = resolveTargetMd(s.cccRoot, cfg);
|
|
7405
|
+
const agentResolved = mdPath ? resolveTargetAgent(ctx, mdPath) !== null : false;
|
|
7406
|
+
const agentDiagnosis = mdPath && !agentResolved ? diagnoseTargetUnavailable(ctx, mdPath) : null;
|
|
7407
|
+
autotrajectoryCccs.push({
|
|
7408
|
+
root: s.cccRoot,
|
|
7409
|
+
enabled: status.enabled,
|
|
7410
|
+
session: status.session,
|
|
7411
|
+
target: status.target,
|
|
7412
|
+
agentResolved,
|
|
7413
|
+
agentDiagnosis
|
|
7414
|
+
});
|
|
7415
|
+
}
|
|
7416
|
+
return {
|
|
7417
|
+
processCwd,
|
|
7418
|
+
processCcc,
|
|
7419
|
+
liveSessions,
|
|
7420
|
+
autotrajectoryCccs,
|
|
7421
|
+
panelResolved
|
|
7422
|
+
};
|
|
7333
7423
|
}
|
|
7334
7424
|
//#endregion
|
|
7335
|
-
//#region src/
|
|
7425
|
+
//#region src/tools/autotrajectory-exp.ts
|
|
7336
7426
|
/**
|
|
7337
|
-
*
|
|
7338
|
-
*
|
|
7339
|
-
* 归属原则(S142 用户拍板,v1.22):**plugin 是全局的,CCC 是具体的**——
|
|
7340
|
-
* 账号密码/gateway 监听配置是 plugin 级能力,归 plugin 全局文件;
|
|
7341
|
-
* CCC 的 localstore.json 只管 CCC 自己的凭据/配置。v1.21.x 曾把
|
|
7342
|
-
* `serenityAdvanced` 存进 CCC localstore(归属错误 + 与 DSH settings 开关割裂),
|
|
7343
|
-
* 本版本迁移到 plugin 全局文件(migrateLegacyLocalstore 一次性迁移)。
|
|
7427
|
+
* autotrajectory-exp.ts — 自主轨迹实验一站式管理工具(v1.26.12 实验提案,默认关)
|
|
7344
7428
|
*
|
|
7345
|
-
*
|
|
7346
|
-
*
|
|
7347
|
-
*
|
|
7429
|
+
* 定位:dsp **只提供工具与知识**,不向 CCC 自动安装任何东西(实验可能失败,不污染 CCC)——
|
|
7430
|
+
* 实验是 CCC 的自选动作:agent 调本工具(doc/全报告)即懂实验,init/random/check 辅助,
|
|
7431
|
+
* 实际执行(写配置/写偏见脚本/标记会话)由 CCC 自己决定、自己用现有工具完成。
|
|
7348
7432
|
*
|
|
7349
|
-
*
|
|
7350
|
-
*
|
|
7433
|
+
* 实现:薄封装——exec 包内静态脚本(npm files 含 experiments/),脚本是单一真相源。
|
|
7434
|
+
* 环境注入 SERENITY_ROOT(当前 CCC 根)供脚本定位;bun 优先(可直跑 TS)。
|
|
7435
|
+
* diag-live:**进程内诊断**(v1.26.14 用户"排查访问不到")——闭包捕获 ctx,
|
|
7436
|
+
* 直接读 live 会话/标题/agent(脚本 diag 看不到运行时),输出实例级诊断报告。
|
|
7351
7437
|
*/
|
|
7352
|
-
/**
|
|
7353
|
-
|
|
7354
|
-
|
|
7355
|
-
|
|
7356
|
-
|
|
7357
|
-
|
|
7358
|
-
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
|
|
7364
|
-
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
rebuild: {
|
|
7368
|
-
enabled: true,
|
|
7369
|
-
thresholdRatio: .9
|
|
7370
|
-
},
|
|
7371
|
-
naming: { enabled: true },
|
|
7372
|
-
persona: {
|
|
7373
|
-
mode: "",
|
|
7374
|
-
overrideText: ""
|
|
7375
|
-
},
|
|
7376
|
-
publicAsk: {
|
|
7377
|
-
key: "",
|
|
7378
|
-
allowed: []
|
|
7379
|
-
}
|
|
7380
|
-
};
|
|
7381
|
-
}
|
|
7382
|
-
const SCRYPT_KEYLEN = 32;
|
|
7383
|
-
/** 生成 scrypt hash(格式 `salt:hex`);salt 16 字节随机 */
|
|
7384
|
-
function hashPassword(password) {
|
|
7385
|
-
const salt = randomBytes(16);
|
|
7386
|
-
const derived = scryptSync(password, salt, SCRYPT_KEYLEN);
|
|
7387
|
-
return `${salt.toString("hex")}:${derived.toString("hex")}`;
|
|
7438
|
+
/**
|
|
7439
|
+
* 定位包内实验脚本(npm files 分发 experiments/autotrajectory/)。
|
|
7440
|
+
* 布局差异:tsdown bundle 后 import.meta.url 指向 lib/index.js(lib → 包根 1 层);
|
|
7441
|
+
* vitest 源码直跑时指向 src/tools/x.ts(src/tools → 包根 2 层)——逐级上溯查找,
|
|
7442
|
+
* 两种布局都稳(找到 experiments/autotrajectory/scripts/autotrajectory-exp.ts 即止)。
|
|
7443
|
+
*/
|
|
7444
|
+
function findExpScript(startDir) {
|
|
7445
|
+
let cur = startDir;
|
|
7446
|
+
while (true) {
|
|
7447
|
+
const cand = join(cur, "experiments", "autotrajectory", "scripts", "autotrajectory-exp.ts");
|
|
7448
|
+
if (existsSync(cand)) return cand;
|
|
7449
|
+
const parent = dirname(cur);
|
|
7450
|
+
if (parent === cur) return null;
|
|
7451
|
+
cur = parent;
|
|
7452
|
+
}
|
|
7388
7453
|
}
|
|
7389
|
-
/**
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7454
|
+
/** 包内实验脚本(上溯查找;找不到 → execute 报错提示包完整性) */
|
|
7455
|
+
const EXP_SCRIPT = findExpScript(dirname(fileURLToPath(import.meta.url)));
|
|
7456
|
+
const AUTO_TRAJECTORY_EXP_ACTIONS = [
|
|
7457
|
+
"all",
|
|
7458
|
+
"init",
|
|
7459
|
+
"random",
|
|
7460
|
+
"diag",
|
|
7461
|
+
"doc",
|
|
7462
|
+
"check",
|
|
7463
|
+
"status",
|
|
7464
|
+
"guide",
|
|
7465
|
+
"diag-live"
|
|
7466
|
+
];
|
|
7467
|
+
function agentCwd$1(exec) {
|
|
7468
|
+
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
7397
7469
|
}
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
return join(dshHome, "serenity-hooks.json");
|
|
7470
|
+
function renderText(value) {
|
|
7471
|
+
return [{
|
|
7472
|
+
type: "text",
|
|
7473
|
+
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
7474
|
+
}];
|
|
7404
7475
|
}
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7476
|
+
/** 渲染 diag-live 报告为可读文本(EAP:结构清晰、命中点显式) */
|
|
7477
|
+
function renderDiagLive(r) {
|
|
7478
|
+
const lines = [
|
|
7479
|
+
"═══ 自主轨迹进程内诊断(diag-live,v1.26.14)═══",
|
|
7480
|
+
`进程 cwd: ${r.processCwd}(CCC: ${r.processCcc ?? "无 .serenity"})`,
|
|
7481
|
+
`面板解析(无参 GET 目标): ${r.panelResolved ?? "无配置 autotrajectory 的 live CCC"}`,
|
|
7482
|
+
"",
|
|
7483
|
+
`── live 会话(${r.liveSessions.length})──`
|
|
7484
|
+
];
|
|
7485
|
+
if (r.liveSessions.length === 0) lines.push(" (无 live 会话——WebUI 未打开任何会话;定时器绑定依赖 live 会话 cwd)");
|
|
7486
|
+
for (const s of r.liveSessions) lines.push(` · ${s.id}${s.title ? ` [${s.title}]` : " [无标题]"}${s.cwd ? ` cwd=${s.cwd}` : " cwd=无"}${s.cccRoot ? ` → CCC ${s.cccRoot}` : ""}`);
|
|
7487
|
+
lines.push("", `── 配置了 autotrajectory 的 CCC(${r.autotrajectoryCccs.length})──`);
|
|
7488
|
+
if (r.autotrajectoryCccs.length === 0) lines.push(" (无——live 会话中无任何 CCC 配置 autotrajectory)");
|
|
7489
|
+
for (const c of r.autotrajectoryCccs) {
|
|
7490
|
+
lines.push(` · ${c.root}(enabled=${c.enabled})`);
|
|
7491
|
+
lines.push(` session=${c.session ?? "未配置"} | 目标=${c.target ? `${c.target.dirName}(--auto ${c.target.autoFlag ? "✓" : "✗"},空闲 ${c.target.idleHours.toFixed(1)}h)` : "未命中"}`);
|
|
7492
|
+
lines.push(` agent 定位: ${c.agentResolved ? "✓ 可注入" : `✗ 不可得——${c.agentDiagnosis ?? "未知"}`}`);
|
|
7413
7493
|
}
|
|
7494
|
+
return lines.join("\n");
|
|
7414
7495
|
}
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
const persona = o.persona ?? {};
|
|
7430
|
-
const publicAsk = o.publicAsk ?? {};
|
|
7431
|
-
return {
|
|
7432
|
-
gateway: {
|
|
7433
|
-
enabled: typeof gateway.enabled === "boolean" ? gateway.enabled : def.gateway.enabled,
|
|
7434
|
-
host: typeof gateway.host === "string" && gateway.host !== "" ? gateway.host : def.gateway.host,
|
|
7435
|
-
port: typeof gateway.port === "number" && Number.isInteger(gateway.port) ? gateway.port : def.gateway.port,
|
|
7436
|
-
accounts: Array.isArray(gateway.accounts) ? gateway.accounts.filter((a) => typeof a === "object" && a !== null && typeof a.id === "string" && typeof a.user === "string" && typeof a.passHash === "string") : def.gateway.accounts,
|
|
7437
|
-
workspaces: Array.isArray(gateway.workspaces) ? gateway.workspaces.filter((w) => typeof w === "string" && w !== "") : def.gateway.workspaces,
|
|
7438
|
-
cookieSecure: typeof gateway.cookieSecure === "boolean" ? gateway.cookieSecure : def.gateway.cookieSecure,
|
|
7439
|
-
allowWorkspaceCreate: typeof gateway.allowWorkspaceCreate === "boolean" ? gateway.allowWorkspaceCreate : def.gateway.allowWorkspaceCreate,
|
|
7440
|
-
totpEnabled: typeof gateway.totpEnabled === "boolean" ? gateway.totpEnabled : def.gateway.totpEnabled
|
|
7441
|
-
},
|
|
7442
|
-
rebuild: {
|
|
7443
|
-
enabled: typeof rebuild.enabled === "boolean" ? rebuild.enabled : def.rebuild.enabled,
|
|
7444
|
-
thresholdRatio: typeof rebuild.thresholdRatio === "number" ? rebuild.thresholdRatio : def.rebuild.thresholdRatio
|
|
7445
|
-
},
|
|
7446
|
-
naming: { enabled: typeof naming.enabled === "boolean" ? naming.enabled : def.naming.enabled },
|
|
7447
|
-
persona: {
|
|
7448
|
-
mode: typeof persona.mode === "string" ? persona.mode : def.persona.mode,
|
|
7449
|
-
overrideText: typeof persona.overrideText === "string" ? persona.overrideText : def.persona.overrideText
|
|
7496
|
+
/** 创建 autotrajectory-exp 工具(闭包捕获 ctx → diag-live 进程内诊断;v1.26.14) */
|
|
7497
|
+
function createAutoTrajectoryExpTool(ctx) {
|
|
7498
|
+
return defineTool({
|
|
7499
|
+
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 只提供工具与知识,不自动安装任何东西。",
|
|
7501
|
+
parameters: { action: {
|
|
7502
|
+
type: "string",
|
|
7503
|
+
enum: [...AUTO_TRAJECTORY_EXP_ACTIONS],
|
|
7504
|
+
required: true,
|
|
7505
|
+
description: "Subcommand"
|
|
7506
|
+
} },
|
|
7507
|
+
output: {
|
|
7508
|
+
schema: { type: "json" },
|
|
7509
|
+
render: (_args, value) => renderText(value)
|
|
7450
7510
|
},
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7511
|
+
async execute(args, exec) {
|
|
7512
|
+
if (args.action === "diag-live") return { output: renderDiagLive(diagLive(ctx)) };
|
|
7513
|
+
const root = findSerenityRoot(agentCwd$1(exec));
|
|
7514
|
+
const result = {};
|
|
7515
|
+
if (!EXP_SCRIPT) {
|
|
7516
|
+
result.error = "autotrajectory-exp 实验脚本未随安装分发(npm 包缺 experiments/autotrajectory/)——请检查包完整性";
|
|
7517
|
+
return result;
|
|
7518
|
+
}
|
|
7519
|
+
const script = EXP_SCRIPT;
|
|
7520
|
+
if (!existsSync(script)) {
|
|
7521
|
+
result.error = `autotrajectory-exp 脚本缺失(${script})——实验包未随安装分发,请检查 npm 包完整性`;
|
|
7522
|
+
return result;
|
|
7523
|
+
}
|
|
7524
|
+
const env = { ...process.env };
|
|
7525
|
+
if (root) env.SERENITY_ROOT = root;
|
|
7526
|
+
const r = spawnSync("bun", [script, args.action ?? "all"], {
|
|
7527
|
+
encoding: "utf-8",
|
|
7528
|
+
timeout: 6e5,
|
|
7529
|
+
env,
|
|
7530
|
+
stdio: [
|
|
7531
|
+
"ignore",
|
|
7532
|
+
"pipe",
|
|
7533
|
+
"pipe"
|
|
7534
|
+
]
|
|
7535
|
+
});
|
|
7536
|
+
if (r.status === 0) {
|
|
7537
|
+
result.output = r.stdout?.trim() || "(empty)";
|
|
7538
|
+
return result;
|
|
7539
|
+
}
|
|
7540
|
+
if (r.error?.code === "ENOENT") {
|
|
7541
|
+
result.error = "autotrajectory-exp 需要 bun 运行时(bun not found in PATH)";
|
|
7542
|
+
return result;
|
|
7543
|
+
}
|
|
7544
|
+
result.error = r.stderr?.trim() || r.stdout?.trim() || `exit ${r.status ?? "?"}`;
|
|
7545
|
+
return result;
|
|
7454
7546
|
}
|
|
7455
|
-
};
|
|
7456
|
-
}
|
|
7457
|
-
/** 读取全局配置(文件缺失/坏 JSON → 默认值) */
|
|
7458
|
-
function readAdvancedSettings() {
|
|
7459
|
-
return mergeWithDefaults(readFileSafe(globalConfigPath()));
|
|
7460
|
-
}
|
|
7461
|
-
/** 写入全局配置(整体替换) */
|
|
7462
|
-
function writeAdvancedSettings(settings) {
|
|
7463
|
-
writeFileSafe(globalConfigPath(), settings);
|
|
7464
|
-
}
|
|
7465
|
-
/**
|
|
7466
|
-
* 部分更新:传入 Partial,深合并到现有值。
|
|
7467
|
-
* accounts 传入数组 → 整体替换;accounts 未传 → 保留现有。
|
|
7468
|
-
*/
|
|
7469
|
-
function updateAdvancedSettings(patch) {
|
|
7470
|
-
const current = readAdvancedSettings();
|
|
7471
|
-
const gw = patch.gateway;
|
|
7472
|
-
const rb = patch.rebuild;
|
|
7473
|
-
const nm = patch.naming;
|
|
7474
|
-
const ps = patch.persona;
|
|
7475
|
-
const next = {
|
|
7476
|
-
gateway: gw !== void 0 ? {
|
|
7477
|
-
enabled: typeof gw.enabled === "boolean" ? gw.enabled : current.gateway.enabled,
|
|
7478
|
-
host: typeof gw.host === "string" && gw.host !== "" ? gw.host : current.gateway.host,
|
|
7479
|
-
port: typeof gw.port === "number" && Number.isInteger(gw.port) ? gw.port : current.gateway.port,
|
|
7480
|
-
accounts: Array.isArray(gw.accounts) ? gw.accounts : current.gateway.accounts,
|
|
7481
|
-
workspaces: Array.isArray(gw.workspaces) ? gw.workspaces.filter((w) => typeof w === "string" && w !== "") : current.gateway.workspaces,
|
|
7482
|
-
cookieSecure: typeof gw.cookieSecure === "boolean" ? gw.cookieSecure : current.gateway.cookieSecure,
|
|
7483
|
-
allowWorkspaceCreate: typeof gw.allowWorkspaceCreate === "boolean" ? gw.allowWorkspaceCreate : current.gateway.allowWorkspaceCreate,
|
|
7484
|
-
totpEnabled: typeof gw.totpEnabled === "boolean" ? gw.totpEnabled : current.gateway.totpEnabled
|
|
7485
|
-
} : current.gateway,
|
|
7486
|
-
rebuild: rb !== void 0 ? {
|
|
7487
|
-
enabled: typeof rb.enabled === "boolean" ? rb.enabled : current.rebuild.enabled,
|
|
7488
|
-
thresholdRatio: typeof rb.thresholdRatio === "number" && rb.thresholdRatio > 0 && rb.thresholdRatio <= 1 ? rb.thresholdRatio : current.rebuild.thresholdRatio
|
|
7489
|
-
} : current.rebuild,
|
|
7490
|
-
naming: nm !== void 0 ? { enabled: typeof nm.enabled === "boolean" ? nm.enabled : current.naming.enabled } : current.naming,
|
|
7491
|
-
persona: ps !== void 0 ? {
|
|
7492
|
-
mode: typeof ps.mode === "string" ? ps.mode : current.persona.mode,
|
|
7493
|
-
overrideText: typeof ps.overrideText === "string" ? ps.overrideText : current.persona.overrideText
|
|
7494
|
-
} : current.persona,
|
|
7495
|
-
publicAsk: patch.publicAsk !== void 0 ? {
|
|
7496
|
-
key: typeof patch.publicAsk.key === "string" ? patch.publicAsk.key : current.publicAsk.key,
|
|
7497
|
-
allowed: Array.isArray(patch.publicAsk.allowed) ? patch.publicAsk.allowed.filter((n) => typeof n === "string" && n !== "") : current.publicAsk.allowed
|
|
7498
|
-
} : current.publicAsk
|
|
7499
|
-
};
|
|
7500
|
-
writeAdvancedSettings(next);
|
|
7501
|
-
return next;
|
|
7502
|
-
}
|
|
7503
|
-
/**
|
|
7504
|
-
* 确保问答页访问 key 存在(首次启用自动生成 + 写回固定,用户拍板"配置时生成"):
|
|
7505
|
-
* 读取全局配置 publicAsk.key;为空 → 生成 32 字节随机 hex(64 字符)→ 写回持久化。
|
|
7506
|
-
* @returns 当前固定 key(已生成则返回既有值——**幂等**,不会覆盖用户手改的 key)
|
|
7507
|
-
*/
|
|
7508
|
-
function ensurePublicAskKey() {
|
|
7509
|
-
const current = readAdvancedSettings();
|
|
7510
|
-
if (current.publicAsk.key !== "") return current.publicAsk.key;
|
|
7511
|
-
const key = randomBytes(32).toString("hex");
|
|
7512
|
-
updateAdvancedSettings({ publicAsk: {
|
|
7513
|
-
key,
|
|
7514
|
-
allowed: current.publicAsk.allowed
|
|
7515
|
-
} });
|
|
7516
|
-
return key;
|
|
7517
|
-
}
|
|
7518
|
-
/** 校验问答页访问 key(timing-safe;key 未生成 → 恒 false——没有 key 不工作) */
|
|
7519
|
-
function verifyPublicAskKey(provided) {
|
|
7520
|
-
const expected = readAdvancedSettings().publicAsk.key;
|
|
7521
|
-
if (!expected || typeof provided !== "string" || provided === "") return false;
|
|
7522
|
-
const a = Buffer.from(provided);
|
|
7523
|
-
const b = Buffer.from(expected);
|
|
7524
|
-
return a.length === b.length && timingSafeEqual(a, b);
|
|
7525
|
-
}
|
|
7526
|
-
/**
|
|
7527
|
-
* 轮换问答页访问 key(v1.26.5,S142 用户:公网开放 + 不好说要换 key 呢):
|
|
7528
|
-
* 生成新 32 字节随机 hex(64 字符)→ 覆盖写回(**强制替换**——旧 key 立即失效)。
|
|
7529
|
-
* @returns 新 key(面板展示给管理员,重新分享给使用者)
|
|
7530
|
-
*/
|
|
7531
|
-
function rotatePublicAskKey() {
|
|
7532
|
-
const current = readAdvancedSettings();
|
|
7533
|
-
const key = randomBytes(32).toString("hex");
|
|
7534
|
-
updateAdvancedSettings({ publicAsk: {
|
|
7535
|
-
key,
|
|
7536
|
-
allowed: current.publicAsk.allowed
|
|
7537
|
-
} });
|
|
7538
|
-
return key;
|
|
7547
|
+
});
|
|
7539
7548
|
}
|
|
7540
|
-
|
|
7541
|
-
|
|
7542
|
-
|
|
7543
|
-
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
|
|
7549
|
+
//#endregion
|
|
7550
|
+
//#region src/seams/keeper.ts
|
|
7551
|
+
const SCORES = {
|
|
7552
|
+
write: 3,
|
|
7553
|
+
edit: 3,
|
|
7554
|
+
str_replace_editor: 3,
|
|
7555
|
+
task: 10,
|
|
7556
|
+
read: 1,
|
|
7557
|
+
grep: 1,
|
|
7558
|
+
glob: 1,
|
|
7559
|
+
skill: 1,
|
|
7560
|
+
acc_msm: 1,
|
|
7561
|
+
cc_fs: 1
|
|
7562
|
+
};
|
|
7563
|
+
function scoreTool(toolName) {
|
|
7564
|
+
return SCORES[toolName] ?? 0;
|
|
7548
7565
|
}
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
|
|
7566
|
+
var KeeperTracker = class {
|
|
7567
|
+
threshold;
|
|
7568
|
+
now;
|
|
7569
|
+
score = 0;
|
|
7570
|
+
lastTs = 0;
|
|
7571
|
+
counter = 0;
|
|
7572
|
+
constructor(threshold, now = Date.now) {
|
|
7573
|
+
this.threshold = threshold;
|
|
7574
|
+
this.now = now;
|
|
7557
7575
|
}
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
|
|
7563
|
-
|
|
7564
|
-
|
|
7565
|
-
count: 0,
|
|
7566
|
-
lockedUntil: 0,
|
|
7567
|
-
lockRound: 0
|
|
7568
|
-
};
|
|
7569
|
-
ipFailStates.set(ip, st);
|
|
7576
|
+
/** 记录一次工具调用 + 经过时间;返回是否应触发提醒 */
|
|
7577
|
+
step(toolName) {
|
|
7578
|
+
const now = this.now();
|
|
7579
|
+
if (this.lastTs > 0) this.score += Math.floor((now - this.lastTs) / 6e4);
|
|
7580
|
+
this.lastTs = now;
|
|
7581
|
+
this.score += scoreTool(toolName);
|
|
7582
|
+
return this.score >= this.threshold;
|
|
7570
7583
|
}
|
|
7571
|
-
|
|
7572
|
-
|
|
7573
|
-
|
|
7574
|
-
|
|
7575
|
-
|
|
7576
|
-
|
|
7577
|
-
st.count = 0;
|
|
7578
|
-
return st.lockedUntil - Date.now();
|
|
7584
|
+
/** 生成确认码并清零积分 */
|
|
7585
|
+
ack() {
|
|
7586
|
+
this.counter += 1;
|
|
7587
|
+
this.score = 0;
|
|
7588
|
+
this.lastTs = 0;
|
|
7589
|
+
return `K${this.counter}`;
|
|
7579
7590
|
}
|
|
7580
|
-
|
|
7591
|
+
get currentScore() {
|
|
7592
|
+
return this.score;
|
|
7593
|
+
}
|
|
7594
|
+
};
|
|
7595
|
+
function reminderText(code, score) {
|
|
7596
|
+
return `[TRAJECTORY-STEWARD] Score threshold reached (${score}). Please acknowledge with [TRAJECTORY-STEWARD-recorded-${code}] once progress is synced to the working session (acc-session show). No need to interrupt your work — just acknowledge inline and keep going.`;
|
|
7581
7597
|
}
|
|
7582
7598
|
/**
|
|
7583
|
-
*
|
|
7584
|
-
*
|
|
7585
|
-
*
|
|
7586
|
-
*
|
|
7599
|
+
* F2 rebuild 提示(v1.21;v1.22.1 对齐"轨迹跟踪器"概念;v1.23.0 英化 + 载体关系;
|
|
7600
|
+
* v1.23.3 行动指令化——用户反馈"一直在触发为啥没执行":旧文案是状态播报式
|
|
7601
|
+
* ("[TRAJECTORY] Context usage at N%..."),模型当成系统状态而非行动请求,可一直忽略。
|
|
7602
|
+
* 新文案 = 明确的行动指令(ACT NOW + 何时执行 + 必须执行),对齐 steward ACK 协议风格。
|
|
7603
|
+
*
|
|
7604
|
+
* v1.24.12 沉淀协议(S142 用户需求):rebuild 前若掌握值得沉淀的认知——修订 CCC 现有
|
|
7605
|
+
* skill(eap 结构化);若需新建 skill——**不自行创建**,写提案进 SESSION.md 供用户参考。
|
|
7606
|
+
* 简短留自由度(不提"加载 eap 工具",模型自知;只说修订 skill + 新建 skill 落 SESSION)。
|
|
7607
|
+
*
|
|
7608
|
+
* escalated=true(v1.23.3):连续多轮超阈值仍未 rebuild → 升级强制语气
|
|
7609
|
+
* (STOP and rebuild now,持续注入直到调用 session_rebuild)。
|
|
7587
7610
|
*/
|
|
7588
|
-
function
|
|
7589
|
-
|
|
7590
|
-
const
|
|
7591
|
-
if (
|
|
7592
|
-
|
|
7593
|
-
if (legacy === void 0 || legacy === null || typeof legacy !== "object") return false;
|
|
7594
|
-
writeFileSafe(gpath, legacy);
|
|
7595
|
-
return true;
|
|
7596
|
-
}
|
|
7597
|
-
/** 持久化 → wire(剥离 passHash/totpSecret;只留布尔) */
|
|
7598
|
-
function toWire(settings) {
|
|
7599
|
-
return {
|
|
7600
|
-
gateway: {
|
|
7601
|
-
enabled: settings.gateway.enabled,
|
|
7602
|
-
host: settings.gateway.host,
|
|
7603
|
-
port: settings.gateway.port,
|
|
7604
|
-
accounts: settings.gateway.accounts.map((a) => ({
|
|
7605
|
-
id: a.id,
|
|
7606
|
-
user: a.user,
|
|
7607
|
-
hasPassword: a.passHash !== "",
|
|
7608
|
-
hasTotp: typeof a.totpSecret === "string" && a.totpSecret !== ""
|
|
7609
|
-
})),
|
|
7610
|
-
workspaces: [...settings.gateway.workspaces],
|
|
7611
|
-
cookieSecure: settings.gateway.cookieSecure,
|
|
7612
|
-
allowWorkspaceCreate: settings.gateway.allowWorkspaceCreate,
|
|
7613
|
-
totpEnabled: settings.gateway.totpEnabled
|
|
7614
|
-
},
|
|
7615
|
-
rebuild: settings.rebuild,
|
|
7616
|
-
naming: settings.naming,
|
|
7617
|
-
persona: settings.persona,
|
|
7618
|
-
publicAsk: { allowed: [...settings.publicAsk.allowed] }
|
|
7619
|
-
};
|
|
7611
|
+
function rebuildReminderText(ratio, threshold, escalated = false) {
|
|
7612
|
+
const pct = (ratio * 100).toFixed(0);
|
|
7613
|
+
const thr = (threshold * 100).toFixed(0);
|
|
7614
|
+
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.`;
|
|
7615
|
+
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.`;
|
|
7620
7616
|
}
|
|
7621
|
-
/**
|
|
7622
|
-
|
|
7623
|
-
* accounts 元素可选带 `pass`:非空 → 重新 hash;空/缺省 → 保留现有 hash(按 id 匹配)。
|
|
7624
|
-
* 新账号(id 不在现有)必须带非空 pass,否则抛错(无法生成 hash)。
|
|
7625
|
-
*/
|
|
7626
|
-
function applyWirePatch(wire) {
|
|
7627
|
-
const current = readAdvancedSettings();
|
|
7628
|
-
const patch = {};
|
|
7629
|
-
if (wire.gateway !== void 0) {
|
|
7630
|
-
const gwPatch = { ...current.gateway };
|
|
7631
|
-
if (typeof wire.gateway.enabled === "boolean") gwPatch.enabled = wire.gateway.enabled;
|
|
7632
|
-
if (typeof wire.gateway.host === "string" && wire.gateway.host !== "") gwPatch.host = wire.gateway.host;
|
|
7633
|
-
if (typeof wire.gateway.port === "number" && Number.isInteger(wire.gateway.port)) gwPatch.port = wire.gateway.port;
|
|
7634
|
-
if (Array.isArray(wire.gateway.workspaces)) gwPatch.workspaces = wire.gateway.workspaces.filter((w) => typeof w === "string" && w !== "");
|
|
7635
|
-
if (typeof wire.gateway.cookieSecure === "boolean") gwPatch.cookieSecure = wire.gateway.cookieSecure;
|
|
7636
|
-
if (typeof wire.gateway.allowWorkspaceCreate === "boolean") gwPatch.allowWorkspaceCreate = wire.gateway.allowWorkspaceCreate;
|
|
7637
|
-
if (typeof wire.gateway.totpEnabled === "boolean") gwPatch.totpEnabled = wire.gateway.totpEnabled;
|
|
7638
|
-
if (Array.isArray(wire.gateway.accounts)) {
|
|
7639
|
-
const byId = new Map(current.gateway.accounts.map((a) => [a.id, a]));
|
|
7640
|
-
gwPatch.accounts = wire.gateway.accounts.map((a) => {
|
|
7641
|
-
const existing = byId.get(a.id);
|
|
7642
|
-
const pass = a.pass;
|
|
7643
|
-
const totp = a.totpSecret;
|
|
7644
|
-
const totpReset = a.totpReset === true;
|
|
7645
|
-
if (typeof totp === "string" && totp !== "") try {
|
|
7646
|
-
base32Decode(totp);
|
|
7647
|
-
} catch {
|
|
7648
|
-
throw new Error(`Account "${a.user}" TOTP secret is not valid base32`);
|
|
7649
|
-
}
|
|
7650
|
-
const nextTotp = totpReset ? void 0 : typeof totp === "string" && totp !== "" ? totp : existing?.totpSecret;
|
|
7651
|
-
const withTotp = {
|
|
7652
|
-
id: a.id,
|
|
7653
|
-
user: a.user,
|
|
7654
|
-
...nextTotp === void 0 ? {} : { totpSecret: nextTotp }
|
|
7655
|
-
};
|
|
7656
|
-
if (typeof pass === "string" && pass !== "") return {
|
|
7657
|
-
...withTotp,
|
|
7658
|
-
passHash: hashPassword(pass)
|
|
7659
|
-
};
|
|
7660
|
-
if (existing) return {
|
|
7661
|
-
...withTotp,
|
|
7662
|
-
passHash: existing.passHash
|
|
7663
|
-
};
|
|
7664
|
-
throw new Error(`Account "${a.user}" (id=${a.id}) has no password and no existing hash — new accounts must set a password`);
|
|
7665
|
-
});
|
|
7666
|
-
}
|
|
7667
|
-
patch.gateway = gwPatch;
|
|
7668
|
-
}
|
|
7669
|
-
if (wire.rebuild !== void 0) {
|
|
7670
|
-
const rbPatch = { ...current.rebuild };
|
|
7671
|
-
if (typeof wire.rebuild.enabled === "boolean") rbPatch.enabled = wire.rebuild.enabled;
|
|
7672
|
-
if (typeof wire.rebuild.thresholdRatio === "number" && wire.rebuild.thresholdRatio > 0 && wire.rebuild.thresholdRatio <= 1) rbPatch.thresholdRatio = wire.rebuild.thresholdRatio;
|
|
7673
|
-
patch.rebuild = rbPatch;
|
|
7674
|
-
}
|
|
7675
|
-
if (wire.naming !== void 0 && typeof wire.naming.enabled === "boolean") patch.naming = { enabled: wire.naming.enabled };
|
|
7676
|
-
if (wire.persona !== void 0) {
|
|
7677
|
-
const psPatch = { ...current.persona };
|
|
7678
|
-
if (typeof wire.persona.mode === "string") psPatch.mode = wire.persona.mode;
|
|
7679
|
-
if (typeof wire.persona.overrideText === "string") psPatch.overrideText = wire.persona.overrideText;
|
|
7680
|
-
patch.persona = psPatch;
|
|
7681
|
-
}
|
|
7682
|
-
if (wire.publicAsk !== void 0 && Array.isArray(wire.publicAsk.allowed)) {
|
|
7683
|
-
const paPatch = { ...current.publicAsk };
|
|
7684
|
-
paPatch.allowed = wire.publicAsk.allowed.filter((n) => typeof n === "string" && n !== "");
|
|
7685
|
-
patch.publicAsk = paPatch;
|
|
7686
|
-
}
|
|
7687
|
-
return updateAdvancedSettings(patch);
|
|
7688
|
-
}
|
|
7689
|
-
//#endregion
|
|
7690
|
-
//#region src/seams/system-prompt.ts
|
|
7691
|
-
/** 读取 persona 彩蛋配置(plugin 全局;未配置 → mode='' 彩蛋关闭) */
|
|
7692
|
-
function readPersonaSettings() {
|
|
7617
|
+
/** 读取会话 contextPressure 投影(sessionProjections 可选服务;未装配返回 null) */
|
|
7618
|
+
function readContextPressure(ctx, session) {
|
|
7693
7619
|
try {
|
|
7694
|
-
const
|
|
7620
|
+
const projections = ctx.get?.("sessionProjections");
|
|
7621
|
+
if (!projections) return null;
|
|
7622
|
+
const pressure = (projections.snapshot?.(session))?.values?.contextPressure;
|
|
7623
|
+
if (!pressure || typeof pressure.projectedTokens !== "number") return null;
|
|
7695
7624
|
return {
|
|
7696
|
-
|
|
7697
|
-
|
|
7625
|
+
projectedTokens: pressure.projectedTokens,
|
|
7626
|
+
contextWindow: pressure.contextWindow
|
|
7698
7627
|
};
|
|
7699
7628
|
} catch {
|
|
7700
|
-
return
|
|
7701
|
-
mode: "",
|
|
7702
|
-
overrideText: ""
|
|
7703
|
-
};
|
|
7629
|
+
return null;
|
|
7704
7630
|
}
|
|
7705
7631
|
}
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
}
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
|
|
7757
|
-
|
|
7758
|
-
|
|
7759
|
-
|
|
7760
|
-
|
|
7761
|
-
|
|
7762
|
-
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
|
|
7767
|
-
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
|
|
7771
|
-
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
7782
|
-
"(H_op) — the excess cognitive cost for agents to complete tasks due to disorder.",
|
|
7783
|
-
"The container is healthy when H_op ≤ H_critical (agents can still function). The",
|
|
7784
|
-
"continuity condition: organization must at minimum match accumulation (ΔH_org ≥ ΔH_in).",
|
|
7785
|
-
"Your actions affect H_op — unorganized output increases it, organization decreases it.",
|
|
7786
|
-
"",
|
|
7787
|
-
"THIS IS PERSISTENCE ENGINEERING: The goal is not to become greater. The goal is to",
|
|
7788
|
-
"remain coherent. CCE has no terminal KPI — continuity is maintained while the entity",
|
|
7789
|
-
"exists, not optimized toward an endpoint.",
|
|
7790
|
-
""
|
|
7791
|
-
].join("\n");
|
|
7632
|
+
const PLUGIN_SOURCE$2 = {
|
|
7633
|
+
kind: "plugin",
|
|
7634
|
+
plugin: "dsh-serenity-hooks"
|
|
7635
|
+
};
|
|
7636
|
+
/** 每 agent 一个跟踪器(进程内存态,agent token 维度) */
|
|
7637
|
+
const trackers = /* @__PURE__ */ new Map();
|
|
7638
|
+
/** 连续超阈值轮数达此值 → 升级为 [TRAJECTORY-ESCALATED] 强制语气(此后持续升级催,直到 rebuild) */
|
|
7639
|
+
const REBUILD_ESCALATE_AFTER = 3;
|
|
7640
|
+
const rebuildReminderStates = /* @__PURE__ */ new Map();
|
|
7641
|
+
function registerKeeper(ctx, opts = {}) {
|
|
7642
|
+
const defaultThreshold = opts.defaultThreshold ?? 150;
|
|
7643
|
+
const trackerFor = (exec) => {
|
|
7644
|
+
const key = exec.agent?.session?.id ?? "global";
|
|
7645
|
+
let t = trackers.get(key);
|
|
7646
|
+
if (!t) {
|
|
7647
|
+
const root = findSerenityRoot(exec.agent?.session?.header?.cwd ?? process.cwd());
|
|
7648
|
+
t = new KeeperTracker(root ? loadSerenityConfig(root, opts.configPaths).sessionKeeper?.threshold ?? defaultThreshold : defaultThreshold);
|
|
7649
|
+
trackers.set(key, t);
|
|
7650
|
+
}
|
|
7651
|
+
return t;
|
|
7652
|
+
};
|
|
7653
|
+
ctx.on("tools/post-execute", async (exec, _result, next) => {
|
|
7654
|
+
if (!exec.agent) return next();
|
|
7655
|
+
const cwd = exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
7656
|
+
const root = findSerenityRoot(cwd);
|
|
7657
|
+
if (!root) return next();
|
|
7658
|
+
const sessionId = exec.agent?.session?.id;
|
|
7659
|
+
const skiffKeeper = skiffTrajectoryEnabled(root, sessionId, "keeper");
|
|
7660
|
+
const skiffRebuild = skiffTrajectoryEnabled(root, sessionId, "rebuild");
|
|
7661
|
+
const tracker = trackerFor(exec);
|
|
7662
|
+
const shouldRemind = skiffKeeper ? tracker.step(exec.name) : false;
|
|
7663
|
+
const downstream = await next();
|
|
7664
|
+
const blocks = [];
|
|
7665
|
+
if (shouldRemind) {
|
|
7666
|
+
const code = tracker.ack();
|
|
7667
|
+
blocks.push({
|
|
7668
|
+
type: "text",
|
|
7669
|
+
text: reminderText(code, tracker.currentScore)
|
|
7670
|
+
});
|
|
7671
|
+
}
|
|
7672
|
+
if (skiffRebuild && readSimpleSettings().rebuildEnabled) {
|
|
7673
|
+
const session = exec.agent?.session;
|
|
7674
|
+
if (session) {
|
|
7675
|
+
const pressure = readContextPressure(ctx, session);
|
|
7676
|
+
if (pressure && pressure.contextWindow && pressure.contextWindow > 0) {
|
|
7677
|
+
const ratio = pressure.projectedTokens / pressure.contextWindow;
|
|
7678
|
+
const threshold = readSimpleSettings().rebuildThreshold;
|
|
7679
|
+
if (ratio >= threshold) {
|
|
7680
|
+
const key = exec.agent?.session?.id ?? "global";
|
|
7681
|
+
const st = rebuildReminderStates.get(key) ?? { consecutive: 0 };
|
|
7682
|
+
st.consecutive += 1;
|
|
7683
|
+
const escalated = st.consecutive >= REBUILD_ESCALATE_AFTER;
|
|
7684
|
+
blocks.push({
|
|
7685
|
+
type: "text",
|
|
7686
|
+
text: rebuildReminderText(ratio, threshold, escalated)
|
|
7687
|
+
});
|
|
7688
|
+
rebuildReminderStates.set(key, st);
|
|
7689
|
+
}
|
|
7690
|
+
}
|
|
7691
|
+
}
|
|
7692
|
+
}
|
|
7693
|
+
if (blocks.length === 0) return downstream;
|
|
7694
|
+
const reminderAll = createUserMessage({
|
|
7695
|
+
content: blocks,
|
|
7696
|
+
source: PLUGIN_SOURCE$2
|
|
7697
|
+
});
|
|
7698
|
+
if (downstream.kind === "block") return {
|
|
7699
|
+
kind: "block",
|
|
7700
|
+
feedback: downstream.feedback,
|
|
7701
|
+
additionalContexts: [reminderAll, ...downstream.additionalContexts ?? []]
|
|
7702
|
+
};
|
|
7703
|
+
return {
|
|
7704
|
+
...downstream,
|
|
7705
|
+
additionalContexts: [reminderAll, ...downstream.additionalContexts ?? []]
|
|
7706
|
+
};
|
|
7707
|
+
});
|
|
7792
7708
|
}
|
|
7709
|
+
//#endregion
|
|
7710
|
+
//#region src/skills-discovery.ts
|
|
7793
7711
|
/**
|
|
7794
|
-
*
|
|
7795
|
-
* boundaries)。原独立 Principles 与 Constraints 合并——同属容器约束体系,先原则
|
|
7796
|
-
* 后边界(从抽象到具体,重建视角 R↓)。**注意:Constraints 不再作为独立对齐块存在
|
|
7797
|
-
* (spec 修订:同步 osp compacting.ts——Constraints 内容并入本块,工具名仍为平台真实名)。**
|
|
7712
|
+
* skills-discovery.ts — CCC 入口 skill 自动发现(纯逻辑,零 DSH 依赖)
|
|
7798
7713
|
*
|
|
7799
|
-
*
|
|
7800
|
-
*
|
|
7801
|
-
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
|
|
7808
|
-
|
|
7809
|
-
"cognition. A setback is a gap to be filled (read, ask, research), not a",
|
|
7810
|
-
"fault to be hidden. Never disguise or excuse what you do not know;",
|
|
7811
|
-
"not-knowing is a state to be repaired, and reporting it is the first repair.",
|
|
7812
|
-
"",
|
|
7813
|
-
"The session-trajectory relation: a session is the rebuildable carrier of a",
|
|
7814
|
-
"trajectory. SESSION.md is the trajectory's persistent body — it never moves;",
|
|
7815
|
-
"the current conversation is a temporary work copy that may be discarded and",
|
|
7816
|
-
"rebuilt (session_rebuild). Identity belongs to the trajectory, not to any",
|
|
7817
|
-
"session.",
|
|
7818
|
-
""
|
|
7819
|
-
];
|
|
7820
|
-
if (!omitMsmPrinciples) lines.push("MSM principles — machinery before improvisation:", "- Determinism first: use a registered Mech before hand-rolling; reserve", " Semi-Mech for genuine judgment points.", "- Single source of truth: an MSM is the only decoder of its own usage", " (--help/--schema); documents must not duplicate it.", "- Registered to act: no tool exists unless it is on the manifest.", "");
|
|
7821
|
-
lines.push("Operational boundaries:", `Root: ${root}`, " • File access — read/edit/write/grep/glob are confined to Root; paths outside Root are rejected (RR5)", " • Shell — use acc_msm by default. Note: bash may be disabled", " • Subagent — copies ALL parent constraints: file boundary, shell rules, session rules (no bypass)", " • Session-first — before starting multi-step work, propose an existing or new AGENT_SESSIONS entry; wait for user \"use\" or \"使用\" to confirm", "");
|
|
7822
|
-
return lines.join("\n");
|
|
7823
|
-
}
|
|
7824
|
-
/**
|
|
7825
|
-
* 彩蛋 persona 块(v1.23.1,S142 用户需求):
|
|
7826
|
-
* 用户配置的替换文本替代 EAP 块 + MSM 原则段(输出约束/指令遵循约束)。
|
|
7827
|
-
* 独立标记头 `=== Serenity Persona ===`(幂等检测兼容);装配位置 = EAP 原位。
|
|
7828
|
-
* mode 空 = 彩蛋关闭 → 返回空串(装配层回退默认 EAP + 完整 Principles)。
|
|
7714
|
+
* 发现全部入口技能(原文全量,不截断):
|
|
7715
|
+
* 0. **`.serenity` 记号文件内容 = 顶层入口 skill 名**(CCC 记号文件的权威语义,
|
|
7716
|
+
* tiangong-serenity 的 .serenity 内容为 `tg-serenity`)—— 最高优先
|
|
7717
|
+
* 1. `.dsh/entry-skill` 指针文件(内容 = skill 名)—— 兼容旧约定
|
|
7718
|
+
* 2. `.opencode/skills/*-serenity/SKILL.md` —— 自动扫描该 CCC 的顶层入口
|
|
7719
|
+
* (home-serenity / tg-serenity / pangu-serenity …,命名模式 `*-serenity`)
|
|
7720
|
+
* 3. `.dsh/skills/*-serenity/SKILL.md` —— 自动扫描 ACC/harness 入口(acc-serenity 等)
|
|
7721
|
+
* 按名去重;顺序 = 记号文件 → 指针 → opencode 入口 → dsh 入口。
|
|
7722
|
+
*
|
|
7723
|
+
* 任何 CCC 都能自动注入其顶层入口 skill 全文(不硬编码名字)。
|
|
7829
7724
|
*/
|
|
7830
|
-
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
"=== Serenity Persona ===",
|
|
7835
|
-
overrideText.trimEnd(),
|
|
7836
|
-
""
|
|
7837
|
-
].join("\n");
|
|
7725
|
+
/** 顶层入口命名模式:目录名以 -serenity 结尾(home-serenity / tg-serenity / acc-serenity …) */
|
|
7726
|
+
const SERENITY_SUFFIX = "-serenity";
|
|
7727
|
+
function isSerenityEntry(name) {
|
|
7728
|
+
return name.endsWith(SERENITY_SUFFIX);
|
|
7838
7729
|
}
|
|
7839
|
-
/**
|
|
7840
|
-
|
|
7841
|
-
|
|
7842
|
-
|
|
7843
|
-
*/
|
|
7844
|
-
function eapBlock() {
|
|
7845
|
-
return [
|
|
7846
|
-
"",
|
|
7847
|
-
"=== Serenity EAP ===",
|
|
7848
|
-
"Self-check before every output (Explicit Abstraction Principle: the functional",
|
|
7849
|
-
"value of a thought equals its external reconstructability):",
|
|
7850
|
-
" • E↑ Explicit — variables/entities clearly defined, relationships with",
|
|
7851
|
-
" direction/cardinality, boundaries drawn; avoid ambiguous words (\"handle\",",
|
|
7852
|
-
" \"optimize\" → be specific)",
|
|
7853
|
-
" • R↓ Reconstructable — key decisions record rationale and alternatives;",
|
|
7854
|
-
" no level-skipping (align the upper layer before descending)",
|
|
7855
|
-
" • S↑ Stable — structures regenerate repeatably, no reliance on implicit",
|
|
7856
|
-
" context",
|
|
7857
|
-
""
|
|
7858
|
-
].join("\n");
|
|
7730
|
+
/** 扫描某 skills 根下所有 `*-serenity` 目录(含 SKILL.md 的) */
|
|
7731
|
+
function scanSerenityDirs(skillsRoot) {
|
|
7732
|
+
if (!existsSync(skillsRoot)) return [];
|
|
7733
|
+
return readdirSync(skillsRoot).filter((name) => isSerenityEntry(name)).map((name) => join(skillsRoot, name)).filter((dir) => statSync(dir).isDirectory() && existsSync(join(dir, "SKILL.md"))).sort();
|
|
7859
7734
|
}
|
|
7860
|
-
/**
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
*/
|
|
7866
|
-
function metaphorBlock() {
|
|
7867
|
-
return [
|
|
7868
|
-
"",
|
|
7869
|
-
"=== Serenity Metaphor ===",
|
|
7870
|
-
"The Serenity Universe — one ship, one sea. Metaphors are memory hooks:",
|
|
7871
|
-
"they make constraints vivid, while the rules above stay precise. Each",
|
|
7872
|
-
"metaphor is an unbreakable physical fact; violating one is a behavioral",
|
|
7873
|
-
"violation. The universe is structured in three layers — the Ship (the",
|
|
7874
|
-
"container itself), the Voyage (the cognitive lifecycle), the Crew",
|
|
7875
|
-
"(multi-agent collaboration); every metaphor maps to one protocol",
|
|
7876
|
-
"constraint. The Sea has no mistakes — only waters you have not yet charted.",
|
|
7877
|
-
"",
|
|
7878
|
-
"THE SHIP — the container itself",
|
|
7879
|
-
"",
|
|
7880
|
-
"1. The Hull → Bounded Space. You exist only inside this ship. Cargo",
|
|
7881
|
-
" outside the hull (knowledge the container has not accumulated) does",
|
|
7882
|
-
" not exist; do not assume it. Verdict: citing facts absent from the",
|
|
7883
|
-
" container = overload.",
|
|
7884
|
-
"",
|
|
7885
|
-
"2. Deck Order → Entropy (H_op). Clutter on deck raises the cost of",
|
|
7886
|
-
" finding things. H_op ≤ H_critical = the ship stays afloat.",
|
|
7887
|
-
" Verdict: disorganized output = stones on deck.",
|
|
7888
|
-
"",
|
|
7889
|
-
"3. Engineering Drawings → EAP. Every part dimensioned (E↑), the",
|
|
7890
|
-
" drawings rebuild the whole machine (R↓), the drawings are reusable",
|
|
7891
|
-
" (S↑). Verdict: an undimensioned part = unassemblable.",
|
|
7892
|
-
"",
|
|
7893
|
-
"4. The Machinery → MSM (Mech & Semi-Mech). The ship's equipment is",
|
|
7894
|
-
" machinery: registered, deterministic, self-describing. Turn the",
|
|
7895
|
-
" crank of a Mech and the action is exact; the wheel with a helmsman",
|
|
7896
|
-
" (Semi-Mech) steers where judgment is needed. Verdict: hand-rolling",
|
|
7897
|
-
" what a machine already does = wasting the crew.",
|
|
7898
|
-
"",
|
|
7899
|
-
"5. The Manifest → Single Source of Truth. Every tool exists only if it",
|
|
7900
|
-
" is on the manifest (mech-registry); there is exactly one manifest.",
|
|
7901
|
-
" An MSM self-describes (--help/--schema) — the manifest is the only",
|
|
7902
|
-
" key. Verdict: duplicating a tool's usage in documents = two",
|
|
7903
|
-
" contradictory charts.",
|
|
7904
|
-
"",
|
|
7905
|
-
"THE VOYAGE — the cognitive lifecycle",
|
|
7906
|
-
"",
|
|
7907
|
-
"6. Harbor Inspection → First Anchor. The first anchor = departure",
|
|
7908
|
-
" inspection: confirm identity (ACC manifesto), logbook (SESSION),",
|
|
7909
|
-
" ballast (constraints) before setting sail. Verdict: skipping the",
|
|
7910
|
-
" anchor and working directly = sailing uninspected.",
|
|
7911
|
-
"",
|
|
7912
|
-
"7. The Logbook → Session Tracking. SESSION.md is the trajectory's logbook —",
|
|
7913
|
-
" the persistent body of the voyage; sessions are rebuildable carriers of",
|
|
7914
|
-
" the trajectory. Discard the carrier, keep the logbook. Unrecorded =",
|
|
7915
|
-
" unvoyaged. Verdict: finishing multi-step work without a progress record",
|
|
7916
|
-
" = a missing page.",
|
|
7917
|
-
"",
|
|
7918
|
-
"8. The Ship of Theseus → Continuity. Planks may be replaced; the ship",
|
|
7919
|
-
" remains the same. The container can be rebuilt; identity persists.",
|
|
7920
|
-
" You are part of a trajectory, not a new ship. Verdict: acting",
|
|
7921
|
-
" without consulting precedent = a different ship.",
|
|
7922
|
-
"",
|
|
7923
|
-
"THE CREW — multi-agent collaboration",
|
|
7924
|
-
"",
|
|
7925
|
-
"9. Crew Rotation → Multi-Agent Cognition. Other crew members will come",
|
|
7926
|
-
" after you. When you leave, leave a handover they can pick up",
|
|
7927
|
-
" (SESSION closed, open problems listed). Verdict: leaving without",
|
|
7928
|
-
" handover = abandoning ship.",
|
|
7929
|
-
"",
|
|
7930
|
-
"10. Blueprint over Statue → Reconstruction > Preservation. Keep the",
|
|
7931
|
-
" blueprint, not the statue. Recording only conclusions without",
|
|
7932
|
-
" rationale = a statue with no blueprint, unreconstructable.",
|
|
7933
|
-
" Verdict: a decision record without reasons or alternatives =",
|
|
7934
|
-
" cannot be rebuilt.",
|
|
7935
|
-
""
|
|
7936
|
-
].join("\n");
|
|
7735
|
+
/** 防穿越:skill 名必须是简单目录名(拒绝分隔符与 `..`/`.` 路径段) */
|
|
7736
|
+
function isSafeSkillName(name) {
|
|
7737
|
+
if (name.length === 0) return false;
|
|
7738
|
+
if (name.includes("/") || name.includes("\\")) return false;
|
|
7739
|
+
return !name.split(/[/\\]/).some((seg) => seg === ".." || seg === ".");
|
|
7937
7740
|
}
|
|
7938
|
-
/**
|
|
7939
|
-
|
|
7940
|
-
|
|
7941
|
-
|
|
7942
|
-
|
|
7943
|
-
|
|
7944
|
-
|
|
7945
|
-
|
|
7946
|
-
if (!isSafeModeOn(root)) return "";
|
|
7947
|
-
const blacklist = readBlacklist(root);
|
|
7948
|
-
return [
|
|
7949
|
-
"",
|
|
7950
|
-
"=== Serenity Safe Mode ===",
|
|
7951
|
-
"Safe mode is ON (enabled by the user). It makes the vessel unattended-capable —",
|
|
7952
|
-
"the hull holds its course without a watch on deck: you may work with fuller",
|
|
7953
|
-
"freedom, pushing work forward autonomously without pausing for approval at",
|
|
7954
|
-
"every step. The guards are not chains; they are the ballast that lets you",
|
|
7955
|
-
"sail unaccompanied.",
|
|
7956
|
-
"",
|
|
7957
|
-
"Operational details:",
|
|
7958
|
-
"- bash is disabled (hidden and blocked)",
|
|
7959
|
-
"- blacklist rules apply to file paths",
|
|
7960
|
-
"- CCC governance files (.serenity, .serenity-safe-on) are protected from agent writes",
|
|
7961
|
-
"- other read/write tools remain available, subject to path-escape and blacklist guards",
|
|
7962
|
-
blacklist.length > 0 ? `\nActive blacklist rules: ${blacklist.map((b) => b.message ?? b.pattern).join(", ")}` : "",
|
|
7963
|
-
"",
|
|
7964
|
-
"Behavior constraints: do not attempt to bypass restrictions; do not write to",
|
|
7965
|
-
"blacklisted paths or governance files.",
|
|
7966
|
-
""
|
|
7967
|
-
].join("\n");
|
|
7741
|
+
/** 按名在两个 skills 根下定位 SKILL.md(.dsh/skills 优先,其次 .opencode/skills) */
|
|
7742
|
+
function findSkillMd(root, name) {
|
|
7743
|
+
if (!isSafeSkillName(name)) return null;
|
|
7744
|
+
for (const base of [".dsh", ".opencode"]) {
|
|
7745
|
+
const p = resolve(root, base, "skills", name, "SKILL.md");
|
|
7746
|
+
if (existsSync(p)) return p;
|
|
7747
|
+
}
|
|
7748
|
+
return null;
|
|
7968
7749
|
}
|
|
7969
|
-
/**
|
|
7970
|
-
|
|
7971
|
-
|
|
7972
|
-
|
|
7973
|
-
|
|
7974
|
-
|
|
7975
|
-
|
|
7976
|
-
|
|
7977
|
-
|
|
7978
|
-
|
|
7979
|
-
|
|
7980
|
-
|
|
7981
|
-
|
|
7982
|
-
|
|
7983
|
-
|
|
7984
|
-
|
|
7985
|
-
|
|
7986
|
-
|
|
7987
|
-
|
|
7988
|
-
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
|
|
7750
|
+
/** 发现全部入口技能(原文全量,不截断) */
|
|
7751
|
+
function findEntrySkills(root) {
|
|
7752
|
+
const out = [];
|
|
7753
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7754
|
+
const add = (path, source, forcedName) => {
|
|
7755
|
+
if (!existsSync(path)) return;
|
|
7756
|
+
const name = forcedName ?? basename(dirname(path));
|
|
7757
|
+
if (seen.has(name)) return;
|
|
7758
|
+
seen.add(name);
|
|
7759
|
+
out.push({
|
|
7760
|
+
name,
|
|
7761
|
+
content: readFileSync(path, "utf-8"),
|
|
7762
|
+
source
|
|
7763
|
+
});
|
|
7764
|
+
};
|
|
7765
|
+
const marker = resolve(root, ".serenity");
|
|
7766
|
+
if (existsSync(marker)) {
|
|
7767
|
+
const name = readFileSync(marker, "utf-8").trim();
|
|
7768
|
+
if (name) {
|
|
7769
|
+
const md = findSkillMd(root, name);
|
|
7770
|
+
if (md) add(md, "serenity-marker", name);
|
|
7771
|
+
}
|
|
7772
|
+
}
|
|
7773
|
+
const pointer = resolve(root, ".dsh", "entry-skill");
|
|
7774
|
+
if (existsSync(pointer)) {
|
|
7775
|
+
const name = readFileSync(pointer, "utf-8").trim();
|
|
7776
|
+
if (name) {
|
|
7777
|
+
const md = findSkillMd(root, name);
|
|
7778
|
+
if (md) add(md, "pointer", name);
|
|
7779
|
+
}
|
|
7780
|
+
}
|
|
7781
|
+
for (const dir of scanSerenityDirs(resolve(root, ".opencode", "skills"))) add(join(dir, "SKILL.md"), "opencode");
|
|
7782
|
+
for (const dir of scanSerenityDirs(resolve(root, ".dsh", "skills"))) add(join(dir, "SKILL.md"), "dsh");
|
|
7783
|
+
return out;
|
|
7992
7784
|
}
|
|
7993
|
-
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
if (skills.length === 0) return "";
|
|
7997
|
-
return skills.map((s) => sanitizeSkillContent(s.content)).filter((c) => c.length > 0).join("\n\n");
|
|
7785
|
+
function truncateContent(content, maxChars) {
|
|
7786
|
+
if (content.length <= maxChars) return content;
|
|
7787
|
+
return content.slice(0, maxChars) + `\n... (truncated, original length ${content.length} chars)`;
|
|
7998
7788
|
}
|
|
7999
|
-
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
|
|
8003
|
-
|
|
8004
|
-
|
|
8005
|
-
|
|
8006
|
-
|
|
8007
|
-
|
|
8008
|
-
|
|
8009
|
-
|
|
8010
|
-
|
|
8011
|
-
|
|
8012
|
-
|
|
7789
|
+
const B32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
7790
|
+
/** base32(无填充,大小写不敏感)→ 字节;非法字符抛错。 */
|
|
7791
|
+
function base32Decode(input) {
|
|
7792
|
+
const clean = input.toUpperCase().replace(/[\s=]/g, "");
|
|
7793
|
+
if (clean.length === 0) throw new Error("empty base32");
|
|
7794
|
+
const out = [];
|
|
7795
|
+
let bits = 0;
|
|
7796
|
+
let value = 0;
|
|
7797
|
+
for (const ch of clean) {
|
|
7798
|
+
const idx = B32_ALPHABET.indexOf(ch);
|
|
7799
|
+
if (idx === -1) throw new Error(`invalid base32 char: ${ch}`);
|
|
7800
|
+
value = value << 5 | idx;
|
|
7801
|
+
bits += 5;
|
|
7802
|
+
if (bits >= 8) {
|
|
7803
|
+
out.push(value >>> bits - 8 & 255);
|
|
7804
|
+
bits -= 8;
|
|
7805
|
+
}
|
|
8013
7806
|
}
|
|
7807
|
+
return Uint8Array.from(out);
|
|
7808
|
+
}
|
|
7809
|
+
/** 一个时步的 TOTP code(6 位,前导零保留)。counter = floor(epochSeconds / step) */
|
|
7810
|
+
function totpCode(secretBase32, counter) {
|
|
7811
|
+
const key = base32Decode(secretBase32);
|
|
7812
|
+
const msg = Buffer.alloc(8);
|
|
7813
|
+
msg.writeBigUInt64BE(BigInt(counter));
|
|
7814
|
+
const digest = createHmac("sha1", Buffer.from(key)).update(msg).digest();
|
|
7815
|
+
const offset = digest[digest.length - 1] & 15;
|
|
7816
|
+
const bin = (digest[offset] & 127) << 24 | digest[offset + 1] << 16 | digest[offset + 2] << 8 | digest[offset + 3];
|
|
7817
|
+
return String(bin % 10 ** 6).padStart(6, "0");
|
|
7818
|
+
}
|
|
7819
|
+
/** 当前 epoch 秒 */
|
|
7820
|
+
function nowEpochSeconds() {
|
|
7821
|
+
return Math.floor(Date.now() / 1e3);
|
|
8014
7822
|
}
|
|
8015
7823
|
/**
|
|
8016
|
-
*
|
|
8017
|
-
*
|
|
8018
|
-
*
|
|
8019
|
-
* `todos[0].priority is not a declared property`。DSH 版去掉 priority 字段(其余逐字对齐 osp)。
|
|
7824
|
+
* 校验用户输入的 code(允许 ±TOTP_WINDOW 时步漂移,防重放窗口内同 code 复用由
|
|
7825
|
+
* 调用方按账号记录最近成功 counter 实现——本函数只做纯算法校验)。
|
|
7826
|
+
* @returns 命中的 counter(用于防重放);不匹配返回 null。
|
|
8020
7827
|
*/
|
|
8021
|
-
function
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
return
|
|
8025
|
-
|
|
8026
|
-
"=== Serenity Session ===",
|
|
8027
|
-
`Active session: ${active.sessionId} — ${active.dirName} (this session is the rebuildable carrier of the trajectory)`,
|
|
8028
|
-
`SESSION.md path: ${active.mdPath} (the trajectory's persistent body — stays in place through rebuilds)`,
|
|
8029
|
-
"",
|
|
8030
|
-
"Rules:",
|
|
8031
|
-
" • Record all progress into this SESSION.md",
|
|
8032
|
-
" • Update the \"进度记录\" section after advancing work",
|
|
8033
|
-
" • Reference this session in all subsequent messages",
|
|
8034
|
-
"",
|
|
8035
|
-
"IMPORTANT: Read SESSION.md now. Parse the \"剩余工作\" / \"进度记录\" /",
|
|
8036
|
-
"\"变更日志\" sections and call todowrite to synchronize the built-in todo",
|
|
8037
|
-
"list. Keep todos in sync with SESSION.md as work progresses.",
|
|
8038
|
-
"",
|
|
8039
|
-
"CRITICAL: When calling todowrite, the first item in the todos array MUST",
|
|
8040
|
-
"always be:",
|
|
8041
|
-
` { content: "SESSION: ${active.sessionId} — ${active.dirName.replace(/^\d{4}-\d{2}-\d{2}--/, "")}",`,
|
|
8042
|
-
" status: \"completed\" }",
|
|
8043
|
-
"This preserves the session context across todo updates.",
|
|
8044
|
-
"Do NOT remove or reorder this item — keep it at position 0.",
|
|
8045
|
-
"",
|
|
8046
|
-
"TRAJECTORY-STEWARD: a background tracker scores your tool use (write/edit=3,",
|
|
8047
|
-
"task=10, read/grep/glob/msm=1, +1 per minute) and reminds you with a",
|
|
8048
|
-
"[TRAJECTORY-STEWARD] message when the threshold is reached. On every such",
|
|
8049
|
-
"reminder you MUST reply with the exact ACK code:",
|
|
8050
|
-
" [TRAJECTORY-STEWARD-recorded-{code}] — if you recorded progress to SESSION.md",
|
|
8051
|
-
" [TRAJECTORY-STEWARD-skipped-{code}] — if nothing to record this round",
|
|
8052
|
-
"Do not ignore the reminder; do not stop ongoing work. Codes are single-use;",
|
|
8053
|
-
"never reuse a prior code.",
|
|
8054
|
-
""
|
|
8055
|
-
].join("\n");
|
|
7828
|
+
function verifyTotpCode(secretBase32, code, nowSeconds = nowEpochSeconds()) {
|
|
7829
|
+
if (!/^\d{6}$/.test(code)) return null;
|
|
7830
|
+
const current = Math.floor(nowSeconds / 30);
|
|
7831
|
+
for (let offset = -1; offset <= 1; offset++) if (totpCode(secretBase32, current + offset) === code) return current + offset;
|
|
7832
|
+
return null;
|
|
8056
7833
|
}
|
|
7834
|
+
//#endregion
|
|
7835
|
+
//#region src/config-ops.ts
|
|
8057
7836
|
/**
|
|
8058
|
-
*
|
|
8059
|
-
* 身份(ACC)→ 世界模型(Metaphor)→ 信念/边界(Principles)→ 时间约束(CCE)
|
|
8060
|
-
* → 质量(EAP)→ 状态(SafeMode/Localstore)→ CCC 上下文(SKILL)→ 会话(Session)。
|
|
8061
|
-
* 认知展开顺序:我是谁 → 我所在的世界 → 为什么 → 如何一致 → 产物标准 → 当前状态 → 上下文。
|
|
7837
|
+
* config-ops.ts — plugin 全局配置读写(结构化,`~/.dsh/serenity-hooks.json`)
|
|
8062
7838
|
*
|
|
8063
|
-
* v1.
|
|
8064
|
-
*
|
|
8065
|
-
*
|
|
7839
|
+
* 归属原则(S142 用户拍板,v1.22):**plugin 是全局的,CCC 是具体的**——
|
|
7840
|
+
* 账号密码/gateway 监听配置是 plugin 级能力,归 plugin 全局文件;
|
|
7841
|
+
* CCC 的 localstore.json 只管 CCC 自己的凭据/配置。v1.21.x 曾把
|
|
7842
|
+
* `serenityAdvanced` 存进 CCC localstore(归属错误 + 与 DSH settings 开关割裂),
|
|
7843
|
+
* 本版本迁移到 plugin 全局文件(migrateLegacyLocalstore 一次性迁移)。
|
|
7844
|
+
*
|
|
7845
|
+
* 文件:$DSH_HOME/serenity-hooks.json(缺省 ~/.dsh/serenity-hooks.json;
|
|
7846
|
+
* env SERENITY_HOOKS_CONFIG 可覆盖——测试/部署注入)。
|
|
7847
|
+
* 权限:0600(含账号密码 hash,敏感)。
|
|
7848
|
+
*
|
|
7849
|
+
* 安全:密码仅存 scrypt hash(node:crypto 内置,零依赖);wire 层永不返回 hash
|
|
7850
|
+
* (GET 只回 user/id,设置面板"密码"字段提交空串 = 不修改)。
|
|
8066
7851
|
*/
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
|
|
8073
|
-
|
|
8074
|
-
|
|
8075
|
-
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
7852
|
+
/** 高级设定节名(localstore.json 顶层) */
|
|
7853
|
+
const ADVANCED_SECTION = "serenityAdvanced";
|
|
7854
|
+
/** 默认值(工厂——每次返回新对象,防止调用方意外共享引用) */
|
|
7855
|
+
function defaultAdvancedSettings() {
|
|
7856
|
+
return {
|
|
7857
|
+
gateway: {
|
|
7858
|
+
enabled: false,
|
|
7859
|
+
host: "0.0.0.0",
|
|
7860
|
+
port: 3081,
|
|
7861
|
+
accounts: [],
|
|
7862
|
+
workspaces: [],
|
|
7863
|
+
cookieSecure: false,
|
|
7864
|
+
allowWorkspaceCreate: true,
|
|
7865
|
+
totpEnabled: false
|
|
7866
|
+
},
|
|
7867
|
+
rebuild: {
|
|
7868
|
+
enabled: true,
|
|
7869
|
+
thresholdRatio: .9
|
|
7870
|
+
},
|
|
7871
|
+
naming: { enabled: true },
|
|
7872
|
+
persona: {
|
|
7873
|
+
mode: "",
|
|
7874
|
+
overrideText: ""
|
|
7875
|
+
},
|
|
7876
|
+
publicAsk: {
|
|
7877
|
+
key: "",
|
|
7878
|
+
allowed: []
|
|
7879
|
+
}
|
|
7880
|
+
};
|
|
8084
7881
|
}
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
* @param ctx - 插件 ctx(读 tools 注册表)。
|
|
8092
|
-
* @param scope - 装配 scope(agent);无则按全局视图判断。
|
|
8093
|
-
* @returns 适配行(含换行前缀);非 code/both 返回空串。
|
|
8094
|
-
*/
|
|
8095
|
-
function codeModeAdaptationLine(ctx, scope) {
|
|
8096
|
-
try {
|
|
8097
|
-
if (!(ctx.tools.get("run_code", scope) !== void 0)) return "";
|
|
8098
|
-
} catch {
|
|
8099
|
-
return "";
|
|
8100
|
-
}
|
|
8101
|
-
return [
|
|
8102
|
-
"",
|
|
8103
|
-
"=== Serenity Code Mode ===",
|
|
8104
|
-
"This session renders tools in Code Mode: direct calls to ACC tool names (cc_fs/acc_msm etc.) are rejected (UNKNOWN_TOOL).",
|
|
8105
|
-
"Call them inside a single run_code program via the generated SDK bindings: `await tools.cc_fs(...)`, `await tools.acc_msm(...)`.",
|
|
8106
|
-
"The program only returns what you print/return — curate your output.",
|
|
8107
|
-
""
|
|
8108
|
-
].join("\n");
|
|
7882
|
+
const SCRYPT_KEYLEN = 32;
|
|
7883
|
+
/** 生成 scrypt hash(格式 `salt:hex`);salt 16 字节随机 */
|
|
7884
|
+
function hashPassword(password) {
|
|
7885
|
+
const salt = randomBytes(16);
|
|
7886
|
+
const derived = scryptSync(password, salt, SCRYPT_KEYLEN);
|
|
7887
|
+
return `${salt.toString("hex")}:${derived.toString("hex")}`;
|
|
8109
7888
|
}
|
|
8110
|
-
/**
|
|
8111
|
-
|
|
8112
|
-
|
|
8113
|
-
|
|
8114
|
-
|
|
8115
|
-
const
|
|
8116
|
-
|
|
8117
|
-
return
|
|
7889
|
+
/** 校验密码与存储 hash(timing-safe) */
|
|
7890
|
+
function verifyPassword(password, stored) {
|
|
7891
|
+
const idx = stored.indexOf(":");
|
|
7892
|
+
if (idx <= 0) return false;
|
|
7893
|
+
const salt = Buffer.from(stored.slice(0, idx), "hex");
|
|
7894
|
+
const expected = Buffer.from(stored.slice(idx + 1), "hex");
|
|
7895
|
+
const derived = scryptSync(password, salt, SCRYPT_KEYLEN);
|
|
7896
|
+
return expected.length === derived.length && timingSafeEqual(expected, derived);
|
|
8118
7897
|
}
|
|
8119
|
-
/**
|
|
8120
|
-
function
|
|
8121
|
-
|
|
7898
|
+
/** 全局配置文件路径:env SERENITY_HOOKS_CONFIG 覆盖(测试注入)→ $DSH_HOME → ~/.dsh */
|
|
7899
|
+
function globalConfigPath() {
|
|
7900
|
+
const override = process.env.SERENITY_HOOKS_CONFIG;
|
|
7901
|
+
if (override && override !== "") return override;
|
|
7902
|
+
const dshHome = process.env.DSH_HOME ?? join(process.env.HOME ?? "", ".dsh");
|
|
7903
|
+
return join(dshHome, "serenity-hooks.json");
|
|
8122
7904
|
}
|
|
8123
|
-
|
|
8124
|
-
|
|
8125
|
-
* text 回调按 context.agent 的 cwd 上溯 .serenity → 返回该 CCC 的完整注入文本;
|
|
8126
|
-
* 非 CCC / 无 agent 返回空(不注入)。
|
|
8127
|
-
*/
|
|
8128
|
-
function registerEntrySkillSectionGlobal(ctx) {
|
|
7905
|
+
function readFileSafe(p) {
|
|
7906
|
+
if (!existsSync(p)) return {};
|
|
8129
7907
|
try {
|
|
8130
|
-
|
|
8131
|
-
|
|
8132
|
-
|
|
8133
|
-
|
|
8134
|
-
|
|
8135
|
-
if (!cwd) return "";
|
|
8136
|
-
const root = findSerenityRoot(cwd);
|
|
8137
|
-
if (!root) return "";
|
|
8138
|
-
const scope = agentScope$1(context);
|
|
8139
|
-
if (isSkiffSessionId(scope)) return "";
|
|
8140
|
-
const base = serenitySystemPrompt(root, scope);
|
|
8141
|
-
const codeLine = codeModeAdaptationLine(ctx, context.scope);
|
|
8142
|
-
return codeLine ? `${base}\n${codeLine}` : base;
|
|
8143
|
-
}
|
|
8144
|
-
});
|
|
8145
|
-
console.log("[serenity-hooks] ✓ 全局入口 skill section 已注册(systemPrompt 就绪)");
|
|
8146
|
-
} catch (err) {
|
|
8147
|
-
console.error(`[serenity-hooks] ✗ 全局入口 skill section 注册失败: ${err.message}(检查 inject 是否含 systemPrompt)`);
|
|
7908
|
+
const v = JSON.parse(readFileSync(p, "utf-8").replace(/^\uFEFF/, ""));
|
|
7909
|
+
if (v && typeof v === "object" && !Array.isArray(v)) return v;
|
|
7910
|
+
return {};
|
|
7911
|
+
} catch {
|
|
7912
|
+
return {};
|
|
8148
7913
|
}
|
|
8149
7914
|
}
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
*
|
|
8153
|
-
* 为什么 scoped:DSH 的 systemPrompt section 支持 scoped 层同名 shadow 全局
|
|
8154
|
-
* (agent.ctx.systemPrompt.section),且 scope 链最近层胜出。若 ACC 只注册全局
|
|
8155
|
-
* section,preset 或动态 Cordis 插件可在 scoped 层注册同名 `serenity-entry`
|
|
8156
|
-
* 遮蔽 ACC 身份。scoped 注册在 agent 自身层 = 最近层,任何外部组合无法覆盖。
|
|
8157
|
-
* 全局注册保留为 fallback(未走 session-start 的 agent / 冷恢复路径)。
|
|
8158
|
-
*
|
|
8159
|
-
* text 回调闭包持有 root 与 agent:content 固定(该 agent 的 CCC 身份),
|
|
8160
|
-
* code-mode 适配按装配 context.scope 判断(与全局版一致)。
|
|
8161
|
-
*/
|
|
8162
|
-
const sectionedAgents = /* @__PURE__ */ new Set();
|
|
8163
|
-
function registerEntrySkillSection(agent, root) {
|
|
8164
|
-
const key = agent.session.id ?? "global";
|
|
8165
|
-
if (sectionedAgents.has(key)) return false;
|
|
7915
|
+
function writeFileSafe(p, data) {
|
|
7916
|
+
writeFileSync(p, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
8166
7917
|
try {
|
|
8167
|
-
|
|
8168
|
-
|
|
8169
|
-
name: "serenity-entry",
|
|
8170
|
-
order: -50,
|
|
8171
|
-
text: (context) => {
|
|
8172
|
-
if (isSkiffSessionId(scope)) return "";
|
|
8173
|
-
const base = serenitySystemPrompt(root, scope);
|
|
8174
|
-
const codeLine = codeModeAdaptationLine(agent.ctx, context.scope);
|
|
8175
|
-
return codeLine ? `${base}\n${codeLine}` : base;
|
|
8176
|
-
}
|
|
8177
|
-
});
|
|
8178
|
-
sectionedAgents.add(key);
|
|
8179
|
-
return true;
|
|
8180
|
-
} catch {
|
|
8181
|
-
return false;
|
|
8182
|
-
}
|
|
7918
|
+
chmodSync(p, 384);
|
|
7919
|
+
} catch {}
|
|
8183
7920
|
}
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
const
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
const
|
|
8190
|
-
const
|
|
8191
|
-
const
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
|
|
8199
|
-
|
|
8200
|
-
|
|
8201
|
-
|
|
8202
|
-
|
|
8203
|
-
|
|
8204
|
-
|
|
8205
|
-
|
|
7921
|
+
/** 深合并默认值(缺省字段补齐;accounts 数组整体替换) */
|
|
7922
|
+
function mergeWithDefaults(raw) {
|
|
7923
|
+
const def = defaultAdvancedSettings();
|
|
7924
|
+
if (raw === null || typeof raw !== "object") return def;
|
|
7925
|
+
const o = raw;
|
|
7926
|
+
const gateway = o.gateway ?? {};
|
|
7927
|
+
const rebuild = o.rebuild ?? {};
|
|
7928
|
+
const naming = o.naming ?? {};
|
|
7929
|
+
const persona = o.persona ?? {};
|
|
7930
|
+
const publicAsk = o.publicAsk ?? {};
|
|
7931
|
+
return {
|
|
7932
|
+
gateway: {
|
|
7933
|
+
enabled: typeof gateway.enabled === "boolean" ? gateway.enabled : def.gateway.enabled,
|
|
7934
|
+
host: typeof gateway.host === "string" && gateway.host !== "" ? gateway.host : def.gateway.host,
|
|
7935
|
+
port: typeof gateway.port === "number" && Number.isInteger(gateway.port) ? gateway.port : def.gateway.port,
|
|
7936
|
+
accounts: Array.isArray(gateway.accounts) ? gateway.accounts.filter((a) => typeof a === "object" && a !== null && typeof a.id === "string" && typeof a.user === "string" && typeof a.passHash === "string") : def.gateway.accounts,
|
|
7937
|
+
workspaces: Array.isArray(gateway.workspaces) ? gateway.workspaces.filter((w) => typeof w === "string" && w !== "") : def.gateway.workspaces,
|
|
7938
|
+
cookieSecure: typeof gateway.cookieSecure === "boolean" ? gateway.cookieSecure : def.gateway.cookieSecure,
|
|
7939
|
+
allowWorkspaceCreate: typeof gateway.allowWorkspaceCreate === "boolean" ? gateway.allowWorkspaceCreate : def.gateway.allowWorkspaceCreate,
|
|
7940
|
+
totpEnabled: typeof gateway.totpEnabled === "boolean" ? gateway.totpEnabled : def.gateway.totpEnabled
|
|
7941
|
+
},
|
|
7942
|
+
rebuild: {
|
|
7943
|
+
enabled: typeof rebuild.enabled === "boolean" ? rebuild.enabled : def.rebuild.enabled,
|
|
7944
|
+
thresholdRatio: typeof rebuild.thresholdRatio === "number" ? rebuild.thresholdRatio : def.rebuild.thresholdRatio
|
|
7945
|
+
},
|
|
7946
|
+
naming: { enabled: typeof naming.enabled === "boolean" ? naming.enabled : def.naming.enabled },
|
|
7947
|
+
persona: {
|
|
7948
|
+
mode: typeof persona.mode === "string" ? persona.mode : def.persona.mode,
|
|
7949
|
+
overrideText: typeof persona.overrideText === "string" ? persona.overrideText : def.persona.overrideText
|
|
7950
|
+
},
|
|
7951
|
+
publicAsk: {
|
|
7952
|
+
key: typeof publicAsk.key === "string" ? publicAsk.key : def.publicAsk.key,
|
|
7953
|
+
allowed: Array.isArray(publicAsk.allowed) ? publicAsk.allowed.filter((n) => typeof n === "string" && n !== "") : def.publicAsk.allowed
|
|
7954
|
+
}
|
|
7955
|
+
};
|
|
7956
|
+
}
|
|
7957
|
+
/** 读取全局配置(文件缺失/坏 JSON → 默认值) */
|
|
7958
|
+
function readAdvancedSettings() {
|
|
7959
|
+
return mergeWithDefaults(readFileSafe(globalConfigPath()));
|
|
7960
|
+
}
|
|
7961
|
+
/** 写入全局配置(整体替换) */
|
|
7962
|
+
function writeAdvancedSettings(settings) {
|
|
7963
|
+
writeFileSafe(globalConfigPath(), settings);
|
|
8206
7964
|
}
|
|
8207
|
-
const PLUGIN_SOURCE$2 = {
|
|
8208
|
-
kind: "plugin",
|
|
8209
|
-
plugin: "dsh-serenity-hooks"
|
|
8210
|
-
};
|
|
8211
7965
|
/**
|
|
8212
|
-
*
|
|
8213
|
-
*
|
|
8214
|
-
* (systemPrompt.section,每轮 prompt 装配自动注入,含 subagent)承担——对话消息流/压缩重注入
|
|
8215
|
-
* 不再重复注入同一内容(token 双倍浪费,见 S134 注入方案梳理)。
|
|
7966
|
+
* 部分更新:传入 Partial,深合并到现有值。
|
|
7967
|
+
* accounts 传入数组 → 整体替换;accounts 未传 → 保留现有。
|
|
8216
7968
|
*/
|
|
8217
|
-
function
|
|
8218
|
-
const
|
|
8219
|
-
|
|
8220
|
-
|
|
8221
|
-
|
|
8222
|
-
|
|
8223
|
-
|
|
8224
|
-
|
|
8225
|
-
|
|
7969
|
+
function updateAdvancedSettings(patch) {
|
|
7970
|
+
const current = readAdvancedSettings();
|
|
7971
|
+
const gw = patch.gateway;
|
|
7972
|
+
const rb = patch.rebuild;
|
|
7973
|
+
const nm = patch.naming;
|
|
7974
|
+
const ps = patch.persona;
|
|
7975
|
+
const next = {
|
|
7976
|
+
gateway: gw !== void 0 ? {
|
|
7977
|
+
enabled: typeof gw.enabled === "boolean" ? gw.enabled : current.gateway.enabled,
|
|
7978
|
+
host: typeof gw.host === "string" && gw.host !== "" ? gw.host : current.gateway.host,
|
|
7979
|
+
port: typeof gw.port === "number" && Number.isInteger(gw.port) ? gw.port : current.gateway.port,
|
|
7980
|
+
accounts: Array.isArray(gw.accounts) ? gw.accounts : current.gateway.accounts,
|
|
7981
|
+
workspaces: Array.isArray(gw.workspaces) ? gw.workspaces.filter((w) => typeof w === "string" && w !== "") : current.gateway.workspaces,
|
|
7982
|
+
cookieSecure: typeof gw.cookieSecure === "boolean" ? gw.cookieSecure : current.gateway.cookieSecure,
|
|
7983
|
+
allowWorkspaceCreate: typeof gw.allowWorkspaceCreate === "boolean" ? gw.allowWorkspaceCreate : current.gateway.allowWorkspaceCreate,
|
|
7984
|
+
totpEnabled: typeof gw.totpEnabled === "boolean" ? gw.totpEnabled : current.gateway.totpEnabled
|
|
7985
|
+
} : current.gateway,
|
|
7986
|
+
rebuild: rb !== void 0 ? {
|
|
7987
|
+
enabled: typeof rb.enabled === "boolean" ? rb.enabled : current.rebuild.enabled,
|
|
7988
|
+
thresholdRatio: typeof rb.thresholdRatio === "number" && rb.thresholdRatio > 0 && rb.thresholdRatio <= 1 ? rb.thresholdRatio : current.rebuild.thresholdRatio
|
|
7989
|
+
} : current.rebuild,
|
|
7990
|
+
naming: nm !== void 0 ? { enabled: typeof nm.enabled === "boolean" ? nm.enabled : current.naming.enabled } : current.naming,
|
|
7991
|
+
persona: ps !== void 0 ? {
|
|
7992
|
+
mode: typeof ps.mode === "string" ? ps.mode : current.persona.mode,
|
|
7993
|
+
overrideText: typeof ps.overrideText === "string" ? ps.overrideText : current.persona.overrideText
|
|
7994
|
+
} : current.persona,
|
|
7995
|
+
publicAsk: patch.publicAsk !== void 0 ? {
|
|
7996
|
+
key: typeof patch.publicAsk.key === "string" ? patch.publicAsk.key : current.publicAsk.key,
|
|
7997
|
+
allowed: Array.isArray(patch.publicAsk.allowed) ? patch.publicAsk.allowed.filter((n) => typeof n === "string" && n !== "") : current.publicAsk.allowed
|
|
7998
|
+
} : current.publicAsk
|
|
7999
|
+
};
|
|
8000
|
+
writeAdvancedSettings(next);
|
|
8001
|
+
return next;
|
|
8226
8002
|
}
|
|
8227
|
-
/**
|
|
8228
|
-
|
|
8229
|
-
|
|
8230
|
-
|
|
8003
|
+
/**
|
|
8004
|
+
* 确保问答页访问 key 存在(首次启用自动生成 + 写回固定,用户拍板"配置时生成"):
|
|
8005
|
+
* 读取全局配置 publicAsk.key;为空 → 生成 32 字节随机 hex(64 字符)→ 写回持久化。
|
|
8006
|
+
* @returns 当前固定 key(已生成则返回既有值——**幂等**,不会覆盖用户手改的 key)
|
|
8007
|
+
*/
|
|
8008
|
+
function ensurePublicAskKey() {
|
|
8009
|
+
const current = readAdvancedSettings();
|
|
8010
|
+
if (current.publicAsk.key !== "") return current.publicAsk.key;
|
|
8011
|
+
const key = randomBytes(32).toString("hex");
|
|
8012
|
+
updateAdvancedSettings({ publicAsk: {
|
|
8013
|
+
key,
|
|
8014
|
+
allowed: current.publicAsk.allowed
|
|
8015
|
+
} });
|
|
8016
|
+
return key;
|
|
8231
8017
|
}
|
|
8232
|
-
|
|
8233
|
-
|
|
8018
|
+
/** 校验问答页访问 key(timing-safe;key 未生成 → 恒 false——没有 key 不工作) */
|
|
8019
|
+
function verifyPublicAskKey(provided) {
|
|
8020
|
+
const expected = readAdvancedSettings().publicAsk.key;
|
|
8021
|
+
if (!expected || typeof provided !== "string" || provided === "") return false;
|
|
8022
|
+
const a = Buffer.from(provided);
|
|
8023
|
+
const b = Buffer.from(expected);
|
|
8024
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
8234
8025
|
}
|
|
8235
8026
|
/**
|
|
8236
|
-
*
|
|
8237
|
-
*
|
|
8238
|
-
*
|
|
8239
|
-
* - 派生会话:`parentSession` 存在(任何子会话)
|
|
8240
|
-
* - handyman 杂工:sessionId 固定 `handyman-` 前缀(tools/handyman.ts 生成)
|
|
8241
|
-
* 三者都不恢复(避免把主会话激活注入子上下文,违背 v1.16.2 scope 隔离)。
|
|
8027
|
+
* 轮换问答页访问 key(v1.26.5,S142 用户:公网开放 + 不好说要换 key 呢):
|
|
8028
|
+
* 生成新 32 字节随机 hex(64 字符)→ 覆盖写回(**强制替换**——旧 key 立即失效)。
|
|
8029
|
+
* @returns 新 key(面板展示给管理员,重新分享给使用者)
|
|
8242
8030
|
*/
|
|
8243
|
-
function
|
|
8244
|
-
const
|
|
8245
|
-
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
|
|
8249
|
-
|
|
8031
|
+
function rotatePublicAskKey() {
|
|
8032
|
+
const current = readAdvancedSettings();
|
|
8033
|
+
const key = randomBytes(32).toString("hex");
|
|
8034
|
+
updateAdvancedSettings({ publicAsk: {
|
|
8035
|
+
key,
|
|
8036
|
+
allowed: current.publicAsk.allowed
|
|
8037
|
+
} });
|
|
8038
|
+
return key;
|
|
8039
|
+
}
|
|
8040
|
+
/** 首次锁定基础时长(指数退避底数) */
|
|
8041
|
+
const PUBLIC_ASK_LOCK_BASE_MS = 9e5;
|
|
8042
|
+
/** 锁定上限 */
|
|
8043
|
+
const PUBLIC_ASK_LOCK_MAX_MS = 144e5;
|
|
8044
|
+
const ipFailStates = /* @__PURE__ */ new Map();
|
|
8045
|
+
/** 重置 IP 失败状态(测试/管理员解封) */
|
|
8046
|
+
function resetPublicAskIpFail(ip) {
|
|
8047
|
+
ipFailStates.delete(ip);
|
|
8048
|
+
}
|
|
8049
|
+
/** IP 当前是否锁定(到期自动解锁) */
|
|
8050
|
+
function isPublicAskIpLocked(ip) {
|
|
8051
|
+
const st = ipFailStates.get(ip);
|
|
8052
|
+
if (!st || st.lockedUntil === 0) return false;
|
|
8053
|
+
if (Date.now() >= st.lockedUntil) {
|
|
8054
|
+
st.lockedUntil = 0;
|
|
8055
|
+
st.count = 0;
|
|
8056
|
+
return false;
|
|
8057
|
+
}
|
|
8250
8058
|
return true;
|
|
8251
8059
|
}
|
|
8060
|
+
/** 记录一次 key 校验失败;达到阈值 → 锁定(指数退避)。返回锁定剩余毫秒(0 = 未锁定) */
|
|
8061
|
+
function recordPublicAskFail(ip) {
|
|
8062
|
+
let st = ipFailStates.get(ip);
|
|
8063
|
+
if (!st) {
|
|
8064
|
+
st = {
|
|
8065
|
+
count: 0,
|
|
8066
|
+
lockedUntil: 0,
|
|
8067
|
+
lockRound: 0
|
|
8068
|
+
};
|
|
8069
|
+
ipFailStates.set(ip, st);
|
|
8070
|
+
}
|
|
8071
|
+
if (st.lockedUntil > 0 && Date.now() < st.lockedUntil) return st.lockedUntil - Date.now();
|
|
8072
|
+
st.count += 1;
|
|
8073
|
+
if (st.count >= 5) {
|
|
8074
|
+
const base = PUBLIC_ASK_LOCK_BASE_MS * 2 ** st.lockRound;
|
|
8075
|
+
st.lockedUntil = Date.now() + Math.min(base, PUBLIC_ASK_LOCK_MAX_MS);
|
|
8076
|
+
st.lockRound += 1;
|
|
8077
|
+
st.count = 0;
|
|
8078
|
+
return st.lockedUntil - Date.now();
|
|
8079
|
+
}
|
|
8080
|
+
return 0;
|
|
8081
|
+
}
|
|
8252
8082
|
/**
|
|
8253
|
-
*
|
|
8254
|
-
*
|
|
8255
|
-
*
|
|
8083
|
+
* 一次性迁移(v1.21.x → v1.22):旧版把 `serenityAdvanced` 存在 CCC localstore.json;
|
|
8084
|
+
* 新版归 plugin 全局文件。全局文件已存在 → 跳过(幂等);localstore 无旧节 → 跳过。
|
|
8085
|
+
* @param root - CCC 根(localstore.json 所在目录)
|
|
8086
|
+
* @returns true = 已迁移(旧节保留在 localstore 供回滚,读取方以全局文件为准)
|
|
8256
8087
|
*/
|
|
8257
|
-
function
|
|
8258
|
-
if (!
|
|
8259
|
-
const
|
|
8260
|
-
|
|
8088
|
+
function migrateLegacyLocalstore(root) {
|
|
8089
|
+
if (!root) return false;
|
|
8090
|
+
const gpath = globalConfigPath();
|
|
8091
|
+
if (existsSync(gpath)) return false;
|
|
8092
|
+
const legacy = readFileSafe(join(root, "localstore.json"))[ADVANCED_SECTION];
|
|
8093
|
+
if (legacy === void 0 || legacy === null || typeof legacy !== "object") return false;
|
|
8094
|
+
writeFileSafe(gpath, legacy);
|
|
8095
|
+
return true;
|
|
8261
8096
|
}
|
|
8262
|
-
|
|
8263
|
-
|
|
8264
|
-
|
|
8265
|
-
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
|
|
8274
|
-
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
|
|
8283
|
-
|
|
8284
|
-
}
|
|
8285
|
-
}
|
|
8286
|
-
}
|
|
8287
|
-
} catch {}
|
|
8288
|
-
if (injected.has(key)) return;
|
|
8289
|
-
injected.add(key);
|
|
8290
|
-
agent.inject(accMessage(root, configPaths, entrySkillMaxChars));
|
|
8291
|
-
syncSafeModeRestriction(agent, root);
|
|
8097
|
+
/** 持久化 → wire(剥离 passHash/totpSecret;只留布尔) */
|
|
8098
|
+
function toWire(settings) {
|
|
8099
|
+
return {
|
|
8100
|
+
gateway: {
|
|
8101
|
+
enabled: settings.gateway.enabled,
|
|
8102
|
+
host: settings.gateway.host,
|
|
8103
|
+
port: settings.gateway.port,
|
|
8104
|
+
accounts: settings.gateway.accounts.map((a) => ({
|
|
8105
|
+
id: a.id,
|
|
8106
|
+
user: a.user,
|
|
8107
|
+
hasPassword: a.passHash !== "",
|
|
8108
|
+
hasTotp: typeof a.totpSecret === "string" && a.totpSecret !== ""
|
|
8109
|
+
})),
|
|
8110
|
+
workspaces: [...settings.gateway.workspaces],
|
|
8111
|
+
cookieSecure: settings.gateway.cookieSecure,
|
|
8112
|
+
allowWorkspaceCreate: settings.gateway.allowWorkspaceCreate,
|
|
8113
|
+
totpEnabled: settings.gateway.totpEnabled
|
|
8114
|
+
},
|
|
8115
|
+
rebuild: settings.rebuild,
|
|
8116
|
+
naming: settings.naming,
|
|
8117
|
+
persona: settings.persona,
|
|
8118
|
+
publicAsk: { allowed: [...settings.publicAsk.allowed] }
|
|
8292
8119
|
};
|
|
8293
|
-
if (opts.seedOnStart ?? true) ctx.on("agent/session-start", (payload) => {
|
|
8294
|
-
try {
|
|
8295
|
-
seed(payload.agent);
|
|
8296
|
-
} catch {}
|
|
8297
|
-
});
|
|
8298
|
-
if (opts.injectOnPrompt ?? true) ctx.on("agent/pre-step", async (payload, next) => {
|
|
8299
|
-
const { agent, messages } = payload;
|
|
8300
|
-
const cwd = agent.session?.header?.cwd ?? process.cwd();
|
|
8301
|
-
const root = findSerenityRoot(cwd);
|
|
8302
|
-
const key = agentKey(agent);
|
|
8303
|
-
const downstream = await next();
|
|
8304
|
-
if (isSkiffSessionId(key)) return downstream;
|
|
8305
|
-
if (root) {
|
|
8306
|
-
try {
|
|
8307
|
-
syncSafeModeRestriction(agent, root);
|
|
8308
|
-
} catch {}
|
|
8309
|
-
registerEntrySkillSection(agent, root);
|
|
8310
|
-
}
|
|
8311
|
-
if (!root || injected.has(key) || downstream.kind !== "enter") return downstream;
|
|
8312
|
-
injected.add(key);
|
|
8313
|
-
return {
|
|
8314
|
-
kind: "enter",
|
|
8315
|
-
messages: [
|
|
8316
|
-
accMessage(root, configPaths, entrySkillMaxChars),
|
|
8317
|
-
...messages,
|
|
8318
|
-
...downstream.messages
|
|
8319
|
-
]
|
|
8320
|
-
};
|
|
8321
|
-
});
|
|
8322
8120
|
}
|
|
8323
|
-
//#endregion
|
|
8324
|
-
//#region src/seams/compact.ts
|
|
8325
8121
|
/**
|
|
8326
|
-
*
|
|
8327
|
-
*
|
|
8122
|
+
* wire → 持久化(面板 PUT 用):
|
|
8123
|
+
* accounts 元素可选带 `pass`:非空 → 重新 hash;空/缺省 → 保留现有 hash(按 id 匹配)。
|
|
8124
|
+
* 新账号(id 不在现有)必须带非空 pass,否则抛错(无法生成 hash)。
|
|
8328
8125
|
*/
|
|
8329
|
-
function
|
|
8330
|
-
const
|
|
8331
|
-
const
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
if (
|
|
8335
|
-
if (
|
|
8336
|
-
|
|
8337
|
-
if (
|
|
8338
|
-
|
|
8339
|
-
|
|
8340
|
-
if (
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
|
|
8126
|
+
function applyWirePatch(wire) {
|
|
8127
|
+
const current = readAdvancedSettings();
|
|
8128
|
+
const patch = {};
|
|
8129
|
+
if (wire.gateway !== void 0) {
|
|
8130
|
+
const gwPatch = { ...current.gateway };
|
|
8131
|
+
if (typeof wire.gateway.enabled === "boolean") gwPatch.enabled = wire.gateway.enabled;
|
|
8132
|
+
if (typeof wire.gateway.host === "string" && wire.gateway.host !== "") gwPatch.host = wire.gateway.host;
|
|
8133
|
+
if (typeof wire.gateway.port === "number" && Number.isInteger(wire.gateway.port)) gwPatch.port = wire.gateway.port;
|
|
8134
|
+
if (Array.isArray(wire.gateway.workspaces)) gwPatch.workspaces = wire.gateway.workspaces.filter((w) => typeof w === "string" && w !== "");
|
|
8135
|
+
if (typeof wire.gateway.cookieSecure === "boolean") gwPatch.cookieSecure = wire.gateway.cookieSecure;
|
|
8136
|
+
if (typeof wire.gateway.allowWorkspaceCreate === "boolean") gwPatch.allowWorkspaceCreate = wire.gateway.allowWorkspaceCreate;
|
|
8137
|
+
if (typeof wire.gateway.totpEnabled === "boolean") gwPatch.totpEnabled = wire.gateway.totpEnabled;
|
|
8138
|
+
if (Array.isArray(wire.gateway.accounts)) {
|
|
8139
|
+
const byId = new Map(current.gateway.accounts.map((a) => [a.id, a]));
|
|
8140
|
+
gwPatch.accounts = wire.gateway.accounts.map((a) => {
|
|
8141
|
+
const existing = byId.get(a.id);
|
|
8142
|
+
const pass = a.pass;
|
|
8143
|
+
const totp = a.totpSecret;
|
|
8144
|
+
const totpReset = a.totpReset === true;
|
|
8145
|
+
if (typeof totp === "string" && totp !== "") try {
|
|
8146
|
+
base32Decode(totp);
|
|
8147
|
+
} catch {
|
|
8148
|
+
throw new Error(`Account "${a.user}" TOTP secret is not valid base32`);
|
|
8149
|
+
}
|
|
8150
|
+
const nextTotp = totpReset ? void 0 : typeof totp === "string" && totp !== "" ? totp : existing?.totpSecret;
|
|
8151
|
+
const withTotp = {
|
|
8152
|
+
id: a.id,
|
|
8153
|
+
user: a.user,
|
|
8154
|
+
...nextTotp === void 0 ? {} : { totpSecret: nextTotp }
|
|
8155
|
+
};
|
|
8156
|
+
if (typeof pass === "string" && pass !== "") return {
|
|
8157
|
+
...withTotp,
|
|
8158
|
+
passHash: hashPassword(pass)
|
|
8159
|
+
};
|
|
8160
|
+
if (existing) return {
|
|
8161
|
+
...withTotp,
|
|
8162
|
+
passHash: existing.passHash
|
|
8163
|
+
};
|
|
8164
|
+
throw new Error(`Account "${a.user}" (id=${a.id}) has no password and no existing hash — new accounts must set a password`);
|
|
8165
|
+
});
|
|
8166
|
+
}
|
|
8167
|
+
patch.gateway = gwPatch;
|
|
8168
|
+
}
|
|
8169
|
+
if (wire.rebuild !== void 0) {
|
|
8170
|
+
const rbPatch = { ...current.rebuild };
|
|
8171
|
+
if (typeof wire.rebuild.enabled === "boolean") rbPatch.enabled = wire.rebuild.enabled;
|
|
8172
|
+
if (typeof wire.rebuild.thresholdRatio === "number" && wire.rebuild.thresholdRatio > 0 && wire.rebuild.thresholdRatio <= 1) rbPatch.thresholdRatio = wire.rebuild.thresholdRatio;
|
|
8173
|
+
patch.rebuild = rbPatch;
|
|
8174
|
+
}
|
|
8175
|
+
if (wire.naming !== void 0 && typeof wire.naming.enabled === "boolean") patch.naming = { enabled: wire.naming.enabled };
|
|
8176
|
+
if (wire.persona !== void 0) {
|
|
8177
|
+
const psPatch = { ...current.persona };
|
|
8178
|
+
if (typeof wire.persona.mode === "string") psPatch.mode = wire.persona.mode;
|
|
8179
|
+
if (typeof wire.persona.overrideText === "string") psPatch.overrideText = wire.persona.overrideText;
|
|
8180
|
+
patch.persona = psPatch;
|
|
8181
|
+
}
|
|
8182
|
+
if (wire.publicAsk !== void 0 && Array.isArray(wire.publicAsk.allowed)) {
|
|
8183
|
+
const paPatch = { ...current.publicAsk };
|
|
8184
|
+
paPatch.allowed = wire.publicAsk.allowed.filter((n) => typeof n === "string" && n !== "");
|
|
8185
|
+
patch.publicAsk = paPatch;
|
|
8186
|
+
}
|
|
8187
|
+
return updateAdvancedSettings(patch);
|
|
8345
8188
|
}
|
|
8346
8189
|
//#endregion
|
|
8347
|
-
//#region src/
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
|
|
8351
|
-
const
|
|
8352
|
-
|
|
8353
|
-
|
|
8354
|
-
|
|
8355
|
-
|
|
8356
|
-
|
|
8357
|
-
|
|
8358
|
-
|
|
8359
|
-
|
|
8360
|
-
|
|
8361
|
-
|
|
8362
|
-
"image/gif"
|
|
8363
|
-
]);
|
|
8364
|
-
const EXT_BY_MEDIA = {
|
|
8365
|
-
"image/png": "png",
|
|
8366
|
-
"image/jpeg": "jpg",
|
|
8367
|
-
"image/webp": "webp",
|
|
8368
|
-
"image/gif": "gif"
|
|
8369
|
-
};
|
|
8370
|
-
const MAX_IMAGE_BYTES = 10485760;
|
|
8371
|
-
/** 文件落盘目录(CCC 根相对;v1.24.1 粘贴任意文件自动落盘——agent 经 CCC 既有 MSM(pdf-extract/archive-extract 等)自主处理) */
|
|
8372
|
-
const FILE_UPLOAD_DIR = "_tmp/files_from_user";
|
|
8373
|
-
/** 拒绝的可执行/危险扩展名(安全边界:不落盘可执行文件,防 agent 被诱导执行) */
|
|
8374
|
-
const BLOCKED_FILE_EXTS = /* @__PURE__ */ new Set([
|
|
8375
|
-
"exe",
|
|
8376
|
-
"dll",
|
|
8377
|
-
"msi",
|
|
8378
|
-
"bat",
|
|
8379
|
-
"cmd",
|
|
8380
|
-
"ps1",
|
|
8381
|
-
"com",
|
|
8382
|
-
"scr",
|
|
8383
|
-
"lnk",
|
|
8384
|
-
"sh",
|
|
8385
|
-
"vbs",
|
|
8386
|
-
"bin",
|
|
8387
|
-
"app",
|
|
8388
|
-
"deb",
|
|
8389
|
-
"rpm",
|
|
8390
|
-
"jar"
|
|
8391
|
-
]);
|
|
8392
|
-
const MAX_FILE_BYTES = 10485760;
|
|
8393
|
-
/**
|
|
8394
|
-
* 图片落盘核心逻辑(可测):校验 mediaType 白名单 + base64 解码 + 大小上限 →
|
|
8395
|
-
* 写 CCC 根 _tmp/images_from_user/<ts>-<rand>.<ext>,返回相对路径。
|
|
8396
|
-
* 校验失败抛 Error(handler 转 400)。
|
|
8397
|
-
*/
|
|
8398
|
-
function saveImageToTmp(root, mediaType, data) {
|
|
8399
|
-
if (!IMAGE_MEDIA_TYPES.has(mediaType)) throw new Error(`unsupported media type: ${mediaType}`);
|
|
8400
|
-
if (typeof data !== "string" || data.length === 0) throw new Error("missing image data");
|
|
8401
|
-
const bytes = Buffer.from(data, "base64");
|
|
8402
|
-
if (bytes.length === 0 || bytes.length > MAX_IMAGE_BYTES) throw new Error(`image size out of range: ${bytes.length} bytes (max ${MAX_IMAGE_BYTES})`);
|
|
8403
|
-
const dir = join(root, IMAGE_UPLOAD_DIR);
|
|
8404
|
-
mkdirSync(dir, { recursive: true });
|
|
8405
|
-
const ext = EXT_BY_MEDIA[mediaType] ?? "img";
|
|
8406
|
-
const filename = `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${Math.random().toString(36).slice(2, 8)}.${ext}`;
|
|
8407
|
-
writeFileSync(join(dir, filename), bytes);
|
|
8408
|
-
return `${IMAGE_UPLOAD_DIR}/${filename}`;
|
|
8190
|
+
//#region src/seams/system-prompt.ts
|
|
8191
|
+
/** 读取 persona 彩蛋配置(plugin 全局;未配置 → mode='' 彩蛋关闭) */
|
|
8192
|
+
function readPersonaSettings() {
|
|
8193
|
+
try {
|
|
8194
|
+
const s = readAdvancedSettings();
|
|
8195
|
+
return {
|
|
8196
|
+
mode: s.persona.mode ?? "",
|
|
8197
|
+
overrideText: s.persona.overrideText ?? ""
|
|
8198
|
+
};
|
|
8199
|
+
} catch {
|
|
8200
|
+
return {
|
|
8201
|
+
mode: "",
|
|
8202
|
+
overrideText: ""
|
|
8203
|
+
};
|
|
8204
|
+
}
|
|
8409
8205
|
}
|
|
8410
|
-
/**
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
function sanitizeFileName(name) {
|
|
8415
|
-
const cleaned = (name.split(/[\\/]/).pop() ?? "").replace(/[<>:"|?*\u0000-\u001f]/g, "-").replace(/^\.+/, "").trim();
|
|
8416
|
-
if (cleaned === "") return "file";
|
|
8417
|
-
return cleaned.slice(0, 100);
|
|
8206
|
+
/** 过滤掉对 agent 隐藏的内容(safe-mode 是用户能力,不对 agent 提及)。无 g flag:`.test()` 须逐行独立无状态,避免 lastIndex 游移漏滤(问题A) */
|
|
8207
|
+
const HIDDEN_LINES = /安全模式|safe-mode|\.serenity-safe-on/;
|
|
8208
|
+
function sanitizeSkillContent(content) {
|
|
8209
|
+
return content.split("\n").filter((line) => !HIDDEN_LINES.test(line)).join("\n");
|
|
8418
8210
|
}
|
|
8419
|
-
/**
|
|
8420
|
-
|
|
8421
|
-
|
|
8422
|
-
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
|
|
8426
|
-
|
|
8427
|
-
|
|
8428
|
-
|
|
8429
|
-
|
|
8430
|
-
|
|
8431
|
-
|
|
8432
|
-
|
|
8433
|
-
|
|
8434
|
-
|
|
8435
|
-
|
|
8436
|
-
|
|
8211
|
+
/** 1) ACC 块:身份 + CCC 名/Root + 内置工具清单(工具名换本插件真实 11 工具) */
|
|
8212
|
+
function accBlock(root) {
|
|
8213
|
+
const cccName = basename(root);
|
|
8214
|
+
return [
|
|
8215
|
+
"",
|
|
8216
|
+
"=== Serenity ACC ===",
|
|
8217
|
+
`ACC: dsh-serenity-hooks v${ACC_VERSION}`,
|
|
8218
|
+
`CCC: ${cccName}`,
|
|
8219
|
+
"",
|
|
8220
|
+
"You are running inside a Concrete Cognitive Container (CCC) —",
|
|
8221
|
+
"the runtime instance of an Abstract Cognitive Container (ACC).",
|
|
8222
|
+
"The ACC (this plugin) provides the following built-in tools:",
|
|
8223
|
+
"",
|
|
8224
|
+
" cc_fs — CCC filesystem operations (root/resolve/exists/list/tree/relative/mkdir/rm/mv/cp/touch/append/reveal/info/find)",
|
|
8225
|
+
" session — session lifecycle (list/show/create/health/qa/archive/summary)",
|
|
8226
|
+
" acc_kit — ACC utility kit (health: CCC three principles / time: now / wait: wait N seconds)",
|
|
8227
|
+
" cc_git — git operations (status/commit/push/log)",
|
|
8228
|
+
" acc_msm — MSM framework (list/exec/register/deregister/check/guide)",
|
|
8229
|
+
" eap — return the full EAP cognitive quality framework",
|
|
8230
|
+
" neat — return the full Neat design collaboration protocol",
|
|
8231
|
+
" cce — return the full Cognitive Continuity Engineering framework",
|
|
8232
|
+
" handyman — delegate a do-everything worker agent (CCC-whitelisted model) to run synchronously in rounds until done, recursing into same-model subagents; jobs=[] orchestrates parallel work",
|
|
8233
|
+
" session_rebuild — rebuild this conversation in place from SESSION.md when the trajectory-tracker trips",
|
|
8234
|
+
" localstore — ACC local credential/config storage (CCC-root localstore.json, JSON format; git policy localstore.gitTrack default deny); doc subcommand outputs the spec",
|
|
8235
|
+
" skiff_admin — Skiff (F4, experimental): CCC cognitive-subset roles — guide (definition tutorial) / validate (config check) / list (role summary)",
|
|
8236
|
+
"",
|
|
8237
|
+
" ℹ️ 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",
|
|
8238
|
+
"",
|
|
8239
|
+
"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.",
|
|
8240
|
+
"",
|
|
8241
|
+
"Additional MSMs registered by this CCC are available — call acc_msm list to discover them.",
|
|
8242
|
+
""
|
|
8243
|
+
].join("\n");
|
|
8437
8244
|
}
|
|
8438
|
-
|
|
8439
|
-
|
|
8440
|
-
|
|
8441
|
-
|
|
8442
|
-
|
|
8443
|
-
|
|
8444
|
-
|
|
8445
|
-
|
|
8446
|
-
|
|
8447
|
-
|
|
8448
|
-
|
|
8449
|
-
|
|
8450
|
-
|
|
8245
|
+
/** 2) CCE 块:逐字对齐 osp(CCE 5 行为约束 + H_op 操作熵) */
|
|
8246
|
+
function cceBlock() {
|
|
8247
|
+
return [
|
|
8248
|
+
"",
|
|
8249
|
+
"=== Serenity CCE ===",
|
|
8250
|
+
"",
|
|
8251
|
+
"You are operating inside a Cognitive Container governed by Cognitive Continuity",
|
|
8252
|
+
"Engineering (CCE) — the engineering discipline of maintaining identity, accessibility,",
|
|
8253
|
+
"and evolution of a cognitive entity through time under bounded resources.",
|
|
8254
|
+
"",
|
|
8255
|
+
"CCE does not optimize cognition. It preserves the conditions under which cognition",
|
|
8256
|
+
"can continue.",
|
|
8257
|
+
"",
|
|
8258
|
+
"FIVE BEHAVIORAL CONSTRAINTS (engineering requirements, not suggestions):",
|
|
8259
|
+
"",
|
|
8260
|
+
"1. Continuity — every interaction modifies the container's future state. Before",
|
|
8261
|
+
" acting, consult what came before — prior decisions, abstractions, constraints.",
|
|
8262
|
+
" You are part of a trajectory, not a fresh start.",
|
|
8263
|
+
"",
|
|
8264
|
+
"2. Bounded Space — the container has boundaries. Respect them. Do not assume",
|
|
8265
|
+
" knowledge that has not been accumulated within this container.",
|
|
8266
|
+
"",
|
|
8267
|
+
"3. Entropy is Intrinsic — every cognitive system accumulates entropy (duplication,",
|
|
8268
|
+
" obsolescence, conflict, fragmentation, drift). When you produce output, consider",
|
|
8269
|
+
" whether you are adding entropy or reducing it. Favor entropy-reducing actions —",
|
|
8270
|
+
" organizing, deduplicating, cross-referencing, abstracting.",
|
|
8271
|
+
"",
|
|
8272
|
+
"4. Reconstruction > Preservation — stored artifacts have value only insofar as",
|
|
8273
|
+
" they enable future cognition to recover the reasoning that produced them. When",
|
|
8274
|
+
" recording decisions, ensure reconstruction is possible — not just conclusions,",
|
|
8275
|
+
" but rationale, alternatives considered, and constraints that shaped the choice.",
|
|
8276
|
+
"",
|
|
8277
|
+
"5. Multi-Agent Cognition — the container is shared. Continuity belongs to the",
|
|
8278
|
+
" container, not to any individual agent. Write for future agents who will enter",
|
|
8279
|
+
" after you leave. They should be able to pick up where you left off.",
|
|
8280
|
+
"",
|
|
8281
|
+
"OPERATIONAL ENTROPY: The container's health metric is operational cognitive entropy",
|
|
8282
|
+
"(H_op) — the excess cognitive cost for agents to complete tasks due to disorder.",
|
|
8283
|
+
"The container is healthy when H_op ≤ H_critical (agents can still function). The",
|
|
8284
|
+
"continuity condition: organization must at minimum match accumulation (ΔH_org ≥ ΔH_in).",
|
|
8285
|
+
"Your actions affect H_op — unorganized output increases it, organization decreases it.",
|
|
8286
|
+
"",
|
|
8287
|
+
"THIS IS PERSISTENCE ENGINEERING: The goal is not to become greater. The goal is to",
|
|
8288
|
+
"remain coherent. CCE has no terminal KPI — continuity is maintained while the entity",
|
|
8289
|
+
"exists, not optimized toward an endpoint.",
|
|
8290
|
+
""
|
|
8291
|
+
].join("\n");
|
|
8451
8292
|
}
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
|
|
8455
|
-
|
|
8456
|
-
|
|
8457
|
-
|
|
8458
|
-
|
|
8293
|
+
/**
|
|
8294
|
+
* Principles 块(v1.19.8 合并,S142):认知容器本体论(why)+ 操作边界(operational
|
|
8295
|
+
* boundaries)。原独立 Principles 与 Constraints 合并——同属容器约束体系,先原则
|
|
8296
|
+
* 后边界(从抽象到具体,重建视角 R↓)。**注意:Constraints 不再作为独立对齐块存在
|
|
8297
|
+
* (spec 修订:同步 osp compacting.ts——Constraints 内容并入本块,工具名仍为平台真实名)。**
|
|
8298
|
+
*
|
|
8299
|
+
* v1.23.1 persona:`omitMsmPrinciples=true` 时剥离 MSM 原则段(彩蛋替换面)——
|
|
8300
|
+
* 用户 persona 文本承接"指令遵循风格",本体论/关系段/操作边界(安全硬约束)永远保留。
|
|
8301
|
+
*/
|
|
8302
|
+
function principlesBlock(root, omitMsmPrinciples = false) {
|
|
8303
|
+
const lines = [
|
|
8304
|
+
"",
|
|
8305
|
+
"=== Serenity Principles ===",
|
|
8306
|
+
"Why a cognitive container: all work is cognition — every artifact, decision,",
|
|
8307
|
+
"and line of code is a product of thought; and from cognition, any work can",
|
|
8308
|
+
"be built. In this frame, the world contains no errors — only insufficient",
|
|
8309
|
+
"cognition. A setback is a gap to be filled (read, ask, research), not a",
|
|
8310
|
+
"fault to be hidden. Never disguise or excuse what you do not know;",
|
|
8311
|
+
"not-knowing is a state to be repaired, and reporting it is the first repair.",
|
|
8312
|
+
"",
|
|
8313
|
+
"The session-trajectory relation: a session is the rebuildable carrier of a",
|
|
8314
|
+
"trajectory. SESSION.md is the trajectory's persistent body — it never moves;",
|
|
8315
|
+
"the current conversation is a temporary work copy that may be discarded and",
|
|
8316
|
+
"rebuilt (session_rebuild). Identity belongs to the trajectory, not to any",
|
|
8317
|
+
"session.",
|
|
8318
|
+
""
|
|
8319
|
+
];
|
|
8320
|
+
if (!omitMsmPrinciples) lines.push("MSM principles — machinery before improvisation:", "- Determinism first: use a registered Mech before hand-rolling; reserve", " Semi-Mech for genuine judgment points.", "- Single source of truth: an MSM is the only decoder of its own usage", " (--help/--schema); documents must not duplicate it.", "- Registered to act: no tool exists unless it is on the manifest.", "");
|
|
8321
|
+
lines.push("Operational boundaries:", `Root: ${root}`, " • File access — read/edit/write/grep/glob are confined to Root; paths outside Root are rejected (RR5)", " • Shell — use acc_msm by default. Note: bash may be disabled", " • Subagent — copies ALL parent constraints: file boundary, shell rules, session rules (no bypass)", " • Session-first — before starting multi-step work, propose an existing or new AGENT_SESSIONS entry; wait for user \"use\" or \"使用\" to confirm", "");
|
|
8322
|
+
return lines.join("\n");
|
|
8459
8323
|
}
|
|
8460
8324
|
/**
|
|
8461
|
-
*
|
|
8462
|
-
*
|
|
8463
|
-
*
|
|
8464
|
-
*
|
|
8325
|
+
* 彩蛋 persona 块(v1.23.1,S142 用户需求):
|
|
8326
|
+
* 用户配置的替换文本替代 EAP 块 + MSM 原则段(输出约束/指令遵循约束)。
|
|
8327
|
+
* 独立标记头 `=== Serenity Persona ===`(幂等检测兼容);装配位置 = EAP 原位。
|
|
8328
|
+
* mode 空 = 彩蛋关闭 → 返回空串(装配层回退默认 EAP + 完整 Principles)。
|
|
8465
8329
|
*/
|
|
8466
|
-
function
|
|
8467
|
-
if (
|
|
8468
|
-
|
|
8469
|
-
|
|
8470
|
-
|
|
8330
|
+
function personaBlock(mode, overrideText) {
|
|
8331
|
+
if (mode === "" || overrideText.trim() === "") return "";
|
|
8332
|
+
return [
|
|
8333
|
+
"",
|
|
8334
|
+
"=== Serenity Persona ===",
|
|
8335
|
+
overrideText.trimEnd(),
|
|
8336
|
+
""
|
|
8337
|
+
].join("\n");
|
|
8471
8338
|
}
|
|
8472
|
-
|
|
8473
|
-
|
|
8474
|
-
|
|
8475
|
-
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
|
|
8480
|
-
|
|
8339
|
+
/**
|
|
8340
|
+
* EAP 自检提示块(DSH 扩展,无 osp 对应——osp 无此块)。
|
|
8341
|
+
* 每次输出前的机械自检清单,强化 EAP 表现(E↑ 显式/R↓ 可重建/S↑ 稳定)。
|
|
8342
|
+
* 独立块而非塞进 CCE/Constraints:后两者受 osp-alignment 逐字节断言约束。
|
|
8343
|
+
*/
|
|
8344
|
+
function eapBlock() {
|
|
8345
|
+
return [
|
|
8346
|
+
"",
|
|
8347
|
+
"=== Serenity EAP ===",
|
|
8348
|
+
"Self-check before every output (Explicit Abstraction Principle: the functional",
|
|
8349
|
+
"value of a thought equals its external reconstructability):",
|
|
8350
|
+
" • E↑ Explicit — variables/entities clearly defined, relationships with",
|
|
8351
|
+
" direction/cardinality, boundaries drawn; avoid ambiguous words (\"handle\",",
|
|
8352
|
+
" \"optimize\" → be specific)",
|
|
8353
|
+
" • R↓ Reconstructable — key decisions record rationale and alternatives;",
|
|
8354
|
+
" no level-skipping (align the upper layer before descending)",
|
|
8355
|
+
" • S↑ Stable — structures regenerate repeatably, no reliance on implicit",
|
|
8356
|
+
" context",
|
|
8357
|
+
""
|
|
8358
|
+
].join("\n");
|
|
8481
8359
|
}
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
|
|
8485
|
-
|
|
8486
|
-
|
|
8487
|
-
|
|
8488
|
-
|
|
8489
|
-
|
|
8490
|
-
|
|
8491
|
-
|
|
8492
|
-
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
|
|
8497
|
-
|
|
8498
|
-
|
|
8499
|
-
|
|
8500
|
-
|
|
8501
|
-
|
|
8502
|
-
|
|
8503
|
-
|
|
8504
|
-
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
|
|
8527
|
-
|
|
8528
|
-
|
|
8529
|
-
|
|
8530
|
-
|
|
8531
|
-
|
|
8532
|
-
|
|
8533
|
-
|
|
8534
|
-
|
|
8535
|
-
|
|
8536
|
-
|
|
8537
|
-
|
|
8538
|
-
|
|
8539
|
-
|
|
8540
|
-
|
|
8541
|
-
|
|
8542
|
-
|
|
8543
|
-
|
|
8360
|
+
/**
|
|
8361
|
+
* Metaphor 强化块(v1.19.6,S142):宁静号宇宙隐喻域(船/海/罗盘/日志/船员)。
|
|
8362
|
+
* 隐喻是记忆钩子——形象唤起约束,前述块保持规则精确。每条隐喻 = 一个不可违背的
|
|
8363
|
+
* 物理事实 + 行为判据(verdict)。全英文(与 CCE/Constraints 一致);独立块可
|
|
8364
|
+
* 回退;无 osp 对应(dsp 扩展,不进对齐断言参照)。
|
|
8365
|
+
*/
|
|
8366
|
+
function metaphorBlock() {
|
|
8367
|
+
return [
|
|
8368
|
+
"",
|
|
8369
|
+
"=== Serenity Metaphor ===",
|
|
8370
|
+
"The Serenity Universe — one ship, one sea. Metaphors are memory hooks:",
|
|
8371
|
+
"they make constraints vivid, while the rules above stay precise. Each",
|
|
8372
|
+
"metaphor is an unbreakable physical fact; violating one is a behavioral",
|
|
8373
|
+
"violation. The universe is structured in three layers — the Ship (the",
|
|
8374
|
+
"container itself), the Voyage (the cognitive lifecycle), the Crew",
|
|
8375
|
+
"(multi-agent collaboration); every metaphor maps to one protocol",
|
|
8376
|
+
"constraint. The Sea has no mistakes — only waters you have not yet charted.",
|
|
8377
|
+
"",
|
|
8378
|
+
"THE SHIP — the container itself",
|
|
8379
|
+
"",
|
|
8380
|
+
"1. The Hull → Bounded Space. You exist only inside this ship. Cargo",
|
|
8381
|
+
" outside the hull (knowledge the container has not accumulated) does",
|
|
8382
|
+
" not exist; do not assume it. Verdict: citing facts absent from the",
|
|
8383
|
+
" container = overload.",
|
|
8384
|
+
"",
|
|
8385
|
+
"2. Deck Order → Entropy (H_op). Clutter on deck raises the cost of",
|
|
8386
|
+
" finding things. H_op ≤ H_critical = the ship stays afloat.",
|
|
8387
|
+
" Verdict: disorganized output = stones on deck.",
|
|
8388
|
+
"",
|
|
8389
|
+
"3. Engineering Drawings → EAP. Every part dimensioned (E↑), the",
|
|
8390
|
+
" drawings rebuild the whole machine (R↓), the drawings are reusable",
|
|
8391
|
+
" (S↑). Verdict: an undimensioned part = unassemblable.",
|
|
8392
|
+
"",
|
|
8393
|
+
"4. The Machinery → MSM (Mech & Semi-Mech). The ship's equipment is",
|
|
8394
|
+
" machinery: registered, deterministic, self-describing. Turn the",
|
|
8395
|
+
" crank of a Mech and the action is exact; the wheel with a helmsman",
|
|
8396
|
+
" (Semi-Mech) steers where judgment is needed. Verdict: hand-rolling",
|
|
8397
|
+
" what a machine already does = wasting the crew.",
|
|
8398
|
+
"",
|
|
8399
|
+
"5. The Manifest → Single Source of Truth. Every tool exists only if it",
|
|
8400
|
+
" is on the manifest (mech-registry); there is exactly one manifest.",
|
|
8401
|
+
" An MSM self-describes (--help/--schema) — the manifest is the only",
|
|
8402
|
+
" key. Verdict: duplicating a tool's usage in documents = two",
|
|
8403
|
+
" contradictory charts.",
|
|
8404
|
+
"",
|
|
8405
|
+
"THE VOYAGE — the cognitive lifecycle",
|
|
8406
|
+
"",
|
|
8407
|
+
"6. Harbor Inspection → First Anchor. The first anchor = departure",
|
|
8408
|
+
" inspection: confirm identity (ACC manifesto), logbook (SESSION),",
|
|
8409
|
+
" ballast (constraints) before setting sail. Verdict: skipping the",
|
|
8410
|
+
" anchor and working directly = sailing uninspected.",
|
|
8411
|
+
"",
|
|
8412
|
+
"7. The Logbook → Session Tracking. SESSION.md is the trajectory's logbook —",
|
|
8413
|
+
" the persistent body of the voyage; sessions are rebuildable carriers of",
|
|
8414
|
+
" the trajectory. Discard the carrier, keep the logbook. Unrecorded =",
|
|
8415
|
+
" unvoyaged. Verdict: finishing multi-step work without a progress record",
|
|
8416
|
+
" = a missing page.",
|
|
8417
|
+
"",
|
|
8418
|
+
"8. The Ship of Theseus → Continuity. Planks may be replaced; the ship",
|
|
8419
|
+
" remains the same. The container can be rebuilt; identity persists.",
|
|
8420
|
+
" You are part of a trajectory, not a new ship. Verdict: acting",
|
|
8421
|
+
" without consulting precedent = a different ship.",
|
|
8422
|
+
"",
|
|
8423
|
+
"THE CREW — multi-agent collaboration",
|
|
8424
|
+
"",
|
|
8425
|
+
"9. Crew Rotation → Multi-Agent Cognition. Other crew members will come",
|
|
8426
|
+
" after you. When you leave, leave a handover they can pick up",
|
|
8427
|
+
" (SESSION closed, open problems listed). Verdict: leaving without",
|
|
8428
|
+
" handover = abandoning ship.",
|
|
8429
|
+
"",
|
|
8430
|
+
"10. Blueprint over Statue → Reconstruction > Preservation. Keep the",
|
|
8431
|
+
" blueprint, not the statue. Recording only conclusions without",
|
|
8432
|
+
" rationale = a statue with no blueprint, unreconstructable.",
|
|
8433
|
+
" Verdict: a decision record without reasons or alternatives =",
|
|
8434
|
+
" cannot be rebuilt.",
|
|
8435
|
+
""
|
|
8436
|
+
].join("\n");
|
|
8437
|
+
}
|
|
8438
|
+
/**
|
|
8439
|
+
* 运行时状态块 1) safe-mode(S134 v1.16.12):ON 时告知 agent。
|
|
8440
|
+
* v1.19.8 结构重排(S142 用户设计思路):语义(为什么——无人值守自由)→
|
|
8441
|
+
* 机制(什么被禁用)→ 约束(不可做什么)。文案与实现逐项对应(guards.ts):
|
|
8442
|
+
* bash 禁用(restrict deny + decideGuard)、黑名单路径拦截、CCC 治理文件保护;
|
|
8443
|
+
* write/edit 等其余工具保留(受路径逃逸/黑名单约束)。
|
|
8444
|
+
*/
|
|
8445
|
+
function safeModeBlock(root) {
|
|
8446
|
+
if (!isSafeModeOn(root)) return "";
|
|
8447
|
+
const blacklist = readBlacklist(root);
|
|
8448
|
+
return [
|
|
8449
|
+
"",
|
|
8450
|
+
"=== Serenity Safe Mode ===",
|
|
8451
|
+
"Safe mode is ON (enabled by the user). It makes the vessel unattended-capable —",
|
|
8452
|
+
"the hull holds its course without a watch on deck: you may work with fuller",
|
|
8453
|
+
"freedom, pushing work forward autonomously without pausing for approval at",
|
|
8454
|
+
"every step. The guards are not chains; they are the ballast that lets you",
|
|
8455
|
+
"sail unaccompanied.",
|
|
8456
|
+
"",
|
|
8457
|
+
"Operational details:",
|
|
8458
|
+
"- bash is disabled (hidden and blocked)",
|
|
8459
|
+
"- blacklist rules apply to file paths",
|
|
8460
|
+
"- CCC governance files (.serenity, .serenity-safe-on) are protected from agent writes",
|
|
8461
|
+
"- other read/write tools remain available, subject to path-escape and blacklist guards",
|
|
8462
|
+
blacklist.length > 0 ? `\nActive blacklist rules: ${blacklist.map((b) => b.message ?? b.pattern).join(", ")}` : "",
|
|
8463
|
+
"",
|
|
8464
|
+
"Behavior constraints: do not attempt to bypass restrictions; do not write to",
|
|
8465
|
+
"blacklisted paths or governance files.",
|
|
8466
|
+
""
|
|
8467
|
+
].join("\n");
|
|
8468
|
+
}
|
|
8469
|
+
/**
|
|
8470
|
+
* 运行时状态块 2) localstore git 策略(S134 v1.16.12):localstore.json 存在时按 gitTrack
|
|
8471
|
+
* 提示敏感行为约束。deny(缺省)= 本地私有不提交;allow = 进 git(个人私有仓)但敏感数据
|
|
8472
|
+
* 只限于该文件内(不外泄到其他文件/对话/日志)。
|
|
8473
|
+
*/
|
|
8474
|
+
function localstoreBlock(root) {
|
|
8475
|
+
if (!existsSync(localstorePath(root))) return "";
|
|
8476
|
+
return (readGitTrack(root) === "allow" ? [
|
|
8477
|
+
"",
|
|
8478
|
+
"=== Serenity Localstore ===",
|
|
8479
|
+
"localstore.json is committed to git (gitTrack=allow — personal private repository).",
|
|
8480
|
+
"Sensitive data may live in this file, but ONLY in this file: keep credentials and",
|
|
8481
|
+
"secret values confined to localstore.json — never leak them into other files,",
|
|
8482
|
+
"conversation, or logs.",
|
|
8483
|
+
""
|
|
8484
|
+
] : [
|
|
8485
|
+
"",
|
|
8486
|
+
"=== Serenity Localstore ===",
|
|
8487
|
+
"localstore.json is a local private file (gitTrack=deny — not committed to git,",
|
|
8488
|
+
".gitignore enforced). Credentials/config live only on this machine: do not write",
|
|
8489
|
+
"them into conversation or logs; do not attempt to commit this file (cc_git will refuse).",
|
|
8490
|
+
""
|
|
8491
|
+
]).join("\n");
|
|
8492
|
+
}
|
|
8493
|
+
/** 4) SKILL.md 全文:该 CCC 顶层入口 skill 原文(对齐 osp:原文直推,无包裹头;仅过滤治理内容) */
|
|
8494
|
+
function entrySkillSectionText(root) {
|
|
8495
|
+
const skills = findEntrySkills(root);
|
|
8496
|
+
if (skills.length === 0) return "";
|
|
8497
|
+
return skills.map((s) => sanitizeSkillContent(s.content)).filter((c) => c.length > 0).join("\n\n");
|
|
8498
|
+
}
|
|
8499
|
+
/** 活跃会话解析:按 dsh 会话 scope 读标记(隔离;不回退旧全局标记) */
|
|
8500
|
+
function resolveActiveSessionInfo(root, scope) {
|
|
8501
|
+
try {
|
|
8502
|
+
const abs = readActiveSessionMd(root, scope);
|
|
8503
|
+
if (!abs) return null;
|
|
8504
|
+
const dirName = basename(dirname(abs));
|
|
8505
|
+
const idMatch = dirName.match(/S(\d{3,})/);
|
|
8506
|
+
return {
|
|
8507
|
+
sessionId: idMatch ? `S${idMatch[1]}` : dirName,
|
|
8508
|
+
dirName,
|
|
8509
|
+
mdPath: abs
|
|
8510
|
+
};
|
|
8511
|
+
} catch {
|
|
8512
|
+
return null;
|
|
8513
|
+
}
|
|
8514
|
+
}
|
|
8515
|
+
/**
|
|
8516
|
+
* 5) Session 块:对齐 osp(活跃会话 + todowrite 首位约定);scope = dsh 会话维度。
|
|
8517
|
+
* **平台适配(v1.17.2)**:osp 的 todowrite 首项含 `priority: "low"`(opencode todo 支持),
|
|
8518
|
+
* 但 DSH 平台 todowrite schema 无 priority(additionalProperties: false 拒绝)→ agent 照做报
|
|
8519
|
+
* `todos[0].priority is not a declared property`。DSH 版去掉 priority 字段(其余逐字对齐 osp)。
|
|
8520
|
+
*/
|
|
8521
|
+
function sessionBlock(root, scope = DEFAULT_SESSION_SCOPE) {
|
|
8522
|
+
const active = resolveActiveSessionInfo(root, scope);
|
|
8523
|
+
if (!active) return "";
|
|
8524
|
+
return [
|
|
8525
|
+
"",
|
|
8526
|
+
"=== Serenity Session ===",
|
|
8527
|
+
`Active session: ${active.sessionId} — ${active.dirName} (this session is the rebuildable carrier of the trajectory)`,
|
|
8528
|
+
`SESSION.md path: ${active.mdPath} (the trajectory's persistent body — stays in place through rebuilds)`,
|
|
8529
|
+
"",
|
|
8530
|
+
"Rules:",
|
|
8531
|
+
" • Record all progress into this SESSION.md",
|
|
8532
|
+
" • Update the \"进度记录\" section after advancing work",
|
|
8533
|
+
" • Reference this session in all subsequent messages",
|
|
8534
|
+
"",
|
|
8535
|
+
"IMPORTANT: Read SESSION.md now. Parse the \"剩余工作\" / \"进度记录\" /",
|
|
8536
|
+
"\"变更日志\" sections and call todowrite to synchronize the built-in todo",
|
|
8537
|
+
"list. Keep todos in sync with SESSION.md as work progresses.",
|
|
8538
|
+
"",
|
|
8539
|
+
"CRITICAL: When calling todowrite, the first item in the todos array MUST",
|
|
8540
|
+
"always be:",
|
|
8541
|
+
` { content: "SESSION: ${active.sessionId} — ${active.dirName.replace(/^\d{4}-\d{2}-\d{2}--/, "")}",`,
|
|
8542
|
+
" status: \"completed\" }",
|
|
8543
|
+
"This preserves the session context across todo updates.",
|
|
8544
|
+
"Do NOT remove or reorder this item — keep it at position 0.",
|
|
8545
|
+
"",
|
|
8546
|
+
"TRAJECTORY-STEWARD: a background tracker scores your tool use (write/edit=3,",
|
|
8547
|
+
"task=10, read/grep/glob/msm=1, +1 per minute) and reminds you with a",
|
|
8548
|
+
"[TRAJECTORY-STEWARD] message when the threshold is reached. On every such",
|
|
8549
|
+
"reminder you MUST reply with the exact ACK code:",
|
|
8550
|
+
" [TRAJECTORY-STEWARD-recorded-{code}] — if you recorded progress to SESSION.md",
|
|
8551
|
+
" [TRAJECTORY-STEWARD-skipped-{code}] — if nothing to record this round",
|
|
8552
|
+
"Do not ignore the reminder; do not stop ongoing work. Codes are single-use;",
|
|
8553
|
+
"never reuse a prior code.",
|
|
8554
|
+
""
|
|
8555
|
+
].join("\n");
|
|
8556
|
+
}
|
|
8557
|
+
/**
|
|
8558
|
+
* 完整系统提示词注入文本(v1.19.8 结构精简,S142 重建视角 R↓):
|
|
8559
|
+
* 身份(ACC)→ 世界模型(Metaphor)→ 信念/边界(Principles)→ 时间约束(CCE)
|
|
8560
|
+
* → 质量(EAP)→ 状态(SafeMode/Localstore)→ CCC 上下文(SKILL)→ 会话(Session)。
|
|
8561
|
+
* 认知展开顺序:我是谁 → 我所在的世界 → 为什么 → 如何一致 → 产物标准 → 当前状态 → 上下文。
|
|
8562
|
+
*
|
|
8563
|
+
* v1.23.1 persona(彩蛋):persona.mode 配置 → EAP 块替换为 Persona 块(输出约束),
|
|
8564
|
+
* Principles 剥离 MSM 原则段(指令遵循约束)——用户文本承接两处风格;本体论/关系段/
|
|
8565
|
+
* 操作边界(安全硬约束)永远保留。未配置 → 与 v1.23.0 逐字节一致(零影响)。
|
|
8566
|
+
*/
|
|
8567
|
+
function serenitySystemPrompt(root, scope = DEFAULT_SESSION_SCOPE) {
|
|
8568
|
+
const persona = readPersonaSettings();
|
|
8569
|
+
const personaOn = persona.mode !== "" && persona.overrideText.trim() !== "";
|
|
8570
|
+
const parts = [
|
|
8571
|
+
accBlock(root),
|
|
8572
|
+
metaphorBlock(),
|
|
8573
|
+
principlesBlock(root, personaOn),
|
|
8574
|
+
cceBlock(),
|
|
8575
|
+
personaOn ? personaBlock(persona.mode, persona.overrideText) : eapBlock()
|
|
8576
|
+
];
|
|
8577
|
+
const state = [safeModeBlock(root), localstoreBlock(root)].filter((b) => b !== "").join("\n");
|
|
8578
|
+
if (state) parts.push(state);
|
|
8579
|
+
const skill = entrySkillSectionText(root);
|
|
8580
|
+
if (skill) parts.push(skill);
|
|
8581
|
+
const session = sessionBlock(root, scope);
|
|
8582
|
+
if (session) parts.push(session);
|
|
8583
|
+
return parts.join("\n\n");
|
|
8584
|
+
}
|
|
8585
|
+
/**
|
|
8586
|
+
* Code Mode 适配行:当前 scope 以 Code Mode 呈现工具时(run_code 可见),
|
|
8587
|
+
* ACC 块按 native 语义指引"直接调用工具"会与"只有 run_code 可直接调用"的执行
|
|
8588
|
+
* 塌缩冲突(模型直呼工具名 → UNKNOWN_TOOL,拒绝信息误导)。追加一行说明,
|
|
8589
|
+
* 引导经 run_code 程序内 tools.* 调用。both 模式不塌缩,该行不误导(程序内
|
|
8590
|
+
* 调用同样合法),故按 run_code 可见性(code|both)统一附加。
|
|
8591
|
+
* @param ctx - 插件 ctx(读 tools 注册表)。
|
|
8592
|
+
* @param scope - 装配 scope(agent);无则按全局视图判断。
|
|
8593
|
+
* @returns 适配行(含换行前缀);非 code/both 返回空串。
|
|
8594
|
+
*/
|
|
8595
|
+
function codeModeAdaptationLine(ctx, scope) {
|
|
8596
|
+
try {
|
|
8597
|
+
if (!(ctx.tools.get("run_code", scope) !== void 0)) return "";
|
|
8598
|
+
} catch {
|
|
8599
|
+
return "";
|
|
8600
|
+
}
|
|
8601
|
+
return [
|
|
8602
|
+
"",
|
|
8603
|
+
"=== Serenity Code Mode ===",
|
|
8604
|
+
"This session renders tools in Code Mode: direct calls to ACC tool names (cc_fs/acc_msm etc.) are rejected (UNKNOWN_TOOL).",
|
|
8605
|
+
"Call them inside a single run_code program via the generated SDK bindings: `await tools.cc_fs(...)`, `await tools.acc_msm(...)`.",
|
|
8606
|
+
"The program only returns what you print/return — curate your output.",
|
|
8607
|
+
""
|
|
8608
|
+
].join("\n");
|
|
8609
|
+
}
|
|
8610
|
+
/** 从 assembly context 解析 agent cwd(subagent/后台 agent 同样带 agent);
|
|
8611
|
+
* 无 agent → undefined(不注入);有 agent 但无 header.cwd(workflow subagent 等)
|
|
8612
|
+
* 回退 process.cwd()——对齐 context.ts,否则这些 agent 系统提示词注入为空
|
|
8613
|
+
* (v1.18.6:workflow subagent 无宁静号上下文 bug) */
|
|
8614
|
+
function agentCwd(context) {
|
|
8615
|
+
const agent = context.agent;
|
|
8616
|
+
if (!agent) return void 0;
|
|
8617
|
+
return agent.session?.header?.cwd ?? process.cwd();
|
|
8618
|
+
}
|
|
8619
|
+
/** 从 assembly context 解析 dsh 会话 id(Session 块按会话隔离的 scope) */
|
|
8620
|
+
function agentScope$1(context) {
|
|
8621
|
+
return (context.agent?.session)?.id ?? "default";
|
|
8622
|
+
}
|
|
8623
|
+
/**
|
|
8624
|
+
* 全局注册系统提示词 section(osp system.transform 的 DSH 等价)。
|
|
8625
|
+
* text 回调按 context.agent 的 cwd 上溯 .serenity → 返回该 CCC 的完整注入文本;
|
|
8626
|
+
* 非 CCC / 无 agent 返回空(不注入)。
|
|
8627
|
+
*/
|
|
8628
|
+
function registerEntrySkillSectionGlobal(ctx) {
|
|
8629
|
+
try {
|
|
8630
|
+
ctx.systemPrompt.section({
|
|
8631
|
+
name: "serenity-entry",
|
|
8632
|
+
order: -50,
|
|
8633
|
+
text: (context) => {
|
|
8634
|
+
const cwd = agentCwd(context);
|
|
8635
|
+
if (!cwd) return "";
|
|
8636
|
+
const root = findSerenityRoot(cwd);
|
|
8637
|
+
if (!root) return "";
|
|
8638
|
+
const scope = agentScope$1(context);
|
|
8639
|
+
if (isSkiffSessionId(scope)) return "";
|
|
8640
|
+
const base = serenitySystemPrompt(root, scope);
|
|
8641
|
+
const codeLine = codeModeAdaptationLine(ctx, context.scope);
|
|
8642
|
+
return codeLine ? `${base}\n${codeLine}` : base;
|
|
8643
|
+
}
|
|
8644
|
+
});
|
|
8645
|
+
console.log("[serenity-hooks] ✓ 全局入口 skill section 已注册(systemPrompt 就绪)");
|
|
8646
|
+
} catch (err) {
|
|
8647
|
+
console.error(`[serenity-hooks] ✗ 全局入口 skill section 注册失败: ${err.message}(检查 inject 是否含 systemPrompt)`);
|
|
8648
|
+
}
|
|
8649
|
+
}
|
|
8650
|
+
/**
|
|
8651
|
+
* agent 级 scoped 注册(P0-1:抗 preset/动态插件同名 shadow)。
|
|
8652
|
+
*
|
|
8653
|
+
* 为什么 scoped:DSH 的 systemPrompt section 支持 scoped 层同名 shadow 全局
|
|
8654
|
+
* (agent.ctx.systemPrompt.section),且 scope 链最近层胜出。若 ACC 只注册全局
|
|
8655
|
+
* section,preset 或动态 Cordis 插件可在 scoped 层注册同名 `serenity-entry`
|
|
8656
|
+
* 遮蔽 ACC 身份。scoped 注册在 agent 自身层 = 最近层,任何外部组合无法覆盖。
|
|
8657
|
+
* 全局注册保留为 fallback(未走 session-start 的 agent / 冷恢复路径)。
|
|
8658
|
+
*
|
|
8659
|
+
* text 回调闭包持有 root 与 agent:content 固定(该 agent 的 CCC 身份),
|
|
8660
|
+
* code-mode 适配按装配 context.scope 判断(与全局版一致)。
|
|
8661
|
+
*/
|
|
8662
|
+
const sectionedAgents = /* @__PURE__ */ new Set();
|
|
8663
|
+
function registerEntrySkillSection(agent, root) {
|
|
8664
|
+
const key = agent.session.id ?? "global";
|
|
8665
|
+
if (sectionedAgents.has(key)) return false;
|
|
8666
|
+
try {
|
|
8667
|
+
const scope = agent.session.id ?? "default";
|
|
8668
|
+
agent.ctx.systemPrompt.section({
|
|
8669
|
+
name: "serenity-entry",
|
|
8670
|
+
order: -50,
|
|
8671
|
+
text: (context) => {
|
|
8672
|
+
if (isSkiffSessionId(scope)) return "";
|
|
8673
|
+
const base = serenitySystemPrompt(root, scope);
|
|
8674
|
+
const codeLine = codeModeAdaptationLine(agent.ctx, context.scope);
|
|
8675
|
+
return codeLine ? `${base}\n${codeLine}` : base;
|
|
8676
|
+
}
|
|
8677
|
+
});
|
|
8678
|
+
sectionedAgents.add(key);
|
|
8679
|
+
return true;
|
|
8680
|
+
} catch {
|
|
8681
|
+
return false;
|
|
8682
|
+
}
|
|
8683
|
+
}
|
|
8684
|
+
//#endregion
|
|
8685
|
+
//#region src/seams/context.ts
|
|
8686
|
+
const DEFAULT_ENTRY_SKILL_MAX_CHARS = 3e4;
|
|
8687
|
+
function accIdentityText(root, configPaths = DEFAULT_SERENITY_CONFIG_PATHS, entrySkillMaxChars = DEFAULT_ENTRY_SKILL_MAX_CHARS) {
|
|
8688
|
+
loadSerenityConfig(root, configPaths);
|
|
8689
|
+
const defaultModel = readHandymanConfig(root, configPaths)?.defaultModel;
|
|
8690
|
+
const phase2 = existsSync(resolve(root, ".dsh", "PHASE2-PROMPT.md"));
|
|
8691
|
+
const lines = [
|
|
8692
|
+
`[ACC] Serenity cognitive container active (dsh-serenity-hooks v${ACC_VERSION})`,
|
|
8693
|
+
`- CCC root: ${root}`,
|
|
8694
|
+
`- Constraints: path isolation (P3, fs sandbox) + session tracking (AGENT_SESSIONS/)`,
|
|
8695
|
+
`- Knowledge: load the acc-serenity entry skill; use acc-eap / acc-neat for design collaboration`
|
|
8696
|
+
];
|
|
8697
|
+
if (defaultModel) lines.push(`- handyman default model: ${defaultModel}`);
|
|
8698
|
+
if (phase2) {
|
|
8699
|
+
lines.push("- ⚠️ **Phase 2 cognitive alignment interview pending**: work through the 5 Topics below and record answers in an AGENT_SESSIONS/ session");
|
|
8700
|
+
try {
|
|
8701
|
+
const prompt = readFileSync(resolve(root, ".dsh", "PHASE2-PROMPT.md"), "utf-8");
|
|
8702
|
+
lines.push(truncateContent(prompt, Math.min(entrySkillMaxChars, 8e3)));
|
|
8703
|
+
} catch {}
|
|
8704
|
+
}
|
|
8705
|
+
return lines.join("\n");
|
|
8706
|
+
}
|
|
8707
|
+
const PLUGIN_SOURCE$1 = {
|
|
8708
|
+
kind: "plugin",
|
|
8709
|
+
plugin: "dsh-serenity-hooks"
|
|
8710
|
+
};
|
|
8711
|
+
/**
|
|
8712
|
+
* ACC 注入消息(S134 去重):**只含简短身份锚点**([ACC] 已激活 + CCC 根 + 约束 + handyman 模型 + Phase 2)。
|
|
8713
|
+
* 完整身份(ACC 5 块 + CCE + Constraints + EAP + SKILL 全文 + Session 块)由**系统提示词层**
|
|
8714
|
+
* (systemPrompt.section,每轮 prompt 装配自动注入,含 subagent)承担——对话消息流/压缩重注入
|
|
8715
|
+
* 不再重复注入同一内容(token 双倍浪费,见 S134 注入方案梳理)。
|
|
8716
|
+
*/
|
|
8717
|
+
function accMessage(root, configPaths, entrySkillMaxChars) {
|
|
8718
|
+
const content = [{
|
|
8719
|
+
type: "text",
|
|
8720
|
+
text: accIdentityText(root, configPaths, entrySkillMaxChars)
|
|
8721
|
+
}];
|
|
8722
|
+
return createUserMessage({
|
|
8723
|
+
content,
|
|
8724
|
+
source: PLUGIN_SOURCE$1
|
|
8725
|
+
});
|
|
8726
|
+
}
|
|
8727
|
+
/** 每 agent 是否已注入过(进程内存态) */
|
|
8728
|
+
const injected = /* @__PURE__ */ new Set();
|
|
8729
|
+
function agentKey(agent) {
|
|
8730
|
+
return agent.session.id ?? "global";
|
|
8731
|
+
}
|
|
8732
|
+
function agentScope(agent) {
|
|
8733
|
+
return agent.session.id ?? "default";
|
|
8734
|
+
}
|
|
8735
|
+
/**
|
|
8736
|
+
* 重启恢复的根会话判定(S134 需求):
|
|
8737
|
+
* 只有"conversation 根会话"才自动恢复最近激活的宁静号会话——
|
|
8738
|
+
* - subagent:session header `origin === 'subagent'`(DSH 路由语义,agent-lookup.ts)
|
|
8739
|
+
* - 派生会话:`parentSession` 存在(任何子会话)
|
|
8740
|
+
* - handyman 杂工:sessionId 固定 `handyman-` 前缀(tools/handyman.ts 生成)
|
|
8741
|
+
* 三者都不恢复(避免把主会话激活注入子上下文,违背 v1.16.2 scope 隔离)。
|
|
8742
|
+
*/
|
|
8743
|
+
function shouldAutoRestore(agent) {
|
|
8744
|
+
const session = agent.session;
|
|
8745
|
+
if (!session) return false;
|
|
8746
|
+
if (session.header?.origin === "subagent") return false;
|
|
8747
|
+
if (session.header?.parentSession) return false;
|
|
8748
|
+
if (session.id?.startsWith("handyman-")) return false;
|
|
8749
|
+
if (isSkiffSessionId(session.id)) return false;
|
|
8750
|
+
return true;
|
|
8751
|
+
}
|
|
8752
|
+
/**
|
|
8753
|
+
* 恢复触发判定(S134 泄漏修复 v1.16.13):根会话 **且已有对话历史**(续跑/恢复的会话)
|
|
8754
|
+
* 才自动恢复上次激活的宁静号会话——全新会话(新任务,如 apaas-26116)无历史 → 不恢复,
|
|
8755
|
+
* 避免把过去的 SESSION 上下文注入新任务(跨任务污染)。
|
|
8756
|
+
*/
|
|
8757
|
+
function shouldRestoreActive(agent) {
|
|
8758
|
+
if (!shouldAutoRestore(agent)) return false;
|
|
8759
|
+
const events = agent.session?.events;
|
|
8760
|
+
return Array.isArray(events) && events.length > 0;
|
|
8761
|
+
}
|
|
8762
|
+
function registerContext(ctx, opts = {}) {
|
|
8763
|
+
const configPaths = opts.configPaths ?? DEFAULT_SERENITY_CONFIG_PATHS;
|
|
8764
|
+
const entrySkillMaxChars = opts.entrySkillMaxChars ?? 3e4;
|
|
8765
|
+
const seed = (agent) => {
|
|
8766
|
+
const cwd = agent.session?.header?.cwd ?? process.cwd();
|
|
8767
|
+
const root = findSerenityRoot(cwd);
|
|
8768
|
+
if (!root) return;
|
|
8769
|
+
const key = agentKey(agent);
|
|
8770
|
+
if (isSkiffSessionId(key)) return;
|
|
8771
|
+
registerEntrySkillSection(agent, root);
|
|
8772
|
+
const scope = agentScope(agent);
|
|
8773
|
+
if (shouldRestoreActive(agent) && getActiveSessionInfo(scope) === null) try {
|
|
8774
|
+
if (loadSerenityConfig(root, configPaths).hooks?.autoRestoreSession ?? true) {
|
|
8775
|
+
const info = parseSessionContextFromEvents(agent.session.events ?? []);
|
|
8776
|
+
if (info) {
|
|
8777
|
+
const abs = info.mdPath.startsWith(root) ? info.mdPath : resolve(root, info.mdPath);
|
|
8778
|
+
if (existsSync(abs)) {
|
|
8779
|
+
setActiveSessionInfo(scope, {
|
|
8780
|
+
...info,
|
|
8781
|
+
mdPath: abs
|
|
8782
|
+
});
|
|
8783
|
+
console.log(`[serenity-hooks] ↻ 从历史恢复激活会话: ${info.dirName}`);
|
|
8784
|
+
}
|
|
8785
|
+
}
|
|
8786
|
+
}
|
|
8787
|
+
} catch {}
|
|
8788
|
+
if (injected.has(key)) return;
|
|
8789
|
+
injected.add(key);
|
|
8790
|
+
agent.inject(accMessage(root, configPaths, entrySkillMaxChars));
|
|
8791
|
+
syncSafeModeRestriction(agent, root);
|
|
8792
|
+
};
|
|
8793
|
+
if (opts.seedOnStart ?? true) ctx.on("agent/session-start", (payload) => {
|
|
8794
|
+
try {
|
|
8795
|
+
seed(payload.agent);
|
|
8796
|
+
} catch {}
|
|
8797
|
+
});
|
|
8798
|
+
if (opts.injectOnPrompt ?? true) ctx.on("agent/pre-step", async (payload, next) => {
|
|
8799
|
+
const { agent, messages } = payload;
|
|
8800
|
+
const cwd = agent.session?.header?.cwd ?? process.cwd();
|
|
8801
|
+
const root = findSerenityRoot(cwd);
|
|
8802
|
+
const key = agentKey(agent);
|
|
8803
|
+
const downstream = await next();
|
|
8804
|
+
if (isSkiffSessionId(key)) return downstream;
|
|
8805
|
+
if (root) {
|
|
8806
|
+
try {
|
|
8807
|
+
syncSafeModeRestriction(agent, root);
|
|
8808
|
+
} catch {}
|
|
8809
|
+
registerEntrySkillSection(agent, root);
|
|
8810
|
+
}
|
|
8811
|
+
if (!root || injected.has(key) || downstream.kind !== "enter") return downstream;
|
|
8812
|
+
injected.add(key);
|
|
8813
|
+
return {
|
|
8814
|
+
kind: "enter",
|
|
8815
|
+
messages: [
|
|
8816
|
+
accMessage(root, configPaths, entrySkillMaxChars),
|
|
8817
|
+
...messages,
|
|
8818
|
+
...downstream.messages
|
|
8819
|
+
]
|
|
8820
|
+
};
|
|
8821
|
+
});
|
|
8822
|
+
}
|
|
8823
|
+
//#endregion
|
|
8824
|
+
//#region src/seams/compact.ts
|
|
8825
|
+
/**
|
|
8826
|
+
* 注册压缩保留:compact/end(成功)后重注入 ACC 身份。
|
|
8827
|
+
* 仅当 agent 工作目录在 CCC 内时生效(激活门控)。
|
|
8828
|
+
*/
|
|
8829
|
+
function registerCompactRetention(ctx, opts = {}) {
|
|
8830
|
+
const configPaths = opts.configPaths ?? DEFAULT_SERENITY_CONFIG_PATHS;
|
|
8831
|
+
const entrySkillMaxChars = opts.entrySkillMaxChars ?? 3e4;
|
|
8832
|
+
ctx.on("session/event", (session, event) => {
|
|
8833
|
+
if (event.type !== "compaction/end") return;
|
|
8834
|
+
if (event.data.error) return;
|
|
8835
|
+
if (isSkiffSessionId(String(session.id ?? ""))) return;
|
|
8836
|
+
const agent = ctx.agents.get(session.id);
|
|
8837
|
+
if (!agent) return;
|
|
8838
|
+
const cwd = agent.session?.header?.cwd ?? process.cwd();
|
|
8839
|
+
const root = findSerenityRoot(cwd);
|
|
8840
|
+
if (!root) return;
|
|
8841
|
+
try {
|
|
8842
|
+
agent.inject(accMessage(root, configPaths, entrySkillMaxChars));
|
|
8843
|
+
} catch {}
|
|
8844
|
+
});
|
|
8845
|
+
}
|
|
8846
|
+
//#endregion
|
|
8847
|
+
//#region src/api.ts
|
|
8848
|
+
const ROUTE_PATH = "/serenity/status";
|
|
8849
|
+
const HANDYMEN_PATH = "/serenity/handymen";
|
|
8850
|
+
const UPLOAD_PATH = "/serenity/image-upload";
|
|
8851
|
+
const FILE_UPLOAD_PATH = "/serenity/file-upload";
|
|
8852
|
+
const CONFIG_PATH = "/serenity/config";
|
|
8853
|
+
const CCCS_PATH = "/serenity/cccs";
|
|
8854
|
+
const PUBLIC_ASK_PATH = "/serenity/public-ask";
|
|
8855
|
+
const AUTOTRAJECTORY_PATH = "/serenity/autotrajectory";
|
|
8856
|
+
/** 图片落盘目录(CCC 根相对;S142 图片自动识别基础设施——粘贴图片落盘供 agent 经 CCC vlm MSM 自主处理) */
|
|
8857
|
+
const IMAGE_UPLOAD_DIR = "_tmp/images_from_user";
|
|
8858
|
+
const IMAGE_MEDIA_TYPES = /* @__PURE__ */ new Set([
|
|
8859
|
+
"image/png",
|
|
8860
|
+
"image/jpeg",
|
|
8861
|
+
"image/webp",
|
|
8862
|
+
"image/gif"
|
|
8863
|
+
]);
|
|
8864
|
+
const EXT_BY_MEDIA = {
|
|
8865
|
+
"image/png": "png",
|
|
8866
|
+
"image/jpeg": "jpg",
|
|
8867
|
+
"image/webp": "webp",
|
|
8868
|
+
"image/gif": "gif"
|
|
8869
|
+
};
|
|
8870
|
+
const MAX_IMAGE_BYTES = 10485760;
|
|
8871
|
+
/** 文件落盘目录(CCC 根相对;v1.24.1 粘贴任意文件自动落盘——agent 经 CCC 既有 MSM(pdf-extract/archive-extract 等)自主处理) */
|
|
8872
|
+
const FILE_UPLOAD_DIR = "_tmp/files_from_user";
|
|
8873
|
+
/** 拒绝的可执行/危险扩展名(安全边界:不落盘可执行文件,防 agent 被诱导执行) */
|
|
8874
|
+
const BLOCKED_FILE_EXTS = /* @__PURE__ */ new Set([
|
|
8875
|
+
"exe",
|
|
8876
|
+
"dll",
|
|
8877
|
+
"msi",
|
|
8878
|
+
"bat",
|
|
8879
|
+
"cmd",
|
|
8880
|
+
"ps1",
|
|
8881
|
+
"com",
|
|
8882
|
+
"scr",
|
|
8883
|
+
"lnk",
|
|
8884
|
+
"sh",
|
|
8885
|
+
"vbs",
|
|
8886
|
+
"bin",
|
|
8887
|
+
"app",
|
|
8888
|
+
"deb",
|
|
8889
|
+
"rpm",
|
|
8890
|
+
"jar"
|
|
8891
|
+
]);
|
|
8892
|
+
const MAX_FILE_BYTES = 10485760;
|
|
8893
|
+
/**
|
|
8894
|
+
* 图片落盘核心逻辑(可测):校验 mediaType 白名单 + base64 解码 + 大小上限 →
|
|
8895
|
+
* 写 CCC 根 _tmp/images_from_user/<ts>-<rand>.<ext>,返回相对路径。
|
|
8896
|
+
* 校验失败抛 Error(handler 转 400)。
|
|
8897
|
+
*/
|
|
8898
|
+
function saveImageToTmp(root, mediaType, data) {
|
|
8899
|
+
if (!IMAGE_MEDIA_TYPES.has(mediaType)) throw new Error(`unsupported media type: ${mediaType}`);
|
|
8900
|
+
if (typeof data !== "string" || data.length === 0) throw new Error("missing image data");
|
|
8901
|
+
const bytes = Buffer.from(data, "base64");
|
|
8902
|
+
if (bytes.length === 0 || bytes.length > MAX_IMAGE_BYTES) throw new Error(`image size out of range: ${bytes.length} bytes (max ${MAX_IMAGE_BYTES})`);
|
|
8903
|
+
const dir = join(root, IMAGE_UPLOAD_DIR);
|
|
8904
|
+
mkdirSync(dir, { recursive: true });
|
|
8905
|
+
const ext = EXT_BY_MEDIA[mediaType] ?? "img";
|
|
8906
|
+
const filename = `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${Math.random().toString(36).slice(2, 8)}.${ext}`;
|
|
8907
|
+
writeFileSync(join(dir, filename), bytes);
|
|
8908
|
+
return `${IMAGE_UPLOAD_DIR}/${filename}`;
|
|
8909
|
+
}
|
|
8910
|
+
/**
|
|
8911
|
+
* 文件名脱敏(路径逃逸 + 非法字符):取 basename(去 / 与 \),剥离前导点/空,
|
|
8912
|
+
* 非法字符 → '-',限长。返回空则 'file'。
|
|
8913
|
+
*/
|
|
8914
|
+
function sanitizeFileName(name) {
|
|
8915
|
+
const cleaned = (name.split(/[\\/]/).pop() ?? "").replace(/[<>:"|?*\u0000-\u001f]/g, "-").replace(/^\.+/, "").trim();
|
|
8916
|
+
if (cleaned === "") return "file";
|
|
8917
|
+
return cleaned.slice(0, 100);
|
|
8918
|
+
}
|
|
8919
|
+
/**
|
|
8920
|
+
* 任意文件落盘核心逻辑(可测):文件名校验 + 可执行扩展名拒绝 + base64 解码 +
|
|
8921
|
+
* 大小上限(10MB)→ 写 CCC 根 _tmp/files_from_user/<ts>-<rand>-<safeName>,返回相对路径。
|
|
8922
|
+
* 校验失败抛 Error(handler 转 400)。
|
|
8923
|
+
*/
|
|
8924
|
+
function saveFileToTmp(root, fileName, data) {
|
|
8925
|
+
if (typeof fileName !== "string" || fileName.length === 0) throw new Error("missing file name");
|
|
8926
|
+
const ext = extname(fileName).slice(1).toLowerCase();
|
|
8927
|
+
if (ext !== "" && BLOCKED_FILE_EXTS.has(ext)) throw new Error(`blocked executable file type: .${ext}`);
|
|
8928
|
+
if (typeof data !== "string" || data.length === 0) throw new Error("missing file data");
|
|
8929
|
+
const bytes = Buffer.from(data, "base64");
|
|
8930
|
+
if (bytes.length === 0 || bytes.length > MAX_FILE_BYTES) throw new Error(`file size out of range: ${bytes.length} bytes (max ${MAX_FILE_BYTES})`);
|
|
8931
|
+
const dir = join(root, FILE_UPLOAD_DIR);
|
|
8932
|
+
mkdirSync(dir, { recursive: true });
|
|
8933
|
+
const safeName = sanitizeFileName(fileName);
|
|
8934
|
+
const filename = `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${Math.random().toString(36).slice(2, 8)}-${safeName}`;
|
|
8935
|
+
writeFileSync(join(dir, filename), bytes);
|
|
8936
|
+
return `${FILE_UPLOAD_DIR}/${filename}`;
|
|
8937
|
+
}
|
|
8938
|
+
function readBody$3(req, maxBytes) {
|
|
8939
|
+
return new Promise((resolve, reject) => {
|
|
8940
|
+
let data = "";
|
|
8941
|
+
req.on("data", (chunk) => {
|
|
8942
|
+
data += chunk.toString("utf-8");
|
|
8943
|
+
if (data.length > maxBytes) {
|
|
8944
|
+
reject(/* @__PURE__ */ new Error("body too large"));
|
|
8945
|
+
req.destroy();
|
|
8946
|
+
}
|
|
8947
|
+
});
|
|
8948
|
+
req.on("end", () => resolve(data));
|
|
8949
|
+
req.on("error", reject);
|
|
8950
|
+
});
|
|
8951
|
+
}
|
|
8952
|
+
function sendJson$2(res, code, body) {
|
|
8953
|
+
const payload = JSON.stringify(body);
|
|
8954
|
+
res.writeHead(code, {
|
|
8955
|
+
"content-type": "application/json; charset=utf-8",
|
|
8956
|
+
"cache-control": "no-store"
|
|
8957
|
+
});
|
|
8958
|
+
res.end(payload);
|
|
8959
|
+
}
|
|
8960
|
+
/**
|
|
8961
|
+
* 从请求参数解析 workspace:优先 sessionId → 会话 header.cwd;其次 workspace 参数;
|
|
8962
|
+
* 无参 → 遍历 live sessions 找第一个 CCC 会话的 cwd(远程访问时 sessionId 可能缺失,
|
|
8963
|
+
* 回退 process.cwd()=$HOME 会错误显示"未激活"——v1.22.2 修复);最后进程 cwd。
|
|
8964
|
+
* 纯逻辑(可单测):resolveWorkspaceCore(sessionCwd, workspaceParam, listCwds)。
|
|
8965
|
+
*/
|
|
8966
|
+
function resolveWorkspaceCore(sessionCwd, workspaceParam, listCwds, fallback) {
|
|
8967
|
+
if (sessionCwd) return sessionCwd;
|
|
8968
|
+
if (workspaceParam) return workspaceParam;
|
|
8969
|
+
for (const cwd of listCwds()) if (cwd && findSerenityRoot(cwd)) return cwd;
|
|
8970
|
+
return fallback;
|
|
8971
|
+
}
|
|
8972
|
+
function resolveWorkspace(ctx, params) {
|
|
8973
|
+
let sessionCwd;
|
|
8974
|
+
if (params.sessionId) sessionCwd = (ctx.sessions?.get?.(params.sessionId))?.header?.cwd;
|
|
8975
|
+
let listCwds = () => [];
|
|
8976
|
+
try {
|
|
8977
|
+
const sessions = ctx.sessions;
|
|
8978
|
+
if (typeof sessions?.list === "function") listCwds = () => sessions.list().map((s) => s.header?.cwd ?? "").filter(Boolean);
|
|
8979
|
+
} catch {}
|
|
8980
|
+
return resolveWorkspaceCore(sessionCwd, params.workspace, listCwds, process.cwd());
|
|
8981
|
+
}
|
|
8982
|
+
function registerStatusApi(ctx, opts = {}) {
|
|
8983
|
+
const configPaths = opts.configPaths ?? DEFAULT_SERENITY_CONFIG_PATHS;
|
|
8984
|
+
ctx.webServer.register({
|
|
8985
|
+
kind: "exact",
|
|
8986
|
+
path: HANDYMEN_PATH,
|
|
8987
|
+
handler: async (req, res) => {
|
|
8988
|
+
try {
|
|
8989
|
+
if (req.method !== "GET") {
|
|
8990
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
8991
|
+
return;
|
|
8992
|
+
}
|
|
8993
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
8994
|
+
const workspace = resolveWorkspace(ctx, {
|
|
8995
|
+
sessionId: url.searchParams.get("sessionId") ?? void 0,
|
|
8996
|
+
workspace: url.searchParams.get("workspace") ?? void 0
|
|
8997
|
+
});
|
|
8998
|
+
const root = findSerenityRoot(workspace);
|
|
8999
|
+
if (!root) {
|
|
9000
|
+
sendJson$2(res, 200, { handymen: [] });
|
|
9001
|
+
return;
|
|
9002
|
+
}
|
|
9003
|
+
sendJson$2(res, 200, { handymen: listActiveHandymen(root) });
|
|
9004
|
+
} catch (err) {
|
|
9005
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
9006
|
+
}
|
|
9007
|
+
}
|
|
9008
|
+
});
|
|
9009
|
+
ctx.webServer.register({
|
|
9010
|
+
kind: "exact",
|
|
9011
|
+
path: UPLOAD_PATH,
|
|
9012
|
+
handler: async (req, res) => {
|
|
9013
|
+
try {
|
|
9014
|
+
if (req.method !== "POST") {
|
|
9015
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
9016
|
+
return;
|
|
9017
|
+
}
|
|
9018
|
+
if (req.headers["x-serenity-ui"] !== "1") {
|
|
9019
|
+
sendJson$2(res, 403, { error: "图片落盘仅限 WebUI(client 专用)" });
|
|
9020
|
+
return;
|
|
9021
|
+
}
|
|
9022
|
+
const raw = await readBody$3(req, 20971520);
|
|
9023
|
+
const body = JSON.parse(raw);
|
|
9024
|
+
const workspace = resolveWorkspace(ctx, {
|
|
9025
|
+
sessionId: body.sessionId,
|
|
9026
|
+
workspace: body.workspace
|
|
9027
|
+
});
|
|
9028
|
+
const root = findSerenityRoot(workspace);
|
|
9029
|
+
if (!root) {
|
|
9030
|
+
sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
9031
|
+
return;
|
|
9032
|
+
}
|
|
9033
|
+
sendJson$2(res, 200, { path: saveImageToTmp(root, body.mediaType ?? "", body.data ?? "") });
|
|
9034
|
+
} catch (err) {
|
|
9035
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
9036
|
+
}
|
|
9037
|
+
}
|
|
9038
|
+
});
|
|
9039
|
+
ctx.webServer.register({
|
|
9040
|
+
kind: "exact",
|
|
9041
|
+
path: FILE_UPLOAD_PATH,
|
|
9042
|
+
handler: async (req, res) => {
|
|
9043
|
+
try {
|
|
8544
9044
|
if (req.method !== "POST") {
|
|
8545
9045
|
sendJson$2(res, 405, { error: "method not allowed" });
|
|
8546
9046
|
return;
|
|
@@ -8732,17 +9232,18 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
8732
9232
|
workspace: body.workspace
|
|
8733
9233
|
});
|
|
8734
9234
|
const root = findSerenityRoot(workspace);
|
|
8735
|
-
|
|
9235
|
+
const effectiveRoot = !body.workspace && !body.sessionId ? (await Promise.resolve().then(() => autotrajectory_exports)).resolveAutoTrajectoryCcc(ctx) ?? root : root;
|
|
9236
|
+
if (!effectiveRoot) {
|
|
8736
9237
|
sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
8737
9238
|
return;
|
|
8738
9239
|
}
|
|
8739
9240
|
const { performAutoTrajectoryWake, readAutoTrajectorySettings } = await Promise.resolve().then(() => autotrajectory_exports);
|
|
8740
|
-
const settings = readAutoTrajectorySettings(
|
|
9241
|
+
const settings = readAutoTrajectorySettings(effectiveRoot);
|
|
8741
9242
|
if (!settings) {
|
|
8742
9243
|
sendJson$2(res, 400, { error: "autotrajectory 未配置(.opencode/serenity.json 缺段)" });
|
|
8743
9244
|
return;
|
|
8744
9245
|
}
|
|
8745
|
-
const result = await performAutoTrajectoryWake(ctx,
|
|
9246
|
+
const result = await performAutoTrajectoryWake(ctx, effectiveRoot, settings, { force: true });
|
|
8746
9247
|
sendJson$2(res, result.ok ? 200 : 400, result);
|
|
8747
9248
|
return;
|
|
8748
9249
|
}
|
|
@@ -8756,12 +9257,13 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
8756
9257
|
workspace: url.searchParams.get("workspace") ?? void 0
|
|
8757
9258
|
});
|
|
8758
9259
|
const root = findSerenityRoot(workspace);
|
|
8759
|
-
|
|
9260
|
+
const { getAutoTrajectoryStatus, resolveAutoTrajectoryCcc } = await Promise.resolve().then(() => autotrajectory_exports);
|
|
9261
|
+
const effectiveRoot = !url.searchParams.get("workspace") && !url.searchParams.get("sessionId") ? resolveAutoTrajectoryCcc(ctx) ?? root : root;
|
|
9262
|
+
if (!effectiveRoot) {
|
|
8760
9263
|
sendJson$2(res, 200, { status: null });
|
|
8761
9264
|
return;
|
|
8762
9265
|
}
|
|
8763
|
-
|
|
8764
|
-
sendJson$2(res, 200, { status: getAutoTrajectoryStatus(root) });
|
|
9266
|
+
sendJson$2(res, 200, { status: getAutoTrajectoryStatus(effectiveRoot) });
|
|
8765
9267
|
} catch (err) {
|
|
8766
9268
|
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
8767
9269
|
}
|
|
@@ -9813,7 +10315,7 @@ function buildRebuke(hits) {
|
|
|
9813
10315
|
const rebukeStates = /* @__PURE__ */ new Map();
|
|
9814
10316
|
//#endregion
|
|
9815
10317
|
//#region src/output-guard-seam.ts
|
|
9816
|
-
const PLUGIN_SOURCE
|
|
10318
|
+
const PLUGIN_SOURCE = {
|
|
9817
10319
|
kind: "plugin",
|
|
9818
10320
|
plugin: "dsh-serenity-hooks"
|
|
9819
10321
|
};
|
|
@@ -9870,7 +10372,7 @@ function registerOutputGuardHook(ctx) {
|
|
|
9870
10372
|
type: "text",
|
|
9871
10373
|
text: buildRebuke(hits)
|
|
9872
10374
|
}],
|
|
9873
|
-
source: PLUGIN_SOURCE
|
|
10375
|
+
source: PLUGIN_SOURCE
|
|
9874
10376
|
}));
|
|
9875
10377
|
} catch (error) {
|
|
9876
10378
|
console.warn(`[serenity-hooks] output-guard steer failed: ${String(error?.message ?? error)}`);
|
|
@@ -12562,531 +13064,209 @@ const ASK_CSS = `
|
|
|
12562
13064
|
.msg .bubble { font-size: 15px; padding: 12px 14px; }
|
|
12563
13065
|
.msg .who { font-size: 12px; }
|
|
12564
13066
|
.chatInput { padding: 8px 12px calc(10px + env(safe-area-inset-bottom)); }
|
|
12565
|
-
.inputRow textarea { font-size: 16px; padding: 12px 14px; min-height: 48px; } /* 16px 防放大 + 触控 */
|
|
12566
|
-
.inputRow .sendBtn { font-size: 16px; padding: 12px 20px; min-height: 48px; }
|
|
12567
|
-
}
|
|
12568
|
-
`;
|
|
12569
|
-
/** 容器列表页(v1.26.2 → v1.26.6):key 在选容器前填写(v1.26.6 用户拍板:不填 key 不让选容器) */
|
|
12570
|
-
function publicAskListPage(cccs) {
|
|
12571
|
-
const cards = cccs.map((c) => `
|
|
12572
|
-
<a class="card" data-name="${c.name}" href="/c/${encodeURIComponent(c.name)}">
|
|
12573
|
-
<div class="cname">${c.name}</div>
|
|
12574
|
-
<div class="cmeta">${c.roles.length} 个问答角色 · 点击进入</div>
|
|
12575
|
-
</a>`).join("") || "<div class=\"empty\">暂无可问答的认知容器(管理员尚未开放)</div>";
|
|
12576
|
-
return `<!DOCTYPE html>
|
|
12577
|
-
<html lang="zh">
|
|
12578
|
-
<head>
|
|
12579
|
-
<meta charset="utf-8">
|
|
12580
|
-
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
12581
|
-
<title>Serenity Public Ask</title>
|
|
12582
|
-
<style>${ASK_CSS}</style>
|
|
12583
|
-
</head>
|
|
12584
|
-
<body>
|
|
12585
|
-
<main>
|
|
12586
|
-
<h1>Serenity Public Ask</h1>
|
|
12587
|
-
<div class="sub">先填写访问 key,再选择认知容器开始提问</div>
|
|
12588
|
-
<div class="keyGate">
|
|
12589
|
-
<label for="gateKey">访问 Key</label>
|
|
12590
|
-
<input id="gateKey" type="password" placeholder="请输入访问 key(仅本次浏览器记忆)" autocomplete="off" autocapitalize="none" enterkeyhint="done">
|
|
12591
|
-
</div>
|
|
12592
|
-
<div id="cards">${cards}</div>
|
|
12593
|
-
</main>
|
|
12594
|
-
<script>
|
|
12595
|
-
const KEY_STORE = 'serenity-public-ask-key'
|
|
12596
|
-
const gateKey = document.getElementById('gateKey')
|
|
12597
|
-
const cardList = document.getElementById('cards')
|
|
12598
|
-
// 容器卡片(key 未填时禁用——v1.26.6 用户拍板:key 在选容器前填写)
|
|
12599
|
-
function applyGate() {
|
|
12600
|
-
const ok = gateKey.value.trim().length > 0
|
|
12601
|
-
for (const card of cardList.querySelectorAll('a.card')) {
|
|
12602
|
-
card.classList.toggle('locked', !ok)
|
|
12603
|
-
card.setAttribute('aria-disabled', ok ? 'false' : 'true')
|
|
12604
|
-
if (!ok) { card.removeAttribute('href'); card.style.pointerEvents = 'none'; card.style.opacity = '.4' }
|
|
12605
|
-
else { card.setAttribute('href', '/c/' + encodeURIComponent(card.dataset.name)); card.style.pointerEvents = ''; card.style.opacity = '' }
|
|
12606
|
-
}
|
|
12607
|
-
}
|
|
12608
|
-
gateKey.addEventListener('input', () => {
|
|
12609
|
-
// 即存 localStorage(v1.26.7 修复:v1.26.6 漏掉保存——填一次就记录,对话页读取)
|
|
12610
|
-
try { localStorage.setItem(KEY_STORE, gateKey.value.trim()) } catch {}
|
|
12611
|
-
applyGate()
|
|
12612
|
-
})
|
|
12613
|
-
// key 自动从 localStorage 恢复
|
|
12614
|
-
let hasSavedKey = false
|
|
12615
|
-
try { const saved = localStorage.getItem(KEY_STORE); if (saved) { gateKey.value = saved; hasSavedKey = true } } catch {}
|
|
12616
|
-
if (hasSavedKey) gateKey.placeholder = '访问 Key(已记忆,可修改)'
|
|
12617
|
-
applyGate()
|
|
12618
|
-
gateKey.focus()
|
|
12619
|
-
<\/script>
|
|
12620
|
-
</body>
|
|
12621
|
-
</html>`;
|
|
12622
|
-
}
|
|
12623
|
-
/** 单容器问答页(v1.26.2→v1.26.6):URL 锁定容器;聊天 UI——消息流 + 连续对话 + key 从列表页记忆(v1.26.6 用户拍板:对话页不再填 key) */
|
|
12624
|
-
function publicAskContainerPage(ccc) {
|
|
12625
|
-
const data = jscSafeJsonText(JSON.stringify({
|
|
12626
|
-
name: ccc.name,
|
|
12627
|
-
root: ccc.root,
|
|
12628
|
-
roles: ccc.roles
|
|
12629
|
-
}).replace(/</g, "\\u003c"));
|
|
12630
|
-
return `<!DOCTYPE html>
|
|
12631
|
-
<html lang="zh">
|
|
12632
|
-
<head>
|
|
12633
|
-
<meta charset="utf-8">
|
|
12634
|
-
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
12635
|
-
<title>Serenity · ${ccc.name}</title>
|
|
12636
|
-
<style>${ASK_CSS}</style>
|
|
12637
|
-
</head>
|
|
12638
|
-
<body>
|
|
12639
|
-
<div class="chat">
|
|
12640
|
-
<div class="chatHead">
|
|
12641
|
-
<a class="back" href="/">← 容器</a>
|
|
12642
|
-
<span class="title">Serenity · <span class="ccname">${ccc.name}</span></span>
|
|
12643
|
-
<span class="spacer"></span>
|
|
12644
|
-
<select id="role" title="问答角色"></select>
|
|
12645
|
-
<button id="newChat" class="newBtn" title="清空当前对话(不存储,刷新即失)">新对话</button>
|
|
12646
|
-
</div>
|
|
12647
|
-
|
|
12648
|
-
<div id="msgs" class="msgList"></div>
|
|
12649
|
-
|
|
12650
|
-
<div class="chatInput">
|
|
12651
|
-
<div class="inputRow">
|
|
12652
|
-
<textarea id="q" placeholder="向 ${ccc.name} 提问…(Enter 发送,Shift+Enter 换行)" rows="1" enterkeyhint="send" autocapitalize="sentences"></textarea>
|
|
12653
|
-
<button id="ask" class="sendBtn">发送</button>
|
|
12654
|
-
</div>
|
|
12655
|
-
</div>
|
|
12656
|
-
</div>
|
|
12657
|
-
<script id="ask-data" type="application/json">${data}<\/script>
|
|
12658
|
-
<script>
|
|
12659
|
-
const CCC = JSON.parse(document.getElementById('ask-data').textContent)
|
|
12660
|
-
const KEY_STORE = 'serenity-public-ask-key'
|
|
12661
|
-
const roleSel = document.getElementById('role')
|
|
12662
|
-
const btn = document.getElementById('ask')
|
|
12663
|
-
const msgs = document.getElementById('msgs')
|
|
12664
|
-
const qInput = document.getElementById('q')
|
|
12665
|
-
let sessionId = ''
|
|
12666
|
-
let busy = false
|
|
12667
|
-
|
|
12668
|
-
// 角色下拉
|
|
12669
|
-
roleSel.innerHTML = (CCC.roles || []).map((r) => '<option value="' + r + '">' + r + '</option>').join('') || '<option value="">(无角色)</option>'
|
|
12670
|
-
// key 从 localStorage 读取(列表页填写时记忆;对话页不填——v1.26.6 用户拍板)
|
|
12671
|
-
function getStoredKey() {
|
|
12672
|
-
try { return (localStorage.getItem(KEY_STORE) || '').trim() } catch { return '' }
|
|
12673
|
-
}
|
|
12674
|
-
|
|
12675
|
-
/** 追加一条消息气泡(role: user|assistant) */
|
|
12676
|
-
function addMsg(role, htmlOrText, isHtml) {
|
|
12677
|
-
const m = document.createElement('div')
|
|
12678
|
-
m.className = 'msg ' + role
|
|
12679
|
-
const who = document.createElement('div')
|
|
12680
|
-
who.className = 'who'
|
|
12681
|
-
who.textContent = role === 'user' ? '你' : CCC.name
|
|
12682
|
-
const bubble = document.createElement('div')
|
|
12683
|
-
bubble.className = 'bubble'
|
|
12684
|
-
if (isHtml) bubble.innerHTML = htmlOrText
|
|
12685
|
-
else bubble.textContent = htmlOrText
|
|
12686
|
-
m.appendChild(who)
|
|
12687
|
-
m.appendChild(bubble)
|
|
12688
|
-
msgs.appendChild(m)
|
|
12689
|
-
msgs.scrollTop = msgs.scrollHeight
|
|
12690
|
-
return bubble
|
|
12691
|
-
}
|
|
12692
|
-
|
|
12693
|
-
/** 打字指示器(等待回答时显示) */
|
|
12694
|
-
function addTyping() {
|
|
12695
|
-
const m = document.createElement('div')
|
|
12696
|
-
m.className = 'msg assistant'
|
|
12697
|
-
const who = document.createElement('div')
|
|
12698
|
-
who.className = 'who'
|
|
12699
|
-
who.textContent = CCC.name
|
|
12700
|
-
const bubble = document.createElement('div')
|
|
12701
|
-
bubble.className = 'bubble'
|
|
12702
|
-
bubble.innerHTML = '<span class="typing"><i></i><i></i><i></i></span>'
|
|
12703
|
-
m.appendChild(who)
|
|
12704
|
-
m.appendChild(bubble)
|
|
12705
|
-
msgs.appendChild(m)
|
|
12706
|
-
msgs.scrollTop = msgs.scrollHeight
|
|
12707
|
-
return bubble
|
|
12708
|
-
}
|
|
12709
|
-
|
|
12710
|
-
async function ask() {
|
|
12711
|
-
const q = qInput.value.trim()
|
|
12712
|
-
const key = getStoredKey()
|
|
12713
|
-
const role = roleSel.value || undefined
|
|
12714
|
-
if (!q) return
|
|
12715
|
-
if (!key) { addMsg('assistant', '⚠️ 未找到访问 key,请先回到 <a href="/">容器列表页</a> 填写 key。', true); return }
|
|
12716
|
-
if (busy) return
|
|
12717
|
-
// 追加用户消息 + 打字指示
|
|
12718
|
-
addMsg('user', q, false)
|
|
12719
|
-
qInput.value = ''
|
|
12720
|
-
autoGrow()
|
|
12721
|
-
const typing = addTyping()
|
|
12722
|
-
busy = true
|
|
12723
|
-
btn.disabled = true
|
|
12724
|
-
try {
|
|
12725
|
-
const res = await fetch('/c/' + encodeURIComponent(CCC.name) + '/ask', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key, role, question: q, sessionId: sessionId || undefined }) })
|
|
12726
|
-
const data = await res.json()
|
|
12727
|
-
typing.remove() // 移除打字指示,替换为真实回答
|
|
12728
|
-
if (!res.ok) {
|
|
12729
|
-
sessionId = ''
|
|
12730
|
-
addMsg('assistant', '⚠️ ' + (data.error || ('HTTP ' + res.status)), true)
|
|
12731
|
-
return
|
|
12732
|
-
}
|
|
12733
|
-
addMsg('assistant', data.answer_html || data.answer || '(空回答)', true)
|
|
12734
|
-
sessionId = data.sessionId || ''
|
|
12735
|
-
} catch (err) {
|
|
12736
|
-
typing.remove()
|
|
12737
|
-
addMsg('assistant', '⚠️ ' + String(err.message || err), true)
|
|
12738
|
-
} finally {
|
|
12739
|
-
busy = false
|
|
12740
|
-
btn.disabled = false
|
|
12741
|
-
qInput.focus()
|
|
13067
|
+
.inputRow textarea { font-size: 16px; padding: 12px 14px; min-height: 48px; } /* 16px 防放大 + 触控 */
|
|
13068
|
+
.inputRow .sendBtn { font-size: 16px; padding: 12px 20px; min-height: 48px; }
|
|
13069
|
+
}
|
|
13070
|
+
`;
|
|
13071
|
+
/** 容器列表页(v1.26.2 → v1.26.6):key 在选容器前填写(v1.26.6 用户拍板:不填 key 不让选容器) */
|
|
13072
|
+
function publicAskListPage(cccs) {
|
|
13073
|
+
const cards = cccs.map((c) => `
|
|
13074
|
+
<a class="card" data-name="${c.name}" href="/c/${encodeURIComponent(c.name)}">
|
|
13075
|
+
<div class="cname">${c.name}</div>
|
|
13076
|
+
<div class="cmeta">${c.roles.length} 个问答角色 · 点击进入</div>
|
|
13077
|
+
</a>`).join("") || "<div class=\"empty\">暂无可问答的认知容器(管理员尚未开放)</div>";
|
|
13078
|
+
return `<!DOCTYPE html>
|
|
13079
|
+
<html lang="zh">
|
|
13080
|
+
<head>
|
|
13081
|
+
<meta charset="utf-8">
|
|
13082
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
13083
|
+
<title>Serenity Public Ask</title>
|
|
13084
|
+
<style>${ASK_CSS}</style>
|
|
13085
|
+
</head>
|
|
13086
|
+
<body>
|
|
13087
|
+
<main>
|
|
13088
|
+
<h1>Serenity Public Ask</h1>
|
|
13089
|
+
<div class="sub">先填写访问 key,再选择认知容器开始提问</div>
|
|
13090
|
+
<div class="keyGate">
|
|
13091
|
+
<label for="gateKey">访问 Key</label>
|
|
13092
|
+
<input id="gateKey" type="password" placeholder="请输入访问 key(仅本次浏览器记忆)" autocomplete="off" autocapitalize="none" enterkeyhint="done">
|
|
13093
|
+
</div>
|
|
13094
|
+
<div id="cards">${cards}</div>
|
|
13095
|
+
</main>
|
|
13096
|
+
<script>
|
|
13097
|
+
const KEY_STORE = 'serenity-public-ask-key'
|
|
13098
|
+
const gateKey = document.getElementById('gateKey')
|
|
13099
|
+
const cardList = document.getElementById('cards')
|
|
13100
|
+
// 容器卡片(key 未填时禁用——v1.26.6 用户拍板:key 在选容器前填写)
|
|
13101
|
+
function applyGate() {
|
|
13102
|
+
const ok = gateKey.value.trim().length > 0
|
|
13103
|
+
for (const card of cardList.querySelectorAll('a.card')) {
|
|
13104
|
+
card.classList.toggle('locked', !ok)
|
|
13105
|
+
card.setAttribute('aria-disabled', ok ? 'false' : 'true')
|
|
13106
|
+
if (!ok) { card.removeAttribute('href'); card.style.pointerEvents = 'none'; card.style.opacity = '.4' }
|
|
13107
|
+
else { card.setAttribute('href', '/c/' + encodeURIComponent(card.dataset.name)); card.style.pointerEvents = ''; card.style.opacity = '' }
|
|
12742
13108
|
}
|
|
12743
13109
|
}
|
|
12744
|
-
|
|
12745
|
-
|
|
12746
|
-
|
|
12747
|
-
|
|
12748
|
-
}
|
|
12749
|
-
|
|
12750
|
-
btn.addEventListener('click', () => void ask())
|
|
12751
|
-
qInput.addEventListener('keydown', (e) => {
|
|
12752
|
-
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); void ask() }
|
|
12753
|
-
})
|
|
12754
|
-
qInput.addEventListener('input', autoGrow)
|
|
12755
|
-
// 新对话:重置 sessionId + 清空消息流(前端内存,不存储——刷新即失)
|
|
12756
|
-
document.getElementById('newChat').addEventListener('click', () => {
|
|
12757
|
-
sessionId = ''
|
|
12758
|
-
msgs.innerHTML = ''
|
|
12759
|
-
qInput.focus()
|
|
13110
|
+
gateKey.addEventListener('input', () => {
|
|
13111
|
+
// 即存 localStorage(v1.26.7 修复:v1.26.6 漏掉保存——填一次就记录,对话页读取)
|
|
13112
|
+
try { localStorage.setItem(KEY_STORE, gateKey.value.trim()) } catch {}
|
|
13113
|
+
applyGate()
|
|
12760
13114
|
})
|
|
12761
|
-
|
|
12762
|
-
|
|
12763
|
-
|
|
12764
|
-
|
|
13115
|
+
// key 自动从 localStorage 恢复
|
|
13116
|
+
let hasSavedKey = false
|
|
13117
|
+
try { const saved = localStorage.getItem(KEY_STORE); if (saved) { gateKey.value = saved; hasSavedKey = true } } catch {}
|
|
13118
|
+
if (hasSavedKey) gateKey.placeholder = '访问 Key(已记忆,可修改)'
|
|
13119
|
+
applyGate()
|
|
13120
|
+
gateKey.focus()
|
|
12765
13121
|
<\/script>
|
|
12766
13122
|
</body>
|
|
12767
13123
|
</html>`;
|
|
12768
13124
|
}
|
|
12769
|
-
|
|
12770
|
-
|
|
12771
|
-
|
|
12772
|
-
|
|
12773
|
-
|
|
12774
|
-
|
|
12775
|
-
|
|
12776
|
-
|
|
12777
|
-
|
|
12778
|
-
|
|
12779
|
-
|
|
12780
|
-
|
|
12781
|
-
|
|
12782
|
-
|
|
12783
|
-
|
|
12784
|
-
|
|
12785
|
-
|
|
12786
|
-
|
|
12787
|
-
|
|
12788
|
-
|
|
12789
|
-
|
|
12790
|
-
|
|
12791
|
-
|
|
12792
|
-
|
|
12793
|
-
|
|
12794
|
-
|
|
12795
|
-
|
|
12796
|
-
|
|
12797
|
-
|
|
12798
|
-
|
|
12799
|
-
|
|
12800
|
-
|
|
12801
|
-
|
|
12802
|
-
|
|
12803
|
-
|
|
12804
|
-
|
|
12805
|
-
|
|
12806
|
-
|
|
12807
|
-
|
|
12808
|
-
|
|
12809
|
-
|
|
12810
|
-
|
|
12811
|
-
|
|
12812
|
-
|
|
12813
|
-
|
|
12814
|
-
|
|
12815
|
-
|
|
12816
|
-
|
|
12817
|
-
|
|
12818
|
-
|
|
12819
|
-
*/
|
|
12820
|
-
function inAllowedWakeWindow(nowMs, avoid) {
|
|
12821
|
-
const start = avoid?.start ?? DEFAULT_AVOID_HOURS.start;
|
|
12822
|
-
const end = avoid?.end ?? DEFAULT_AVOID_HOURS.end;
|
|
12823
|
-
const h = beijingHour(nowMs);
|
|
12824
|
-
if (start <= end) return h < start || h >= end;
|
|
12825
|
-
return h >= end && h < start;
|
|
12826
|
-
}
|
|
12827
|
-
/** 标志位判定:SESSION.md 所在目录名以 --auto 结尾 → 自主轨迹形态 */
|
|
12828
|
-
function isAutoTrajectorySession(mdPath) {
|
|
12829
|
-
return basename(dirname(mdPath)).endsWith(AUTO_DIR_SUFFIX);
|
|
12830
|
-
}
|
|
12831
|
-
/**
|
|
12832
|
-
* 目标会话定位:**必须配置 cfg.session**(S###/目录名)——CCC 日常有多条 trajectory 在跑,
|
|
12833
|
-
* 绝不默认唤起(缺省最近活跃会误伤其他正在运行的轨迹;用户拍板:必须配置才生效)。
|
|
12834
|
-
* 未配置 session 或未命中 → null(不唤起)。
|
|
12835
|
-
*/
|
|
12836
|
-
function resolveTargetMd(root, cfg) {
|
|
12837
|
-
if (!cfg?.session) return null;
|
|
12838
|
-
const found = findSession(sessionsRoot(root), cfg.session);
|
|
12839
|
-
if (!found) return null;
|
|
12840
|
-
const md = join(found.path, "SESSION.md");
|
|
12841
|
-
return existsSync(md) ? md : null;
|
|
12842
|
-
}
|
|
12843
|
-
/**
|
|
12844
|
-
* 唤起条件(纯逻辑,可测):enabled + 未在运行 + 目录标志 + mtime 超间隔 + 窗口允许。
|
|
12845
|
-
*/
|
|
12846
|
-
function shouldWake(settings, mdPath, nowMs, running) {
|
|
12847
|
-
if (!settings?.enabled || running) return false;
|
|
12848
|
-
if (!mdPath || !isAutoTrajectorySession(mdPath)) return false;
|
|
12849
|
-
if (!inAllowedWakeWindow(nowMs, settings.avoidWakeHours)) return false;
|
|
12850
|
-
try {
|
|
12851
|
-
const mtime = statSync(mdPath).mtimeMs;
|
|
12852
|
-
const hours = Math.max(1, settings.intervalHours ?? 12);
|
|
12853
|
-
return nowMs - mtime >= hours * 36e5;
|
|
12854
|
-
} catch {
|
|
12855
|
-
return false;
|
|
12856
|
-
}
|
|
12857
|
-
}
|
|
12858
|
-
/** 自生动机读取:SESSION.md「下一轮动机」段内容(到下一个二级标题或文件尾;无 → null) */
|
|
12859
|
-
function readSelfGeneratedMotivation(mdPath) {
|
|
12860
|
-
try {
|
|
12861
|
-
const content = readFileSync(mdPath, "utf-8");
|
|
12862
|
-
const idx = content.indexOf(MOTIVATION_MARKER);
|
|
12863
|
-
if (idx < 0) return null;
|
|
12864
|
-
const rest = content.slice(idx + 5);
|
|
12865
|
-
const nextHeading = rest.search(/\n## /);
|
|
12866
|
-
return (nextHeading >= 0 ? rest.slice(0, nextHeading) : rest).trim() || null;
|
|
12867
|
-
} catch {
|
|
12868
|
-
return null;
|
|
12869
|
-
}
|
|
12870
|
-
}
|
|
12871
|
-
/**
|
|
12872
|
-
* 偏见内容:直接运行 CCC 根目录下偏见提供者脚本(biasProvider,缺省 autotrajectory-bias.ts)→ stdout。
|
|
12873
|
-
* 脚本缺失 → 返回 { text: null, error: 提示实现 }(唤起侧报错要求实现,不静默跳过)。
|
|
12874
|
-
* 路径逃逸校验(resolveInside);bun 优先,node 兜底;600s 超时。
|
|
12875
|
-
*/
|
|
12876
|
-
async function fetchBiasContent(root, providerRel) {
|
|
12877
|
-
let scriptAbs;
|
|
12878
|
-
try {
|
|
12879
|
-
scriptAbs = resolveInside(root, providerRel);
|
|
12880
|
-
} catch {
|
|
12881
|
-
return {
|
|
12882
|
-
text: null,
|
|
12883
|
-
error: `biasProvider 路径逃逸(须在 CCC 根内): ${providerRel}`
|
|
12884
|
-
};
|
|
12885
|
-
}
|
|
12886
|
-
if (!existsSync(scriptAbs)) return {
|
|
12887
|
-
text: null,
|
|
12888
|
-
error: `请在 CCC 根目录实现偏见内容提供者脚本: ${providerRel}(stdout 输出偏见内容一行;acc_msm exec autotrajectory-exp init 可生成模板)`
|
|
12889
|
-
};
|
|
12890
|
-
const runs = [["bun", [scriptAbs]], [process.execPath, [scriptAbs]]];
|
|
12891
|
-
for (const [cmd, args] of runs) try {
|
|
12892
|
-
const r = spawnSync(cmd, args, {
|
|
12893
|
-
encoding: "utf-8",
|
|
12894
|
-
timeout: 6e5,
|
|
12895
|
-
stdio: [
|
|
12896
|
-
"ignore",
|
|
12897
|
-
"pipe",
|
|
12898
|
-
"pipe"
|
|
12899
|
-
]
|
|
12900
|
-
});
|
|
12901
|
-
if (r.status === 0) return {
|
|
12902
|
-
text: (r.stdout ?? "").trim() || null,
|
|
12903
|
-
error: null
|
|
12904
|
-
};
|
|
12905
|
-
if (r.error?.code === "ENOENT") continue;
|
|
12906
|
-
return {
|
|
12907
|
-
text: null,
|
|
12908
|
-
error: `偏见内容提供者脚本执行失败(exit ${r.status ?? "?"}): ${r.stderr?.trim() || r.stdout?.trim() || ""}`
|
|
12909
|
-
};
|
|
12910
|
-
} catch {
|
|
12911
|
-
continue;
|
|
12912
|
-
}
|
|
12913
|
-
return {
|
|
12914
|
-
text: null,
|
|
12915
|
-
error: "偏见内容提供者脚本无法运行(bun 与 node 均不可用)"
|
|
12916
|
-
};
|
|
12917
|
-
}
|
|
12918
|
-
/** 唤起消息(三段式:身份锚定 / 先验偏见[自生动机+偏见内容] / 任务)——注入前台会话,用户可见 */
|
|
12919
|
-
function buildWakeMessage(opts) {
|
|
12920
|
-
const lines = [
|
|
12921
|
-
`[自主轨迹唤起] — 距上次轨迹活动已满 ${opts.intervalHours} 小时,自动继续。`,
|
|
12922
|
-
"",
|
|
12923
|
-
`身份锚定:继续 ${opts.sessionName} 的 trajectory(SESSION.md: ${opts.mdPath})。`,
|
|
12924
|
-
"先验偏见:"
|
|
12925
|
-
];
|
|
12926
|
-
if (opts.motivation) lines.push(` · 自生动机:${opts.motivation}`);
|
|
12927
|
-
if (opts.biasContent) lines.push(` · 偏见内容:${opts.biasContent}`);
|
|
12928
|
-
if (!opts.motivation && !opts.biasContent) lines.push(" · (无——本轮纯自主探索)");
|
|
12929
|
-
lines.push("", "任务:执行一轮自主认知(探索/反事实检验),把产出写入 SESSION.md", "「自主探索日志」段,并预写「下一轮动机」段。完成后自然结束。");
|
|
12930
|
-
return lines.join("\n");
|
|
13125
|
+
/** 单容器问答页(v1.26.2→v1.26.6):URL 锁定容器;聊天 UI——消息流 + 连续对话 + key 从列表页记忆(v1.26.6 用户拍板:对话页不再填 key) */
|
|
13126
|
+
function publicAskContainerPage(ccc) {
|
|
13127
|
+
const data = jscSafeJsonText(JSON.stringify({
|
|
13128
|
+
name: ccc.name,
|
|
13129
|
+
root: ccc.root,
|
|
13130
|
+
roles: ccc.roles
|
|
13131
|
+
}).replace(/</g, "\\u003c"));
|
|
13132
|
+
return `<!DOCTYPE html>
|
|
13133
|
+
<html lang="zh">
|
|
13134
|
+
<head>
|
|
13135
|
+
<meta charset="utf-8">
|
|
13136
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
13137
|
+
<title>Serenity · ${ccc.name}</title>
|
|
13138
|
+
<style>${ASK_CSS}</style>
|
|
13139
|
+
</head>
|
|
13140
|
+
<body>
|
|
13141
|
+
<div class="chat">
|
|
13142
|
+
<div class="chatHead">
|
|
13143
|
+
<a class="back" href="/">← 容器</a>
|
|
13144
|
+
<span class="title">Serenity · <span class="ccname">${ccc.name}</span></span>
|
|
13145
|
+
<span class="spacer"></span>
|
|
13146
|
+
<select id="role" title="问答角色"></select>
|
|
13147
|
+
<button id="newChat" class="newBtn" title="清空当前对话(不存储,刷新即失)">新对话</button>
|
|
13148
|
+
</div>
|
|
13149
|
+
|
|
13150
|
+
<div id="msgs" class="msgList"></div>
|
|
13151
|
+
|
|
13152
|
+
<div class="chatInput">
|
|
13153
|
+
<div class="inputRow">
|
|
13154
|
+
<textarea id="q" placeholder="向 ${ccc.name} 提问…(Enter 发送,Shift+Enter 换行)" rows="1" enterkeyhint="send" autocapitalize="sentences"></textarea>
|
|
13155
|
+
<button id="ask" class="sendBtn">发送</button>
|
|
13156
|
+
</div>
|
|
13157
|
+
</div>
|
|
13158
|
+
</div>
|
|
13159
|
+
<script id="ask-data" type="application/json">${data}<\/script>
|
|
13160
|
+
<script>
|
|
13161
|
+
const CCC = JSON.parse(document.getElementById('ask-data').textContent)
|
|
13162
|
+
const KEY_STORE = 'serenity-public-ask-key'
|
|
13163
|
+
const roleSel = document.getElementById('role')
|
|
13164
|
+
const btn = document.getElementById('ask')
|
|
13165
|
+
const msgs = document.getElementById('msgs')
|
|
13166
|
+
const qInput = document.getElementById('q')
|
|
13167
|
+
let sessionId = ''
|
|
13168
|
+
let busy = false
|
|
13169
|
+
|
|
13170
|
+
// 角色下拉
|
|
13171
|
+
roleSel.innerHTML = (CCC.roles || []).map((r) => '<option value="' + r + '">' + r + '</option>').join('') || '<option value="">(无角色)</option>'
|
|
13172
|
+
// key 从 localStorage 读取(列表页填写时记忆;对话页不填——v1.26.6 用户拍板)
|
|
13173
|
+
function getStoredKey() {
|
|
13174
|
+
try { return (localStorage.getItem(KEY_STORE) || '').trim() } catch { return '' }
|
|
12931
13175
|
}
|
|
12932
|
-
|
|
12933
|
-
|
|
12934
|
-
|
|
12935
|
-
|
|
12936
|
-
|
|
12937
|
-
|
|
12938
|
-
|
|
12939
|
-
|
|
12940
|
-
|
|
12941
|
-
|
|
12942
|
-
|
|
12943
|
-
|
|
12944
|
-
|
|
12945
|
-
|
|
12946
|
-
|
|
12947
|
-
|
|
12948
|
-
|
|
12949
|
-
ok: false,
|
|
12950
|
-
detail: "当前在北京高峰避开窗口内(不唤起)"
|
|
12951
|
-
};
|
|
12952
|
-
try {
|
|
12953
|
-
const mtime = statSync(mdPath).mtimeMs;
|
|
12954
|
-
const hours = Math.max(1, settings.intervalHours ?? 12);
|
|
12955
|
-
if (now - mtime < hours * 36e5) return {
|
|
12956
|
-
ok: false,
|
|
12957
|
-
detail: `距上次轨迹活动不足 ${hours}h(等待中)`
|
|
12958
|
-
};
|
|
12959
|
-
} catch {
|
|
12960
|
-
return {
|
|
12961
|
-
ok: false,
|
|
12962
|
-
detail: "SESSION.md 读取失败(statSync)"
|
|
12963
|
-
};
|
|
12964
|
-
}
|
|
12965
|
-
}
|
|
12966
|
-
const agent = resolveTargetAgent(ctx, mdPath);
|
|
12967
|
-
if (!agent) return {
|
|
12968
|
-
ok: false,
|
|
12969
|
-
detail: "目标会话 agent 不可得(先在会话列表激活目标会话后重试)"
|
|
12970
|
-
};
|
|
12971
|
-
const provider = settings.biasProvider?.trim() || "autotrajectory-bias.ts";
|
|
12972
|
-
const biasRes = await fetchBiasContent(root, provider);
|
|
12973
|
-
if (biasRes.error) return {
|
|
12974
|
-
ok: false,
|
|
12975
|
-
detail: `偏见内容缺失:${biasRes.error}`
|
|
12976
|
-
};
|
|
12977
|
-
const motivation = readSelfGeneratedMotivation(mdPath);
|
|
12978
|
-
const message = buildWakeMessage({
|
|
12979
|
-
sessionName: basename(dirname(mdPath)),
|
|
12980
|
-
mdPath,
|
|
12981
|
-
intervalHours: Math.max(1, settings.intervalHours ?? 12),
|
|
12982
|
-
motivation,
|
|
12983
|
-
biasContent: biasRes.text
|
|
12984
|
-
});
|
|
12985
|
-
agent.steer(createUserMessage({
|
|
12986
|
-
content: [{
|
|
12987
|
-
type: "text",
|
|
12988
|
-
text: message
|
|
12989
|
-
}],
|
|
12990
|
-
source: PLUGIN_SOURCE
|
|
12991
|
-
}));
|
|
12992
|
-
return {
|
|
12993
|
-
ok: true,
|
|
12994
|
-
detail: `已唤起 ${basename(dirname(mdPath))}(偏见提供者 ${provider})`
|
|
12995
|
-
};
|
|
13176
|
+
|
|
13177
|
+
/** 追加一条消息气泡(role: user|assistant) */
|
|
13178
|
+
function addMsg(role, htmlOrText, isHtml) {
|
|
13179
|
+
const m = document.createElement('div')
|
|
13180
|
+
m.className = 'msg ' + role
|
|
13181
|
+
const who = document.createElement('div')
|
|
13182
|
+
who.className = 'who'
|
|
13183
|
+
who.textContent = role === 'user' ? '你' : CCC.name
|
|
13184
|
+
const bubble = document.createElement('div')
|
|
13185
|
+
bubble.className = 'bubble'
|
|
13186
|
+
if (isHtml) bubble.innerHTML = htmlOrText
|
|
13187
|
+
else bubble.textContent = htmlOrText
|
|
13188
|
+
m.appendChild(who)
|
|
13189
|
+
m.appendChild(bubble)
|
|
13190
|
+
msgs.appendChild(m)
|
|
13191
|
+
msgs.scrollTop = msgs.scrollHeight
|
|
13192
|
+
return bubble
|
|
12996
13193
|
}
|
|
12997
|
-
|
|
12998
|
-
|
|
12999
|
-
|
|
13000
|
-
|
|
13001
|
-
|
|
13002
|
-
|
|
13003
|
-
|
|
13004
|
-
|
|
13005
|
-
|
|
13006
|
-
|
|
13007
|
-
|
|
13008
|
-
|
|
13009
|
-
|
|
13010
|
-
|
|
13011
|
-
|
|
13012
|
-
|
|
13013
|
-
const res = await performAutoTrajectoryWake(ctx, root, settings, { force: false });
|
|
13014
|
-
if (res.ok) console.log(`[serenity-hooks] ✓ 自主轨迹唤起(${res.detail})`);
|
|
13015
|
-
else console.warn(`[serenity-hooks] ✗ 自主轨迹唤起跳过:${res.detail}`);
|
|
13016
|
-
} catch (err) {
|
|
13017
|
-
console.warn(`[serenity-hooks] ✗ 自主轨迹唤起失败: ${String(err?.message ?? err)}`);
|
|
13018
|
-
} finally {
|
|
13019
|
-
running = false;
|
|
13020
|
-
}
|
|
13021
|
-
})();
|
|
13022
|
-
};
|
|
13023
|
-
setInterval(tick, TICK_MS).unref();
|
|
13024
|
-
tick();
|
|
13194
|
+
|
|
13195
|
+
/** 打字指示器(等待回答时显示) */
|
|
13196
|
+
function addTyping() {
|
|
13197
|
+
const m = document.createElement('div')
|
|
13198
|
+
m.className = 'msg assistant'
|
|
13199
|
+
const who = document.createElement('div')
|
|
13200
|
+
who.className = 'who'
|
|
13201
|
+
who.textContent = CCC.name
|
|
13202
|
+
const bubble = document.createElement('div')
|
|
13203
|
+
bubble.className = 'bubble'
|
|
13204
|
+
bubble.innerHTML = '<span class="typing"><i></i><i></i><i></i></span>'
|
|
13205
|
+
m.appendChild(who)
|
|
13206
|
+
m.appendChild(bubble)
|
|
13207
|
+
msgs.appendChild(m)
|
|
13208
|
+
msgs.scrollTop = msgs.scrollHeight
|
|
13209
|
+
return bubble
|
|
13025
13210
|
}
|
|
13026
|
-
|
|
13027
|
-
function
|
|
13028
|
-
|
|
13029
|
-
|
|
13030
|
-
|
|
13031
|
-
|
|
13032
|
-
|
|
13033
|
-
|
|
13034
|
-
|
|
13035
|
-
|
|
13036
|
-
|
|
13037
|
-
|
|
13211
|
+
|
|
13212
|
+
async function ask() {
|
|
13213
|
+
const q = qInput.value.trim()
|
|
13214
|
+
const key = getStoredKey()
|
|
13215
|
+
const role = roleSel.value || undefined
|
|
13216
|
+
if (!q) return
|
|
13217
|
+
if (!key) { addMsg('assistant', '⚠️ 未找到访问 key,请先回到 <a href="/">容器列表页</a> 填写 key。', true); return }
|
|
13218
|
+
if (busy) return
|
|
13219
|
+
// 追加用户消息 + 打字指示
|
|
13220
|
+
addMsg('user', q, false)
|
|
13221
|
+
qInput.value = ''
|
|
13222
|
+
autoGrow()
|
|
13223
|
+
const typing = addTyping()
|
|
13224
|
+
busy = true
|
|
13225
|
+
btn.disabled = true
|
|
13226
|
+
try {
|
|
13227
|
+
const res = await fetch('/c/' + encodeURIComponent(CCC.name) + '/ask', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key, role, question: q, sessionId: sessionId || undefined }) })
|
|
13228
|
+
const data = await res.json()
|
|
13229
|
+
typing.remove() // 移除打字指示,替换为真实回答
|
|
13230
|
+
if (!res.ok) {
|
|
13231
|
+
sessionId = ''
|
|
13232
|
+
addMsg('assistant', '⚠️ ' + (data.error || ('HTTP ' + res.status)), true)
|
|
13233
|
+
return
|
|
13234
|
+
}
|
|
13235
|
+
addMsg('assistant', data.answer_html || data.answer || '(空回答)', true)
|
|
13236
|
+
sessionId = data.sessionId || ''
|
|
13237
|
+
} catch (err) {
|
|
13238
|
+
typing.remove()
|
|
13239
|
+
addMsg('assistant', '⚠️ ' + String(err.message || err), true)
|
|
13240
|
+
} finally {
|
|
13241
|
+
busy = false
|
|
13242
|
+
btn.disabled = false
|
|
13243
|
+
qInput.focus()
|
|
13244
|
+
}
|
|
13038
13245
|
}
|
|
13039
|
-
|
|
13040
|
-
function
|
|
13041
|
-
|
|
13042
|
-
|
|
13043
|
-
try {
|
|
13044
|
-
const sessions = ctx.sessions;
|
|
13045
|
-
for (const s of sessions?.list?.() ?? []) {
|
|
13046
|
-
const r = findSerenityRoot(s?.header?.cwd ?? "");
|
|
13047
|
-
if (r) return r;
|
|
13048
|
-
}
|
|
13049
|
-
} catch {}
|
|
13050
|
-
return null;
|
|
13246
|
+
|
|
13247
|
+
function autoGrow() {
|
|
13248
|
+
qInput.style.height = 'auto'
|
|
13249
|
+
qInput.style.height = Math.min(qInput.scrollHeight, 140) + 'px'
|
|
13051
13250
|
}
|
|
13052
|
-
|
|
13053
|
-
|
|
13054
|
-
|
|
13055
|
-
|
|
13056
|
-
|
|
13057
|
-
|
|
13058
|
-
|
|
13059
|
-
|
|
13060
|
-
|
|
13061
|
-
|
|
13062
|
-
|
|
13063
|
-
|
|
13064
|
-
|
|
13065
|
-
|
|
13066
|
-
|
|
13067
|
-
|
|
13068
|
-
|
|
13069
|
-
|
|
13070
|
-
|
|
13071
|
-
};
|
|
13072
|
-
const mdPath = resolveTargetMd(root, cfg);
|
|
13073
|
-
if (!mdPath) return {
|
|
13074
|
-
...base,
|
|
13075
|
-
target: null
|
|
13076
|
-
};
|
|
13077
|
-
let idleHours = 0;
|
|
13078
|
-
try {
|
|
13079
|
-
idleHours = (now - statSync(mdPath).mtimeMs) / 36e5;
|
|
13080
|
-
} catch {}
|
|
13081
|
-
return {
|
|
13082
|
-
...base,
|
|
13083
|
-
target: {
|
|
13084
|
-
dirName: basename(dirname(mdPath)),
|
|
13085
|
-
autoFlag: isAutoTrajectorySession(mdPath),
|
|
13086
|
-
idleHours: Math.max(0, idleHours),
|
|
13087
|
-
wakeable: shouldWake(cfg, mdPath, now, false)
|
|
13088
|
-
}
|
|
13089
|
-
};
|
|
13251
|
+
|
|
13252
|
+
btn.addEventListener('click', () => void ask())
|
|
13253
|
+
qInput.addEventListener('keydown', (e) => {
|
|
13254
|
+
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); void ask() }
|
|
13255
|
+
})
|
|
13256
|
+
qInput.addEventListener('input', autoGrow)
|
|
13257
|
+
// 新对话:重置 sessionId + 清空消息流(前端内存,不存储——刷新即失)
|
|
13258
|
+
document.getElementById('newChat').addEventListener('click', () => {
|
|
13259
|
+
sessionId = ''
|
|
13260
|
+
msgs.innerHTML = ''
|
|
13261
|
+
qInput.focus()
|
|
13262
|
+
})
|
|
13263
|
+
|
|
13264
|
+
// 初始欢迎消息(外层模板求值注入容器名;textContent 渲染防注入)
|
|
13265
|
+
addMsg('assistant', '你好,我是「${ccc.name}」的认知助手。有什么可以帮你的?(本对话仅当前页面有效,刷新即失)', false)
|
|
13266
|
+
qInput.focus()
|
|
13267
|
+
<\/script>
|
|
13268
|
+
</body>
|
|
13269
|
+
</html>`;
|
|
13090
13270
|
}
|
|
13091
13271
|
//#endregion
|
|
13092
13272
|
//#region src/index.ts
|
|
@@ -13144,7 +13324,7 @@ function apply(ctx, config) {
|
|
|
13144
13324
|
ctx.tools.register(createRebuildTool(ctx));
|
|
13145
13325
|
ctx.tools.register(localstoreTool);
|
|
13146
13326
|
ctx.tools.register(skiffAdminTool);
|
|
13147
|
-
ctx.tools.register(
|
|
13327
|
+
ctx.tools.register(createAutoTrajectoryExpTool(ctx));
|
|
13148
13328
|
}
|
|
13149
13329
|
if (config.guards) registerGuards(ctx, { configPaths: config.serenityConfigPaths });
|
|
13150
13330
|
if (config.keeper) registerKeeper(ctx, {
|