@shgroup/dsh-serenity-hooks 1.24.9 → 1.24.11

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.24.9",
3
+ "version": "1.24.11",
4
4
  "main": "lib/index.js",
5
5
  "description": "宁静号 ACC harness(Native Cordis 插件):真实 DSH 工具 cc_fs/session/acc_msm/eap/neat/cce/handyman/session_rebuild + 拦截缝机械约束(safe-mode/路径守卫)+ 高级设定面板(双端口网关/账号管理)。适配 DSH 公开版(0.1.0-rc,deepseek-ai/deepseek-harness)。",
6
6
  "engines": {
package/lib/client.js CHANGED
@@ -8,7 +8,7 @@ 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$3 = "/* SafeModePanel — 会话头部 Serenity 状态卡片(v1.22 UX 重构)\n * 卡片 → 点击 → 官方 Modalbody 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: 8px;\n height: 30px;\n padding: 0 8px 0 14px;\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/* 方案 O 盾牌版(v1.24.2):绿点 = Serenity 生效(恒绿);盾牌 = SAFE 状态\n * (琥珀空心盾 = SAFE OFF 未保护提醒 / 绿实心盾 = SAFE ON 保护中安心) */\n\n.sp-dot {\n flex: none;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: var(--dsw-alias-state-success-primary);\n}\n\n.sp-dotOff {\n background: var(--dsw-alias-label-dimmed);\n}\n\n.sp-cardInactive {\n opacity: 0.85;\n}\n\n.sp-cardInactive .sp-brand {\n color: var(--dsw-alias-label-secondary);\n font-weight: 500;\n}\n\n.sp-brand {\n color: var(--dsw-alias-label-primary);\n font-weight: 600;\n}\n\n.sp-ver {\n color: var(--dsw-alias-label-secondary);\n font-size: 11px;\n font-family: var(--ds-font-family-code, monospace);\n}\n\n.sp-sep {\n flex: none;\n width: 1px;\n height: 14px;\n background: var(--dsw-alias-border-l1);\n}\n\n.sp-shield {\n display: inline-flex;\n width: 16px;\n height: 16px;\n flex: none;\n}\n\n.sp-shield svg {\n width: 16px;\n height: 16px;\n}\n\n.sp-statusWord {\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.02em;\n white-space: nowrap;\n}\n\n/* SAFE OFF(未保护):琥珀——提醒,非错误(用户自己的开关选择) */\n.sp-cardUnprotected .sp-shield {\n color: var(--dsw-alias-state-warning-primary);\n}\n\n.sp-cardUnprotected .sp-statusWord {\n color: var(--dsw-alias-state-warning-primary);\n}\n\n/* SAFE ON(保护中):绿——安心(设计稿 SVG fill=none:纯描边盾,与琥珀空心盾同构,颜色表达状态) */\n.sp-cardProtected .sp-shield {\n color: var(--dsw-alias-state-success-primary);\n}\n\n.sp-cardProtected .sp-statusWord {\n color: var(--dsw-alias-state-success-primary);\n}\n\n/* 滑块快速开关(v1.24.4 方案 O 滑块变体):SAFE 文字保留 + 滑块——\n * 滑块左 = OFF(琥珀 ✗,写工具全开)/ 右 = ON(绿 ✓,写工具隐藏),开关隐喻表达开关状态 */\n.sp-switch {\n display: inline-flex;\n align-items: center;\n width: 30px;\n height: 17px;\n border-radius: 999px;\n position: relative;\n flex: none;\n cursor: pointer;\n transition: background 0.2s;\n}\n\n.sp-switchOff {\n background: color-mix(in srgb, var(--dsw-alias-state-warning-primary) 28%, var(--dsw-alias-border-l2));\n}\n\n.sp-switchOn {\n background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 40%, var(--dsw-alias-border-l2));\n}\n\n.sp-switchThumb {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 13px;\n height: 13px;\n border-radius: 50%;\n background: #fff; /* 设计稿字面:滑块 thumb 恒白(暗色下也白,与彩色轨道对比——标准开关视觉) */\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);\n position: absolute;\n top: 2px;\n transition: left 0.2s;\n}\n\n.sp-switchOff .sp-switchThumb {\n left: 2px;\n}\n\n.sp-switchOn .sp-switchThumb {\n left: 15px;\n}\n\n.sp-switchThumb svg {\n width: 8px;\n height: 8px;\n}\n\n.sp-switchOff .sp-switchThumb svg {\n color: var(--dsw-alias-state-warning-primary);\n}\n\n.sp-switchOn .sp-switchThumb svg {\n color: var(--dsw-alias-state-success-primary);\n}\n\n.sp-switchBusy {\n opacity: 0.6;\n cursor: default;\n}\n\n.sp-chev {\n color: var(--dsw-alias-label-dimmed);\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: color-mix(in srgb, var(--dsw-alias-state-error-primary) 14%, transparent);\n color: var(--dsw-alias-state-error-primary);\n border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary) 45%, transparent);\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/* handyman 运行状态区块 */\n\n.sp-handymenToggle {\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-handymenToggle:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.sp-handymenList {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin-top: 2px;\n}\n\n.sp-handymanItem {\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-handymanHead {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n}\n\n.sp-handymanLabel {\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-handymanRound {\n flex: none;\n font-size: 11px;\n color: var(--dsw-alias-label-dimmed);\n}\n\n.sp-handymanTime {\n flex: none;\n font-size: 11px;\n color: var(--dsw-alias-label-dimmed);\n}\n\n.sp-handymanResp {\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-handymenMore {\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.24.10 胶囊改版,对齐 OcgoDockEntry pill)\n * 胶囊 999px 全圆 → 点击 → 自绘 popover340px 右上角卡片,外点/Escape 关闭)\n * popover 内:分组(groupTitle 标题 + rowCard 行卡)——看齐官方 settings 设计语言\n * 遵循 web-styling.md:颜色只用 --dsw-alias-* 语义 token(明暗自适应)+ --sp-safe-green 统一绿家族;\n * 前缀 sp- 防冲突。 */\n\n/* ── 统一绿色变量家族(一处改全改;emerald 500~600 区间保证深色对比度) ── */\n\n.sp-root {\n --sp-safe-green: #0ba875; /* emerald-550:SAFE ON 主色 */\n --sp-safe-green-deep: #059669; /* emerald-600:渐变深端 */\n}\n\n/* ── 头部状态胶囊(可点击) ── */\n\n.sp-card {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 4px 9px; /* 无固定 height:由内容撑高(胶囊) */\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-2, color-mix(in srgb, var(--dsw-alias-bg-module) 70%, transparent));\n color: inherit;\n font-size: 12px;\n line-height: 1;\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/* 绿点 = Serenity 在线(恒亮绿,不随 SAFE 变);离线/未激活 */\n.sp-dot {\n flex: none;\n width: 7px;\n height: 7px;\n border-radius: 50%;\n background: var(--dsw-alias-state-success-primary);\n}\n\n.sp-dotOff {\n background: var(--dsw-alias-label-dimmed);\n}\n\n.sp-cardInactive {\n opacity: 0.85;\n}\n\n.sp-cardInactive .sp-brand {\n color: var(--dsw-alias-label-secondary);\n font-weight: 500;\n}\n\n.sp-brand {\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n\n/* 版本号:monospace + translateY(1px) 下沉对齐(与文字基线齐平) */\n.sp-ver {\n color: var(--dsw-alias-label-secondary);\n font-size: 11px;\n font-family: var(--ds-font-family-code, monospace);\n transform: translateY(1px);\n}\n\n/* 1px 深灰分隔线(两侧紧凑) */\n.sp-sep {\n flex: none;\n width: 1px;\n height: 12px;\n margin: 0 2px;\n background: var(--dsw-alias-label-tertiary);\n}\n\n.sp-shield {\n display: inline-flex;\n width: 14px;\n height: 14px;\n flex: none;\n}\n\n.sp-shield svg {\n width: 14px;\n height: 14px;\n}\n\n.sp-statusWord {\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0;\n white-space: nowrap;\n}\n\n/* SAFE OFF(未保护):灰——低姿态提醒,非错误(用户自己的开关选择) */\n.sp-cardUnprotected .sp-shield {\n color: var(--dsw-alias-label-secondary);\n}\n\n.sp-cardUnprotected .sp-statusWord {\n color: var(--dsw-alias-label-secondary);\n}\n\n/* SAFE ON(保护中):emerald 绿——安心(盾牌实色 + SAFE 文字渐变质感) */\n.sp-cardProtected .sp-shield {\n color: var(--sp-safe-green);\n}\n\n.sp-cardProtected .sp-statusWord {\n color: var(--sp-safe-green);\n background: linear-gradient(135deg, var(--sp-safe-green) 0%, var(--sp-safe-green-deep) 100%);\n -webkit-background-clip: text;\n background-clip: text;\n -webkit-text-fill-color: transparent;\n}\n\n/* ── Mac 风格滑块(v1.24.10:轨道 24×13 / thumb 11px 灰白 #f2f4f6 无图标 / cubic-bezier 平滑) ── */\n\n.sp-switch {\n display: inline-block;\n width: 24px;\n height: 13px;\n border-radius: 999px;\n position: relative;\n flex: none;\n cursor: pointer;\n transition: background 0.25s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n.sp-switchOff {\n background: var(--dsw-alias-border-l2);\n}\n\n.sp-switchOn {\n background: var(--sp-safe-green);\n}\n\n.sp-switchThumb {\n position: absolute;\n top: 1px;\n width: 11px;\n height: 11px;\n border-radius: 50%;\n background: #f2f4f6; /* Mac 灰白 thumb(明暗通用) */\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);\n transition: left 0.25s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n.sp-switchOff .sp-switchThumb {\n left: 1px;\n}\n\n.sp-switchOn .sp-switchThumb {\n left: 12px; /* 24 - 11 - 1 */\n}\n\n/* CSS 特异性坑(S013 指南):.sp-switch:hover (0,2,0) 会覆盖 .sp-switchOn (0,1,0)\n * → ON 态 hover 绿底消失。必须同特异性且后定义补回。 */\n.sp-switch:hover {\n filter: brightness(1.08);\n}\n\n.sp-switchOn:hover {\n background: var(--sp-safe-green);\n}\n\n.sp-switchBusy {\n opacity: 0.6;\n cursor: default;\n pointer-events: none;\n}\n\n.sp-chev {\n color: var(--dsw-alias-label-dimmed);\n flex: none;\n}\n\n/* ── 自绘 popover(340px 右上角小卡片,替代官方 Modal) ── */\n\n.sp-root {\n position: relative;\n display: inline-flex;\n}\n\n.sp-pop {\n position: absolute;\n top: calc(100% + 8px);\n right: 0;\n width: 340px;\n max-width: calc(100vw - 24px);\n z-index: 2000;\n box-sizing: border-box;\n padding: 14px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 14px;\n background: var(--dsw-alias-bg-module, #1e1e1e);\n box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);\n}\n\n.sp-popBody {\n display: flex;\n flex-direction: column;\n gap: 14px;\n max-height: calc(100vh - 140px);\n overflow-y: auto;\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-layer-2, color-mix(in srgb, var(--dsw-alias-bg-module) 60%, transparent));\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-layer-2, color-mix(in srgb, var(--dsw-alias-bg-module) 60%, transparent));\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: color-mix(in srgb, var(--dsw-alias-state-error-primary) 14%, transparent);\n color: var(--dsw-alias-state-error-primary);\n border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary) 45%, transparent);\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/* handyman 运行状态区块 */\n\n.sp-handymenToggle {\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-handymenToggle:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.sp-handymenList {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin-top: 2px;\n}\n\n.sp-handymanItem {\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-handymanHead {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n}\n\n.sp-handymanLabel {\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-handymanRound {\n flex: none;\n font-size: 11px;\n color: var(--dsw-alias-label-dimmed);\n}\n\n.sp-handymanTime {\n flex: none;\n font-size: 11px;\n color: var(--dsw-alias-label-dimmed);\n}\n\n.sp-handymanResp {\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-handymenMore {\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=\"SafeModePanel\"]") === null) {
13
13
  const tag = document.createElement("style");
14
14
  tag.setAttribute("data-sp-css", "SafeModePanel");
@@ -19,7 +19,7 @@ window.__ModuleLoader__.load({
19
19
  //#region src/client/SafeModePanel.tsx
20
20
  const API = "/serenity/status";
21
21
  const HANDYMEN_API = "/serenity/handymen";
22
- /** 盾牌 SVG(方案 O 盾牌版 v1.24.2;stroke currentColor——琥珀=OFF 提醒 / 绿=ON 安心由状态类控制) */
22
+ /** 盾牌 SVGv1.24.5 校准:fill=none 纯描边;ON=绿 emerald / OFF=灰 label-secondary 由状态类控制) */
23
23
  const SHIELD_SVG = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
24
24
  viewBox: "0 0 16 16",
25
25
  fill: "none",
@@ -39,45 +39,18 @@ window.__ModuleLoader__.load({
39
39
  strokeLinejoin: "round"
40
40
  })]
41
41
  });
