@shgroup/dsh-serenity-hooks 1.24.0 → 1.24.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-serenity-hooks",
3
- "version": "1.24.0",
3
+ "version": "1.24.2",
4
4
  "main": "lib/index.js",
5
5
  "description": "宁静号 ACC harness(Native Cordis 插件):真实 DSH 工具 cc_fs/session/acc_msm/eap/neat/cce/handyman/session_rebuild + 拦截缝机械约束(safe-mode/路径守卫)+ 高级设定面板(双端口网关/账号管理)。适配 DSH 公开版(0.1.0-rc,deepseek-ai/deepseek-harness)。",
6
6
  "engines": {
package/lib/client.js CHANGED
@@ -8,7 +8,7 @@ window.__ModuleLoader__.load({
8
8
  let react = require("react");
9
9
  let react_jsx_runtime = require("react/jsx-runtime");
10
10
  //#region \0sp-css:/home/yh/home/home-serenity/AI_LAB/dsh-serenity-plugin/hooks/dsh-serenity-hooks/src/client/SafeModePanel
11
- const css$3 = "/* SafeModePanel — 会话头部 Serenity 状态卡片(v1.22 UX 重构)\n * 卡片 → 点击 → 官方 Modal(body portal 遮罩 + 居中)\n * 模态内:tab 栏 + 分组(groupTitle 标题 + rowCard 行卡)——看齐官方 settings 设计语言\n * 遵循 web-styling.md:颜色只用 --dsw-alias-* 语义 token(明暗自适应);\n * 前缀 sp- 防冲突。 */\n\n/* ── 头部状态卡片(可点击) ── */\n\n.sp-card {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 26px;\n padding: 0 10px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n background: var(--dsw-alias-bg-module-platform);\n color: inherit;\n font-size: 12px;\n cursor: pointer;\n transition: background var(--ds-transition-duration-fast, 0.1s) var(--ds-ease-in-out, ease),\n border-color var(--ds-transition-duration-fast, 0.1s) var(--ds-ease-in-out, ease);\n}\n\n.sp-card:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n border-color: var(--dsw-alias-border-l2);\n}\n\n.sp-card:focus-visible {\n outline: 2px solid var(--dsw-alias-button-ghost-active-border);\n outline-offset: 1px;\n}\n\n.sp-card:disabled {\n opacity: 0.6;\n cursor: default;\n}\n\n.sp-dot {\n flex: none;\n width: 7px;\n height: 7px;\n border-radius: 50%;\n}\n\n.sp-dotOn {\n background: var(--dsw-alias-state-success-primary);\n}\n\n.sp-dotOff {\n background: var(--dsw-alias-label-dimmed);\n}\n\n/* safe-mode tag(实心方形标签,无 icon——从外面一眼可见;ON 红底 SAFE / OFF 灰底 OFF) */\n.sp-tag {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n height: 16px;\n padding: 0 7px;\n border-radius: 4px;\n font-size: 10px;\n font-weight: 700;\n line-height: 1;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n}\n\n.sp-tagOn {\n background: var(--dsw-alias-state-error-primary);\n color: var(--dsw-alias-label-primary-foreground);\n border: 1px solid var(--dsw-alias-state-error-primary);\n}\n\n.sp-tagOff {\n background: var(--dsw-alias-interactive-bg-active, rgba(128, 128, 128, 0.16));\n color: var(--dsw-alias-label-secondary);\n border: 1px solid var(--dsw-alias-border-l1);\n}\n\n.sp-brand {\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n\n.sp-chev {\n color: var(--dsw-alias-label-dimmed);\n}\n\n/* ── 官方 Modal 定制(宽度 + 内容紧凑) ── */\n\n.sp-modal {\n width: 520px;\n max-width: calc(100vw - 48px);\n}\n\n/* ── tab 栏 ── */\n\n.sp-tabs {\n display: flex;\n gap: 6px;\n padding: 0 2px 12px;\n border-bottom: 1px solid var(--dsw-alias-border-l1);\n}\n\n.sp-tab {\n flex: 1;\n padding: 8px 0;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n transition: background var(--ds-transition-duration-fast, 0.1s) var(--ds-ease-in-out, ease);\n}\n\n.sp-tab:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.sp-tabOn {\n background: var(--dsw-alias-interactive-bg-active, rgba(128, 128, 128, 0.2));\n color: var(--dsw-alias-label-primary);\n font-weight: 600;\n}\n\n/* ── 模态内容 ── */\n\n.sp-modalBody {\n display: flex;\n flex-direction: column;\n gap: 16px;\n padding-top: 14px;\n /* 内容超出视口时 body 区滚动(兜底——不再截断);整卡不滚动 */\n overflow-y: auto;\n max-height: calc(100vh - 200px);\n}\n\n/* ── 分组(官方 settings 语言:groupTitle + rows) ── */\n\n.sp-group {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.sp-groupHead {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n\n.sp-groupTitle {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n font-weight: 600;\n letter-spacing: 0.02em;\n text-transform: uppercase;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sp-rows {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n/* 行卡(rowCard:border + radius + padding;值与 label 分离、长内容可换行) */\n.sp-row {\n display: flex;\n align-items: baseline;\n gap: 12px;\n min-width: 0;\n padding: 8px 12px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-module);\n}\n\n.sp-label {\n flex: none;\n width: 76px;\n font-size: 12px;\n font-weight: 500;\n color: var(--dsw-alias-label-secondary);\n white-space: nowrap;\n}\n\n.sp-value {\n flex: 1;\n min-width: 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n font-family: var(--ds-font-family-code, monospace);\n word-break: break-all;\n overflow-wrap: anywhere;\n}\n\n/* 安全模式动作区 */\n.sp-actions {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 8px 12px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-module);\n}\n\n.sp-toggle {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 24px;\n padding: 0 10px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 6px;\n font-size: 12px;\n cursor: pointer;\n transition: background var(--ds-transition-duration-fast, 0.1s) var(--ds-ease-in-out, ease);\n}\n\n.sp-toggleLg {\n height: 30px;\n padding: 0 16px;\n font-size: 13px;\n}\n\n.sp-toggleOn {\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 14%, transparent);\n color: var(--dsw-alias-state-error-primary);\n border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary) 45%, transparent);\n}\n\n.sp-toggleOff {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n\n.sp-toggleIcon {\n flex: none;\n}\n\n.sp-toggleDesc {\n flex: 1;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* handyman 运行状态区块 */\n\n.sp-handymenToggle {\n flex: none;\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 11px;\n cursor: pointer;\n}\n\n.sp-handymenToggle:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.sp-handymenList {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin-top: 2px;\n}\n\n.sp-handymanItem {\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 8px 12px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n min-width: 0;\n}\n\n.sp-handymanHead {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n}\n\n.sp-handymanLabel {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 12px;\n font-weight: 500;\n color: var(--dsw-alias-label-primary);\n}\n\n.sp-handymanRound {\n flex: none;\n font-size: 11px;\n color: var(--dsw-alias-label-dimmed);\n}\n\n.sp-handymanTime {\n flex: none;\n font-size: 11px;\n color: var(--dsw-alias-label-dimmed);\n}\n\n.sp-handymanResp {\n font-size: 11px;\n line-height: 16px;\n color: var(--dsw-alias-label-secondary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.sp-handymenMore {\n font-size: 11px;\n color: var(--dsw-alias-label-dimmed);\n padding: 4px 12px;\n}\n\n/* 底部提示(配置入口引导) */\n.sp-hint {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n word-break: break-word;\n overflow-wrap: anywhere;\n padding: 8px 12px;\n border: 1px dashed var(--dsw-alias-border-l2);\n border-radius: 10px;\n}\n";
11
+ const css$3 = "/* SafeModePanel — 会话头部 Serenity 状态卡片(v1.22 UX 重构)\n * 卡片 → 点击 → 官方 Modal(body portal 遮罩 + 居中)\n * 模态内:tab 栏 + 分组(groupTitle 标题 + rowCard 行卡)——看齐官方 settings 设计语言\n * 遵循 web-styling.md:颜色只用 --dsw-alias-* 语义 token(明暗自适应);\n * 前缀 sp- 防冲突。 */\n\n/* ── 头部状态卡片(可点击) ── */\n\n.sp-card {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n height: 30px;\n padding: 0 8px 0 14px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n background: var(--dsw-alias-bg-module-platform);\n color: inherit;\n font-size: 12px;\n cursor: pointer;\n transition: background var(--ds-transition-duration-fast, 0.1s) var(--ds-ease-in-out, ease),\n border-color var(--ds-transition-duration-fast, 0.1s) var(--ds-ease-in-out, ease);\n}\n\n.sp-card:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n border-color: var(--dsw-alias-border-l2);\n}\n\n.sp-card:focus-visible {\n outline: 2px solid var(--dsw-alias-button-ghost-active-border);\n outline-offset: 1px;\n}\n\n.sp-card:disabled {\n opacity: 0.6;\n cursor: default;\n}\n\n/* 方案 O 盾牌版(v1.24.2):绿点 = Serenity 生效(恒绿);盾牌 = SAFE 状态\n * (琥珀空心盾 = SAFE OFF 未保护提醒 / 绿实心盾 = SAFE ON 保护中安心) */\n\n.sp-dot {\n flex: none;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: var(--dsw-alias-state-success-primary);\n}\n\n.sp-dotOff {\n background: var(--dsw-alias-label-dimmed);\n}\n\n.sp-cardInactive {\n opacity: 0.85;\n}\n\n.sp-cardInactive .sp-brand {\n color: var(--dsw-alias-label-secondary);\n font-weight: 500;\n}\n\n.sp-brand {\n color: var(--dsw-alias-label-primary);\n font-weight: 600;\n}\n\n.sp-ver {\n color: var(--dsw-alias-label-dimmed);\n font-size: 11px;\n font-family: var(--ds-font-family-code, monospace);\n}\n\n.sp-sep {\n flex: none;\n width: 1px;\n height: 14px;\n background: var(--dsw-alias-border-l1);\n}\n\n.sp-shield {\n display: inline-flex;\n width: 16px;\n height: 16px;\n flex: none;\n}\n\n.sp-shield svg {\n width: 16px;\n height: 16px;\n}\n\n.sp-statusWord {\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.02em;\n white-space: nowrap;\n}\n\n/* SAFE OFF(未保护):琥珀——提醒,非错误(用户自己的开关选择) */\n.sp-cardUnprotected .sp-shield {\n color: var(--dsw-alias-state-warning-primary);\n}\n\n.sp-cardUnprotected .sp-statusWord {\n color: var(--dsw-alias-state-warning-primary);\n}\n\n/* SAFE ON(保护中):绿——安心 */\n.sp-cardProtected .sp-shield {\n color: var(--dsw-alias-state-success-primary);\n}\n\n.sp-cardProtected .sp-shield path:first-child {\n fill: color-mix(in srgb, var(--dsw-alias-state-success-primary) 18%, transparent);\n}\n\n.sp-cardProtected .sp-statusWord {\n color: var(--dsw-alias-state-success-primary);\n}\n\n.sp-chev {\n color: var(--dsw-alias-label-dimmed);\n flex: none;\n}\n\n/* ── 官方 Modal 定制(宽度 + 内容紧凑) ── */\n\n.sp-modal {\n width: 520px;\n max-width: calc(100vw - 48px);\n}\n\n/* ── tab 栏 ── */\n\n.sp-tabs {\n display: flex;\n gap: 6px;\n padding: 0 2px 12px;\n border-bottom: 1px solid var(--dsw-alias-border-l1);\n}\n\n.sp-tab {\n flex: 1;\n padding: 8px 0;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n transition: background var(--ds-transition-duration-fast, 0.1s) var(--ds-ease-in-out, ease);\n}\n\n.sp-tab:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.sp-tabOn {\n background: var(--dsw-alias-interactive-bg-active, rgba(128, 128, 128, 0.2));\n color: var(--dsw-alias-label-primary);\n font-weight: 600;\n}\n\n/* ── 模态内容 ── */\n\n.sp-modalBody {\n display: flex;\n flex-direction: column;\n gap: 16px;\n padding-top: 14px;\n /* 内容超出视口时 body 区滚动(兜底——不再截断);整卡不滚动 */\n overflow-y: auto;\n max-height: calc(100vh - 200px);\n}\n\n/* ── 分组(官方 settings 语言:groupTitle + rows) ── */\n\n.sp-group {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.sp-groupHead {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n\n.sp-groupTitle {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n font-weight: 600;\n letter-spacing: 0.02em;\n text-transform: uppercase;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sp-rows {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n/* 行卡(rowCard:border + radius + padding;值与 label 分离、长内容可换行) */\n.sp-row {\n display: flex;\n align-items: baseline;\n gap: 12px;\n min-width: 0;\n padding: 8px 12px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-module);\n}\n\n.sp-label {\n flex: none;\n width: 76px;\n font-size: 12px;\n font-weight: 500;\n color: var(--dsw-alias-label-secondary);\n white-space: nowrap;\n}\n\n.sp-value {\n flex: 1;\n min-width: 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n font-family: var(--ds-font-family-code, monospace);\n word-break: break-all;\n overflow-wrap: anywhere;\n}\n\n/* 安全模式动作区 */\n.sp-actions {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 8px 12px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-module);\n}\n\n.sp-toggle {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 24px;\n padding: 0 10px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 6px;\n font-size: 12px;\n cursor: pointer;\n transition: background var(--ds-transition-duration-fast, 0.1s) var(--ds-ease-in-out, ease);\n}\n\n.sp-toggleLg {\n height: 30px;\n padding: 0 16px;\n font-size: 13px;\n}\n\n.sp-toggleOn {\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 14%, transparent);\n color: var(--dsw-alias-state-error-primary);\n border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary) 45%, transparent);\n}\n\n.sp-toggleOff {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n\n.sp-toggleIcon {\n flex: none;\n}\n\n.sp-toggleDesc {\n flex: 1;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* handyman 运行状态区块 */\n\n.sp-handymenToggle {\n flex: none;\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 11px;\n cursor: pointer;\n}\n\n.sp-handymenToggle:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.sp-handymenList {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin-top: 2px;\n}\n\n.sp-handymanItem {\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 8px 12px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n min-width: 0;\n}\n\n.sp-handymanHead {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n}\n\n.sp-handymanLabel {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 12px;\n font-weight: 500;\n color: var(--dsw-alias-label-primary);\n}\n\n.sp-handymanRound {\n flex: none;\n font-size: 11px;\n color: var(--dsw-alias-label-dimmed);\n}\n\n.sp-handymanTime {\n flex: none;\n font-size: 11px;\n color: var(--dsw-alias-label-dimmed);\n}\n\n.sp-handymanResp {\n font-size: 11px;\n line-height: 16px;\n color: var(--dsw-alias-label-secondary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.sp-handymenMore {\n font-size: 11px;\n color: var(--dsw-alias-label-dimmed);\n padding: 4px 12px;\n}\n\n/* 底部提示(配置入口引导) */\n.sp-hint {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n word-break: break-word;\n overflow-wrap: anywhere;\n padding: 8px 12px;\n border: 1px dashed var(--dsw-alias-border-l2);\n border-radius: 10px;\n}\n";
12
12
  if (typeof document !== "undefined" && document.querySelector("style[data-sp-css=\"SafeModePanel\"]") === null) {
13
13
  const tag = document.createElement("style");
14
14
  tag.setAttribute("data-sp-css", "SafeModePanel");
@@ -19,6 +19,26 @@ window.__ModuleLoader__.load({
19
19
  //#region src/client/SafeModePanel.tsx
20
20
  const API = "/serenity/status";
21
21
  const HANDYMEN_API = "/serenity/handymen";
22
+ /** 盾牌 SVG(方案 O 盾牌版 v1.24.2;stroke currentColor——琥珀=OFF 提醒 / 绿=ON 安心由状态类控制) */
23
+ const SHIELD_SVG = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
24
+ viewBox: "0 0 16 16",
25
+ fill: "none",
26
+ width: 16,
27
+ height: 16,
28
+ "aria-hidden": "true",
29
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
30
+ d: "M8 1.5L13.5 3.5V7.2C13.5 10.6 11.2 13.6 8 14.8C4.8 13.6 2.5 10.6 2.5 7.2V3.5L8 1.5Z",
31
+ stroke: "currentColor",
32
+ strokeWidth: 1.3,
33
+ strokeLinejoin: "round"
34
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
35
+ d: "M5.8 8L7.3 9.5L10.2 6.6",
36
+ stroke: "currentColor",
37
+ strokeWidth: 1.2,
38
+ strokeLinecap: "round",
39
+ strokeLinejoin: "round"
40
+ })]
41
+ });
22
42
  /** 拼接类名(等价 clsx 最小子集,避免额外依赖) */
23
43
  function cx(...parts) {
24
44
  return parts.filter(Boolean).join(" ");
@@ -94,22 +114,32 @@ window.__ModuleLoader__.load({
94
114
  const runningHandymen = handymen.filter((l) => !l.done);
95
115
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
96
116
  type: "button",
97
- className: cx("sp-card"),
117
+ className: cx("sp-card", inCcc ? status.safeModeOn ? "sp-cardProtected" : "sp-cardUnprotected" : "sp-cardInactive"),
98
118
  "aria-haspopup": "dialog",
99
119
  "aria-expanded": open,
100
120
  onClick: () => setOpen(true),
101
- title: inCcc ? `Serenity v${status.accVersion} — ${status.root}${status.safeModeOn ? "(safe-mode ON" : "(safe-mode OFF"}` : "Serenity(未激活)",
121
+ title: inCcc ? `Serenity v${status.accVersion} — ${status.root}(${status.safeModeOn ? "SAFE ON — 写工具已隐藏" : "SAFE OFF — 写工具全开,无保护"})` : "Serenity(未激活)",
102
122
  children: [
103
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: cx("sp-dot", inCcc ? "sp-dotOn" : "sp-dotOff") }),
104
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
123
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: cx("sp-dot", inCcc ? void 0 : "sp-dotOff") }),
124
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
105
125
  className: cx("sp-brand"),
106
- children: ["Serenity v", status.accVersion]
126
+ children: "Serenity"
107
127
  }),
