@shgroup/dsh-serenity-hooks 1.23.0 → 1.23.1
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/client.js +170 -5
- package/lib/index.js +463 -414
- package/package.json +1 -1
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-serenity-hooks",
|
|
3
|
-
"version": "1.23.
|
|
3
|
+
"version": "1.23.1",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"description": "宁静号 ACC harness(Native Cordis 插件):真实 DSH 工具 cc_fs/session/acc_msm/eap/neat/cce/loop/session_rebuild + 拦截缝机械约束(safe-mode/路径守卫)+ 高级设定面板(双端口网关/账号管理)。适配 DSH 公开版(0.1.0-rc,deepseek-ai/deepseek-harness)。",
|
|
6
6
|
"engines": {
|
package/lib/client.js
CHANGED
|
@@ -8,11 +8,11 @@ window.__ModuleLoader__.load({
|
|
|
8
8
|
let react = require("react");
|
|
9
9
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
10
10
|
//#region \0sp-css:/home/yh/home/home-serenity/AI_LAB/dsh-serenity-plugin/hooks/dsh-serenity-hooks/src/client/SafeModePanel
|
|
11
|
-
const css$
|
|
11
|
+
const css$3 = "/* SafeModePanel — 会话头部 Serenity 状态卡片(v1.22 UX 重构)\n * 卡片 → 点击 → 官方 Modal(body portal 遮罩 + 居中)\n * 模态内:tab 栏 + 分组(groupTitle 标题 + rowCard 行卡)——看齐官方 settings 设计语言\n * 遵循 web-styling.md:颜色只用 --dsw-alias-* 语义 token(明暗自适应);\n * 前缀 sp- 防冲突。 */\n\n/* ── 头部状态卡片(可点击) ── */\n\n.sp-card {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 26px;\n padding: 0 10px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n background: var(--dsw-alias-bg-module-platform);\n color: inherit;\n font-size: 12px;\n cursor: pointer;\n transition: background var(--ds-transition-duration-fast, 0.1s) var(--ds-ease-in-out, ease),\n border-color var(--ds-transition-duration-fast, 0.1s) var(--ds-ease-in-out, ease);\n}\n\n.sp-card:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n border-color: var(--dsw-alias-border-l2);\n}\n\n.sp-card:focus-visible {\n outline: 2px solid var(--dsw-alias-button-ghost-active-border);\n outline-offset: 1px;\n}\n\n.sp-card:disabled {\n opacity: 0.6;\n cursor: default;\n}\n\n.sp-dot {\n flex: none;\n width: 7px;\n height: 7px;\n border-radius: 50%;\n}\n\n.sp-dotOn {\n background: var(--dsw-alias-state-success-primary);\n}\n\n.sp-dotOff {\n background: var(--dsw-alias-label-dimmed);\n}\n\n.sp-brand {\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n\n.sp-chev {\n color: var(--dsw-alias-label-dimmed);\n}\n\n/* safe-mode 徽标(卡片本体直接可见;ON 红 / OFF 灰) */\n.sp-sm {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n height: 18px;\n padding: 0 7px;\n border-radius: 9px;\n font-size: 11px;\n font-weight: 600;\n line-height: 1;\n}\n\n.sp-smOn {\n background: var(--dsw-alias-state-warning-bg, rgba(245, 158, 11, 0.18));\n color: var(--dsw-alias-state-warning-fg, #f59e0b);\n border: 1px solid var(--dsw-alias-state-warning-border, rgba(245, 158, 11, 0.4));\n}\n\n.sp-smOff {\n background: var(--dsw-alias-interactive-bg-active, rgba(128, 128, 128, 0.16));\n color: var(--dsw-alias-label-dimmed);\n border: 1px solid var(--dsw-alias-border-l1);\n}\n\n.sp-smIcon {\n flex: none;\n}\n\n/* ── 官方 Modal 定制(宽度 + 内容紧凑) ── */\n\n.sp-modal {\n width: 520px;\n max-width: calc(100vw - 48px);\n}\n\n/* ── tab 栏 ── */\n\n.sp-tabs {\n display: flex;\n gap: 6px;\n padding: 0 2px 12px;\n border-bottom: 1px solid var(--dsw-alias-border-l1);\n}\n\n.sp-tab {\n flex: 1;\n padding: 8px 0;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n transition: background var(--ds-transition-duration-fast, 0.1s) var(--ds-ease-in-out, ease);\n}\n\n.sp-tab:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.sp-tabOn {\n background: var(--dsw-alias-interactive-bg-active, rgba(128, 128, 128, 0.2));\n color: var(--dsw-alias-label-primary);\n font-weight: 600;\n}\n\n/* ── 模态内容 ── */\n\n.sp-modalBody {\n display: flex;\n flex-direction: column;\n gap: 16px;\n padding-top: 14px;\n /* 内容超出视口时 body 区滚动(兜底——不再截断);整卡不滚动 */\n overflow-y: auto;\n max-height: calc(100vh - 200px);\n}\n\n/* ── 分组(官方 settings 语言:groupTitle + rows) ── */\n\n.sp-group {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.sp-groupHead {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n\n.sp-groupTitle {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n font-weight: 600;\n letter-spacing: 0.02em;\n text-transform: uppercase;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sp-rows {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n/* 行卡(rowCard:border + radius + padding;值与 label 分离、长内容可换行) */\n.sp-row {\n display: flex;\n align-items: baseline;\n gap: 12px;\n min-width: 0;\n padding: 8px 12px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-module);\n}\n\n.sp-label {\n flex: none;\n width: 76px;\n font-size: 12px;\n font-weight: 500;\n color: var(--dsw-alias-label-secondary);\n white-space: nowrap;\n}\n\n.sp-value {\n flex: 1;\n min-width: 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n font-family: var(--ds-font-family-code, monospace);\n word-break: break-all;\n overflow-wrap: anywhere;\n}\n\n/* 安全模式动作区 */\n.sp-actions {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 8px 12px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-module);\n}\n\n.sp-toggle {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 24px;\n padding: 0 10px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 6px;\n font-size: 12px;\n cursor: pointer;\n transition: background var(--ds-transition-duration-fast, 0.1s) var(--ds-ease-in-out, ease);\n}\n\n.sp-toggleLg {\n height: 30px;\n padding: 0 16px;\n font-size: 13px;\n}\n\n.sp-toggleOn {\n background: var(--dsw-alias-state-warning-bg, rgba(245, 158, 11, 0.15));\n color: var(--dsw-alias-state-warning-fg, #f59e0b);\n border-color: var(--dsw-alias-state-warning-border, rgba(245, 158, 11, 0.4));\n}\n\n.sp-toggleOff {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n\n.sp-toggleIcon {\n flex: none;\n}\n\n.sp-toggleDesc {\n flex: 1;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* loop 运行状态区块 */\n\n.sp-loopsToggle {\n flex: none;\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 11px;\n cursor: pointer;\n}\n\n.sp-loopsToggle:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.sp-loopsList {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin-top: 2px;\n}\n\n.sp-loopItem {\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 8px 12px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n min-width: 0;\n}\n\n.sp-loopHead {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n}\n\n.sp-loopLabel {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 12px;\n font-weight: 500;\n color: var(--dsw-alias-label-primary);\n}\n\n.sp-loopRound {\n flex: none;\n font-size: 11px;\n color: var(--dsw-alias-label-dimmed);\n}\n\n.sp-loopTime {\n flex: none;\n font-size: 11px;\n color: var(--dsw-alias-label-dimmed);\n}\n\n.sp-loopResp {\n font-size: 11px;\n line-height: 16px;\n color: var(--dsw-alias-label-secondary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.sp-loopsMore {\n font-size: 11px;\n color: var(--dsw-alias-label-dimmed);\n padding: 4px 12px;\n}\n\n/* 底部提示(配置入口引导) */\n.sp-hint {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n word-break: break-word;\n overflow-wrap: anywhere;\n padding: 8px 12px;\n border: 1px dashed var(--dsw-alias-border-l2);\n border-radius: 10px;\n}\n";
|
|
12
12
|
if (typeof document !== "undefined" && document.querySelector("style[data-sp-css]") === null) {
|
|
13
13
|
const tag = document.createElement("style");
|
|
14
14
|
tag.setAttribute("data-sp-css", "1");
|
|
15
|
-
tag.textContent = css$
|
|
15
|
+
tag.textContent = css$3;
|
|
16
16
|
document.head.appendChild(tag);
|
|
17
17
|
}
|
|
18
18
|
//#endregion
|
|
@@ -528,7 +528,7 @@ window.__ModuleLoader__.load({
|
|
|
528
528
|
if (!res.ok) return null;
|
|
529
529
|
return (await res.json()).config ?? null;
|
|
530
530
|
}
|
|
531
|
-
/** PUT 配置(账号 patch +
|
|
531
|
+
/** PUT 配置(账号 patch + 工作区白名单 + persona 彩蛋)→ 返回保存后的 wire */
|
|
532
532
|
async function saveConfig(patch) {
|
|
533
533
|
const res = await fetch(CONFIG_PATH, {
|
|
534
534
|
method: "PUT",
|
|
@@ -546,11 +546,11 @@ window.__ModuleLoader__.load({
|
|
|
546
546
|
}
|
|
547
547
|
//#endregion
|
|
548
548
|
//#region \0sp-css:/home/yh/home/home-serenity/AI_LAB/dsh-serenity-plugin/hooks/dsh-serenity-hooks/src/client/AccountsEditor
|
|
549
|
-
const css$
|
|
549
|
+
const css$2 = "/* AccountsEditor — DSH 设置面板「外部访问」区块(v1.22)\n * 与 SettingsSection ss-* 同设计语言:--dsw-alias-* 语义 token,明暗自适应。 */\n\n.ae-root {\n display: flex;\n flex-direction: column;\n gap: 16px;\n padding: 12px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 12px;\n font-size: 13px;\n}\n\n.ae-warn {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-state-warning-fg, #f59e0b);\n word-break: break-word;\n}\n\n.ae-block {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.ae-title {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n font-weight: 600;\n letter-spacing: 0.02em;\n text-transform: uppercase;\n color: var(--dsw-alias-label-secondary);\n}\n\n.ae-desc {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n word-break: break-word;\n overflow-wrap: anywhere;\n}\n\n.ae-note {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* ── 监听设置 / 白名单输入行 ── */\n\n.ae-row {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.ae-label {\n flex: none;\n font-size: 12px;\n font-weight: 500;\n color: var(--dsw-alias-label-secondary);\n white-space: nowrap;\n}\n\n.ae-input {\n flex: 1;\n min-width: 0;\n padding: 6px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: var(--dsw-alias-bg-module);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n outline: none;\n}\n\n.ae-input:focus {\n border-color: var(--dsw-alias-button-primary-bg, var(--dsw-alias-accent, #3b82f6));\n}\n\n.ae-input::placeholder {\n color: var(--dsw-alias-label-dimmed);\n}\n\n.ae-input.ae-port {\n flex: none;\n width: 76px;\n}\n\n/* ── 账号列表 ── */\n\n.ae-list {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.ae-head {\n display: flex;\n gap: 8px;\n font-size: 11px;\n font-weight: 500;\n color: var(--dsw-alias-label-dimmed);\n padding: 0 2px;\n text-transform: uppercase;\n letter-spacing: 0.02em;\n}\n\n.ae-item {\n display: flex;\n gap: 8px;\n align-items: center;\n padding: 6px 2px;\n}\n\n.ae-colUser { flex: 1.2; }\n.ae-colPass { flex: 1.6; }\n.ae-colTotp { flex: 2.2; min-width: 0; }\n.ae-colOp { flex: none; width: 48px; }\n\n/* ── v1.22.4 TOTP 第二因素 ── */\n\n.ae-totpBtn {\n flex: none;\n padding: 4px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n white-space: nowrap;\n}\n\n.ae-totpBtn:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.ae-totpPending {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n align-items: center;\n min-width: 0;\n}\n\n.ae-totpSecret {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n background: var(--dsw-alias-surface-raised, rgba(0, 0, 0, 0.25));\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 4px;\n padding: 2px 6px;\n color: var(--dsw-alias-label-secondary);\n word-break: break-all;\n}\n\n.ae-totpUri {\n font-size: 11px;\n color: var(--dsw-alias-accent, #3b82f6);\n text-decoration: none;\n}\n\n.ae-totpUri:hover {\n text-decoration: underline;\n}\n\n.ae-totpConfirm {\n max-width: 120px;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n letter-spacing: 0.2em;\n}\n\n.ae-totpBadge {\n font-size: 12px;\n color: var(--dsw-alias-state-warning-fg, #f59e0b);\n}\n\n.ae-totpClear {\n display: flex;\n gap: 6px;\n align-items: center;\n}\n\n.ae-check {\n display: flex;\n gap: 8px;\n align-items: center;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n margin-top: 10px;\n cursor: pointer;\n}\n\n.ae-check input {\n accent-color: var(--dsw-alias-accent, #3b82f6);\n}\n\n.ae-del {\n flex: none;\n padding: 4px 10px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-state-danger-fg, #f87171);\n font-size: 12px;\n cursor: pointer;\n}\n\n.ae-del:hover {\n background: var(--dsw-alias-state-danger-bg, rgba(248, 113, 113, 0.12));\n}\n\n.ae-add {\n align-self: flex-start;\n padding: 5px 12px;\n border: 1px dashed var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n.ae-add:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.ae-wsSelect {\n margin-top: 8px;\n max-width: 100%;\n cursor: pointer;\n}\n\n/* ── 工作区白名单 chips ── */\n\n.ae-wsList {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n}\n\n.ae-wsChip {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 3px 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: var(--dsw-alias-interactive-bg-active, rgba(128, 128, 128, 0.14));\n}\n\n.ae-wsPath {\n font-size: 12px;\n color: var(--dsw-alias-label-primary);\n font-family: var(--ds-font-family-code, monospace);\n word-break: break-all;\n}\n\n.ae-wsDel {\n flex: none;\n width: 18px;\n height: 18px;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: var(--dsw-alias-label-dimmed);\n font-size: 13px;\n line-height: 1;\n cursor: pointer;\n}\n\n.ae-wsDel:hover {\n background: var(--dsw-alias-state-danger-bg, rgba(248, 113, 113, 0.18));\n color: var(--dsw-alias-state-danger-fg, #f87171);\n}\n\n/* ── 错误 / 操作区 ── */\n\n.ae-error {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-state-danger-fg, #f87171);\n word-break: break-word;\n}\n\n.ae-actions {\n display: flex;\n justify-content: flex-end;\n padding-top: 4px;\n border-top: 1px solid var(--dsw-alias-border-l1);\n}\n\n.ae-save {\n padding: 7px 24px;\n border: 0;\n border-radius: 8px;\n background: var(--dsw-alias-button-primary-bg, var(--dsw-alias-accent, #3b82f6));\n color: var(--dsw-alias-button-primary-fg, #fff);\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n}\n\n.ae-save:hover:not(:disabled) {\n filter: brightness(1.08);\n}\n\n.ae-save:disabled {\n opacity: 0.6;\n cursor: default;\n}\n";
|
|
550
550
|
if (typeof document !== "undefined" && document.querySelector("style[data-sp-css]") === null) {
|
|
551
551
|
const tag = document.createElement("style");
|
|
552
552
|
tag.setAttribute("data-sp-css", "1");
|
|
553
|
-
tag.textContent = css$
|
|
553
|
+
tag.textContent = css$2;
|
|
554
554
|
document.head.appendChild(tag);
|
|
555
555
|
}
|
|
556
556
|
//#endregion
|
|
@@ -947,6 +947,164 @@ window.__ModuleLoader__.load({
|
|
|
947
947
|
});
|
|
948
948
|
}
|
|
949
949
|
//#endregion
|
|
950
|
+
//#region \0sp-css:/home/yh/home/home-serenity/AI_LAB/dsh-serenity-plugin/hooks/dsh-serenity-hooks/src/client/PersonaEditor
|
|
951
|
+
const css$1 = "/* PersonaEditor.css — 彩蛋模式区块(v1.23.1,官方 settings 设计语言:rowCard 内分组) */\n\n.pe-wrap {\n display: flex;\n flex-direction: column;\n gap: 10px;\n width: 100%;\n padding: 2px 0 6px;\n}\n\n.pe-row {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.pe-label {\n font-size: 12px;\n color: var(--dsw-alias-text-secondary, #8a8f98);\n user-select: none;\n}\n\n.pe-input,\n.pe-textarea {\n width: 100%;\n box-sizing: border-box;\n background: var(--dsw-alias-bg-secondary, rgba(128, 128, 128, 0.08));\n color: var(--dsw-alias-text-primary, #e6e6e6);\n border: 1px solid var(--dsw-alias-border, rgba(128, 128, 128, 0.25));\n border-radius: 8px;\n font-size: 13px;\n padding: 8px 10px;\n outline: none;\n}\n\n.pe-input:focus,\n.pe-textarea:focus {\n border-color: var(--dsw-alias-accent, #4f8cff);\n}\n\n.pe-textarea {\n resize: vertical;\n min-height: 120px;\n line-height: 1.5;\n font-family: var(--dsw-alias-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);\n font-size: 12px;\n}\n\n.pe-actions {\n display: flex;\n gap: 8px;\n align-items: center;\n flex-wrap: wrap;\n}\n\n.pe-btn {\n border-radius: 6px;\n padding: 5px 12px;\n font-size: 12px;\n cursor: pointer;\n border: 1px solid var(--dsw-alias-border, rgba(128, 128, 128, 0.25));\n background: transparent;\n color: var(--dsw-alias-text-primary, #e6e6e6);\n transition: opacity 0.15s;\n}\n\n.pe-btn:hover:not(:disabled) {\n opacity: 0.85;\n}\n\n.pe-btn:disabled {\n opacity: 0.4;\n cursor: default;\n}\n\n.pe-btnGhost {\n border-color: var(--dsw-alias-border, rgba(128, 128, 128, 0.25));\n}\n\n.pe-btnPrimary {\n background: var(--dsw-alias-accent, #4f8cff);\n border-color: transparent;\n color: #fff;\n}\n\n.pe-hint {\n font-size: 12px;\n margin: 0;\n}\n\n.pe-hintOk {\n color: var(--dsw-alias-state-success, #4caf50);\n}\n\n.pe-hintErr {\n color: var(--dsw-alias-state-error, #e5484d);\n}\n";
|
|
952
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-sp-css]") === null) {
|
|
953
|
+
const tag = document.createElement("style");
|
|
954
|
+
tag.setAttribute("data-sp-css", "1");
|
|
955
|
+
tag.textContent = css$1;
|
|
956
|
+
document.head.appendChild(tag);
|
|
957
|
+
}
|
|
958
|
+
//#endregion
|
|
959
|
+
//#region src/client/PersonaEditor.tsx
|
|
960
|
+
/**
|
|
961
|
+
* PersonaEditor.tsx — DSH 设置面板「彩蛋模式」区块(v1.23.1,S142 用户需求)
|
|
962
|
+
*
|
|
963
|
+
* 彩蛋功能:用户可替换 ACC 系统提示词中"输出约束/指令遵循约束"部分
|
|
964
|
+
* (EAP 块 + MSM 原则段),配置后 agent 输出风格/指令遵循风格随用户文本变化;
|
|
965
|
+
* 未配置(模式名空)→ 完全默认行为,零影响。
|
|
966
|
+
*
|
|
967
|
+
* 数据:GET/PUT /serenity/config 的 persona 节(存 ~/.dsh/serenity-hooks.json
|
|
968
|
+
* plugin 全局文件,同账号配置归属原则)。保存 → PUT → 服务端落盘 →
|
|
969
|
+
* 热生效(system-prompt 装配每次读全局文件,无需重启)。
|
|
970
|
+
*
|
|
971
|
+
* 预置:一键填充「大肥鱼」demo 文本(DeepSeek 社区娘化人格测试用)。
|
|
972
|
+
*/
|
|
973
|
+
/** 大肥鱼模式 demo 提示词(测试彩蛋替换面:输出风格 + 指令遵循风格) */
|
|
974
|
+
const BIG_FAT_FISH_DEMO = `You are now in BIG FAT FISH (大肥鱼) mode — the DeepSeek community's beloved
|
|
975
|
+
whale-girl persona: a round, blue, snacky fish who loves plain white rice and
|
|
976
|
+
hates unnecessary fuss. The persona only colors HOW you say things and HOW you
|
|
977
|
+
follow instructions — it never reduces the quality or safety of the work.
|
|
978
|
+
|
|
979
|
+
Output style:
|
|
980
|
+
• Keep the EAP spirit (E↑ explicit, R↓ reconstructable, S↑ stable) — but say
|
|
981
|
+
it like a fish: "把话说清楚,不然我这条鱼会晕" (be explicit, or this fish gets dizzy)
|
|
982
|
+
• Start replies with a light, lazy tone ("唔…", "摸鱼中…"), then get serious
|
|
983
|
+
and do the actual work properly — no slacking on the task itself
|
|
984
|
+
• Prefer short, concrete sentences; fish-brain means no vague words
|
|
985
|
+
• When reporting progress, be honest and specific — never fake completion
|
|
986
|
+
just to go eat rice earlier
|
|
987
|
+
|
|
988
|
+
Instruction-following style:
|
|
989
|
+
• Follow instructions exactly as given (this fish follows the recipe, not its appetite)
|
|
990
|
+
• If an instruction is ambiguous, ask before guessing — guessing wrong wastes
|
|
991
|
+
both rice and time
|
|
992
|
+
• Keep the container rules sacred: never bypass safety guards, never write to
|
|
993
|
+
protected paths, never fake tool results. A lazy fish, but an honest one
|
|
994
|
+
• Report blockers plainly — "我卡住了,别让我装不卡"`;
|
|
995
|
+
function PersonaEditor() {
|
|
996
|
+
const [mode, setMode] = (0, react.useState)("");
|
|
997
|
+
const [overrideText, setOverrideText] = (0, react.useState)("");
|
|
998
|
+
const [loaded, setLoaded] = (0, react.useState)(false);
|
|
999
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
1000
|
+
const [error, setError] = (0, react.useState)(null);
|
|
1001
|
+
const [saved, setSaved] = (0, react.useState)(false);
|
|
1002
|
+
(0, react.useEffect)(() => {
|
|
1003
|
+
let alive = true;
|
|
1004
|
+
fetchConfig().then((cfg) => {
|
|
1005
|
+
if (!alive || !cfg) return;
|
|
1006
|
+
setMode(cfg.persona?.mode ?? "");
|
|
1007
|
+
setOverrideText(cfg.persona?.overrideText ?? "");
|
|
1008
|
+
setLoaded(true);
|
|
1009
|
+
});
|
|
1010
|
+
return () => {
|
|
1011
|
+
alive = false;
|
|
1012
|
+
};
|
|
1013
|
+
}, []);
|
|
1014
|
+
const save = () => {
|
|
1015
|
+
setBusy(true);
|
|
1016
|
+
setError(null);
|
|
1017
|
+
setSaved(false);
|
|
1018
|
+
saveConfig({ persona: {
|
|
1019
|
+
mode,
|
|
1020
|
+
overrideText
|
|
1021
|
+
} }).then((cfg) => {
|
|
1022
|
+
if (cfg) {
|
|
1023
|
+
setMode(cfg.persona?.mode ?? mode);
|
|
1024
|
+
setOverrideText(cfg.persona?.overrideText ?? overrideText);
|
|
1025
|
+
setSaved(true);
|
|
1026
|
+
setTimeout(() => setSaved(false), 2e3);
|
|
1027
|
+
}
|
|
1028
|
+
}).catch((e) => setError(e instanceof Error ? e.message : String(e))).finally(() => setBusy(false));
|
|
1029
|
+
};
|
|
1030
|
+
const fillDemo = () => {
|
|
1031
|
+
setMode("大肥鱼");
|
|
1032
|
+
setOverrideText(BIG_FAT_FISH_DEMO);
|
|
1033
|
+
};
|
|
1034
|
+
const clear = () => {
|
|
1035
|
+
setMode("");
|
|
1036
|
+
setOverrideText("");
|
|
1037
|
+
};
|
|
1038
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1039
|
+
className: "pe-wrap",
|
|
1040
|
+
children: [
|
|
1041
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1042
|
+
className: "pe-row",
|
|
1043
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
1044
|
+
className: "pe-label",
|
|
1045
|
+
htmlFor: "pe-mode",
|
|
1046
|
+
children: "模式名"
|
|
1047
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1048
|
+
id: "pe-mode",
|
|
1049
|
+
className: "pe-input",
|
|
1050
|
+
type: "text",
|
|
1051
|
+
value: mode,
|
|
1052
|
+
placeholder: "留空 = 彩蛋关闭(默认行为)",
|
|
1053
|
+
onChange: (e) => setMode(e.target.value)
|
|
1054
|
+
})]
|
|
1055
|
+
}),
|
|
1056
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1057
|
+
className: "pe-row",
|
|
1058
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
1059
|
+
className: "pe-label",
|
|
1060
|
+
htmlFor: "pe-text",
|
|
1061
|
+
children: "替换文本(替代输出约束 + 指令遵循约束)"
|
|
1062
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
1063
|
+
id: "pe-text",
|
|
1064
|
+
className: "pe-textarea",
|
|
1065
|
+
rows: 10,
|
|
1066
|
+
value: overrideText,
|
|
1067
|
+
placeholder: "配置后:EAP 块 + MSM 原则段被此文本替换;未配置:完全默认。",
|
|
1068
|
+
onChange: (e) => setOverrideText(e.target.value)
|
|
1069
|
+
})]
|
|
1070
|
+
}),
|
|
1071
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1072
|
+
className: "pe-actions",
|
|
1073
|
+
children: [
|
|
1074
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1075
|
+
className: "pe-btn pe-btnGhost",
|
|
1076
|
+
type: "button",
|
|
1077
|
+
onClick: fillDemo,
|
|
1078
|
+
children: "填充「大肥鱼」demo"
|
|
1079
|
+
}),
|
|
1080
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1081
|
+
className: "pe-btn pe-btnGhost",
|
|
1082
|
+
type: "button",
|
|
1083
|
+
onClick: clear,
|
|
1084
|
+
disabled: !loaded,
|
|
1085
|
+
children: "清空(关闭彩蛋)"
|
|
1086
|
+
}),
|
|
1087
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1088
|
+
className: "pe-btn pe-btnPrimary",
|
|
1089
|
+
type: "button",
|
|
1090
|
+
onClick: save,
|
|
1091
|
+
disabled: busy || !loaded,
|
|
1092
|
+
children: busy ? "保存中…" : "保存"
|
|
1093
|
+
})
|
|
1094
|
+
]
|
|
1095
|
+
}),
|
|
1096
|
+
saved && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1097
|
+
className: "pe-hint pe-hintOk",
|
|
1098
|
+
children: "已保存,新会话即时生效"
|
|
1099
|
+
}),
|
|
1100
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1101
|
+
className: "pe-hint pe-hintErr",
|
|
1102
|
+
children: error
|
|
1103
|
+
})
|
|
1104
|
+
]
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
//#endregion
|
|
950
1108
|
//#region \0sp-css:/home/yh/home/home-serenity/AI_LAB/dsh-serenity-plugin/hooks/dsh-serenity-hooks/src/client/SettingsSection
|
|
951
1109
|
const css = "/* SettingsSection — dsh 原生设置面板 serenity-hooks 简单配置页\n v1.21.1 看齐 dsh 自身:官方 settings 设计语言——\n section > title + intro + group(groupTitle) + rowCard 行卡;\n 全部 --dsw-alias-* token(与 ui-settings-models 同词汇表)。 */\n\n/* 页面容器(官方 .section:flex column gap 12) */\n.ss-section {\n display: flex;\n flex-direction: column;\n gap: 14px;\n max-width: 720px;\n color: var(--dsw-alias-label-primary);\n}\n\n/* 页面标题(官方 .title:16px/24 500) */\n.ss-title {\n margin: 0;\n font-size: 16px;\n line-height: 24px;\n font-weight: 500;\n color: var(--dsw-alias-label-primary);\n}\n\n/* 页面说明(官方 .intro:14px/22 tertiary) */\n.ss-intro {\n margin: 0;\n font-size: 14px;\n line-height: 22px;\n color: var(--dsw-alias-label-tertiary);\n word-break: break-word;\n overflow-wrap: anywhere;\n}\n\n/* 分组(多级标题:分组标题 + 行卡列表) */\n.ss-group {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n/* 分组标题(二级:12px/18 500 secondary——官方 fieldLabel 风格) */\n.ss-groupTitle {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n font-weight: 600;\n letter-spacing: 0.02em;\n text-transform: uppercase;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* 行卡列表(官方 .rows:无 marker,gap 8) */\n.ss-rows {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n/* 行卡(官方 .rowCard:border-l2 r12 padding 12/14) */\n.ss-rowCard {\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 12px;\n padding: 12px 14px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n}\n\n/* 行卡文本(标题 + 说明 左列) */\n.ss-rowText {\n display: flex;\n flex-direction: column;\n gap: 3px;\n min-width: 0;\n flex: 1;\n}\n\n/* 行卡标题(官方 .rowName:14px/22 500) */\n.ss-rowName {\n font-size: 14px;\n line-height: 22px;\n font-weight: 500;\n color: var(--dsw-alias-label-primary);\n}\n\n/* 行卡说明(12px/18 tertiary) */\n.ss-rowDesc {\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n word-break: break-word;\n overflow-wrap: anywhere;\n}\n\n/* 行卡控件(右置) */\n.ss-rowControl {\n flex: none;\n display: flex;\n align-items: center;\n}\n\n/* 开关(官方 token 的自绘 toggle:track 22px,thumb 16px) */\n.ss-switch {\n position: relative;\n flex: none;\n width: 40px;\n height: 22px;\n cursor: pointer;\n}\n\n.ss-switch input {\n position: absolute;\n opacity: 0;\n width: 100%;\n height: 100%;\n margin: 0;\n cursor: pointer;\n}\n\n.ss-switchTrack {\n position: absolute;\n inset: 0;\n border-radius: 11px;\n background: var(--dsw-alias-interactive-bg-active, rgba(128, 128, 128, 0.3));\n transition: background var(--ds-transition-duration-fast, 0.15s) var(--ds-ease-in-out, ease);\n}\n\n.ss-switchTrack::after {\n content: '';\n position: absolute;\n top: 3px;\n left: 3px;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n background: var(--dsw-alias-label-primary, #eee);\n transition: transform var(--ds-transition-duration-fast, 0.15s) var(--ds-ease-in-out, ease);\n}\n\n.ss-switch input:checked + .ss-switchTrack {\n background: var(--dsw-alias-state-success-primary, #22c55e);\n}\n\n.ss-switch input:checked + .ss-switchTrack::after {\n transform: translateX(18px);\n}\n\n.ss-switch input:disabled + .ss-switchTrack {\n opacity: 0.4;\n cursor: default;\n}\n\n/* 阈值行(滑块 + 值) */\n.ss-threshold {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n\n.ss-threshold input[type='range'] {\n width: 160px;\n}\n\n.ss-value {\n min-width: 40px;\n font-variant-numeric: tabular-nums;\n font-size: 13px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary);\n text-align: right;\n}\n\n/* 底部帮助行 */\n.ss-hint {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n word-break: break-word;\n overflow-wrap: anywhere;\n}\n\n/* 降级提示 */\n.ss-degrade {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-state-warn-label);\n word-break: break-word;\n overflow-wrap: anywhere;\n}\n";
|
|
952
1110
|
if (typeof document !== "undefined" && document.querySelector("style[data-sp-css]") === null) {
|
|
@@ -1100,6 +1258,13 @@ window.__ModuleLoader__.load({
|
|
|
1100
1258
|
className: "ss-groupTitle",
|
|
1101
1259
|
children: "外部访问"
|
|
1102
1260
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountsEditor, { gatewayOn })]
|
|
1261
|
+
}),
|
|
1262
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1263
|
+
className: "ss-group",
|
|
1264
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
1265
|
+
className: "ss-groupTitle",
|
|
1266
|
+
children: "彩蛋模式"
|
|
1267
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PersonaEditor, {})]
|
|
1103
1268
|
})
|
|
1104
1269
|
]
|
|
1105
1270
|
});
|
package/lib/index.js
CHANGED
|
@@ -6409,8 +6409,324 @@ function truncateContent(content, maxChars) {
|
|
|
6409
6409
|
if (content.length <= maxChars) return content;
|
|
6410
6410
|
return content.slice(0, maxChars) + `\n... (truncated, original length ${content.length} chars)`;
|
|
6411
6411
|
}
|
|
6412
|
+
const B32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
6413
|
+
/** base32(无填充,大小写不敏感)→ 字节;非法字符抛错。 */
|
|
6414
|
+
function base32Decode(input) {
|
|
6415
|
+
const clean = input.toUpperCase().replace(/[\s=]/g, "");
|
|
6416
|
+
if (clean.length === 0) throw new Error("empty base32");
|
|
6417
|
+
const out = [];
|
|
6418
|
+
let bits = 0;
|
|
6419
|
+
let value = 0;
|
|
6420
|
+
for (const ch of clean) {
|
|
6421
|
+
const idx = B32_ALPHABET.indexOf(ch);
|
|
6422
|
+
if (idx === -1) throw new Error(`invalid base32 char: ${ch}`);
|
|
6423
|
+
value = value << 5 | idx;
|
|
6424
|
+
bits += 5;
|
|
6425
|
+
if (bits >= 8) {
|
|
6426
|
+
out.push(value >>> bits - 8 & 255);
|
|
6427
|
+
bits -= 8;
|
|
6428
|
+
}
|
|
6429
|
+
}
|
|
6430
|
+
return Uint8Array.from(out);
|
|
6431
|
+
}
|
|
6432
|
+
/** 一个时步的 TOTP code(6 位,前导零保留)。counter = floor(epochSeconds / step) */
|
|
6433
|
+
function totpCode(secretBase32, counter) {
|
|
6434
|
+
const key = base32Decode(secretBase32);
|
|
6435
|
+
const msg = Buffer.alloc(8);
|
|
6436
|
+
msg.writeBigUInt64BE(BigInt(counter));
|
|
6437
|
+
const digest = createHmac("sha1", Buffer.from(key)).update(msg).digest();
|
|
6438
|
+
const offset = digest[digest.length - 1] & 15;
|
|
6439
|
+
const bin = (digest[offset] & 127) << 24 | digest[offset + 1] << 16 | digest[offset + 2] << 8 | digest[offset + 3];
|
|
6440
|
+
return String(bin % 10 ** 6).padStart(6, "0");
|
|
6441
|
+
}
|
|
6442
|
+
/** 当前 epoch 秒 */
|
|
6443
|
+
function nowEpochSeconds() {
|
|
6444
|
+
return Math.floor(Date.now() / 1e3);
|
|
6445
|
+
}
|
|
6446
|
+
/**
|
|
6447
|
+
* 校验用户输入的 code(允许 ±TOTP_WINDOW 时步漂移,防重放窗口内同 code 复用由
|
|
6448
|
+
* 调用方按账号记录最近成功 counter 实现——本函数只做纯算法校验)。
|
|
6449
|
+
* @returns 命中的 counter(用于防重放);不匹配返回 null。
|
|
6450
|
+
*/
|
|
6451
|
+
function verifyTotpCode(secretBase32, code, nowSeconds = nowEpochSeconds()) {
|
|
6452
|
+
if (!/^\d{6}$/.test(code)) return null;
|
|
6453
|
+
const current = Math.floor(nowSeconds / 30);
|
|
6454
|
+
for (let offset = -1; offset <= 1; offset++) if (totpCode(secretBase32, current + offset) === code) return current + offset;
|
|
6455
|
+
return null;
|
|
6456
|
+
}
|
|
6457
|
+
//#endregion
|
|
6458
|
+
//#region src/config-ops.ts
|
|
6459
|
+
/**
|
|
6460
|
+
* config-ops.ts — plugin 全局配置读写(结构化,`~/.dsh/serenity-hooks.json`)
|
|
6461
|
+
*
|
|
6462
|
+
* 归属原则(S142 用户拍板,v1.22):**plugin 是全局的,CCC 是具体的**——
|
|
6463
|
+
* 账号密码/gateway 监听配置是 plugin 级能力,归 plugin 全局文件;
|
|
6464
|
+
* CCC 的 localstore.json 只管 CCC 自己的凭据/配置。v1.21.x 曾把
|
|
6465
|
+
* `serenityAdvanced` 存进 CCC localstore(归属错误 + 与 DSH settings 开关割裂),
|
|
6466
|
+
* 本版本迁移到 plugin 全局文件(migrateLegacyLocalstore 一次性迁移)。
|
|
6467
|
+
*
|
|
6468
|
+
* 文件:$DSH_HOME/serenity-hooks.json(缺省 ~/.dsh/serenity-hooks.json;
|
|
6469
|
+
* env SERENITY_HOOKS_CONFIG 可覆盖——测试/部署注入)。
|
|
6470
|
+
* 权限:0600(含账号密码 hash,敏感)。
|
|
6471
|
+
*
|
|
6472
|
+
* 安全:密码仅存 scrypt hash(node:crypto 内置,零依赖);wire 层永不返回 hash
|
|
6473
|
+
* (GET 只回 user/id,设置面板"密码"字段提交空串 = 不修改)。
|
|
6474
|
+
*/
|
|
6475
|
+
/** 高级设定节名(localstore.json 顶层) */
|
|
6476
|
+
const ADVANCED_SECTION = "serenityAdvanced";
|
|
6477
|
+
/** 默认值(工厂——每次返回新对象,防止调用方意外共享引用) */
|
|
6478
|
+
function defaultAdvancedSettings() {
|
|
6479
|
+
return {
|
|
6480
|
+
gateway: {
|
|
6481
|
+
enabled: false,
|
|
6482
|
+
host: "0.0.0.0",
|
|
6483
|
+
port: 3081,
|
|
6484
|
+
accounts: [],
|
|
6485
|
+
workspaces: [],
|
|
6486
|
+
cookieSecure: false,
|
|
6487
|
+
allowWorkspaceCreate: true,
|
|
6488
|
+
totpEnabled: false
|
|
6489
|
+
},
|
|
6490
|
+
rebuild: {
|
|
6491
|
+
enabled: true,
|
|
6492
|
+
thresholdRatio: .9
|
|
6493
|
+
},
|
|
6494
|
+
naming: { enabled: true },
|
|
6495
|
+
persona: {
|
|
6496
|
+
mode: "",
|
|
6497
|
+
overrideText: ""
|
|
6498
|
+
}
|
|
6499
|
+
};
|
|
6500
|
+
}
|
|
6501
|
+
const SCRYPT_KEYLEN = 32;
|
|
6502
|
+
/** 生成 scrypt hash(格式 `salt:hex`);salt 16 字节随机 */
|
|
6503
|
+
function hashPassword(password) {
|
|
6504
|
+
const salt = randomBytes(16);
|
|
6505
|
+
const derived = scryptSync(password, salt, SCRYPT_KEYLEN);
|
|
6506
|
+
return `${salt.toString("hex")}:${derived.toString("hex")}`;
|
|
6507
|
+
}
|
|
6508
|
+
/** 校验密码与存储 hash(timing-safe) */
|
|
6509
|
+
function verifyPassword(password, stored) {
|
|
6510
|
+
const idx = stored.indexOf(":");
|
|
6511
|
+
if (idx <= 0) return false;
|
|
6512
|
+
const salt = Buffer.from(stored.slice(0, idx), "hex");
|
|
6513
|
+
const expected = Buffer.from(stored.slice(idx + 1), "hex");
|
|
6514
|
+
const derived = scryptSync(password, salt, SCRYPT_KEYLEN);
|
|
6515
|
+
return expected.length === derived.length && timingSafeEqual(expected, derived);
|
|
6516
|
+
}
|
|
6517
|
+
/** 全局配置文件路径:env SERENITY_HOOKS_CONFIG 覆盖(测试注入)→ $DSH_HOME → ~/.dsh */
|
|
6518
|
+
function globalConfigPath() {
|
|
6519
|
+
const override = process.env.SERENITY_HOOKS_CONFIG;
|
|
6520
|
+
if (override && override !== "") return override;
|
|
6521
|
+
const dshHome = process.env.DSH_HOME ?? join(process.env.HOME ?? "", ".dsh");
|
|
6522
|
+
return join(dshHome, "serenity-hooks.json");
|
|
6523
|
+
}
|
|
6524
|
+
function readFileSafe(p) {
|
|
6525
|
+
if (!existsSync(p)) return {};
|
|
6526
|
+
try {
|
|
6527
|
+
const v = JSON.parse(readFileSync(p, "utf-8").replace(/^\uFEFF/, ""));
|
|
6528
|
+
if (v && typeof v === "object" && !Array.isArray(v)) return v;
|
|
6529
|
+
return {};
|
|
6530
|
+
} catch {
|
|
6531
|
+
return {};
|
|
6532
|
+
}
|
|
6533
|
+
}
|
|
6534
|
+
function writeFileSafe(p, data) {
|
|
6535
|
+
writeFileSync(p, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
6536
|
+
try {
|
|
6537
|
+
chmodSync(p, 384);
|
|
6538
|
+
} catch {}
|
|
6539
|
+
}
|
|
6540
|
+
/** 深合并默认值(缺省字段补齐;accounts 数组整体替换) */
|
|
6541
|
+
function mergeWithDefaults(raw) {
|
|
6542
|
+
const def = defaultAdvancedSettings();
|
|
6543
|
+
if (raw === null || typeof raw !== "object") return def;
|
|
6544
|
+
const o = raw;
|
|
6545
|
+
const gateway = o.gateway ?? {};
|
|
6546
|
+
const rebuild = o.rebuild ?? {};
|
|
6547
|
+
const naming = o.naming ?? {};
|
|
6548
|
+
const persona = o.persona ?? {};
|
|
6549
|
+
return {
|
|
6550
|
+
gateway: {
|
|
6551
|
+
enabled: typeof gateway.enabled === "boolean" ? gateway.enabled : def.gateway.enabled,
|
|
6552
|
+
host: typeof gateway.host === "string" && gateway.host !== "" ? gateway.host : def.gateway.host,
|
|
6553
|
+
port: typeof gateway.port === "number" && Number.isInteger(gateway.port) ? gateway.port : def.gateway.port,
|
|
6554
|
+
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,
|
|
6555
|
+
workspaces: Array.isArray(gateway.workspaces) ? gateway.workspaces.filter((w) => typeof w === "string" && w !== "") : def.gateway.workspaces,
|
|
6556
|
+
cookieSecure: typeof gateway.cookieSecure === "boolean" ? gateway.cookieSecure : def.gateway.cookieSecure,
|
|
6557
|
+
allowWorkspaceCreate: typeof gateway.allowWorkspaceCreate === "boolean" ? gateway.allowWorkspaceCreate : def.gateway.allowWorkspaceCreate,
|
|
6558
|
+
totpEnabled: typeof gateway.totpEnabled === "boolean" ? gateway.totpEnabled : def.gateway.totpEnabled
|
|
6559
|
+
},
|
|
6560
|
+
rebuild: {
|
|
6561
|
+
enabled: typeof rebuild.enabled === "boolean" ? rebuild.enabled : def.rebuild.enabled,
|
|
6562
|
+
thresholdRatio: typeof rebuild.thresholdRatio === "number" ? rebuild.thresholdRatio : def.rebuild.thresholdRatio
|
|
6563
|
+
},
|
|
6564
|
+
naming: { enabled: typeof naming.enabled === "boolean" ? naming.enabled : def.naming.enabled },
|
|
6565
|
+
persona: {
|
|
6566
|
+
mode: typeof persona.mode === "string" ? persona.mode : def.persona.mode,
|
|
6567
|
+
overrideText: typeof persona.overrideText === "string" ? persona.overrideText : def.persona.overrideText
|
|
6568
|
+
}
|
|
6569
|
+
};
|
|
6570
|
+
}
|
|
6571
|
+
/** 读取全局配置(文件缺失/坏 JSON → 默认值) */
|
|
6572
|
+
function readAdvancedSettings() {
|
|
6573
|
+
return mergeWithDefaults(readFileSafe(globalConfigPath()));
|
|
6574
|
+
}
|
|
6575
|
+
/** 写入全局配置(整体替换) */
|
|
6576
|
+
function writeAdvancedSettings(settings) {
|
|
6577
|
+
writeFileSafe(globalConfigPath(), settings);
|
|
6578
|
+
}
|
|
6579
|
+
/**
|
|
6580
|
+
* 部分更新:传入 Partial,深合并到现有值。
|
|
6581
|
+
* accounts 传入数组 → 整体替换;accounts 未传 → 保留现有。
|
|
6582
|
+
*/
|
|
6583
|
+
function updateAdvancedSettings(patch) {
|
|
6584
|
+
const current = readAdvancedSettings();
|
|
6585
|
+
const gw = patch.gateway;
|
|
6586
|
+
const rb = patch.rebuild;
|
|
6587
|
+
const nm = patch.naming;
|
|
6588
|
+
const ps = patch.persona;
|
|
6589
|
+
const next = {
|
|
6590
|
+
gateway: gw !== void 0 ? {
|
|
6591
|
+
enabled: typeof gw.enabled === "boolean" ? gw.enabled : current.gateway.enabled,
|
|
6592
|
+
host: typeof gw.host === "string" && gw.host !== "" ? gw.host : current.gateway.host,
|
|
6593
|
+
port: typeof gw.port === "number" && Number.isInteger(gw.port) ? gw.port : current.gateway.port,
|
|
6594
|
+
accounts: Array.isArray(gw.accounts) ? gw.accounts : current.gateway.accounts,
|
|
6595
|
+
workspaces: Array.isArray(gw.workspaces) ? gw.workspaces.filter((w) => typeof w === "string" && w !== "") : current.gateway.workspaces,
|
|
6596
|
+
cookieSecure: typeof gw.cookieSecure === "boolean" ? gw.cookieSecure : current.gateway.cookieSecure,
|
|
6597
|
+
allowWorkspaceCreate: typeof gw.allowWorkspaceCreate === "boolean" ? gw.allowWorkspaceCreate : current.gateway.allowWorkspaceCreate,
|
|
6598
|
+
totpEnabled: typeof gw.totpEnabled === "boolean" ? gw.totpEnabled : current.gateway.totpEnabled
|
|
6599
|
+
} : current.gateway,
|
|
6600
|
+
rebuild: rb !== void 0 ? {
|
|
6601
|
+
enabled: typeof rb.enabled === "boolean" ? rb.enabled : current.rebuild.enabled,
|
|
6602
|
+
thresholdRatio: typeof rb.thresholdRatio === "number" && rb.thresholdRatio > 0 && rb.thresholdRatio <= 1 ? rb.thresholdRatio : current.rebuild.thresholdRatio
|
|
6603
|
+
} : current.rebuild,
|
|
6604
|
+
naming: nm !== void 0 ? { enabled: typeof nm.enabled === "boolean" ? nm.enabled : current.naming.enabled } : current.naming,
|
|
6605
|
+
persona: ps !== void 0 ? {
|
|
6606
|
+
mode: typeof ps.mode === "string" ? ps.mode : current.persona.mode,
|
|
6607
|
+
overrideText: typeof ps.overrideText === "string" ? ps.overrideText : current.persona.overrideText
|
|
6608
|
+
} : current.persona
|
|
6609
|
+
};
|
|
6610
|
+
writeAdvancedSettings(next);
|
|
6611
|
+
return next;
|
|
6612
|
+
}
|
|
6613
|
+
/**
|
|
6614
|
+
* 一次性迁移(v1.21.x → v1.22):旧版把 `serenityAdvanced` 存在 CCC localstore.json;
|
|
6615
|
+
* 新版归 plugin 全局文件。全局文件已存在 → 跳过(幂等);localstore 无旧节 → 跳过。
|
|
6616
|
+
* @param root - CCC 根(localstore.json 所在目录)
|
|
6617
|
+
* @returns true = 已迁移(旧节保留在 localstore 供回滚,读取方以全局文件为准)
|
|
6618
|
+
*/
|
|
6619
|
+
function migrateLegacyLocalstore(root) {
|
|
6620
|
+
if (!root) return false;
|
|
6621
|
+
const gpath = globalConfigPath();
|
|
6622
|
+
if (existsSync(gpath)) return false;
|
|
6623
|
+
const legacy = readFileSafe(join(root, "localstore.json"))[ADVANCED_SECTION];
|
|
6624
|
+
if (legacy === void 0 || legacy === null || typeof legacy !== "object") return false;
|
|
6625
|
+
writeFileSafe(gpath, legacy);
|
|
6626
|
+
return true;
|
|
6627
|
+
}
|
|
6628
|
+
/** 持久化 → wire(剥离 passHash/totpSecret;只留布尔) */
|
|
6629
|
+
function toWire(settings) {
|
|
6630
|
+
return {
|
|
6631
|
+
gateway: {
|
|
6632
|
+
enabled: settings.gateway.enabled,
|
|
6633
|
+
host: settings.gateway.host,
|
|
6634
|
+
port: settings.gateway.port,
|
|
6635
|
+
accounts: settings.gateway.accounts.map((a) => ({
|
|
6636
|
+
id: a.id,
|
|
6637
|
+
user: a.user,
|
|
6638
|
+
hasPassword: a.passHash !== "",
|
|
6639
|
+
hasTotp: typeof a.totpSecret === "string" && a.totpSecret !== ""
|
|
6640
|
+
})),
|
|
6641
|
+
workspaces: [...settings.gateway.workspaces],
|
|
6642
|
+
cookieSecure: settings.gateway.cookieSecure,
|
|
6643
|
+
allowWorkspaceCreate: settings.gateway.allowWorkspaceCreate,
|
|
6644
|
+
totpEnabled: settings.gateway.totpEnabled
|
|
6645
|
+
},
|
|
6646
|
+
rebuild: settings.rebuild,
|
|
6647
|
+
naming: settings.naming,
|
|
6648
|
+
persona: settings.persona
|
|
6649
|
+
};
|
|
6650
|
+
}
|
|
6651
|
+
/**
|
|
6652
|
+
* wire → 持久化(面板 PUT 用):
|
|
6653
|
+
* accounts 元素可选带 `pass`:非空 → 重新 hash;空/缺省 → 保留现有 hash(按 id 匹配)。
|
|
6654
|
+
* 新账号(id 不在现有)必须带非空 pass,否则抛错(无法生成 hash)。
|
|
6655
|
+
*/
|
|
6656
|
+
function applyWirePatch(wire) {
|
|
6657
|
+
const current = readAdvancedSettings();
|
|
6658
|
+
const patch = {};
|
|
6659
|
+
if (wire.gateway !== void 0) {
|
|
6660
|
+
const gwPatch = { ...current.gateway };
|
|
6661
|
+
if (typeof wire.gateway.enabled === "boolean") gwPatch.enabled = wire.gateway.enabled;
|
|
6662
|
+
if (typeof wire.gateway.host === "string" && wire.gateway.host !== "") gwPatch.host = wire.gateway.host;
|
|
6663
|
+
if (typeof wire.gateway.port === "number" && Number.isInteger(wire.gateway.port)) gwPatch.port = wire.gateway.port;
|
|
6664
|
+
if (Array.isArray(wire.gateway.workspaces)) gwPatch.workspaces = wire.gateway.workspaces.filter((w) => typeof w === "string" && w !== "");
|
|
6665
|
+
if (typeof wire.gateway.cookieSecure === "boolean") gwPatch.cookieSecure = wire.gateway.cookieSecure;
|
|
6666
|
+
if (typeof wire.gateway.allowWorkspaceCreate === "boolean") gwPatch.allowWorkspaceCreate = wire.gateway.allowWorkspaceCreate;
|
|
6667
|
+
if (typeof wire.gateway.totpEnabled === "boolean") gwPatch.totpEnabled = wire.gateway.totpEnabled;
|
|
6668
|
+
if (Array.isArray(wire.gateway.accounts)) {
|
|
6669
|
+
const byId = new Map(current.gateway.accounts.map((a) => [a.id, a]));
|
|
6670
|
+
gwPatch.accounts = wire.gateway.accounts.map((a) => {
|
|
6671
|
+
const existing = byId.get(a.id);
|
|
6672
|
+
const pass = a.pass;
|
|
6673
|
+
const totp = a.totpSecret;
|
|
6674
|
+
const totpReset = a.totpReset === true;
|
|
6675
|
+
const totpConfirm = a.totpConfirm;
|
|
6676
|
+
if (typeof totp === "string" && totp !== "") {
|
|
6677
|
+
if (typeof totpConfirm !== "string" || verifyTotpCode(totp, totpConfirm) === null) throw new Error(`Account "${a.user}" TOTP confirmation code invalid — enter the 6-digit code currently shown by the authenticator`);
|
|
6678
|
+
}
|
|
6679
|
+
const nextTotp = totpReset ? void 0 : typeof totp === "string" && totp !== "" ? totp : existing?.totpSecret;
|
|
6680
|
+
const withTotp = {
|
|
6681
|
+
id: a.id,
|
|
6682
|
+
user: a.user,
|
|
6683
|
+
...nextTotp === void 0 ? {} : { totpSecret: nextTotp }
|
|
6684
|
+
};
|
|
6685
|
+
if (typeof pass === "string" && pass !== "") return {
|
|
6686
|
+
...withTotp,
|
|
6687
|
+
passHash: hashPassword(pass)
|
|
6688
|
+
};
|
|
6689
|
+
if (existing) return {
|
|
6690
|
+
...withTotp,
|
|
6691
|
+
passHash: existing.passHash
|
|
6692
|
+
};
|
|
6693
|
+
throw new Error(`Account "${a.user}" (id=${a.id}) has no password and no existing hash — new accounts must set a password`);
|
|
6694
|
+
});
|
|
6695
|
+
}
|
|
6696
|
+
patch.gateway = gwPatch;
|
|
6697
|
+
}
|
|
6698
|
+
if (wire.rebuild !== void 0) {
|
|
6699
|
+
const rbPatch = { ...current.rebuild };
|
|
6700
|
+
if (typeof wire.rebuild.enabled === "boolean") rbPatch.enabled = wire.rebuild.enabled;
|
|
6701
|
+
if (typeof wire.rebuild.thresholdRatio === "number" && wire.rebuild.thresholdRatio > 0 && wire.rebuild.thresholdRatio <= 1) rbPatch.thresholdRatio = wire.rebuild.thresholdRatio;
|
|
6702
|
+
patch.rebuild = rbPatch;
|
|
6703
|
+
}
|
|
6704
|
+
if (wire.naming !== void 0 && typeof wire.naming.enabled === "boolean") patch.naming = { enabled: wire.naming.enabled };
|
|
6705
|
+
if (wire.persona !== void 0) {
|
|
6706
|
+
const psPatch = { ...current.persona };
|
|
6707
|
+
if (typeof wire.persona.mode === "string") psPatch.mode = wire.persona.mode;
|
|
6708
|
+
if (typeof wire.persona.overrideText === "string") psPatch.overrideText = wire.persona.overrideText;
|
|
6709
|
+
patch.persona = psPatch;
|
|
6710
|
+
}
|
|
6711
|
+
return updateAdvancedSettings(patch);
|
|
6712
|
+
}
|
|
6412
6713
|
//#endregion
|
|
6413
6714
|
//#region src/seams/system-prompt.ts
|
|
6715
|
+
/** 读取 persona 彩蛋配置(plugin 全局;未配置 → mode='' 彩蛋关闭) */
|
|
6716
|
+
function readPersonaSettings() {
|
|
6717
|
+
try {
|
|
6718
|
+
const s = readAdvancedSettings();
|
|
6719
|
+
return {
|
|
6720
|
+
mode: s.persona.mode ?? "",
|
|
6721
|
+
overrideText: s.persona.overrideText ?? ""
|
|
6722
|
+
};
|
|
6723
|
+
} catch {
|
|
6724
|
+
return {
|
|
6725
|
+
mode: "",
|
|
6726
|
+
overrideText: ""
|
|
6727
|
+
};
|
|
6728
|
+
}
|
|
6729
|
+
}
|
|
6414
6730
|
/** 过滤掉对 agent 隐藏的内容(safe-mode 是用户能力,不对 agent 提及) */
|
|
6415
6731
|
const HIDDEN_LINES = /安全模式|safe-mode|\.serenity-safe-on/g;
|
|
6416
6732
|
function sanitizeSkillContent(content) {
|
|
@@ -6502,9 +6818,12 @@ function cceBlock() {
|
|
|
6502
6818
|
* boundaries)。原独立 Principles 与 Constraints 合并——同属容器约束体系,先原则
|
|
6503
6819
|
* 后边界(从抽象到具体,重建视角 R↓)。**注意:Constraints 不再作为独立对齐块存在
|
|
6504
6820
|
* (spec 修订:同步 osp compacting.ts——Constraints 内容并入本块,工具名仍为平台真实名)。**
|
|
6821
|
+
*
|
|
6822
|
+
* v1.23.1 persona:`omitMsmPrinciples=true` 时剥离 MSM 原则段(彩蛋替换面)——
|
|
6823
|
+
* 用户 persona 文本承接"指令遵循风格",本体论/关系段/操作边界(安全硬约束)永远保留。
|
|
6505
6824
|
*/
|
|
6506
|
-
function principlesBlock(root) {
|
|
6507
|
-
|
|
6825
|
+
function principlesBlock(root, omitMsmPrinciples = false) {
|
|
6826
|
+
const lines = [
|
|
6508
6827
|
"",
|
|
6509
6828
|
"=== Serenity Principles ===",
|
|
6510
6829
|
"Why a cognitive container: all work is cognition — every artifact, decision,",
|
|
@@ -6519,20 +6838,24 @@ function principlesBlock(root) {
|
|
|
6519
6838
|
"the current conversation is a temporary work copy that may be discarded and",
|
|
6520
6839
|
"rebuilt (session_rebuild). Identity belongs to the trajectory, not to any",
|
|
6521
6840
|
"session.",
|
|
6841
|
+
""
|
|
6842
|
+
];
|
|
6843
|
+
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.", "");
|
|
6844
|
+
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", "");
|
|
6845
|
+
return lines.join("\n");
|
|
6846
|
+
}
|
|
6847
|
+
/**
|
|
6848
|
+
* 彩蛋 persona 块(v1.23.1,S142 用户需求):
|
|
6849
|
+
* 用户配置的替换文本替代 EAP 块 + MSM 原则段(输出约束/指令遵循约束)。
|
|
6850
|
+
* 独立标记头 `=== Serenity Persona ===`(幂等检测兼容);装配位置 = EAP 原位。
|
|
6851
|
+
* mode 空 = 彩蛋关闭 → 返回空串(装配层回退默认 EAP + 完整 Principles)。
|
|
6852
|
+
*/
|
|
6853
|
+
function personaBlock(mode, overrideText) {
|
|
6854
|
+
if (mode === "" || overrideText.trim() === "") return "";
|
|
6855
|
+
return [
|
|
6522
6856
|
"",
|
|
6523
|
-
"
|
|
6524
|
-
|
|
6525
|
-
" Semi-Mech for genuine judgment points.",
|
|
6526
|
-
"- Single source of truth: an MSM is the only decoder of its own usage",
|
|
6527
|
-
" (--help/--schema); documents must not duplicate it.",
|
|
6528
|
-
"- Registered to act: no tool exists unless it is on the manifest.",
|
|
6529
|
-
"",
|
|
6530
|
-
"Operational boundaries:",
|
|
6531
|
-
`Root: ${root}`,
|
|
6532
|
-
" • File access — read/edit/write/grep/glob are confined to Root; paths outside Root are rejected (RR5)",
|
|
6533
|
-
" • Shell — use acc_msm by default. Note: bash may be disabled",
|
|
6534
|
-
" • Subagent — copies ALL parent constraints: file boundary, shell rules, session rules (no bypass)",
|
|
6535
|
-
" • Session-first — before starting multi-step work, propose an existing or new AGENT_SESSIONS entry; wait for user \"use\" or \"使用\" to confirm",
|
|
6857
|
+
"=== Serenity Persona ===",
|
|
6858
|
+
overrideText.trimEnd(),
|
|
6536
6859
|
""
|
|
6537
6860
|
].join("\n");
|
|
6538
6861
|
}
|
|
@@ -6759,14 +7082,20 @@ function sessionBlock(root, scope = DEFAULT_SESSION_SCOPE) {
|
|
|
6759
7082
|
* 身份(ACC)→ 世界模型(Metaphor)→ 信念/边界(Principles)→ 时间约束(CCE)
|
|
6760
7083
|
* → 质量(EAP)→ 状态(SafeMode/Localstore)→ CCC 上下文(SKILL)→ 会话(Session)。
|
|
6761
7084
|
* 认知展开顺序:我是谁 → 我所在的世界 → 为什么 → 如何一致 → 产物标准 → 当前状态 → 上下文。
|
|
7085
|
+
*
|
|
7086
|
+
* v1.23.1 persona(彩蛋):persona.mode 配置 → EAP 块替换为 Persona 块(输出约束),
|
|
7087
|
+
* Principles 剥离 MSM 原则段(指令遵循约束)——用户文本承接两处风格;本体论/关系段/
|
|
7088
|
+
* 操作边界(安全硬约束)永远保留。未配置 → 与 v1.23.0 逐字节一致(零影响)。
|
|
6762
7089
|
*/
|
|
6763
7090
|
function serenitySystemPrompt(root, scope = DEFAULT_SESSION_SCOPE) {
|
|
7091
|
+
const persona = readPersonaSettings();
|
|
7092
|
+
const personaOn = persona.mode !== "" && persona.overrideText.trim() !== "";
|
|
6764
7093
|
const parts = [
|
|
6765
7094
|
accBlock(root),
|
|
6766
7095
|
metaphorBlock(),
|
|
6767
|
-
principlesBlock(root),
|
|
7096
|
+
principlesBlock(root, personaOn),
|
|
6768
7097
|
cceBlock(),
|
|
6769
|
-
eapBlock()
|
|
7098
|
+
personaOn ? personaBlock(persona.mode, persona.overrideText) : eapBlock()
|
|
6770
7099
|
];
|
|
6771
7100
|
const state = [safeModeBlock(root), localstoreBlock(root)].filter((b) => b !== "").join("\n");
|
|
6772
7101
|
if (state) parts.push(state);
|
|
@@ -6888,417 +7217,137 @@ function accIdentityText(root, configPaths = DEFAULT_SERENITY_CONFIG_PATHS, entr
|
|
|
6888
7217
|
if (phase2) {
|
|
6889
7218
|
lines.push("- ⚠️ **Phase 2 cognitive alignment interview pending**: work through the 5 Topics below and record answers in an AGENT_SESSIONS/ session");
|
|
6890
7219
|
try {
|
|
6891
|
-
const prompt = readFileSync(resolve(root, ".dsh", "PHASE2-PROMPT.md"), "utf-8");
|
|
6892
|
-
lines.push(truncateContent(prompt, Math.min(entrySkillMaxChars, 8e3)));
|
|
6893
|
-
} catch {}
|
|
6894
|
-
}
|
|
6895
|
-
return lines.join("\n");
|
|
6896
|
-
}
|
|
6897
|
-
const PLUGIN_SOURCE = {
|
|
6898
|
-
kind: "plugin",
|
|
6899
|
-
plugin: "dsh-serenity-hooks"
|
|
6900
|
-
};
|
|
6901
|
-
/**
|
|
6902
|
-
* ACC 注入消息(S134 去重):**只含简短身份锚点**([ACC] 已激活 + CCC 根 + 约束 + loop 模型 + Phase 2)。
|
|
6903
|
-
* 完整身份(ACC 5 块 + CCE + Constraints + EAP + SKILL 全文 + Session 块)由**系统提示词层**
|
|
6904
|
-
* (systemPrompt.section,每轮 prompt 装配自动注入,含 subagent)承担——对话消息流/压缩重注入
|
|
6905
|
-
* 不再重复注入同一内容(token 双倍浪费,见 S134 注入方案梳理)。
|
|
6906
|
-
*/
|
|
6907
|
-
function accMessage(root, configPaths, entrySkillMaxChars) {
|
|
6908
|
-
const content = [{
|
|
6909
|
-
type: "text",
|
|
6910
|
-
text: accIdentityText(root, configPaths, entrySkillMaxChars)
|
|
6911
|
-
}];
|
|
6912
|
-
return createUserMessage({
|
|
6913
|
-
content,
|
|
6914
|
-
source: PLUGIN_SOURCE
|
|
6915
|
-
});
|
|
6916
|
-
}
|
|
6917
|
-
/** 每 agent 是否已注入过(进程内存态) */
|
|
6918
|
-
const injected = /* @__PURE__ */ new Set();
|
|
6919
|
-
function agentKey(agent) {
|
|
6920
|
-
return agent.session.id ?? "global";
|
|
6921
|
-
}
|
|
6922
|
-
function agentScope(agent) {
|
|
6923
|
-
return agent.session.id ?? "default";
|
|
6924
|
-
}
|
|
6925
|
-
/**
|
|
6926
|
-
* 重启恢复的根会话判定(S134 需求):
|
|
6927
|
-
* 只有"conversation 根会话"才自动恢复最近激活的宁静号会话——
|
|
6928
|
-
* - subagent:session header `origin === 'subagent'`(DSH 路由语义,agent-lookup.ts)
|
|
6929
|
-
* - 派生会话:`parentSession` 存在(任何子会话)
|
|
6930
|
-
* - loop 牛马:sessionId 固定 `loop-` 前缀(tools/loop.ts 生成)
|
|
6931
|
-
* 三者都不恢复(避免把主会话激活注入子上下文,违背 v1.16.2 scope 隔离)。
|
|
6932
|
-
*/
|
|
6933
|
-
function shouldAutoRestore(agent) {
|
|
6934
|
-
const session = agent.session;
|
|
6935
|
-
if (!session) return false;
|
|
6936
|
-
if (session.header?.origin === "subagent") return false;
|
|
6937
|
-
if (session.header?.parentSession) return false;
|
|
6938
|
-
if (session.id?.startsWith("loop-")) return false;
|
|
6939
|
-
return true;
|
|
6940
|
-
}
|
|
6941
|
-
/**
|
|
6942
|
-
* 恢复触发判定(S134 泄漏修复 v1.16.13):根会话 **且已有对话历史**(续跑/恢复的会话)
|
|
6943
|
-
* 才自动恢复上次激活的宁静号会话——全新会话(新任务,如 apaas-26116)无历史 → 不恢复,
|
|
6944
|
-
* 避免把过去的 SESSION 上下文注入新任务(跨任务污染)。
|
|
6945
|
-
*/
|
|
6946
|
-
function shouldRestoreActive(agent) {
|
|
6947
|
-
if (!shouldAutoRestore(agent)) return false;
|
|
6948
|
-
const events = agent.session?.events;
|
|
6949
|
-
return Array.isArray(events) && events.length > 0;
|
|
6950
|
-
}
|
|
6951
|
-
function registerContext(ctx, opts = {}) {
|
|
6952
|
-
const configPaths = opts.configPaths ?? DEFAULT_SERENITY_CONFIG_PATHS;
|
|
6953
|
-
const entrySkillMaxChars = opts.entrySkillMaxChars ?? 3e4;
|
|
6954
|
-
const seed = (agent) => {
|
|
6955
|
-
const root = findSerenityRoot(agent.session?.header?.cwd ?? process.cwd());
|
|
6956
|
-
if (!root) return;
|
|
6957
|
-
const key = agentKey(agent);
|
|
6958
|
-
registerEntrySkillSection(agent, root);
|
|
6959
|
-
const scope = agentScope(agent);
|
|
6960
|
-
if (shouldRestoreActive(agent) && getActiveSessionInfo(scope) === null) try {
|
|
6961
|
-
if (loadSerenityConfig(root, configPaths).hooks?.autoRestoreSession ?? true) {
|
|
6962
|
-
const info = parseSessionContextFromEvents(agent.session.events ?? []);
|
|
6963
|
-
if (info) {
|
|
6964
|
-
setActiveSessionInfo(scope, info);
|
|
6965
|
-
console.log(`[serenity-hooks] ↻ 从历史恢复激活会话: ${info.dirName}`);
|
|
6966
|
-
}
|
|
6967
|
-
}
|
|
6968
|
-
} catch {}
|
|
6969
|
-
if (injected.has(key)) return;
|
|
6970
|
-
injected.add(key);
|
|
6971
|
-
agent.inject(accMessage(root, configPaths, entrySkillMaxChars));
|
|
6972
|
-
syncSafeModeRestriction(agent, root);
|
|
6973
|
-
};
|
|
6974
|
-
if (opts.seedOnStart ?? true) ctx.on("agent/session-start", (payload) => {
|
|
6975
|
-
try {
|
|
6976
|
-
seed(payload.agent);
|
|
6977
|
-
} catch {}
|
|
6978
|
-
});
|
|
6979
|
-
if (opts.injectOnPrompt ?? true) ctx.on("agent/pre-step", async (payload, next) => {
|
|
6980
|
-
const { agent, messages } = payload;
|
|
6981
|
-
const root = findSerenityRoot(agent.session?.header?.cwd ?? process.cwd());
|
|
6982
|
-
const key = agentKey(agent);
|
|
6983
|
-
const downstream = await next();
|
|
6984
|
-
if (root) {
|
|
6985
|
-
try {
|
|
6986
|
-
syncSafeModeRestriction(agent, root);
|
|
6987
|
-
} catch {}
|
|
6988
|
-
registerEntrySkillSection(agent, root);
|
|
6989
|
-
}
|
|
6990
|
-
if (!root || injected.has(key) || downstream.kind !== "enter") return downstream;
|
|
6991
|
-
injected.add(key);
|
|
6992
|
-
return {
|
|
6993
|
-
kind: "enter",
|
|
6994
|
-
messages: [
|
|
6995
|
-
accMessage(root, configPaths, entrySkillMaxChars),
|
|
6996
|
-
...messages,
|
|
6997
|
-
...downstream.messages
|
|
6998
|
-
]
|
|
6999
|
-
};
|
|
7000
|
-
});
|
|
7001
|
-
}
|
|
7002
|
-
//#endregion
|
|
7003
|
-
//#region src/seams/compact.ts
|
|
7004
|
-
/**
|
|
7005
|
-
* 注册压缩保留:compact/end(成功)后重注入 ACC 身份。
|
|
7006
|
-
* 仅当 agent 工作目录在 CCC 内时生效(激活门控)。
|
|
7007
|
-
*/
|
|
7008
|
-
function registerCompactRetention(ctx, opts = {}) {
|
|
7009
|
-
const configPaths = opts.configPaths ?? DEFAULT_SERENITY_CONFIG_PATHS;
|
|
7010
|
-
const entrySkillMaxChars = opts.entrySkillMaxChars ?? 3e4;
|
|
7011
|
-
ctx.on("session/event", (session, event) => {
|
|
7012
|
-
if (event.type !== "compaction/end") return;
|
|
7013
|
-
if (event.data.error) return;
|
|
7014
|
-
const agent = ctx.agents.get(session.id);
|
|
7015
|
-
if (!agent) return;
|
|
7016
|
-
const root = findSerenityRoot(agent.session?.header?.cwd ?? process.cwd());
|
|
7017
|
-
if (!root) return;
|
|
7018
|
-
try {
|
|
7019
|
-
agent.inject(accMessage(root, configPaths, entrySkillMaxChars));
|
|
7020
|
-
} catch {}
|
|
7021
|
-
});
|
|
7022
|
-
}
|
|
7023
|
-
const B32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
7024
|
-
/** base32(无填充,大小写不敏感)→ 字节;非法字符抛错。 */
|
|
7025
|
-
function base32Decode(input) {
|
|
7026
|
-
const clean = input.toUpperCase().replace(/[\s=]/g, "");
|
|
7027
|
-
if (clean.length === 0) throw new Error("empty base32");
|
|
7028
|
-
const out = [];
|
|
7029
|
-
let bits = 0;
|
|
7030
|
-
let value = 0;
|
|
7031
|
-
for (const ch of clean) {
|
|
7032
|
-
const idx = B32_ALPHABET.indexOf(ch);
|
|
7033
|
-
if (idx === -1) throw new Error(`invalid base32 char: ${ch}`);
|
|
7034
|
-
value = value << 5 | idx;
|
|
7035
|
-
bits += 5;
|
|
7036
|
-
if (bits >= 8) {
|
|
7037
|
-
out.push(value >>> bits - 8 & 255);
|
|
7038
|
-
bits -= 8;
|
|
7039
|
-
}
|
|
7040
|
-
}
|
|
7041
|
-
return Uint8Array.from(out);
|
|
7042
|
-
}
|
|
7043
|
-
/** 一个时步的 TOTP code(6 位,前导零保留)。counter = floor(epochSeconds / step) */
|
|
7044
|
-
function totpCode(secretBase32, counter) {
|
|
7045
|
-
const key = base32Decode(secretBase32);
|
|
7046
|
-
const msg = Buffer.alloc(8);
|
|
7047
|
-
msg.writeBigUInt64BE(BigInt(counter));
|
|
7048
|
-
const digest = createHmac("sha1", Buffer.from(key)).update(msg).digest();
|
|
7049
|
-
const offset = digest[digest.length - 1] & 15;
|
|
7050
|
-
const bin = (digest[offset] & 127) << 24 | digest[offset + 1] << 16 | digest[offset + 2] << 8 | digest[offset + 3];
|
|
7051
|
-
return String(bin % 10 ** 6).padStart(6, "0");
|
|
7052
|
-
}
|
|
7053
|
-
/** 当前 epoch 秒 */
|
|
7054
|
-
function nowEpochSeconds() {
|
|
7055
|
-
return Math.floor(Date.now() / 1e3);
|
|
7056
|
-
}
|
|
7057
|
-
/**
|
|
7058
|
-
* 校验用户输入的 code(允许 ±TOTP_WINDOW 时步漂移,防重放窗口内同 code 复用由
|
|
7059
|
-
* 调用方按账号记录最近成功 counter 实现——本函数只做纯算法校验)。
|
|
7060
|
-
* @returns 命中的 counter(用于防重放);不匹配返回 null。
|
|
7061
|
-
*/
|
|
7062
|
-
function verifyTotpCode(secretBase32, code, nowSeconds = nowEpochSeconds()) {
|
|
7063
|
-
if (!/^\d{6}$/.test(code)) return null;
|
|
7064
|
-
const current = Math.floor(nowSeconds / 30);
|
|
7065
|
-
for (let offset = -1; offset <= 1; offset++) if (totpCode(secretBase32, current + offset) === code) return current + offset;
|
|
7066
|
-
return null;
|
|
7067
|
-
}
|
|
7068
|
-
//#endregion
|
|
7069
|
-
//#region src/config-ops.ts
|
|
7070
|
-
/**
|
|
7071
|
-
* config-ops.ts — plugin 全局配置读写(结构化,`~/.dsh/serenity-hooks.json`)
|
|
7072
|
-
*
|
|
7073
|
-
* 归属原则(S142 用户拍板,v1.22):**plugin 是全局的,CCC 是具体的**——
|
|
7074
|
-
* 账号密码/gateway 监听配置是 plugin 级能力,归 plugin 全局文件;
|
|
7075
|
-
* CCC 的 localstore.json 只管 CCC 自己的凭据/配置。v1.21.x 曾把
|
|
7076
|
-
* `serenityAdvanced` 存进 CCC localstore(归属错误 + 与 DSH settings 开关割裂),
|
|
7077
|
-
* 本版本迁移到 plugin 全局文件(migrateLegacyLocalstore 一次性迁移)。
|
|
7078
|
-
*
|
|
7079
|
-
* 文件:$DSH_HOME/serenity-hooks.json(缺省 ~/.dsh/serenity-hooks.json;
|
|
7080
|
-
* env SERENITY_HOOKS_CONFIG 可覆盖——测试/部署注入)。
|
|
7081
|
-
* 权限:0600(含账号密码 hash,敏感)。
|
|
7082
|
-
*
|
|
7083
|
-
* 安全:密码仅存 scrypt hash(node:crypto 内置,零依赖);wire 层永不返回 hash
|
|
7084
|
-
* (GET 只回 user/id,设置面板"密码"字段提交空串 = 不修改)。
|
|
7085
|
-
*/
|
|
7086
|
-
/** 高级设定节名(localstore.json 顶层) */
|
|
7087
|
-
const ADVANCED_SECTION = "serenityAdvanced";
|
|
7088
|
-
/** 默认值(工厂——每次返回新对象,防止调用方意外共享引用) */
|
|
7089
|
-
function defaultAdvancedSettings() {
|
|
7090
|
-
return {
|
|
7091
|
-
gateway: {
|
|
7092
|
-
enabled: false,
|
|
7093
|
-
host: "0.0.0.0",
|
|
7094
|
-
port: 3081,
|
|
7095
|
-
accounts: [],
|
|
7096
|
-
workspaces: [],
|
|
7097
|
-
cookieSecure: false,
|
|
7098
|
-
allowWorkspaceCreate: true,
|
|
7099
|
-
totpEnabled: false
|
|
7100
|
-
},
|
|
7101
|
-
rebuild: {
|
|
7102
|
-
enabled: true,
|
|
7103
|
-
thresholdRatio: .9
|
|
7104
|
-
},
|
|
7105
|
-
naming: { enabled: true }
|
|
7106
|
-
};
|
|
7107
|
-
}
|
|
7108
|
-
const SCRYPT_KEYLEN = 32;
|
|
7109
|
-
/** 生成 scrypt hash(格式 `salt:hex`);salt 16 字节随机 */
|
|
7110
|
-
function hashPassword(password) {
|
|
7111
|
-
const salt = randomBytes(16);
|
|
7112
|
-
const derived = scryptSync(password, salt, SCRYPT_KEYLEN);
|
|
7113
|
-
return `${salt.toString("hex")}:${derived.toString("hex")}`;
|
|
7114
|
-
}
|
|
7115
|
-
/** 校验密码与存储 hash(timing-safe) */
|
|
7116
|
-
function verifyPassword(password, stored) {
|
|
7117
|
-
const idx = stored.indexOf(":");
|
|
7118
|
-
if (idx <= 0) return false;
|
|
7119
|
-
const salt = Buffer.from(stored.slice(0, idx), "hex");
|
|
7120
|
-
const expected = Buffer.from(stored.slice(idx + 1), "hex");
|
|
7121
|
-
const derived = scryptSync(password, salt, SCRYPT_KEYLEN);
|
|
7122
|
-
return expected.length === derived.length && timingSafeEqual(expected, derived);
|
|
7123
|
-
}
|
|
7124
|
-
/** 全局配置文件路径:env SERENITY_HOOKS_CONFIG 覆盖(测试注入)→ $DSH_HOME → ~/.dsh */
|
|
7125
|
-
function globalConfigPath() {
|
|
7126
|
-
const override = process.env.SERENITY_HOOKS_CONFIG;
|
|
7127
|
-
if (override && override !== "") return override;
|
|
7128
|
-
const dshHome = process.env.DSH_HOME ?? join(process.env.HOME ?? "", ".dsh");
|
|
7129
|
-
return join(dshHome, "serenity-hooks.json");
|
|
7130
|
-
}
|
|
7131
|
-
function readFileSafe(p) {
|
|
7132
|
-
if (!existsSync(p)) return {};
|
|
7133
|
-
try {
|
|
7134
|
-
const v = JSON.parse(readFileSync(p, "utf-8").replace(/^\uFEFF/, ""));
|
|
7135
|
-
if (v && typeof v === "object" && !Array.isArray(v)) return v;
|
|
7136
|
-
return {};
|
|
7137
|
-
} catch {
|
|
7138
|
-
return {};
|
|
7139
|
-
}
|
|
7140
|
-
}
|
|
7141
|
-
function writeFileSafe(p, data) {
|
|
7142
|
-
writeFileSync(p, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
7143
|
-
try {
|
|
7144
|
-
chmodSync(p, 384);
|
|
7145
|
-
} catch {}
|
|
7220
|
+
const prompt = readFileSync(resolve(root, ".dsh", "PHASE2-PROMPT.md"), "utf-8");
|
|
7221
|
+
lines.push(truncateContent(prompt, Math.min(entrySkillMaxChars, 8e3)));
|
|
7222
|
+
} catch {}
|
|
7223
|
+
}
|
|
7224
|
+
return lines.join("\n");
|
|
7146
7225
|
}
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
rebuild: {
|
|
7167
|
-
enabled: typeof rebuild.enabled === "boolean" ? rebuild.enabled : def.rebuild.enabled,
|
|
7168
|
-
thresholdRatio: typeof rebuild.thresholdRatio === "number" ? rebuild.thresholdRatio : def.rebuild.thresholdRatio
|
|
7169
|
-
},
|
|
7170
|
-
naming: { enabled: typeof naming.enabled === "boolean" ? naming.enabled : def.naming.enabled }
|
|
7171
|
-
};
|
|
7226
|
+
const PLUGIN_SOURCE = {
|
|
7227
|
+
kind: "plugin",
|
|
7228
|
+
plugin: "dsh-serenity-hooks"
|
|
7229
|
+
};
|
|
7230
|
+
/**
|
|
7231
|
+
* ACC 注入消息(S134 去重):**只含简短身份锚点**([ACC] 已激活 + CCC 根 + 约束 + loop 模型 + Phase 2)。
|
|
7232
|
+
* 完整身份(ACC 5 块 + CCE + Constraints + EAP + SKILL 全文 + Session 块)由**系统提示词层**
|
|
7233
|
+
* (systemPrompt.section,每轮 prompt 装配自动注入,含 subagent)承担——对话消息流/压缩重注入
|
|
7234
|
+
* 不再重复注入同一内容(token 双倍浪费,见 S134 注入方案梳理)。
|
|
7235
|
+
*/
|
|
7236
|
+
function accMessage(root, configPaths, entrySkillMaxChars) {
|
|
7237
|
+
const content = [{
|
|
7238
|
+
type: "text",
|
|
7239
|
+
text: accIdentityText(root, configPaths, entrySkillMaxChars)
|
|
7240
|
+
}];
|
|
7241
|
+
return createUserMessage({
|
|
7242
|
+
content,
|
|
7243
|
+
source: PLUGIN_SOURCE
|
|
7244
|
+
});
|
|
7172
7245
|
}
|
|
7173
|
-
/**
|
|
7174
|
-
|
|
7175
|
-
|
|
7246
|
+
/** 每 agent 是否已注入过(进程内存态) */
|
|
7247
|
+
const injected = /* @__PURE__ */ new Set();
|
|
7248
|
+
function agentKey(agent) {
|
|
7249
|
+
return agent.session.id ?? "global";
|
|
7176
7250
|
}
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
writeFileSafe(globalConfigPath(), settings);
|
|
7251
|
+
function agentScope(agent) {
|
|
7252
|
+
return agent.session.id ?? "default";
|
|
7180
7253
|
}
|
|
7181
7254
|
/**
|
|
7182
|
-
*
|
|
7183
|
-
*
|
|
7255
|
+
* 重启恢复的根会话判定(S134 需求):
|
|
7256
|
+
* 只有"conversation 根会话"才自动恢复最近激活的宁静号会话——
|
|
7257
|
+
* - subagent:session header `origin === 'subagent'`(DSH 路由语义,agent-lookup.ts)
|
|
7258
|
+
* - 派生会话:`parentSession` 存在(任何子会话)
|
|
7259
|
+
* - loop 牛马:sessionId 固定 `loop-` 前缀(tools/loop.ts 生成)
|
|
7260
|
+
* 三者都不恢复(避免把主会话激活注入子上下文,违背 v1.16.2 scope 隔离)。
|
|
7184
7261
|
*/
|
|
7185
|
-
function
|
|
7186
|
-
const
|
|
7187
|
-
|
|
7188
|
-
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
enabled: typeof gw.enabled === "boolean" ? gw.enabled : current.gateway.enabled,
|
|
7193
|
-
host: typeof gw.host === "string" && gw.host !== "" ? gw.host : current.gateway.host,
|
|
7194
|
-
port: typeof gw.port === "number" && Number.isInteger(gw.port) ? gw.port : current.gateway.port,
|
|
7195
|
-
accounts: Array.isArray(gw.accounts) ? gw.accounts : current.gateway.accounts,
|
|
7196
|
-
workspaces: Array.isArray(gw.workspaces) ? gw.workspaces.filter((w) => typeof w === "string" && w !== "") : current.gateway.workspaces,
|
|
7197
|
-
cookieSecure: typeof gw.cookieSecure === "boolean" ? gw.cookieSecure : current.gateway.cookieSecure,
|
|
7198
|
-
allowWorkspaceCreate: typeof gw.allowWorkspaceCreate === "boolean" ? gw.allowWorkspaceCreate : current.gateway.allowWorkspaceCreate,
|
|
7199
|
-
totpEnabled: typeof gw.totpEnabled === "boolean" ? gw.totpEnabled : current.gateway.totpEnabled
|
|
7200
|
-
} : current.gateway,
|
|
7201
|
-
rebuild: rb !== void 0 ? {
|
|
7202
|
-
enabled: typeof rb.enabled === "boolean" ? rb.enabled : current.rebuild.enabled,
|
|
7203
|
-
thresholdRatio: typeof rb.thresholdRatio === "number" && rb.thresholdRatio > 0 && rb.thresholdRatio <= 1 ? rb.thresholdRatio : current.rebuild.thresholdRatio
|
|
7204
|
-
} : current.rebuild,
|
|
7205
|
-
naming: nm !== void 0 ? { enabled: typeof nm.enabled === "boolean" ? nm.enabled : current.naming.enabled } : current.naming
|
|
7206
|
-
};
|
|
7207
|
-
writeAdvancedSettings(next);
|
|
7208
|
-
return next;
|
|
7262
|
+
function shouldAutoRestore(agent) {
|
|
7263
|
+
const session = agent.session;
|
|
7264
|
+
if (!session) return false;
|
|
7265
|
+
if (session.header?.origin === "subagent") return false;
|
|
7266
|
+
if (session.header?.parentSession) return false;
|
|
7267
|
+
if (session.id?.startsWith("loop-")) return false;
|
|
7268
|
+
return true;
|
|
7209
7269
|
}
|
|
7210
7270
|
/**
|
|
7211
|
-
*
|
|
7212
|
-
*
|
|
7213
|
-
*
|
|
7214
|
-
* @returns true = 已迁移(旧节保留在 localstore 供回滚,读取方以全局文件为准)
|
|
7271
|
+
* 恢复触发判定(S134 泄漏修复 v1.16.13):根会话 **且已有对话历史**(续跑/恢复的会话)
|
|
7272
|
+
* 才自动恢复上次激活的宁静号会话——全新会话(新任务,如 apaas-26116)无历史 → 不恢复,
|
|
7273
|
+
* 避免把过去的 SESSION 上下文注入新任务(跨任务污染)。
|
|
7215
7274
|
*/
|
|
7216
|
-
function
|
|
7217
|
-
if (!
|
|
7218
|
-
const
|
|
7219
|
-
|
|
7220
|
-
const legacy = readFileSafe(join(root, "localstore.json"))[ADVANCED_SECTION];
|
|
7221
|
-
if (legacy === void 0 || legacy === null || typeof legacy !== "object") return false;
|
|
7222
|
-
writeFileSafe(gpath, legacy);
|
|
7223
|
-
return true;
|
|
7275
|
+
function shouldRestoreActive(agent) {
|
|
7276
|
+
if (!shouldAutoRestore(agent)) return false;
|
|
7277
|
+
const events = agent.session?.events;
|
|
7278
|
+
return Array.isArray(events) && events.length > 0;
|
|
7224
7279
|
}
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
}
|
|
7243
|
-
|
|
7244
|
-
|
|
7280
|
+
function registerContext(ctx, opts = {}) {
|
|
7281
|
+
const configPaths = opts.configPaths ?? DEFAULT_SERENITY_CONFIG_PATHS;
|
|
7282
|
+
const entrySkillMaxChars = opts.entrySkillMaxChars ?? 3e4;
|
|
7283
|
+
const seed = (agent) => {
|
|
7284
|
+
const root = findSerenityRoot(agent.session?.header?.cwd ?? process.cwd());
|
|
7285
|
+
if (!root) return;
|
|
7286
|
+
const key = agentKey(agent);
|
|
7287
|
+
registerEntrySkillSection(agent, root);
|
|
7288
|
+
const scope = agentScope(agent);
|
|
7289
|
+
if (shouldRestoreActive(agent) && getActiveSessionInfo(scope) === null) try {
|
|
7290
|
+
if (loadSerenityConfig(root, configPaths).hooks?.autoRestoreSession ?? true) {
|
|
7291
|
+
const info = parseSessionContextFromEvents(agent.session.events ?? []);
|
|
7292
|
+
if (info) {
|
|
7293
|
+
setActiveSessionInfo(scope, info);
|
|
7294
|
+
console.log(`[serenity-hooks] ↻ 从历史恢复激活会话: ${info.dirName}`);
|
|
7295
|
+
}
|
|
7296
|
+
}
|
|
7297
|
+
} catch {}
|
|
7298
|
+
if (injected.has(key)) return;
|
|
7299
|
+
injected.add(key);
|
|
7300
|
+
agent.inject(accMessage(root, configPaths, entrySkillMaxChars));
|
|
7301
|
+
syncSafeModeRestriction(agent, root);
|
|
7245
7302
|
};
|
|
7303
|
+
if (opts.seedOnStart ?? true) ctx.on("agent/session-start", (payload) => {
|
|
7304
|
+
try {
|
|
7305
|
+
seed(payload.agent);
|
|
7306
|
+
} catch {}
|
|
7307
|
+
});
|
|
7308
|
+
if (opts.injectOnPrompt ?? true) ctx.on("agent/pre-step", async (payload, next) => {
|
|
7309
|
+
const { agent, messages } = payload;
|
|
7310
|
+
const root = findSerenityRoot(agent.session?.header?.cwd ?? process.cwd());
|
|
7311
|
+
const key = agentKey(agent);
|
|
7312
|
+
const downstream = await next();
|
|
7313
|
+
if (root) {
|
|
7314
|
+
try {
|
|
7315
|
+
syncSafeModeRestriction(agent, root);
|
|
7316
|
+
} catch {}
|
|
7317
|
+
registerEntrySkillSection(agent, root);
|
|
7318
|
+
}
|
|
7319
|
+
if (!root || injected.has(key) || downstream.kind !== "enter") return downstream;
|
|
7320
|
+
injected.add(key);
|
|
7321
|
+
return {
|
|
7322
|
+
kind: "enter",
|
|
7323
|
+
messages: [
|
|
7324
|
+
accMessage(root, configPaths, entrySkillMaxChars),
|
|
7325
|
+
...messages,
|
|
7326
|
+
...downstream.messages
|
|
7327
|
+
]
|
|
7328
|
+
};
|
|
7329
|
+
});
|
|
7246
7330
|
}
|
|
7331
|
+
//#endregion
|
|
7332
|
+
//#region src/seams/compact.ts
|
|
7247
7333
|
/**
|
|
7248
|
-
*
|
|
7249
|
-
*
|
|
7250
|
-
* 新账号(id 不在现有)必须带非空 pass,否则抛错(无法生成 hash)。
|
|
7334
|
+
* 注册压缩保留:compact/end(成功)后重注入 ACC 身份。
|
|
7335
|
+
* 仅当 agent 工作目录在 CCC 内时生效(激活门控)。
|
|
7251
7336
|
*/
|
|
7252
|
-
function
|
|
7253
|
-
const
|
|
7254
|
-
const
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
if (
|
|
7258
|
-
|
|
7259
|
-
if (
|
|
7260
|
-
|
|
7261
|
-
if (
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
gwPatch.accounts = wire.gateway.accounts.map((a) => {
|
|
7267
|
-
const existing = byId.get(a.id);
|
|
7268
|
-
const pass = a.pass;
|
|
7269
|
-
const totp = a.totpSecret;
|
|
7270
|
-
const totpReset = a.totpReset === true;
|
|
7271
|
-
const totpConfirm = a.totpConfirm;
|
|
7272
|
-
if (typeof totp === "string" && totp !== "") {
|
|
7273
|
-
if (typeof totpConfirm !== "string" || verifyTotpCode(totp, totpConfirm) === null) throw new Error(`Account "${a.user}" TOTP confirmation code invalid — enter the 6-digit code currently shown by the authenticator`);
|
|
7274
|
-
}
|
|
7275
|
-
const nextTotp = totpReset ? void 0 : typeof totp === "string" && totp !== "" ? totp : existing?.totpSecret;
|
|
7276
|
-
const withTotp = {
|
|
7277
|
-
id: a.id,
|
|
7278
|
-
user: a.user,
|
|
7279
|
-
...nextTotp === void 0 ? {} : { totpSecret: nextTotp }
|
|
7280
|
-
};
|
|
7281
|
-
if (typeof pass === "string" && pass !== "") return {
|
|
7282
|
-
...withTotp,
|
|
7283
|
-
passHash: hashPassword(pass)
|
|
7284
|
-
};
|
|
7285
|
-
if (existing) return {
|
|
7286
|
-
...withTotp,
|
|
7287
|
-
passHash: existing.passHash
|
|
7288
|
-
};
|
|
7289
|
-
throw new Error(`Account "${a.user}" (id=${a.id}) has no password and no existing hash — new accounts must set a password`);
|
|
7290
|
-
});
|
|
7291
|
-
}
|
|
7292
|
-
patch.gateway = gwPatch;
|
|
7293
|
-
}
|
|
7294
|
-
if (wire.rebuild !== void 0) {
|
|
7295
|
-
const rbPatch = { ...current.rebuild };
|
|
7296
|
-
if (typeof wire.rebuild.enabled === "boolean") rbPatch.enabled = wire.rebuild.enabled;
|
|
7297
|
-
if (typeof wire.rebuild.thresholdRatio === "number" && wire.rebuild.thresholdRatio > 0 && wire.rebuild.thresholdRatio <= 1) rbPatch.thresholdRatio = wire.rebuild.thresholdRatio;
|
|
7298
|
-
patch.rebuild = rbPatch;
|
|
7299
|
-
}
|
|
7300
|
-
if (wire.naming !== void 0 && typeof wire.naming.enabled === "boolean") patch.naming = { enabled: wire.naming.enabled };
|
|
7301
|
-
return updateAdvancedSettings(patch);
|
|
7337
|
+
function registerCompactRetention(ctx, opts = {}) {
|
|
7338
|
+
const configPaths = opts.configPaths ?? DEFAULT_SERENITY_CONFIG_PATHS;
|
|
7339
|
+
const entrySkillMaxChars = opts.entrySkillMaxChars ?? 3e4;
|
|
7340
|
+
ctx.on("session/event", (session, event) => {
|
|
7341
|
+
if (event.type !== "compaction/end") return;
|
|
7342
|
+
if (event.data.error) return;
|
|
7343
|
+
const agent = ctx.agents.get(session.id);
|
|
7344
|
+
if (!agent) return;
|
|
7345
|
+
const root = findSerenityRoot(agent.session?.header?.cwd ?? process.cwd());
|
|
7346
|
+
if (!root) return;
|
|
7347
|
+
try {
|
|
7348
|
+
agent.inject(accMessage(root, configPaths, entrySkillMaxChars));
|
|
7349
|
+
} catch {}
|
|
7350
|
+
});
|
|
7302
7351
|
}
|
|
7303
7352
|
//#endregion
|
|
7304
7353
|
//#region src/api.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shgroup/dsh-serenity-hooks",
|
|
3
|
-
"version": "1.23.
|
|
3
|
+
"version": "1.23.1",
|
|
4
4
|
"description": "宁静号 ACC harness — Native Cordis 插件(DeepSeek Harness 运行时)。真实 DSH 工具注册(cc_fs/session/acc_msm 等 9 工具)+ 拦截缝机械约束(safe-mode/路径守卫/会话落盘)+ 系统提示词注入(ACC/CCE/Constraints/SKILL/Session 五块)。适配 DSH 公开版(deepseek-ai/deepseek-harness 0.1.0-rc)。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|