42
- /** 滑块 thumb 图标(v1.24.4 方案 O 滑块变体:OFF=琥珀 ✗ / ON=绿 ✓,开关隐喻表达开关状态) */
43
- const SWITCH_OFF_ICON = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
44
- viewBox: "0 0 12 12",
45
- fill: "none",
46
- width: 8,
47
- height: 8,
48
- "aria-hidden": "true",
49
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
50
- d: "M3 3L9 9M9 3L3 9",
51
- stroke: "currentColor",
52
- strokeWidth: 1.8,
53
- strokeLinecap: "round"
54
- })
55
- });
56
- const SWITCH_ON_ICON = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
57
- viewBox: "0 0 12 12",
58
- fill: "none",
59
- width: 8,
60
- height: 8,
61
- "aria-hidden": "true",
62
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
63
- d: "M2 6.2L4.8 9L10 3.4",
64
- stroke: "currentColor",
65
- strokeWidth: 1.8,
66
- strokeLinecap: "round",
67
- strokeLinejoin: "round"
68
- })
69
- });
70
42
  /** 拼接类名(等价 clsx 最小子集,避免额外依赖) */
71
43
  function cx(...parts) {
72
44
  return parts.filter(Boolean).join(" ");
73
45
  }
74
- /** 会话头部:CCC 状态栏卡片(点击官方 Modal 居中,纯状态展示) */
46
+ /** 会话头部:Serenity 状态胶囊(点击自绘 popover,纯状态展示) */
75
47
  function SafeModePanel(props) {
76
48
  const [status, setStatus] = (0, react.useState)(null);
77
49
  const [busy, setBusy] = (0, react.useState)(false);
78
50
  const [open, setOpen] = (0, react.useState)(false);
79
51
  const [handymen, setHandymen] = (0, react.useState)([]);
80
52
  const [handymenOpen, setHandymenOpen] = (0, react.useState)(false);
53
+ const rootRef = (0, react.useRef)(null);
81
54
  const sessionId = props.sessionId;
82
55
  const refresh = (0, react.useCallback)(async () => {
83
56
  const qs = sessionId ? `?sessionId=${encodeURIComponent(sessionId)}` : "";
@@ -110,6 +83,21 @@ window.__ModuleLoader__.load({
110
83
  clearInterval(timer);
111
84
  };
112
85
  }, [open, sessionId]);
86
+ (0, react.useEffect)(() => {
87
+ if (!open) return;
88
+ const onDocMouseDown = (e) => {
89
+ if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false);
90
+ };
91
+ const onKey = (e) => {
92
+ if (e.key === "Escape") setOpen(false);
93
+ };
94
+ document.addEventListener("mousedown", onDocMouseDown);
95
+ document.addEventListener("keydown", onKey);
96
+ return () => {
97
+ document.removeEventListener("mousedown", onDocMouseDown);
98
+ document.removeEventListener("keydown", onKey);
99
+ };
100
+ }, [open]);
113
101
  const toggle = async (on) => {
114
102
  setBusy(true);
115
103
  try {
@@ -140,231 +128,230 @@ window.__ModuleLoader__.load({
140
128
  });
141
129
  const inCcc = status.root !== null;
142
130
  const runningHandymen = handymen.filter((l) => !l.done);
143
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
144
- type: "button",
145
- className: cx("sp-card", inCcc ? status.safeModeOn ? "sp-cardProtected" : "sp-cardUnprotected" : "sp-cardInactive"),
146
- "aria-haspopup": "dialog",
147
- "aria-expanded": open,
148
- onClick: () => setOpen(true),
149
- title: inCcc ? `Serenity v${status.accVersion} — ${status.root}(${status.safeModeOn ? "SAFE ON — 写工具已隐藏" : "SAFE OFF — 写工具全开,无保护"})` : "Serenity(未激活)",
150
- children: [
151
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: cx("sp-dot", inCcc ? void 0 : "sp-dotOff") }),
152
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
153
- className: cx("sp-brand"),
154
- children: "Serenity"
155
- }),
156
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
157
- className: cx("sp-ver"),
158
- children: ["v", status.accVersion]
159
- }),
160
- inCcc && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
161
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: cx("sp-sep") }),
131
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
132
+ className: cx("sp-root"),
133
+ ref: rootRef,
134
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
135
+ type: "button",
136
+ className: cx("sp-card", inCcc ? status.safeModeOn ? "sp-cardProtected" : "sp-cardUnprotected" : "sp-cardInactive"),
137
+ "aria-haspopup": "dialog",
138
+ "aria-expanded": open,
139
+ onClick: () => setOpen((v) => !v),
140
+ title: inCcc ? `Serenity v${status.accVersion} — ${status.root}(${status.safeModeOn ? "SAFE ON — 写工具已隐藏" : "SAFE OFF — 写工具全开,无保护"})` : "Serenity(未激活)",
141
+ children: [
142
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: cx("sp-dot", inCcc ? void 0 : "sp-dotOff") }),
162
143
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
163
- className: cx("sp-shield"),
164
- children: SHIELD_SVG
144
+ className: cx("sp-brand"),
145
+ children: "Serenity"
165
146
  }),