108
- inCcc && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
109
- className: cx("sp-tag", status.safeModeOn ? "sp-tagOn" : "sp-tagOff"),
110
- title: status.safeModeOn ? "safe-mode 已开启(隐藏写工具)" : "safe-mode 关闭",
111
- children: status.safeModeOn ? "SAFE" : "OFF"
128
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
129
+ className: cx("sp-ver"),
130
+ children: ["v", status.accVersion]
112
131
  }),
132
+ inCcc && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
133
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: cx("sp-sep") }),
134
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
135
+ className: cx("sp-shield"),
136
+ children: SHIELD_SVG
137
+ }),
138
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
139
+ className: cx("sp-statusWord"),
140
+ children: status.safeModeOn ? "SAFE ON" : "SAFE OFF"
141
+ })
142
+ ] }),
113
143
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {
114
144
  size: 12,
115
145
  className: cx("sp-chev")
@@ -308,7 +338,7 @@ window.__ModuleLoader__.load({
308
338
  return `The user provided ${paths.length} images:\n${paths.map((p) => `- ${p}`).join("\n")}`;
309
339
  }
310
340
  /** 浏览器 File → base64(与 ui-conversation serializeImages 等价的最小实现) */
311
- function fileToBase64(file) {
341
+ function fileToBase64$1(file) {
312
342
  return file.arrayBuffer().then((buffer) => {
313
343
  const bytes = new Uint8Array(buffer);
314
344
  let binary = "";
@@ -322,7 +352,7 @@ window.__ModuleLoader__.load({
322
352
  * sessionId 必传:node half 经会话 header.cwd 解析 CCC 根(进程 cwd 不可靠)。
323
353
  */
324
354
  async function uploadImage(file, sessionId) {
325
- const data = await fileToBase64(file);
355
+ const data = await fileToBase64$1(file);
326
356
  const res = await fetch(UPLOAD_PATH, {
327
357
  method: "POST",
328
358
  headers: {
@@ -417,6 +447,120 @@ window.__ModuleLoader__.load({
417
447
  return null;
418
448
  }
419
449
  //#endregion
450
+ //#region src/client/file-fallback-api.ts
451
+ /**
452
+ * file-fallback-api.ts — 任意文件粘贴自动落盘的浏览器操作面(v1.24.1)
453
+ *
454
+ * 图片链路(v1.20)是「发送失败补救」被动式;任意文件 DSH 原生发不了
455
+ * (InputBar.onPaste 把所有文件交给 intakeImages → 非图片被 addImages 拒绝),
456
+ * 因此本模块提供**主动拦截**面:
457
+ * - collectNonImageFiles(data) → 剪贴板中非图片 File(纯函数,可单测)
458
+ * - uploadFile(file, sessionId) → POST /serenity/file-upload(node half 写 _tmp/files_from_user/)
459
+ * - fileNoteTemplate(paths) → 消息模板(draft 追加,随发送进消息——用户拍板)
460
+ */
461
+ /** 文件落盘接口路径(node half api.ts,client 专属 x-serenity-ui 头) */
462
+ const FILE_UPLOAD_PATH = "/serenity/file-upload";
463
+ /**
464
+ * 从剪贴板收集非图片文件(图片交给 DSH 原生 rail 链路;仅非图片需要落盘)。
465
+ * 纯函数(items 可注入)——jsdom 环境下 DataTransfer 不可用,测试注入 items 数组。
466
+ */
467
+ function collectNonImageFiles(items) {
468
+ const out = [];
469
+ for (const item of items) {
470
+ if (item?.kind !== "file") continue;
471
+ if (typeof item.type === "string" && item.type.startsWith("image/")) continue;
472
+ const file = typeof item.getAsFile === "function" ? item.getAsFile() : null;
473
+ if (file !== null) out.push(file);
474
+ }
475
+ return out;
476
+ }
477
+ /**
478
+ * 文件提示消息模板(协议固有,用户拍板:draft 追加 + 对话里写名具体路径,agent 直接可用):
479
+ * 单文件:The user provided a file (path: ...);多文件:每行一条路径
480
+ */
481
+ function fileNoteTemplate(paths) {
482
+ if (paths.length === 1) return `The user provided a file (path: ${paths[0]})`;
483
+ return `The user provided ${paths.length} files:\n${paths.map((p) => `- ${p}`).join("\n")}`;
484
+ }
485
+ /** 浏览器 File → base64(与 image-fallback-api fileToBase64 等价的最小实现,保持模块独立) */
486
+ function fileToBase64(file) {
487
+ return file.arrayBuffer().then((buffer) => {
488
+ const bytes = new Uint8Array(buffer);
489
+ let binary = "";
490
+ const chunk = 32768;
491
+ for (let offset = 0; offset < bytes.length; offset += chunk) binary += String.fromCharCode(...bytes.subarray(offset, offset + chunk));
492
+ return btoa(binary);
493
+ });
494
+ }
495
+ /**
496
+ * 上传一个文件到 CCC _tmp/files_from_user/,返回相对路径(如 _tmp/files_from_user/<ts>-<rand>-x.pdf)。
497
+ * sessionId 必传:node half 经会话 header.cwd 解析 CCC 根(进程 cwd 不可靠)。
498
+ */
499
+ async function uploadFile(file, sessionId) {
500
+ const data = await fileToBase64(file);
501
+ const res = await fetch(FILE_UPLOAD_PATH, {
502
+ method: "POST",
503
+ headers: {
504
+ "content-type": "application/json",
505
+ "x-serenity-ui": "1"
506
+ },
507
+ body: JSON.stringify({
508
+ sessionId,
509
+ name: file.name,
510
+ data
511
+ })
512
+ });
513
+ const body = await res.json();
514
+ if (!res.ok || typeof body.path !== "string") throw new Error(`serenity file upload failed: ${body.error ?? res.status}`);
515
+ return body.path;
516
+ }
517
+ //#endregion
518
+ //#region src/client/FileFallbackDock.tsx
519
+ /** input.dock 条目(静默):非图片文件粘贴自动落盘 + draft 追加,无 UI */
520
+ function FileFallbackDock(props) {
521
+ const zone = props;
522
+ const sessionId = props.sessionId;
523
+ const { uploadFile } = props;
524
+ const inputActions = props.inputActions;
525
+ const inputRef = (0, react.useRef)(zone.input);
526
+ inputRef.current = zone.input;
527
+ const pendingRef = (0, react.useRef)([]);
528
+ (0, react.useEffect)(() => {
529
+ const handler = (e) => {
530
+ const data = e.clipboardData;
531
+ if (!data) return;
532
+ const items = Array.from(data.items);
533
+ const files = collectNonImageFiles(items);
534
+ if (files.length === 0) return;
535
+ const hasImage = items.some((item) => item.kind === "file" && typeof item.type === "string" && item.type.startsWith("image/"));
536
+ const hasText = data.getData("text/plain") !== "";
537
+ if (!hasImage && !hasText) e.preventDefault();
538
+ (async () => {
539
+ try {
540
+ const saved = [];
541
+ for (const file of files) saved.push(await uploadFile(file, String(sessionId)));
542
+ const note = fileNoteTemplate(saved);
543
+ const base = inputRef.current?.draft ?? "";
544
+ pendingRef.current.push(note);
545
+ const text = [base, ...pendingRef.current].filter(Boolean).join("\n");
546
+ pendingRef.current = [];
547
+ if (inputActions?.setDraft !== void 0) inputActions.setDraft(text);
548
+ else console.warn("[serenity] file fallback: inputActions.setDraft unavailable — file saved but draft not annotated");
549
+ } catch (err) {
550
+ console.warn(`[serenity] file fallback failed: ${String(err?.message ?? err)}`);
551
+ }
552
+ })();
553
+ };
554
+ document.addEventListener("paste", handler, true);
555
+ return () => document.removeEventListener("paste", handler, true);
556
+ }, [
557
+ sessionId,
558
+ uploadFile,
559
+ inputActions
560
+ ]);
561
+ return null;
562
+ }
563
+ //#endregion
420
564
  //#region src/client/accounts-api.ts
421
565
  /** wire 账号 → 本地编辑行 */
422
566
  function accountDraftFromWire(a) {
@@ -1301,6 +1445,12 @@ Instruction-following style:
1301
1445
  resendText: (sessionId, text) => resendText(scope, sessionId, text)
1302
1446
  })
1303
1447
  }, ImageFallbackDock), "serenity: image fallback dock");
1448
+ scope.effect(() => scope.slots.register({
1449
+ name: "conversation.input.dock",
1450
+ id: "serenity-file-fallback",
1451
+ order: 110,
1452
+ inject: () => ({ uploadFile: (file, sessionId) => uploadFile(file, sessionId) })
1453
+ }, FileFallbackDock), "serenity: file fallback dock");
1304
1454
  const serenityScope = scope.get("settingsScope").bind(SERENITY_SCOPE_SPEC);
1305
1455
  scope.effect(() => scope.slots.register({
1306
1456
  name: "settings.section",
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
2
  import { defineTool } from "@deepseek-ai/dsh-tools";
3
3
  import { appendFileSync, chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
4
- import { basename, dirname, join, relative, resolve } from "node:path";
4
+ import { basename, dirname, extname, join, relative, resolve } from "node:path";
5
5
  import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
6
6
  import { homedir, platform } from "node:os";
7
7
  import { fileURLToPath } from "node:url";
@@ -7514,6 +7514,7 @@ function registerCompactRetention(ctx, opts = {}) {
7514
7514
  const ROUTE_PATH = "/serenity/status";
7515
7515
  const HANDYMEN_PATH = "/serenity/handymen";
7516
7516
  const UPLOAD_PATH = "/serenity/image-upload";
7517
+ const FILE_UPLOAD_PATH = "/serenity/file-upload";
7517
7518
  const CONFIG_PATH = "/serenity/config";
7518
7519
  /** 图片落盘目录(CCC 根相对;S142 图片自动识别基础设施——粘贴图片落盘供 agent 经 CCC vlm MSM 自主处理) */
7519
7520
  const IMAGE_UPLOAD_DIR = "_tmp/images_from_user";
@@ -7530,6 +7531,28 @@ const EXT_BY_MEDIA = {
7530
7531
  "image/gif": "gif"
7531
7532
  };
7532
7533
  const MAX_IMAGE_BYTES = 10485760;
7534
+ /** 文件落盘目录(CCC 根相对;v1.24.1 粘贴任意文件自动落盘——agent 经 CCC 既有 MSM(pdf-extract/archive-extract 等)自主处理) */
7535
+ const FILE_UPLOAD_DIR = "_tmp/files_from_user";
7536
+ /** 拒绝的可执行/危险扩展名(安全边界:不落盘可执行文件,防 agent 被诱导执行) */
7537
+ const BLOCKED_FILE_EXTS = /* @__PURE__ */ new Set([
7538
+ "exe",
7539
+ "dll",
7540
+ "msi",
7541
+ "bat",
7542
+ "cmd",
7543
+ "ps1",
7544
+ "com",
7545
+ "scr",
7546
+ "lnk",
7547
+ "sh",
7548
+ "vbs",
7549
+ "bin",
7550
+ "app",
7551
+ "deb",
7552
+ "rpm",
7553
+ "jar"
7554
+ ]);
7555
+ const MAX_FILE_BYTES = 10485760;
7533
7556
  /**
7534
7557
  * 图片落盘核心逻辑(可测):校验 mediaType 白名单 + base64 解码 + 大小上限 →
7535
7558
  * 写 CCC 根 _tmp/images_from_user/<ts>-<rand>.<ext>,返回相对路径。
@@ -7547,6 +7570,34 @@ function saveImageToTmp(root, mediaType, data) {
7547
7570
  writeFileSync(join(dir, filename), bytes);
7548
7571
  return `${IMAGE_UPLOAD_DIR}/${filename}`;
7549
7572
  }
7573
+ /**
7574
+ * 文件名脱敏(路径逃逸 + 非法字符):取 basename(去 / 与 \),剥离前导点/空,
7575
+ * 非法字符 → '-',限长。返回空则 'file'。
7576
+ */
7577
+ function sanitizeFileName(name) {
7578
+ const cleaned = (name.split(/[\\/]/).pop() ?? "").replace(/[<>:"|?*\u0000-\u001f]/g, "-").replace(/^\.+/, "").trim();
7579
+ if (cleaned === "") return "file";
7580
+ return cleaned.slice(0, 100);
7581
+ }
7582
+ /**
7583
+ * 任意文件落盘核心逻辑(可测):文件名校验 + 可执行扩展名拒绝 + base64 解码 +
7584
+ * 大小上限(10MB)→ 写 CCC 根 _tmp/files_from_user/<ts>-<rand>-<safeName>,返回相对路径。
7585
+ * 校验失败抛 Error(handler 转 400)。
7586
+ */
7587
+ function saveFileToTmp(root, fileName, data) {
7588
+ if (typeof fileName !== "string" || fileName.length === 0) throw new Error("missing file name");
7589
+ const ext = extname(fileName).slice(1).toLowerCase();
7590
+ if (ext !== "" && BLOCKED_FILE_EXTS.has(ext)) throw new Error(`blocked executable file type: .${ext}`);
7591
+ if (typeof data !== "string" || data.length === 0) throw new Error("missing file data");
7592
+ const bytes = Buffer.from(data, "base64");
7593
+ if (bytes.length === 0 || bytes.length > MAX_FILE_BYTES) throw new Error(`file size out of range: ${bytes.length} bytes (max ${MAX_FILE_BYTES})`);
7594
+ const dir = join(root, FILE_UPLOAD_DIR);
7595
+ mkdirSync(dir, { recursive: true });
7596
+ const safeName = sanitizeFileName(fileName);
7597
+ const filename = `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${Math.random().toString(36).slice(2, 8)}-${safeName}`;
7598
+ writeFileSync(join(dir, filename), bytes);
7599
+ return `${FILE_UPLOAD_DIR}/${filename}`;
7600
+ }
7550
7601
  function readBody$1(req, maxBytes) {
7551
7602
  return new Promise((resolve, reject) => {
7552
7603
  let data = "";
@@ -7647,6 +7698,36 @@ function registerStatusApi(ctx, opts = {}) {
7647
7698
  }
7648
7699
  }
7649
7700
  });
7701
+ ctx.webServer.register({
7702
+ kind: "exact",
7703
+ path: FILE_UPLOAD_PATH,
7704
+ handler: async (req, res) => {
7705
+ try {
7706
+ if (req.method !== "POST") {
7707
+ sendJson(res, 405, { error: "method not allowed" });
7708
+ return;
7709
+ }
7710
+ if (req.headers["x-serenity-ui"] !== "1") {
7711
+ sendJson(res, 403, { error: "文件落盘仅限 WebUI(client 专用)" });
7712
+ return;
7713
+ }
7714
+ const raw = await readBody$1(req, 20971520);
7715
+ const body = JSON.parse(raw);
7716
+ const workspace = resolveWorkspace(ctx, {
7717
+ sessionId: body.sessionId,
7718
+ workspace: body.workspace
7719
+ });
7720
+ const root = findSerenityRoot(workspace);
7721
+ if (!root) {
7722
+ sendJson(res, 404, { error: `no CCC found from workspace: ${workspace}` });
7723
+ return;
7724
+ }
7725
+ sendJson(res, 200, { path: saveFileToTmp(root, body.name ?? "", body.data ?? "") });
7726
+ } catch (err) {
7727
+ sendJson(res, 400, { error: err.message ?? String(err) });
7728
+ }
7729
+ }
7730
+ });
7650
7731
  ctx.webServer.register({
7651
7732
  kind: "exact",
7652
7733
  path: ROUTE_PATH,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.24.0",
3
+ "version": "1.24.2",
4
4
  "description": "宁静号 ACC harness — Native Cordis 插件(DeepSeek Harness 运行时)。真实 DSH 工具注册(cc_fs/session/acc_msm 等 9 工具)+ 拦截缝机械约束(safe-mode/路径守卫/会话落盘)+ 系统提示词注入(ACC/CCE/Constraints/SKILL/Session 五块)。适配 DSH 公开版(deepseek-ai/deepseek-harness 0.1.0-rc)。",
5
5
  "license": "MIT",
6
6
  "repository": {