@wisdoverse/dsh-inline-media-viewer 1.0.2 → 1.0.5

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/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.5
4
+
5
+ - Register the browser bundle under the scoped package name expected by the
6
+ DSH module loader.
7
+
8
+ ## 1.0.4
9
+
10
+ - Read media candidates from the standard assistant-step projection so the
11
+ first inline preview renders reliably.
12
+ - Avoid extracting duplicate paths from Markdown links, code spans, and URLs.
13
+
14
+ ## 1.0.3
15
+
16
+ - Clarify and enforce the transport split: workspace files use the DSH host,
17
+ regular HTTP(S) media loads directly, and only recognized ComfyUI URLs use
18
+ the optional proxy.
19
+ - Mark ComfyUI as optional throughout the bilingual settings UI and README.
20
+
3
21
  ## 1.0.2
4
22
 
5
23
  - Make the settings navigation and all setting copy react live to DSH's
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
  <a href="CHANGELOG.md"><img alt="Latest tag" src="https://img.shields.io/github/v/tag/Wisdoverse/dsh-inline-media-viewer-plugin?style=flat-square&amp;label=version"></a>
9
9
  <a href="LICENSE"><img alt="License" src="https://img.shields.io/github/license/Wisdoverse/dsh-inline-media-viewer-plugin?style=flat-square"></a>
10
10
  <a href="package.json"><img alt="Top language" src="https://img.shields.io/github/languages/top/Wisdoverse/dsh-inline-media-viewer-plugin?style=flat-square"></a>
11
- <a href="#configuration"><img alt="ComfyUI support" src="https://img.shields.io/badge/ComfyUI-supported-ff6f00?style=flat-square"></a>
11
+ <a href="#configuration"><img alt="Optional ComfyUI" src="https://img.shields.io/badge/ComfyUI-optional-ff6f00?style=flat-square"></a>
12
12
  </p>
13
13
 
14
14
  <p align="center">
@@ -40,9 +40,14 @@ audio/sound.mp3 → <audio controls>
40
40
  | --- | --- |
41
41
  | Inline rendering | Displays images, videos, and audio directly below the chat turn that mentions them. |
42
42
  | Workspace-safe reads | Resolves local paths with `realpath` and rejects traversal, out-of-workspace paths, and symlink escapes. |
43
- | ComfyUI proxy | Fetches recognized ComfyUI media URLs server-side from the configured origin, including for remote users and HTTPS pages. |
43
+ | Optional ComfyUI proxy | Fetches recognized ComfyUI media URLs server-side from the configured origin, including for remote users and HTTPS pages. |
44
44
  | Bounded transfers | Limits each file or response to 48 MiB and remote requests to 20 seconds. |
45
- | User controls | Provides auto-render, per-turn item limit, media height, and ComfyUI origin settings. |
45
+ | User controls | Provides auto-render, per-turn item limit, media height, and optional ComfyUI origin settings. |
46
+
47
+ > [!NOTE]
48
+ > ComfyUI is not required. Workspace files and regular HTTP(S) media URLs work
49
+ > when ComfyUI is not installed or running; its proxy is activated only for
50
+ > recognized ComfyUI URLs.
46
51
 
47
52
  ## Supported media
48
53
 
@@ -105,7 +110,7 @@ switch live between English and Chinese.
105
110
  | Auto-render detected media | On | On / off |
106
111
  | Maximum items per turn | `12` | `1`–`30` |
107
112
  | Maximum media height | `380 px` | `160`–`1200 px` |
108
- | ComfyUI origin | Empty | `http(s)://host[:port]` |
113
+ | ComfyUI origin (optional) | Empty | `http(s)://host[:port]` |
109
114
 
110
115
  Settings are stored in the DSH settings document. Writes are available only on
111
116
  loopback connections; remote browsers can read settings but cannot persist
@@ -113,6 +118,7 @@ changes.
113
118
 
114
119
  ### ComfyUI origin
115
120
 
121
+ - Skip this setting entirely if you do not use ComfyUI.
116
122
  - Leave the field empty to use `http://127.0.0.1:8188`.
117
123
  - Accepted forms are `http://host[:port]`, `https://host[:port]`, and bare
118
124
  `host[:port]` (`http` is assumed).
package/README.zh-CN.md CHANGED
@@ -8,7 +8,7 @@
8
8
  <a href="CHANGELOG.md"><img alt="最新标签" src="https://img.shields.io/github/v/tag/Wisdoverse/dsh-inline-media-viewer-plugin?style=flat-square&amp;label=version"></a>
