@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 +1 -1
- package/lib/client.js +162 -12
- package/lib/index.js +82 -1
- 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.24.
|
|
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:
|
|
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}
|
|
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 ?
|
|
104
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
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:
|
|
126
|
+
children: "Serenity"
|
|
107
127
|
}),
|
|
108
|
-
|
|
109
|
-
className: cx("sp-
|
|
110
|
-
|
|
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.
|
|
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": {
|