@shgroup/dsh-serenity-hooks 1.67.0 → 1.69.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dsh.plugin.json +1 -1
- package/lib/api.d.ts +27 -4
- package/lib/client.js +73 -67
- package/lib/index.js +171 -7
- package/lib/voyage-page.d.ts +5 -0
- package/lib/voyage-vocab.d.ts +51 -0
- package/package.json +1 -1
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-serenity-hooks",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.69.0",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"description": "宁静号 ACC harness(Native Cordis 插件):给 DSH 装一个「AI 工作区」——14 个工具(container_fs/cognitive_trajectory/dashboard/container_git/msm/praxis/ontology/serenity-skill-creator/cheap-worker/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫)+ 认知轨迹日志与原地重建 + 网页登录入口/微信桥/子角色/官方 ACP 面/认知轨迹唤醒注册表。适配 DSH 0.1.7-rc.1 / 0.2.0-rc.1 / 0.2.1-rc.1(三档,deepseek-ai/deepseek-harness)。",
|
|
6
6
|
"engines": {
|
package/lib/api.d.ts
CHANGED
|
@@ -34,11 +34,34 @@ interface StatusApiRegistration {
|
|
|
34
34
|
configPaths?: string[];
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
|
-
* 从请求参数解析 workspace
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
37
|
+
* 从请求参数解析 workspace:sessionId → 会话 header.cwd;其次 workspace 参数;
|
|
38
|
+
* 再次**唯一**的 CCC 会话 cwd;最后进程 cwd。
|
|
39
|
+
*
|
|
40
|
+
* 🔴🔴 **2026-10-18 修缺陷(owner 报「在别的 CCC 点开显示的还是 home-serenity 的信息」)**:
|
|
41
|
+
* 旧实现第 ③ 步是「**遍历 live sessions 找第一个 CCC 会话的 cwd**」—— 那是**跨工作区猜测**:
|
|
42
|
+
* 只要 sessionId 缺失或那条会话查不到(ACP/skiff 会话、尚未 materialize 的会话、远程访问),
|
|
43
|
+
* 它就会**静默返回另一个 CCC 的路径**,于是面板把**别人的轨迹/技能/安全模式**当成你的显示出来。
|
|
44
|
+
* 这比"显示未激活"危险得多 —— 用户会据此做判断(以为自己的 safe-mode 开着、以为那是自己的轨迹)。
|
|
45
|
+
*
|
|
46
|
+
* 修法与既有先例一致(`rebuild.ts` 对"跨轨迹猜测"的裁决:**响亮失败优于静默接错**):
|
|
47
|
+
* **多个候选 ⇒ 不猜**。只有当 live sessions 里的 CCC 根**唯一**时才采用它
|
|
48
|
+
* (那正是 v1.22.2 要救的场景:单 CCC 部署 + 远程访问缺 sessionId);
|
|
49
|
+
* 一旦出现**两个及以上**不同 CCC 根,就退回进程 cwd(→ 面板显示"未激活"),绝不挑一个顶替。
|
|
50
|
+
*
|
|
51
|
+
* @param sessionCwd sessionId 查到的会话 cwd(最权威)
|
|
52
|
+
* @param workspaceParam 显式 workspace 参数
|
|
53
|
+
* @param listCwds 枚举 live sessions 的 cwd(用于"唯一 CCC"兜底)
|
|
54
|
+
* @param fallback 最后兜底(进程 cwd)
|
|
41
55
|
*/
|
|
42
56
|
export declare function resolveWorkspaceCore(sessionCwd: string | undefined, workspaceParam: string | undefined, listCwds: () => string[], fallback: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* 解析本次请求所属的 workspace。
|
|
59
|
+
* 🔴 2026-10-18:导出给 `registerVoyageApi` 复用(航行页词表按当前 CCC 注入)——
|
|
60
|
+
* **单一真相源**,⛔ 不让调用方另写一份"找 CCC"的逻辑。
|
|
61
|
+
*/
|
|
62
|
+
export declare function resolveWorkspace(ctx: Context, params: {
|
|
63
|
+
sessionId?: string;
|
|
64
|
+
workspace?: string;
|
|
65
|
+
}): string;
|
|
43
66
|
export declare function registerStatusApi(ctx: Context, opts?: StatusApiRegistration): void;
|
|
44
67
|
export {};
|
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:src/client/SafeModePanel
|
|
11
|
-
const css$5 = "/* 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 v1.39.1(所有者:「下拉 dialog / 弹窗内容过宽后,会被左侧元素遮挡」):\n 🔴 根因 = **祖先 `overflow:hidden` 裁切**,不是 z-index 也不是宽度不够。\n 真机取证(CDP hit-test,1280/1600/1920 三档一致):\n · 胶囊右端 x=954.2、卡片 680 宽 ⇒ 左边界 274.2\n · 而会话中列 `pI_x6G_centerCol` / `wSkVaW_root` 都 `overflow:hidden` 且左边界 = **280**\n ⇒ 左边缘 **5.8px** 被裁掉(左边框线与圆角消失)。\n 加宽前(340px)左边界 614.2 远在 280 右侧 ⇒ **这正是 v1.36.1 加宽引入的回归**。\n 修法:**改为 `position: fixed`** —— fixed 子元素**不受祖先 overflow 裁切**;\n 坐标由 SafeModePanel 用 `getBoundingClientRect()` 算出并夹取进视口(见 .tsx 的 useLayoutEffect)。\n (本仓 client 侧**无 `react-dom` 依赖** ⇒ `createPortal` 不可用;fixed 是等效且更轻的出路。\n 已在真机验证祖先链**无** transform/filter/contain/will-change ⇒ fixed 不会被降级为相对某祖先。)\n 宽度仍走 `min(680px, 100vw-24px)`(窄视口兜底),窄屏退回单栏见文末 @media。 */\n.sp-pop {\n position: fixed; /* 不受祖先 overflow:hidden 裁切;left/top 由 JS 计算 */\n top: 0; /* ← 声明默认值,JS 会覆写(未落位时组件给 -9999 屏外值,不会闪到右下角) */\n left: 0;\n /* 🔴 2026-10-18(owner:「可以到 1000px 宽,长度更长些,允许存在滚动条」):\n 680 → **1000px**。这是 CCC 状态面板的**容器**,未来更多 CCC 内容都装在这里,\n ⇒ 宽度按\"要承载什么\"给,不按\"当前装了多少\"给。\n 窄视口仍走 `calc(100vw - 24px)` 兜底(不因追求宽度而在小屏上溢出)。 */\n width: min(1000px, 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 /* 🔴 2026-10-18(owner:「长度更长些,允许存在滚动条」):120 → **80**。\n 上限贴着视口走,把能给的垂直空间尽量给出去;真正超长时由**内部两栏各自滚动**消化\n (左栏 `.sp-popBody` 有 overflow-y:auto,右栏动画吃满行高)——卡片本身仍不溢出视口。 */\n max-height: calc(100vh - 80px);\n}\n/* 左栏:内容区(**主体**)—— owner 2026-10-18:「内容区作为主体,用多 panel 模式」\n 旧形态:固定 340px 的信息栏 + 吃掉剩余宽度的动画栏(动画占了大半张卡片)。\n 现在:内容区 `flex: 1` 吃掉主体,动画降为**定宽窄栏**(见 .sp-voyage)。\n ⚠️ 顺序仍由 `order` 决定(内容在左、动画在右),与 DOM 顺序解耦。 */\n.sp-popVoyage > .sp-content {\n order: 1;\n position: relative;\n z-index: 1;\n}\n/* 右栏:动画——从\"吃剩余宽度\"降为**定宽装饰条**\n owner 的原话是\"内容区作为主体\" ⇒ 装饰不该再与主体争面积。\n 240px:够看清飞船与词元,又不至于把两个 panel 挤窄。窄屏(@media)时退回全宽置于下方。 */\n.sp-voyage {\n order: 2;\n flex: none;\n width: 240px;\n min-width: 0;\n position: relative;\n pointer-events: none; /* 纯展示:不挡点击、不抢焦点 */\n overflow: hidden;\n border-radius: 10px;\n /* 卡片加高后动画也随之变长,避免\"加高只换来留白\" */\n min-height: 360px;\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(520px, calc(100vw - 24px));\n }\n .sp-popVoyage {\n flex-direction: column;\n align-items: stretch;\n }\n .sp-popVoyage > .sp-content {\n flex: 1 1 auto;\n }\n /* 单栏时 panel 网格也退回单列(窄屏两列并排会各自只剩半屏不到的宽度) */\n .sp-panels {\n grid-template-columns: 1fr;\n }\n .sp-voyage {\n flex: none;\n width: auto;\n height: 180px;\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/* ── safe-mode 状态徽标(owner 2026-10-18:「显示小标志即可,是个状态信息」)──────────\n 旧形态 = 盾牌 + SAFE 字样 + Mac 滑块(看起来像控件、实际可点)。\n 现在 = **不可交互**的小徽标:一个状态点 + 两三个字母。\n ⚠️ 刻意**不做 hover 反馈、不做 cursor:pointer** —— 它不是按钮。\n 这类\"长得像控件却只是信息\"的形态,正是旧版让人误以为面板能改 safe-mode 的成因。 */\n.sp-safeBadge {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n flex: none;\n padding: 1px 7px;\n border-radius: 99px;\n font-size: 10.5px;\n line-height: 15px;\n font-weight: 600;\n letter-spacing: 0.03em;\n border: 1px solid transparent;\n white-space: nowrap;\n}\n.sp-safeBadgeOn {\n color: #6ee7b7;\n background: rgba(16, 185, 129, 0.12);\n border-color: rgba(16, 185, 129, 0.35);\n}\n.sp-safeBadgeOff {\n color: var(--dsw-alias-label-secondary);\n background: rgba(127, 127, 127, 0.1);\n border-color: var(--dsw-alias-border-l1);\n}\n.sp-safeDot {\n width: 5px;\n height: 5px;\n border-radius: 50%;\n flex: none;\n background: currentColor;\n}\n\n/* ── 运行环境:一行元信息(owner:「比较丑,简易一些」)──────────────\n 旧形态 = 三个带边框的 row 卡片。它们是**背景事实**(路径/模型/守卫),\n 逐行加框等于把\"不值得对焦的信息\"提到与主体同等的视觉权重。\n 现在 = 一行等宽小字,溢出省略(path 很长时靠 title 给全文)。 */\n.sp-meta {\n flex: none;\n display: flex;\n align-items: baseline;\n gap: 6px;\n min-width: 0;\n font-size: 11px;\n line-height: 16px;\n color: var(--dsw-alias-label-secondary);\n font-family: var(--ds-font-family-code, monospace);\n white-space: nowrap;\n overflow: hidden;\n}\n.sp-metaPath {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.sp-metaSep {\n flex: none;\n opacity: 0.45;\n}\n\n/* ── 多 panel 主体(owner:「内容区作为主体,用多 panel 模式」)──────────\n 布局契约:这一层是**主体**,所以它拿 `flex: 1`;右栏动画被压到定宽窄条。\n panel 网格两列等宽 —— 轨迹与技能并排,各自内部滚动。\n ⚠️ 用 grid 而不是 flex:两 panel 要**等宽且同高**(等高让\"多 panel\"读起来是一个整体)。 */\n.sp-content {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 0;\n display: flex;\n flex-direction: column;\n gap: 10px;\n overflow: hidden;\n}\n.sp-panels {\n flex: 1 1 auto;\n min-height: 0;\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 10px;\n overflow: hidden;\n}\n.sp-panel {\n min-width: 0;\n min-height: 0;\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 10px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-2);\n}\n.sp-panelHead {\n flex: none;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n.sp-panelTitle {\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.sp-panelEmpty {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n}\n/* panel 内列表:**自身滚动**(不把 panel 顶长),外层不再套第二层滚动 */\n.sp-list {\n margin: 0;\n padding: 0;\n list-style: none;\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n.sp-item {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n padding: 4px 6px;\n border-radius: 6px;\n font-family: var(--ds-font-family-code, monospace);\n font-size: 11px;\n line-height: 16px;\n}\n.sp-item:hover {\n background: var(--dsw-alias-bg-overlay);\n}\n/* 标识列:UID 或编码。定宽让多行对齐,与目录名分离 */\n.sp-itemId {\n flex: none;\n min-width: 56px;\n max-width: 120px;\n font-weight: 600;\n color: var(--dsw-alias-label-secondary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.sp-itemName {\n flex: 1;\n min-width: 0;\n color: var(--dsw-alias-label-primary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.sp-itemNameFull {\n flex: 1 1 auto;\n}\n.sp-itemAge {\n flex: none;\n min-width: 32px;\n text-align: right;\n color: var(--dsw-alias-label-secondary);\n}\n/* 空壳标记:目录在但没有 SESSION.md ⇒ **真异常**(激活会失败),不是阶段 */\n.sp-itemWarn {\n flex: none;\n padding: 0 5px;\n border-radius: 4px;\n font-size: 10px;\n line-height: 14px;\n color: #fff;\n background: #c2410c;\n}\n/* 已建模标记:🟢 **信息**不是缺陷(无 skill.ttl 完全正常)⇒ 中性灰,不用告警色 */\n.sp-itemTag {\n flex: none;\n padding: 0 5px;\n border-radius: 4px;\n font-size: 10px;\n line-height: 14px;\n color: var(--dsw-alias-label-secondary);\n border: 1px solid var(--dsw-alias-border-l1);\n}\n\n/* ── 活跃轨迹清单(owner 2026-10-18 需求二)─────────────────────────\n 形态:每行 = [S### 定宽] [目录名 伸缩] [天数 定宽右对齐] [空壳标记 条件]\n · 用 mono 字体:目录名与 S### 是**标识符**,等宽才扫得快\n · 名字用 `text-overflow: ellipsis` 单行截断 + `title` 全文 —— 目录名很长时\n 宁可行内截断,也不让一行折成三四行把整个清单撑得读不下去\n · 整个列表**有上限高度 + 自身滚动**:轨迹会越攒越多,而它的容器(左栏)不该跟着无限长 */\n.sp-count {\n font-size: 11px;\n line-height: 16px;\n font-weight: 600;\n color: var(--dsw-alias-label-secondary);\n font-family: var(--ds-font-family-code, monospace);\n padding: 0 6px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 99px;\n}\n\n.sp-trajList {\n margin: 0;\n padding: 0;\n list-style: none;\n display: flex;\n flex-direction: column;\n gap: 2px;\n /* 清单自身封顶:活跃轨迹会越攒越多,超出后本列表滚动,不把左栏顶长 */\n max-height: 240px;\n overflow-y: auto;\n}\n\n.sp-trajItem {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n padding: 5px 8px;\n border-radius: 8px;\n font-family: var(--ds-font-family-code, monospace);\n font-size: 11px;\n line-height: 16px;\n}\n.sp-trajItem:hover {\n background: var(--dsw-alias-bg-layer-2);\n}\n\n.sp-trajId {\n flex: none;\n min-width: 40px;\n font-weight: 600;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sp-trajName {\n flex: 1;\n min-width: 0;\n color: var(--dsw-alias-label-primary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.sp-trajAge {\n flex: none;\n color: var(--dsw-alias-label-secondary);\n text-align: right;\n min-width: 32px;\n}\n\n/* 空壳标记:目录在但没有 SESSION.md —— 这是**真异常**(激活会失败),不是阶段。\n ⚠️ 因此用告警色,而不是任何\"状态\"色。 */\n.sp-trajWarn {\n flex: none;\n padding: 0 5px;\n border-radius: 4px;\n font-size: 10px;\n line-height: 14px;\n color: #fff;\n background: #c2410c;\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 /* 🔴 曾写死 `width: 76px` ⇒ 「CheapWorker 模型」这类长标签会**溢出到数值上**\n (nowrap + 定宽 ⇒ 文字直接压过去,不换行也不报错)。\n 改 `min-width`:短标签仍在同一列对齐,长标签自己撑开,绝不压数值。 */\n flex: none;\n min-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/* 🔴 2026-10-14:`.sp-cheapWorkers*` / `.sp-cheapWorker*` 样式区块已随 background 模式删除\n —— 那是「运行中 cheap-worker」列表(进度文件驱动)的样式,而进度文件不再有生产者。 */\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$5 = "/* 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 v1.39.1(所有者:「下拉 dialog / 弹窗内容过宽后,会被左侧元素遮挡」):\n 🔴 根因 = **祖先 `overflow:hidden` 裁切**,不是 z-index 也不是宽度不够。\n 真机取证(CDP hit-test,1280/1600/1920 三档一致):\n · 胶囊右端 x=954.2、卡片 680 宽 ⇒ 左边界 274.2\n · 而会话中列 `pI_x6G_centerCol` / `wSkVaW_root` 都 `overflow:hidden` 且左边界 = **280**\n ⇒ 左边缘 **5.8px** 被裁掉(左边框线与圆角消失)。\n 加宽前(340px)左边界 614.2 远在 280 右侧 ⇒ **这正是 v1.36.1 加宽引入的回归**。\n 修法:**改为 `position: fixed`** —— fixed 子元素**不受祖先 overflow 裁切**;\n 坐标由 SafeModePanel 用 `getBoundingClientRect()` 算出并夹取进视口(见 .tsx 的 useLayoutEffect)。\n (本仓 client 侧**无 `react-dom` 依赖** ⇒ `createPortal` 不可用;fixed 是等效且更轻的出路。\n 已在真机验证祖先链**无** transform/filter/contain/will-change ⇒ fixed 不会被降级为相对某祖先。)\n 宽度仍走 `min(680px, 100vw-24px)`(窄视口兜底),窄屏退回单栏见文末 @media。 */\n.sp-pop {\n position: fixed; /* 不受祖先 overflow:hidden 裁切;left/top 由 JS 计算 */\n top: 0; /* ← 声明默认值,JS 会覆写(未落位时组件给 -9999 屏外值,不会闪到右下角) */\n left: 0;\n /* 🔴 2026-10-18(owner:「可以到 1000px 宽,长度更长些,允许存在滚动条」):\n 680 → **1000px**。这是 CCC 状态面板的**容器**,未来更多 CCC 内容都装在这里,\n ⇒ 宽度按\"要承载什么\"给,不按\"当前装了多少\"给。\n 窄视口仍走 `calc(100vw - 24px)` 兜底(不因追求宽度而在小屏上溢出)。 */\n width: min(1000px, 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 /* 🔴 2026-10-18(owner:「长度更长些,允许存在滚动条」):120 → **80**。\n 上限贴着视口走,把能给的垂直空间尽量给出去;真正超长时由**内部两栏各自滚动**消化\n (左栏 `.sp-popBody` 有 overflow-y:auto,右栏动画吃满行高)——卡片本身仍不溢出视口。 */\n max-height: calc(100vh - 80px);\n}\n/* 左栏:内容区(**主体**)—— owner 2026-10-18:「内容区作为主体,用多 panel 模式」\n 旧形态:固定 340px 的信息栏 + 吃掉剩余宽度的动画栏(动画占了大半张卡片)。\n 现在:内容区 `flex: 1` 吃掉主体,动画降为**定宽窄栏**(见 .sp-voyage)。\n ⚠️ 顺序仍由 `order` 决定(内容在左、动画在右),与 DOM 顺序解耦。 */\n/* 左栏:内容区——**定宽一列**(tab 只显示一个列表,不需要铺满)\n 🔴 2026-10-18 owner 修正后的分工:内容区占**固定列宽**,动画吃剩余空间。\n 为什么定宽而不是 flex:1:tab 里只有一个列表,铺得越宽每行留白越多、反而更难读;\n 定宽 420px 让\"标识 + 名字 + 天数\"三列都有合适密度,剩下的横向空间整体让给动画。 */\n.sp-popVoyage > .sp-content {\n order: 1;\n position: relative;\n z-index: 1;\n flex: none;\n width: 420px;\n min-width: 0;\n}\n/* 右栏:动画——**恢复为可伸缩**(owner 2026-10-18 修正:「剩下的位置还是给动画」)\n 🔵 为什么这次可以给它 flex:1 而上一版不行:上一版两 panel 并排把宽度吃光了;\n 改成 tab 后内容区只占一列 ⇒ 右侧空出来的位置本来就该是动画的。\n ⚠️ 仍然 `min-width: 0`:动画 iframe 不该把内容区挤窄(内容区是 flex:none 的一列)。 */\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 /* 卡片加高后动画也随之变长,避免\"加高只换来留白\" */\n min-height: 360px;\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(520px, calc(100vw - 24px));\n }\n .sp-popVoyage {\n flex-direction: column;\n align-items: stretch;\n }\n .sp-popVoyage > .sp-content {\n /* 单栏下不再定宽 420px:跟随卡片宽度 */\n flex: 1 1 auto;\n width: auto;\n }\n .sp-voyage {\n flex: none;\n width: auto;\n height: 180px;\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/* ── safe-mode 状态徽标(owner 2026-10-18:「显示小标志即可,是个状态信息」)──────────\n 旧形态 = 盾牌 + SAFE 字样 + Mac 滑块(看起来像控件、实际可点)。\n 现在 = **不可交互**的小徽标:一个状态点 + 两三个字母。\n ⚠️ 刻意**不做 hover 反馈、不做 cursor:pointer** —— 它不是按钮。\n 这类\"长得像控件却只是信息\"的形态,正是旧版让人误以为面板能改 safe-mode 的成因。 */\n.sp-safeBadge {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n flex: none;\n padding: 1px 7px;\n border-radius: 99px;\n font-size: 10.5px;\n line-height: 15px;\n font-weight: 600;\n letter-spacing: 0.03em;\n border: 1px solid transparent;\n white-space: nowrap;\n}\n.sp-safeBadgeOn {\n color: #6ee7b7;\n background: rgba(16, 185, 129, 0.12);\n border-color: rgba(16, 185, 129, 0.35);\n}\n.sp-safeBadgeOff {\n color: var(--dsw-alias-label-secondary);\n background: rgba(127, 127, 127, 0.1);\n border-color: var(--dsw-alias-border-l1);\n}\n.sp-safeDot {\n width: 5px;\n height: 5px;\n border-radius: 50%;\n flex: none;\n background: currentColor;\n}\n\n/* ── 运行环境:一行元信息(owner:「比较丑,简易一些」)──────────────\n 旧形态 = 三个带边框的 row 卡片。它们是**背景事实**(路径/模型/守卫),\n 逐行加框等于把\"不值得对焦的信息\"提到与主体同等的视觉权重。\n 现在 = 一行等宽小字,溢出省略(path 很长时靠 title 给全文)。 */\n.sp-meta {\n flex: none;\n display: flex;\n align-items: baseline;\n gap: 6px;\n min-width: 0;\n font-size: 11px;\n line-height: 16px;\n color: var(--dsw-alias-label-secondary);\n font-family: var(--ds-font-family-code, monospace);\n white-space: nowrap;\n overflow: hidden;\n}\n.sp-metaPath {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.sp-metaSep {\n flex: none;\n opacity: 0.45;\n}\n\n/* ── 主体:tab 容器(owner 2026-10-18 修正:「活动轨迹和 skills 使用 tab 来实现,\n 剩下的位置还是给动画」)─────────────────────────────────────────────\n 这条修正**推翻了上一版\"两 panel 并排\"的布局**,理由值得写下来(R↓):\n 并排会把内容区横着吃掉一倍宽度,动画栏只能被压成 240px 定宽窄条。\n 改成 tab 后——**同一块面积轮换显示两个列表**——内容区宽度减半,\n 省下的横向空间**正好还给动画**(见 .sp-voyage 恢复 flex:1)。\n ⇒ \"tab\" 在本面板里不只是交互选择,它**同时是布局手段**。 */\n.sp-content {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 0;\n display: flex;\n flex-direction: column;\n gap: 10px;\n overflow: hidden;\n}\n.sp-tabs {\n flex: 1 1 auto;\n min-height: 0;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 8px;\n overflow: hidden;\n}\n/* tab 条:贴近官方 settings 的分段控件语域(低调、无边框盒) */\n.sp-tabBar {\n flex: none;\n display: flex;\n align-items: center;\n gap: 4px;\n padding-bottom: 6px;\n border-bottom: 1px solid var(--dsw-alias-border-l1);\n}\n.sp-tab {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 4px 10px;\n border: 0;\n border-radius: 8px 8px 0 0;\n background: transparent;\n font-size: 12px;\n line-height: 18px;\n font-weight: 600;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n /* 选中态**不只依赖颜色**(色盲不友好):底部 2px 高亮条 + 文字色提亮 */\n border-bottom: 2px solid transparent;\n margin-bottom: -7px;\n}\n.sp-tab:hover {\n color: var(--dsw-alias-label-primary);\n}\n.sp-tabActive {\n color: var(--dsw-alias-label-primary);\n border-bottom-color: #10b981;\n}\n.sp-panelEmpty {\n margin: 0;\n padding: 8px 4px;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n}\n/* tab 内容列表:**自身滚动**(不把容器顶长) */\n.sp-list {\n margin: 0;\n padding: 0;\n list-style: none;\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n.sp-item {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n padding: 4px 6px;\n border-radius: 6px;\n font-family: var(--ds-font-family-code, monospace);\n font-size: 11px;\n line-height: 16px;\n}\n.sp-item:hover {\n background: var(--dsw-alias-bg-overlay);\n}\n/* 标识列:UID 或编码。定宽让多行对齐,与目录名分离 */\n.sp-itemId {\n flex: none;\n min-width: 56px;\n max-width: 120px;\n font-weight: 600;\n color: var(--dsw-alias-label-secondary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.sp-itemName {\n flex: 1;\n min-width: 0;\n color: var(--dsw-alias-label-primary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.sp-itemNameFull {\n flex: 1 1 auto;\n}\n.sp-itemAge {\n flex: none;\n min-width: 32px;\n text-align: right;\n color: var(--dsw-alias-label-secondary);\n}\n/* 空壳标记:目录在但没有 SESSION.md ⇒ **真异常**(激活会失败),不是阶段 */\n.sp-itemWarn {\n flex: none;\n padding: 0 5px;\n border-radius: 4px;\n font-size: 10px;\n line-height: 14px;\n color: #fff;\n background: #c2410c;\n}\n/* 已建模标记:🟢 **信息**不是缺陷(无 skill.ttl 完全正常)⇒ 中性灰,不用告警色 */\n.sp-itemTag {\n flex: none;\n padding: 0 5px;\n border-radius: 4px;\n font-size: 10px;\n line-height: 14px;\n color: var(--dsw-alias-label-secondary);\n border: 1px solid var(--dsw-alias-border-l1);\n}\n\n/* ── 活跃轨迹清单(owner 2026-10-18 需求二)─────────────────────────\n 形态:每行 = [S### 定宽] [目录名 伸缩] [天数 定宽右对齐] [空壳标记 条件]\n · 用 mono 字体:目录名与 S### 是**标识符**,等宽才扫得快\n · 名字用 `text-overflow: ellipsis` 单行截断 + `title` 全文 —— 目录名很长时\n 宁可行内截断,也不让一行折成三四行把整个清单撑得读不下去\n · 整个列表**有上限高度 + 自身滚动**:轨迹会越攒越多,而它的容器(左栏)不该跟着无限长 */\n.sp-count {\n font-size: 11px;\n line-height: 16px;\n font-weight: 600;\n color: var(--dsw-alias-label-secondary);\n font-family: var(--ds-font-family-code, monospace);\n padding: 0 6px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 99px;\n}\n\n.sp-trajList {\n margin: 0;\n padding: 0;\n list-style: none;\n display: flex;\n flex-direction: column;\n gap: 2px;\n /* 清单自身封顶:活跃轨迹会越攒越多,超出后本列表滚动,不把左栏顶长 */\n max-height: 240px;\n overflow-y: auto;\n}\n\n.sp-trajItem {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n padding: 5px 8px;\n border-radius: 8px;\n font-family: var(--ds-font-family-code, monospace);\n font-size: 11px;\n line-height: 16px;\n}\n.sp-trajItem:hover {\n background: var(--dsw-alias-bg-layer-2);\n}\n\n.sp-trajId {\n flex: none;\n min-width: 40px;\n font-weight: 600;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sp-trajName {\n flex: 1;\n min-width: 0;\n color: var(--dsw-alias-label-primary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.sp-trajAge {\n flex: none;\n color: var(--dsw-alias-label-secondary);\n text-align: right;\n min-width: 32px;\n}\n\n/* 空壳标记:目录在但没有 SESSION.md —— 这是**真异常**(激活会失败),不是阶段。\n ⚠️ 因此用告警色,而不是任何\"状态\"色。 */\n.sp-trajWarn {\n flex: none;\n padding: 0 5px;\n border-radius: 4px;\n font-size: 10px;\n line-height: 14px;\n color: #fff;\n background: #c2410c;\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 /* 🔴 曾写死 `width: 76px` ⇒ 「CheapWorker 模型」这类长标签会**溢出到数值上**\n (nowrap + 定宽 ⇒ 文字直接压过去,不换行也不报错)。\n 改 `min-width`:短标签仍在同一列对齐,长标签自己撑开,绝不压数值。 */\n flex: none;\n min-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/* 🔴 2026-10-14:`.sp-cheapWorkers*` / `.sp-cheapWorker*` 样式区块已随 background 模式删除\n —— 那是「运行中 cheap-worker」列表(进度文件驱动)的样式,而进度文件不再有生产者。 */\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");
|
|
@@ -32,6 +32,9 @@ window.__ModuleLoader__.load({
|
|
|
32
32
|
function SafeModePanel(props) {
|
|
33
33
|
const [status, setStatus] = (0, react.useState)(null);
|
|
34
34
|
const [open, setOpen] = (0, react.useState)(false);
|
|
35
|
+
/** tab 切换(2026-10-18 owner 修正:活跃轨迹 / 技能 用 tab,而不是并排两个 panel)
|
|
36
|
+
* ⚠️ 纯前端显示状态 —— 两份数据后端一次性都给全,切换**不触发任何请求**。 */
|
|
37
|
+
const [tab, setTab] = (0, react.useState)("trajectories");
|
|
35
38
|
const rootRef = (0, react.useRef)(null);
|
|
36
39
|
const popRef = (0, react.useRef)(null);
|
|
37
40
|
const [popPos, setPopPos] = (0, react.useState)(null);
|
|
@@ -176,77 +179,80 @@ window.__ModuleLoader__.load({
|
|
|
176
179
|
] })
|
|
177
180
|
]
|
|
178
181
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
179
|
-
className: cx("sp-
|
|
180
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("
|
|
181
|
-
className: cx("sp-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
182
|
+
className: cx("sp-tabs"),
|
|
183
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
184
|
+
className: cx("sp-tabBar"),
|
|
185
|
+
role: "tablist",
|
|
186
|
+
"aria-label": "CCC 状态面板",
|
|
187
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
188
|
+
type: "button",
|
|
189
|
+
role: "tab",
|
|
190
|
+
"aria-selected": tab === "trajectories",
|
|
191
|
+
className: cx("sp-tab", tab === "trajectories" && "sp-tabActive"),
|
|
192
|
+
onClick: () => setTab("trajectories"),
|
|
193
|
+
children: ["活跃轨迹", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
188
194
|
className: cx("sp-count"),
|
|
189
195
|
children: trajectories.length
|
|
190
196
|
})]
|
|
191
|
-
}),
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
className: cx("sp-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
children: [
|
|
199
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
200
|
-
className: cx("sp-itemId"),
|
|
201
|
-
title: t.uid !== null ? "UID(唯一标识符)" : "编码(未设定 UID)",
|
|
202
|
-
children: t.uid ?? (t.sessionId || "—")
|
|
203
|
-
}),
|
|
204
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
205
|
-
className: cx("sp-itemName"),
|
|
206
|
-
title: t.dirName,
|
|
207
|
-
children: t.dirName
|
|
208
|
-
}),
|
|
209
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
210
|
-
className: cx("sp-itemAge"),
|
|
211
|
-
children: [t.ageDays, "d"]
|
|
212
|
-
}),
|
|
213
|
-
!t.hasSessionMd && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
214
|
-
className: cx("sp-itemWarn"),
|
|
215
|
-
title: "目录存在但没有 SESSION.md(空壳)",
|
|
216
|
-
children: "空壳"
|
|
217
|
-
})
|
|
218
|
-
]
|
|
219
|
-
}, t.dirName))
|
|
220
|
-
})]
|
|
221
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
222
|
-
className: cx("sp-panel"),
|
|
223
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
224
|
-
className: cx("sp-panelHead"),
|
|
225
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
226
|
-
className: cx("sp-panelTitle"),
|
|
227
|
-
children: "技能"
|
|
228
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
197
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
198
|
+
type: "button",
|
|
199
|
+
role: "tab",
|
|
200
|
+
"aria-selected": tab === "skills",
|
|
201
|
+
className: cx("sp-tab", tab === "skills" && "sp-tabActive"),
|
|
202
|
+
onClick: () => setTab("skills"),
|
|
203
|
+
children: ["技能", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
229
204
|
className: cx("sp-count"),
|
|
230
205
|
children: skills.length
|
|
231
206
|
})]
|
|
232
|
-
}), skills.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
233
|
-
className: cx("sp-panelEmpty"),
|
|
234
|
-
children: "暂无技能"
|
|
235
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
236
|
-
className: cx("sp-list"),
|
|
237
|
-
children: skills.map((s) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
238
|
-
className: cx("sp-item"),
|
|
239
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
240
|
-
className: cx("sp-itemName", "sp-itemNameFull"),
|
|
241
|
-
title: s.name,
|
|
242
|
-
children: s.name
|
|
243
|
-
}), s.modeled && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
244
|
-
className: cx("sp-itemTag"),
|
|
245
|
-
title: "已建模(存在 skill.ttl)",
|
|
246
|
-
children: "建模"
|
|
247
|
-
})]
|
|
248
|
-
}, s.name))
|
|
249
207
|
})]
|
|
208
|
+
}), tab === "trajectories" ? trajectories.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
209
|
+
className: cx("sp-panelEmpty"),
|
|
210
|
+
children: "暂无活跃轨迹"
|
|
211
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
212
|
+
className: cx("sp-list"),
|
|
213
|
+
role: "tabpanel",
|
|
214
|
+
children: trajectories.map((t) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
215
|
+
className: cx("sp-item"),
|
|
216
|
+
children: [
|
|
217
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
218
|
+
className: cx("sp-itemId"),
|
|
219
|
+
title: t.uid !== null ? "UID(唯一标识符)" : "编码(未设定 UID)",
|
|
220
|
+
children: t.uid ?? (t.sessionId || "—")
|
|
221
|
+
}),
|
|
222
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
223
|
+
className: cx("sp-itemName"),
|
|
224
|
+
title: t.dirName,
|
|
225
|
+
children: t.dirName
|
|
226
|
+
}),
|
|
227
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
228
|
+
className: cx("sp-itemAge"),
|
|
229
|
+
children: [t.ageDays, "d"]
|
|
230
|
+
}),
|
|
231
|
+
!t.hasSessionMd && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
232
|
+
className: cx("sp-itemWarn"),
|
|
233
|
+
title: "目录存在但没有 SESSION.md(空壳)",
|
|
234
|
+
children: "空壳"
|
|
235
|
+
})
|
|
236
|
+
]
|
|
237
|
+
}, t.dirName))
|
|
238
|
+
}) : skills.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
239
|
+
className: cx("sp-panelEmpty"),
|
|
240
|
+
children: "暂无技能"
|
|
241
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
242
|
+
className: cx("sp-list"),
|
|
243
|
+
role: "tabpanel",
|
|
244
|
+
children: skills.map((s) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
245
|
+
className: cx("sp-item"),
|
|
246
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
247
|
+
className: cx("sp-itemName", "sp-itemNameFull"),
|
|
248
|
+
title: s.name,
|
|
249
|
+
children: s.name
|
|
250
|
+
}), s.modeled && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
251
|
+
className: cx("sp-itemTag"),
|
|
252
|
+
title: "已建模(存在 skill.ttl)",
|
|
253
|
+
children: "建模"
|
|
254
|
+
})]
|
|
255
|
+
}, s.name))
|
|
250
256
|
})]
|
|
251
257
|
})] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
252
258
|
className: cx("sp-panelEmpty"),
|
|
@@ -256,11 +262,11 @@ window.__ModuleLoader__.load({
|
|
|
256
262
|
className: cx("sp-voyage"),
|
|
257
263
|
"aria-hidden": "true",
|
|
258
264
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("iframe", {
|
|
259
|
-
src: `/serenity/voyage?card=1&v=${encodeURIComponent(status.accVersion)}`,
|
|
265
|
+
src: `/serenity/voyage?card=1&v=${encodeURIComponent(status.accVersion)}${status.root ? `&workspace=${encodeURIComponent(status.root)}` : ""}`,
|
|
260
266
|
title: "",
|
|
261
267
|
loading: "lazy",
|
|
262
268
|
tabIndex: -1
|
|
263
|
-
}, status.accVersion)
|
|
269
|
+
}, `${status.accVersion}|${status.root ?? ""}`)
|
|
264
270
|
})]
|
|
265
271
|
})]
|
|
266
272
|
});
|
package/lib/index.js
CHANGED
|
@@ -15117,17 +15117,42 @@ function requireWebUi(req, res, message) {
|
|
|
15117
15117
|
return false;
|
|
15118
15118
|
}
|
|
15119
15119
|
/**
|
|
15120
|
-
* 从请求参数解析 workspace
|
|
15121
|
-
*
|
|
15122
|
-
*
|
|
15123
|
-
*
|
|
15120
|
+
* 从请求参数解析 workspace:sessionId → 会话 header.cwd;其次 workspace 参数;
|
|
15121
|
+
* 再次**唯一**的 CCC 会话 cwd;最后进程 cwd。
|
|
15122
|
+
*
|
|
15123
|
+
* 🔴🔴 **2026-10-18 修缺陷(owner 报「在别的 CCC 点开显示的还是 home-serenity 的信息」)**:
|
|
15124
|
+
* 旧实现第 ③ 步是「**遍历 live sessions 找第一个 CCC 会话的 cwd**」—— 那是**跨工作区猜测**:
|
|
15125
|
+
* 只要 sessionId 缺失或那条会话查不到(ACP/skiff 会话、尚未 materialize 的会话、远程访问),
|
|
15126
|
+
* 它就会**静默返回另一个 CCC 的路径**,于是面板把**别人的轨迹/技能/安全模式**当成你的显示出来。
|
|
15127
|
+
* 这比"显示未激活"危险得多 —— 用户会据此做判断(以为自己的 safe-mode 开着、以为那是自己的轨迹)。
|
|
15128
|
+
*
|
|
15129
|
+
* 修法与既有先例一致(`rebuild.ts` 对"跨轨迹猜测"的裁决:**响亮失败优于静默接错**):
|
|
15130
|
+
* **多个候选 ⇒ 不猜**。只有当 live sessions 里的 CCC 根**唯一**时才采用它
|
|
15131
|
+
* (那正是 v1.22.2 要救的场景:单 CCC 部署 + 远程访问缺 sessionId);
|
|
15132
|
+
* 一旦出现**两个及以上**不同 CCC 根,就退回进程 cwd(→ 面板显示"未激活"),绝不挑一个顶替。
|
|
15133
|
+
*
|
|
15134
|
+
* @param sessionCwd sessionId 查到的会话 cwd(最权威)
|
|
15135
|
+
* @param workspaceParam 显式 workspace 参数
|
|
15136
|
+
* @param listCwds 枚举 live sessions 的 cwd(用于"唯一 CCC"兜底)
|
|
15137
|
+
* @param fallback 最后兜底(进程 cwd)
|
|
15124
15138
|
*/
|
|
15125
15139
|
function resolveWorkspaceCore(sessionCwd, workspaceParam, listCwds, fallback) {
|
|
15126
15140
|
if (sessionCwd) return sessionCwd;
|
|
15127
15141
|
if (workspaceParam) return workspaceParam;
|
|
15128
|
-
|
|
15142
|
+
const roots = /* @__PURE__ */ new Set();
|
|
15143
|
+
for (const cwd of listCwds()) {
|
|
15144
|
+
if (!cwd) continue;
|
|
15145
|
+
const r = cccRootForCwd(cwd);
|
|
15146
|
+
if (r) roots.add(r);
|
|
15147
|
+
}
|
|
15148
|
+
if (roots.size === 1) return [...roots][0];
|
|
15129
15149
|
return fallback;
|
|
15130
15150
|
}
|
|
15151
|
+
/**
|
|
15152
|
+
* 解析本次请求所属的 workspace。
|
|
15153
|
+
* 🔴 2026-10-18:导出给 `registerVoyageApi` 复用(航行页词表按当前 CCC 注入)——
|
|
15154
|
+
* **单一真相源**,⛔ 不让调用方另写一份"找 CCC"的逻辑。
|
|
15155
|
+
*/
|
|
15131
15156
|
function resolveWorkspace(ctx, params) {
|
|
15132
15157
|
let sessionCwd;
|
|
15133
15158
|
if (params.sessionId) sessionCwd = (hostSessions(ctx)?.get?.(params.sessionId))?.header?.cwd;
|
|
@@ -15652,6 +15677,126 @@ function registerAcpAccountsApi(ctx) {
|
|
|
15652
15677
|
});
|
|
15653
15678
|
}
|
|
15654
15679
|
//#endregion
|
|
15680
|
+
//#region src/voyage-vocab.ts
|
|
15681
|
+
/**
|
|
15682
|
+
* voyage-vocab.ts — 航行页词表改为**按当前 CCC 运行时注入**(纯逻辑,零 DSH 依赖)
|
|
15683
|
+
*
|
|
15684
|
+
* ## 为什么需要它(owner 2026-10-18 报的缺陷)
|
|
15685
|
+
* 资产 `assets/serenity-voyage.html` 的词表是**构建期扫描容器写死的常量**:
|
|
15686
|
+
* `var VOCAB = {"skills":[...],"msms":[...],"zh":[...],"onto":[...],"counts":{...}}`
|
|
15687
|
+
* v2 改版把"写死名单"改成"构建期扫描"是为防名单漂移——方向对,但代价是:
|
|
15688
|
+
* **一份静态资产只能属于一个 CCC**。于是任何别的 CCC 打开动画,看到的都是
|
|
15689
|
+
* **构建它的那个 CCC** 的技能/工具/本体名字与计数。owner 原话:
|
|
15690
|
+
* 「为什么我在别的 CCC 点开显示的还是我们 home-serenity 的信息」「动画也有这个毛病」。
|
|
15691
|
+
*
|
|
15692
|
+
* ## 修法(owner 裁决:运行时按当前 CCC 注入)
|
|
15693
|
+
* 资产**保持静态**(一份文件服务所有 CCC,永不漂);由它的**服务端路由**在返回前
|
|
15694
|
+
* 把 `var VOCAB = {...};` 那一行替换成从该请求所属 CCC 实况读出的词表。
|
|
15695
|
+
*
|
|
15696
|
+
* 三条判据(R↓):
|
|
15697
|
+
* ① **只替换那一行**(锚 = 行首 `var VOCAB = ` 到行尾 `;`)—— 资产里其余 3000+ 行
|
|
15698
|
+
* three.js / 渲染代码**一个字节都不动**。
|
|
15699
|
+
* ② **读不出来就走原样**:锚认不出、或原 VOCAB 解析失败 ⇒ **返回入参本身**,不报错。
|
|
15700
|
+
* 航行页是**装饰**,不该因为某个 CCC 缺目录、或资产结构变了就打不开(旁路容忍铁律)。
|
|
15701
|
+
* ③ **`zh` 不来自容器**:中文隐喻词是作品的语汇装饰,属资产自身 ⇒ 注入时**保留原值**。
|
|
15702
|
+
* 实现上不"重建"整个对象,而是**在原对象上覆盖**四个实况键 —— 未列出的键自动保留。
|
|
15703
|
+
*/
|
|
15704
|
+
/** 资产里那一行的锚 —— `var VOCAB = {...};`(`.*?` 非贪婪 + 行尾分号) */
|
|
15705
|
+
const VOCAB_LINE_RE = /^var VOCAB = (\{.*\});[ \t]*$/m;
|
|
15706
|
+
/**
|
|
15707
|
+
* 扫描一个 CCC 的实况词表。**每一项独立容错**:任一来源缺失/读坏 ⇒ 该项为空,其余照常。
|
|
15708
|
+
*
|
|
15709
|
+
* | 字段 | 来源 | 为什么是它 |
|
|
15710
|
+
* |---|---|---|
|
|
15711
|
+
* | `skills` | `.opencode/skills/` 下含 `SKILL.md` 的目录名 | 与状态面板同一条判据 |
|
|
15712
|
+
* | `msms` | `mech-registry.json` 的注册名 | **MSM 的唯一真相源** |
|
|
15713
|
+
* | `onto` | `.opencode/skills/` 下含 `skill.ttl` 的目录名 | 增强型(建模过)的 skill |
|
|
15714
|
+
* | `counts` | 上述长度 + 轨迹数 | 计数与词表同源,否则 HUD 与星场对不上 |
|
|
15715
|
+
*
|
|
15716
|
+
* @param root CCC 根
|
|
15717
|
+
*/
|
|
15718
|
+
function scanVoyageLiveVocab(root) {
|
|
15719
|
+
const skills = scanSkillDirs(root, "SKILL.md");
|
|
15720
|
+
const onto = scanSkillDirs(root, "skill.ttl");
|
|
15721
|
+
const msms = scanMsmNames(root);
|
|
15722
|
+
return {
|
|
15723
|
+
skills,
|
|
15724
|
+
msms,
|
|
15725
|
+
onto,
|
|
15726
|
+
counts: {
|
|
15727
|
+
skill: skills.length,
|
|
15728
|
+
msm: msms.length,
|
|
15729
|
+
onto: onto.length,
|
|
15730
|
+
session: scanTrajectoryCount(root)
|
|
15731
|
+
}
|
|
15732
|
+
};
|
|
15733
|
+
}
|
|
15734
|
+
/** 技能名:`.opencode/skills/<name>/<marker>` 存在的目录名(marker = `SKILL.md` 或 `skill.ttl`) */
|
|
15735
|
+
function scanSkillDirs(root, marker) {
|
|
15736
|
+
const dir = join(root, ".opencode", "skills");
|
|
15737
|
+
try {
|
|
15738
|
+
if (!existsSync(dir)) return [];
|
|
15739
|
+
return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).filter((e) => existsSync(join(dir, e.name, marker))).map((e) => e.name).sort();
|
|
15740
|
+
} catch {
|
|
15741
|
+
return [];
|
|
15742
|
+
}
|
|
15743
|
+
}
|
|
15744
|
+
/**
|
|
15745
|
+
* MSM 名:读 `mech-registry.json` 的 `msms[].name`。
|
|
15746
|
+
*
|
|
15747
|
+
* 🔴 **为什么不扫 `scripts/`**:注册表是 MSM 的**唯一真相源**;扫目录会把共享库
|
|
15748
|
+
* (`dev-kit/`、`lib/*`)也算成 MSM —— 本仓对"入口判据"已翻车多次。
|
|
15749
|
+
* ⚠️ 注册表若换了位置 ⇒ **静默降级为空**(装饰页不该因此打不开)。
|
|
15750
|
+
*/
|
|
15751
|
+
function scanMsmNames(root) {
|
|
15752
|
+
const candidates = [join(root, ".opencode", "skills", "home-serenity", "references", "mech-registry.json"), join(root, ".opencode", "references", "mech-registry.json")];
|
|
15753
|
+
for (const p of candidates) try {
|
|
15754
|
+
if (!existsSync(p)) continue;
|
|
15755
|
+
const names = (JSON.parse(readFileSync(p, "utf-8")).msms ?? []).map((m) => typeof m?.name === "string" ? m.name : "").filter((n) => n !== "");
|
|
15756
|
+
if (names.length > 0) return [...new Set(names)].sort();
|
|
15757
|
+
} catch {}
|
|
15758
|
+
return [];
|
|
15759
|
+
}
|
|
15760
|
+
/** 轨迹数:`AGENT_SESSIONS/` 下非 `_` / `.` 前缀的目录(与轨迹层"活跃"判据一致) */
|
|
15761
|
+
function scanTrajectoryCount(root) {
|
|
15762
|
+
const dir = join(root, "AGENT_SESSIONS");
|
|
15763
|
+
try {
|
|
15764
|
+
if (!existsSync(dir)) return 0;
|
|
15765
|
+
return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).filter((e) => !e.name.startsWith("_") && !e.name.startsWith(".")).length;
|
|
15766
|
+
} catch {
|
|
15767
|
+
return 0;
|
|
15768
|
+
}
|
|
15769
|
+
}
|
|
15770
|
+
/**
|
|
15771
|
+
* 把实况词表注入资产 HTML。
|
|
15772
|
+
*
|
|
15773
|
+
* 做法 = **在原 VOCAB 对象上覆盖**四个实况键(不重建整个对象)⇒ `zh` 等装饰键自动保留。
|
|
15774
|
+
* 任一环节不成立就**原样返回**(见文件头判据②),因此:
|
|
15775
|
+
* @returns 注入后的 HTML;未注入时**与入参逐字节相同**
|
|
15776
|
+
*/
|
|
15777
|
+
function injectVoyageVocab(html, live) {
|
|
15778
|
+
if (live === null) return html;
|
|
15779
|
+
const m = VOCAB_LINE_RE.exec(html);
|
|
15780
|
+
if (!m) return html;
|
|
15781
|
+
let base;
|
|
15782
|
+
try {
|
|
15783
|
+
base = JSON.parse(m[1]);
|
|
15784
|
+
} catch {
|
|
15785
|
+
return html;
|
|
15786
|
+
}
|
|
15787
|
+
const merged = {
|
|
15788
|
+
...base,
|
|
15789
|
+
skills: live.skills,
|
|
15790
|
+
msms: live.msms,
|
|
15791
|
+
onto: live.onto,
|
|
15792
|
+
counts: {
|
|
15793
|
+
...typeof base.counts === "object" && base.counts !== null ? base.counts : {},
|
|
15794
|
+
...live.counts
|
|
15795
|
+
}
|
|
15796
|
+
};
|
|
15797
|
+
return html.replace(VOCAB_LINE_RE, () => `var VOCAB = ${JSON.stringify(merged)};`);
|
|
15798
|
+
}
|
|
15799
|
+
//#endregion
|
|
15655
15800
|
//#region src/voyage-page.ts
|
|
15656
15801
|
const VOYAGE_PATH = "/serenity/voyage";
|
|
15657
15802
|
/**
|
|
@@ -15688,6 +15833,9 @@ function sendJson$2(res, status, body) {
|
|
|
15688
15833
|
/**
|
|
15689
15834
|
* 注册 `/serenity/voyage`(GET)。
|
|
15690
15835
|
* @param opts.assetPath 覆盖资产路径(测试用;生产不传)
|
|
15836
|
+
* @param opts.resolveRoot 解析本次请求所属 CCC 根(生产由 api.ts 的 resolveWorkspace 提供)。
|
|
15837
|
+
* 🔴 **2026-10-18 新增**:词表按请求所属 CCC **运行时注入**(owner 报「在别的 CCC 点开
|
|
15838
|
+
* 显示的还是 home-serenity 的信息」)。不传 ⇒ 不注入(保持资产原样 = 旧行为)。
|
|
15691
15839
|
*/
|
|
15692
15840
|
function registerVoyageApi(ctx, opts = {}) {
|
|
15693
15841
|
ctx.webServer.register({
|
|
@@ -15705,9 +15853,18 @@ function registerVoyageApi(ctx, opts = {}) {
|
|
|
15705
15853
|
sendJson$2(res, 500, { error: `voyage asset unavailable: ${err?.message ?? String(err)}` });
|
|
15706
15854
|
return;
|
|
15707
15855
|
}
|
|
15856
|
+
let root = null;
|
|
15857
|
+
try {
|
|
15858
|
+
root = opts.resolveRoot?.(req) ?? null;
|
|
15859
|
+
} catch {
|
|
15860
|
+
root = null;
|
|
15861
|
+
}
|
|
15862
|
+
if (root !== null) try {
|
|
15863
|
+
html = injectVoyageVocab(html, scanVoyageLiveVocab(root));
|
|
15864
|
+
} catch {}
|
|
15708
15865
|
res.statusCode = 200;
|
|
15709
15866
|
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
15710
|
-
res.setHeader("cache-control", "private, max-age=86400");
|
|
15867
|
+
res.setHeader("cache-control", root !== null ? "no-cache" : "private, max-age=86400");
|
|
15711
15868
|
res.setHeader("content-length", String(Buffer.byteLength(html)));
|
|
15712
15869
|
res.end(html);
|
|
15713
15870
|
}
|
|
@@ -25465,7 +25622,14 @@ function apply(ctx, config) {
|
|
|
25465
25622
|
});
|
|
25466
25623
|
if (config.api) {
|
|
25467
25624
|
registerStatusApi(ctx, { configPaths: config.serenityConfigPaths });
|
|
25468
|
-
registerVoyageApi(ctx)
|
|
25625
|
+
registerVoyageApi(ctx, { resolveRoot: (req) => {
|
|
25626
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
25627
|
+
const ws = resolveWorkspace(ctx, {
|
|
25628
|
+
sessionId: url.searchParams.get("sessionId") ?? void 0,
|
|
25629
|
+
workspace: url.searchParams.get("workspace") ?? void 0
|
|
25630
|
+
});
|
|
25631
|
+
return cccRootForCwd(ws);
|
|
25632
|
+
} });
|
|
25469
25633
|
}
|
|
25470
25634
|
registerSettingsSection(ctx, config);
|
|
25471
25635
|
if (config.tools) registerDiagramTool(ctx);
|
package/lib/voyage-page.d.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* 技能/公理/MSM/本体词元与实测容器规模数字。
|
|
22
22
|
*/
|
|
23
23
|
import type { Context } from 'cordis';
|
|
24
|
+
import type { IncomingMessage } from 'node:http';
|
|
24
25
|
export declare const VOYAGE_PATH = "/serenity/voyage";
|
|
25
26
|
/**
|
|
26
27
|
* 资产相对包根的路径。
|
|
@@ -38,7 +39,11 @@ export declare function resetVoyageCache(): void;
|
|
|
38
39
|
/**
|
|
39
40
|
* 注册 `/serenity/voyage`(GET)。
|
|
40
41
|
* @param opts.assetPath 覆盖资产路径(测试用;生产不传)
|
|
42
|
+
* @param opts.resolveRoot 解析本次请求所属 CCC 根(生产由 api.ts 的 resolveWorkspace 提供)。
|
|
43
|
+
* 🔴 **2026-10-18 新增**:词表按请求所属 CCC **运行时注入**(owner 报「在别的 CCC 点开
|
|
44
|
+
* 显示的还是 home-serenity 的信息」)。不传 ⇒ 不注入(保持资产原样 = 旧行为)。
|
|
41
45
|
*/
|
|
42
46
|
export declare function registerVoyageApi(ctx: Context, opts?: {
|
|
43
47
|
assetPath?: string;
|
|
48
|
+
resolveRoot?: (req: IncomingMessage) => string | null;
|
|
44
49
|
}): void;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* voyage-vocab.ts — 航行页词表改为**按当前 CCC 运行时注入**(纯逻辑,零 DSH 依赖)
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么需要它(owner 2026-10-18 报的缺陷)
|
|
5
|
+
* 资产 `assets/serenity-voyage.html` 的词表是**构建期扫描容器写死的常量**:
|
|
6
|
+
* `var VOCAB = {"skills":[...],"msms":[...],"zh":[...],"onto":[...],"counts":{...}}`
|
|
7
|
+
* v2 改版把"写死名单"改成"构建期扫描"是为防名单漂移——方向对,但代价是:
|
|
8
|
+
* **一份静态资产只能属于一个 CCC**。于是任何别的 CCC 打开动画,看到的都是
|
|
9
|
+
* **构建它的那个 CCC** 的技能/工具/本体名字与计数。owner 原话:
|
|
10
|
+
* 「为什么我在别的 CCC 点开显示的还是我们 home-serenity 的信息」「动画也有这个毛病」。
|
|
11
|
+
*
|
|
12
|
+
* ## 修法(owner 裁决:运行时按当前 CCC 注入)
|
|
13
|
+
* 资产**保持静态**(一份文件服务所有 CCC,永不漂);由它的**服务端路由**在返回前
|
|
14
|
+
* 把 `var VOCAB = {...};` 那一行替换成从该请求所属 CCC 实况读出的词表。
|
|
15
|
+
*
|
|
16
|
+
* 三条判据(R↓):
|
|
17
|
+
* ① **只替换那一行**(锚 = 行首 `var VOCAB = ` 到行尾 `;`)—— 资产里其余 3000+ 行
|
|
18
|
+
* three.js / 渲染代码**一个字节都不动**。
|
|
19
|
+
* ② **读不出来就走原样**:锚认不出、或原 VOCAB 解析失败 ⇒ **返回入参本身**,不报错。
|
|
20
|
+
* 航行页是**装饰**,不该因为某个 CCC 缺目录、或资产结构变了就打不开(旁路容忍铁律)。
|
|
21
|
+
* ③ **`zh` 不来自容器**:中文隐喻词是作品的语汇装饰,属资产自身 ⇒ 注入时**保留原值**。
|
|
22
|
+
* 实现上不"重建"整个对象,而是**在原对象上覆盖**四个实况键 —— 未列出的键自动保留。
|
|
23
|
+
*/
|
|
24
|
+
/** 从 CCC 实况读出的词表(只含**会变**的四个键;`zh` 与其它装饰键不在其中) */
|
|
25
|
+
export interface VoyageLiveVocab {
|
|
26
|
+
skills: string[];
|
|
27
|
+
msms: string[];
|
|
28
|
+
onto: string[];
|
|
29
|
+
counts: Record<string, number>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 扫描一个 CCC 的实况词表。**每一项独立容错**:任一来源缺失/读坏 ⇒ 该项为空,其余照常。
|
|
33
|
+
*
|
|
34
|
+
* | 字段 | 来源 | 为什么是它 |
|
|
35
|
+
* |---|---|---|
|
|
36
|
+
* | `skills` | `.opencode/skills/` 下含 `SKILL.md` 的目录名 | 与状态面板同一条判据 |
|
|
37
|
+
* | `msms` | `mech-registry.json` 的注册名 | **MSM 的唯一真相源** |
|
|
38
|
+
* | `onto` | `.opencode/skills/` 下含 `skill.ttl` 的目录名 | 增强型(建模过)的 skill |
|
|
39
|
+
* | `counts` | 上述长度 + 轨迹数 | 计数与词表同源,否则 HUD 与星场对不上 |
|
|
40
|
+
*
|
|
41
|
+
* @param root CCC 根
|
|
42
|
+
*/
|
|
43
|
+
export declare function scanVoyageLiveVocab(root: string): VoyageLiveVocab;
|
|
44
|
+
/**
|
|
45
|
+
* 把实况词表注入资产 HTML。
|
|
46
|
+
*
|
|
47
|
+
* 做法 = **在原 VOCAB 对象上覆盖**四个实况键(不重建整个对象)⇒ `zh` 等装饰键自动保留。
|
|
48
|
+
* 任一环节不成立就**原样返回**(见文件头判据②),因此:
|
|
49
|
+
* @returns 注入后的 HTML;未注入时**与入参逐字节相同**
|
|
50
|
+
*/
|
|
51
|
+
export declare function injectVoyageVocab(html: string, live: VoyageLiveVocab | null): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shgroup/dsh-serenity-hooks",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.69.0",
|
|
4
4
|
"description": "宁静号 ACC harness(Native Cordis 插件)——给 DeepSeek Harness 装一个「AI 工作区」:14 个工具(container_fs/cognitive_trajectory/dashboard/container_git/msm/praxis/ontology/serenity-skill-creator/cheap-worker/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫/对外输出守卫)+ 认知轨迹日志与原地重建 + 网页登录入口/微信桥/子角色/官方 ACP 面/认知轨迹唤醒注册表。适配 DSH 0.1.7-rc.1 / 0.2.0-rc.1 / 0.2.1-rc.1(三档)。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|