9
9
  <a href="LICENSE"><img alt="许可证" src="https://img.shields.io/github/license/Wisdoverse/dsh-inline-media-viewer-plugin?style=flat-square"></a>
10
10
  <a href="package.json"><img alt="主要语言" src="https://img.shields.io/github/languages/top/Wisdoverse/dsh-inline-media-viewer-plugin?style=flat-square"></a>
11
- <a href="#配置"><img alt="支持 ComfyUI" src="https://img.shields.io/badge/ComfyUI-supported-ff6f00?style=flat-square"></a>
11
+ <a href="#配置"><img alt="可选 ComfyUI" src="https://img.shields.io/badge/ComfyUI-optional-ff6f00?style=flat-square"></a>
12
12
  </p>
13
13
 
14
14
  <p align="center">
@@ -39,9 +39,13 @@ audio/sound.mp3 → <audio controls>
39
39
  | --- | --- |
40
40
  | 内联渲染 | 在提及媒体的聊天消息下方直接显示图像、视频和音频。 |
41
41
  | 工作区安全读取 | 使用 `realpath` 解析本地路径,并拒绝目录穿越、工作区外路径和符号链接逃逸。 |
42
- | ComfyUI 代理 | 由服务端从配置的源站获取识别到的 ComfyUI 媒体 URL,使远程用户和 HTTPS 页面也能正常查看。 |
42
+ | 可选 ComfyUI 代理 | 由服务端从配置的源站获取识别到的 ComfyUI 媒体 URL,使远程用户和 HTTPS 页面也能正常查看。 |
43
43
  | 资源限制 | 每个文件或响应最大 48 MiB,远程请求最长 20 秒。 |
44
- | 用户控制 | 提供自动渲染、每条消息的媒体数量、媒体高度和 ComfyUI 源站设置。 |
44
+ | 用户控制 | 提供自动渲染、每条消息的媒体数量、媒体高度和可选的 ComfyUI 源站设置。 |
45
+
46
+ > [!NOTE]
47
+ > ComfyUI 不是必需依赖。即使没有安装或运行 ComfyUI,工作区文件和普通 HTTP(S)
48
+ > 媒体 URL 仍可正常使用;只有识别为 ComfyUI 的 URL 才会启用代理。
45
49
 
46
50
  ## 支持的媒体格式
47
51
 
@@ -102,12 +106,13 @@ dsh plugin --profile web add @wisdoverse/dsh-inline-media-viewer
102
106
  | 自动渲染检测到的媒体 | 开启 | 开启 / 关闭 |
103
107
  | 每条消息的最大媒体数量 | `12` | `1`–`30` |
104
108
  | 媒体最大高度 | `380 px` | `160`–`1200 px` |
105
- | ComfyUI 源站 | 留空 | `http(s)://host[:port]` |
109
+ | ComfyUI 源站(可选) | 留空 | `http(s)://host[:port]` |
106
110
 
107
111
  设置值保存在 DSH 设置文档中。只有回环连接可以写入;远程浏览器可以读取设置,但不能持久化修改。
108
112
 
109
113
  ### ComfyUI 源站
110
114
 
115
+ - 不使用 ComfyUI 时无需配置此项。
111
116
  - 留空时使用 `http://127.0.0.1:8188`。
112
117
  - 支持 `http://host[:port]`、`https://host[:port]` 和不带协议的
113
118
  `host[:port]`(默认使用 `http`)。
package/client/client.js CHANGED
@@ -1,5 +1,5 @@
1
1
  window.__ModuleLoader__.load({
2
- id: "dsh-inline-media-viewer",
2
+ id: "@wisdoverse/dsh-inline-media-viewer",
3
3
  factory: (require) => {
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
@@ -9,7 +9,6 @@ window.__ModuleLoader__.load({
9
9
  const { createElement: h, useEffect, useMemo, useState } = React;
10
10
  const CHANNEL = "/inline-media";
11
11
  const ENDPOINT = "read";
12
- const DATA_KEY = "inlineMedia";
13
12
  const DISPLAY_CAP = 12;
14
13
  const SETTINGS_NAMESPACE = "inline-media";
15
14
  // Empty config = the host-side built-in default
@@ -88,79 +87,48 @@ window.__ModuleLoader__.load({
88
87
  found.push({ source, kind });
89
88
  };
90
89
 
91
- for (const match of text.matchAll(/!?\[[^\]]*\]\(([^)]+)\)/g)) add(match[1]);
92
- for (const match of text.matchAll(/`([^`\n]+)`/g)) add(match[1]);
93
- for (const match of text.matchAll(/https?:\/\/[^\s<>"'`]+/gi)) add(match[0]);
90
+ let plain = text.replace(/!?\[[^\]]*\]\(([^)]+)\)/g, (match, source) => {
91
+ add(source);
92
+ return " ".repeat(match.length);
93
+ });
94
+ plain = plain.replace(/`([^`\n]+)`/g, (match, source) => {
95
+ add(source);
96
+ return " ".repeat(match.length);
97
+ });
98
+ plain = plain.replace(/https?:\/\/[^\s<>"'`]+/gi, (source) => {
99
+ add(source);
100
+ return " ".repeat(source.length);
101
+ });
94
102
 
