@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/lib/store.js ADDED
@@ -0,0 +1,144 @@
1
+ import { createHash, randomUUID } from 'node:crypto'
2
+ import { mkdir, open, rename, unlink } from 'node:fs/promises'
3
+ import { homedir } from 'node:os'
4
+ import { join, resolve } from 'node:path'
5
+
6
+ export const PREVIEW_ENDPOINT = 'csv-and-image-preview/get'
7
+ export const MAX_INPUT_BYTES = 8 * 1024 * 1024
8
+ const MAX_RECORD_BYTES = 16 * 1024 * 1024
9
+
10
+ function defaultDirectory() {
11
+ let home = process.env.DSH_HOME?.trim() || join(homedir(), '.dsh')
12
+ if (home === '~') home = homedir()
13
+ else if (/^~[/\\]/.test(home)) home = join(homedir(), home.slice(2))
14
+ return resolve(home, 'storages', 'csv-and-image-preview')
15
+ }
16
+
17
+ /** Bound the allocation even when a source file grows after stat(). */
18
+ export async function readBounded(path, limit = MAX_INPUT_BYTES, signal) {
19
+ signal?.throwIfAborted()
20
+ const file = await open(path, 'r')
21
+ try {
22
+ const info = await file.stat()
23
+ if (!info.isFile()) throw new Error('预览路径必须是普通文件。')
24
+ if (info.size > limit) throw new Error(`文件超过预览上限 ${limit / 1024 / 1024} MB。`)
25
+ const buffer = Buffer.alloc(Math.min(info.size + 1, limit + 1))
26
+ let length = 0
27
+ while (length < buffer.length) {
28
+ signal?.throwIfAborted()
29
+ const { bytesRead } = await file.read(buffer, length, buffer.length - length, null)
30
+ if (bytesRead === 0) break
31
+ length += bytesRead
32
+ }
33
+ signal?.throwIfAborted()
34
+ if (length > limit || length > info.size) throw new Error('文件在读取时变大,请重试预览。')
35
+ return buffer.subarray(0, length)
36
+ } finally {
37
+ await file.close()
38
+ }
39
+ }
40
+
41
+ function validIdentity(value) {
42
+ return typeof value === 'string' && value.length > 0 && value.length <= 512
43
+ }
44
+
45
+ export function validateRequest(request) {
46
+ if (!request || !validIdentity(request.sessionId) || !validIdentity(request.callId)
47
+ || !['preview_image', 'preview_csv'].includes(request.toolName)) {
48
+ throw new Error('无效的预览请求。')
49
+ }
50
+ }
51
+
52
+ export function executionIdentity(exec, toolName) {
53
+ const identity = { sessionId: exec?.agent?.session?.id, callId: exec?.callId, toolName }
54
+ validateRequest(identity)
55
+ return identity
56
+ }
57
+
58
+ /** Immutable per-call snapshots, outside model output and original workspace files. */
59
+ export class PreviewStore {
60
+ constructor(directory = defaultDirectory()) { this.directory = resolve(directory) }
61
+
62
+ path(identity) {
63
+ validateRequest(identity)
64
+ const key = createHash('sha256').update(JSON.stringify([
65
+ identity.sessionId, identity.callId, identity.toolName,
66
+ ])).digest('hex')
67
+ return join(this.directory, `${key}.json`)
68
+ }
69
+
70
+ async put(identity, payload, signal) {
71
+ const target = this.path(identity)
72
+ const body = JSON.stringify({ version: 1, ...identity, payload })
73
+ if (Buffer.byteLength(body) > MAX_RECORD_BYTES) throw new Error('预览快照超过存储上限。')
74
+ signal?.throwIfAborted()
75
+ await mkdir(this.directory, { recursive: true })
76
+ const temporary = `${target}.${randomUUID()}.tmp`
77
+ try {
78
+ const file = await open(temporary, 'wx', 0o600)
79
+ try {
80
+ await file.writeFile(body, { encoding: 'utf8', signal })
81
+ await file.sync()
82
+ } finally { await file.close() }
83
+ signal?.throwIfAborted()
84
+ await rename(temporary, target)
85
+ } finally {
86
+ await unlink(temporary).catch(error => { if (error.code !== 'ENOENT') throw error })
87
+ }
88
+ }
89
+
90
+ async get(identity, signal) {
91
+ let bytes
92
+ try { bytes = await readBounded(this.path(identity), MAX_RECORD_BYTES, signal) }
93
+ catch (error) { if (error.code === 'ENOENT') return null; throw error }
94
+ const record = JSON.parse(bytes.toString('utf8'))
95
+ if (record.version !== 1 || record.sessionId !== identity.sessionId
96
+ || record.callId !== identity.callId || record.toolName !== identity.toolName) {
97
+ throw new Error('预览快照与当前会话不匹配。')
98
+ }
99
+ return record.payload
100
+ }
101
+ }
102
+
103
+ /** Connection applies browser authentication before this handler is invoked. */
104
+ export function createPreviewHandler(store) {
105
+ return async (_endpoint, request, signal) => {
106
+ try { validateRequest(request) }
107
+ catch { return { ok: false, error: { code: 'preview/invalid', message: '无效的预览请求。', details: {} } } }
108
+ try {
109
+ const value = await store.get(request, signal)
110
+ if (value === null) return { ok: false, error: {
111
+ code: 'preview/missing', message: '未找到此调用的预览快照,旧版嵌套调用需要重新预览。', details: {},
112
+ } }
113
+ return { ok: true, value }
114
+ } catch {
115
+ return { ok: false, error: { code: 'preview/read', message: '读取预览快照失败,请重试。', details: {} } }
116
+ }
117
+ }
118
+ }
119
+
120
+ /** Own only this exact path. The single shared /api interceptor belongs to DSH's Gateway. */
121
+ export function createPreviewRoute(store) {
122
+ const handle = createPreviewHandler(store)
123
+ return {
124
+ path: `/api/${PREVIEW_ENDPOINT}`,
125
+ methods: ['POST'],
126
+ requestBody: 'buffered',
127
+ async fetch(request) {
128
+ if (request.headers.get('content-type')?.split(';')[0].trim().toLowerCase() !== 'application/json') {
129
+ return new Response('content type must be application/json', { status: 415 })
130
+ }
131
+ let message
132
+ try { message = await request.json() }
133
+ catch { return new Response('invalid JSON', { status: 400 }) }
134
+ if (!message || message.type !== 'client-request' || !validIdentity(message.rpcId)
135
+ || message.method !== PREVIEW_ENDPOINT) {
136
+ return new Response('invalid preview RPC envelope', { status: 400 })
137
+ }
138
+ return Response.json({
139
+ type: 'server-response', rpcId: message.rpcId,
140
+ result: await handle(PREVIEW_ENDPOINT, message.payload, request.signal),
141
+ }, { headers: { 'Cache-Control': 'private, no-store' } })
142
+ },
143
+ }
144
+ }
@@ -1,54 +1,68 @@
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.
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
5
  */
