@shgroup/dsh-serenity-hooks 1.19.9 → 1.20.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/dsh.plugin.json +1 -1
- package/lib/client.js +159 -4
- package/lib/index.js +66 -3
- package/package.json +1 -1
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-serenity-hooks",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.1",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"description": "宁静号 ACC harness(Native Cordis 插件):真实 DSH 工具 cc_fs/session/acc_msm/eap/neat/cce/loop + 拦截缝机械约束(safe-mode/路径守卫)。适配 DSH 公开版(0.1.0-rc,deepseek-ai/deepseek-harness)。私有(dsh-external 组织)。",
|
|
6
6
|
"engines": {
|
package/lib/client.js
CHANGED
|
@@ -8,11 +8,11 @@ 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 = "/* SafeModePanel — 会话头部 Serenity 状态徽章(点击展开详情卡)。\n * 遵循 web-styling.md:颜色只用 --dsw-alias-* 语义 token(明暗自适应);\n * 尺寸与官方 header 标签一致;前缀 sp- 防冲突。 */\n\n.sp-root {\n position: relative;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 24px;\n padding: 0 4px 0 4px;\n border-radius: 6px;\n background: var(--dsw-alias-bg-module-platform);\n font-size: 12px;\n line-height: 24px;\n white-space: nowrap;\n}\n\n.sp-badge {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 22px;\n padding: 0 6px;\n border: none;\n border-radius: 5px;\n background: transparent;\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}\n\n.sp-badge:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.sp-badge:focus-visible {\n outline: 2px solid var(--dsw-alias-button-ghost-active-border);\n outline-offset: 1px;\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.sp-brand {\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n\n.sp-toggle {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n height: 18px;\n padding: 0 8px;\n border: none;\n border-radius: 5px;\n font-size: 12px;\n font-weight: 600;\n line-height: 18px;\n cursor: pointer;\n transition: background var(--ds-transition-duration-fast, 0.1s) var(--ds-ease-in-out, ease);\n}\n\n.sp-toggle:focus-visible {\n outline: 2px solid var(--dsw-alias-button-ghost-active-border);\n outline-offset: 1px;\n}\n\n.sp-toggle:disabled {\n opacity: 0.55;\n cursor: default;\n}\n\n.sp-toggleOn {\n background: var(--dsw-alias-state-error-primary);\n color: var(--dsw-alias-label-primary-foreground);\n}\n\n.sp-toggleOn:hover:not(:disabled) {\n background: var(--dsw-alias-state-error-secondary);\n}\n\n.sp-toggleOff {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n\n.sp-toggleOff:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover-solid);\n}\n\n.sp-toggleIcon {\n flex: none;\n}\n\n.sp-toggleLg {\n height: 26px;\n padding: 0 14px;\n font-size: 13px;\n line-height: 26px;\n border-radius: 6px;\n}\n\n/* 详情卡:相对徽章绝对定位(右对齐),卡片 token */\n.sp-pop {\n position: absolute;\n top: calc(100% + 8px);\n right: 0;\n z-index: 20;\n width: 300px;\n box-sizing: border-box;\n padding: 12px 14px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 12px;\n background: var(--dsw-specific-tip);\n box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.sp-popRow {\n display: flex;\n align-items: baseline;\n gap: 10px;\n min-width: 0;\n}\n\n.sp-popLabel {\n flex: none;\n width: 34px;\n font-size: 12px;\n color: var(--dsw-alias-label-caption);\n}\n\n.sp-popValue {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n font-family: var(--ds-font-family-code, monospace);\n}\n\n.sp-popActions {\n display: flex;\n justify-content: flex-end;\n padding-top: 4px;\n border-top: 1px solid var(--dsw-alias-border-l1);\n margin-top: 2px;\n}\n\n/* loop 运行状态区块(WebUI 等待界面;并行任务各自一行) */\n.sp-popSection {\n display: flex;\n flex-direction: column;\n gap: 4px;\n padding-top: 6px;\n border-top: 1px solid var(--dsw-alias-border-l1);\n margin-top: 2px;\n}\n\n.sp-loopItem {\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 2px 0 2px 44px;\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: 600;\n color: var(--dsw-alias-label-primary);\n}\n\n.sp-loopRound {\n flex: none;\n font-size: 11px;\n color: var(--dsw-alias-state-info-primary);\n font-family: var(--ds-font-family-code, monospace);\n}\n\n.sp-loopTime {\n flex: none;\n font-size: 11px;\n color: var(--dsw-alias-label-caption);\n}\n\n.sp-loopResp {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n font-family: var(--ds-font-family-code, monospace);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .sp-toggle,\n .sp-badge {\n transition: none;\n }\n}\n";
|
|
11
|
+
const css$1 = "/* SafeModePanel — 会话头部 Serenity 状态徽章(点击展开详情卡)。\n * 遵循 web-styling.md:颜色只用 --dsw-alias-* 语义 token(明暗自适应);\n * 尺寸与官方 header 标签一致;前缀 sp- 防冲突。 */\n\n.sp-root {\n position: relative;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 24px;\n padding: 0 4px 0 4px;\n border-radius: 6px;\n background: var(--dsw-alias-bg-module-platform);\n font-size: 12px;\n line-height: 24px;\n white-space: nowrap;\n}\n\n.sp-badge {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 22px;\n padding: 0 6px;\n border: none;\n border-radius: 5px;\n background: transparent;\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}\n\n.sp-badge:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.sp-badge:focus-visible {\n outline: 2px solid var(--dsw-alias-button-ghost-active-border);\n outline-offset: 1px;\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.sp-brand {\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n\n.sp-toggle {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n height: 18px;\n padding: 0 8px;\n border: none;\n border-radius: 5px;\n font-size: 12px;\n font-weight: 600;\n line-height: 18px;\n cursor: pointer;\n transition: background var(--ds-transition-duration-fast, 0.1s) var(--ds-ease-in-out, ease);\n}\n\n.sp-toggle:focus-visible {\n outline: 2px solid var(--dsw-alias-button-ghost-active-border);\n outline-offset: 1px;\n}\n\n.sp-toggle:disabled {\n opacity: 0.55;\n cursor: default;\n}\n\n.sp-toggleOn {\n background: var(--dsw-alias-state-error-primary);\n color: var(--dsw-alias-label-primary-foreground);\n}\n\n.sp-toggleOn:hover:not(:disabled) {\n background: var(--dsw-alias-state-error-secondary);\n}\n\n.sp-toggleOff {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n\n.sp-toggleOff:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover-solid);\n}\n\n.sp-toggleIcon {\n flex: none;\n}\n\n.sp-toggleLg {\n height: 26px;\n padding: 0 14px;\n font-size: 13px;\n line-height: 26px;\n border-radius: 6px;\n}\n\n/* 详情卡:相对徽章绝对定位(右对齐),卡片 token */\n.sp-pop {\n position: absolute;\n top: calc(100% + 8px);\n right: 0;\n z-index: 20;\n width: 300px;\n box-sizing: border-box;\n padding: 12px 14px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 12px;\n background: var(--dsw-specific-tip);\n box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.sp-popRow {\n display: flex;\n align-items: baseline;\n gap: 10px;\n min-width: 0;\n}\n\n.sp-popLabel {\n flex: none;\n width: 34px;\n font-size: 12px;\n color: var(--dsw-alias-label-caption);\n}\n\n.sp-popValue {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n font-family: var(--ds-font-family-code, monospace);\n}\n\n.sp-popActions {\n display: flex;\n justify-content: flex-end;\n padding-top: 4px;\n border-top: 1px solid var(--dsw-alias-border-l1);\n margin-top: 2px;\n}\n\n/* loop 运行状态区块(WebUI 等待界面;并行任务各自一行) */\n.sp-popSection {\n display: flex;\n flex-direction: column;\n gap: 4px;\n padding-top: 6px;\n border-top: 1px solid var(--dsw-alias-border-l1);\n margin-top: 2px;\n}\n\n.sp-loopItem {\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 2px 0 2px 44px;\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: 600;\n color: var(--dsw-alias-label-primary);\n}\n\n.sp-loopRound {\n flex: none;\n font-size: 11px;\n color: var(--dsw-alias-state-info-primary);\n font-family: var(--ds-font-family-code, monospace);\n}\n\n.sp-loopTime {\n flex: none;\n font-size: 11px;\n color: var(--dsw-alias-label-caption);\n}\n\n.sp-loopResp {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n font-family: var(--ds-font-family-code, monospace);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .sp-toggle,\n .sp-badge {\n transition: none;\n }\n}\n";
|
|
12
12
|
if (typeof document !== "undefined" && document.querySelector("style[data-sp-css]") === null) {
|
|
13
13
|
const tag = document.createElement("style");
|
|
14
14
|
tag.setAttribute("data-sp-css", "1");
|
|
15
|
-
tag.textContent = css;
|
|
15
|
+
tag.textContent = css$1;
|
|
16
16
|
document.head.appendChild(tag);
|
|
17
17
|
}
|
|
18
18
|
//#endregion
|
|
@@ -240,15 +240,170 @@ window.__ModuleLoader__.load({
|
|
|
240
240
|
});
|
|
241
241
|
}
|
|
242
242
|
//#endregion
|
|
243
|
+
//#region \0sp-css:/home/yh/home/home-serenity/AI_LAB/dsh-serenity-plugin/hooks/dsh-serenity-hooks/src/client/ImageFallbackDock
|
|
244
|
+
const css = "/* ImageFallbackDock — 图片自动落盘兜底状态条(S142) */\n\n.serenity-image-fallback {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n font-size: 12px;\n line-height: 1.5;\n padding: 2px 8px;\n border-radius: 6px;\n background: var(--dsw-alias-surface-muted, rgba(127, 127, 127, 0.1));\n color: var(--dsw-alias-text-secondary, currentColor);\n}\n\n.serenity-image-fallback[data-state='busy'] {\n color: var(--dsw-alias-text-secondary, currentColor);\n}\n\n.serenity-image-fallback[data-state='done'] {\n color: var(--dsw-alias-text-success, currentColor);\n}\n\n.serenity-image-fallback[data-state='error'] {\n color: var(--dsw-alias-text-danger, currentColor);\n}\n\n.serenity-image-fallback code {\n font-family: var(--dsw-alias-font-mono, monospace);\n font-size: 11px;\n background: var(--dsw-alias-surface-muted, rgba(127, 127, 127, 0.12));\n border-radius: 4px;\n padding: 0 4px;\n}\n";
|
|
245
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-sp-css]") === null) {
|
|
246
|
+
const tag = document.createElement("style");
|
|
247
|
+
tag.setAttribute("data-sp-css", "1");
|
|
248
|
+
tag.textContent = css;
|
|
249
|
+
document.head.appendChild(tag);
|
|
250
|
+
}
|
|
251
|
+
//#endregion
|
|
252
|
+
//#region src/client/ImageFallbackDock.tsx
|
|
253
|
+
/** input.dock 条目:图片发送失败自动落盘 + 文本重发 */
|
|
254
|
+
function ImageFallbackDock(props) {
|
|
255
|
+
const zone = props;
|
|
256
|
+
const session = zone.session;
|
|
257
|
+
const input = zone.input;
|
|
258
|
+
const sessionId = props.sessionId;
|
|
259
|
+
const { uploadImage, getDraftFiles, resendText } = props;
|
|
260
|
+
const [state, setState] = (0, react.useState)("idle");
|
|
261
|
+
const [paths, setPaths] = (0, react.useState)([]);
|
|
262
|
+
const [errorText, setErrorText] = (0, react.useState)(null);
|
|
263
|
+
const handlingRef = (0, react.useRef)(false);
|
|
264
|
+
const promptError = session?.promptError?.error;
|
|
265
|
+
const imageIds = input?.imageIds ?? [];
|
|
266
|
+
(0, react.useEffect)(() => {
|
|
267
|
+
if (promptError?.code !== "attachment-error") return;
|
|
268
|
+
if (promptError.details?.reason !== "MODEL_DOES_NOT_SUPPORT_IMAGES") return;
|
|
269
|
+
if (handlingRef.current || imageIds.length === 0) return;
|
|
270
|
+
handlingRef.current = true;
|
|
271
|
+
setState("uploading");
|
|
272
|
+
(async () => {
|
|
273
|
+
try {
|
|
274
|
+
const files = await getDraftFiles(String(sessionId), imageIds);
|
|
275
|
+
if (files.length === 0) throw new Error("no draft image files");
|
|
276
|
+
const saved = [];
|
|
277
|
+
for (const file of files) saved.push(await uploadImage(file, String(sessionId)));
|
|
278
|
+
const note = saved.map((p) => `用户提供了图片在 ${p}`).join("\n");
|
|
279
|
+
const draft = input?.draft ?? "";
|
|
280
|
+
const text = draft === "" ? note : `${draft}\n${note}`;
|
|
281
|
+
await resendText(String(sessionId), text);
|
|
282
|
+
setPaths(saved);
|
|
283
|
+
setState("done");
|
|
284
|
+
} catch (err) {
|
|
285
|
+
const message = String(err?.message ?? err);
|
|
286
|
+
console.warn(`[serenity] image fallback failed: ${message}`);
|
|
287
|
+
setErrorText(message);
|
|
288
|
+
setState("error");
|
|
289
|
+
} finally {
|
|
290
|
+
handlingRef.current = false;
|
|
291
|
+
}
|
|
292
|
+
})();
|
|
293
|
+
}, [
|
|
294
|
+
promptError,
|
|
295
|
+
imageIds,
|
|
296
|
+
sessionId,
|
|
297
|
+
uploadImage,
|
|
298
|
+
getDraftFiles,
|
|
299
|
+
resendText,
|
|
300
|
+
input?.draft
|
|
301
|
+
]);
|
|
302
|
+
if (state === "idle") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
303
|
+
className: "serenity-image-fallback",
|
|
304
|
+
"data-state": "idle"
|
|
305
|
+
});
|
|
306
|
+
if (state === "uploading") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
307
|
+
className: "serenity-image-fallback",
|
|
308
|
+
"data-state": "busy",
|
|
309
|
+
children: "图片已保存中…"
|
|
310
|
+
});
|
|
311
|
+
if (state === "done") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
312
|
+
className: "serenity-image-fallback",
|
|
313
|
+
"data-state": "done",
|
|
314
|
+
children: paths.map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: p }, p))
|
|
315
|
+
});
|
|
316
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
317
|
+
className: "serenity-image-fallback",
|
|
318
|
+
"data-state": "error",
|
|
319
|
+
children: ["图片保存失败:", errorText ?? "未知错误"]
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
//#endregion
|
|
323
|
+
//#region src/client/image-fallback-api.ts
|
|
324
|
+
/** 图片落盘接口路径(node half api.ts,client 专属 x-serenity-ui 头) */
|
|
325
|
+
const UPLOAD_PATH = "/serenity/image-upload";
|
|
326
|
+
/** 浏览器 File → base64(与 ui-conversation serializeImages 等价的最小实现) */
|
|
327
|
+
function fileToBase64(file) {
|
|
328
|
+
return file.arrayBuffer().then((buffer) => {
|
|
329
|
+
const bytes = new Uint8Array(buffer);
|
|
330
|
+
let binary = "";
|
|
331
|
+
const chunk = 32768;
|
|
332
|
+
for (let offset = 0; offset < bytes.length; offset += chunk) binary += String.fromCharCode(...bytes.subarray(offset, offset + chunk));
|
|
333
|
+
return btoa(binary);
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* 上传一张图片到 CCC _tmp/images_from_user/,返回相对路径(如 _tmp/images_from_user/xxx.png)。
|
|
338
|
+
* sessionId 必传:node half 经会话 header.cwd 解析 CCC 根(进程 cwd 不可靠)。
|
|
339
|
+
*/
|
|
340
|
+
async function uploadImage(file, sessionId) {
|
|
341
|
+
const data = await fileToBase64(file);
|
|
342
|
+
const res = await fetch(UPLOAD_PATH, {
|
|
343
|
+
method: "POST",
|
|
344
|
+
headers: {
|
|
345
|
+
"content-type": "application/json",
|
|
346
|
+
"x-serenity-ui": "1"
|
|
347
|
+
},
|
|
348
|
+
body: JSON.stringify({
|
|
349
|
+
sessionId,
|
|
350
|
+
mediaType: file.type,
|
|
351
|
+
name: file.name,
|
|
352
|
+
data
|
|
353
|
+
})
|
|
354
|
+
});
|
|
355
|
+
const body = await res.json();
|
|
356
|
+
if (!res.ok || typeof body.path !== "string") throw new Error(`serenity image upload failed: ${body.error ?? res.status}`);
|
|
357
|
+
return body.path;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* 取 rail 图片的浏览器 File。
|
|
361
|
+
* conversation.draftImages 是 root singleton 的公开方法(读 controller 的 draftAttachments Map,
|
|
362
|
+
* 与调用 ctx 的作用域无关)——直接 ctx.get('conversation'),避开 scope 寻址不确定性。
|
|
363
|
+
*/
|
|
364
|
+
async function getDraftFiles(ctx, _sessionId, ids) {
|
|
365
|
+
const draftImages = (ctx.get?.("conversation"))?.draftImages;
|
|
366
|
+
if (draftImages === void 0) return [];
|
|
367
|
+
return (draftImages(ids) ?? []).map((a) => a.file);
|
|
368
|
+
}
|
|
369
|
+
/** 纯文本重发(绕过图片门禁——不含 image part,模型永不触发 MODEL_DOES_NOT_SUPPORT_IMAGES) */
|
|
370
|
+
async function resendText(ctx, sessionId, text) {
|
|
371
|
+
const prompt = ctx.sessions.binding(sessionId)?.session?.prompt;
|
|
372
|
+
if (prompt === void 0) throw new Error("serenity image fallback: session unavailable");
|
|
373
|
+
const result = await prompt([{
|
|
374
|
+
type: "text",
|
|
375
|
+
text
|
|
376
|
+
}], "queue");
|
|
377
|
+
if (!result.ok) throw new Error(`serenity image fallback resend failed: ${result.error?.code}: ${result.error?.message}`);
|
|
378
|
+
}
|
|
379
|
+
//#endregion
|
|
243
380
|
//#region src/client/index.ts
|
|
244
|
-
const inject = [
|
|
381
|
+
const inject = [
|
|
382
|
+
"slots",
|
|
383
|
+
"conversation",
|
|
384
|
+
"sessions"
|
|
385
|
+
];
|
|
245
386
|
function apply(ctx) {
|
|
246
|
-
ctx.inject([
|
|
387
|
+
ctx.inject([
|
|
388
|
+
"slots",
|
|
389
|
+
"conversation",
|
|
390
|
+
"sessions"
|
|
391
|
+
], (scope) => {
|
|
247
392
|
scope.effect(() => scope.slots.register({
|
|
248
393
|
name: "conversation.session.header.actions",
|
|
249
394
|
id: "serenity-safe",
|
|
250
395
|
order: 10
|
|
251
396
|
}, SafeModePanel), "serenity: header status badge");
|
|
397
|
+
scope.effect(() => scope.slots.register({
|
|
398
|
+
name: "conversation.input.dock",
|
|
399
|
+
id: "serenity-image-fallback",
|
|
400
|
+
order: 100,
|
|
401
|
+
inject: () => ({
|
|
402
|
+
uploadImage: (file, sessionId) => uploadImage(file, sessionId),
|
|
403
|
+
getDraftFiles: (sessionId, ids) => getDraftFiles(scope, sessionId, ids),
|
|
404
|
+
resendText: (sessionId, text) => resendText(scope, sessionId, text)
|
|
405
|
+
})
|
|
406
|
+
}, ImageFallbackDock), "serenity: image fallback dock");
|
|
252
407
|
});
|
|
253
408
|
}
|
|
254
409
|
//#endregion
|
package/lib/index.js
CHANGED
|
@@ -4594,12 +4594,45 @@ function registerCompactRetention(ctx, opts = {}) {
|
|
|
4594
4594
|
//#region src/api.ts
|
|
4595
4595
|
const ROUTE_PATH = "/serenity/status";
|
|
4596
4596
|
const LOOPS_PATH = "/serenity/loops";
|
|
4597
|
-
|
|
4597
|
+
const UPLOAD_PATH = "/serenity/image-upload";
|
|
4598
|
+
/** 图片落盘目录(CCC 根相对;S142 图片自动识别基础设施——粘贴图片落盘供 agent 经 CCC vlm MSM 自主处理) */
|
|
4599
|
+
const IMAGE_UPLOAD_DIR = "_tmp/images_from_user";
|
|
4600
|
+
const IMAGE_MEDIA_TYPES = /* @__PURE__ */ new Set([
|
|
4601
|
+
"image/png",
|
|
4602
|
+
"image/jpeg",
|
|
4603
|
+
"image/webp",
|
|
4604
|
+
"image/gif"
|
|
4605
|
+
]);
|
|
4606
|
+
const EXT_BY_MEDIA = {
|
|
4607
|
+
"image/png": "png",
|
|
4608
|
+
"image/jpeg": "jpg",
|
|
4609
|
+
"image/webp": "webp",
|
|
4610
|
+
"image/gif": "gif"
|
|
4611
|
+
};
|
|
4612
|
+
const MAX_IMAGE_BYTES = 10485760;
|
|
4613
|
+
/**
|
|
4614
|
+
* 图片落盘核心逻辑(可测):校验 mediaType 白名单 + base64 解码 + 大小上限 →
|
|
4615
|
+
* 写 CCC 根 _tmp/images_from_user/<ts>-<rand>.<ext>,返回相对路径。
|
|
4616
|
+
* 校验失败抛 Error(handler 转 400)。
|
|
4617
|
+
*/
|
|
4618
|
+
function saveImageToTmp(root, mediaType, data) {
|
|
4619
|
+
if (!IMAGE_MEDIA_TYPES.has(mediaType)) throw new Error(`unsupported media type: ${mediaType}`);
|
|
4620
|
+
if (typeof data !== "string" || data.length === 0) throw new Error("missing image data");
|
|
4621
|
+
const bytes = Buffer.from(data, "base64");
|
|
4622
|
+
if (bytes.length === 0 || bytes.length > MAX_IMAGE_BYTES) throw new Error(`image size out of range: ${bytes.length} bytes (max ${MAX_IMAGE_BYTES})`);
|
|
4623
|
+
const dir = join(root, IMAGE_UPLOAD_DIR);
|
|
4624
|
+
mkdirSync(dir, { recursive: true });
|
|
4625
|
+
const ext = EXT_BY_MEDIA[mediaType] ?? "img";
|
|
4626
|
+
const filename = `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${Math.random().toString(36).slice(2, 8)}.${ext}`;
|
|
4627
|
+
writeFileSync(join(dir, filename), bytes);
|
|
4628
|
+
return `${IMAGE_UPLOAD_DIR}/${filename}`;
|
|
4629
|
+
}
|
|
4630
|
+
function readBody(req, maxBytes) {
|
|
4598
4631
|
return new Promise((resolve, reject) => {
|
|
4599
4632
|
let data = "";
|
|
4600
4633
|
req.on("data", (chunk) => {
|
|
4601
4634
|
data += chunk.toString("utf-8");
|
|
4602
|
-
if (data.length >
|
|
4635
|
+
if (data.length > maxBytes) {
|
|
4603
4636
|
reject(/* @__PURE__ */ new Error("body too large"));
|
|
4604
4637
|
req.destroy();
|
|
4605
4638
|
}
|
|
@@ -4651,6 +4684,36 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
4651
4684
|
}
|
|
4652
4685
|
}
|
|
4653
4686
|
});
|
|
4687
|
+
ctx.webServer.register({
|
|
4688
|
+
kind: "exact",
|
|
4689
|
+
path: UPLOAD_PATH,
|
|
4690
|
+
handler: async (req, res) => {
|
|
4691
|
+
try {
|
|
4692
|
+
if (req.method !== "POST") {
|
|
4693
|
+
sendJson(res, 405, { error: "method not allowed" });
|
|
4694
|
+
return;
|
|
4695
|
+
}
|
|
4696
|
+
if (req.headers["x-serenity-ui"] !== "1") {
|
|
4697
|
+
sendJson(res, 403, { error: "图片落盘仅限 WebUI(client 专用)" });
|
|
4698
|
+
return;
|
|
4699
|
+
}
|
|
4700
|
+
const raw = await readBody(req, 20971520);
|
|
4701
|
+
const body = JSON.parse(raw);
|
|
4702
|
+
const workspace = resolveWorkspace(ctx, {
|
|
4703
|
+
sessionId: body.sessionId,
|
|
4704
|
+
workspace: body.workspace
|
|
4705
|
+
});
|
|
4706
|
+
const root = findSerenityRoot(workspace);
|
|
4707
|
+
if (!root) {
|
|
4708
|
+
sendJson(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
4709
|
+
return;
|
|
4710
|
+
}
|
|
4711
|
+
sendJson(res, 200, { path: saveImageToTmp(root, body.mediaType ?? "", body.data ?? "") });
|
|
4712
|
+
} catch (err) {
|
|
4713
|
+
sendJson(res, 400, { error: err.message ?? String(err) });
|
|
4714
|
+
}
|
|
4715
|
+
}
|
|
4716
|
+
});
|
|
4654
4717
|
ctx.webServer.register({
|
|
4655
4718
|
kind: "exact",
|
|
4656
4719
|
path: ROUTE_PATH,
|
|
@@ -4677,7 +4740,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
4677
4740
|
sendJson(res, 403, { error: "safe-mode 切换仅限 WebUI(agent 不可自行开关)" });
|
|
4678
4741
|
return;
|
|
4679
4742
|
}
|
|
4680
|
-
const raw = await readBody(req);
|
|
4743
|
+
const raw = await readBody(req, 65536);
|
|
4681
4744
|
const body = JSON.parse(raw);
|
|
4682
4745
|
const workspace = resolveWorkspace(ctx, {
|
|
4683
4746
|
sessionId: body.sessionId,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shgroup/dsh-serenity-hooks",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.1",
|
|
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": {
|