166
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
167
- className: cx("sp-statusWord"),
168
- children: "SAFE"
147
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
148
+ className: cx("sp-ver"),
149
+ children: ["v", status.accVersion]
169
150
  }),
170
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
171
- className: cx("sp-switch", status.safeModeOn ? "sp-switchOn" : "sp-switchOff", busy ? "sp-switchBusy" : void 0),
172
- role: "switch",
173
- "aria-checked": status.safeModeOn,
174
- title: status.safeModeOn ? "safe-mode ON(点击关闭)" : "safe-mode OFF(点击开启)",
175
- onClick: (e) => {
176
- e.stopPropagation();
177
- if (busy) return;
178
- toggle(!status.safeModeOn);
179
- },
180
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
181
- className: cx("sp-switchThumb"),
182
- children: status.safeModeOn ? SWITCH_ON_ICON : SWITCH_OFF_ICON
151
+ inCcc && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
152
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: cx("sp-sep") }),
153
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
154
+ className: cx("sp-shield"),
155
+ children: SHIELD_SVG
156
+ }),
157
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
158
+ className: cx("sp-statusWord"),
159
+ children: "SAFE"
160
+ }),
161
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
162
+ className: cx("sp-switch", status.safeModeOn ? "sp-switchOn" : "sp-switchOff", busy ? "sp-switchBusy" : void 0),
163
+ role: "switch",
164
+ "aria-checked": status.safeModeOn,
165
+ title: status.safeModeOn ? "safe-mode ON(点击关闭)" : "safe-mode OFF(点击开启)",
166
+ onClick: (e) => {
167
+ e.stopPropagation();
168
+ if (busy) return;
169
+ toggle(!status.safeModeOn);
170
+ },
171
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: cx("sp-switchThumb") })
183
172
  })
173
+ ] }),
174
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {
175
+ size: 12,
176
+ className: cx("sp-chev")
184
177
  })
185
- ] }),
186
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {
187
- size: 12,
188
- className: cx("sp-chev")
189
- })
190
- ]
191
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
192
- open,
193
- onClose: () => setOpen(false),
194
- title: "CCC 状态栏",
195
- closeLabel: "关闭",
196
- className: cx("sp-modal"),
197
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
198
- className: cx("sp-modalBody"),
199
- children: inCcc ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
200
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
201
- className: cx("sp-group"),
202
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
203
- className: cx("sp-groupTitle"),
204
- children: "运行环境"
205
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
206
- className: cx("sp-rows"),
178
+ ]
179
+ }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
180
+ className: cx("sp-pop"),
181
+ role: "dialog",
182
+ "aria-label": "CCC 状态栏",
183
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
184
+ className: cx("sp-popBody"),
185
+ children: inCcc ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
186
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
187
+ className: cx("sp-group"),
188
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
189
+ className: cx("sp-groupTitle"),
190
+ children: "运行环境"
191
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
192
+ className: cx("sp-rows"),
193
+ children: [
194
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
195
+ className: cx("sp-row"),
196
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
197
+ className: cx("sp-label"),
198
+ children: "CCC"
199
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
200
+ className: cx("sp-value"),
201
+ title: status.root ?? void 0,
202
+ children: status.root
203
+ })]
204
+ }),
205
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
206
+ className: cx("sp-row"),
207
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
208
+ className: cx("sp-label"),
209
+ children: "Handyman 模型"
210
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
211
+ className: cx("sp-value"),
212
+ title: status.handymanModel ? `handyman.models: ${status.handymanModel}` : void 0,
213
+ children: status.handymanModel ?? "未配置"
214
+ })]
215
+ }),
216
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
217
+ className: cx("sp-row"),
218
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
219
+ className: cx("sp-label"),
220
+ children: "守卫"
221
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
222
+ className: cx("sp-value"),
223
+ children: [
224
+ "blacklist ",
225
+ status.blacklist.length,
226
+ " 条",
227
+ status.threshold !== null ? ` · keeper 阈值 ${status.threshold}` : ""
228
+ ]
229
+ })]
230
+ })
231
+ ]
232
+ })]
233
+ }),
234
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
235
+ className: cx("sp-group"),
236
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
237
+ className: cx("sp-groupTitle"),
238
+ children: "安全模式"
239
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
240
+ className: cx("sp-actions"),
241
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
242
+ type: "button",
243
+ className: cx("sp-toggle", status.safeModeOn ? "sp-toggleOn" : "sp-toggleOff", "sp-toggleLg"),
244
+ disabled: busy,
245
+ onClick: () => void toggle(!status.safeModeOn),
246
+ children: [
247
+ status.safeModeOn && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutline16, {
248
+ size: 14,
249
+ className: cx("sp-toggleIcon")
250
+ }),
251
+ "safe-mode ",
252
+ status.safeModeOn ? "ON" : "OFF"
253
+ ]
254
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
255
+ className: cx("sp-toggleDesc"),
256
+ children: status.safeModeOn ? "写工具已隐藏,无人值守安全航行" : "写工具可用(点击开启)"
257
+ })]
258
+ })]
259
+ }),
260
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
261
+ className: cx("sp-group"),
207
262
  children: [
208
263
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
209
- className: cx("sp-row"),
210
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
211
- className: cx("sp-label"),
212
- children: "CCC"
213
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
214
- className: cx("sp-value"),
215
- title: status.root ?? void 0,
216
- children: status.root
264
+ className: cx("sp-groupHead"),
265
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
266
+ className: cx("sp-groupTitle"),
267
+ children: "Handyman 运行"
268
+ }), handymen.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
269
+ type: "button",
270
+ className: cx("sp-handymenToggle"),
271
+ onClick: () => setHandymenOpen((v) => !v),
272
+ children: handymenOpen ? "收起" : "详情"
217
273
  })]