6
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
7
+ /** Execution identity supplied by DSH for native and run_code child calls. */
8
+ export interface PreviewExecution {
9
+ callId: string
10
+ signal: AbortSignal
11
+ agent: { session: { id: string; header: { cwd?: string } } }
16
12
  }
17
13
 
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
14
+ export interface PreviewSnapshotStore {
15
+ put(identity: { sessionId: string; callId: string; toolName: string }, payload: unknown, signal?: AbortSignal): Promise<void>
27
16
  }
28
-
29
- /** Cordis plugin exported by the host half (lib/index.js). */
30
- export interface ImagePreviewPlugin {
31
- readonly name: 'csv-and-image-preview'
32
- apply(ctx: unknown): void
33
- }
34
-
35
- /** A keyed display image payload carried on tool result meta. */
36
- export interface PreviewMeta {
37
- src: string
38
- mime: string
39
- label: string
40
- }
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
-
53
- declare const plugin: ImagePreviewPlugin
54
- export default plugin
17
+
18
+ /** The `preview_image` tool definition (a dsh ToolDefinition object). */
19
+ export interface PreviewToolDefinition {
20
+ name: 'preview_image'
21
+ description: string
22
+ parameters: Record<string, unknown>
23
+ output: Record<string, unknown>
24
+ execute(args: unknown, exec: PreviewExecution): Promise<string>
25
+ presentCall(args: unknown): { card: string; title: string; kind: string } | undefined
26
+ presentResult(args: unknown): { card: string; title: string } | undefined
27
+ }
28
+
29
+ /** The `preview_csv` tool definition (a dsh ToolDefinition object). */
30
+ export interface CsvToolDefinition {
31
+ name: 'preview_csv'
32
+ description: string
33
+ parameters: Record<string, unknown>
34
+ output: Record<string, unknown>
35
+ execute(args: unknown, exec: PreviewExecution): Promise<string>
36
+ presentCall(args: unknown): { card: string; title: string; kind: string } | undefined
37
+ presentResult(args: unknown): { card: string; title: string } | undefined
38
+ }
39
+
40
+ /** Cordis plugin exported by the host half (lib/index.js). */
41
+ export interface ImagePreviewPlugin {
42
+ readonly name: 'csv-and-image-preview'
43
+ apply(ctx: unknown): void
44
+ }
45
+
46
+ /** A keyed display image payload carried on tool result meta. */
47
+ export interface PreviewMeta {
48
+ src: string
49
+ mime: string
50
+ label: string
51
+ }
52
+
53
+ /** A keyed bounded table payload carried on tool result meta (rows[0] = header). */
54
+ export interface CsvPreviewMeta {
55
+ label: string
56
+ delimiter: string
57
+ rows: string[][]
58
+ totalRows: number
59
+ totalCols: number
60
+ rowsShown: number
61
+ colsShown: number
62
+ }
63
+
64
+ export declare const name: 'csv-and-image-preview'
65
+ export declare const inject: string[]
66
+ export declare function apply(ctx: unknown): void
67
+ export declare function createPreviewTool(store?: PreviewSnapshotStore): PreviewToolDefinition
68
+ export declare function createCsvTool(store?: PreviewSnapshotStore): CsvToolDefinition
package/package.json CHANGED
@@ -1,67 +1,81 @@
1
- {
2
- "name": "@jetecho/dsh-csv-and-image-preview",
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
- "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
- "csv",
46
- "table",
47
- "preview",
48
- "visual"
49
- ],
50
- "license": "MIT",
51
- "author": "jetecho",
52
- "publishConfig": {
53
- "access": "public"
54
- },
55
- "repository": {
56
- "type": "git",
57
- "url": "git+https://github.com/jetecho/dsh-csv-and-image-preview.git"
58
- },
59
- "homepage": "https://github.com/jetecho/dsh-csv-and-image-preview#readme",
60
- "peerDependencies": {
61
- "@deepseek-ai/cordis": "^4.0.1",
62
- "react": "^18.0.0 || ^19.0.0"
63
- },
64
- "dependencies": {
65
- "react": "^18.3.1"
66
- }
67
- }
1
+ {
2
+ "name": "@jetecho/dsh-csv-and-image-preview",
3
+ "version": "0.1.3",
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
+ "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
+ "test": "node --test tests/*.test.mjs",
28
+ "preview:test": "node scripts/preview-test.mjs",
29
+ "build": "node scripts/build.mjs",
30
+ "prepack": "node scripts/prepack.mjs"
31
+ },
32
+ "dsh": {
33
+ "bundle": {
34
+ "patch": "./cordis.patch.yml"
35
+ },
36
+ "client": {
37
+ "inject": [
38
+ "@deepseek-ai/dsh-client-connection",
39
+ "@deepseek-ai/dsh-client-ui-renderer",
40
+ "@deepseek-ai/dsh-client-ui-conversation"
41
+ ],
42
+ "platform": "web"
43
+ }
44
+ },
45
+ "keywords": [
46
+ "dsh",
47
+ "deepseek-harness",
48
+ "dsh-plugin",
49
+ "image",
50
+ "svg",
51
+ "csv",
52
+ "table",
53
+ "preview",
54
+ "visual"
55
+ ],
56
+ "license": "MIT",
57
+ "author": "jetecho",
58
+ "publishConfig": {
59
+ "access": "public"
60
+ },
61
+ "repository": {
62
+ "type": "git",
63
+ "url": "git+https://github.com/jetecho/dsh-csv-and-image-preview.git"
64
+ },
65
+ "homepage": "https://github.com/jetecho/dsh-csv-and-image-preview#readme",
66
+ "peerDependencies": {
67
+ "@deepseek-ai/cordis": "^4.0.2",
68
+ "react": "^18.0.0 || ^19.0.0"
69
+ },
70
+ "devDependencies": {
71
+ "@deepseek-ai/dsh-client-connection": "^0.1.5-rc.1",
72
+ "@deepseek-ai/dsh-client-store": "^0.1.5-rc.1",
73
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.1",
74
+ "@deepseek-ai/dsh-tools": "0.1.5-rc.1",
75
+ "immer": "^10.2.0",
76
+ "react": "^18.3.1",
77
+ "react-dom": "^18.3.1",
78
+ "react-test-renderer": "^18.3.1",
79
+ "zustand": "~4.4.7"
80
+ }
81
+ }