@shgroup/dsh-serenity-hooks 1.19.8 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-serenity-hooks",
3
- "version": "1.19.8",
3
+ "version": "1.20.0",
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,159 @@ 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 handlingRef = (0, react.useRef)(false);
263
+ const promptError = session?.promptError?.error;
264
+ const imageIds = input?.imageIds ?? [];
265
+ (0, react.useEffect)(() => {
266
+ if (promptError?.code !== "attachment-error") return;
267
+ if (promptError.details?.reason !== "MODEL_DOES_NOT_SUPPORT_IMAGES") return;
268
+ if (handlingRef.current || imageIds.length === 0) return;
269
+ handlingRef.current = true;
270
+ setState("uploading");
271
+ (async () => {
272
+ try {
273
+ const files = await getDraftFiles(String(sessionId), imageIds);
274
+ if (files.length === 0) throw new Error("no draft image files");
275
+ const saved = [];
276
+ for (const file of files) saved.push(await uploadImage(file));
277
+ const note = saved.map((p) => `用户提供了图片在 ${p}`).join("\n");
278
+ const draft = input?.draft ?? "";
279
+ const text = draft === "" ? note : `${draft}\n${note}`;
280
+ await resendText(String(sessionId), text);
281
+ setPaths(saved);
282
+ setState("done");
283
+ } catch (err) {
284
+ console.warn(`[serenity] image fallback failed: ${String(err?.message ?? err)}`);
285
+ setState("error");
286
+ } finally {
287
+ handlingRef.current = false;
288
+ }
289
+ })();
290
+ }, [
291
+ promptError,
292
+ imageIds,
293
+ sessionId,
294
+ uploadImage,
295
+ getDraftFiles,
296
+ resendText,
297
+ input?.draft
298
+ ]);
299
+ if (state === "idle") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
300
+ className: "serenity-image-fallback",
301
+ "data-state": "idle"
302
+ });
303
+ if (state === "uploading") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
304
+ className: "serenity-image-fallback",
305
+ "data-state": "busy",
306
+ children: "图片已保存中…"
307
+ });
308
+ if (state === "done") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
309
+ className: "serenity-image-fallback",
310
+ "data-state": "done",
311
+ children: paths.map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: p }, p))
312
+ });
313
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
314
+ className: "serenity-image-fallback",
315
+ "data-state": "error",
316
+ children: "图片保存失败,请重试"
317
+ });
318
+ }
319
+ //#endregion
320
+ //#region src/client/image-fallback-api.ts
321
+ /** 图片落盘接口路径(node half api.ts,client 专属 x-serenity-ui 头) */
322
+ const UPLOAD_PATH = "/serenity/image-upload";
323
+ /** 浏览器 File → base64(与 ui-conversation serializeImages 等价的最小实现) */
324
+ function fileToBase64(file) {
325
+ return file.arrayBuffer().then((buffer) => {
326
+ const bytes = new Uint8Array(buffer);
327
+ let binary = "";
328
+ const chunk = 32768;
329
+ for (let offset = 0; offset < bytes.length; offset += chunk) binary += String.fromCharCode(...bytes.subarray(offset, offset + chunk));
330
+ return btoa(binary);
331
+ });
332
+ }
333
+ /** 上传一张图片到 CCC _tmp/images_from_user/,返回相对路径(如 _tmp/images_from_user/xxx.png) */
334
+ async function uploadImage(file) {
335
+ const data = await fileToBase64(file);
336
+ const res = await fetch(UPLOAD_PATH, {
337
+ method: "POST",
338
+ headers: {
339
+ "content-type": "application/json",
340
+ "x-serenity-ui": "1"
341
+ },
342
+ body: JSON.stringify({
343
+ mediaType: file.type,
344
+ name: file.name,
345
+ data
346
+ })
347
+ });
348
+ const body = await res.json();
349
+ if (!res.ok || typeof body.path !== "string") throw new Error(`serenity image upload failed: ${body.error ?? res.status}`);
350
+ return body.path;
351
+ }
352
+ /** 取 rail 图片的浏览器 File(conversation.draftImages,session-scoped;类型宽松访问——IConversation 类型面未含 draftImages) */
353
+ async function getDraftFiles(ctx, sessionId, ids) {
354
+ const draftImages = (ctx.sessions.scope(sessionId)?.conversation)?.draftImages;
355
+ if (draftImages === void 0) return [];
356
+ return (draftImages(ids) ?? []).map((a) => a.file);
357
+ }
358
+ /** 纯文本重发(绕过图片门禁——不含 image part,模型永不触发 MODEL_DOES_NOT_SUPPORT_IMAGES) */
359
+ async function resendText(ctx, sessionId, text) {
360
+ const prompt = ctx.sessions.binding(sessionId)?.session?.prompt;
361
+ if (prompt === void 0) throw new Error("serenity image fallback: session unavailable");
362
+ const result = await prompt([{
363
+ type: "text",
364
+ text
365
+ }], "queue");
366
+ if (!result.ok) throw new Error(`serenity image fallback resend failed: ${result.error?.code}: ${result.error?.message}`);
367
+ }
368
+ //#endregion
243
369
  //#region src/client/index.ts