218
274
  }),
219
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
220
- className: cx("sp-row"),
221
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
222
- className: cx("sp-label"),
223
- children: "Handyman 模型"
224
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
225
- className: cx("sp-value"),
226
- title: status.handymanModel ? `handyman.models: ${status.handymanModel}` : void 0,
227
- children: status.handymanModel ?? "未配置"
228
- })]
275
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
276
+ className: cx("sp-rows"),
277
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
278
+ className: cx("sp-row"),
279
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
280
+ className: cx("sp-label"),
281
+ children: "状态"
282
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
283
+ className: cx("sp-value"),
284
+ children: runningHandymen.length > 0 ? `运行中 ${runningHandymen.length}${handymen.length > 0 ? ` / 共 ${handymen.length}` : ""}` : "无运行中 handyman"
285
+ })]
286
+ })
229
287
  }),
230
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
231
- className: cx("sp-row"),
232
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
233
- className: cx("sp-label"),
234
- children: "守卫"
235
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
236
- className: cx("sp-value"),
288
+ handymenOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
289
+ className: cx("sp-handymenList"),
290
+ children: [handymen.slice(0, 3).map((l) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
291
+ className: cx("sp-handymanItem"),
292
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
293
+ className: cx("sp-handymanHead"),
294
+ children: [
295
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
296
+ className: cx("sp-handymanLabel"),
297
+ title: l.label,
298
+ children: l.label
299
+ }),
300
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
301
+ className: cx("sp-handymanRound"),
302
+ children: l.done ? "✓ 完成" : `R${l.round}`
303
+ }),
304
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
305
+ className: cx("sp-handymanTime"),
306
+ children: new Date(l.updated).toLocaleTimeString()
307
+ })
308
+ ]
309
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
310
+ className: cx("sp-handymanResp"),
311
+ title: l.lastResponse,
312
+ children: l.lastResponse.slice(0, 80) || (l.done ? "已完成" : "(等待首轮响应)")
313
+ })]
314
+ }, l.label)), handymen.length > 3 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
315
+ className: cx("sp-handymenMore"),
237
316
  children: [
238
- "blacklist ",
239
- status.blacklist.length,
240
- " ",
241
- status.threshold !== null ? ` · keeper 阈值 ${status.threshold}` : ""
317
+ " ",
318
+ handymen.length,
319
+ " 个 handyman(其余见等待界面)"
242
320
  ]
243
321
  })]
244
322
  })
245
323
  ]
246
- })]
247
- }),
248
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
324
+ }),
325
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
326
+ className: cx("sp-hint"),
327
+ children: [
328
+ "💡 开关/阈值/账号/工作区白名单请在 ",
329
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "设置 → Serenity" }),
330
+ " 中配置。"
331
+ ]
332
+ })
333
+ ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
249
334
  className: cx("sp-group"),
250
335
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
251
336
  className: cx("sp-groupTitle"),
252
- children: "安全模式"
253
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
254
- className: cx("sp-actions"),
255
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
256
- type: "button",
257
- className: cx("sp-toggle", status.safeModeOn ? "sp-toggleOn" : "sp-toggleOff", "sp-toggleLg"),
258
- disabled: busy,
259
- onClick: () => void toggle(!status.safeModeOn),
260
- children: [
261
- status.safeModeOn && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutline16, {
262
- size: 14,
263
- className: cx("sp-toggleIcon")
264
- }),
265
- "safe-mode ",
266
- status.safeModeOn ? "ON" : "OFF"
267
- ]
268
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
269
- className: cx("sp-toggleDesc"),
270
- children: status.safeModeOn ? "写工具已隐藏,无人值守安全航行" : "写工具可用(点击开启)"
271
- })]
272
- })]
273
- }),
274
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
275
- className: cx("sp-group"),
276
- children: [
277
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
278
- className: cx("sp-groupHead"),
279
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
280
- className: cx("sp-groupTitle"),
281
- children: "Handyman 运行"
282
- }), handymen.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
283
- type: "button",
284
- className: cx("sp-handymenToggle"),
285
- onClick: () => setHandymenOpen((v) => !v),
286
- children: handymenOpen ? "收起" : "详情"
287
- })]
288
- }),
289
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
290
- className: cx("sp-rows"),
291
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
292
- className: cx("sp-row"),
293
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
294
- className: cx("sp-label"),
295
- children: "状态"
296
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
297
- className: cx("sp-value"),
298
- children: runningHandymen.length > 0 ? `运行中 ${runningHandymen.length}${handymen.length > 0 ? ` / 共 ${handymen.length}` : ""}` : "无运行中 handyman"
299
- })]
300
- })
301
- }),
302
- handymenOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
303
- className: cx("sp-handymenList"),
304
- children: [handymen.slice(0, 3).map((l) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
305
- className: cx("sp-handymanItem"),
306
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
307
- className: cx("sp-handymanHead"),
308
- children: [
309
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
310
- className: cx("sp-handymanLabel"),
311
- title: l.label,
312
- children: l.label
313
- }),
314
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
315
- className: cx("sp-handymanRound"),
316
- children: l.done ? "✓ 完成" : `R${l.round}`
317
- }),
318
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
319
- className: cx("sp-handymanTime"),
320
- children: new Date(l.updated).toLocaleTimeString()
321
- })
322
- ]
323
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
324
- className: cx("sp-handymanResp"),
325
- title: l.lastResponse,
326
- children: l.lastResponse.slice(0, 80) || (l.done ? "已完成" : "(等待首轮响应)")
327
- })]
328
- }, l.label)), handymen.length > 3 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
329
- className: cx("sp-handymenMore"),
330
- children: [
331
- "… 共 ",
332
- handymen.length,
333
- " 个 handyman(其余见等待界面)"
334
- ]
337
+ children: "状态"
338
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
339
+ className: cx("sp-rows"),
340
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
341
+ className: cx("sp-row"),
342
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
343
+ className: cx("sp-label"),
344
+ children: "ACC"
345
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
346
+ className: cx("sp-value"),
347
+ children: "未激活(工作区不在 CCC 内)"
335
348
  })]
336
349
  })
337
- ]
338
- }),
339
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
340
- className: cx("sp-hint"),
341
- children: [
342
- "💡 开关/阈值/账号/工作区白名单请在 ",
343
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "设置 → Serenity" }),
344
- " 中配置。"
345
- ]
350
+ })]
346
351
  })
347
- ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
348
- className: cx("sp-group"),
349
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
350
- className: cx("sp-groupTitle"),
351
- children: "状态"
352
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
353
- className: cx("sp-rows"),
354
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
355
- className: cx("sp-row"),
356
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
357
- className: cx("sp-label"),
358
- children: "ACC"
359
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
360
- className: cx("sp-value"),
361
- children: "未激活(工作区不在 CCC 内)"
362
- })]
363
- })
364
- })]
365
352
  })
366
- })
367
- })] });
353
+ })]
354
+ });
368
355
  }
369
356
  //#endregion
370
357
  //#region src/client/image-fallback-api.ts
package/lib/index.js CHANGED
@@ -6,6 +6,7 @@ import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
6
6
  import { homedir, platform } from "node:os";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { promisify } from "node:util";
