@shgroup/dsh-serenity-hooks 1.35.0 → 1.36.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-serenity-hooks",
3
- "version": "1.35.0",
3
+ "version": "1.36.1",
4
4
  "main": "lib/index.js",
5
5
  "description": "宁静号 ACC harness(Native Cordis 插件):给 DSH 装一个「AI 工作区」——11 个工具(container_fs/container_trajectory/dashboard/container_git/msm/praxis/handyman/localstore/container_admin/im-bridge/acc-diag;后两个按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫)+ 轨迹日志与原地重建 + 网页登录入口/微信桥/子角色/对外问答页/trajectory 唤醒注册表。适配 DSH 0.1.5-rc.2(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.24.10 胶囊改版,对齐 OcgoDockEntry pill)\n * 胶囊 999px 全圆 → 点击 → 自绘 popover(340px 右上角卡片,外点/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);\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-overlay);\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);\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);\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 全圆 → 点击 → 自绘 popover(340px 右上角卡片,外点/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);\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/* ── 自绘 popover(卡片,替代官方 Modal)\n v1.36.1(所有者:「卡片做成双倍宽,左侧是信息,右侧是动画」):340 → **680px**(双倍)。\n 胶囊在头部右端,卡片 `right:0` 对齐 ⇒ 加宽只会**向左**长;再叠 `min(…, 100vw-24px)` 兜住窄视口。 */\n.sp-pop {\n position: absolute;\n top: calc(100% + 8px);\n right: 0;\n width: min(680px, 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-overlay);\n box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);\n}\n\n/* ── 航行动画栏:宁静号心智模型(ACC 层资产 /serenity/voyage,S142 §12.44 → v1.36.1 改右栏)\n v1.36.1 改版(所有者:「卡片做成双倍宽,左侧是信息,右侧是动画」):\n 动画从**背景层**改为**右栏**。这一改动解开了 v1.36.0 的取舍死结——\n 旧布局里动画垫在正文底下,必须靠 0.75 的卡片底色把它压住才读得清字;\n 分段之后两者不再争同一片像素 ⇒ 动画可以亮、正文可以干净,不必再互相妥协。\n ⚠️ 动画本身仍是「卡片模式」(词元压到 22%、铭牌层关闭),本栏只负责**给位置**,不改作品。 */\n.sp-popVoyage {\n overflow: hidden; /* 裁掉 iframe 在 14px 圆角外的溢出 */\n /* 分段后不再需要 0.75 的重压暗:压到 0.9 只压整体观感,不与正文争对比度。\n 若所有者觉得动画太亮/太暗,这仍是主旋钮(次旋钮 = iframe 的 blur)。 */\n background: rgba(6, 11, 14, 0.9);\n display: flex;\n flex-direction: row;\n /* 两栏都从**同一行高**分配(row 轴 cross = 高度):左栏按内容、右栏吃满行高。\n 行高 = 左栏内容高度,且被 max-height 封顶 ⇒ 信息长时内部滚动,动画不把行撑破。 */\n align-items: stretch;\n gap: 12px;\n padding: 12px;\n max-height: calc(100vh - 120px);\n}\n/* 左栏:信息(正文)——固定 340px,与旧卡片同宽,保证原有排印不变 */\n.sp-popVoyage > .sp-popBody {\n position: relative;\n z-index: 1;\n order: 1;\n flex: none;\n width: 340px;\n min-width: 0;\n}\n/* 右栏:动画——吃掉剩余宽度,并吃满左栏高度 */\n.sp-voyage {\n order: 2;\n flex: 1 1 auto;\n min-width: 0;\n position: relative;\n pointer-events: none; /* 纯展示:不挡点击、不抢焦点 */\n overflow: hidden;\n border-radius: 10px;\n min-height: 260px; /* 动画的下限:信息很少时也不塌成一条 */\n}\n.sp-voyage iframe {\n display: block;\n width: 100%;\n height: 100%;\n border: 0;\n /* ⚠️ 用 iframe 自身的 filter 而**不是** backdrop-filter:iframe 内容不参与本文档的\n backdrop(跨文档不采样)⇒ backdrop-filter 对它无效,只有 filter 才是可靠的杠杆。\n v1.36.1:动画移入独立右栏后不再压在文字后面 ⇒ 磨砂可以收得很轻(1.2 → 0.6px),\n 既保留柔化,又让飞船细节(舷窗/引擎环)比旧版清楚。 */\n filter: blur(0.6px);\n}\n\n/* 窄屏(放不下两栏)⇒ 退回单栏:信息在上、动画在下(DOM 顺序即如此,无需改结构) */\n@media (max-width: 860px) {\n .sp-pop {\n /* 两栏放不下时,卡片回到\"不超过视口\"的旧行为 */\n width: min(420px, calc(100vw - 24px));\n }\n .sp-popVoyage {\n flex-direction: column;\n align-items: stretch;\n }\n .sp-popVoyage > .sp-popBody {\n width: auto; /* 单栏下不再固定 340px,跟随卡片宽度 */\n flex: none; /* 按内容高(仍受自身 max-height 约束) */\n }\n .sp-voyage {\n flex: none;\n height: 200px; /* 单栏时给动画明确高度,避免被 flex 压扁 */\n min-height: 0;\n }\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);\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);\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");
