@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-serenity-hooks",
3
- "version": "1.23.0",
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$2 = "/* 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";
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$2;
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 + 工作区白名单)→ 返回保存后的 wire */
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$1 = "/* 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";
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$1;
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
- return [
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
- "MSM principles machinery before improvisation:",
6524
- "- Determinism first: use a registered Mech before hand-rolling; reserve",
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
- /** 深合并默认值(缺省字段补齐;accounts 数组整体替换) */
7148
- function mergeWithDefaults(raw) {
7149
- const def = defaultAdvancedSettings();
7150
- if (raw === null || typeof raw !== "object") return def;
7151
- const o = raw;
7152
- const gateway = o.gateway ?? {};
7153
- const rebuild = o.rebuild ?? {};
7154
- const naming = o.naming ?? {};
7155
- return {
7156
- gateway: {
7157
- enabled: typeof gateway.enabled === "boolean" ? gateway.enabled : def.gateway.enabled,
7158
- host: typeof gateway.host === "string" && gateway.host !== "" ? gateway.host : def.gateway.host,
7159
- port: typeof gateway.port === "number" && Number.isInteger(gateway.port) ? gateway.port : def.gateway.port,
7160
- 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,
7161
- workspaces: Array.isArray(gateway.workspaces) ? gateway.workspaces.filter((w) => typeof w === "string" && w !== "") : def.gateway.workspaces,
7162
- cookieSecure: typeof gateway.cookieSecure === "boolean" ? gateway.cookieSecure : def.gateway.cookieSecure,
7163
- allowWorkspaceCreate: typeof gateway.allowWorkspaceCreate === "boolean" ? gateway.allowWorkspaceCreate : def.gateway.allowWorkspaceCreate,
7164
- totpEnabled: typeof gateway.totpEnabled === "boolean" ? gateway.totpEnabled : def.gateway.totpEnabled
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
- /** 读取全局配置(文件缺失/坏 JSON 默认值) */
7174
- function readAdvancedSettings() {
7175
- return mergeWithDefaults(readFileSafe(globalConfigPath()));
7246
+ /** agent 是否已注入过(进程内存态) */
7247
+ const injected = /* @__PURE__ */ new Set();
7248
+ function agentKey(agent) {
7249
+ return agent.session.id ?? "global";
7176
7250
  }
7177
- /** 写入全局配置(整体替换) */
7178
- function writeAdvancedSettings(settings) {
7179
- writeFileSafe(globalConfigPath(), settings);
7251
+ function agentScope(agent) {
7252
+ return agent.session.id ?? "default";
7180
7253
  }
7181
7254
  /**
7182
- * 部分更新:传入 Partial,深合并到现有值。
7183
- * accounts 传入数组 → 整体替换;accounts 未传 → 保留现有。
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 updateAdvancedSettings(patch) {
7186
- const current = readAdvancedSettings();
7187
- const gw = patch.gateway;
7188
- const rb = patch.rebuild;
7189
- const nm = patch.naming;
7190
- const next = {
7191
- gateway: gw !== void 0 ? {
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
- * 一次性迁移(v1.21.x v1.22):旧版把 `serenityAdvanced` 存在 CCC localstore.json;
7212
- * 新版归 plugin 全局文件。全局文件已存在 跳过(幂等);localstore 无旧节 → 跳过。
7213
- * @param root - CCC 根(localstore.json 所在目录)
7214
- * @returns true = 已迁移(旧节保留在 localstore 供回滚,读取方以全局文件为准)
7271
+ * 恢复触发判定(S134 泄漏修复 v1.16.13):根会话 **且已有对话历史**(续跑/恢复的会话)
7272
+ * 才自动恢复上次激活的宁静号会话——全新会话(新任务,如 apaas-26116)无历史不恢复,
7273
+ * 避免把过去的 SESSION 上下文注入新任务(跨任务污染)。
7215
7274
  */
7216
- function migrateLegacyLocalstore(root) {
7217
- if (!root) return false;
7218
- const gpath = globalConfigPath();
7219
- if (existsSync(gpath)) return false;
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
- /** 持久化 wire(剥离 passHash/totpSecret;只留布尔) */
7226
- function toWire(settings) {
7227
- return {
7228
- gateway: {
7229
- enabled: settings.gateway.enabled,
7230
- host: settings.gateway.host,
7231
- port: settings.gateway.port,
7232
- accounts: settings.gateway.accounts.map((a) => ({
7233
- id: a.id,
7234
- user: a.user,
7235
- hasPassword: a.passHash !== "",
7236
- hasTotp: typeof a.totpSecret === "string" && a.totpSecret !== ""
7237
- })),
7238
- workspaces: [...settings.gateway.workspaces],
7239
- cookieSecure: settings.gateway.cookieSecure,
7240
- allowWorkspaceCreate: settings.gateway.allowWorkspaceCreate,
7241
- totpEnabled: settings.gateway.totpEnabled
7242
- },
7243
- rebuild: settings.rebuild,
7244
- naming: settings.naming
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
- * wire 持久化(面板 PUT 用):
7249
- * accounts 元素可选带 `pass`:非空 重新 hash;空/缺省 → 保留现有 hash(按 id 匹配)。
7250
- * 新账号(id 不在现有)必须带非空 pass,否则抛错(无法生成 hash)。
7334
+ * 注册压缩保留:compact/end(成功)后重注入 ACC 身份。
7335
+ * 仅当 agent 工作目录在 CCC 内时生效(激活门控)。
7251
7336
  */
7252
- function applyWirePatch(wire) {
7253
- const current = readAdvancedSettings();
7254
- const patch = {};
7255
- if (wire.gateway !== void 0) {
7256
- const gwPatch = { ...current.gateway };
7257
- if (typeof wire.gateway.enabled === "boolean") gwPatch.enabled = wire.gateway.enabled;
7258
- if (typeof wire.gateway.host === "string" && wire.gateway.host !== "") gwPatch.host = wire.gateway.host;
7259
- if (typeof wire.gateway.port === "number" && Number.isInteger(wire.gateway.port)) gwPatch.port = wire.gateway.port;
7260
- if (Array.isArray(wire.gateway.workspaces)) gwPatch.workspaces = wire.gateway.workspaces.filter((w) => typeof w === "string" && w !== "");
7261
- if (typeof wire.gateway.cookieSecure === "boolean") gwPatch.cookieSecure = wire.gateway.cookieSecure;
7262
- if (typeof wire.gateway.allowWorkspaceCreate === "boolean") gwPatch.allowWorkspaceCreate = wire.gateway.allowWorkspaceCreate;
7263
- if (typeof wire.gateway.totpEnabled === "boolean") gwPatch.totpEnabled = wire.gateway.totpEnabled;
7264
- if (Array.isArray(wire.gateway.accounts)) {
7265
- const byId = new Map(current.gateway.accounts.map((a) => [a.id, a]));
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.0",
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": {