9
+ import { platform as platform$1 } from "node:process";
9
10
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
10
11
  import { createHmac, randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
11
12
  import { deriveEventMessage } from "@deepseek-ai/dsh-session";
@@ -136,12 +137,15 @@ function readBlacklist(root, paths = DEFAULT_SERENITY_CONFIG_PATHS) {
136
137
  }
137
138
  return out;
138
139
  }
139
- /** 匹配黑名单规则;命中返回条目,未命中返回 null。前缀匹配 / regex: 前缀(对齐 osp */
140
+ /** 匹配黑名单规则;命中返回条目,未命中返回 null。前缀匹配 / regex: 前缀(对齐 osp)。Windows:规则反斜杠归一化为正斜杠与 rel 对齐 */
140
141
  function matchBlacklist(relPath, rules) {
141
142
  for (const rule of rules) if (rule.pattern.startsWith("regex:")) try {
142
143
  if (new RegExp(rule.pattern.slice(6)).test(relPath)) return rule;
143
144
  } catch {}
144
- else if (relPath.startsWith(rule.pattern)) return rule;
145
+ else {
146
+ const pat = rule.pattern.split("\\").join("/");
147
+ if (relPath.startsWith(pat)) return rule;
148
+ }
145
149
  return null;
146
150
  }
147
151
  //#endregion
