@shgroup/dsh-serenity-hooks 1.24.8 → 1.24.10

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.8",
3
+ "version": "1.24.10",
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");
@@ -6521,8 +6558,15 @@ function scanSerenityDirs(skillsRoot) {
6521
6558
  if (!existsSync(skillsRoot)) return [];
6522
6559
  return readdirSync(skillsRoot).filter((name) => isSerenityEntry(name)).map((name) => join(skillsRoot, name)).filter((dir) => statSync(dir).isDirectory() && existsSync(join(dir, "SKILL.md"))).sort();
6523
6560
  }
6561
+ /** 防穿越:skill 名必须是简单目录名(拒绝分隔符与 `..`/`.` 路径段) */
6562
+ function isSafeSkillName(name) {
6563
+ if (name.length === 0) return false;
6564
+ if (name.includes("/") || name.includes("\\")) return false;
6565
+ return !name.split(/[/\\]/).some((seg) => seg === ".." || seg === ".");
6566
+ }
6524
6567
  /** 按名在两个 skills 根下定位 SKILL.md(.dsh/skills 优先,其次 .opencode/skills) */
6525
6568
  function findSkillMd(root, name) {
6569
+ if (!isSafeSkillName(name)) return null;
6526
6570
  for (const base of [".dsh", ".opencode"]) {
6527
6571
  const p = resolve(root, base, "skills", name, "SKILL.md");
6528
6572
  if (existsSync(p)) return p;
@@ -6887,8 +6931,8 @@ function readPersonaSettings() {
6887
6931
  };
6888
6932
  }
6889
6933
  }
6890
- /** 过滤掉对 agent 隐藏的内容(safe-mode 是用户能力,不对 agent 提及) */
6891
- const HIDDEN_LINES = /安全模式|safe-mode|\.serenity-safe-on/g;
6934
+ /** 过滤掉对 agent 隐藏的内容(safe-mode 是用户能力,不对 agent 提及)。无 g flag:`.test()` 须逐行独立无状态,避免 lastIndex 游移漏滤(问题A) */
6935
+ const HIDDEN_LINES = /安全模式|safe-mode|\.serenity-safe-on/;
6892
6936
  function sanitizeSkillContent(content) {
6893
6937
  return content.split("\n").filter((line) => !HIDDEN_LINES.test(line)).join("\n");
6894
6938
  }
@@ -7876,9 +7920,15 @@ function listOpencodeSkillDirs(root) {
7876
7920
  if (!existsSync(dir)) return [];
7877
7921
  return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => join(dir, e.name)).filter((d) => existsSync(join(d, "SKILL.md")));
7878
7922
  }
