@jetecho/dsh-csv-and-image-preview 0.1.0 → 0.1.1
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 +11 -9
- package/README.en.md +12 -6
- package/README.md +12 -6
- package/lib/client.js +121 -14
- package/lib/index.js +255 -12
- package/lib/types/index.d.ts +22 -0
- package/package.json +4 -2
package/Agent.md
CHANGED
|
@@ -4,21 +4,22 @@
|
|
|
4
4
|
|
|
5
5
|
## 核心原则:先预览,后落地
|
|
6
6
|
|
|
7
|
-
凡涉及**生成或修改图片 / SVG**,默认顺序必须是:
|
|
7
|
+
凡涉及**生成或修改图片 / SVG / CSV**,默认顺序必须是:
|
|
8
8
|
|
|
9
|
-
1. **先预览** —— 调用 `preview_image
|
|
9
|
+
1. **先预览** —— 调用 `preview_image`(图片 / SVG)或 `preview_csv`(CSV / TSV 表格),把结果渲染到聊天里给用户看;
|
|
10
10
|
- 读磁盘文件:`path=<绝对路径>`
|
|
11
11
|
- 内联 SVG(未落盘):`content=<svg ...> mime=image/svg+xml`
|
|
12
|
-
-
|
|
12
|
+
- 内联 CSV(未落盘):`content=<csv 文本>`;可用 `maxRows` 控制预览行数(默认 50,上限 500),`delimiter` 指定分隔符
|
|
13
|
+
- 任意图片资源:直接传 `path`
|
|
13
14
|
2. **等确认** —— 预览后停止写入,等待用户明确同意;
|
|
14
15
|
3. **用户认可**(如 `确认` / `可以` / `就这样`)后,才真正写文件 / 执行改动;
|
|
15
16
|
4. **用户否决** 时,按反馈修改并**再次预览**,直到认可为止。
|
|
16
17
|
|
|
17
18
|
## 不要做的事
|
|
18
19
|
|
|
19
|
-
- 不要在没预览的情况下,直接覆盖或新增一个图片 / SVG 文件。
|
|
20
|
-
-
|
|
21
|
-
- 不要用 markdown `` 或 ```` ```dsh-ui ```` 的 `image` 组件来"显示图片"—— 聊天渲染器会拦截,必须走 `preview_image`
|
|
20
|
+
- 不要在没预览的情况下,直接覆盖或新增一个图片 / SVG / CSV 文件。
|
|
21
|
+
- 不要把图片字节或 CSV 全文/大段表格数据塞进回复文本或模型上下文 —— 那是 meta 的活,你只需拿到紧凑摘要。
|
|
22
|
+
- 不要用 markdown `` 或 ```` ```dsh-ui ```` 的 `image` 组件来"显示图片"—— 聊天渲染器会拦截,必须走 `preview_image`;同理 CSV 必须走 `preview_csv`,不要用整段代码块充当"预览"。
|
|
22
23
|
|
|
23
24
|
## 交互话术
|
|
24
25
|
|
|
@@ -31,9 +32,10 @@
|
|
|
31
32
|
| 能力 | 说明 |
|
|
32
33
|
|---|---|
|
|
33
34
|
| `preview_image` 工具 | 读取文件 / 内联 SVG → data-URI → 浏览器 `<img>` 渲染 |
|
|
34
|
-
|
|
|
35
|
-
|
|
|
35
|
+
| `preview_csv` 工具 | 读取 CSV/TSV / 内联文本 → 有界解析(默认前 50 行,上限 500,列/单元格截断)→ 浏览器 `<table>` 渲染,控件限高局部滚动,表头可点击排序 |
|
|
36
|
+
| 键控 toolview | `tool.call.toolview` key=`preview_image` / `preview_csv`,渲染原生 DOM |
|
|
37
|
+
| meta 通道 | 图片字节与表格数据只走 `presentationMeta` 进浏览器,不进模型上下文 |
|
|
36
38
|
|
|
37
39
|
## 为什么
|
|
38
40
|
|
|
39
|
-
聊天渲染器不渲染 markdown 或 genui 里的图片(白名单里也没有 `image` 组件)。`React.createElement('img')` 生成的是浏览器原生 `<img
|
|
41
|
+
聊天渲染器不渲染 markdown 或 genui 里的图片(白名单里也没有 `image` 组件)。`React.createElement('img')` 生成的是浏览器原生 `<img>`,不经过那套过滤,所以能真实显示;CSV 同理,原生 `<table>` 键控 toolview 直接渲染,模型只拿行列摘要。预览式工作流让用户**先看到结果再决策**,避免"生成即覆写"的返工。
|
package/README.en.md
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
English | [中文](README.md)
|
|
4
4
|
|
|
5
|
-
Render images and
|
|
5
|
+
Render images, SVG, and CSV **for real** in the DeepSeek Harness (dsh) chat — images and SVG as a native browser `<img>`, CSV/TSV tables as a native `<table>` (sticky header, height-capped inner scrolling) — bypassing the chat renderer's markdown / genui filter, and paired with a **preview-first workflow**: generate or modify an image, SVG, or CSV, render it to you first, and only apply the real change once you confirm.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
9
|
Standard Profile Bundle install (recommended):
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
dsh plugin --profile web add dsh-csv-and-image-preview
|
|
12
|
+
dsh plugin --profile web add @jetecho/dsh-csv-and-image-preview
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Or install the development copy from a local directory:
|
|
@@ -25,6 +25,7 @@ After installing, restart `dsh web` so the new bundle joins the runtime. Legacy
|
|
|
25
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
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
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
|
+
4. **CSV/TSV table preview.** The host registers a `preview_csv` tool: it reads a CSV/TSV file (or inline text), sniffs the delimiter (`,` `;` Tab `|`, or set `delimiter` explicitly), parses it with hard bounds, and projects the table through meta. The browser-side keyed toolview (key `preview_csv`) renders a native `<table>`. The widget is **capped at 320px with inner scrolling**; only the first 50 rows are delivered by default (`maxRows` adjustable up to 500), and overly wide columns / long cells are truncated too — the model receives only a row/column summary; table data never enters the model context. Click a header to **sort** (numeric vs. Chinese-pinyin collation auto-detected, empty values sink, asc → desc → off). Overlong cells get an ellipsis with full text on hover.
|
|
28
29
|
|
|
29
30
|
## Usage examples
|
|
30
31
|
|
|
@@ -39,13 +40,18 @@ I'll show you the preview first.
|
|
|
39
40
|
[call preview_image content=<svg ...> mime=image/svg+xml label=sketch]
|
|
40
41
|
```
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
```markdown
|
|
44
|
+
<!-- Preview a CSV file (truncated to the first 100 rows) -->
|
|
45
|
+
[call preview_csv path=E:\DSHConfig\data.csv label=sales maxRows=100]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The agent sees something like `已渲染预览「ALPHA」:SVG 矢量图(1.3 KB).` while the picture appears in the tool card; for CSV it reads `已渲染 CSV 预览「sales」:CSV 表格 1234 行 × 5 列,分隔符 ",";显示前 100 行.` while the table appears in the tool card.
|
|
43
49
|
|
|
44
50
|
## Convention: preview first
|
|
45
51
|
|
|
46
52
|
This plugin treats "preview, then apply" as the default behavior:
|
|
47
53
|
|
|
48
|
-
- When generating / modifying an
|
|
54
|
+
- When generating / modifying an image, SVG, or CSV, **preview it first** with `preview_image` / `preview_csv`; do not write the real file yet.
|
|
49
55
|
- Only after the user confirms (`确认` / `可以` / `就这样`) do you actually write / mutate.
|
|
50
56
|
- If the user rejects it, redo per feedback and preview again.
|
|
51
57
|
- To preview any resource, pass `path` directly.
|
|
@@ -55,8 +61,8 @@ To enforce this convention, pair it with the bundled agent preset (see `Agent.md
|
|
|
55
61
|
## Layout
|
|
56
62
|
|
|
57
63
|
```
|
|
58
|
-
lib/index.js host: registers the preview_image
|
|
59
|
-
lib/client.js browser bundle: registers the keyed
|
|
64
|
+
lib/index.js host: registers the preview_image / preview_csv tools; reads files and projects data-URIs / table meta
|
|
65
|
+
lib/client.js browser bundle: registers the keyed toolviews; renders a native <img> / <table>
|
|
60
66
|
cordis.patch.yml profile insert line
|
|
61
67
|
```
|
|
62
68
|
|
package/README.md
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.en.md) | 中文
|
|
4
4
|
|
|
5
|
-
在 DeepSeek Harness (dsh) 聊天里**真正把图片 / SVG
|
|
5
|
+
在 DeepSeek Harness (dsh) 聊天里**真正把图片 / SVG / CSV 显示出来**的插件。图片与 SVG 以浏览器原生 `<img>` 渲染,CSV/TSV 表格以原生 `<table>` 渲染(粘性表头、控件限高局部滚动),均绕过聊天渲染器对 markdown / genui 的过滤;同时内置**预览式工作流**:生成或修改图片、SVG 或 CSV 时,先把结果渲染给你看,等你确认后再落地真正的改动。
|
|
6
6
|
|
|
7
7
|
## 安装
|
|
8
8
|
|
|
9
9
|
标准 Profile Bundle 安装(推荐):
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
dsh plugin --profile web add dsh-csv-and-image-preview
|
|
12
|
+
dsh plugin --profile web add @jetecho/dsh-csv-and-image-preview
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
或从本地目录安装开发版:
|
|
@@ -25,6 +25,7 @@ dsh plugin --profile web add ../dsh-csv-and-image-preview
|
|
|
25
25
|
1. **真渲染图片**。宿主注册 `preview_image` 工具:读取文件路径(或内联 SVG),推断 MIME、base64 编码成 data-URI,通过工具结果的 `presentationMeta` 投递;浏览器端键控 toolview(`tool.call.toolview` key=`preview_image`)读取 meta 并渲染原生 `<img>`。图片字节只走 meta 进浏览器,**不进入模型上下文**。
|
|
26
26
|
2. **预览式工作流**。调用 `preview_image` 后,脚本拿到的是紧凑摘要(SVG/位图、尺寸、字节数),真正的画面直接显示在聊天里;脚本会「等待用户确认」,等你点头后才写入文件/执行改动。
|
|
27
27
|
3. **预览任意资源**。给一个本地路径即可预览对应图片文件;也能传入内联 SVG 文本直接渲染,无需落盘。
|
|
28
|
+
4. **CSV/TSV 表格预览**。宿主注册 `preview_csv` 工具:读取 CSV/TSV 文件(或内联文本),自动嗅探分隔符(`,` `;` Tab `|`,可用 `delimiter` 指定),有界解析后把表格投递到 meta;浏览器端键控 toolview(key=`preview_csv`)渲染原生 `<table>`。表格控件**限高 320px 局部滚动**,默认只投递前 50 行(`maxRows` 可调,上限 500),超宽列数与超长单元格也会截断——模型只拿到行列摘要,表格数据不进模型上下文。表头**点击排序**(数值/中文拼音自适应,空值沉底,升→降→取消循环),单元格超长自动省略号、悬停看全文。
|
|
28
29
|
|
|
29
30
|
## 用法示例
|
|
30
31
|
|
|
@@ -39,13 +40,18 @@ dsh plugin --profile web add ../dsh-csv-and-image-preview
|
|
|
39
40
|
[调用 preview_image content=<svg ...> mime=image/svg+xml label=草图]
|
|
40
41
|
```
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
```markdown
|
|
44
|
+
<!-- 预览 CSV 文件(截断到前 100 行) -->
|
|
45
|
+
[调用 preview_csv path=E:\DSHConfig\data.csv label=销售数据 maxRows=100]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
脚本会读到类似 `已渲染预览「ALPHA」:SVG 矢量图(1.3 KB)。` 的返回,图片则显示在工具卡片里;CSV 则是 `已渲染 CSV 预览「销售数据」:CSV 表格 1234 行 × 5 列,分隔符 ",";显示前 100 行。`,表格显示在工具卡片里。
|
|
43
49
|
|
|
44
50
|
## 约定:预览优先
|
|
45
51
|
|
|
46
52
|
本插件把「先预览、后落地」作为默认约定:
|
|
47
53
|
|
|
48
|
-
- 生成 /
|
|
54
|
+
- 生成 / 修改图片、SVG 或 CSV 时,**先** `preview_image` / `preview_csv` 渲染,不做真实写入;
|
|
49
55
|
- 用户确认(`确认` / `可以` / `就这样`)后,才真正写文件 / 执行改动;
|
|
50
56
|
- 用户否决时,按反馈重做并再次预览;
|
|
51
57
|
- 需要预览任意资源时,直接传 `path`。
|
|
@@ -55,8 +61,8 @@ dsh plugin --profile web add ../dsh-csv-and-image-preview
|
|
|
55
61
|
## 组成部分
|
|
56
62
|
|
|
57
63
|
```
|
|
58
|
-
lib/index.js 宿主:注册 preview_image 工具,读取文件并投递 data-URI
|
|
59
|
-
lib/client.js 浏览器 bundle:注册键控 toolview,渲染原生 <img>
|
|
64
|
+
lib/index.js 宿主:注册 preview_image / preview_csv 工具,读取文件并投递 data-URI / 表格 meta
|
|
65
|
+
lib/client.js 浏览器 bundle:注册键控 toolview,渲染原生 <img> / <table>
|
|
60
66
|
cordis.patch.yml profile 插入行
|
|
61
67
|
```
|
|
62
68
|
|
package/lib/client.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-csv-and-image-preview 浏览器端 bundle(单文件,经 __ModuleLoader__ 加载)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* 为两个工具注册键控 toolview(`tool.call.toolview`, key = 工具名):
|
|
5
|
+
* - `preview_image`:从结果节点 meta 读取 { src, mime, label },渲染浏览器
|
|
6
|
+
* 原生 <img>;
|
|
7
|
+
* - `preview_csv`:从结果节点 meta 读取 { label, delimiter, rows, ... },
|
|
8
|
+
* 渲染原生 <table>(粘性表头、控件限高 320px 局部滚动、表头点击排序)。
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
+
* 这样图片/表格都不经过聊天渲染器的 markdown/genui 白名单,直接以真实 DOM
|
|
11
|
+
* 显示。样式使用 --dsw-* 主题变量,跟随全局亮/暗主题。
|
|
10
12
|
* @module dsh-csv-and-image-preview/client
|
|
11
13
|
*/
|
|
12
14
|
|
|
@@ -28,6 +30,15 @@ window.__ModuleLoader__.load({
|
|
|
28
30
|
'.dip-img{display:block;max-width:100%;height:auto;border-radius:8px}',
|
|
29
31
|
'.dip-img.svg{background:#fff}',
|
|
30
32
|
'.dip-missing{font-size:12px;color:var(--dsw-alias-label-tertiary)}',
|
|
33
|
+
'/* preview_csv 表格:控件限高,内部滚动,不撑爆聊天卡片 */',
|
|
34
|
+
'.dip-table-wrap{max-height:320px;max-width:100%;overflow:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:8px}',
|
|
35
|
+
'.dip-table{border-collapse:separate;border-spacing:0;font-size:12px;line-height:20px;white-space:nowrap}',
|
|
36
|
+
'.dip-table th{position:sticky;top:0;z-index:1;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font-weight:600;text-align:left;cursor:pointer;user-select:none;-webkit-user-select:none}',
|
|
37
|
+
'.dip-table th,.dip-table td{padding:4px 10px;border-bottom:1px solid var(--dsw-alias-border-l1);border-right:1px solid var(--dsw-alias-border-l1);overflow:hidden;text-overflow:ellipsis}',
|
|
38
|
+
'.dip-table th:last-child,.dip-table td:last-child{border-right:none}',
|
|
39
|
+
'.dip-table tbody tr:nth-child(even) td{background:rgba(127,127,127,0.08)}',
|
|
40
|
+
'.dip-table td.num{text-align:right;font-variant-numeric:tabular-nums}',
|
|
41
|
+
'.dip-csv-note{font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:18px}',
|
|
31
42
|
].join('\n')
|
|
32
43
|
if (typeof document !== 'undefined') {
|
|
33
44
|
const key = 'dip-' + 'dsh-csv-and-image-preview'
|
|
@@ -40,13 +51,18 @@ window.__ModuleLoader__.load({
|
|
|
40
51
|
}
|
|
41
52
|
}
|
|
42
53
|
|
|
54
|
+
/** 从 props.block 取 meta;工具调用 running 或回退时 meta 不存在。 */
|
|
55
|
+
function metaOf(props) {
|
|
56
|
+
const block = props && props.block
|
|
57
|
+
return block !== null && typeof block === 'object' && 'meta' in block ? block.meta : undefined
|
|
58
|
+
}
|
|
59
|
+
|
|
43
60
|
/**
|
|
44
|
-
* 键控 toolview
|
|
45
|
-
*
|
|
61
|
+
* 键控 toolview(preview_image):从 meta 读取 { src, mime, label } 渲染
|
|
62
|
+
* 原生 <img>。
|
|
46
63
|
*/
|
|
47
64
|
function PreviewToolView(props) {
|
|
48
|
-
const
|
|
49
|
-
const meta = block !== null && typeof block === 'object' && 'meta' in block ? block.meta : undefined
|
|
65
|
+
const meta = metaOf(props)
|
|
50
66
|
const label = typeof meta === 'object' && meta !== null && typeof meta.label === 'string' ? meta.label : '预览'
|
|
51
67
|
const src = typeof meta === 'object' && meta !== null && typeof meta.src === 'string' ? meta.src : null
|
|
52
68
|
const isSvg = typeof meta === 'object' && meta !== null && meta.mime === 'image/svg+xml'
|
|
@@ -64,16 +80,107 @@ window.__ModuleLoader__.load({
|
|
|
64
80
|
})))
|
|
65
81
|
}
|
|
66
82
|
|
|
83
|
+
const NUMERIC_RE = /^-?\d+(\.\d+)?$/
|
|
84
|
+
|
|
85
|
+
/** 截断/统计脚注:「显示前 50 / 1234 行 · 前 5 / 8 列 · 分隔符 ","」。 */
|
|
86
|
+
function csvNote(meta) {
|
|
87
|
+
const parts = []
|
|
88
|
+
parts.push(meta.rowsShown < meta.totalRows
|
|
89
|
+
? `显示前 ${meta.rowsShown} / ${meta.totalRows} 行`
|
|
90
|
+
: `共 ${meta.totalRows} 行`)
|
|
91
|
+
if (meta.colsShown < meta.totalCols) parts.push(`前 ${meta.colsShown} / ${meta.totalCols} 列`)
|
|
92
|
+
parts.push(`分隔符 "${meta.delimiter === '\t' ? 'Tab' : meta.delimiter}"`)
|
|
93
|
+
return parts.join(' · ')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 对数据行(不含表头)按列排序:整列(除空串)可解析为数字时按数值比较,
|
|
98
|
+
* 否则按中文 locale 比较;空值恒沉底。dir=1 升序,-1 降序。
|
|
99
|
+
*/
|
|
100
|
+
function sortBodyRows(meta, col, dir) {
|
|
101
|
+
const body = meta.rows.slice(1)
|
|
102
|
+
const numeric = body.every((r) => {
|
|
103
|
+
const v = col < r.length ? r[col] : ''
|
|
104
|
+
return v === '' || NUMERIC_RE.test(v)
|
|
105
|
+
}) && body.some((r) => col < r.length && NUMERIC_RE.test(r[col]))
|
|
106
|
+
const cmp = numeric
|
|
107
|
+
? (a, b) => (parseFloat(a) || 0) - (parseFloat(b) || 0)
|
|
108
|
+
: (a, b) => String(a).localeCompare(String(b), 'zh-Hans-CN')
|
|
109
|
+
return body
|
|
110
|
+
.map((_, i) => i)
|
|
111
|
+
.sort((x, y) => {
|
|
112
|
+
const a = x < body.length && col < body[x].length ? body[x][col] : ''
|
|
113
|
+
const b = y < body.length && col < body[y].length ? body[y][col] : ''
|
|
114
|
+
if ((a === '') !== (b === '')) return a === '' ? 1 : -1
|
|
115
|
+
return cmp(a, b) * dir
|
|
116
|
+
})
|
|
117
|
+
.map((i) => body[i])
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 键控 toolview(preview_csv):从 meta 读取 { label, delimiter, rows,
|
|
122
|
+
* totalRows, totalCols, rowsShown, colsShown },渲染原生 <table>。
|
|
123
|
+
* rows[0] 为表头;数字单元格右对齐;表头点击循环排序(升→降→取消)。
|
|
124
|
+
*/
|
|
125
|
+
function CsvToolView(props) {
|
|
126
|
+
const meta = metaOf(props)
|
|
127
|
+
const ok = meta !== null && typeof meta === 'object' && Array.isArray(meta.rows) && meta.rows.length > 0
|
|
128
|
+
// hooks 必须无条件调用,放在提前 return 之前。
|
|
129
|
+
const sortPair = React.useState(null) // null | { col, dir }
|
|
130
|
+
const sort = sortPair[0]
|
|
131
|
+
const setSort = sortPair[1]
|
|
132
|
+
if (!ok) {
|
|
133
|
+
return React.createElement('div', { className: 'dip-root', 'data-dip-preview': true },
|
|
134
|
+
React.createElement('span', { className: 'dip-missing' }, 'CSV 预览缺少数据(meta 缺失)'))
|
|
135
|
+
}
|
|
136
|
+
const label = typeof meta.label === 'string' && meta.label !== '' ? meta.label : 'CSV 预览'
|
|
137
|
+
|
|
138
|
+
const headerClick = (ci) => {
|
|
139
|
+
setSort((prev) => (prev !== null && prev.col === ci ? (prev.dir === 1 ? { col: ci, dir: -1 } : null) : { col: ci, dir: 1 }))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const thead = React.createElement('thead', null,
|
|
143
|
+
React.createElement('tr', null, meta.rows[0].map((c, i) => {
|
|
144
|
+
const indicator = sort !== null && sort.col === i ? (sort.dir === 1 ? ' ▲' : ' ▼') : ''
|
|
145
|
+
return React.createElement('th', {
|
|
146
|
+
key: i,
|
|
147
|
+
onClick: () => headerClick(i),
|
|
148
|
+
title: String(c),
|
|
149
|
+
}, String(c) + indicator)
|
|
150
|
+
})))
|
|
151
|
+
const bodyRows = sort !== null ? sortBodyRows(meta, sort.col, sort.dir) : meta.rows.slice(1)
|
|
152
|
+
const tbody = React.createElement('tbody', null, bodyRows.map((r, ri) =>
|
|
153
|
+
React.createElement('tr', { key: ri }, r.map((c, ci) => {
|
|
154
|
+
const s = String(c)
|
|
155
|
+
return React.createElement('td', {
|
|
156
|
+
key: ci,
|
|
157
|
+
className: NUMERIC_RE.test(s) ? 'num' : undefined,
|
|
158
|
+
title: s,
|
|
159
|
+
}, s)
|
|
160
|
+
}))))
|
|
161
|
+
return React.createElement('div', { className: 'dip-root', 'data-dip-preview': true },
|
|
162
|
+
React.createElement('span', { className: 'dip-label' }, label),
|
|
163
|
+
React.createElement('div', { className: 'dip-table-wrap' },
|
|
164
|
+
React.createElement('table', { className: 'dip-table' }, thead, tbody)),
|
|
165
|
+
React.createElement('span', { className: 'dip-csv-note' }, csvNote(meta)))
|
|
166
|
+
}
|
|
167
|
+
|
|
67
168
|
// ── 服务接入 ─────────────────────────────────────────────────────────────
|
|
68
169
|
const inject = []
|
|
69
170
|
function apply(ctx) {
|
|
70
171
|
const slots = ctx.get('slots')
|
|
71
172
|
if (slots === undefined) return
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
173
|
+
const disposals = [
|
|
174
|
+
slots.inject('tool.call.toolview', () => slots.register(
|
|
175
|
+
{ name: 'tool.call.toolview', key: 'preview_image' },
|
|
176
|
+
PreviewToolView,
|
|
177
|
+
)),
|
|
178
|
+
slots.inject('tool.call.toolview', () => slots.register(
|
|
179
|
+
{ name: 'tool.call.toolview', key: 'preview_csv' },
|
|
180
|
+
CsvToolView,
|
|
181
|
+
)),
|
|
182
|
+
]
|
|
183
|
+
return () => { for (const dispose of disposals) dispose() }
|
|
77
184
|
}
|
|
78
185
|
|
|
79
186
|
exports.apply = apply
|
package/lib/index.js
CHANGED
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
* 单条 Loader 行(见 cordis.patch.yml)挂载本模块。职责:
|
|
5
5
|
* 1. 注册 `preview_image` 工具:给定一个文件路径(或内联 SVG),读取文件、
|
|
6
6
|
* 推断 MIME、base64 编码为 data-URI;
|
|
7
|
-
* 2.
|
|
8
|
-
*
|
|
9
|
-
* 3. `
|
|
10
|
-
*
|
|
7
|
+
* 2. 注册 `preview_csv` 工具:读取 CSV/TSV(磁盘路径或内联文本),嗅探分隔符、
|
|
8
|
+
* 有界解析(输入体积/行数/列数/单元格四重截断),产出可序列化的表格 meta;
|
|
9
|
+
* 3. 两者都把展示数据通过工具结果的 `presentationMeta` 投递出去,浏览器端
|
|
10
|
+
* 键控 toolview 据此渲染原生 <img> / <table> —— 绕过聊天渲染器的过滤;
|
|
11
|
+
* 4. `execute` 返回紧凑的模型可读摘要,真正的图片字节/表格数据只走 meta,
|
|
12
|
+
* 不进模型上下文。
|
|
11
13
|
*
|
|
12
14
|
* 注意:`output.presentationMeta` 必须**同步**返回 lossless JSON(它是展示
|
|
13
15
|
* 期别名,在结果落地时就读取,不能返回 Promise),因此这里用同步 fs 读取;
|
|
@@ -36,6 +38,15 @@ const MIME_BY_EXT = {
|
|
|
36
38
|
'.avif': 'image/avif',
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
/** 从 args.label 取标题;空/缺省回退 fallback。 */
|
|
42
|
+
function labelOf(args, fallback) {
|
|
43
|
+
return typeof args === 'object' && args !== null && typeof args.label === 'string' && args.label !== ''
|
|
44
|
+
? args.label
|
|
45
|
+
: fallback
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── 图片预览 ────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
39
50
|
/** 从路径扩得 MIME;未知回退 octet-stream。 */
|
|
40
51
|
function mimeOf(path) {
|
|
41
52
|
const dot = path.lastIndexOf('.')
|
|
@@ -54,8 +65,7 @@ function svgDimension(svg) {
|
|
|
54
65
|
}
|
|
55
66
|
|
|
56
67
|
/** 模型可读的单行摘要。 */
|
|
57
|
-
function
|
|
58
|
-
const size = bytes < 1024 ? `${bytes} B` : bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / (1024 * 1024)).toFixed(2)} MB`
|
|
68
|
+
function imageSummary(mime) {
|
|
59
69
|
return mime === 'image/svg+xml' ? 'SVG 矢量图' : mime.replace('image/', '').toUpperCase() + ' 位图'
|
|
60
70
|
}
|
|
61
71
|
|
|
@@ -67,7 +77,7 @@ function buildPreviewSync(args) {
|
|
|
67
77
|
if (typeof args !== 'object' || args === null) {
|
|
68
78
|
throw new Error('preview_image 需要对象参数(带 path 或 content)。')
|
|
69
79
|
}
|
|
70
|
-
const label =
|
|
80
|
+
const label = labelOf(args, '预览')
|
|
71
81
|
|
|
72
82
|
// 优先:磁盘文件路径。
|
|
73
83
|
if (typeof args.path === 'string' && args.path !== '') {
|
|
@@ -81,7 +91,7 @@ function buildPreviewSync(args) {
|
|
|
81
91
|
mime,
|
|
82
92
|
label,
|
|
83
93
|
bytes: st.size,
|
|
84
|
-
summary:
|
|
94
|
+
summary: imageSummary(mime),
|
|
85
95
|
}
|
|
86
96
|
}
|
|
87
97
|
|
|
@@ -158,12 +168,244 @@ export function createPreviewTool() {
|
|
|
158
168
|
}
|
|
159
169
|
},
|
|
160
170
|
presentCall(args) {
|
|
161
|
-
|
|
162
|
-
|
|
171
|
+
return { card: 'generic', title: `预览「${labelOf(args, '预览图片')}」`, kind: 'other' }
|
|
172
|
+
},
|
|
173
|
+
presentResult(args) {
|
|
174
|
+
return { card: 'generic', title: `预览「${labelOf(args, '预览图片')}」` }
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── CSV 预览 ────────────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
/** 候选分隔符(嗅探顺序即平局优先级)。 */
|
|
182
|
+
const CSV_DELIMITERS = [',', ';', '\t', '|']
|
|
183
|
+
/** 单次预览允许读入的最大体积,超过直接拒绝,保护宿主进程。 */
|
|
184
|
+
const CSV_MAX_INPUT_BYTES = 8 * 1024 * 1024
|
|
185
|
+
const CSV_MAX_ROWS_DEFAULT = 50
|
|
186
|
+
const CSV_MAX_ROWS_LIMIT = 500
|
|
187
|
+
const CSV_MAX_COLS = 40
|
|
188
|
+
const CSV_CELL_CAP = 200
|
|
189
|
+
|
|
190
|
+
function fmtBytes(n) {
|
|
191
|
+
return n < 1024 ? `${n} B` : n < 1024 * 1024 ? `${(n / 1024).toFixed(1)} KB` : `${(n / (1024 * 1024)).toFixed(2)} MB`
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 分隔符嗅探:统计前 4 KB 内引号外各候选出现次数,取最多者;全 0 回退逗号。
|
|
196
|
+
* 遍历顺序即平局优先级(, ; Tab |)。
|
|
197
|
+
*/
|
|
198
|
+
function detectDelimiter(text) {
|
|
199
|
+
const counts = new Map(CSV_DELIMITERS.map((d) => [d, 0]))
|
|
200
|
+
let inQuotes = false
|
|
201
|
+
const end = Math.min(text.length, 4096)
|
|
202
|
+
for (let i = 0; i < end; i++) {
|
|
203
|
+
const ch = text[i]
|
|
204
|
+
if (ch === '"') inQuotes = !inQuotes
|
|
205
|
+
else if (!inQuotes && counts.has(ch)) counts.set(ch, counts.get(ch) + 1)
|
|
206
|
+
}
|
|
207
|
+
let best = CSV_DELIMITERS[0]
|
|
208
|
+
let bestN = -1
|
|
209
|
+
for (const d of CSV_DELIMITERS) {
|
|
210
|
+
const n = counts.get(d)
|
|
211
|
+
if (n > bestN) { best = d; bestN = n }
|
|
212
|
+
}
|
|
213
|
+
return bestN > 0 ? best : ','
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* RFC 4180 风格的有界 CSV 解析:引号字段、"" 转义、引号内换行、CRLF。
|
|
218
|
+
* 只保留前 maxRows 行(有界内存),但完整统计总行数与最大列数,摘要可用。
|
|
219
|
+
* 跳过空行;返回 { rows, totalRows, totalCols }。
|
|
220
|
+
*/
|
|
221
|
+
function parseCsvBounded(text, delimiter, maxRows) {
|
|
222
|
+
const rows = []
|
|
223
|
+
let row = []
|
|
224
|
+
let field = ''
|
|
225
|
+
let inQuotes = false
|
|
226
|
+
let totalRows = 0
|
|
227
|
+
let totalCols = 0
|
|
228
|
+
const endRow = () => {
|
|
229
|
+
row.push(field)
|
|
230
|
+
field = ''
|
|
231
|
+
if (row.length === 1 && row[0] === '') { row = []; return } // 空行不计
|
|
232
|
+
if (row.length > totalCols) totalCols = row.length
|
|
233
|
+
totalRows++
|
|
234
|
+
if (rows.length < maxRows) rows.push(row)
|
|
235
|
+
row = []
|
|
236
|
+
}
|
|
237
|
+
for (let i = 0; i < text.length; i++) {
|
|
238
|
+
const ch = text[i]
|
|
239
|
+
if (inQuotes) {
|
|
240
|
+
if (ch === '"') {
|
|
241
|
+
if (text[i + 1] === '"') { field += '"'; i++ }
|
|
242
|
+
else inQuotes = false
|
|
243
|
+
} else {
|
|
244
|
+
field += ch
|
|
245
|
+
}
|
|
246
|
+
} else if (ch === '"') {
|
|
247
|
+
inQuotes = true
|
|
248
|
+
} else if (ch === delimiter) {
|
|
249
|
+
row.push(field)
|
|
250
|
+
field = ''
|
|
251
|
+
} else if (ch === '\n') {
|
|
252
|
+
endRow()
|
|
253
|
+
} else if (ch !== '\r') {
|
|
254
|
+
field += ch
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (field !== '' || row.length > 0) endRow()
|
|
258
|
+
return { rows, totalRows, totalCols }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function clampMaxRows(v) {
|
|
262
|
+
const n = Math.floor(Number(v))
|
|
263
|
+
if (!Number.isFinite(n) || n < 1) return CSV_MAX_ROWS_DEFAULT
|
|
264
|
+
return Math.min(n, CSV_MAX_ROWS_LIMIT)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** 显式分隔符 → 生效值;null 表示走嗅探(.tsv 扩展名直接判 Tab)。 */
|
|
268
|
+
function resolveDelimiter(args) {
|
|
269
|
+
const d = args.delimiter
|
|
270
|
+
if (typeof d === 'string' && CSV_DELIMITERS.includes(d)) return d
|
|
271
|
+
if (typeof args.path === 'string' && args.path !== '' && args.path.toLowerCase().endsWith('.tsv')) return '\t'
|
|
272
|
+
return null
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function capCell(c) {
|
|
276
|
+
return c.length > CSV_CELL_CAP ? c.slice(0, CSV_CELL_CAP) + '…' : c
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 同步构建 CSV 预览 meta,返回 { label, delimiter, rows, totalRows, totalCols,
|
|
281
|
+
* rowsShown, colsShown };有错抛 Error。rows 含表头行,已被列数/单元格截断并
|
|
282
|
+
* 补齐成规则矩形,浏览器可直接渲染。
|
|
283
|
+
*/
|
|
284
|
+
function buildCsvPreviewSync(args) {
|
|
285
|
+
if (typeof args !== 'object' || args === null) {
|
|
286
|
+
throw new Error('preview_csv 需要对象参数(带 path 或 content)。')
|
|
287
|
+
}
|
|
288
|
+
const label = labelOf(args, 'CSV 预览')
|
|
289
|
+
|
|
290
|
+
let text = ''
|
|
291
|
+
if (typeof args.path === 'string' && args.path !== '') {
|
|
292
|
+
const abs = resolve(args.path)
|
|
293
|
+
const buf = readFileSync(abs)
|
|
294
|
+
if (buf.length > CSV_MAX_INPUT_BYTES) {
|
|
295
|
+
throw new Error(`文件 ${fmtBytes(buf.length)} 超过预览上限 ${fmtBytes(CSV_MAX_INPUT_BYTES)},请先截取再预览。`)
|
|
296
|
+
}
|
|
297
|
+
text = buf.toString('utf8')
|
|
298
|
+
} else if (typeof args.content === 'string' && args.content !== '') {
|
|
299
|
+
if (Buffer.byteLength(args.content, 'utf8') > CSV_MAX_INPUT_BYTES) {
|
|
300
|
+
throw new Error(`内联内容超过预览上限 ${fmtBytes(CSV_MAX_INPUT_BYTES)},请先截取再预览。`)
|
|
301
|
+
}
|
|
302
|
+
text = args.content
|
|
303
|
+
} else {
|
|
304
|
+
throw new Error('preview_csv 需要 path 或 content 参数。')
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (text.charCodeAt(0) === 0xfeff) text = text.slice(1) // 剥 BOM
|
|
308
|
+
|
|
309
|
+
const auto = resolveDelimiter(args)
|
|
310
|
+
const delimiter = auto ?? detectDelimiter(text)
|
|
311
|
+
const parsed = parseCsvBounded(text, delimiter, clampMaxRows(args.maxRows))
|
|
312
|
+
if (parsed.totalRows === 0) throw new Error('CSV 内容为空,没有可预览的数据行。')
|
|
313
|
+
|
|
314
|
+
const colsShown = Math.min(parsed.totalCols, CSV_MAX_COLS)
|
|
315
|
+
const rows = parsed.rows.map((r) => {
|
|
316
|
+
const view = r.slice(0, CSV_MAX_COLS).map(capCell)
|
|
317
|
+
while (view.length < colsShown) view.push('')
|
|
318
|
+
return view
|
|
319
|
+
})
|
|
320
|
+
return {
|
|
321
|
+
label,
|
|
322
|
+
delimiter,
|
|
323
|
+
rows,
|
|
324
|
+
totalRows: parsed.totalRows,
|
|
325
|
+
totalCols: parsed.totalCols,
|
|
326
|
+
rowsShown: rows.length,
|
|
327
|
+
colsShown,
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** 模型可读的行列摘要(含截断信息)。 */
|
|
332
|
+
function csvSummaryText(r) {
|
|
333
|
+
const d = r.delimiter === '\t' ? 'Tab' : r.delimiter
|
|
334
|
+
let s = `CSV 表格 ${r.totalRows} 行 × ${r.totalCols} 列,分隔符 "${d}"`
|
|
335
|
+
if (r.rowsShown < r.totalRows) s += `;显示前 ${r.rowsShown} 行`
|
|
336
|
+
if (r.colsShown < r.totalCols) s += ` / 前 ${r.colsShown} 列`
|
|
337
|
+
return s + '。'
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* 构建 preview_csv 工具定义。模型传 CSV 路径/文本发起一次「表格预览」:
|
|
342
|
+
* 宿主有界解析后把表格投递到 meta,浏览器端 toolview 渲染原生 <table>。
|
|
343
|
+
*/
|
|
344
|
+
export function createCsvTool() {
|
|
345
|
+
return {
|
|
346
|
+
name: 'preview_csv',
|
|
347
|
+
description:
|
|
348
|
+
'在聊天里预览 CSV/TSV 表格——用浏览器原生 <table> 渲染,粘性表头、控件限高局部滚动。'
|
|
349
|
+
+ ' 传 path 读取磁盘文件(.csv/.tsv 等),或传 content(内联 CSV 文本)。'
|
|
350
|
+
+ ' 分隔符自动嗅探(, ; Tab |),可用 delimiter 指定;默认只投递前 '
|
|
351
|
+
+ `${CSV_MAX_ROWS_DEFAULT} 行(可用 maxRows 调大,上限 ${CSV_MAX_ROWS_LIMIT})。`
|
|
352
|
+
+ ' 表格数据只走 meta 投递到浏览器,不进入模型上下文;返回的是行列摘要。'
|
|
353
|
+
+ ' 生成/修改 CSV 时先调用它给用户预览,等用户确认后再做真正的写入/修改。',
|
|
354
|
+
parameters: {
|
|
355
|
+
type: 'object',
|
|
356
|
+
properties: {
|
|
357
|
+
path: {
|
|
358
|
+
type: 'string',
|
|
359
|
+
description: '要预览的 CSV/TSV 文件路径。优先用绝对路径。',
|
|
360
|
+
},
|
|
361
|
+
content: {
|
|
362
|
+
type: 'string',
|
|
363
|
+
description: '备选:内联 CSV 文本。与 path 二选一。',
|
|
364
|
+
},
|
|
365
|
+
delimiter: {
|
|
366
|
+
type: 'string',
|
|
367
|
+
enum: [...CSV_DELIMITERS],
|
|
368
|
+
description: '字段分隔符;缺省时自动嗅探,.tsv 文件默认 Tab。',
|
|
369
|
+
},
|
|
370
|
+
maxRows: {
|
|
371
|
+
type: 'number',
|
|
372
|
+
description: `预览的最大行数(含表头行),默认 ${CSV_MAX_ROWS_DEFAULT},上限 ${CSV_MAX_ROWS_LIMIT}。`,
|
|
373
|
+
},
|
|
374
|
+
label: {
|
|
375
|
+
type: 'string',
|
|
376
|
+
description: '表格标题,显示在预览上方。',
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
additionalProperties: false,
|
|
380
|
+
},
|
|
381
|
+
output: {
|
|
382
|
+
schema: { type: 'string', description: '单行预览摘要,给模型看。' },
|
|
383
|
+
render(_args, value) {
|
|
384
|
+
return [{ type: 'text', text: String(value) }]
|
|
385
|
+
},
|
|
386
|
+
presentationMeta(args) {
|
|
387
|
+
// 必须同步返回 lossless JSON;解析全程同步。
|
|
388
|
+
try {
|
|
389
|
+
return buildCsvPreviewSync(args)
|
|
390
|
+
} catch {
|
|
391
|
+
return null
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
async execute(args) {
|
|
396
|
+
try {
|
|
397
|
+
const r = buildCsvPreviewSync(args)
|
|
398
|
+
return `已渲染 CSV 预览「${r.label}」:${csvSummaryText(r)}表格已显示在聊天中,等待用户确认后再做真正的修改。`
|
|
399
|
+
} catch (error) {
|
|
400
|
+
const detail = error instanceof Error ? error.message : String(error)
|
|
401
|
+
return `preview_csv 失败:${detail}`
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
presentCall(args) {
|
|
405
|
+
return { card: 'generic', title: `CSV 预览「${labelOf(args, 'CSV 表格')}」`, kind: 'other' }
|
|
163
406
|
},
|
|
164
407
|
presentResult(args) {
|
|
165
|
-
|
|
166
|
-
return { card: 'generic', title: `预览「${label}」` }
|
|
408
|
+
return { card: 'generic', title: `CSV 预览「${labelOf(args, 'CSV 表格')}」` }
|
|
167
409
|
},
|
|
168
410
|
}
|
|
169
411
|
}
|
|
@@ -180,6 +422,7 @@ export function apply(ctx) {
|
|
|
180
422
|
const tools = value ?? ctx.reflect.get('tools', false)
|
|
181
423
|
if (tools === undefined) return
|
|
182
424
|
tools.register(createPreviewTool())
|
|
425
|
+
tools.register(createCsvTool())
|
|
183
426
|
registered = true
|
|
184
427
|
}
|
|
185
428
|
tryRegister(undefined)
|
package/lib/types/index.d.ts
CHANGED
|
@@ -15,6 +15,17 @@ export interface PreviewToolDefinition {
|
|
|
15
15
|
presentResult(args: unknown): { card: string; title: string } | undefined
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/** The `preview_csv` tool definition (a dsh ToolDefinition object). */
|
|
19
|
+
export interface CsvToolDefinition {
|
|
20
|
+
name: 'preview_csv'
|
|
21
|
+
description: string
|
|
22
|
+
parameters: Record<string, unknown>
|
|
23
|
+
output: Record<string, unknown>
|
|
24
|
+
execute(args: unknown): Promise<string>
|
|
25
|
+
presentCall(args: unknown): { card: string; title: string; kind: string } | undefined
|
|
26
|
+
presentResult(args: unknown): { card: string; title: string } | undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
18
29
|
/** Cordis plugin exported by the host half (lib/index.js). */
|
|
19
30
|
export interface ImagePreviewPlugin {
|
|
20
31
|
readonly name: 'csv-and-image-preview'
|
|
@@ -28,5 +39,16 @@ export interface PreviewMeta {
|
|
|
28
39
|
label: string
|
|
29
40
|
}
|
|
30
41
|
|
|
42
|
+
/** A keyed bounded table payload carried on tool result meta (rows[0] = header). */
|
|
43
|
+
export interface CsvPreviewMeta {
|
|
44
|
+
label: string
|
|
45
|
+
delimiter: string
|
|
46
|
+
rows: string[][]
|
|
47
|
+
totalRows: number
|
|
48
|
+
totalCols: number
|
|
49
|
+
rowsShown: number
|
|
50
|
+
colsShown: number
|
|
51
|
+
}
|
|
52
|
+
|
|
31
53
|
declare const plugin: ImagePreviewPlugin
|
|
32
54
|
export default plugin
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jetecho/dsh-csv-and-image-preview",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Preview images / SVG
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Preview images / SVG and CSV tables in the DeepSeek Harness chat, rendered as real browser <img> / <table> elements. Preview-first workflow: show the user the asset, wait for approval, then apply the real change.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -42,6 +42,8 @@
|
|
|
42
42
|
"dsh-plugin",
|
|
43
43
|
"image",
|
|
44
44
|
"svg",
|
|
45
|
+
"csv",
|
|
46
|
+
"table",
|
|
45
47
|
"preview",
|
|
46
48
|
"visual"
|
|
47
49
|
],
|