@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 +2 -2
- package/dsh.plugin.json +3 -3
- package/lib/client.js +152 -32
- package/lib/index.js +391 -215
- package/lib/invariant.js +1 -1
- package/package.json +1 -1
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` / `
|
|
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
|
-
"
|
|
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.
|
|
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/
|
|
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
|
-
"
|
|
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/*
|
|
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
|
|
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 [
|
|
32
|
-
const [
|
|
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(`${
|
|
53
|
-
if (alive)
|
|
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)
|
|
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
|
|
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: "
|
|
150
|
+
children: "Handyman 模型"
|
|
151
151
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
152
152
|
className: cx("sp-value"),
|
|
153
|
-
title: status.
|
|
154
|
-
children: status.
|
|
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: "
|
|
209
|
-
}),
|
|
208
|
+
children: "Handyman 运行"
|
|
209
|
+
}), handymen.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
210
210
|
type: "button",
|
|
211
|
-
className: cx("sp-
|
|
212
|
-
onClick: () =>
|
|
213
|
-
children:
|
|
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:
|
|
225
|
+
children: runningHandymen.length > 0 ? `运行中 ${runningHandymen.length}${handymen.length > 0 ? ` / 共 ${handymen.length}` : ""}` : "无运行中 handyman"
|
|
226
226
|
})]
|
|
227
227
|
})
|
|
228
228
|
}),
|
|
229
|
-
|
|
230
|
-
className: cx("sp-
|
|
231
|
-
children: [
|
|
232
|
-
className: cx("sp-
|
|
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-
|
|
234
|
+
className: cx("sp-handymanHead"),
|
|
235
235
|
children: [
|
|
236
236
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
237
|
-
className: cx("sp-
|
|
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-
|
|
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-
|
|
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-
|
|
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)),
|
|
256
|
-
className: cx("sp-
|
|
255
|
+
}, l.label)), handymen.length > 3 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
256
|
+
className: cx("sp-handymenMore"),
|
|
257
257
|
children: [
|
|
258
258
|
"… 共 ",
|
|
259
|
-
|
|
260
|
-
" 个
|
|
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",
|