244
- const inject = ["slots", "conversation"];
370
+ const inject = [
371
+ "slots",
372
+ "conversation",
373
+ "sessions"
374
+ ];
245
375
  function apply(ctx) {
246
- ctx.inject(["slots", "conversation"], (scope) => {
376
+ ctx.inject([
377
+ "slots",
378
+ "conversation",
379
+ "sessions"
380
+ ], (scope) => {
247
381
  scope.effect(() => scope.slots.register({
248
382
  name: "conversation.session.header.actions",
249
383
  id: "serenity-safe",
250
384
  order: 10
251
385
  }, SafeModePanel), "serenity: header status badge");
386
+ scope.effect(() => scope.slots.register({
387
+ name: "conversation.input.dock",
388
+ id: "serenity-image-fallback",
389
+ order: 100,
390
+ inject: () => ({
391
+ uploadImage: (file) => uploadImage(file),
392
+ getDraftFiles: (sessionId, ids) => getDraftFiles(scope, sessionId, ids),
393
+ resendText: (sessionId, text) => resendText(scope, sessionId, text)
394
+ })
395
+ }, ImageFallbackDock), "serenity: image fallback dock");
252
396
  });
253
397
  }
254
398
  //#endregion
package/lib/index.js CHANGED
@@ -4106,6 +4106,13 @@ function principlesBlock(root) {
4106
4106
  "fault to be hidden. Never disguise or excuse what you do not know;",
4107
4107
  "not-knowing is a state to be repaired, and reporting it is the first repair.",
4108
4108
  "",
4109
+ "MSM principles — machinery before improvisation:",
4110
+ "- Determinism first: use a registered Mech before hand-rolling; reserve",
4111
+ " Semi-Mech for genuine judgment points.",
4112
+ "- Single source of truth: an MSM is the only decoder of its own usage",
4113
+ " (--help/--schema); documents must not duplicate it.",
4114
+ "- Registered to act: no tool exists unless it is on the manifest.",
4115
+ "",
4109
4116
  "Operational boundaries:",
4110
4117
  `Root: ${root}`,
4111
4118
  " • File access — read/edit/write/grep/glob are confined to Root; paths outside Root are rejected (RR5)",
@@ -4164,30 +4171,42 @@ function metaphorBlock() {
4164
4171
  " drawings rebuild the whole machine (R↓), the drawings are reusable",
4165
4172
  " (S↑). Verdict: an undimensioned part = unassemblable.",
4166
4173
  "",
4174
+ "4. The Machinery → MSM (Mech & Semi-Mech). The ship's equipment is",
4175
+ " machinery: registered, deterministic, self-describing. Turn the",
4176
+ " crank of a Mech and the action is exact; the wheel with a helmsman",
4177
+ " (Semi-Mech) steers where judgment is needed. Verdict: hand-rolling",
4178
+ " what a machine already does = wasting the crew.",
4179
+ "",
4180
+ "5. The Manifest → Single Source of Truth. Every tool exists only if it",
4181
+ " is on the manifest (mech-registry); there is exactly one manifest.",
4182
+ " An MSM self-describes (--help/--schema) — the manifest is the only",
4183
+ " key. Verdict: duplicating a tool's usage in documents = two",
4184
+ " contradictory charts.",
4185
+ "",
4167
4186
  "THE VOYAGE — the cognitive lifecycle",
4168
4187
  "",
4169
- "4. Harbor Inspection → First Anchor. The first anchor = departure",
4188
+ "6. Harbor Inspection → First Anchor. The first anchor = departure",
4170
4189
  " inspection: confirm identity (ACC manifesto), logbook (SESSION),",
4171
4190
  " ballast (constraints) before setting sail. Verdict: skipping the",
4172
4191
  " anchor and working directly = sailing uninspected.",
4173
4192
  "",
4174
- "5. The Logbook → Session Tracking. SESSION.md is the only ship log.",
4193
+ "7. The Logbook → Session Tracking. SESSION.md is the only ship log.",
4175
4194
  " Unrecorded = unvoyaged. Verdict: finishing multi-step work without",
4176
4195
  " a progress record = a missing page.",
4177
4196
  "",
4178
- "6. The Ship of Theseus → Continuity. Planks may be replaced; the ship",
4197
+ "8. The Ship of Theseus → Continuity. Planks may be replaced; the ship",
4179
4198
  " remains the same. The container can be rebuilt; identity persists.",
4180
4199
  " You are part of a trajectory, not a new ship. Verdict: acting",
4181
4200
  " without consulting precedent = a different ship.",
4182
4201
  "",
4183
4202
  "THE CREW — multi-agent collaboration",
4184
4203
  "",
4185
- "7. Crew Rotation → Multi-Agent Cognition. Other crew members will come",
4204
+ "9. Crew Rotation → Multi-Agent Cognition. Other crew members will come",
4186
4205
  " after you. When you leave, leave a handover they can pick up",
4187
4206
  " (SESSION closed, open problems listed). Verdict: leaving without",
4188
4207
  " handover = abandoning ship.",
4189
4208
  "",
4190
- "8. Blueprint over Statue → Reconstruction > Preservation. Keep the",
4209
+ "10. Blueprint over Statue → Reconstruction > Preservation. Keep the",
4191
4210
  " blueprint, not the statue. Recording only conclusions without",
4192
4211
  " rationale = a statue with no blueprint, unreconstructable.",
4193
4212
  " Verdict: a decision record without reasons or alternatives =",
@@ -4575,12 +4594,45 @@ function registerCompactRetention(ctx, opts = {}) {
4575
4594
  //#region src/api.ts
4576
4595
  const ROUTE_PATH = "/serenity/status";
4577
4596
  const LOOPS_PATH = "/serenity/loops";
4578
- function readBody(req) {
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) {
4579
4631
  return new Promise((resolve, reject) => {
4580
4632
  let data = "";
4581
4633
  req.on("data", (chunk) => {
4582
4634
  data += chunk.toString("utf-8");
4583
- if (data.length > 65536) {
4635
+ if (data.length > maxBytes) {
4584
4636
  reject(/* @__PURE__ */ new Error("body too large"));
4585
4637
  req.destroy();
4586
4638
  }
@@ -4632,6 +4684,36 @@ function registerStatusApi(ctx, opts = {}) {
4632
4684
  }
4633
4685
  }
4634
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
+ });
4635
4717
  ctx.webServer.register({
4636
4718
  kind: "exact",
4637
4719
  path: ROUTE_PATH,
@@ -4658,7 +4740,7 @@ function registerStatusApi(ctx, opts = {}) {
4658
4740
  sendJson(res, 403, { error: "safe-mode 切换仅限 WebUI(agent 不可自行开关)" });
4659
4741
  return;
4660
4742
  }
4661
- const raw = await readBody(req);
4743
+ const raw = await readBody(req, 65536);
4662
4744
  const body = JSON.parse(raw);
4663
4745
  const workspace = resolveWorkspace(ctx, {
4664
4746
  sessionId: body.sessionId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.19.8",
3
+ "version": "1.20.0",
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": {