95
103
  const ext = Array.from(MEDIA_EXTENSIONS).join("|");
96
104
  const pathPattern = new RegExp(
97
105
  String.raw`(?:\/|\.\.?\/|[\w.-]+\/)[^\s<>"'\x60()\[\]{}]+?\.(?:${ext})(?:\?[^\s<>"'\x60]*)?`,
98
106
  "gi",
99
107
  );
100
- for (const match of text.matchAll(pathPattern)) add(match[0]);
108
+ for (const match of plain.matchAll(pathPattern)) add(match[0]);
101
109
  return found;
102
110
  }
103
111
 
104
- function assistantText(event) {
105
- if (event.type !== "assistant/message") return "";
106
- const content = event.data && event.data.message && event.data.message.content;
107
- if (!Array.isArray(content)) return "";
108
- return content
109
- .filter((block) => block && block.type === "text" && typeof block.text === "string")
110
- .map((block) => block.text)
111
- .join("\n");
112
- }
113
-
114
- const mediaDefinition = {
115
- kind: DATA_KEY,
116
- match(event) {
117
- if (event.type === "turn/start") {
118
- return { id: String(event.data.turn), role: "start" };
119
- }
120
- if (event.type === "assistant/message" && Number.isInteger(event.data && event.data.turn)) {
121
- return { id: String(event.data.turn), role: "update" };
122
- }
123
- return null;
124
- },
125
- start(_context, match) {
126
- return { turn: match.event.data.turn, candidates: [] };
127
- },
128
- update(context, match) {
129
- const additions = extractCandidates(assistantText(match.event)).map((candidate) => ({
130
- ...candidate,
131
- seq: match.event.seq,
132
- }));
133
- if (additions.length === 0) return context.state;
134
- const seen = new Set(context.state.candidates.map((candidate) => candidate.source));
135
- const fresh = additions.filter((candidate) => {
136
- if (seen.has(candidate.source)) return false;
137
- seen.add(candidate.source);
138
- return true;
139
- });
140
- if (fresh.length === 0) return context.state;
141
- return {
142
- ...context.state,
143
- candidates: [...context.state.candidates, ...fresh].slice(0, DISPLAY_CAP),
144
- };
145
- },
146
- buildLocationData(context, scope) {
147
- if (scope !== "turn" || context.state === undefined) return null;
148
- return {
149
- kind: "turn",
150
- turn: context.state.turn,
151
- key: DATA_KEY,
152
- value: { candidates: context.state.candidates },
153
- };
154
- },
155
- };
156
-
157
112
  function selectMedia(owner) {
158
113
  const settings = readSettings();
159
114
  if (!settings.autoRender) return null;
160
- const data = owner.turn.data.get(DATA_KEY);
161
- if (!data || !Array.isArray(data.candidates)) return null;
162
- const candidates = data.candidates.filter((candidate) => candidate.seq <= owner.seq);
163
- return candidates.length === 0 ? null : candidates.slice(0, settings.displayCap);
115
+ const candidates = [];
116
+ const seen = new Set();
117
+ for (const step of owner.turn.steps) {
118
+ const assistant = step.data.get("assistant-step");
119
+ if (!assistant || !assistant.finalNode || assistant.finalNode.seq > owner.seq || !Array.isArray(assistant.blocks)) continue;
120
+ const text = assistant.blocks
121
+ .filter((block) => block && block.kind === "text" && typeof block.text === "string")
122
+ .map((block) => block.text)
123
+ .join("\n");
124
+ for (const candidate of extractCandidates(text)) {
125
+ if (seen.has(candidate.source)) continue;
126
+ seen.add(candidate.source);
127
+ candidates.push(candidate);
128
+ if (candidates.length === settings.displayCap) return candidates;
129
+ }
130
+ }
131
+ return candidates.length === 0 ? null : candidates;
164
132
  }
165
133
 
