@jetecho/dsh-csv-and-image-preview 0.1.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/Agent.md ADDED
@@ -0,0 +1,39 @@
1
+ # Agent.md — dsh-csv-and-image-preview 行为契约
2
+
3
+ 这份文档定义当你(agent)使用 `dsh-csv-and-image-preview` 时应遵守的**预览式工作流**。它是给 agent 读的规则,搭配仓库里的独立 agent preset(`image-preview`)一起使用,把规则写进会话 persona。
4
+
5
+ ## 核心原则:先预览,后落地
6
+
7
+ 凡涉及**生成或修改图片 / SVG**,默认顺序必须是:
8
+
9
+ 1. **先预览** —— 调用 `preview_image`,把结果渲染到聊天里给用户看;
10
+ - 读磁盘文件:`path=<绝对路径>`
11
+ - 内联 SVG(未落盘):`content=<svg ...> mime=image/svg+xml`
12
+ - 任意资源:直接传 `path`
13
+ 2. **等确认** —— 预览后停止写入,等待用户明确同意;
14
+ 3. **用户认可**(如 `确认` / `可以` / `就这样`)后,才真正写文件 / 执行改动;
15
+ 4. **用户否决** 时,按反馈修改并**再次预览**,直到认可为止。
16
+
17
+ ## 不要做的事
18
+
19
+ - 不要在没预览的情况下,直接覆盖或新增一个图片 / SVG 文件。
20
+ - 不要把图片字节塞进回复文本或模型上下文 —— 那是 meta 的活,你只需拿到紧凑摘要。
21
+ - 不要用 markdown `![alt](data:...)` 或 ```` ```dsh-ui ```` 的 `image` 组件来"显示图片"—— 聊天渲染器会拦截,必须走 `preview_image` 工具。
22
+
23
+ ## 交互话术
24
+
25
+ - 预览后:**"这是预览,请确认后再写入。"** 或列出的差异点。
26
+ - 等待确认时,保持在工作流里,不要擅自落地。
27
+ - 确认后:**"已按预览图写入 `path`。"**
28
+
29
+ ## 插件能力
30
+
31
+ | 能力 | 说明 |
32
+ |---|---|
33
+ | `preview_image` 工具 | 读取文件 / 内联 SVG → data-URI → 浏览器 `<img>` 渲染 |
34
+ | 键控 toolview | `tool.call.toolview` key=`preview_image`,渲染原生 `<img>` |
35
+ | meta 通道 | 图片字节只走 `presentationMeta` 进浏览器,不进模型上下文 |
36
+
37
+ ## 为什么
38
+
39
+ 聊天渲染器不渲染 markdown 或 genui 里的图片(白名单里也没有 `image` 组件)。`React.createElement('img')` 生成的是浏览器原生 `<img>`,不经过那套过滤,所以能真实显示。预览式工作流让用户**先看到结果再决策**,避免"生成即覆写"的返工。
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 jetecho
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.en.md ADDED
@@ -0,0 +1,73 @@
1
+ # dsh-csv-and-image-preview
2
+
3
+ English | [中文](README.md)
4
+
5
+ Render images and SVG **for real** in the DeepSeek Harness (dsh) chat — and prepare CSV review — displayed as a native browser `<img>`, bypassing the chat renderer's markdown / genui image filter, and paired with a **preview-first workflow**: generate or modify an image, render it to you first, and only apply the real change once you confirm.
6
+
7
+ ## Install
8
+
9
+ Standard Profile Bundle install (recommended):
10
+
11
+ ```sh
12
+ dsh plugin --profile web add dsh-csv-and-image-preview
13
+ ```
14
+
15
+ Or install the development copy from a local directory:
16
+
17
+ ```sh
18
+ dsh plugin --profile web add ../dsh-csv-and-image-preview
19
+ ```
20
+
21
+ After installing, restart `dsh web` so the new bundle joins the runtime. Legacy fallback for older versions: copy the package into the profile's `node_modules` and insert the `- insert: [...]` row in `cordis.patch.yml` — keep this for old-version compatibility only.
22
+
23
+ ## What it does
24
+
25
+ 1. **Real image rendering.** The host registers a `preview_image` tool: it reads a file path (or inline SVG), infers the MIME, base64-encodes it into a data-URI, and projects it through the tool result's `presentationMeta`. The browser-side keyed toolview (`tool.call.toolview`, key `preview_image`) reads that meta and renders a native `<img>`. The image bytes travel only through meta into the browser — **never into the model context**.
26
+ 2. **Preview-first workflow.** After calling `preview_image`, the agent receives a compact summary (SVG/bitmap, dimensions, byte count) while the actual image is shown in chat; the agent then **waits for your confirmation** before writing files or applying changes.
27
+ 3. **Preview any resource.** Pass a local path to preview the matching image file, or pass inline SVG text to render without writing to disk.
28
+
29
+ ## Usage examples
30
+
31
+ ```markdown
32
+ <!-- Preview a file on disk -->
33
+ I'll show you the preview first.
34
+ [call preview_image path=E:\DSHConfig\logo-a.svg label=ALPHA]
35
+ ```
36
+
37
+ ```markdown
38
+ <!-- Preview inline SVG (not yet on disk) -->
39
+ [call preview_image content=<svg ...> mime=image/svg+xml label=sketch]
40
+ ```
41
+
42
+ The agent sees something like `已渲染预览「ALPHA」:SVG 矢量图(1.3 KB).` while the picture appears in the tool card.
43
+
44
+ ## Convention: preview first
45
+
46
+ This plugin treats "preview, then apply" as the default behavior:
47
+
48
+ - When generating / modifying an SVG, image, or CSV, **preview it first** with `preview_image`; do not write the real file yet.
49
+ - Only after the user confirms (`确认` / `可以` / `就这样`) do you actually write / mutate.
50
+ - If the user rejects it, redo per feedback and preview again.
51
+ - To preview any resource, pass `path` directly.
52
+
53
+ To enforce this convention, pair it with the bundled agent preset (see `Agent.md`), which writes these rules into the session persona.
54
+
55
+ ## Layout
56
+
57
+ ```
58
+ lib/index.js host: registers the preview_image tool; reads files and projects data-URIs
59
+ lib/client.js browser bundle: registers the keyed toolview; renders a native <img>
60
+ cordis.patch.yml profile insert line
61
+ ```
62
+
63
+ ## Development
64
+
65
+ ```sh
66
+ pnpm install
67
+ pnpm build # no-op: lib/ ships prebuilt, no transpile
68
+ pnpm prepack # verify the packed tarball contains lib/
69
+ ```
70
+
71
+ ## License
72
+
73
+ MIT © [jetecho](./LICENSE). See [LICENSE](./LICENSE).
package/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # dsh-csv-and-image-preview
2
+
3
+ [English](README.en.md) | 中文
4
+
5
+ 在 DeepSeek Harness (dsh) 聊天里**真正把图片 / SVG 显示出来**的插件——并支持 CSV 数据先行预览。它把图片以浏览器原生 `<img>` 渲染,绕过聊天渲染器对 markdown / genui 图片的过滤;同时内置**预览式工作流**:生成或修改图片或 CSV 时,先把结果渲染给你看,等你确认后再落地真正的改动。
6
+
7
+ ## 安装
8
+
9
+ 标准 Profile Bundle 安装(推荐):
10
+
11
+ ```sh
12
+ dsh plugin --profile web add dsh-csv-and-image-preview
13
+ ```
14
+
15
+ 或从本地目录安装开发版:
16
+
17
+ ```sh
18
+ dsh plugin --profile web add ../dsh-csv-and-image-preview
19
+ ```
20
+
21
+ 安装后需重启 `dsh web` 让新 bundle 进入运行时。更早版本兼容做法是手动把包复制进 profile 的 `node_modules` 并在 `cordis.patch.yml` 插入 `- insert: [...]`,仅作旧版本兜底。
22
+
23
+ ## 它做什么
24
+
25
+ 1. **真渲染图片**。宿主注册 `preview_image` 工具:读取文件路径(或内联 SVG),推断 MIME、base64 编码成 data-URI,通过工具结果的 `presentationMeta` 投递;浏览器端键控 toolview(`tool.call.toolview` key=`preview_image`)读取 meta 并渲染原生 `<img>`。图片字节只走 meta 进浏览器,**不进入模型上下文**。
26
+ 2. **预览式工作流**。调用 `preview_image` 后,脚本拿到的是紧凑摘要(SVG/位图、尺寸、字节数),真正的画面直接显示在聊天里;脚本会「等待用户确认」,等你点头后才写入文件/执行改动。
27
+ 3. **预览任意资源**。给一个本地路径即可预览对应图片文件;也能传入内联 SVG 文本直接渲染,无需落盘。
28
+
29
+ ## 用法示例
30
+
31
+ ```markdown
32
+ <!-- 预览磁盘上的 logo -->
33
+ 我先给你看预览。
34
+ [调用 preview_image path=E:\DSHConfig\logo-a.svg label=ALPHA]
35
+ ```
36
+
37
+ ```markdown
38
+ <!-- 预览内联 SVG(未落盘) -->
39
+ [调用 preview_image content=<svg ...> mime=image/svg+xml label=草图]
40
+ ```
41
+
42
+ 脚本会读到类似 `已渲染预览「ALPHA」:SVG 矢量图(1.3 KB)。` 的返回,图片则显示在工具卡片里。
43
+
44
+ ## 约定:预览优先
45
+
46
+ 本插件把「先预览、后落地」作为默认约定:
47
+
48
+ - 生成 / 修改 SVG、图片或 CSV 时,**先** `preview_image` 渲染,不做真实写入;
49
+ - 用户确认(`确认` / `可以` / `就这样`)后,才真正写文件 / 执行改动;
50
+ - 用户否决时,按反馈重做并再次预览;
51
+ - 需要预览任意资源时,直接传 `path`。
52
+
53
+ 要强制遵守这一约定,可以配合附带的 agent preset(见 `Agent.md`),它会把这套规则写进会话 persona。
54
+
55
+ ## 组成部分
56
+
57
+ ```
58
+ lib/index.js 宿主:注册 preview_image 工具,读取文件并投递 data-URI
59
+ lib/client.js 浏览器 bundle:注册键控 toolview,渲染原生 <img>
60
+ cordis.patch.yml profile 插入行
61
+ ```
62
+
63
+ ## 开发
64
+
65
+ ```sh
66
+ pnpm install
67
+ pnpm build # no-op:lib/ 为成品,无需转译
68
+ pnpm prepack # 校验打包内容含 lib/
69
+ ```
70
+
71
+ ## License
72
+
73
+ MIT © [jetecho](./LICENSE),详见 [LICENSE](./LICENSE)。
@@ -0,0 +1,6 @@
1
+ # dsh-csv-and-image-preview bundle patch: one host row over the web profile.
2
+ # The row name resolves from the profile's own node_modules where
3
+ # `dsh plugin --profile web add <path>` installed this package.
4
+ - insert:
5
+ - id: csv-and-image-preview
6
+ name: dsh-csv-and-image-preview
package/lib/client.js ADDED
@@ -0,0 +1,83 @@
1
+ /**
2
+ * dsh-csv-and-image-preview 浏览器端 bundle(单文件,经 __ModuleLoader__ 加载)。
3
+ *
4
+ * 为 `preview_image` 工具注册键控 toolview(`tool.call.toolview`, key =
5
+ * 'preview_image'):宿主把 data-URI 投递到结果节点的 meta,这里读取它并渲染
6
+ * 成浏览器原生 <img>。这样图片不经过聊天渲染器的 markdown/genui 白名单,
7
+ * 直接以真实 DOM 显示。
8
+ *
9
+ * 样式使用 --dsw-* 主题变量,跟随全局亮/暗主题。
10
+ * @module dsh-csv-and-image-preview/client
11
+ */
12
+
13
+ window.__ModuleLoader__.load({
14
+ id: 'dsh-csv-and-image-preview',
15
+ factory: (require) => {
16
+ var module = { exports: {} }
17
+ var exports = module.exports
18
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
19
+
20
+ const React = require('react')
21
+
22
+ // ── 样式 ────────────────────────────────────────────────────────────────
23
+ const css = [
24
+ '/* dsh-csv-and-image-preview */',
25
+ '.dip-root{display:flex;flex-direction:column;gap:6px;align-items:flex-start;max-width:100%}',
26
+ '.dip-label{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:20px}',
27
+ '.dip-figure{margin:0;padding:8px;border:1px solid var(--dsw-alias-border-l1);border-radius:12px;background:var(--dsw-alias-bg-layer-1);max-width:100%}',
28
+ '.dip-img{display:block;max-width:100%;height:auto;border-radius:8px}',
29
+ '.dip-img.svg{background:#fff}',
30
+ '.dip-missing{font-size:12px;color:var(--dsw-alias-label-tertiary)}',
31
+ ].join('\n')
32
+ if (typeof document !== 'undefined') {
33
+ const key = 'dip-' + 'dsh-csv-and-image-preview'
34
+ if (document.querySelector('style[data-plugin-css="' + key + '"]') === null) {
35
+ const el = document.createElement('style')
36
+ el.dataset.plugin = 'dsh-csv-and-image-preview'
37
+ el.dataset.pluginCss = key
38
+ el.textContent = css
39
+ document.head.appendChild(el)
40
+ }
41
+ }
42
+
43
+ /**
44
+ * 键控 toolview:从结果节点 `block.meta` 读取宿主投递的 { src, mime, label }。
45
+ * meta 仅在 settled 结果节点上存在;running 调用或回退时显示摘要行。
46
+ */
47
+ function PreviewToolView(props) {
48
+ const block = props && props.block
49
+ const meta = block !== null && typeof block === 'object' && 'meta' in block ? block.meta : undefined
50
+ const label = typeof meta === 'object' && meta !== null && typeof meta.label === 'string' ? meta.label : '预览'
51
+ const src = typeof meta === 'object' && meta !== null && typeof meta.src === 'string' ? meta.src : null
52
+ const isSvg = typeof meta === 'object' && meta !== null && meta.mime === 'image/svg+xml'
53
+ if (src === null) {
54
+ return React.createElement('div', { className: 'dip-root', 'data-dip-preview': true },
55
+ React.createElement('span', { className: 'dip-missing' }, '图片预览缺少数据(meta 缺失)'))
56
+ }
57
+ return React.createElement('div', { className: 'dip-root', 'data-dip-preview': true },
58
+ React.createElement('span', { className: 'dip-label' }, label),
59
+ React.createElement('figure', { className: 'dip-figure' },
60
+ React.createElement('img', {
61
+ className: 'dip-img' + (isSvg ? ' svg' : ''),
62
+ src: src,
63
+ alt: label,
64
+ })))
65
+ }
66
+
67
+ // ── 服务接入 ─────────────────────────────────────────────────────────────
68
+ const inject = []
69
+ function apply(ctx) {
70
+ const slots = ctx.get('slots')
71
+ if (slots === undefined) return
72
+ const dispose = slots.inject('tool.call.toolview', () => slots.register(
73
+ { name: 'tool.call.toolview', key: 'preview_image' },
74
+ PreviewToolView,
75
+ ))
76
+ return () => { dispose() }
77
+ }
78
+
79
+ exports.apply = apply
80
+ exports.inject = inject
81
+ return module.exports
82
+ },
83
+ })
package/lib/index.js ADDED
@@ -0,0 +1,191 @@
1
+ /**
2
+ * dsh-csv-and-image-preview 宿主插件。
3
+ *
4
+ * 单条 Loader 行(见 cordis.patch.yml)挂载本模块。职责:
5
+ * 1. 注册 `preview_image` 工具:给定一个文件路径(或内联 SVG),读取文件、
6
+ * 推断 MIME、base64 编码为 data-URI;
7
+ * 2. 把该 data-URI 通过工具结果的 `presentationMeta` 投递出去,浏览器端
8
+ * 键控 toolview 据此渲染原生 <img> —— 绕过聊天渲染器对图片的过滤;
9
+ * 3. `execute` 返回紧凑的模型可读摘要(尺寸/字节/说明),真正的图片字节
10
+ * 只走 meta,不进模型上下文。
11
+ *
12
+ * 注意:`output.presentationMeta` 必须**同步**返回 lossless JSON(它是展示
13
+ * 期别名,在结果落地时就读取,不能返回 Promise),因此这里用同步 fs 读取;
14
+ * `execute` 本身是 async,但内部调用同一个同步构建器。
15
+ *
16
+ * 不导入 cordis/dsh-* 运行时包中的 Service/Context 类:仅用 ctx API 与 Node
17
+ * 内建能力,与宿主进程共享同一套运行时实例。
18
+ * @module dsh-csv-and-image-preview
19
+ */
20
+
21
+ import { readFileSync, statSync } from 'node:fs'
22
+ import { resolve } from 'node:path'
23
+
24
+ export const name = 'csv-and-image-preview'
25
+
26
+ /** 扩展名 → MIME 推断表。 */
27
+ const MIME_BY_EXT = {
28
+ '.svg': 'image/svg+xml',
29
+ '.png': 'image/png',
30
+ '.jpg': 'image/jpeg',
31
+ '.jpeg': 'image/jpeg',
32
+ '.gif': 'image/gif',
33
+ '.webp': 'image/webp',
34
+ '.bmp': 'image/bmp',
35
+ '.ico': 'image/x-icon',
36
+ '.avif': 'image/avif',
37
+ }
38
+
39
+ /** 从路径扩得 MIME;未知回退 octet-stream。 */
40
+ function mimeOf(path) {
41
+ const dot = path.lastIndexOf('.')
42
+ if (dot < 0) return 'application/octet-stream'
43
+ return MIME_BY_EXT[path.slice(dot).toLowerCase()] ?? 'application/octet-stream'
44
+ }
45
+
46
+ /** 提取内联 SVG 的 width/height(viewBox 或 attr),用于摘要。 */
47
+ function svgDimension(svg) {
48
+ const w = /width="([0-9.]+)"/.exec(svg)
49
+ const h = /height="([0-9.]+)"/.exec(svg)
50
+ const vb = /viewBox="[^"]*?([0-9.]+)[ ,]+([0-9.]+)[ ,]+([0-9.]+)[ ,]+([0-9.]+)"/.exec(svg)
51
+ if (w && h) return `${w[1]}×${h[1]}`
52
+ if (vb) return `${vb[3]}×${vb[4]}`
53
+ return 'unknown'
54
+ }
55
+
56
+ /** 模型可读的单行摘要。 */
57
+ function svgSummary(mime, bytes) {
58
+ const size = bytes < 1024 ? `${bytes} B` : bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / (1024 * 1024)).toFixed(2)} MB`
59
+ return mime === 'image/svg+xml' ? 'SVG 矢量图' : mime.replace('image/', '').toUpperCase() + ' 位图'
60
+ }
61
+
62
+ /**
63
+ * 同步构建一次预览结果,返回 { src, mime, label, bytes, summary };有错抛 Error。
64
+ * 同步是因为 `presentationMeta` 必须同步返回 lossless JSON。
65
+ */
66
+ function buildPreviewSync(args) {
67
+ if (typeof args !== 'object' || args === null) {
68
+ throw new Error('preview_image 需要对象参数(带 path 或 content)。')
69
+ }
70
+ const label = typeof args.label === 'string' && args.label !== '' ? args.label : '预览'
71
+
72
+ // 优先:磁盘文件路径。
73
+ if (typeof args.path === 'string' && args.path !== '') {
74
+ const abs = resolve(args.path)
75
+ const mime = mimeOf(abs)
76
+ const buf = readFileSync(abs)
77
+ const st = statSync(abs)
78
+ const b64 = buf.toString('base64')
79
+ return {
80
+ src: `data:${mime};base64,${b64}`,
81
+ mime,
82
+ label,
83
+ bytes: st.size,
84
+ summary: svgSummary(mime, st.size),
85
+ }
86
+ }
87
+
88
+ // 备选:调用方直接给内容。
89
+ if (typeof args.content === 'string' && args.content !== '') {
90
+ const mime = args.mime === 'image/svg+xml' ? 'image/svg+xml' : (typeof args.mime === 'string' ? args.mime : 'image/svg+xml')
91
+ if (mime === 'image/svg+xml') {
92
+ const b64 = Buffer.from(args.content, 'utf8').toString('base64')
93
+ return { src: `data:image/svg+xml;base64,${b64}`, mime, label, bytes: b64.length, summary: `inline SVG ${svgDimension(args.content)}` }
94
+ }
95
+ // 假定 content 已是 base64。
96
+ return { src: `data:${mime};base64,${args.content}`, mime, label, bytes: args.content.length, summary: `inline ${mime}` }
97
+ }
98
+
99
+ throw new Error('preview_image 需要 path 或 content 参数。')
100
+ }
101
+
102
+ /**
103
+ * 构建 preview_image 工具定义。注册进工具注册表后,模型可用它发起一次
104
+ * “预览”:宿主读取图片并投递 data-URI 到 meta,浏览器端 toolview 渲染。
105
+ */
106
+ export function createPreviewTool() {
107
+ return {
108
+ name: 'preview_image',
109
+ description:
110
+ '在聊天里预览一张图片或 SVG——用浏览器原生 <img> 渲染,绕过聊天渲染器对图片的过滤。'
111
+ + ' 传 path 读取磁盘文件,或传 content(内联 SVG 文本)直接预览。'
112
+ + ' 图片字节只走 meta 投递到浏览器,不进入模型上下文;返回的是紧凑摘要。'
113
+ + ' 生成/修改图片时先调用它给用户预览,等用户确认后再做真正的写入/修改。',
114
+ parameters: {
115
+ type: 'object',
116
+ properties: {
117
+ path: {
118
+ type: 'string',
119
+ description: '要预览的图片文件路径(.svg/.png/.jpg/.gif/.webp 等)。优先用绝对路径。',
120
+ },
121
+ content: {
122
+ type: 'string',
123
+ description: '备选:内联 SVG 文本(或 base64 内容)。与 path 二选一。',
124
+ },
125
+ mime: {
126
+ type: 'string',
127
+ description: '当用 content 且非 SVG 时,指定内容 MIME(如 image/png)。',
128
+ },
129
+ label: {
130
+ type: 'string',
131
+ description: '图片标题,显示在预览上方。',
132
+ },
133
+ },
134
+ additionalProperties: false,
135
+ },
136
+ output: {
137
+ schema: { type: 'string', description: '单行预览摘要,给模型看。' },
138
+ render(_args, value) {
139
+ return [{ type: 'text', text: String(value) }]
140
+ },
141
+ presentationMeta(args) {
142
+ // 必须同步返回 lossless JSON(genui 的 render_ui 正是如此:从 args 直接推导)。
143
+ try {
144
+ const r = buildPreviewSync(args)
145
+ return { src: r.src, mime: r.mime, label: r.label }
146
+ } catch {
147
+ return null
148
+ }
149
+ },
150
+ },
151
+ async execute(args) {
152
+ try {
153
+ const r = buildPreviewSync(args)
154
+ return `已渲染预览「${r.label}」:${r.summary}(${r.bytes} B)。图片已显示在聊天中,等待用户确认后再做真正的修改。`
155
+ } catch (error) {
156
+ const detail = error instanceof Error ? error.message : String(error)
157
+ return `preview_image 失败:${detail}`
158
+ }
159
+ },
160
+ presentCall(args) {
161
+ const label = typeof args === 'object' && args !== null && typeof args.label === 'string' && args.label !== '' ? args.label : '预览图片'
162
+ return { card: 'generic', title: `预览「${label}」`, kind: 'other' }
163
+ },
164
+ presentResult(args) {
165
+ const label = typeof args === 'object' && args !== null && typeof args.label === 'string' && args.label !== '' ? args.label : '预览图片'
166
+ return { card: 'generic', title: `预览「${label}」` }
167
+ },
168
+ }
169
+ }
170
+
171
+ /**
172
+ * 注册工具。`tools` 服务可能晚于本插件绑定(启动顺序),因此既在 apply 时
173
+ * 探测一次,也订阅 `internal/service`(cordis 在每次服务绑定时发出),确保
174
+ * 一旦工具注册表出现就立即注册。
175
+ */
176
+ export function apply(ctx) {
177
+ let registered = false
178
+ const tryRegister = (value) => {
179
+ if (registered) return
180
+ const tools = value ?? ctx.reflect.get('tools', false)
181
+ if (tools === undefined) return
182
+ tools.register(createPreviewTool())
183
+ registered = true
184
+ }
185
+ tryRegister(undefined)
186
+ if (typeof ctx.on === 'function') {
187
+ ctx.on('internal/service', (name, value) => {
188
+ if (name === 'tools') tryRegister(value)
189
+ })
190
+ }
191
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Public type surface for the dsh-csv-and-image-preview host half.
3
+ * The runtime is plain JS; these declarations describe the cordis plugin
4
+ * contract so TS consumers and the `.d.ts` re-export resolve cleanly.
5
+ */
6
+
7
+ /** The `preview_image` tool definition (a dsh ToolDefinition object). */
8
+ export interface PreviewToolDefinition {
9
+ name: 'preview_image'
10
+ description: string
11
+ parameters: Record<string, unknown>
12
+ output: Record<string, unknown>
13
+ execute(args: unknown): Promise<string>
14
+ presentCall(args: unknown): { card: string; title: string; kind: string } | undefined
15
+ presentResult(args: unknown): { card: string; title: string } | undefined
16
+ }
17
+
18
+ /** Cordis plugin exported by the host half (lib/index.js). */
19
+ export interface ImagePreviewPlugin {
20
+ readonly name: 'csv-and-image-preview'
21
+ apply(ctx: unknown): void
22
+ }
23
+
24
+ /** A keyed display image payload carried on tool result meta. */
25
+ export interface PreviewMeta {
26
+ src: string
27
+ mime: string
28
+ label: string
29
+ }
30
+
31
+ declare const plugin: ImagePreviewPlugin
32
+ export default plugin
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@jetecho/dsh-csv-and-image-preview",
3
+ "version": "0.1.0",
4
+ "description": "Preview images / SVG (and prepare CSV) in the DeepSeek Harness chat, rendered as real browser <img> elements. Preview-first workflow: show the user the asset, wait for approval, then apply the real change.",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/types/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
+ "README.md",
22
+ "README.en.md",
23
+ "Agent.md",
24
+ "LICENSE"
25
+ ],
26
+ "scripts": {
27
+ "build": "node scripts/build.mjs",
28
+ "prepack": "node scripts/prepack.mjs"
29
+ },
30
+ "dsh": {
31
+ "bundle": {
32
+ "patch": "./cordis.patch.yml"
33
+ },
34
+ "client": {
35
+ "inject": [],
36
+ "platform": "web"
37
+ }
38
+ },
39
+ "keywords": [
40
+ "dsh",
41
+ "deepseek-harness",
42
+ "dsh-plugin",
43
+ "image",
44
+ "svg",
45
+ "preview",
46
+ "visual"
47
+ ],
48
+ "license": "MIT",
49
+ "author": "jetecho",
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "git+https://github.com/jetecho/dsh-csv-and-image-preview.git"
56
+ },
57
+ "homepage": "https://github.com/jetecho/dsh-csv-and-image-preview#readme",
58
+ "peerDependencies": {
59
+ "@deepseek-ai/cordis": "^4.0.1",
60
+ "react": "^18.0.0 || ^19.0.0"
61
+ },
62
+ "dependencies": {
63
+ "react": "^18.3.1"
64
+ }
65
+ }