@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 +18 -0
- package/README.md +10 -4
- package/README.zh-CN.md +9 -4
- package/client/client.js +43 -70
- package/package.json +2 -2
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&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
|
|
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&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="
|
|
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
|
-
| 用户控制 |
|
|
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
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
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
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
|
|
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 =
|
|
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: "
|
|
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: "
|
|
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", "
|
|
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.
|
|
4
|
-
"description": "Persistent inline image, video, and audio previews for DeepSeek Harness Web conversations, with workspace-confined local reads and
|
|
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"
|