166
134
  function normalizeComfyOrigin(input) {
@@ -197,6 +165,11 @@ window.__ModuleLoader__.load({
197
165
  }
198
166
  }
199
167
 
168
+ function mediaTransport(source, comfyUrl) {
169
+ if (!/^https?:\/\//i.test(source)) return "workspace";
170
+ return isComfySource(source, comfyUrl) ? "comfy-proxy" : "direct";
171
+ }
172
+
200
173
  function basename(source) {
201
174
  try {
202
175
  const url = new URL(source);
@@ -209,7 +182,7 @@ window.__ModuleLoader__.load({
209
182
  }
210
183
 
211
184
  function MediaCard({ candidate, connection, sessionId, imageMaxPx, comfyUrl }) {
212
- const proxied = !/^https?:\/\//i.test(candidate.source) || isComfySource(candidate.source, comfyUrl);
185
+ const proxied = mediaTransport(candidate.source, comfyUrl) !== "direct";
213
186
  const [state, setState] = useState(() => proxied
214
187
  ? { status: "loading", src: "" }
215
188
  : { status: "ready", src: candidate.source });
@@ -385,8 +358,8 @@ window.__ModuleLoader__.load({
385
358
  displayCapHint: "1–30",
386
359
  imageMaxPx: "媒体最大高度 (px)",
387
360
  imageMaxPxHint: "160–1200",
388
- comfyUrl: "ComfyUI 地址",
389
- comfyUrlHint: "主机代理拉流的服务器地址,格式 http(s)://host[:port];留空使用内置默认地址。",
361
+ comfyUrl: "ComfyUI 地址(可选)",
362
+ comfyUrlHint: "仅用于 ComfyUI 链接;不使用 ComfyUI 可保持为空,本地文件和普通网络 URL 不受影响。",
390
363
  comfyUrlError: "地址无效,请使用 http://host:8188 这类格式。",
391
364
  reset: "恢复默认",
392
365
  writable: "当前连接不可回写:设置仅临时保存在本会话,刷新后恢复默认。",
@@ -400,8 +373,8 @@ window.__ModuleLoader__.load({
400
373
  displayCapHint: "1–30",
401
374
  imageMaxPx: "Max media height (px)",
402
375
  imageMaxPxHint: "160–1200",
403
- comfyUrl: "ComfyUI address",
404
- comfyUrlHint: "Origin the host proxy fetches media from: http(s)://host[:port]; empty uses the built-in default.",
376
+ comfyUrl: "ComfyUI address (optional)",
377
+ comfyUrlHint: "Used only for ComfyUI links. Leave empty when not using ComfyUI; local files and regular web URLs work independently.",
405
378
  comfyUrlError: "Invalid address; use the http://host:8188 form.",
406
379
  reset: "Reset to defaults",
407
380
  writable: "This connection cannot write settings: values are session-local until a loopback connection is used.",
@@ -523,7 +496,7 @@ window.__ModuleLoader__.load({
523
496
  }
524
497
 
525
498
  const name = "dsh-inline-media-viewer";
526
- const inject = ["slots", "conversationEvents", "connection", "settingsScope", "locale"];
499
+ const inject = ["slots", "connection", "settingsScope", "locale"];
527
500
 
528
501
  function apply(ctx) {
529
502
  const connection = ctx.get("connection");
@@ -531,7 +504,6 @@ window.__ModuleLoader__.load({
531
504
  const t = ctx.locale.bind(SETTINGS_NS);
532
505
  const scope = ctx.settingsScope.bind({ namespace: SETTINGS_NAMESPACE });
533
506
  settingsScope = scope;
534
- ctx.conversationEvents.register(mediaDefinition);
535
507
  ctx.slots.inject("conversation.chat.turnTail", () => ctx.slots.register({
536
508
  name: "conversation.chat.turnTail",
537
509
  select: selectMedia,
@@ -550,6 +522,7 @@ window.__ModuleLoader__.load({
550
522
  exports.apply = apply;
551
523
  exports.inject = inject;
552
524
  exports.name = name;
525
+ exports.testing = Object.freeze({ extractCandidates, mediaTransport, selectMedia });
553
526
  return module.exports;
554
527
  },
555
528
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@wisdoverse/dsh-inline-media-viewer",
3
- "version": "1.0.2",
4
- "description": "Persistent inline image, video, and audio previews for DeepSeek Harness Web conversations, with workspace-confined local reads and a configurable ComfyUI proxy.",
3
+ "version": "1.0.5",
4
+ "description": "Persistent inline image, video, and audio previews for DeepSeek Harness Web conversations, with workspace-confined local reads and an optional ComfyUI proxy.",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/Wisdoverse/dsh-inline-media-viewer-plugin.git"