@@ -176,11 +176,11 @@ window.__ModuleLoader__.load({
176
176
  className: cx("sp-chev")
177
177
  })
178
178
  ]
179
- }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
180
- className: cx("sp-pop"),
179
+ }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
180
+ className: cx("sp-pop", "sp-popVoyage"),
181
181
  role: "dialog",
182
182
  "aria-label": "CCC 状态栏",
183
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
183
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
184
184
  className: cx("sp-popBody"),
185
185
  children: inCcc ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
186
186
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -349,7 +349,16 @@ window.__ModuleLoader__.load({
349
349
  })
350
350
  })]
351
351
  })
352
- })
352
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
353
+ className: cx("sp-voyage"),
354
+ "aria-hidden": "true",
355
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("iframe", {
356
+ src: `/serenity/voyage?card=1&v=${encodeURIComponent(status.accVersion)}`,
357
+ title: "",
358
+ loading: "lazy",
359
+ tabIndex: -1
360
+ }, status.accVersion)
361
+ })]
353
362
  })]
354
363
  });
355
364
  }
package/lib/index.js CHANGED
@@ -4626,8 +4626,8 @@ var KeeperTracker = class {
4626
4626
  return this.score;
4627
4627
  }
4628
4628
  };
4629
- function reminderText(code, score) {
4630
- return `${eventToken("checkpoint")} Score threshold reached (${score}). Please acknowledge with [${ACK_PREFIX}-${code}] once progress is synced to the working session (acc-session show). No need to interrupt your work — just acknowledge inline and keep going.`;
4629
+ function reminderText(code) {
4630
+ return `${eventToken("checkpoint")} Score threshold reached. Please acknowledge with [${ACK_PREFIX}-${code}] once progress is synced to the working session (acc-session show). No need to interrupt your work — just acknowledge inline and keep going.`;
4631
4631
  }
4632
4632
  /**
4633
4633
  * F2 rebuild 提示(v1.21;v1.22.1 对齐"轨迹跟踪器"概念;v1.23.0 英化 + 载体关系;
@@ -4642,18 +4642,24 @@ function reminderText(code, score) {
4642
4642
  * escalated=true(v1.23.3):连续多轮超阈值仍未 rebuild → 升级强制语气
4643
4643
  * (STOP and rebuild now,持续注入直到调用 container_trajectory rebuild)。
4644
4644
  *
4645
- * 需求①(S142 用户拍板):百分比比例 → K 数值——tokensK = 实际占用(千 token),
4646
- * thresholdK = 配置阈值(千 token);文案 `Context usage at NNNK (threshold NNNK)`。
4645
+ * 需求①(S142 用户拍板,v1.28.0):百分比比例 → K 数值——**判定**改为绝对
4646
+ * `projectedTokens thresholdK*1000`(触发语义,未变)。
4647
+ * 🔴 2026-09-16(S142 所有者指令,D64):**判定用 K,但注入文本里不再给任何数值**——
4648
+ * 原文案 `Context usage at NNNK (threshold NNNK)` 属于"向 LLM 透露上下文消耗与限制",
4649
+ * 是非必要信息(LLM 不据此决策,只占注意力)。⇒ 参数退化为仅 `escalated`。
4650
+ * ⚠️ 触发判定与提醒行为**一字未改**(仍读 projectedTokens / thresholdK 比对;仍升级催);
4651
+ * 变的只是**注入文本不再携带 tokensK / thresholdK**。
4652
+ * 使用者若要查看当前占用/阈值:看面板(`rebuildThresholdK` 滑块 + 状态显示)——那是给人看的。
4647
4653
  *
4648
4654
  * v1.31.1 交接协议(S142 用户需求"要求 LLM 将当前手头事项写在 SESSION.md 尾部,并要求
4649
4655
  * rebuild 后去读并处理"):**写侧** = 这里(两条文案都要求把 in-flight 事项写在
4650
4656
  * SESSION.md 末尾的 `IN_FLIGHT_HEADING` 之下);**读侧** = rebuild 锚点(rebuild.ts)。
4651
4657
  * 两侧共用 trajectory-assistant 的标题常量(单一真相源)。
4652
4658
  */
4653
- function rebuildReminderText(tokensK, thresholdK, escalated = false) {
4659
+ function rebuildReminderText(escalated = false) {
4654
4660
  const handover = ` Before rebuilding, write your current in-flight items (the exact step you are in the middle of, what is not finished yet, and what the next action is) at the very end of SESSION.md under the heading "${IN_FLIGHT_HEADING}" — the rebuilt conversation reads that section first and continues from it.`;
4655
- if (escalated) return `${eventToken("limitMandatory")} Context usage at ${Math.round(tokensK)}K (threshold ${Math.round(thresholdK)}K) — you have been reminded repeatedly and have NOT called the container_trajectory rebuild action. This is now mandatory: STOP at the current task step, preserve valuable cognition into the CCC skills (or write a new-skill proposal into SESSION.md), then call container_trajectory rebuild immediately, passing --summary "<content summary ≤20 chars>" (required; the dsh session title is renamed to S###-YYYY-MM-DD-<summary> after rebuild).${handover} The conversation will be cleared and rebuilt in place; SESSION.md is the persistent trajectory and stays in place — identity continues from it. Do not continue working without rebuilding; this reminder persists until you call container_trajectory rebuild.`;
4656
- return `${eventToken("limit")} Context usage at ${Math.round(tokensK)}K (threshold ${Math.round(thresholdK)}K). This session is the rebuildable carrier of the trajectory: SESSION.md is the persistent body, this conversation is only a temporary work copy. Before rebuilding: if this conversation produced valuable cognition, revise the relevant existing skill of this CCC (structure it with eap); if a new skill is warranted, write a short proposal into SESSION.md for the user to review — do not create it yourself.${handover} ACT NOW: at the next natural pause (end of the current task step), call the container_trajectory rebuild action — passing --summary "<content summary ≤20 chars>" describing the next work phase (required; the dsh session title is renamed to S###-YYYY-MM-DD-<summary> after rebuild) — to clear and rebuild this conversation: the current copy is discarded, identity continues from SESSION.md. If you are in the middle of an unbreakable step, continue it, then rebuild at its end. Do not ignore this; rebuild is the expected action, not an option.`;
4661
+ if (escalated) return `${eventToken("limitMandatory")} Context usage has reached the configured rebuild threshold — you have been reminded repeatedly and have NOT called the container_trajectory rebuild action. This is now mandatory: STOP at the current task step, preserve valuable cognition into the CCC skills (or write a new-skill proposal into SESSION.md), then call container_trajectory rebuild immediately, passing --summary "<content summary ≤20 chars>" (required; the dsh session title is renamed to S###-YYYY-MM-DD-<summary> after rebuild).${handover} The conversation will be cleared and rebuilt in place; SESSION.md is the persistent trajectory and stays in place — identity continues from it. Do not continue working without rebuilding; this reminder persists until you call container_trajectory rebuild.`;
4662
+ return `${eventToken("limit")} Context usage has reached the configured rebuild threshold. This session is the rebuildable carrier of the trajectory: SESSION.md is the persistent body, this conversation is only a temporary work copy. Before rebuilding: if this conversation produced valuable cognition, revise the relevant existing skill of this CCC (structure it with eap); if a new skill is warranted, write a short proposal into SESSION.md for the user to review — do not create it yourself.${handover} ACT NOW: at the next natural pause (end of the current task step), call the container_trajectory rebuild action — passing --summary "<content summary ≤20 chars>" describing the next work phase (required; the dsh session title is renamed to S###-YYYY-MM-DD-<summary> after rebuild) — to clear and rebuild this conversation: the current copy is discarded, identity continues from SESSION.md. If you are in the middle of an unbreakable step, continue it, then rebuild at its end. Do not ignore this; rebuild is the expected action, not an option.`;
4657
4663
  }
4658
4664
  /** 读取会话 contextPressure 投影(sessionProjections 可选服务;未装配返回 null) */
4659
4665
  function readContextPressure(ctx, session) {
@@ -4798,7 +4804,7 @@ function registerKeeper(ctx, opts = {}) {
4798
4804
  const code = tracker.ack();
4799
4805
  blocks.push({
4800
4806
  type: "text",
4801
- text: reminderText(code, tracker.currentScore)
4807
+ text: reminderText(code)
4802
4808
  });
4803
4809
  }
4804
4810
  if (skiffRebuild && readSimpleSettings().rebuildEnabled) {
@@ -4806,7 +4812,6 @@ function registerKeeper(ctx, opts = {}) {
4806
4812
  if (session) {
4807
4813
  const pressure = readContextPressure(ctx, session);
4808
4814
  if (pressure && pressure.projectedTokens > 0) {
4809
- const tokensK = pressure.projectedTokens / 1e3;
4810
4815
  const thresholdK = readSimpleSettings().rebuildThresholdK;
4811
4816
  if (pressure.projectedTokens >= thresholdK * 1e3) {
4812
4817
  const key = exec.agent?.session?.id ?? "global";
@@ -4815,7 +4820,7 @@ function registerKeeper(ctx, opts = {}) {
4815
4820
  const escalated = st.consecutive >= REBUILD_ESCALATE_AFTER;
4816
4821
  blocks.push({
4817
4822
  type: "text",
4818
- text: rebuildReminderText(tokensK, thresholdK, escalated)
4823
+ text: rebuildReminderText(escalated)
4819
4824
  });
4820
4825
  rebuildReminderStates.set(key, st);
4821
4826
  }
@@ -6472,7 +6477,7 @@ function readBody$4(req, maxBytes) {
6472
6477
  req.on("error", reject);
6473
6478
  });
6474
6479
  }
6475
- function sendJson$3(res, code, body) {
6480
+ function sendJson$4(res, code, body) {
6476
6481
  const payload = JSON.stringify(body);
6477
6482
  res.writeHead(code, {
6478
6483
  "content-type": "application/json; charset=utf-8",
@@ -6497,7 +6502,7 @@ function senderIsWebUi(req) {
6497
6502
  */
6498
6503
  function requireWebUi(req, res, message) {
6499
6504
  if (senderIsWebUi(req)) return true;
6500
- sendJson$3(res, 403, { error: message });
6505
+ sendJson$4(res, 403, { error: message });
6501
6506
  return false;
6502
6507
  }
6503
6508
  /**
@@ -6530,7 +6535,7 @@ function registerStatusApi(ctx, opts = {}) {
6530
6535
  handler: async (req, res) => {
6531
6536
  try {
6532
6537
  if (req.method !== "GET") {
6533
- sendJson$3(res, 405, { error: "method not allowed" });
6538
+ sendJson$4(res, 405, { error: "method not allowed" });
6534
6539
  return;
6535
6540
  }
6536
6541
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
@@ -6540,12 +6545,12 @@ function registerStatusApi(ctx, opts = {}) {
6540
6545
  });
6541
6546
  const root = cccRootForCwd(workspace);
6542
6547
  if (!root) {
6543
- sendJson$3(res, 200, { handymen: [] });
6548
+ sendJson$4(res, 200, { handymen: [] });
6544
6549
  return;
6545
6550
  }
6546
- sendJson$3(res, 200, { handymen: listActiveHandymen(root) });
6551
+ sendJson$4(res, 200, { handymen: listActiveHandymen(root) });
6547
6552
  } catch (err) {
6548
- sendJson$3(res, 400, { error: err.message ?? String(err) });
6553
+ sendJson$4(res, 400, { error: err.message ?? String(err) });
6549
6554
  }
6550
6555
  }
6551
6556
  });
@@ -6555,7 +6560,7 @@ function registerStatusApi(ctx, opts = {}) {
6555
6560
  handler: async (req, res) => {
6556
6561
  try {
6557
6562
  if (req.method !== "POST") {
6558
- sendJson$3(res, 405, { error: "method not allowed" });
6563
+ sendJson$4(res, 405, { error: "method not allowed" });
6559
6564
  return;
6560
6565
  }
6561
6566
  if (!requireWebUi(req, res, "图片落盘仅限 WebUI(client 专用)")) return;
@@ -6567,12 +6572,12 @@ function registerStatusApi(ctx, opts = {}) {
6567
6572
  });
6568
6573
  const root = cccRootForCwd(workspace);
6569
6574
  if (!root) {
6570
- sendJson$3(res, 404, { error: `no CCC found from workspace: ${workspace}` });
6575
+ sendJson$4(res, 404, { error: `no CCC found from workspace: ${workspace}` });
6571
6576
  return;
6572
6577
  }
6573
- sendJson$3(res, 200, { path: saveImageToTmp(root, body.mediaType ?? "", body.data ?? "") });
6578
+ sendJson$4(res, 200, { path: saveImageToTmp(root, body.mediaType ?? "", body.data ?? "") });
6574
6579
  } catch (err) {
6575
- sendJson$3(res, 400, { error: err.message ?? String(err) });
6580
+ sendJson$4(res, 400, { error: err.message ?? String(err) });
6576
6581
  }
6577
6582
  }
6578
6583
  });
@@ -6582,7 +6587,7 @@ function registerStatusApi(ctx, opts = {}) {
6582
6587
  handler: async (req, res) => {
6583
6588
  try {
6584
6589
  if (req.method !== "POST") {
6585
- sendJson$3(res, 405, { error: "method not allowed" });
6590
+ sendJson$4(res, 405, { error: "method not allowed" });
6586
6591
  return;
6587
6592
  }
6588
6593
  if (!requireWebUi(req, res, "文件落盘仅限 WebUI(client 专用)")) return;
@@ -6594,12 +6599,12 @@ function registerStatusApi(ctx, opts = {}) {
6594
6599
  });
6595
6600
  const root = cccRootForCwd(workspace);
6596
6601
  if (!root) {
6597
- sendJson$3(res, 404, { error: `no CCC found from workspace: ${workspace}` });
6602
+ sendJson$4(res, 404, { error: `no CCC found from workspace: ${workspace}` });
6598
6603
  return;
6599
6604
  }
6600
- sendJson$3(res, 200, { path: saveFileToTmp(root, body.name ?? "", body.data ?? "") });
6605
+ sendJson$4(res, 200, { path: saveFileToTmp(root, body.name ?? "", body.data ?? "") });
6601
6606
  } catch (err) {
6602
- sendJson$3(res, 400, { error: err.message ?? String(err) });
6607
+ sendJson$4(res, 400, { error: err.message ?? String(err) });
6603
6608
  }
6604
6609
  }
6605
6610
  });
@@ -6615,7 +6620,7 @@ function registerStatusApi(ctx, opts = {}) {
6615
6620
  workspace: url.searchParams.get("workspace") ?? void 0
6616
6621
  }), configPaths);
6617
6622
  const runtime = ctx.get("codeRuntime");
6618
- sendJson$3(res, 200, {
6623
+ sendJson$4(res, 200, {
6619
6624
  ...status,
6620
6625
  ...runtime === void 0 ? { codeRuntime: null } : { codeRuntime: {
6621
6626
  language: runtime.language,
@@ -6634,19 +6639,19 @@ function registerStatusApi(ctx, opts = {}) {
6634
6639
  });
6635
6640
  const root = cccRootForCwd(workspace);
6636
6641
  if (!root) {
6637
- sendJson$3(res, 404, { error: `no CCC found from workspace: ${workspace}` });
6642
+ sendJson$4(res, 404, { error: `no CCC found from workspace: ${workspace}` });
6638
6643
  return;
6639
6644
  }
6640
6645
  const result = setSafeMode(root, body.on === true);
6641
- sendJson$3(res, 200, {
6646
+ sendJson$4(res, 200, {
6642
6647
  ...getStatus(workspace, configPaths),
6643
6648
  ...result
6644
6649
  });
6645
6650
  return;
6646
6651
  }
6647
- sendJson$3(res, 405, { error: "method not allowed" });
6652
+ sendJson$4(res, 405, { error: "method not allowed" });
6648
6653
  } catch (err) {
6649
- sendJson$3(res, 400, { error: err.message ?? String(err) });
6654
+ sendJson$4(res, 400, { error: err.message ?? String(err) });
6650
6655
  }
6651
6656
  }
6652
6657
  });
@@ -6662,7 +6667,7 @@ function registerStatusApi(ctx, opts = {}) {
6662
6667
  try {
6663
6668
  known = projectKnownWorkspaces(hostService(ctx, "workspaceRegistry")?.list?.() ?? [], settings.gateway.workspaces);
6664
6669
  } catch {}
6665
- sendJson$3(res, 200, {
6670
+ sendJson$4(res, 200, {
6666
6671
  config: toWire(settings),
6667
6672
  knownWorkspaces: known
6668
6673
  });
@@ -6674,12 +6679,12 @@ function registerStatusApi(ctx, opts = {}) {
6674
6679
  try {
6675
6680
  ctx.emit?.("serenity/config-updated");
6676
6681
  } catch {}
6677
- sendJson$3(res, 200, { config: toWire(saved) });
6682
+ sendJson$4(res, 200, { config: toWire(saved) });
6678
6683
  return;
6679
6684
  }
6680
- sendJson$3(res, 405, { error: "method not allowed" });
6685
+ sendJson$4(res, 405, { error: "method not allowed" });
6681
6686
  } catch (err) {
6682
- sendJson$3(res, 400, { error: err.message ?? String(err) });
6687
+ sendJson$4(res, 400, { error: err.message ?? String(err) });
6683
6688
  }
6684
6689
  }
6685
6690
  });
@@ -6689,7 +6694,7 @@ function registerStatusApi(ctx, opts = {}) {
6689
6694
  handler: async (req, res) => {
6690
6695
  try {
6691
6696
  if (req.method !== "GET") {
6692
- sendJson$3(res, 405, { error: "method not allowed" });
6697
+ sendJson$4(res, 405, { error: "method not allowed" });
6693
6698
  return;
6694
6699
  }
6695
6700
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
@@ -6698,12 +6703,12 @@ function registerStatusApi(ctx, opts = {}) {
6698
6703
  workspace: url.searchParams.get("workspace") ?? void 0
6699
6704
  });
6700
6705
  const root = cccRootForCwd(workspace) ?? "";
6701
- sendJson$3(res, 200, { cccs: await listCccs(ctx, {
6706
+ sendJson$4(res, 200, { cccs: await listCccs(ctx, {
6702
6707
  defaultRoot: root,
6703
6708
  withRoles: true
6704
6709
  }) });
6705
6710
  } catch (err) {
6706
- sendJson$3(res, 400, { error: err.message ?? String(err) });
6711
+ sendJson$4(res, 400, { error: err.message ?? String(err) });
6707
6712
  }
6708
6713
  }
6709
6714
  });
@@ -6716,14 +6721,14 @@ function registerStatusApi(ctx, opts = {}) {
6716
6721
  if (req.method === "PUT") {
6717
6722
  const raw = await readBody$4(req, 16384);
6718
6723
  if (JSON.parse(raw).action !== "rotate") {
6719
- sendJson$3(res, 400, { error: "unsupported action (expected \"rotate\")" });
6724
+ sendJson$4(res, 400, { error: "unsupported action (expected \"rotate\")" });
6720
6725
  return;
6721
6726
  }
6722
- sendJson$3(res, 200, { key: rotatePublicAskKey() });
6727
+ sendJson$4(res, 200, { key: rotatePublicAskKey() });
6723
6728
  return;
6724
6729
  }
6725
6730
  if (req.method !== "GET") {
6726
- sendJson$3(res, 405, { error: "method not allowed" });
6731
+ sendJson$4(res, 405, { error: "method not allowed" });
6727
6732
  return;
6728
6733
  }
6729
6734
  const settings = readAdvancedSettings();
@@ -6736,7 +6741,7 @@ function registerStatusApi(ctx, opts = {}) {
6736
6741
  name,
6737
6742
  url: `${base}/c/${encodeURIComponent(name)}`
6738
6743
  }));
6739
- sendJson$3(res, 200, {
6744
+ sendJson$4(res, 200, {
6740
6745
  enabled: simple.publicAskEnabled ?? false,
6741
6746
  port,
6742
6747
  key: ensurePublicAskKey(),
@@ -6745,7 +6750,7 @@ function registerStatusApi(ctx, opts = {}) {
6745
6750
  listUrl: `${base}/`
6746
6751
  });
6747
6752
  } catch (err) {
6748
- sendJson$3(res, 400, { error: err.message ?? String(err) });
6753
+ sendJson$4(res, 400, { error: err.message ?? String(err) });
6749
6754
  }
6750
6755
  }
6751
6756
  });
@@ -6759,7 +6764,7 @@ function registerStatusApi(ctx, opts = {}) {
6759
6764
  if (req.method === "GET") {
6760
6765
  const chk = requireCcc(url.searchParams.get("ccc") ?? void 0);
6761
6766
  if (!chk.ok) {
6762
- sendJson$3(res, 400, { error: chk.error });
6767
+ sendJson$4(res, 400, { error: chk.error });
6763
6768
  return;
6764
6769
  }
6765
6770
  const { readWeixinSettings } = await import("./weixin-route-B2ajFypI.js").then((n) => n.u);
@@ -6773,7 +6778,7 @@ function registerStatusApi(ctx, opts = {}) {
6773
6778
  enabled: a.enabled !== false,
6774
6779
  bound: readWeixinCredential(chk.root, a.accountId) !== null
6775
6780
  }));
6776
- sendJson$3(res, 200, {
6781
+ sendJson$4(res, 200, {
6777
6782
  enabled: settings.enabled === true,
6778
6783
  botType: settings.botType ?? void 0,
6779
6784
  accounts,
@@ -6783,14 +6788,14 @@ function registerStatusApi(ctx, opts = {}) {
6783
6788
  return;
6784
6789
  }
6785
6790
  if (req.method !== "POST") {
6786
- sendJson$3(res, 405, { error: "method not allowed" });
6791
+ sendJson$4(res, 405, { error: "method not allowed" });
6787
6792
  return;
6788
6793
  }
6789
6794
  const raw = await readBody$4(req, 131072);
6790
6795
  const body = JSON.parse(raw);
6791
6796
  const chk = requireCcc(body.ccc);
6792
6797
  if (!chk.ok) {
6793
- sendJson$3(res, 400, { error: chk.error });
6798
+ sendJson$4(res, 400, { error: chk.error });
6794
6799
  return;
6795
6800
  }
6796
6801
  const root = chk.root;
@@ -6807,7 +6812,7 @@ function registerStatusApi(ctx, opts = {}) {
6807
6812
  startedAt: Date.now(),
6808
6813
  polling: false
6809
6814
  });
6810
- sendJson$3(res, 200, {
6815
+ sendJson$4(res, 200, {
6811
6816
  qrcode: qr.qrcode,
6812
6817
  qrcode_img_content: qr.qrcode_img_content,
6813
6818
  loginKey
@@ -6816,31 +6821,31 @@ function registerStatusApi(ctx, opts = {}) {
6816
6821
  }
6817
6822
  if (body.action === "remove-account") {
6818
6823
  if (!body.accountId) {
6819
- sendJson$3(res, 400, { error: "missing accountId" });
6824
+ sendJson$4(res, 400, { error: "missing accountId" });
6820
6825
  return;
6821
6826
  }
6822
6827
  const { removeWeixinAccount } = await import("./weixin-route-B2ajFypI.js").then((n) => n.u);
6823
6828
  const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
6824
6829
  removeWeixinAccount(root, body.accountId);
6825
6830
  syncCccBridge(ctx, root);
6826
- sendJson$3(res, 200, { removed: body.accountId });
6831
+ sendJson$4(res, 200, { removed: body.accountId });
6827
6832
  return;
6828
6833
  }
6829
6834
  if (body.action === "save-routes") {
6830
6835
  const routes = body.routes ?? [];
6831
6836
  if (!Array.isArray(routes) || routes.some((r) => typeof r?.user !== "string" || typeof r?.role !== "string" || r.user === "" || r.role === "")) {
6832
- sendJson$3(res, 400, { error: "invalid routes (expected [{user, role}, ...])" });
6837
+ sendJson$4(res, 400, { error: "invalid routes (expected [{user, role}, ...])" });
6833
6838
  return;
6834
6839
  }
6835
6840
  const { saveWeixinRoutes } = await import("./weixin-route-B2ajFypI.js").then((n) => n.u);
6836
6841
  const { readSkiffRoles } = await import("./skiff-role-CEHL5cek.js").then((n) => n.u);
6837
6842
  const roles = readSkiffRoles(root);
6838
6843
  for (const r of routes) if (!roles.has(r.role)) {
6839
- sendJson$3(res, 400, { error: `unknown role: ${r.role} (not in ${root} skiff.roles)` });
6844
+ sendJson$4(res, 400, { error: `unknown role: ${r.role} (not in ${root} skiff.roles)` });
6840
6845
  return;
6841
6846
  }
6842
6847
  saveWeixinRoutes(root, routes);
6843
- sendJson$3(res, 200, { saved: routes.length });
6848
+ sendJson$4(res, 200, { saved: routes.length });
6844
6849
  return;
6845
6850
  }
6846
6851
  if (body.action === "set-enabled") {
@@ -6849,7 +6854,7 @@ function registerStatusApi(ctx, opts = {}) {
6849
6854
  const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
6850
6855
  const settings = readWeixinSettings(root);
6851
6856
  if (enabled && (settings.accounts ?? []).length === 0) {
6852
- sendJson$3(res, 400, { error: "启用前请先扫码绑定至少一个账号" });
6857
+ sendJson$4(res, 400, { error: "启用前请先扫码绑定至少一个账号" });
6853
6858
  return;
6854
6859
  }
6855
6860
  setWeixinEnabled(root, enabled);
@@ -6858,12 +6863,12 @@ function registerStatusApi(ctx, opts = {}) {
6858
6863
  const { stopCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
6859
6864
  stopCccBridge(root);
6860
6865
  }
6861
- sendJson$3(res, 200, { enabled });
6866
+ sendJson$4(res, 200, { enabled });
6862
6867
  return;
6863
6868
  }
6864
- sendJson$3(res, 400, { error: `unsupported action: ${body.action ?? ""}` });
6869
+ sendJson$4(res, 400, { error: `unsupported action: ${body.action ?? ""}` });
6865
6870
  } catch (err) {
6866
- sendJson$3(res, 400, { error: err.message ?? String(err) });
6871
+ sendJson$4(res, 400, { error: err.message ?? String(err) });
6867
6872
  }
6868
6873
  }
6869
6874
  });
@@ -6874,18 +6879,18 @@ function registerStatusApi(ctx, opts = {}) {
6874
6879
  try {
6875
6880
  if (!requireWebUi(req, res, "微信桥配置仅限 WebUI(client 专用)")) return;
6876
6881
  if (req.method !== "GET") {
6877
- sendJson$3(res, 405, { error: "method not allowed" });
6882
+ sendJson$4(res, 405, { error: "method not allowed" });
6878
6883
  return;
6879
6884
  }
6880
6885
  const key = new URL(req.url ?? "/", "http://127.0.0.1").searchParams.get("key") ?? "";
6881
6886
  const login = weixinLogins.get(key);
6882
6887
  if (!login) {
6883
- sendJson$3(res, 404, { error: "login not found or expired" });
6888
+ sendJson$4(res, 404, { error: "login not found or expired" });
6884
6889
  return;
6885
6890
  }
6886
6891
  if (Date.now() - login.startedAt > WEIXIN_LOGIN_TTL_MS) {
6887
6892
  weixinLogins.delete(key);
6888
- sendJson$3(res, 200, { status: "expired" });
6893
+ sendJson$4(res, 200, { status: "expired" });
6889
6894
  return;
6890
6895
  }
6891
6896
  const { pollQRStatus } = await import("./weixin-api-CtFYm53S.js").then((n) => n.u);
@@ -6898,7 +6903,7 @@ function registerStatusApi(ctx, opts = {}) {
6898
6903
  switch (status.status) {
6899
6904
  case "confirmed": {
6900
6905
  if (!status.bot_token) {
6901
- sendJson$3(res, 200, {
6906
+ sendJson$4(res, 200, {
6902
6907
  status: "error",
6903
6908
  error: "confirmed but no bot_token"
6904
6909
  });
@@ -6919,7 +6924,7 @@ function registerStatusApi(ctx, opts = {}) {
6919
6924
  });
6920
6925
  syncCccBridge(ctx, login.root);
6921
6926
  weixinLogins.delete(key);
6922
- sendJson$3(res, 200, {
6927
+ sendJson$4(res, 200, {
6923
6928
  status: "confirmed",
6924
6929
  accountId,
6925
6930
  tokenSaved: true
@@ -6928,14 +6933,14 @@ function registerStatusApi(ctx, opts = {}) {
6928
6933
  }
6929
6934
  case "expired":
6930
6935
  weixinLogins.delete(key);
6931
- sendJson$3(res, 200, { status: "expired" });
6936
+ sendJson$4(res, 200, { status: "expired" });
6932
6937
  return;
6933
6938
  default:
6934
- sendJson$3(res, 200, { status: status.status ?? "wait" });
6939
+ sendJson$4(res, 200, { status: status.status ?? "wait" });
6935
6940
  return;
6936
6941
  }
6937
6942
  } catch (err) {
6938
- sendJson$3(res, 400, { error: err.message ?? String(err) });
6943
+ sendJson$4(res, 400, { error: err.message ?? String(err) });
6939
6944
  }
6940
6945
  }
6941
6946
  });
@@ -6957,7 +6962,7 @@ function registerStatusApi(ctx, opts = {}) {
6957
6962
  const root = sessionsRootDir();
6958
6963
  if (req.method === "GET") {
6959
6964
  const { candidates } = performCleanup(root, cutoffDaysAgo(days), liveIds, { dryRun: true });
6960
- sendJson$3(res, 200, {
6965
+ sendJson$4(res, 200, {
6961
6966
  root,
6962
6967
  olderThanDays: days,
6963
6968
  dryRun: true,
@@ -6971,12 +6976,12 @@ function registerStatusApi(ctx, opts = {}) {
6971
6976
  return;
6972
6977
  }
6973
6978
  if (req.method !== "POST") {
6974
- sendJson$3(res, 405, { error: "method not allowed" });
6979
+ sendJson$4(res, 405, { error: "method not allowed" });
6975
6980
  return;
6976
6981
  }
6977
6982
  if (!requireWebUi(req, res, "会话清理仅限 WebUI(client 专用)")) return;
6978
6983
  const { result } = performCleanup(root, cutoffDaysAgo(days), liveIds);
6979
- sendJson$3(res, 200, {
6984
+ sendJson$4(res, 200, {
6980
6985
  root,
6981
6986
  olderThanDays: days,
6982
6987
  dryRun: false,
@@ -6984,7 +6989,7 @@ function registerStatusApi(ctx, opts = {}) {
6984
6989
  errors: result.errors
6985
6990
  });
6986
6991
  } catch (err) {
6987
- sendJson$3(res, 400, { error: err.message ?? String(err) });
6992
+ sendJson$4(res, 400, { error: err.message ?? String(err) });
6988
6993
  }
6989
6994
  }
6990
6995
  });
@@ -7009,6 +7014,68 @@ function requireCcc(root) {
7009
7014
  };
7010
7015
  }
7011
7016
  //#endregion
7017
+ //#region src/voyage-page.ts
7018
+ const VOYAGE_PATH = "/serenity/voyage";
7019
+ /**
7020
+ * 资产相对包根的路径。
7021
+ * 🔒 它**必须**同时出现在 `package.json` 的 `files` 白名单里——否则发布包缺文件,
7022
+ * 路由会在运行时报「资产不可用」(同 v1.26.15 那次 `files` 漏 chunk 的事故形态)。
7023
+ * `dsh-develop pack-check` 的 `required` 已把它列为发布前硬断言。
7024
+ */
7025
+ const VOYAGE_ASSET = "assets/serenity-voyage.html";
7026
+ /** 包根 = 本模块所在目录的上一级(构建产物在 `lib/`,与 `assets/` 同级;同 constants.ts 定位法) */
7027
+ function packageRootPath() {
7028
+ return join(dirname(fileURLToPath(import.meta.url)), "..");
7029
+ }
7030
+ /** 资产绝对路径(导出以便测试与 pack-check 共用同一判据来源) */
7031
+ function voyageAssetPath() {
7032
+ return join(packageRootPath(), VOYAGE_ASSET);
7033
+ }
7034
+ /** 缓存(830 KB,避免每次展开卡片都读盘;进程内只读一次) */
7035
+ let cachedHtml = null;
7036
+ /** 读取资产全文。读不到即抛错(由 handler 转 500,绝不静默发空页)。 */
7037
+ function loadVoyageHtml(assetPath = voyageAssetPath()) {
7038
+ if (assetPath === voyageAssetPath()) {
7039
+ if (cachedHtml === null) cachedHtml = readFileSync(assetPath, "utf-8");
7040
+ return cachedHtml;
7041
+ }
7042
+ return readFileSync(assetPath, "utf-8");
7043
+ }
7044
+ /** 发 JSON(错误分支用;与 api.ts 的 sendJson 同形,但本模块不依赖它,保持零耦合) */
7045
+ function sendJson$3(res, status, body) {
7046
+ res.statusCode = status;
7047
+ res.setHeader("content-type", "application/json; charset=utf-8");
7048
+ res.end(JSON.stringify(body));
7049
+ }
7050
+ /**
7051
+ * 注册 `/serenity/voyage`(GET)。
7052
+ * @param opts.assetPath 覆盖资产路径(测试用;生产不传)
7053
+ */
7054
+ function registerVoyageApi(ctx, opts = {}) {
7055
+ ctx.webServer.register({
7056
+ kind: "exact",
7057
+ path: VOYAGE_PATH,
7058
+ handler: (req, res) => {
7059
+ if (req.method !== "GET") {
7060
+ sendJson$3(res, 405, { error: "method not allowed" });
7061
+ return;
7062
+ }
7063
+ let html;
7064
+ try {
7065
+ html = loadVoyageHtml(opts.assetPath ?? voyageAssetPath());
7066
+ } catch (err) {
7067
+ sendJson$3(res, 500, { error: `voyage asset unavailable: ${err?.message ?? String(err)}` });
7068
+ return;
7069
+ }
7070
+ res.statusCode = 200;
7071
+ res.setHeader("content-type", "text/html; charset=utf-8");
7072
+ res.setHeader("cache-control", "private, max-age=86400");
7073
+ res.setHeader("content-length", String(Buffer.byteLength(html)));
7074
+ res.end(html);
7075
+ }
7076
+ });
7077
+ }
7078
+ //#endregion
7012
7079
  //#region src/seams/env.ts
7013
7080
  /** 纯解析:给定 cwd,返回可注入的 DSH_SERENITY_* 事实(非 CCC 返回空) */
7014
7081
  function resolveSerenityEnv(cwd) {
@@ -14833,7 +14900,10 @@ function apply(ctx, config) {
14833
14900
  configPaths: config.serenityConfigPaths,
14834
14901
  entrySkillMaxChars: config.entrySkillMaxChars
14835
14902
  });
14836
- if (config.api) registerStatusApi(ctx, { configPaths: config.serenityConfigPaths });
14903
+ if (config.api) {
14904
+ registerStatusApi(ctx, { configPaths: config.serenityConfigPaths });
14905
+ registerVoyageApi(ctx);
14906
+ }
14837
14907
  registerSettingsSection(ctx, config);
14838
14908
  registerGateway(ctx);
14839
14909
  registerRebuildTurnHook(ctx);