@jetecho/dsh-csv-and-image-preview 0.1.1 → 0.1.3
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 +45 -41
- package/LICENSE +21 -21
- package/README.en.md +81 -79
- package/README.md +85 -79
- package/cordis.patch.yml +6 -6
- package/lib/client.js +318 -190
- package/lib/index.js +380 -420
- package/lib/store.js +144 -0
- package/lib/types/index.d.ts +63 -49
- package/package.json +81 -67
package/lib/client.js
CHANGED
|
@@ -1,190 +1,318 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-csv-and-image-preview 浏览器端 bundle(单文件,经 __ModuleLoader__ 加载)。
|
|
3
|
-
*
|
|
4
|
-
* 为两个工具注册键控 toolview(`tool.call.toolview`, key = 工具名):
|
|
5
|
-
* - `preview_image
|
|
6
|
-
* 原生 <img>;
|
|
7
|
-
* - `preview_csv
|
|
8
|
-
* 渲染原生 <table>(粘性表头、控件限高 320px 局部滚动、表头点击排序)。
|
|
9
|
-
*
|
|
10
|
-
* 这样图片/表格都不经过聊天渲染器的 markdown/genui 白名单,直接以真实 DOM
|
|
11
|
-
* 显示。样式使用 --dsw-* 主题变量,跟随全局亮/暗主题。
|
|
12
|
-
* @module dsh-csv-and-image-preview/client
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
window.__ModuleLoader__.load({
|
|
16
|
-
id: 'dsh-csv-and-image-preview',
|
|
17
|
-
factory: (require) => {
|
|
18
|
-
var module = { exports: {} }
|
|
19
|
-
var exports = module.exports
|
|
20
|
-
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
21
|
-
|
|
22
|
-
const React = require('react')
|
|
23
|
-
|
|
24
|
-
// ── 样式 ────────────────────────────────────────────────────────────────
|
|
25
|
-
const css = [
|
|
26
|
-
'/* dsh-csv-and-image-preview */',
|
|
27
|
-
'.dip-root{display:flex;flex-direction:column;gap:6px;align-items:flex-start;max-width:100%}',
|
|
28
|
-
'.dip-label{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:20px}',
|
|
29
|
-
'.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%}',
|
|
30
|
-
'.dip-img{display:block;max-width:100%;height:auto;border-radius:8px}',
|
|
31
|
-
'.dip-img.svg{background:#fff}',
|
|
32
|
-
'.dip-missing{font-size:12px;color:var(--dsw-alias-label-tertiary)}',
|
|
33
|
-
'
|
|
34
|
-
'
|
|
35
|
-
'.dip-table{
|
|
36
|
-
'.dip-table
|
|
37
|
-
'.dip-table th
|
|
38
|
-
'.dip-table th
|
|
39
|
-
'.dip-table
|
|
40
|
-
'.dip-table td
|
|
41
|
-
'.dip-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
el
|
|
48
|
-
el.dataset.
|
|
49
|
-
el.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
})
|
|
1
|
+
/**
|
|
2
|
+
* dsh-csv-and-image-preview 浏览器端 bundle(单文件,经 __ModuleLoader__ 加载)。
|
|
3
|
+
*
|
|
4
|
+
* 为两个工具注册键控 toolview(`tool.call.toolview`, key = 工具名):
|
|
5
|
+
* - `preview_image`:通过会话/调用 ID 读取快照(兼容旧 meta),渲染浏览器
|
|
6
|
+
* 原生 <img>;
|
|
7
|
+
* - `preview_csv`:读取表格快照 { label, delimiter, rows, ... },
|
|
8
|
+
* 渲染原生 <table>(粘性表头、控件限高 320px 局部滚动、表头点击排序)。
|
|
9
|
+
*
|
|
10
|
+
* 这样图片/表格都不经过聊天渲染器的 markdown/genui 白名单,直接以真实 DOM
|
|
11
|
+
* 显示。样式使用 --dsw-* 主题变量,跟随全局亮/暗主题。
|
|
12
|
+
* @module dsh-csv-and-image-preview/client
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
window.__ModuleLoader__.load({
|
|
16
|
+
id: '@jetecho/dsh-csv-and-image-preview',
|
|
17
|
+
factory: (require) => {
|
|
18
|
+
var module = { exports: {} }
|
|
19
|
+
var exports = module.exports
|
|
20
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
21
|
+
|
|
22
|
+
const React = require('react')
|
|
23
|
+
|
|
24
|
+
// ── 样式 ────────────────────────────────────────────────────────────────
|
|
25
|
+
const css = [
|
|
26
|
+
'/* dsh-csv-and-image-preview */',
|
|
27
|
+
'.dip-root{display:flex;flex-direction:column;gap:6px;align-items:flex-start;max-width:100%}',
|
|
28
|
+
'.dip-label{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:20px}',
|
|
29
|
+
'.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%}',
|
|
30
|
+
'.dip-img{display:block;max-width:100%;height:auto;border-radius:8px}',
|
|
31
|
+
'.dip-img.svg{background:#fff}',
|
|
32
|
+
'.dip-missing{font-size:12px;color:var(--dsw-alias-label-tertiary)}',
|
|
33
|
+
'.dip-error{font-size:12px;color:var(--dsw-alias-state-error-primary,#c33)}',
|
|
34
|
+
'/* preview_csv 表格:控件限高,内部滚动,不撑爆聊天卡片 */',
|
|
35
|
+
'.dip-table-wrap{max-height:320px;max-width:100%;overflow:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:8px}',
|
|
36
|
+
'.dip-table{border-collapse:separate;border-spacing:0;font-size:12px;line-height:20px;white-space:nowrap}',
|
|
37
|
+
'.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}',
|
|
38
|
+
'.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}',
|
|
39
|
+
'.dip-table th:last-child,.dip-table td:last-child{border-right:none}',
|
|
40
|
+
'.dip-table tbody tr:nth-child(even) td{background:rgba(127,127,127,0.08)}',
|
|
41
|
+
'.dip-table td.num{text-align:right;font-variant-numeric:tabular-nums}',
|
|
42
|
+
'.dip-csv-note{font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:18px}',
|
|
43
|
+
].join('\n')
|
|
44
|
+
if (typeof document !== 'undefined') {
|
|
45
|
+
const key = 'dip-' + 'dsh-csv-and-image-preview'
|
|
46
|
+
if (document.querySelector('style[data-plugin-css="' + key + '"]') === null) {
|
|
47
|
+
const el = document.createElement('style')
|
|
48
|
+
el.dataset.plugin = 'dsh-csv-and-image-preview'
|
|
49
|
+
el.dataset.pluginCss = key
|
|
50
|
+
el.textContent = css
|
|
51
|
+
document.head.appendChild(el)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 从 props.block 取 meta;工具调用 running 或回退时 meta 不存在。 */
|
|
56
|
+
function metaOf(props) {
|
|
57
|
+
const block = props && props.block
|
|
58
|
+
return block !== null && typeof block === 'object' && 'meta' in block ? block.meta : undefined
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function legacyMeta(props) {
|
|
62
|
+
const meta = metaOf(props)
|
|
63
|
+
if (!meta || typeof meta !== 'object') return null
|
|
64
|
+
if (props.toolName === 'preview_image' && typeof meta.src === 'string') return meta
|
|
65
|
+
if (props.toolName === 'preview_csv' && Array.isArray(meta.rows)) return meta
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Cancel stale reads on navigation; re-read after connection recovery or retry. */
|
|
70
|
+
function usePreview(props) {
|
|
71
|
+
const block = props.block
|
|
72
|
+
const legacy = legacyMeta(props)
|
|
73
|
+
const settled = block?.kind === 'tool-result'
|
|
74
|
+
const text = (block?.content || []).filter(c => c.type === 'text').map(c => c.text).join('\n')
|
|
75
|
+
const failed = block?.isError || /^preview_(?:image|csv) 失败:/.test(text)
|
|
76
|
+
const key = JSON.stringify([props.sessionId, props.callId, props.toolName])
|
|
77
|
+
const [attempt, setAttempt] = React.useState(0)
|
|
78
|
+
const [state, setState] = React.useState(null)
|
|
79
|
+
const generation = React.useSyncExternalStore(props.subscribeConnection, props.getConnection, props.getConnection)
|
|
80
|
+
React.useEffect(() => {
|
|
81
|
+
if (legacy || !settled || failed) return
|
|
82
|
+
const controller = new AbortController()
|
|
83
|
+
let active = true
|
|
84
|
+
setState({ key, loading: true })
|
|
85
|
+
props.loadPreview({ sessionId: props.sessionId, callId: props.callId, toolName: props.toolName }, controller.signal)
|
|
86
|
+
.then(meta => { if (active) setState({ key, meta }) })
|
|
87
|
+
.catch(error => { if (active) setState({ key, error: error.message || '加载预览失败。' }) })
|
|
88
|
+
return () => { active = false; controller.abort() }
|
|
89
|
+
}, [key, settled, failed, legacy, attempt, generation, props.loadPreview])
|
|
90
|
+
if (failed) return { error: text || '预览执行失败。' }
|
|
91
|
+
if (legacy) return { meta: legacy }
|
|
92
|
+
if (!settled) return { loading: true, message: '正在生成预览…' }
|
|
93
|
+
const current = state?.key === key ? state : { loading: true }
|
|
94
|
+
return { ...current, retry: () => setAttempt(n => n + 1) }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function PreviewStatus({ state }) {
|
|
98
|
+
return React.createElement('div', { className: 'dip-root', 'data-dip-preview': true },
|
|
99
|
+
React.createElement('span', { className: state.error ? 'dip-error' : 'dip-missing', role: state.error ? 'alert' : 'status' },
|
|
100
|
+
state.error || state.message || '正在加载预览…'),
|
|
101
|
+
state.error && state.retry ? React.createElement('button', { type: 'button', onClick: state.retry }, '重试') : null)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function validImage(meta) {
|
|
105
|
+
return meta && typeof meta.src === 'string'
|
|
106
|
+
&& /^data:image\/(?:svg\+xml|png|jpeg|gif|webp|bmp|x-icon|avif);base64,/.test(meta.src)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 键控 toolview(preview_image):从 meta 读取 { src, mime, label } 渲染
|
|
111
|
+
* 原生 <img>。
|
|
112
|
+
*/
|
|
113
|
+
function PreviewToolView(props) {
|
|
114
|
+
const state = usePreview(props)
|
|
115
|
+
const meta = state.meta
|
|
116
|
+
const [failedSrc, setFailedSrc] = React.useState(null)
|
|
117
|
+
React.useEffect(() => { setFailedSrc(null) }, [props.sessionId, props.callId])
|
|
118
|
+
const label = typeof meta === 'object' && meta !== null && typeof meta.label === 'string' ? meta.label : '预览'
|
|
119
|
+
const src = typeof meta === 'object' && meta !== null && typeof meta.src === 'string' ? meta.src : null
|
|
120
|
+
const isSvg = typeof meta === 'object' && meta !== null && meta.mime === 'image/svg+xml'
|
|
121
|
+
if (!meta) return React.createElement(PreviewStatus, { state })
|
|
122
|
+
if (!validImage(meta)) return React.createElement(PreviewStatus, { state: { error: '图片预览格式无效。' } })
|
|
123
|
+
if (failedSrc === src) return React.createElement(PreviewStatus, { state: {
|
|
124
|
+
error: '浏览器无法解码这张图片,请检查图片文件或重新生成预览。', retry: () => setFailedSrc(null),
|
|
125
|
+
} })
|
|
126
|
+
return React.createElement('div', { className: 'dip-root', 'data-dip-preview': true },
|
|
127
|
+
React.createElement('span', { className: 'dip-label' }, label),
|
|
128
|
+
React.createElement('figure', { className: 'dip-figure' },
|
|
129
|
+
React.createElement('img', {
|
|
130
|
+
className: 'dip-img' + (isSvg ? ' svg' : ''),
|
|
131
|
+
src: src,
|
|
132
|
+
alt: label,
|
|
133
|
+
onError: () => setFailedSrc(src),
|
|
134
|
+
})))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const NUMERIC_RE = /^-?\d+(\.\d+)?$/
|
|
138
|
+
|
|
139
|
+
/** 截断/统计脚注:「显示前 50 / 1234 行 · 前 5 / 8 列 · 分隔符 ","」。 */
|
|
140
|
+
function csvNote(meta) {
|
|
141
|
+
const parts = []
|
|
142
|
+
parts.push(meta.rowsShown < meta.totalRows
|
|
143
|
+
? `显示前 ${meta.rowsShown} / ${meta.totalRows} 行`
|
|
144
|
+
: `共 ${meta.totalRows} 行`)
|
|
145
|
+
if (meta.colsShown < meta.totalCols) parts.push(`前 ${meta.colsShown} / ${meta.totalCols} 列`)
|
|
146
|
+
parts.push(`分隔符 "${meta.delimiter === '\t' ? 'Tab' : meta.delimiter}"`)
|
|
147
|
+
return parts.join(' · ')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 对数据行(不含表头)按列排序:整列(除空串)可解析为数字时按数值比较,
|
|
152
|
+
* 否则按中文 locale 比较;空值恒沉底。dir=1 升序,-1 降序。
|
|
153
|
+
*/
|
|
154
|
+
function sortBodyRows(meta, col, dir) {
|
|
155
|
+
const body = meta.rows.slice(1)
|
|
156
|
+
const numeric = body.every((r) => {
|
|
157
|
+
const v = col < r.length ? r[col] : ''
|
|
158
|
+
return v === '' || NUMERIC_RE.test(v)
|
|
159
|
+
}) && body.some((r) => col < r.length && NUMERIC_RE.test(r[col]))
|
|
160
|
+
const cmp = numeric
|
|
161
|
+
? (a, b) => (parseFloat(a) || 0) - (parseFloat(b) || 0)
|
|
162
|
+
: (a, b) => String(a).localeCompare(String(b), 'zh-Hans-CN')
|
|
163
|
+
return body
|
|
164
|
+
.map((_, i) => i)
|
|
165
|
+
.sort((x, y) => {
|
|
166
|
+
const a = x < body.length && col < body[x].length ? body[x][col] : ''
|
|
167
|
+
const b = y < body.length && col < body[y].length ? body[y][col] : ''
|
|
168
|
+
if ((a === '') !== (b === '')) return a === '' ? 1 : -1
|
|
169
|
+
return cmp(a, b) * dir
|
|
170
|
+
})
|
|
171
|
+
.map((i) => body[i])
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 键控 toolview(preview_csv):从 meta 读取 { label, delimiter, rows,
|
|
176
|
+
* totalRows, totalCols, rowsShown, colsShown },渲染原生 <table>。
|
|
177
|
+
* rows[0] 为表头;数字单元格右对齐;表头点击循环排序(升→降→取消)。
|
|
178
|
+
*/
|
|
179
|
+
function CsvToolView(props) {
|
|
180
|
+
const state = usePreview(props)
|
|
181
|
+
const meta = state.meta
|
|
182
|
+
const ok = meta !== null && typeof meta === 'object' && Array.isArray(meta.rows)
|
|
183
|
+
&& meta.rows.length > 0 && meta.rows.every(Array.isArray)
|
|
184
|
+
// hooks 必须无条件调用,放在提前 return 之前。
|
|
185
|
+
const sortPair = React.useState(null) // null | { col, dir }
|
|
186
|
+
const sort = sortPair[0]
|
|
187
|
+
const setSort = sortPair[1]
|
|
188
|
+
React.useEffect(() => { setSort(null) }, [meta])
|
|
189
|
+
if (!ok) {
|
|
190
|
+
return React.createElement(PreviewStatus, { state: meta ? { error: 'CSV 预览数据无效。' } : state })
|
|
191
|
+
}
|
|
192
|
+
const label = typeof meta.label === 'string' && meta.label !== '' ? meta.label : 'CSV 预览'
|
|
193
|
+
|
|
194
|
+
const headerClick = (ci) => {
|
|
195
|
+
setSort((prev) => (prev !== null && prev.col === ci ? (prev.dir === 1 ? { col: ci, dir: -1 } : null) : { col: ci, dir: 1 }))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const thead = React.createElement('thead', null,
|
|
199
|
+
React.createElement('tr', null, meta.rows[0].map((c, i) => {
|
|
200
|
+
const indicator = sort !== null && sort.col === i ? (sort.dir === 1 ? ' ▲' : ' ▼') : ''
|
|
201
|
+
return React.createElement('th', {
|
|
202
|
+
key: i,
|
|
203
|
+
onClick: () => headerClick(i),
|
|
204
|
+
title: String(c),
|
|
205
|
+
}, String(c) + indicator)
|
|
206
|
+
})))
|
|
207
|
+
const bodyRows = sort !== null ? sortBodyRows(meta, sort.col, sort.dir) : meta.rows.slice(1)
|
|
208
|
+
const tbody = React.createElement('tbody', null, bodyRows.map((r, ri) =>
|
|
209
|
+
React.createElement('tr', { key: ri }, r.map((c, ci) => {
|
|
210
|
+
const s = String(c)
|
|
211
|
+
return React.createElement('td', {
|
|
212
|
+
key: ci,
|
|
213
|
+
className: NUMERIC_RE.test(s) ? 'num' : undefined,
|
|
214
|
+
title: s,
|
|
215
|
+
}, s)
|
|
216
|
+
}))))
|
|
217
|
+
return React.createElement('div', { className: 'dip-root', 'data-dip-preview': true },
|
|
218
|
+
React.createElement('span', { className: 'dip-label' }, label),
|
|
219
|
+
React.createElement('div', { className: 'dip-table-wrap' },
|
|
220
|
+
React.createElement('table', { className: 'dip-table' }, thead, tbody)),
|
|
221
|
+
React.createElement('span', { className: 'dip-csv-note' }, csvNote(meta)))
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── 服务接入 ─────────────────────────────────────────────────────────────
|
|
225
|
+
const PREVIEWS_KEY = 'csv-and-image-preview'
|
|
226
|
+
const isPreviewTool = name => name === 'preview_image' || name === 'preview_csv'
|
|
227
|
+
|
|
228
|
+
// Replay successful native and PTC previews as dedicated Chat nodes.
|
|
229
|
+
// Their end-of-turn anchors remain outside the collapsible process interval.
|
|
230
|
+
const previewCalls = {
|
|
231
|
+
kind: PREVIEWS_KEY + '/calls',
|
|
232
|
+
target: 'chat',
|
|
233
|
+
match(event) {
|
|
234
|
+
if (event.type === 'tool/call') return { id: String(event.data.callId), role: 'start' }
|
|
235
|
+
if (event.type === 'tool/result' && event.surfaceOp === 'append') {
|
|
236
|
+
return { id: String(event.data.message.source.callId), role: 'update' }
|
|
237
|
+
}
|
|
238
|
+
if (event.type === 'tool/ptc-dispatch' && typeof event.data.rootCallId === 'string') {
|
|
239
|
+
return { id: event.data.rootCallId, role: 'update' }
|
|
240
|
+
}
|
|
241
|
+
return null
|
|
242
|
+
},
|
|
243
|
+
start(_context, match) { return { name: match.event.data.name, previews: [] } },
|
|
244
|
+
update(context, match) {
|
|
245
|
+
const { event } = match
|
|
246
|
+
const nested = event.type === 'tool/ptc-dispatch'
|
|
247
|
+
const toolName = nested ? event.data.name : context.state.name
|
|
248
|
+
const result = nested ? event.data : event.data.message.content[0]
|
|
249
|
+
if (!isPreviewTool(toolName) || !result || result.isError) return context.state
|
|
250
|
+
const content = result.content || []
|
|
251
|
+
if (content.some(item => item.type === 'text' && /^preview_(?:image|csv) 失败:/.test(item.text))) return context.state
|
|
252
|
+
const callId = String(nested ? event.data.subCallId : event.data.message.source.callId)
|
|
253
|
+
const preview = { callId, toolName, seq: event.seq,
|
|
254
|
+
block: { kind: 'tool-result', isError: false, content,
|
|
255
|
+
...!nested && event.data.meta ? { meta: event.data.meta } : {} } }
|
|
256
|
+
return { ...context.state, previews: [...context.state.previews.filter(item => item.callId !== callId), preview] }
|
|
257
|
+
},
|
|
258
|
+
buildViewNode(context) {
|
|
259
|
+
const location = context.start?.location
|
|
260
|
+
if (!context.state || !location?.turn) return null
|
|
261
|
+
const end = location.turn.end
|
|
262
|
+
return { key: context.key, id: context.id, kind: context.kind, target: 'chat', location,
|
|
263
|
+
// Placing this row after turn/end keeps it outside the process interval.
|
|
264
|
+
// A hidden node keeps its identity while the toolview handles live output.
|
|
265
|
+
anchorSeq: end ? end.seq + 0.01 : context.start.event.seq,
|
|
266
|
+
visibility: end && context.state.previews.length ? 'visible' : 'hidden',
|
|
267
|
+
data: { previews: context.state.previews } }
|
|
268
|
+
},
|
|
269
|
+
}
|
|
270
|
+
function ChatPreviews(props) {
|
|
271
|
+
return React.createElement('div', { className: 'dip-root', 'data-dip-turn-previews': true },
|
|
272
|
+
props.node.data.previews.map(preview => React.createElement(preview.toolName === 'preview_image' ? PreviewToolView : CsvToolView,
|
|
273
|
+
{ ...props, ...preview, key: props.sessionId + ':' + preview.callId })))
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const inject = ['slots', 'connection', 'uiConversation']
|
|
277
|
+
function apply(ctx) {
|
|
278
|
+
const slots = ctx.slots
|
|
279
|
+
const connection = ctx.connection
|
|
280
|
+
const bindings = {
|
|
281
|
+
async loadPreview(request, signal) {
|
|
282
|
+
const result = await connection.rpc.call('/api', 'csv-and-image-preview/get', request, signal)
|
|
283
|
+
if (!result.ok) throw new Error(result.error.message)
|
|
284
|
+
const value = result.value
|
|
285
|
+
if (request.toolName === 'preview_image' ? !validImage(value)
|
|
286
|
+
: !value || !Array.isArray(value.rows) || !value.rows.length || !value.rows.every(Array.isArray)) {
|
|
287
|
+
throw new Error('返回的预览数据无效。')
|
|
288
|
+
}
|
|
289
|
+
return result.value
|
|
290
|
+
},
|
|
291
|
+
subscribeConnection: listener => connection.generation.subscribe(listener),
|
|
292
|
+
getConnection: () => connection.generation.getSnapshot(),
|
|
293
|
+
}
|
|
294
|
+
ctx.effect(() => {
|
|
295
|
+
const disposals = [
|
|
296
|
+
ctx.uiConversation.events.register(previewCalls),
|
|
297
|
+
slots.inject('tool.call.toolview', () => slots.register(
|
|
298
|
+
{ name: 'tool.call.toolview', key: 'preview_image', inject: () => bindings },
|
|
299
|
+
PreviewToolView,
|
|
300
|
+
)),
|
|
301
|
+
slots.inject('tool.call.toolview', () => slots.register(
|
|
302
|
+
{ name: 'tool.call.toolview', key: 'preview_csv', inject: () => bindings },
|
|
303
|
+
CsvToolView,
|
|
304
|
+
)),
|
|
305
|
+
slots.inject('conversation.chat.node', () => slots.register(
|
|
306
|
+
{ name: 'conversation.chat.node', key: PREVIEWS_KEY + '/calls', inject: () => bindings },
|
|
307
|
+
ChatPreviews,
|
|
308
|
+
)),
|
|
309
|
+
]
|
|
310
|
+
return () => { for (const dispose of disposals) dispose() }
|
|
311
|
+
}, 'preview: toolviews')
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
exports.apply = apply
|
|
315
|
+
exports.inject = inject
|
|
316
|
+
return module.exports
|
|
317
|
+
},
|
|
318
|
+
})
|