@dfy-plugins/dsh-visualize 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 xiaoxiao44443
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # @dfy-plugins/dsh-visualize
2
+
3
+ 为 DeepSeek Harness 提供 `dfy-visualize` Skill 和 `dfy_visualize_render` Tool,把工作区中的 HTML 发布成对话内可交互的可视化。
4
+
5
+ ## 存储
6
+
7
+ 每个产物都归当前会话所有:
8
+
9
+ ```text
10
+ session-<uuid>/
11
+ session.jsonl.zstd
12
+ artifacts/
13
+ visualizations/
14
+ <artifact-uuid>/
15
+ index.html
16
+ manifest.json
17
+ assets/
18
+ ```
19
+
20
+ 归档会保留它;归档管理中永久删除对话时,整个 `session-*` 目录被删除,可视化和素材也会随之清理。
21
+
22
+ ## 安全边界
23
+
24
+ - iframe 仅开放 `allow-scripts`,不开放同源、表单、弹窗和顶层导航。
25
+ - CSP 禁止网络请求、外部对象和嵌套页面。
26
+ - Host 只会从会话后端返回的绝对持久化位置中读取 artifact,路由参数不会直接拼成任意文件路径。
27
+
@@ -0,0 +1,5 @@
1
+ # @dfy-plugins/dsh-visualize bundle patch — session-owned HTML Tool, Skill and sandboxed Client renderer.
2
+ - insert:
3
+ - id: visualize
4
+ name: '@dfy-plugins/dsh-visualize'
5
+
package/lib/client.js ADDED
@@ -0,0 +1,309 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@dfy-plugins/dsh-visualize",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ "use strict";
7
+ var __create = Object.create;
8
+ var __defProp = Object.defineProperty;
9
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
10
+ var __getOwnPropNames = Object.getOwnPropertyNames;
11
+ var __getProtoOf = Object.getPrototypeOf;
12
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
13
+ var __export = (target, all) => {
14
+ for (var name2 in all)
15
+ __defProp(target, name2, { get: all[name2], enumerable: true });
16
+ };
17
+ var __copyProps = (to, from, except, desc) => {
18
+ if (from && typeof from === "object" || typeof from === "function") {
19
+ for (let key of __getOwnPropNames(from))
20
+ if (!__hasOwnProp.call(to, key) && key !== except)
21
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
22
+ }
23
+ return to;
24
+ };
25
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
26
+ // If the importer is in node compatibility mode or this is not an ESM
27
+ // file that has been converted to a CommonJS file using a Babel-
28
+ // compatible transform (i.e. "__esModule" has not been set), then set
29
+ // "default" to the CommonJS "module.exports" for node compatibility.
30
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
31
+ mod
32
+ ));
33
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
34
+
35
+ // src/client.tsx
36
+ var client_exports = {};
37
+ __export(client_exports, {
38
+ apply: () => apply,
39
+ inject: () => inject,
40
+ name: () => name
41
+ });
42
+ module.exports = __toCommonJS(client_exports);
43
+ var import_react = __toESM(require("react"), 1);
44
+ var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
45
+
46
+ // src/logic.ts
47
+ var MAX_HTML_BYTES = 2 * 1024 * 1024;
48
+ var MAX_ASSET_BYTES = 8 * 1024 * 1024;
49
+ var MAX_TOTAL_ASSET_BYTES = 24 * 1024 * 1024;
50
+ var MAX_ASSETS = 24;
51
+ function visualizationUrl(meta) {
52
+ return `/api/dsh-visualize/artifacts/${encodeURIComponent(meta.sessionId)}/${encodeURIComponent(meta.artifactId)}/index.html`;
53
+ }
54
+ function parseVisualizationMeta(value) {
55
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
56
+ const meta = value;
57
+ if (meta.kind !== "dsh-visualization" || meta.version !== 1) return void 0;
58
+ if (typeof meta.sessionId !== "string" || meta.sessionId.length === 0 || meta.sessionId.length > 200) return void 0;
59
+ if (typeof meta.artifactId !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(meta.artifactId)) {
60
+ return void 0;
61
+ }
62
+ if (typeof meta.title !== "string" || meta.title.length === 0 || meta.title.length > 120) return void 0;
63
+ if (!Number.isSafeInteger(meta.assetCount) || (meta.assetCount ?? -1) < 0 || (meta.assetCount ?? 0) > MAX_ASSETS) {
64
+ return void 0;
65
+ }
66
+ return meta;
67
+ }
68
+
69
+ // src/client.tsx
70
+ var name = "visualize";
71
+ var inject = ["slots"];
72
+ var TOOL_NAME = "dfy_visualize_render";
73
+ var STYLE_ID = "@dfy-plugins/dsh-visualize";
74
+ var MESSAGE_SOURCE = "dsh-visualize";
75
+ var MIN_FRAME_HEIGHT = 180;
76
+ var MAX_FRAME_HEIGHT = 4096;
77
+ var STYLES = `
78
+ .dsh-visualize-tool { display: flex; min-width: 0; width: 100%; flex-direction: column; }
79
+ .dsh-visualize-row { position: relative; display: flex; min-width: 0; min-height: 1lh; align-items: center; overflow: hidden; }
80
+ .dsh-visualize-tool[data-state='running'] .dsh-visualize-row::after { position: absolute; inset: 0 auto 0 0; width: 300px; animation: dsh-visualize-sweep 2.6s ease-out infinite; background: linear-gradient(90deg, transparent 0%, color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%, transparent 100%); content: ''; pointer-events: none; }
81
+ .dsh-visualize-leading { display: inline-flex; width: 16px; height: 16px; flex: none; align-items: center; justify-content: center; margin-right: 6px; color: var(--dsw-alias-label-tertiary); }
82
+ .dsh-visualize-title { flex: none; color: var(--dsw-alias-label-secondary); font-size: 14px; line-height: 24px; }
83
+ .dsh-visualize-separator { width: 2px; height: 2px; flex: none; margin: 0 8px; border-radius: 1px; background: var(--dsw-alias-label-caption); }
84
+ .dsh-visualize-toggle { display: flex; min-width: 0; max-width: 100%; min-height: 1lh; flex: 0 1 auto; appearance: none; align-items: center; padding: 0; border: 0; background: transparent; color: inherit; cursor: pointer; font: inherit; text-align: left; }
85
+ .dsh-visualize-toggle:focus-visible, .dsh-visualize-inspect:focus-visible { border-radius: 4px; outline: 2px solid var(--dsw-alias-brand-primary, #298df8); outline-offset: -1px; }
86
+ .dsh-visualize-inspect { display: inline-flex; min-width: 0; min-height: 1lh; flex: none; appearance: none; align-items: center; padding: 0; border: 0; background: transparent; color: inherit; font: inherit; }
87
+ button.dsh-visualize-inspect { cursor: pointer; }
88
+ .dsh-visualize-summary { min-width: 0; overflow: hidden; color: var(--dsw-alias-label-tertiary); font-size: 14px; line-height: 24px; text-overflow: ellipsis; white-space: nowrap; }
89
+ .dsh-visualize-summary[data-error] { color: var(--dsw-alias-state-error-primary, #e5484d); }
90
+ .dsh-visualize-toggle:hover .dsh-visualize-summary { color: var(--dsw-alias-label-primary); }
91
+ .dsh-visualize-chevron { flex: none; margin-left: 4px; color: var(--dsw-alias-label-tertiary); opacity: 0; transform: rotate(-90deg); transition: opacity .12s ease, transform .16s ease; }
92
+ .dsh-visualize-toggle:hover .dsh-visualize-chevron { opacity: 1; }
93
+ .dsh-visualize-toggle[aria-expanded='true'] .dsh-visualize-chevron { opacity: 1; transform: rotate(0); }
94
+ .dsh-visualize-details { display: flex; flex-direction: column; }
95
+ .dsh-visualize-details[hidden], .dsh-visualize-panel[hidden] { display: none !important; }
96
+ .dsh-visualize-io-card { display: flex; flex-direction: column; margin: 4px 0 4px 4px; overflow: hidden; border: 1px solid var(--dsw-alias-border-l1); border-radius: 12px; background: var(--dsw-alias-markdown-code-block); font: var(--dsw-font-markdown-code-block-small); }
97
+ .dsh-visualize-io-section { display: grid; max-height: 150px; grid-template-columns: max-content minmax(0, 1fr); align-items: baseline; column-gap: 14px; padding: 12px 16px; overflow: auto; }
98
+ .dsh-visualize-io-label { position: sticky; top: 0; align-self: start; color: var(--dsw-alias-label-caption); }
99
+ .dsh-visualize-io-text { min-width: 0; margin: 0; color: var(--dsw-alias-label-secondary); white-space: pre-wrap; overflow-wrap: anywhere; font: inherit; }
100
+ .dsh-visualize-io-text[data-error] { color: var(--dsw-alias-state-error-primary); }
101
+ .dsh-visualize-io-divider { height: 1px; flex: none; background: var(--dsw-alias-border-l2); }
102
+ .dsh-visualize-inspect-action { display: inline-flex; align-self: flex-start; align-items: center; gap: 4px; margin: 4px 0 2px 4px; padding: 2px 8px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 999px; background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-secondary); cursor: pointer; font-size: 11px; line-height: 16px; }
103
+ .dsh-visualize-inspect-action:hover { background: var(--dsw-alias-interactive-bg-hover-solid); color: var(--dsw-alias-label-primary); }
104
+ .dsh-visualize-panel { display: block; min-width: 0; max-width: calc(100% - 22px); margin: 8px 0 8px 22px; }
105
+ .dsh-visualize-frame { display: block; width: 100%; flex: none; border: 0; background: transparent; }
106
+ @keyframes dsh-visualize-sweep { 0% { left: -300px; } 90%, 100% { left: 100%; } }
107
+ @media (max-width: 680px) { .dsh-visualize-panel { max-width: 100%; margin-left: 0; } }
108
+ @media (prefers-reduced-motion: reduce) { .dsh-visualize-tool[data-state='running'] .dsh-visualize-row::after { display: none; animation: none; } .dsh-visualize-chevron { transition: none; } }
109
+ `;
110
+ function installStyles() {
111
+ const existing = document.querySelector(`style[data-plugin=${JSON.stringify(STYLE_ID)}]`);
112
+ const tag = document.createElement("style");
113
+ tag.dataset.plugin = STYLE_ID;
114
+ tag.textContent = STYLES;
115
+ if (existing === null) document.head.appendChild(tag);
116
+ else existing.replaceWith(tag);
117
+ return () => tag.remove();
118
+ }
119
+ function firstLine(value) {
120
+ return value.split("\n", 1)[0] ?? value;
121
+ }
122
+ function resultText(block) {
123
+ if (!("kind" in block)) return null;
124
+ const parts = block.content.flatMap((item) => {
125
+ if (typeof item === "object" && item !== null && item.type === "text") {
126
+ const text = item.text;
127
+ return typeof text === "string" ? [text] : [];
128
+ }
129
+ return [];
130
+ });
131
+ if (parts.length === 0 && block.error !== void 0) return `${block.error.name}: ${block.error.code}`;
132
+ return parts.join("\n") || null;
133
+ }
134
+ function requestedTitle(block) {
135
+ const raw = ("kind" in block ? block.call?.argsRaw : block.argsRaw) ?? "";
136
+ try {
137
+ const value = JSON.parse(raw);
138
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
139
+ const title = value.title;
140
+ return typeof title === "string" && title.trim().length > 0 ? title.trim().slice(0, 120) : void 0;
141
+ } catch {
142
+ return void 0;
143
+ }
144
+ }
145
+ function requestedFilePath(block) {
146
+ const raw = ("kind" in block ? block.call?.argsRaw : block.argsRaw) ?? "";
147
+ try {
148
+ const value = JSON.parse(raw);
149
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
150
+ const filePath = value.file_path;
151
+ return typeof filePath === "string" && filePath.trim().length > 0 ? filePath.trim().slice(0, 1e3) : void 0;
152
+ } catch {
153
+ return void 0;
154
+ }
155
+ }
156
+ function toolInput(block) {
157
+ const raw = (("kind" in block ? block.call?.argsRaw : block.argsRaw) ?? "").trim();
158
+ if (raw.length === 0) return null;
159
+ try {
160
+ return JSON.stringify(JSON.parse(raw), null, 2);
161
+ } catch {
162
+ return raw;
163
+ }
164
+ }
165
+ function currentTheme() {
166
+ const root = document.documentElement;
167
+ const declared = root.dataset.theme ?? root.getAttribute("data-color-scheme");
168
+ if (declared === "dark" || root.classList.contains("dark")) return "dark";
169
+ if (declared === "light" || root.classList.contains("light")) return "light";
170
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
171
+ }
172
+ function VisualizationFrame({
173
+ meta,
174
+ sourceFile,
175
+ visible
176
+ }) {
177
+ const iframeRef = import_react.default.useRef(null);
178
+ const [height, setHeight] = import_react.default.useState(MIN_FRAME_HEIGHT);
179
+ const [measured, setMeasured] = import_react.default.useState(false);
180
+ const [scrollable, setScrollable] = import_react.default.useState(false);
181
+ const src = visualizationUrl(meta);
182
+ const sendTheme = import_react.default.useCallback(() => {
183
+ iframeRef.current?.contentWindow?.postMessage({ source: MESSAGE_SOURCE, type: "theme", theme: currentTheme() }, "*");
184
+ }, []);
185
+ import_react.default.useEffect(() => {
186
+ const receive = (event) => {
187
+ const frame = iframeRef.current;
188
+ if (frame === null || event.source !== frame.contentWindow) return;
189
+ const value = event.data;
190
+ if (value?.source !== MESSAGE_SOURCE || value.type !== "resize" || value.artifactId !== meta.artifactId) return;
191
+ if (typeof value.height !== "number" || !Number.isFinite(value.height)) return;
192
+ const measuredHeight = Math.ceil(value.height);
193
+ const nextHeight = Math.min(MAX_FRAME_HEIGHT, Math.max(MIN_FRAME_HEIGHT, measuredHeight));
194
+ const nextScrollable = measuredHeight > MAX_FRAME_HEIGHT;
195
+ setHeight(nextHeight);
196
+ setScrollable(nextScrollable);
197
+ setMeasured(true);
198
+ };
199
+ window.addEventListener("message", receive);
200
+ return () => window.removeEventListener("message", receive);
201
+ }, [meta.artifactId]);
202
+ import_react.default.useEffect(() => {
203
+ const root = document.documentElement;
204
+ const observer = new MutationObserver(sendTheme);
205
+ observer.observe(root, { attributes: true, attributeFilter: ["class", "data-theme", "data-color-scheme"] });
206
+ const media = window.matchMedia("(prefers-color-scheme: dark)");
207
+ media.addEventListener("change", sendTheme);
208
+ return () => {
209
+ observer.disconnect();
210
+ media.removeEventListener("change", sendTheme);
211
+ };
212
+ }, [sendTheme]);
213
+ return /* @__PURE__ */ import_react.default.createElement(
214
+ "div",
215
+ {
216
+ className: "dsh-visualize-panel",
217
+ "data-dsh-artifact-content": "visualization",
218
+ "data-dsh-artifact-url": src,
219
+ "data-dsh-source-file": sourceFile,
220
+ hidden: !visible
221
+ },
222
+ /* @__PURE__ */ import_react.default.createElement(
223
+ "iframe",
224
+ {
225
+ ref: iframeRef,
226
+ className: "dsh-visualize-frame",
227
+ src,
228
+ title: meta.title,
229
+ sandbox: "allow-scripts",
230
+ referrerPolicy: "no-referrer",
231
+ scrolling: scrollable ? "auto" : "no",
232
+ style: {
233
+ height: `${String(height)}px`,
234
+ opacity: measured ? 1 : 0,
235
+ pointerEvents: measured ? "auto" : "none"
236
+ },
237
+ onLoad: () => {
238
+ sendTheme();
239
+ }
240
+ }
241
+ )
242
+ );
243
+ }
244
+ function VisualizationToolRow({ block, inspect }) {
245
+ const settled = "kind" in block;
246
+ const stopped = settled && block.error?.code === "interrupted";
247
+ const state = !settled ? "running" : stopped ? "stopped" : block.isError ? "error" : "ok";
248
+ const meta = settled && !block.isError ? parseVisualizationMeta(block.meta) : void 0;
249
+ const pendingTitle = requestedTitle(block);
250
+ const sourceFile = requestedFilePath(block);
251
+ const input = toolInput(block);
252
+ const output = resultText(block);
253
+ const summary = state === "running" ? pendingTitle === void 0 ? "\u6B63\u5728\u521B\u5EFA\u53EF\u89C6\u5316" : `\u6B63\u5728\u521B\u5EFA\uFF1A${pendingTitle}` : state === "error" ? firstLine(output ?? "\u53EF\u89C6\u5316\u521B\u5EFA\u5931\u8D25") : state === "stopped" ? "\u5DF2\u505C\u6B62\u521B\u5EFA\u53EF\u89C6\u5316" : meta === void 0 ? "\u5DF2\u521B\u5EFA\u53EF\u89C6\u5316" : `\u5DF2\u521B\u5EFA\uFF1A${meta.title}`;
254
+ const artifactKey = meta === void 0 ? void 0 : `${meta.sessionId}:${meta.artifactId}`;
255
+ const [open, setOpen] = import_react.default.useState(meta !== void 0);
256
+ const [detailsOpen, setDetailsOpen] = import_react.default.useState(false);
257
+ const detailsExpandable = input !== null || output !== null;
258
+ const currentArtifact = import_react.default.useRef(artifactKey);
259
+ import_react.default.useEffect(() => {
260
+ if (artifactKey === void 0 || artifactKey === currentArtifact.current) return;
261
+ currentArtifact.current = artifactKey;
262
+ setOpen(true);
263
+ }, [artifactKey]);
264
+ const heading = /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, /* @__PURE__ */ import_react.default.createElement("span", { className: "dsh-visualize-leading" }, state === "error" ? /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.StateDot, { state: "error" }) : state === "stopped" ? /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.StateDot, { state: "warning" }) : /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.IconSparkle16, { size: 14 })), /* @__PURE__ */ import_react.default.createElement("span", { className: "dsh-visualize-title" }, "DFY VISUALIZE"));
265
+ return /* @__PURE__ */ import_react.default.createElement(
266
+ "div",
267
+ {
268
+ className: "dsh-visualize-tool",
269
+ "data-state": state,
270
+ "data-tool": TOOL_NAME,
271
+ "data-dsh-visualization-output": meta === void 0 ? void 0 : ""
272
+ },
273
+ /* @__PURE__ */ import_react.default.createElement("div", { className: "dsh-visualize-row" }, !detailsExpandable ? /* @__PURE__ */ import_react.default.createElement("span", { className: "dsh-visualize-inspect" }, heading) : /* @__PURE__ */ import_react.default.createElement(
274
+ "button",
275
+ {
276
+ type: "button",
277
+ className: "dsh-visualize-inspect",
278
+ "aria-label": `DFY VISUALIZE\uFF0C${detailsOpen ? "\u6536\u8D77\u53C2\u6570" : "\u5C55\u5F00\u53C2\u6570"}`,
279
+ "aria-expanded": detailsOpen,
280
+ onClick: () => setDetailsOpen((value) => !value)
281
+ },
282
+ heading
283
+ ), /* @__PURE__ */ import_react.default.createElement("span", { className: "dsh-visualize-separator", "aria-hidden": true }), meta === void 0 ? /* @__PURE__ */ import_react.default.createElement("span", { className: "dsh-visualize-summary", "data-error": state === "error" || void 0 }, summary) : /* @__PURE__ */ import_react.default.createElement(
284
+ "button",
285
+ {
286
+ type: "button",
287
+ className: "dsh-visualize-toggle",
288
+ "aria-label": `${summary}\uFF0C${open ? "\u6536\u8D77" : "\u5C55\u5F00"}`,
289
+ "aria-expanded": open,
290
+ onClick: () => setOpen((value) => !value)
291
+ },
292
+ /* @__PURE__ */ import_react.default.createElement("span", { className: "dsh-visualize-summary" }, summary),
293
+ /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.IconChevronDownOutline14, { className: "dsh-visualize-chevron", size: 14 })
294
+ )),
295
+ /* @__PURE__ */ import_react.default.createElement("div", { className: "dsh-visualize-details", hidden: !detailsOpen }, /* @__PURE__ */ import_react.default.createElement("div", { className: "dsh-visualize-io-card" }, input === null ? null : /* @__PURE__ */ import_react.default.createElement("div", { className: "dsh-visualize-io-section" }, /* @__PURE__ */ import_react.default.createElement("span", { className: "dsh-visualize-io-label" }, "IN"), /* @__PURE__ */ import_react.default.createElement("pre", { className: "dsh-visualize-io-text" }, input)), input === null || output === null ? null : /* @__PURE__ */ import_react.default.createElement("span", { className: "dsh-visualize-io-divider", "aria-hidden": true }), output === null ? null : /* @__PURE__ */ import_react.default.createElement("div", { className: "dsh-visualize-io-section" }, /* @__PURE__ */ import_react.default.createElement("span", { className: "dsh-visualize-io-label" }, "OUT"), /* @__PURE__ */ import_react.default.createElement("pre", { className: "dsh-visualize-io-text", "data-error": state === "error" || void 0 }, output))), inspect === void 0 ? null : /* @__PURE__ */ import_react.default.createElement("button", { type: "button", className: "dsh-visualize-inspect-action", onClick: inspect }, /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.IconInspectOutline12, null), " Inspect")),
296
+ meta !== void 0 ? /* @__PURE__ */ import_react.default.createElement(VisualizationFrame, { key: artifactKey, meta, sourceFile, visible: open }) : null
297
+ );
298
+ }
299
+ function apply(ctx) {
300
+ ctx.effect(installStyles, "dsh-visualize: client styles");
301
+ ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
302
+ name: "tool.call.toolview",
303
+ key: TOOL_NAME
304
+ }, VisualizationToolRow));
305
+ }
306
+
307
+ return module.exports;
308
+ }
309
+ });
package/lib/index.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ export declare const name = "visualize";
3
+ export declare const inject: string[];
4
+ export declare const VISUALIZATION_API_PATH = "/api/dsh-visualize/artifacts";
5
+ export declare function apply(ctx: Context): void;
package/lib/index.js ADDED
@@ -0,0 +1,373 @@
1
+ /** Session-owned, sandboxed HTML visualizations for DeepSeek Harness. */
2
+ import { persistedSessionArtifactDirectory, sessionArtifactDirectory } from '@dfy-plugins/resource-core/session-storage';
3
+ import { defineTool } from '@deepseek-ai/dsh-tools';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { mkdir, readFile, realpath, rename, rm, writeFile } from 'node:fs/promises';
6
+ import { basename, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
7
+ import { injectVisualizationBridge, MAX_ASSET_BYTES, MAX_ASSETS, MAX_HTML_BYTES, MAX_TOTAL_ASSET_BYTES, normalizeTitle, validateAssetName, visualizationUrl, } from './logic.js';
8
+ export const name = 'visualize';
9
+ export const inject = ['tools', 'fs', 'skills', 'sessionPersistence'];
10
+ export const VISUALIZATION_API_PATH = '/api/dsh-visualize/artifacts';
11
+ const TOOL_NAME = 'dfy_visualize_render';
12
+ const SKILL_NAME = 'dfy-visualize';
13
+ const ARTIFACT_SUBDIR = join('artifacts', 'visualizations');
14
+ const SKILL_CONTENT = `# DFY Visualize
15
+
16
+ Use this Skill when an explanation benefits from an interactive chart, diagram, comparison, simulator, or UI mockup rendered directly in the conversation.
17
+
18
+ ## Host surface contract
19
+
20
+ The artifact is embedded directly beneath an assistant response; it is not a standalone web page. Unless the user explicitly requests a card, poster, dashboard shell, or full-canvas background:
21
+
22
+ - Keep \`html\` and \`body\` marginless and transparent. Do not use \`min-height: 100vh\` or page-level flex/grid centering merely to imitate a standalone page.
23
+ - Keep the Host canvas itself transparent; do not paint a page-sized background behind the artifact.
24
+ - Keep the top-level visualization surface transparent and unframed, and use the available conversation width. Organize structural groups and repeated content with layout, spacing, dividers, or visual marks instead of container chrome.
25
+ - Use a card-like surface only for a necessary bounded interactive field or concise summary. Keep charts, maps, diagrams, tables, control groups, and the whole visualization unframed, and never nest cards.
26
+ - Do not render a heading that repeats the Tool \`title\`, and do not add a viewer titlebar, status, reload, fullscreen, or theme toolbar. Start with the actual visualization or controls.
27
+ - Backgrounds, borders, radii, and shadows remain appropriate for meaningful internal elements such as plot areas, nodes, swatches, inputs, and data cells.
28
+ - Support the Host theme through \`:root[data-theme='dark']\` as well as the initial color scheme when theme-specific colors are needed.
29
+
30
+ ## Required workflow
31
+
32
+ 1. Create a complete responsive HTML document in the current workspace. Keep CSS and JavaScript inline whenever practical.
33
+ 2. Do not use CDNs, remote scripts, remote fonts, network requests, forms, popups, or top-window navigation. The viewer intentionally blocks them.
34
+ 3. Make the page usable at narrow and wide widths. Avoid a fixed canvas width and avoid document-level scrolling when the content can size naturally.
35
+ 4. Keep primary content that expands or collapses through interaction in normal document flow so the Host can measure its changing height. Do not give the document a fixed height, position primary layout content absolutely or fixed, or clip it with \`overflow: hidden\`; out-of-flow positioning remains appropriate for genuine overlays such as menus, tooltips, and dialogs.
36
+ 5. If the HTML needs local images, fonts, CSS, or JavaScript, reference each as \`assets/<basename>\` and pass every source file in \`asset_paths\`. Asset basenames must be unique.
37
+ 6. Before publishing, inspect the HTML against the Host surface contract. Remove page-sized decoration, unnecessary wrapper surfaces, nested cards, and repeated titles added by habit.
38
+ 7. Call \`${TOOL_NAME}\` exactly once with the HTML \`file_path\`, a short \`title\`, and any \`asset_paths\`.
39
+ 8. After success, give only a concise explanation or usage hint. The Harness UI displays the visualization after the final response; do not repeat it, embed it, call the render tool again, or paste the HTML into the conversation.
40
+
41
+ The published artifact is immutable and belongs to the current session. Archiving keeps it; permanently deleting the session removes it together with the transcript.`;
42
+ function inside(parent, child) {
43
+ const rel = relative(resolve(parent), resolve(child));
44
+ return rel === '' || (!isAbsolute(rel) && rel !== '..' && !rel.startsWith(`..${sep}`));
45
+ }
46
+ async function readWorkspaceFile(ctx, exec, requestedPath, limit) {
47
+ const cwd = exec.agent?.session.header.cwd;
48
+ const target = await ctx.fs.resolve(requestedPath, {
49
+ ...(cwd === undefined ? {} : { cwd }),
50
+ signal: exec.signal,
51
+ });
52
+ const info = await ctx.fs.stat(target, exec.signal);
53
+ if (info === undefined) {
54
+ ctx.emit('fs/observed', target, { kind: 'absent' }, exec);
55
+ throw new Error(`visualization source not found: ${target.displayPath}`);
56
+ }
57
+ if (info.type !== 'file')
58
+ throw new Error(`visualization source is not a regular file: ${target.displayPath}`);
59
+ if (info.size !== undefined && info.size > limit) {
60
+ throw new Error(`visualization source exceeds the ${String(limit)} byte limit: ${target.displayPath}`);
61
+ }
62
+ const data = await ctx.fs.readBytes(target, exec.signal, limit);
63
+ ctx.emit('fs/observed', target, { kind: 'present', version: info.version }, exec);
64
+ return { data, displayPath: target.displayPath, version: info.version };
65
+ }
66
+ function decodeHtml(file) {
67
+ try {
68
+ return new TextDecoder('utf-8', { fatal: true }).decode(file.data);
69
+ }
70
+ catch {
71
+ throw new Error(`visualization HTML is not valid UTF-8: ${file.displayPath}`);
72
+ }
73
+ }
74
+ function sessionDirectory(ctx, exec) {
75
+ const agent = exec.agent;
76
+ if (agent === undefined)
77
+ throw new Error('visualization rendering requires an active DSH session');
78
+ const directory = sessionArtifactDirectory(ctx.sessionPersistence, agent.session.header);
79
+ if (directory === undefined) {
80
+ throw new Error('the configured session persistence backend does not expose a session-owned artifact directory');
81
+ }
82
+ return { sessionId: String(agent.id), directory };
83
+ }
84
+ async function publishVisualization(ctx, exec, requestedHtmlPath, requestedTitle, requestedAssets, rememberedSessionDirs) {
85
+ exec.signal.throwIfAborted();
86
+ if (!/\.html?$/i.test(requestedHtmlPath))
87
+ throw new Error('visualization file_path must end in .html or .htm');
88
+ if (requestedAssets.length > MAX_ASSETS)
89
+ throw new Error(`a visualization may contain at most ${String(MAX_ASSETS)} assets`);
90
+ const htmlFile = await readWorkspaceFile(ctx, exec, requestedHtmlPath, MAX_HTML_BYTES);
91
+ const html = decodeHtml(htmlFile);
92
+ const fallbackTitle = basename(htmlFile.displayPath, extname(htmlFile.displayPath));
93
+ const title = normalizeTitle(requestedTitle, fallbackTitle);
94
+ const assets = [];
95
+ const names = new Set();
96
+ let totalAssetBytes = 0;
97
+ for (const requestedPath of requestedAssets) {
98
+ exec.signal.throwIfAborted();
99
+ const file = await readWorkspaceFile(ctx, exec, requestedPath, MAX_ASSET_BYTES);
100
+ const assetName = validateAssetName(basename(file.displayPath));
101
+ const comparisonKey = process.platform === 'win32' ? assetName.toLocaleLowerCase('en-US') : assetName;
102
+ if (names.has(comparisonKey))
103
+ throw new Error(`visualization asset basenames must be unique: ${assetName}`);
104
+ names.add(comparisonKey);
105
+ totalAssetBytes += file.data.byteLength;
106
+ if (totalAssetBytes > MAX_TOTAL_ASSET_BYTES) {
107
+ throw new Error(`visualization assets exceed the ${String(MAX_TOTAL_ASSET_BYTES)} byte total limit`);
108
+ }
109
+ assets.push({ name: assetName, bytes: file.data.byteLength, data: file.data });
110
+ }
111
+ const session = sessionDirectory(ctx, exec);
112
+ const artifactId = randomUUID();
113
+ const artifactsRoot = resolve(session.directory, ARTIFACT_SUBDIR);
114
+ if (!inside(session.directory, artifactsRoot))
115
+ throw new Error('invalid session artifact root');
116
+ const finalDirectory = resolve(artifactsRoot, artifactId);
117
+ const temporaryDirectory = resolve(artifactsRoot, `.tmp-${artifactId}`);
118
+ if (!inside(artifactsRoot, finalDirectory) || !inside(artifactsRoot, temporaryDirectory)) {
119
+ throw new Error('invalid visualization artifact path');
120
+ }
121
+ const meta = {
122
+ kind: 'dsh-visualization',
123
+ version: 1,
124
+ sessionId: session.sessionId,
125
+ artifactId,
126
+ title,
127
+ assetCount: assets.length,
128
+ };
129
+ const renderedHtml = injectVisualizationBridge(html, artifactId);
130
+ try {
131
+ await mkdir(join(temporaryDirectory, 'assets'), { recursive: true });
132
+ exec.signal.throwIfAborted();
133
+ await writeFile(join(temporaryDirectory, 'index.html'), renderedHtml, { encoding: 'utf8', flag: 'wx' });
134
+ for (const asset of assets) {
135
+ exec.signal.throwIfAborted();
136
+ await writeFile(join(temporaryDirectory, 'assets', asset.name), asset.data, { flag: 'wx' });
137
+ }
138
+ await writeFile(join(temporaryDirectory, 'manifest.json'), `${JSON.stringify({
139
+ ...meta,
140
+ createdAt: Date.now(),
141
+ htmlBytes: htmlFile.data.byteLength,
142
+ assets: assets.map(({ name, bytes }) => ({ name, bytes })),
143
+ }, null, 2)}\n`, { encoding: 'utf8', flag: 'wx' });
144
+ exec.signal.throwIfAborted();
145
+ await rename(temporaryDirectory, finalDirectory);
146
+ }
147
+ catch (error) {
148
+ await rm(temporaryDirectory, { recursive: true, force: true }).catch(() => { });
149
+ throw error;
150
+ }
151
+ rememberedSessionDirs.set(session.sessionId, session.directory);
152
+ return {
153
+ ...meta,
154
+ url: visualizationUrl(meta),
155
+ htmlBytes: htmlFile.data.byteLength,
156
+ assets: assets.map(({ name, bytes }) => ({ name, bytes })),
157
+ };
158
+ }
159
+ function renderResult(value) {
160
+ const assetLabel = value.assetCount === 0 ? '' : `,包含 ${String(value.assetCount)} 个素材`;
161
+ return `已创建可视化“${value.title}”${assetLabel}。可视化已显示在对话中。`;
162
+ }
163
+ function createVisualizationTool(ctx, rememberedSessionDirs) {
164
+ return defineTool({
165
+ name: TOOL_NAME,
166
+ description: `Publish a workspace HTML document as a sandboxed interactive visualization in the current conversation. Read the ${SKILL_NAME} Skill before every call. Keep the top-level surface transparent and unframed, omit repeated title/header or viewer chrome, and reserve card surfaces for necessary bounded content.`,
167
+ parameters: {
168
+ file_path: {
169
+ type: 'string',
170
+ required: true,
171
+ description: 'Workspace path to a complete UTF-8 .html or .htm document.',
172
+ },
173
+ title: {
174
+ type: 'string',
175
+ description: 'Short user-facing title for the visualization.',
176
+ },
177
+ asset_paths: {
178
+ type: 'array',
179
+ items: { type: 'string' },
180
+ description: 'Optional workspace files copied beside the HTML. Reference them from HTML as assets/<basename>.',
181
+ },
182
+ },
183
+ output: {
184
+ schema: {
185
+ type: 'object',
186
+ additionalProperties: false,
187
+ properties: {
188
+ kind: { type: 'string', required: true },
189
+ version: { type: 'integer', required: true },
190
+ sessionId: { type: 'string', required: true },
191
+ artifactId: { type: 'string', required: true },
192
+ title: { type: 'string', required: true },
193
+ assetCount: { type: 'integer', required: true },
194
+ url: { type: 'string', required: true },
195
+ htmlBytes: { type: 'integer', required: true },
196
+ assets: {
197
+ type: 'array',
198
+ required: true,
199
+ items: {
200
+ type: 'object',
201
+ additionalProperties: false,
202
+ properties: {
203
+ name: { type: 'string', required: true },
204
+ bytes: { type: 'integer', required: true },
205
+ },
206
+ },
207
+ },
208
+ },
209
+ },
210
+ render: (_args, value) => [{ type: 'text', text: renderResult(value) }],
211
+ presentationMeta: (_args, value) => {
212
+ const artifact = value;
213
+ return {
214
+ kind: artifact.kind,
215
+ version: artifact.version,
216
+ sessionId: artifact.sessionId,
217
+ artifactId: artifact.artifactId,
218
+ title: artifact.title,
219
+ assetCount: artifact.assetCount,
220
+ };
221
+ },
222
+ },
223
+ isConcurrencySafe: () => false,
224
+ execute: async (args, exec) => publishVisualization(ctx, exec, args.file_path, args.title, args.asset_paths ?? [], rememberedSessionDirs),
225
+ presentCall(args) {
226
+ return {
227
+ card: 'generic',
228
+ title: 'DFY VISUALIZE',
229
+ kind: 'read',
230
+ locations: [{ path: args.file_path }],
231
+ };
232
+ },
233
+ });
234
+ }
235
+ function sendJson(res, status, body) {
236
+ const payload = JSON.stringify(body);
237
+ res.writeHead(status, {
238
+ 'content-type': 'application/json; charset=utf-8',
239
+ 'content-length': Buffer.byteLength(payload),
240
+ 'cache-control': 'no-store',
241
+ });
242
+ res.end(payload);
243
+ }
244
+ function contentType(path) {
245
+ switch (extname(path).toLowerCase()) {
246
+ case '.html':
247
+ case '.htm': return 'text/html; charset=utf-8';
248
+ case '.css': return 'text/css; charset=utf-8';
249
+ case '.js':
250
+ case '.mjs': return 'text/javascript; charset=utf-8';
251
+ case '.json': return 'application/json; charset=utf-8';
252
+ case '.svg': return 'image/svg+xml';
253
+ case '.png': return 'image/png';
254
+ case '.jpg':
255
+ case '.jpeg': return 'image/jpeg';
256
+ case '.webp': return 'image/webp';
257
+ case '.gif': return 'image/gif';
258
+ case '.avif': return 'image/avif';
259
+ case '.woff': return 'font/woff';
260
+ case '.woff2': return 'font/woff2';
261
+ case '.mp3': return 'audio/mpeg';
262
+ case '.mp4': return 'video/mp4';
263
+ case '.webm': return 'video/webm';
264
+ default: return 'application/octet-stream';
265
+ }
266
+ }
267
+ async function persistedSessionDirectory(ctx, sessionId, rememberedSessionDirs, signal) {
268
+ const remembered = rememberedSessionDirs.get(sessionId);
269
+ if (remembered !== undefined)
270
+ return remembered;
271
+ const directory = await persistedSessionArtifactDirectory(ctx.sessionPersistence, sessionId, signal);
272
+ if (directory === undefined)
273
+ return undefined;
274
+ rememberedSessionDirs.set(sessionId, directory);
275
+ return directory;
276
+ }
277
+ async function resolveArtifactFile(ctx, rememberedSessionDirs, sessionId, artifactId, relativeFile, signal) {
278
+ if (sessionId.length === 0 || sessionId.length > 200)
279
+ return undefined;
280
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(artifactId))
281
+ return undefined;
282
+ const sessionDir = await persistedSessionDirectory(ctx, sessionId, rememberedSessionDirs, signal);
283
+ if (sessionDir === undefined)
284
+ return undefined;
285
+ const artifactRoot = resolve(sessionDir, ARTIFACT_SUBDIR, artifactId);
286
+ const candidate = resolve(artifactRoot, relativeFile);
287
+ if (!inside(artifactRoot, candidate))
288
+ return undefined;
289
+ try {
290
+ const [realRoot, realCandidate] = await Promise.all([realpath(artifactRoot), realpath(candidate)]);
291
+ if (!inside(realRoot, realCandidate))
292
+ return undefined;
293
+ return realCandidate;
294
+ }
295
+ catch {
296
+ return undefined;
297
+ }
298
+ }
299
+ function decodeRoute(req) {
300
+ const pathname = new URL(req.url ?? VISUALIZATION_API_PATH, 'http://localhost').pathname;
301
+ const suffix = pathname.slice(VISUALIZATION_API_PATH.length).replace(/^\//, '');
302
+ let parts;
303
+ try {
304
+ parts = suffix.split('/').map((part) => decodeURIComponent(part));
305
+ }
306
+ catch {
307
+ return undefined;
308
+ }
309
+ const [sessionId, artifactId, first, second, ...rest] = parts;
310
+ if (sessionId === undefined || artifactId === undefined || first === undefined || rest.length > 0)
311
+ return undefined;
312
+ if (first === 'index.html' && second === undefined)
313
+ return { sessionId, artifactId, relativeFile: first };
314
+ if (first === 'assets' && second !== undefined) {
315
+ try {
316
+ return { sessionId, artifactId, relativeFile: join('assets', validateAssetName(second)) };
317
+ }
318
+ catch {
319
+ return undefined;
320
+ }
321
+ }
322
+ return undefined;
323
+ }
324
+ export function apply(ctx) {
325
+ const rememberedSessionDirs = new Map();
326
+ const disposeTool = ctx.tools.register(createVisualizationTool(ctx, rememberedSessionDirs));
327
+ const disposeSkill = ctx.skills.register({
328
+ name: SKILL_NAME,
329
+ description: '创建可在对话中直接交互的 HTML 图表、图示、模拟器和界面原型。',
330
+ source: 'runtime',
331
+ content: SKILL_CONTENT,
332
+ invocation: { modelInvocable: true, userInvocable: true },
333
+ });
334
+ ctx.inject(['webServer'], (webCtx) => {
335
+ const route = {
336
+ kind: 'prefix',
337
+ path: VISUALIZATION_API_PATH,
338
+ async handler(req, res) {
339
+ if (req.method !== 'GET')
340
+ return sendJson(res, 405, { error: 'method not allowed' });
341
+ const request = decodeRoute(req);
342
+ if (request === undefined)
343
+ return sendJson(res, 404, { error: 'visualization artifact not found' });
344
+ const file = await resolveArtifactFile(ctx, rememberedSessionDirs, request.sessionId, request.artifactId, request.relativeFile, AbortSignal.timeout(10_000));
345
+ if (file === undefined)
346
+ return sendJson(res, 404, { error: 'visualization artifact not found' });
347
+ try {
348
+ const data = await readFile(file);
349
+ const isHtml = request.relativeFile === 'index.html';
350
+ res.writeHead(200, {
351
+ 'content-type': contentType(file),
352
+ 'content-length': data.byteLength,
353
+ 'cache-control': 'private, max-age=31536000, immutable',
354
+ 'x-content-type-options': 'nosniff',
355
+ ...(isHtml ? {
356
+ 'content-security-policy': "default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; media-src 'self' data: blob:; connect-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'",
357
+ } : {}),
358
+ });
359
+ res.end(data);
360
+ }
361
+ catch {
362
+ sendJson(res, 404, { error: 'visualization artifact not found' });
363
+ }
364
+ },
365
+ };
366
+ webCtx.webServer.register(route);
367
+ });
368
+ ctx.effect(() => () => {
369
+ rememberedSessionDirs.clear();
370
+ disposeSkill();
371
+ disposeTool();
372
+ });
373
+ }
package/lib/logic.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ export declare const MAX_HTML_BYTES: number;
2
+ export declare const MAX_ASSET_BYTES: number;
3
+ export declare const MAX_TOTAL_ASSET_BYTES: number;
4
+ export declare const MAX_ASSETS = 24;
5
+ export interface VisualizationMeta {
6
+ kind: 'dsh-visualization';
7
+ version: 1;
8
+ sessionId: string;
9
+ artifactId: string;
10
+ title: string;
11
+ assetCount: number;
12
+ }
13
+ export declare function normalizeTitle(value: string | undefined, fallback: string): string;
14
+ export declare function validateAssetName(value: string): string;
15
+ export declare function visualizationUrl(meta: Pick<VisualizationMeta, 'sessionId' | 'artifactId'>): string;
16
+ export declare function injectVisualizationBridge(html: string, artifactId: string): string;
17
+ export declare function parseVisualizationMeta(value: unknown): VisualizationMeta | undefined;
package/lib/logic.js ADDED
@@ -0,0 +1,53 @@
1
+ const BRIDGE_SOURCE = 'dsh-visualize';
2
+ export const MAX_HTML_BYTES = 2 * 1024 * 1024;
3
+ export const MAX_ASSET_BYTES = 8 * 1024 * 1024;
4
+ export const MAX_TOTAL_ASSET_BYTES = 24 * 1024 * 1024;
5
+ export const MAX_ASSETS = 24;
6
+ export function normalizeTitle(value, fallback) {
7
+ const title = value?.trim() || fallback.trim() || '交互式可视化';
8
+ return title.replace(/[\u0000-\u001f\u007f]/g, ' ').replace(/\s+/g, ' ').slice(0, 120);
9
+ }
10
+ export function validateAssetName(value) {
11
+ const name = value.normalize('NFC');
12
+ if (name.length === 0 || name.length > 160 || name === '.' || name === '..') {
13
+ throw new Error(`invalid visualization asset name: ${JSON.stringify(value)}`);
14
+ }
15
+ if (/[\\/\u0000-\u001f\u007f]/.test(name)) {
16
+ throw new Error(`visualization asset name contains an unsafe character: ${JSON.stringify(value)}`);
17
+ }
18
+ return name;
19
+ }
20
+ export function visualizationUrl(meta) {
21
+ return `/api/dsh-visualize/artifacts/${encodeURIComponent(meta.sessionId)}/${encodeURIComponent(meta.artifactId)}/index.html`;
22
+ }
23
+ export function injectVisualizationBridge(html, artifactId) {
24
+ const script = `<script data-dsh-visualize-bridge>(()=>{\n`
25
+ + `const source=${JSON.stringify(BRIDGE_SOURCE)};const artifactId=${JSON.stringify(artifactId)};\n`
26
+ + `let frame=0;const measure=()=>{frame=0;const d=document.documentElement,b=document.body;const height=Math.max(d?.offsetHeight||0,b?.scrollHeight||0,b?.offsetHeight||0,Math.ceil(b?.getBoundingClientRect().height||0));parent.postMessage({source,type:'resize',artifactId,height},'*')};\n`
27
+ + `const schedule=()=>{if(frame===0)frame=requestAnimationFrame(measure)};\n`
28
+ + `addEventListener('load',schedule);addEventListener('resize',schedule);\n`
29
+ + `addEventListener('message',(event)=>{if(event.source!==parent||event.data?.source!==source||event.data?.type!=='theme')return;const theme=event.data.theme==='dark'?'dark':'light';document.documentElement.dataset.theme=theme;document.documentElement.style.colorScheme=theme;schedule()});\n`
30
+ + `new ResizeObserver(schedule).observe(document.documentElement);if(document.body)new ResizeObserver(schedule).observe(document.body);schedule();\n`
31
+ + `})()</script>`;
32
+ const closingBody = /<\/body\s*>/i;
33
+ return closingBody.test(html) ? html.replace(closingBody, `${script}</body>`) : `${html}${script}`;
34
+ }
35
+ export function parseVisualizationMeta(value) {
36
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
37
+ return undefined;
38
+ const meta = value;
39
+ if (meta.kind !== 'dsh-visualization' || meta.version !== 1)
40
+ return undefined;
41
+ if (typeof meta.sessionId !== 'string' || meta.sessionId.length === 0 || meta.sessionId.length > 200)
42
+ return undefined;
43
+ if (typeof meta.artifactId !== 'string'
44
+ || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(meta.artifactId)) {
45
+ return undefined;
46
+ }
47
+ if (typeof meta.title !== 'string' || meta.title.length === 0 || meta.title.length > 120)
48
+ return undefined;
49
+ if (!Number.isSafeInteger(meta.assetCount) || (meta.assetCount ?? -1) < 0 || (meta.assetCount ?? 0) > MAX_ASSETS) {
50
+ return undefined;
51
+ }
52
+ return meta;
53
+ }
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "@dfy-plugins/dsh-visualize",
3
+ "version": "0.1.2",
4
+ "description": "在 DeepSeek Harness 对话中以会话级 artifact 安全展示交互式 HTML 可视化。",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ },
13
+ "./client": {
14
+ "default": "./lib/client.js"
15
+ },
16
+ "./package.json": "./package.json"
17
+ },
18
+ "files": [
19
+ "lib",
20
+ "cordis.patch.yml"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public",
24
+ "registry": "https://registry.npmjs.org/"
25
+ },
26
+ "license": "MIT",
27
+ "peerDependencies": {
28
+ "@deepseek-ai/cordis": "^4.0.1",
29
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.1-rc.2 || ^0.1.2-alpha.1 || ^0.1.5-alpha.1",
30
+ "@deepseek-ai/dsh-client-ui-tool": "^0.1.1-rc.2 || ^0.1.2-alpha.1 || ^0.1.5-alpha.1",
31
+ "@deepseek-ai/dsh-fs": "^0.1.1-rc.2 || ^0.1.2-alpha.1 || ^0.1.5-alpha.1",
32
+ "@deepseek-ai/dsh-host-webserver": "^0.1.1-rc.2 || ^0.1.2-alpha.1 || ^0.1.5-alpha.1",
33
+ "@deepseek-ai/dsh-session-persistence": "^0.1.1-rc.2 || ^0.1.2-alpha.1 || ^0.1.5-alpha.1",
34
+ "@deepseek-ai/dsh-skill": "^0.1.1-rc.2 || ^0.1.2-alpha.1 || ^0.1.5-alpha.1",
35
+ "@deepseek-ai/dsh-tools": "^0.1.1-rc.2 || ^0.1.2-alpha.1 || ^0.1.5-alpha.1",
36
+ "react": "^18.2.0"
37
+ },
38
+ "devDependencies": {
39
+ "@deepseek-ai/cordis": "^4.0.1",
40
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.1-rc.2",
41
+ "@deepseek-ai/dsh-client-ui-tool": "^0.1.1-rc.2",
42
+ "@deepseek-ai/dsh-fs": "^0.1.1-rc.2",
43
+ "@deepseek-ai/dsh-host-webserver": "^0.1.1-rc.2",
44
+ "@deepseek-ai/dsh-session-persistence": "^0.1.1-rc.2",
45
+ "@deepseek-ai/dsh-skill": "^0.1.1-rc.2",
46
+ "@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
47
+ "@types/node": "^24.0.0",
48
+ "@types/react": "~18.3.1",
49
+ "esbuild": "^0.24.0",
50
+ "react": "^18.2.0",
51
+ "typescript": "^5.5.0"
52
+ },
53
+ "dsh": {
54
+ "bundle": {
55
+ "patch": "./cordis.patch.yml"
56
+ },
57
+ "client": {
58
+ "platform": "web",
59
+ "inject": [
60
+ "@deepseek-ai/dsh-client-ui-primitives",
61
+ "@deepseek-ai/dsh-client-ui-tool"
62
+ ]
63
+ }
64
+ },
65
+ "dependencies": {
66
+ "@dfy-plugins/resource-core": "^0.1.1"
67
+ },
68
+ "repository": {
69
+ "type": "git",
70
+ "url": "git+https://github.com/xiaoxiao44443/dfy-dsh-plugins.git",
71
+ "directory": "plugins/visualize"
72
+ },
73
+ "homepage": "https://github.com/xiaoxiao44443/dfy-dsh-plugins#readme",
74
+ "bugs": {
75
+ "url": "https://github.com/xiaoxiao44443/dfy-dsh-plugins/issues"
76
+ },
77
+ "keywords": [
78
+ "deepseek-harness",
79
+ "dsh-plugin"
80
+ ],
81
+ "scripts": {
82
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json --noEmit",
83
+ "build": "tsc -p tsconfig.json && node scripts/build-client.mjs",
84
+ "test": "tsc -p tsconfig.json && node --test"
85
+ }
86
+ }