@@ -212,7 +216,8 @@ function validateWritePath(root, target) {
212
216
  const absPath = target.startsWith("/") ? resolve(target) : resolveInside(root, target);
213
217
  if (!pathInside(resolve(root), absPath)) throw new Error(`cc-fs: path "${target}" resolves to "${absPath}" which is outside serenity root "${root}"`);
214
218
  const rel = relative(root, absPath).split("\\").join("/");
215
- if (rel.endsWith("/mech-registry.json") && rel.includes("/.opencode/skills/")) throw new Error(`cc-fs: refusing to directly modify mech-registry.json — use acc_msm register/deregister instead`);
219
+ const relCi = process.platform === "win32" ? rel.toLowerCase() : rel;
220
+ if (relCi.endsWith("/mech-registry.json") && /(^|\/)(opencode|\.opencode)\/skills\//.test(relCi)) throw new Error(`cc-fs: refusing to directly modify mech-registry.json — use acc_msm register/deregister instead`);
216
221
  if (existsSync(absPath)) try {
217
222
  const real = realpathSync(absPath);
218
223
  if (!pathInside(resolve(root), real)) throw new Error(`cc-fs: path "${target}" resolves via symlink to "${real}" outside serenity root "${root}"`);
@@ -356,6 +361,8 @@ function runCcFs(root, args) {
356
361
  if (!args.src || !args.dst) throw new Error("mv requires src + dst");
357
362
  const srcAbs = validateWritePath(root, args.src);
358
363
  const dstAbs = validateWritePath(root, args.dst);
364
+ assertNotProtected(root, srcAbs, `mv src ${args.src}`);
365
+ assertNotProtected(root, dstAbs, `mv dst ${args.dst}`);
359
366
  if (!existsSync(srcAbs)) throw new Error(`mv: source not found: ${args.src}`);
360
367
  if (existsSync(dstAbs)) throw new Error(`mv: destination already exists: ${args.dst}`);
361
368
  const parentDir = dirname(dstAbs);
@@ -367,6 +374,8 @@ function runCcFs(root, args) {
367
374
  if (!args.src || !args.dst) throw new Error("cp requires src + dst");
368
375
  const srcAbs = validateWritePath(root, args.src);
369
376
  const dstAbs = validateWritePath(root, args.dst);
377
+ assertNotProtected(root, srcAbs, `cp src ${args.src}`);
378
+ assertNotProtected(root, dstAbs, `cp dst ${args.dst}`);
370
379
  if (!existsSync(srcAbs)) throw new Error(`cp: source not found: ${args.src}`);
371
380
  if (existsSync(dstAbs)) throw new Error(`cp: destination already exists: ${args.dst}`);
372
381
  if (statSync(srcAbs).isDirectory() && !(args.recursive ?? false)) throw new Error(`cp: source is a directory, use recursive to copy: ${args.src}`);
@@ -408,8 +417,8 @@ function runCcFs(root, args) {
408
417
  const revealPath = statSync(absPath).isDirectory() ? absPath : dirname(absPath);
409
418
  execFileSync("xdg-open", [revealPath], { timeout: 1e4 });
410
419
  } else if (os === "win32") {
411
- const winArgs = statSync(absPath).isDirectory() ? [absPath] : [`/select,${absPath}`];
412
- const child = spawn("explorer", winArgs, {
420
+ const winPath = statSync(absPath).isDirectory() ? absPath : /[\s,]/.test(absPath) ? dirname(absPath) : `/select,${absPath}`;
421
+ const child = spawn("explorer", [winPath], {
413
422
  detached: true,
414
423
  stdio: "ignore",
415
424
  windowsHide: false
@@ -453,7 +462,7 @@ function runCcFs(root, args) {
453
462
  if (hasGlobChars) {
454
463
  const regexStr = "^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".") + "$";
455
464
  try {
456
- return new RegExp(regexStr).test(name);
465
+ return new RegExp(regexStr, platform() === "win32" ? "i" : void 0).test(name);
457
466
  } catch {
458
467
  return name.includes(pattern);
459
468
  }
@@ -1496,6 +1505,27 @@ const gitTool = defineTool({
1496
1505
  */
1497
1506
  const execFileAsync = promisify(execFile);
1498
1507
  const MSM_TIMEOUT_MS = 6e5;
1508
+ const BUN_EXE_CANDIDATES = [
1509
+ () => join(process.env["USERPROFILE"] ?? process.env["HOME"] ?? "", ".bun", "bin", "bun.exe"),
1510
+ () => join(process.env["APPDATA"] ?? "", "npm", "node_modules", "bun", "bin", "bun.exe"),
1511
+ () => join(process.env["LOCALAPPDATA"] ?? "", "npm", "node_modules", "bun", "bin", "bun.exe")
1512
+ ];
1513
+ let bunExeCache;
1514
+ function bunExecutablePath() {
1515
+ if (bunExeCache !== void 0) return bunExeCache;
1516
+ if (platform$1 !== "win32") {
1517
+ bunExeCache = "bun";
1518
+ return bunExeCache;
1519
+ }
1520
+ for (const cand of BUN_EXE_CANDIDATES) try {
1521
+ if (existsSync(cand())) {
1522
+ bunExeCache = cand();
1523
+ return bunExeCache;
1524
+ }
1525
+ } catch {}
1526
+ bunExeCache = null;
1527
+ return null;
1528
+ }
1499
1529
  /** CCC 名:从 .serenity 首行解析(对齐 osp readSerenityCccName) */
1500
1530
  function readCccName(root) {
1501
1531
  try {
@@ -1709,7 +1739,7 @@ function runMsm(root, args) {
1709
1739
  const { entry, businessArgs, fmtJson, hasHelp, protocol } = prepareExec(root, args);
1710
1740
  const p = protocolResult(protocol);
1711
1741
  if (p !== void 0) return p;
1712
- let r = spawnSync("bun", [entry.path, ...businessArgs], {
1742
+ const baseOpts = {
1713
1743
  cwd: root,
1714
1744
  encoding: "utf-8",
1715
1745
  timeout: MSM_TIMEOUT_MS,
@@ -1719,21 +1749,23 @@ function runMsm(root, args) {
1719
1749
  "pipe"
1720
1750
  ],
1721
1751
  env: buildMsmEnv(root)
1722
- });
1752
+ };
1753
+ const bunBin = bunExecutablePath();
1754
+ let r = bunBin ? spawnSync(bunBin, [entry.path, ...businessArgs], baseOpts) : {
1755
+ error: { code: "ENOENT" },
1756
+ status: null,
1757
+ stdout: "",
1758
+ stderr: "",
1759
+ pid: 0,
1760
+ signal: null
1761
+ };
1723
1762
  if (r.error && isBunMissing(r.error)) r = spawnSync(NPX_BIN, [
1724
1763
  "tsx",
1725
1764
  entry.path,
1726
1765
  ...businessArgs
1727
1766
  ], {
1728
- cwd: root,
1729
- encoding: "utf-8",
1730
- timeout: MSM_TIMEOUT_MS,
1731
- stdio: [
1732
- "pipe",
1733
- "pipe",
1734
- "pipe"
1735
- ],
1736
- env: buildMsmEnv(root)
1767
+ ...baseOpts,
1768
+ shell: platform$1 === "win32"
1737
1769
  });
1738
1770
  return msmExecResult(entry.name, r.status ?? 2, r.stdout ?? "", r.stderr ?? "", fmtJson, hasHelp);
1739
1771
  }
@@ -1747,7 +1779,8 @@ function runMsm(root, args) {
1747
1779
  if (!existsSync(scriptAbs)) throw new Error(`MSM script not found: "${path}"`);
1748
1780
  if (loadMsmEntries(root).some((e) => e.name === name)) throw new Error(`MSM already registered: "${name}"`);
1749
1781
  const regPath = registryPathFor(root, skill);
1750
- const isV1Wrapped = existsSync(regPath) && !Array.isArray(JSON.parse(readFileSync(regPath, "utf-8")));
1782
+ const raw = existsSync(regPath) ? readFileSync(regPath, "utf-8").replace(/^\uFEFF/, "") : "";
1783
+ const isV1Wrapped = raw !== "" && !Array.isArray(JSON.parse(raw));
1751
1784
  const entries = existsSync(regPath) ? parseRegistry(readFileSync(regPath, "utf-8")) : [];
1752
1785
  let flags;
1753
1786
  if (args.flags) try {
@@ -1794,7 +1827,8 @@ function runMsm(root, args) {
1794
1827
  case "deregister": {
1795
1828
  const name = args.name ?? "";
1796
1829
  for (const regPath of findRegistries(root)) {
1797
- const isV1Wrapped = !Array.isArray(JSON.parse(readFileSync(regPath, "utf-8")));
1830
+ const dRaw = readFileSync(regPath, "utf-8").replace(/^\uFEFF/, "");
1831
+ const isV1Wrapped = !Array.isArray(JSON.parse(dRaw));
1798
1832
  const entries = parseRegistry(readFileSync(regPath, "utf-8"));
1799
1833
  const idx = entries.findIndex((e) => e.name === name);
1800
1834
  if (idx >= 0) {
@@ -1828,8 +1862,9 @@ function runMsm(root, args) {
1828
1862
  case "check": {
1829
1863
  const entries = loadMsmEntries(root);
1830
1864
  const issues = [];
1831
- const registeredPaths = new Set(entries.map((e) => e.path));
1832
- for (const scriptPath of scanSkillScripts(root)) if (!registeredPaths.has(scriptPath)) issues.push({
1865
+ const norm = (p) => p.split("\\").join("/").toLowerCase();
1866
+ const registeredPaths = new Set(entries.map((e) => norm(e.path)));
1867
+ for (const scriptPath of scanSkillScripts(root)) if (!registeredPaths.has(norm(scriptPath))) issues.push({
1833
1868
  name: scriptPath,
1834
1869
  check: "M3",
1835
1870
  detail: "script not registered in mech-registry"
@@ -1980,15 +2015,20 @@ async function runMsmAsync(root, args) {
1980
2015
  const { entry, businessArgs, fmtJson, hasHelp, protocol } = prepareExec(root, args);
1981
2016
  const p = protocolResult(protocol);
1982
2017
  if (p !== void 0) return p;
2018
+ const bunBin = bunExecutablePath();
2019
+ const baseOpts = {
2020
+ cwd: root,
2021
+ encoding: "utf-8",
2022
+ timeout: MSM_TIMEOUT_MS,
2023
+ maxBuffer: 67108864,
2024
+ env: buildMsmEnv(root)
2025
+ };
1983
2026
  try {
1984
- const r = await execFileAsync("bun", [entry.path, ...businessArgs], {
1985
- cwd: root,
1986
- encoding: "utf-8",
1987
- timeout: MSM_TIMEOUT_MS,
1988
- maxBuffer: 67108864,
1989
- env: buildMsmEnv(root)
1990
- });
1991
- return msmExecResult(entry.name, 0, r.stdout, r.stderr, fmtJson, hasHelp);
2027
+ if (bunBin) {
2028
+ const r = await execFileAsync(bunBin, [entry.path, ...businessArgs], baseOpts);
2029
+ return msmExecResult(entry.name, 0, r.stdout, r.stderr, fmtJson, hasHelp);
2030
+ }
2031
+ throw { code: "ENOENT" };
1992
2032
  } catch (e) {
1993
2033
  const err = e;
1994
2034
  if (isBunMissing(err)) try {
@@ -1997,11 +2037,8 @@ async function runMsmAsync(root, args) {
1997
2037
  entry.path,
1998
2038
  ...businessArgs
1999
2039
  ], {
2000
- cwd: root,
2001
- encoding: "utf-8",
2002
- timeout: MSM_TIMEOUT_MS,
2003
- maxBuffer: 67108864,
2004
- env: buildMsmEnv(root)
2040
+ ...baseOpts,
2041
+ shell: platform$1 === "win32"
2005
2042
  });
2006
2043
  return msmExecResult(entry.name, 0, r.stdout, r.stderr, fmtJson, hasHelp);
2007
2044
  } catch (e2) {
@@ -2337,9 +2374,9 @@ function handymanPresetInheritance(parentCtx) {
2337
2374
  * 进度文件(handyman-<label>.md/.json)、续跑、轮次 prompt 结构、stop token。
2338
2375
  * 不兼容旧 loop- 进度文件(用户拍板:仅新 handyman- 前缀)。
2339
2376
  */
2340
- /** label 脱敏(Windows 审计问题 17):非法字符 → '-',去尾点/空格,限长 */
2377
+ /** label 脱敏(Windows 审计问题 17):非法字符 → '-',去尾点/空格,限长(按码点截断,修复代理对切散 U+FFFD) */
2341
2378
  function sanitizeLabel(label) {
2342
- return label.replace(/[<>:"/\\|?*\u0000-\u001f]/g, "-").replace(/[ .]+$/g, "").slice(0, 50);
2379
+ return [...label.replace(/[<>:"/\\|?*\u0000-\u001f]/g, "-").replace(/[ .]+$/g, "")].slice(0, 50).join("");
2343
2380
  }
2344
2381
  function handymanProgressPaths(root, label) {
2345
2382
  const dir = join(root, "AGENT_SESSIONS");
@@ -5190,26 +5227,38 @@ function closeSession(root, key, confirm, scope = DEFAULT_SESSION_SCOPE) {
5190
5227
  return `Session "${session.dirName}" closed and marked as completed.`;
5191
5228
  }
5192
5229
  /**
5193
- * 从会话历史(events)解析最后一条 [SESSION CONTEXT] 标记(进程重启恢复;
5194
- * 只扫**当前会话**自己的历史——无跨会话串台)。
5230
+ * 从会话历史(events)解析会话身份(进程重启恢复;只扫**当前会话**自己的历史——无跨会话串台)。
5231
+ *
5232
+ * v1.24.11 稳固化(S142 用户需求:重建后新会话必须准确知道从哪个 SESSION 恢复):
5233
+ * **路径规范行即可,不再要求 [SESSION CONTEXT] 标记**——`session use` 上下文与重建锚点
5234
+ * (buildRebuildAnchor 的 `- Persistent trajectory — SESSION.md path: <rel>` 行)同格式,
5235
+ * 因此**仅靠重建锚点的会话(从未显式 use)同样可恢复**。从**尾到头**扫描,最后一条
5236
+ * 合法路径胜出(时间序最新);会话目录名/ID 从路径本身派生(单一真相源,不猜)。
5237
+ * 路径可为相对(重建锚点存 rel)——绝对化与存在性校验在调用方(知道 root)执行。
5195
5238
  */
5196
5239
  function parseSessionContextFromEvents(events) {
5197
5240
  for (let i = events.length - 1; i >= 0; i--) {
5198
5241
  const strs = [];
5199
5242
  collectStrings(events[i], strs);
5200
5243
  for (const s of strs) {
5201
- const idx = s.indexOf(SESSION_CONTEXT_MARKER);
5202
- if (idx < 0) continue;
5203
- const dirName = s.slice(idx + 28).trim().split("\n")[0]?.trim() ?? "";
5204
- const mdMatch = s.match(/SESSION\.md path:\s*([^\r\n]+)/);
5205
- if (/^\d{4}-\d{2}-\d{2}--/.test(dirName) && mdMatch) {
5206
- const idMatch = dirName.match(/--S(\d{3,})--/);
5207
- return {
5208
- sessionId: idMatch ? `S${idMatch[1]}` : basename(dirName),
5209
- dirName,
5210
- mdPath: mdMatch[1].trim()
5211
- };
5212
- }
5244
+ const md = extractSessionMdPathFromText(s);
5245
+ if (!md) continue;
5246
+ let dirName;
5247
+ let filePath;
5248
+ if (basename(md) === SESSION_MD) {
5249
+ dirName = basename(dirname(md));
5250
+ filePath = md;
5251
+ } else if (isSessionDirName(basename(md))) {
5252
+ dirName = basename(md);
5253
+ filePath = join(md, SESSION_MD);
5254
+ } else continue;
5255
+ if (!isSessionDirName(dirName)) continue;
5256
+ const idMatch = dirName.match(/--S(\d{3,})--/);
5257
+ return {
5258
+ sessionId: idMatch ? `S${idMatch[1]}` : dirName,
5259
+ dirName,
5260
+ mdPath: filePath
5261
+ };
5213
5262
  }
5214
5263
  }
5215
5264
  return null;
@@ -5222,6 +5271,35 @@ function collectStrings(v, out) {
5222
5271
  }
5223
5272
  if (v && typeof v === "object") for (const val of Object.values(v)) collectStrings(val, out);
5224
5273
  }
5274
+ /** 规范行正则:`SESSION.md path: <路径>`(路径可含空格 → [^\r\n]+ 整行匹配) */
5275
+ const SESSION_MD_PATH_RE = /SESSION\.md path:\s*([^\r\n]+)/;
5276
+ /** 从文本提取 SESSION.md 路径(use 上下文 / 重建锚点规范行通用;无匹配返回 null)。
5277
+ * 同行已知尾注(如系统提示词 Session 块的 persistent-body 注释)剥除——规范行只取路径本体。 */
5278
+ function extractSessionMdPathFromText(text) {
5279
+ const m = text.match(SESSION_MD_PATH_RE);
5280
+ if (!m) return null;
5281
+ let p = m[1].trim();
5282
+ const suffix = p.search(/ \(the trajectory's persistent body/);
5283
+ if (suffix > 0) p = p.slice(0, suffix).trim();
5284
+ return p;
5285
+ }
5286
+ /** 会话目录名形态校验(createSession 恒带日期前缀 `YYYY-MM-DD--`) */
5287
+ function isSessionDirName(dirName) {
5288
+ return /^\d{4}-\d{2}-\d{2}--/.test(dirName) && dirName.length > 11;
5289
+ }
5290
+ /**
5291
+ * 约定回退(v1.24.11):AGENT_SESSIONS 下最新修改的**未完成**会话的 SESSION.md。
5292
+ * readAllSessions 已按「未完成优先 + mtime 降序」排序 → 首个未完成且含 SESSION.md 即最新活动。
5293
+ * 只作最后手段(内存/events/锚点全缺时),保证重建锚点至少指向一个真实存在的轨迹。
5294
+ */
5295
+ function findLatestActiveSessionMd(root) {
5296
+ for (const s of readAllSessions(sessionsRoot(root))) {
5297
+ if (s.status.completed) continue;
5298
+ const md = join(s.path, SESSION_MD);
5299
+ if (existsSync(md)) return md;
5300
+ }
5301
+ return null;
5302
+ }
5225
5303
  /** health 子命令(对齐 osp healthCheck:stale/stalled/ghost/drift 四类检查,文本输出) */
5226
5304
  function healthCheck(root) {
5227
5305
  const sessions = readAllSessions(sessionsRoot(root));
@@ -6091,10 +6169,53 @@ function buildRebuildAnchor(root, sessionName, activeMdPath, anchorMessages = DE
6091
6169
  ...anchorMessages.flatMap((text) => [stripAckSuffix(text), ""]),
6092
6170
  sessionName !== "" ? `Continue the work of ${sessionName}.` : "Continue the current work.",
6093
6171
  ...sessionLine ? [sessionLine] : [],
6094
- `- Persistent trajectory (SESSION.md, unmoved): ${rel}`,
6172
+ `- Persistent trajectory SESSION.md path: ${rel}`,
6173
+ ` (the trajectory's persistent body — stays in place through rebuilds)`,
6095
6174
  `- Read that SESSION.md first (goal/decisions/progress/unresolved), then continue from the last checkpoint.`
6096
6175
  ].join("\n");
6097
6176
  }
6177
+ /** 从会话 surface 首条 user 消息(重建锚点)解析 SESSION.md 路径(events 异常/缺失时的兜底) */
6178
+ function parseAnchorMdPath(session) {
6179
+ try {
6180
+ const nodes = [...session.surface.nodes];
6181
+ if (nodes.length === 0) return null;
6182
+ const events = session.events;
6183
+ if (!Array.isArray(events)) return null;
6184
+ const event = events[nodes[0]];
6185
+ if (!event) return null;
6186
+ const message = deriveEventMessage(event);
6187
+ if (!message) return null;
6188
+ return extractSessionMdPathFromText(message.content?.map((c) => c.type === "text" ? c.text ?? "" : "").join("") ?? "");
6189
+ } catch {
6190
+ return null;
6191
+ }
6192
+ }
6193
+ /** 从 SESSION.md 绝对路径派生会话名(S###;issue 目录回退完整目录名) */
6194
+ function sessionNameFromMdPath(mdPath) {
6195
+ const dirName = basename(dirname(mdPath));
6196
+ const idMatch = dirName.match(/--S(\d{3,})--/);
6197
+ return idMatch ? `S${idMatch[1]}` : dirName;
6198
+ }
6199
+ /**
6200
+ * 稳固的 SESSION.md 定位(多层候选 + 存在性校验,**绝不输出虚假路径**):
6201
+ * ① 内存活跃会话(本会话显式 use 过)→ ② events 恢复(use 标记 + 重建锚点规范行,进程重启后)
6202
+ * → ③ surface 首条锚点(events 异常时兜底)→ ④ AGENT_SESSIONS 约定回退(最新未完成活动目录)。
6203
+ * 每候选 resolve 后 existsSync 校验(相对路径按 root 解析);全部失败返回 null → 调用方报错引导。
6204
+ */
6205
+ function resolveSessionMdPath(root, scope, session) {
6206
+ const candidates = [];
6207
+ candidates.push(getActiveSessionInfo(scope)?.mdPath ?? null);
6208
+ const events = session.events;
6209
+ if (Array.isArray(events)) candidates.push(parseSessionContextFromEvents(events)?.mdPath ?? null);
6210
+ candidates.push(parseAnchorMdPath(session));
6211
+ candidates.push(findLatestActiveSessionMd(root));
6212
+ for (const c of candidates) {
6213
+ if (!c) continue;
6214
+ const abs = c.startsWith(root) ? c : resolve(root, c);
6215
+ if (existsSync(abs)) return abs;
6216
+ }
6217
+ return null;
6218
+ }
6098
6219
  const pendingRebuilds = /* @__PURE__ */ new Map();
6099
6220
  /** 陈旧队列存活时长(毫秒):超过则丢弃(turn 异常结束/agent 崩溃时防残留误清空) */
6100
6221
  const PENDING_TTL_MS = 6e5;
@@ -6108,19 +6229,11 @@ const PENDING_TTL_MS = 6e5;
6108
6229
  async function queueRebuild(ctx, opts) {
6109
6230
  if (!readSimpleSettings().rebuildEnabled) throw new Error("session_rebuild is disabled (rebuild.enabled=false — enable it in the dsh settings panel)");
6110
6231
  const { root, note, dshSessionId } = opts;
6111
- if (!ctx.sessions?.get?.(dshSessionId)) throw new Error(`Unable to locate dsh session ${dshSessionId} (session may be closed)`);
6112
- const scope = dshSessionId;
6113
- let sessionMdPath = null;
6114
- let sessionName = "";
6115
- try {
6116
- const active = getActiveSessionInfo(scope);
6117
- if (active?.mdPath) {
6118
- sessionMdPath = active.mdPath;
6119
- sessionName = active.sessionId ?? "";
6120
- }
6121
- } catch {}
6122
- const mdPath = sessionMdPath ?? join(root, "AGENT_SESSIONS", "SESSION.md");
6123
- const anchor = buildRebuildAnchor(root, sessionName, mdPath);
6232
+ const session = ctx.sessions?.get?.(dshSessionId);
6233
+ if (!session) throw new Error(`Unable to locate dsh session ${dshSessionId} (session may be closed)`);
6234
+ const mdPath = resolveSessionMdPath(root, dshSessionId, session);
6235
+ if (!mdPath) throw new Error("Unable to determine the active SESSION.md — no session context found in this conversation. Run \"session use <S###>\" first to activate the trajectory to resume, then retry session_rebuild.");
6236
+ const anchor = buildRebuildAnchor(root, getActiveSessionInfo(dshSessionId)?.sessionId ?? sessionNameFromMdPath(mdPath), mdPath);
6124
6237
  pendingRebuilds.set(dshSessionId, {
6125
6238
  anchor,
6126
6239
  queuedAt: Date.now()
@@ -6128,7 +6241,7 @@ async function queueRebuild(ctx, opts) {
6128
6241
  return {
6129
6242
  queued: true,
6130
6243
  anchor,
6131
- sessionMdPath
6244
+ sessionMdPath: mdPath
6132
6245
  };
6133
6246
  }
6134
6247
  /**
@@ -6521,8 +6634,15 @@ function scanSerenityDirs(skillsRoot) {
6521
6634
  if (!existsSync(skillsRoot)) return [];
6522
6635
  return readdirSync(skillsRoot).filter((name) => isSerenityEntry(name)).map((name) => join(skillsRoot, name)).filter((dir) => statSync(dir).isDirectory() && existsSync(join(dir, "SKILL.md"))).sort();
6523
6636
  }
6637
+ /** 防穿越:skill 名必须是简单目录名(拒绝分隔符与 `..`/`.` 路径段) */
6638
+ function isSafeSkillName(name) {
6639
+ if (name.length === 0) return false;
6640
+ if (name.includes("/") || name.includes("\\")) return false;
6641
+ return !name.split(/[/\\]/).some((seg) => seg === ".." || seg === ".");
6642
+ }
6524
6643
  /** 按名在两个 skills 根下定位 SKILL.md(.dsh/skills 优先,其次 .opencode/skills) */
6525
6644
  function findSkillMd(root, name) {
6645
+ if (!isSafeSkillName(name)) return null;
6526
6646
  for (const base of [".dsh", ".opencode"]) {
6527
6647
  const p = resolve(root, base, "skills", name, "SKILL.md");
6528
6648
  if (existsSync(p)) return p;
@@ -6887,8 +7007,8 @@ function readPersonaSettings() {
6887
7007
  };
6888
7008
  }
6889
7009
  }
6890
- /** 过滤掉对 agent 隐藏的内容(safe-mode 是用户能力,不对 agent 提及) */
6891
- const HIDDEN_LINES = /安全模式|safe-mode|\.serenity-safe-on/g;
7010
+ /** 过滤掉对 agent 隐藏的内容(safe-mode 是用户能力,不对 agent 提及)。无 g flag:`.test()` 须逐行独立无状态,避免 lastIndex 游移漏滤(问题A) */
7011
+ const HIDDEN_LINES = /安全模式|safe-mode|\.serenity-safe-on/;
6892
7012
  function sanitizeSkillContent(content) {
6893
7013
  return content.split("\n").filter((line) => !HIDDEN_LINES.test(line)).join("\n");
6894
7014
  }
@@ -7451,8 +7571,14 @@ function registerContext(ctx, opts = {}) {
7451
7571
  if (loadSerenityConfig(root, configPaths).hooks?.autoRestoreSession ?? true) {
7452
7572
  const info = parseSessionContextFromEvents(agent.session.events ?? []);
7453
7573
  if (info) {
7454
- setActiveSessionInfo(scope, info);
7455
- console.log(`[serenity-hooks] ↻ 从历史恢复激活会话: ${info.dirName}`);
7574
+ const abs = info.mdPath.startsWith(root) ? info.mdPath : resolve(root, info.mdPath);
7575
+ if (existsSync(abs)) {
7576
+ setActiveSessionInfo(scope, {
7577
+ ...info,
7578
+ mdPath: abs
7579
+ });
7580
+ console.log(`[serenity-hooks] ↻ 从历史恢复激活会话: ${info.dirName}`);
7581
+ }
7456
7582
  }
7457
7583
  }
7458
7584
  } catch {}
@@ -7876,9 +8002,15 @@ function listOpencodeSkillDirs(root) {
7876
8002
  if (!existsSync(dir)) return [];
7877
8003
  return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => join(dir, e.name)).filter((d) => existsSync(join(d, "SKILL.md")));
7878
8004
  }
7879
- /** 加载一个 opencode skill(frontmatter 解析 + 正文) */
8005
+ /** 加载一个 opencode skill(frontmatter 解析 + 正文);读失败(EPERM/占用等)返回 null,单目录容错 */
7880
8006
  function loadOpencodeSkill(skillDir) {
7881
- const { meta, content } = parseFrontmatter(readFileSync(join(skillDir, "SKILL.md"), "utf-8"));
8007
+ let raw;
8008
+ try {
8009
+ raw = readFileSync(join(skillDir, "SKILL.md"), "utf-8");
8010
+ } catch {
8011
+ return null;
8012
+ }
8013
+ const { meta, content } = parseFrontmatter(raw);
7882
8014
  return {
7883
8015
  name: meta.name,
7884
8016
  dir: skillDir,
@@ -7902,7 +8034,7 @@ function registerOpencodeSkills(ctx) {
7902
8034
  const candidates = [];
7903
8035
  for (const dir of listOpencodeSkillDirs(root)) {
7904
8036
  const skill = loadOpencodeSkill(dir);
7905
- if (!skill.meta.name) continue;
8037
+ if (!skill || !skill.meta.name) continue;
7906
8038
  candidates.push({
7907
8039
  name: skill.meta.name,
7908
8040
  description: skill.meta.description || "(opencode skill)",
@@ -7930,6 +8062,7 @@ function registerOpencodeSkills(ctx) {
7930
8062
  async get(candidate) {
7931
8063
  const dir = candidate.locator;
7932
8064
  const skill = loadOpencodeSkill(dir);
8065
+ if (!skill) return void 0;
7933
8066
  return {
7934
8067
  name: skill.meta.name,
7935
8068
  description: skill.meta.description || candidate.description,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.24.9",
3
+ "version": "1.24.11",
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": {