@shgroup/dsh-serenity-hooks 1.23.8 → 1.24.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/README.md CHANGED
@@ -14,7 +14,7 @@ dsh plugin --profile web add @shgroup/dsh-serenity-hooks
14
14
 
15
15
  ## 工具
16
16
 
17
- `cc_fs` / `session` / `acc_kit` / `cc_git` / `acc_msm` / `eap` / `neat` / `cce` / `loop`
17
+ `cc_fs` / `session` / `acc_kit` / `cc_git` / `acc_msm` / `eap` / `neat` / `cce` / `handyman`
18
18
 
19
19
  ## 系统提示词
20
20
 
@@ -26,7 +26,7 @@ dsh plugin --profile web add @shgroup/dsh-serenity-hooks
26
26
 
27
27
  ```jsonc
28
28
  {
29
- "loop": { "defaultModel": "provider/model" },
29
+ "handyman": { "models": ["provider/model"], "defaultModel": "provider/model" },
30
30
  "sessionKeeper": { "threshold": 100 },
31
31
  "safeMode": { "blacklist": [".secrets/", "regex:\\.env$"] }
32
32
  }
package/dsh.plugin.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "id": "dsh-serenity-hooks",
3
- "version": "1.23.8",
3
+ "version": "1.24.1",
4
4
  "main": "lib/index.js",
5
- "description": "宁静号 ACC harness(Native Cordis 插件):真实 DSH 工具 cc_fs/session/acc_msm/eap/neat/cce/loop/session_rebuild + 拦截缝机械约束(safe-mode/路径守卫)+ 高级设定面板(双端口网关/账号管理)。适配 DSH 公开版(0.1.0-rc,deepseek-ai/deepseek-harness)。",
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": {
7
7
  "dsh": ">=0.1.0-rc.5"
8
8
  },
@@ -16,7 +16,7 @@
16
16
  "eap",
17
17
  "neat",
18
18
  "cce",
19
- "loop",
19
+ "handyman",
20
20
  "session_rebuild",
21
21
  "localstore"
22
22
  ],
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/* loop 运行状态区块 */\n\n.sp-loopsToggle {\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-loopsToggle:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.sp-loopsList {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin-top: 2px;\n}\n\n.sp-loopItem {\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-loopHead {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n}\n\n.sp-loopLabel {\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-loopRound {\n flex: none;\n font-size: 11px;\n color: var(--dsw-alias-label-dimmed);\n}\n\n.sp-loopTime {\n flex: none;\n font-size: 11px;\n color: var(--dsw-alias-label-dimmed);\n}\n\n.sp-loopResp {\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-loopsMore {\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: 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";
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");
@@ -18,7 +18,7 @@ window.__ModuleLoader__.load({
18
18
  //#endregion
19
19
  //#region src/client/SafeModePanel.tsx
20
20
  const API = "/serenity/status";
21
- const LOOPS_API = "/serenity/loops";
21
+ const HANDYMEN_API = "/serenity/handymen";
22
22
  /** 拼接类名(等价 clsx 最小子集,避免额外依赖) */
23
23
  function cx(...parts) {
24
24
  return parts.filter(Boolean).join(" ");
@@ -28,8 +28,8 @@ window.__ModuleLoader__.load({
28
28
  const [status, setStatus] = (0, react.useState)(null);
29
29
  const [busy, setBusy] = (0, react.useState)(false);
30
30
  const [open, setOpen] = (0, react.useState)(false);
31
- const [loops, setLoops] = (0, react.useState)([]);
32
- const [loopsOpen, setLoopsOpen] = (0, react.useState)(false);
31
+ const [handymen, setHandymen] = (0, react.useState)([]);
32
+ const [handymenOpen, setHandymenOpen] = (0, react.useState)(false);
33
33
  const sessionId = props.sessionId;
34
34
  const refresh = (0, react.useCallback)(async () => {
35
35
  const qs = sessionId ? `?sessionId=${encodeURIComponent(sessionId)}` : "";
@@ -49,10 +49,10 @@ window.__ModuleLoader__.load({
49
49
  const qs = sessionId ? `?sessionId=${encodeURIComponent(sessionId)}` : "";
50
50
  const tick = async () => {
51
51
  try {
52
- const data = await (await fetch(`${LOOPS_API}${qs}`, { headers: { accept: "application/json" } })).json();
53
- if (alive) setLoops(data.loops ?? []);
52
+ const data = await (await fetch(`${HANDYMEN_API}${qs}`, { headers: { accept: "application/json" } })).json();
53
+ if (alive) setHandymen(data.handymen ?? []);
54
54
  } catch {
55
- if (alive) setLoops([]);
55
+ if (alive) setHandymen([]);
56
56
  }
57
57
  };
58
58
  tick();
@@ -91,7 +91,7 @@ window.__ModuleLoader__.load({
91
91
  })]
92
92
  });
93
93
  const inCcc = status.root !== null;
94
- const runningLoops = loops.filter((l) => !l.done);
94
+ const runningHandymen = handymen.filter((l) => !l.done);
95
95
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
96
96
  type: "button",
97
97
  className: cx("sp-card"),
@@ -147,11 +147,11 @@ window.__ModuleLoader__.load({
147
147
  className: cx("sp-row"),
148
148
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
149
149
  className: cx("sp-label"),
150
- children: "Loop 模型"
150
+ children: "Handyman 模型"
151
151
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
152
152
  className: cx("sp-value"),
153
- title: status.loopModel ? `loop.defaultModel: ${status.loopModel}` : void 0,
154
- children: status.loopModel ?? "未配置"
153
+ title: status.handymanModel ? `handyman.models: ${status.handymanModel}` : void 0,
154
+ children: status.handymanModel ?? "未配置"
155
155
  })]
156
156
  }),
157
157
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -205,12 +205,12 @@ window.__ModuleLoader__.load({
205
205
  className: cx("sp-groupHead"),
206
206
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
207
207
  className: cx("sp-groupTitle"),
208
- children: "Loop 运行"
209
- }), loops.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
208
+ children: "Handyman 运行"
209
+ }), handymen.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
210
210
  type: "button",
211
- className: cx("sp-loopsToggle"),
212
- onClick: () => setLoopsOpen((v) => !v),
213
- children: loopsOpen ? "收起" : "详情"
211
+ className: cx("sp-handymenToggle"),
212
+ onClick: () => setHandymenOpen((v) => !v),
213
+ children: handymenOpen ? "收起" : "详情"
214
214
  })]
215
215
  }),
216
216
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -222,42 +222,42 @@ window.__ModuleLoader__.load({
222
222
  children: "状态"
223
223
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
224
224
  className: cx("sp-value"),
225
- children: runningLoops.length > 0 ? `运行中 ${runningLoops.length}${loops.length > 0 ? ` / 共 ${loops.length}` : ""}` : "无运行中 loop"
225
+ children: runningHandymen.length > 0 ? `运行中 ${runningHandymen.length}${handymen.length > 0 ? ` / 共 ${handymen.length}` : ""}` : "无运行中 handyman"
226
226
  })]
227
227
  })
228
228
  }),
229
- loopsOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
230
- className: cx("sp-loopsList"),
231
- children: [loops.slice(0, 3).map((l) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
232
- className: cx("sp-loopItem"),
229
+ handymenOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
230
+ className: cx("sp-handymenList"),
231
+ children: [handymen.slice(0, 3).map((l) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
232
+ className: cx("sp-handymanItem"),
233
233
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
234
- className: cx("sp-loopHead"),
234
+ className: cx("sp-handymanHead"),
235
235
  children: [
236
236
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
237
- className: cx("sp-loopLabel"),
237
+ className: cx("sp-handymanLabel"),
238
238
  title: l.label,
239
239
  children: l.label
240
240
  }),
241
241
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
242
- className: cx("sp-loopRound"),
242
+ className: cx("sp-handymanRound"),
243
243
  children: l.done ? "✓ 完成" : `R${l.round}`
244
244
  }),
245
245
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
246
- className: cx("sp-loopTime"),
246
+ className: cx("sp-handymanTime"),
247
247
  children: new Date(l.updated).toLocaleTimeString()
248
248
  })
249
249
  ]
250
250
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
251
- className: cx("sp-loopResp"),
251
+ className: cx("sp-handymanResp"),
252
252
  title: l.lastResponse,
253
253
  children: l.lastResponse.slice(0, 80) || (l.done ? "已完成" : "(等待首轮响应)")
254
254
  })]
255
- }, l.label)), loops.length > 3 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
256
- className: cx("sp-loopsMore"),
255
+ }, l.label)), handymen.length > 3 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
256
+ className: cx("sp-handymenMore"),
257
257
  children: [
258
258
  "… 共 ",
259
- loops.length,
260
- " 个 loop(其余见等待界面)"
259
+ handymen.length,
260
+ " 个 handyman(其余见等待界面)"
261
261
  ]
262
262
  })]
263
263
  })
@@ -308,7 +308,7 @@ window.__ModuleLoader__.load({
308
308
  return `The user provided ${paths.length} images:\n${paths.map((p) => `- ${p}`).join("\n")}`;
309
309
  }
310
310
  /** 浏览器 File → base64(与 ui-conversation serializeImages 等价的最小实现) */
311
- function fileToBase64(file) {
311
+ function fileToBase64$1(file) {
312
312
  return file.arrayBuffer().then((buffer) => {
313
313
  const bytes = new Uint8Array(buffer);
314
314
  let binary = "";
@@ -322,7 +322,7 @@ window.__ModuleLoader__.load({
322
322
  * sessionId 必传:node half 经会话 header.cwd 解析 CCC 根(进程 cwd 不可靠)。
323
323
  */
324
324
  async function uploadImage(file, sessionId) {
325
- const data = await fileToBase64(file);
325
+ const data = await fileToBase64$1(file);
326
326
  const res = await fetch(UPLOAD_PATH, {
327
327
  method: "POST",
328
328
  headers: {
@@ -417,6 +417,120 @@ window.__ModuleLoader__.load({
417
417
  return null;
418
418
  }
419
419
  //#endregion
420
+ //#region src/client/file-fallback-api.ts
421
+ /**
422
+ * file-fallback-api.ts — 任意文件粘贴自动落盘的浏览器操作面(v1.24.1)
423
+ *
424
+ * 图片链路(v1.20)是「发送失败补救」被动式;任意文件 DSH 原生发不了
425
+ * (InputBar.onPaste 把所有文件交给 intakeImages → 非图片被 addImages 拒绝),
426
+ * 因此本模块提供**主动拦截**面:
427
+ * - collectNonImageFiles(data) → 剪贴板中非图片 File(纯函数,可单测)
428
+ * - uploadFile(file, sessionId) → POST /serenity/file-upload(node half 写 _tmp/files_from_user/)
429
+ * - fileNoteTemplate(paths) → 消息模板(draft 追加,随发送进消息——用户拍板)
430
+ */
431
+ /** 文件落盘接口路径(node half api.ts,client 专属 x-serenity-ui 头) */
432
+ const FILE_UPLOAD_PATH = "/serenity/file-upload";
433
+ /**
434
+ * 从剪贴板收集非图片文件(图片交给 DSH 原生 rail 链路;仅非图片需要落盘)。
435
+ * 纯函数(items 可注入)——jsdom 环境下 DataTransfer 不可用,测试注入 items 数组。
436
+ */
437
+ function collectNonImageFiles(items) {
438
+ const out = [];
439
+ for (const item of items) {
440
+ if (item?.kind !== "file") continue;
441
+ if (typeof item.type === "string" && item.type.startsWith("image/")) continue;
442
+ const file = typeof item.getAsFile === "function" ? item.getAsFile() : null;
443
+ if (file !== null) out.push(file);
444
+ }
445
+ return out;
446
+ }
447
+ /**
448
+ * 文件提示消息模板(协议固有,用户拍板:draft 追加 + 对话里写名具体路径,agent 直接可用):
449
+ * 单文件:The user provided a file (path: ...);多文件:每行一条路径
450
+ */
451
+ function fileNoteTemplate(paths) {
452
+ if (paths.length === 1) return `The user provided a file (path: ${paths[0]})`;
453
+ return `The user provided ${paths.length} files:\n${paths.map((p) => `- ${p}`).join("\n")}`;
454
+ }
455
+ /** 浏览器 File → base64(与 image-fallback-api fileToBase64 等价的最小实现,保持模块独立) */
456
+ function fileToBase64(file) {
457
+ return file.arrayBuffer().then((buffer) => {
458
+ const bytes = new Uint8Array(buffer);
459
+ let binary = "";
460
+ const chunk = 32768;
461
+ for (let offset = 0; offset < bytes.length; offset += chunk) binary += String.fromCharCode(...bytes.subarray(offset, offset + chunk));
462
+ return btoa(binary);
463
+ });
464
+ }
465
+ /**
466
+ * 上传一个文件到 CCC _tmp/files_from_user/,返回相对路径(如 _tmp/files_from_user/<ts>-<rand>-x.pdf)。
467
+ * sessionId 必传:node half 经会话 header.cwd 解析 CCC 根(进程 cwd 不可靠)。
468
+ */
469
+ async function uploadFile(file, sessionId) {
470
+ const data = await fileToBase64(file);
471
+ const res = await fetch(FILE_UPLOAD_PATH, {
472
+ method: "POST",
473
+ headers: {
474
+ "content-type": "application/json",
475
+ "x-serenity-ui": "1"
476
+ },
477
+ body: JSON.stringify({
478
+ sessionId,
479
+ name: file.name,
480
+ data
481
+ })
482
+ });
483
+ const body = await res.json();
484
+ if (!res.ok || typeof body.path !== "string") throw new Error(`serenity file upload failed: ${body.error ?? res.status}`);
485
+ return body.path;
486
+ }
487
+ //#endregion
488
+ //#region src/client/FileFallbackDock.tsx
489
+ /** input.dock 条目(静默):非图片文件粘贴自动落盘 + draft 追加,无 UI */
490
+ function FileFallbackDock(props) {
491
+ const zone = props;
492
+ const sessionId = props.sessionId;
493
+ const { uploadFile } = props;
494
+ const inputActions = props.inputActions;
495
+ const inputRef = (0, react.useRef)(zone.input);
496
+ inputRef.current = zone.input;
497
+ const pendingRef = (0, react.useRef)([]);
498
+ (0, react.useEffect)(() => {
499
+ const handler = (e) => {
500
+ const data = e.clipboardData;
501
+ if (!data) return;
502
+ const items = Array.from(data.items);
503
+ const files = collectNonImageFiles(items);
504
+ if (files.length === 0) return;
505
+ const hasImage = items.some((item) => item.kind === "file" && typeof item.type === "string" && item.type.startsWith("image/"));
506
+ const hasText = data.getData("text/plain") !== "";
507
+ if (!hasImage && !hasText) e.preventDefault();
508
+ (async () => {
509
+ try {
510
+ const saved = [];
511
+ for (const file of files) saved.push(await uploadFile(file, String(sessionId)));
512
+ const note = fileNoteTemplate(saved);
513
+ const base = inputRef.current?.draft ?? "";
514
+ pendingRef.current.push(note);
515
+ const text = [base, ...pendingRef.current].filter(Boolean).join("\n");
516
+ pendingRef.current = [];
517
+ if (inputActions?.setDraft !== void 0) inputActions.setDraft(text);
518
+ else console.warn("[serenity] file fallback: inputActions.setDraft unavailable — file saved but draft not annotated");
519
+ } catch (err) {
520
+ console.warn(`[serenity] file fallback failed: ${String(err?.message ?? err)}`);
521
+ }
522
+ })();
523
+ };
524
+ document.addEventListener("paste", handler, true);
525
+ return () => document.removeEventListener("paste", handler, true);
526
+ }, [
527
+ sessionId,
528
+ uploadFile,
529
+ inputActions
530
+ ]);
531
+ return null;
532
+ }
533
+ //#endregion
420
534
  //#region src/client/accounts-api.ts
421
535
  /** wire 账号 → 本地编辑行 */
422
536
  function accountDraftFromWire(a) {
@@ -1301,6 +1415,12 @@ Instruction-following style:
1301
1415
  resendText: (sessionId, text) => resendText(scope, sessionId, text)
1302
1416
  })
1303
1417
  }, ImageFallbackDock), "serenity: image fallback dock");
1418
+ scope.effect(() => scope.slots.register({
1419
+ name: "conversation.input.dock",
1420
+ id: "serenity-file-fallback",
1421
+ order: 110,
1422
+ inject: () => ({ uploadFile: (file, sessionId) => uploadFile(file, sessionId) })
1423
+ }, FileFallbackDock), "serenity: file fallback dock");
1304
1424
  const serenityScope = scope.get("settingsScope").bind(SERENITY_SCOPE_SPEC);
1305
1425
  scope.effect(() => scope.slots.register({
1306
1426
  name: "settings.section",