7879
- /** 加载一个 opencode skill(frontmatter 解析 + 正文) */
7923
+ /** 加载一个 opencode skill(frontmatter 解析 + 正文);读失败(EPERM/占用等)返回 null,单目录容错 */
7880
7924
  function loadOpencodeSkill(skillDir) {
7881
- const { meta, content } = parseFrontmatter(readFileSync(join(skillDir, "SKILL.md"), "utf-8"));
7925
+ let raw;
7926
+ try {
7927
+ raw = readFileSync(join(skillDir, "SKILL.md"), "utf-8");
7928
+ } catch {
7929
+ return null;
7930
+ }
7931
+ const { meta, content } = parseFrontmatter(raw);
7882
7932
  return {
7883
7933
  name: meta.name,
7884
7934
  dir: skillDir,
@@ -7902,7 +7952,7 @@ function registerOpencodeSkills(ctx) {
7902
7952
  const candidates = [];
7903
7953
  for (const dir of listOpencodeSkillDirs(root)) {
7904
7954
  const skill = loadOpencodeSkill(dir);
7905
- if (!skill.meta.name) continue;
7955
+ if (!skill || !skill.meta.name) continue;
7906
7956
  candidates.push({
7907
7957
  name: skill.meta.name,
7908
7958
  description: skill.meta.description || "(opencode skill)",
@@ -7930,6 +7980,7 @@ function registerOpencodeSkills(ctx) {
7930
7980
  async get(candidate) {
7931
7981
  const dir = candidate.locator;
7932
7982
  const skill = loadOpencodeSkill(dir);
7983
+ if (!skill) return void 0;
7933
7984
  return {
7934
7985
  name: skill.meta.name,
7935
7986
  description: skill.meta.description || candidate.description,
@@ -8018,9 +8069,45 @@ function accountLockRemaining(user) {
8018
8069
  const st = getFailState(user);
8019
8070
  return st.lockedUntil > 0 && Date.now() < st.lockedUntil ? st.lockedUntil - Date.now() : 0;
8020
8071
  }
8021
- /** 生成 CSRF cookie 值(随机 32B hex) */
8072
+ /** CSRF token 有效期(毫秒):10 分钟窗口(扫码/多标签场景留足时间) */
8073
+ const CSRF_TTL_MS = 6e5;
8074
+ /**
8075
+ * 服务端 CSRF token 集合(v1.24.9 修复:多标签/刷新竞争导致登录死循环)。
8076
+ * 背景:每次 GET 登录页都生成新 token 覆盖 cookie——多标签页场景下,早先打开的
8077
+ * 标签携带旧 form token 提交时与最新 cookie 不匹配 → "会话校验失败"永远失败
8078
+ * (S142 用户实测:cookieCsrf=present formCsrf=present 但不匹配)。
8079
+ * 修复:每次 GET 生成的 token 存入集合(TTL 10min),提交时 form/cookie token
8080
+ * 只要 ∈ 集合即有效——多标签各自 token 都接受。
8081
+ */
8082
+ const csrfTokens = /* @__PURE__ */ new Map();
8083
+ /** 生成 CSRF token(随机 32B hex)并入集合 */
8022
8084
  function newCsrfToken() {
8023
- return randomBytes(32).toString("hex");
8085
+ const token = randomBytes(32).toString("hex");
8086
+ csrfTokens.set(token, Date.now() + CSRF_TTL_MS);
8087
+ if (csrfTokens.size > 50) {
8088
+ const now = Date.now();
8089
+ for (const [t, expiry] of csrfTokens) if (expiry < now) csrfTokens.delete(t);
8090
+ if (csrfTokens.size > 50) {
8091
+ let oldest = null;
8092
+ let oldestExpiry = Infinity;
8093
+ for (const [t, expiry] of csrfTokens) if (expiry < oldestExpiry) {
8094
+ oldest = t;
8095
+ oldestExpiry = expiry;
8096
+ }
8097
+ if (oldest !== null) csrfTokens.delete(oldest);
8098
+ }
8099
+ }
8100
+ return token;
8101
+ }
8102
+ /** token 是否有效(在集合且未过期;过期自动清理) */
8103
+ function isCsrfValid(token) {
8104
+ const expiry = csrfTokens.get(token);
8105
+ if (expiry === void 0) return false;
8106
+ if (expiry < Date.now()) {
8107
+ csrfTokens.delete(token);
8108
+ return false;
8109
+ }
8110
+ return true;
8024
8111
  }
8025
8112
  /** 从请求头取 CSRF token(X-CSRF-Token 或表单字段) */
8026
8113
  function csrfFromRequest(req, body) {
@@ -8029,12 +8116,6 @@ function csrfFromRequest(req, body) {
8029
8116
  const form = body.get("csrf");
8030
8117
  return form && form !== "" ? form : null;
8031
8118
  }
8032
- /** 常量时间比较(token 校验用) */
8033
- function safeEqual(a, b) {
8034
- const ab = Buffer.from(a);
8035
- const bb = Buffer.from(b);
8036
- return ab.length === bb.length && timingSafeEqual(ab, bb);
8037
- }
8038
8119
  /**
8039
8120
  * Origin 校验(S3):跨站 POST 被浏览器强制带 Origin(同源 GET/导航除外)。
8040
8121
  * 允许:① 同源(Origin.host === 网关自身 host)② 主端口 loopback(127.0.0.1:mainPort)。
@@ -8456,20 +8537,26 @@ function startGateway(config, getAccounts) {
8456
8537
  const remote = req.socket.remoteAddress ?? "unknown";
8457
8538
  const cookieCsrf = cookieValue(req.headers.cookie, "serenity_csrf");
8458
8539
  const formCsrf = csrfFromRequest(req, params);
8459
- if (cookieCsrf === void 0 || formCsrf === null || !safeEqual(cookieCsrf, formCsrf)) {
8540
+ if (cookieCsrf === void 0 || formCsrf === null || !isCsrfValid(formCsrf) || !isCsrfValid(cookieCsrf)) {
8460
8541
  console.warn(`[serenity-hooks] 登录拒绝(CSRF 校验失败):user=${user} ip=${remote} cookieSecure=${cookieSecure ? "on" : "off"} cookieCsrf=${cookieCsrf === void 0 ? "missing" : "present"} formCsrf=${formCsrf === null ? "missing" : "present"}`);
8461
- res.writeHead(403, { "content-type": "text/html; charset=utf-8" });
8462
- res.end(loginPageHtml("会话校验失败,请刷新页面重试" + (cookieSecure ? "(若仍失败:设置面板关闭 Secure Cookie——明文 HTTP 下必须关闭)" : "")));
8542
+ const retryCsrf = newCsrfToken();
8543
+ res.writeHead(403, {
8544
+ "content-type": "text/html; charset=utf-8",
8545
+ "set-cookie": `serenity_csrf=${retryCsrf}; HttpOnly; SameSite=Strict; Path=/`
8546
+ });
8547
+ res.end(loginPageHtml("会话校验失败,请刷新页面重试" + (cookieSecure ? "(若仍失败:设置面板关闭 Secure Cookie——明文 HTTP 下必须关闭)" : ""), retryCsrf));
8463
8548
  return;
8464
8549
  }
8465
8550
  const lockedRemaining = accountLockRemaining(user);
8466
8551
  if (lockedRemaining > 0) {
8467
8552
  console.warn(`[serenity-hooks] 登录拒绝(账号锁定中):user=${user} ip=${remote} 剩余=${Math.ceil(lockedRemaining / 6e4)}min`);
8553
+ const retryCsrf = newCsrfToken();
8468
8554
  res.writeHead(429, {
8469
8555
  "content-type": "text/html; charset=utf-8",
8470
- "retry-after": String(Math.ceil(lockedRemaining / 1e3))
8556
+ "retry-after": String(Math.ceil(lockedRemaining / 1e3)),
8557
+ "set-cookie": `serenity_csrf=${retryCsrf}; HttpOnly; SameSite=Strict; Path=/`
8471
8558
  });
8472
- res.end(loginPageHtml(`账号已锁定,请 ${Math.ceil(lockedRemaining / 6e4)} 分钟后再试`));
8559
+ res.end(loginPageHtml(`账号已锁定,请 ${Math.ceil(lockedRemaining / 6e4)} 分钟后再试`, retryCsrf));
8473
8560
  return;
8474
8561
  }
8475
8562
  const account = getAccounts().find((a) => a.user === user);
@@ -8495,8 +8582,12 @@ function startGateway(config, getAccounts) {
8495
8582
  const lockMs = recordLoginFailure(user);
8496
8583
  console.warn(`[serenity-hooks] 登录失败: user=${user} ip=${remote}${lockMs > 0 ? ` 锁定=${Math.ceil(lockMs / 6e4)}min` : ""}`);
8497
8584
  const msg = lockMs > 0 ? `尝试过多,账号已锁定 ${Math.ceil(lockMs / 6e4)} 分钟` : "用户名、密码或验证码错误";
8498
- res.writeHead(401, { "content-type": "text/html; charset=utf-8" });
8499
- res.end(loginPageHtml(msg));
8585
+ const retryCsrf = newCsrfToken();
8586
+ res.writeHead(401, {
8587
+ "content-type": "text/html; charset=utf-8",
8588
+ "set-cookie": `serenity_csrf=${retryCsrf}; HttpOnly; SameSite=Strict; Path=/`
8589
+ });
8590
+ res.end(loginPageHtml(msg, retryCsrf));
8500
8591
  });
8501
8592
  return;
8502
8593
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.24.8",
3
+ "version": "1.24.10",
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": {