chenbhao Claude Opus 4.6 commited on
Commit
f0b3c03
·
1 Parent(s): 3d23e7d

refactor(ImageShowTool): download URLs to temp file, fix concurrency for kitty protocol

Browse files

URLs now downloaded to /tmp before passing to timg as file path instead of
piped stdin. Disable concurrency safety since kitty protocol sequences are
in-band and concurrent terminal writes corrupt each other.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

src/tools/ImageShowTool/ImageShowTool.ts CHANGED
@@ -1,9 +1,9 @@
1
  import { execFileSync } from 'child_process'
 
2
  import fs from 'node:fs'
3
  import { z } from 'zod/v4'
4
  import { Jimp } from "jimp";
5
  import { buildTool, type ToolDef } from '../../Tool.js'
6
- import { loadImageFromUrl } from "../../ink-picture/utils/jimpURL.ts";
7
  import { lazySchema } from '../../utils/lazySchema.js'
8
  import { whichSync } from '../../utils/which.js'
9
  import type { PermissionDecision } from '../../utils/permissions/PermissionResult.js'
@@ -48,9 +48,34 @@ export function isUrl(path: string): boolean {
48
  return path.startsWith("http://") || path.startsWith("https://");
49
  }
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  export async function loadImage(path: string) {
52
  if (isUrl(path)) {
53
- return loadImageFromUrl(path);
 
 
 
 
 
 
54
  } else {
55
  // Read file to Buffer first, then pass to Jimp.read().
56
  // Jimp v1.x has inconsistent path resolution in some runtimes
@@ -143,8 +168,9 @@ export const ImageShowTool = buildTool({
143
  get outputSchema(): OutputSchema {
144
  return outputSchema()
145
  },
 
146
  isConcurrencySafe() {
147
- return true
148
  },
149
  isReadOnly() {
150
  return true
@@ -174,8 +200,17 @@ export const ImageShowTool = buildTool({
174
  renderToolUseMessage,
175
  renderToolResultMessage,
176
  async call({ src }) {
 
177
  try {
178
- const image = await loadImage(src)
 
 
 
 
 
 
 
 
179
  const rows = process.stdout.rows ?? 24
180
  const dims = calculateDimensions(
181
  image.bitmap.width,
@@ -188,13 +223,10 @@ export const ImageShowTool = buildTool({
188
  try {
189
  const timgPath = whichSync('timg')
190
  if (timgPath) {
191
- // Pipe image to timg via stdin, capture Kitty protocol output
192
- const pngBuffer = await image.getBuffer("image/png")
193
  kittySequence = execFileSync(
194
  timgPath,
195
- ['-p', 'kitty', '-g', `${dims.width}x${dims.height}`, '-'],
196
  {
197
- input: pngBuffer,
198
  encoding: 'utf8',
199
  timeout: 30000,
200
  maxBuffer: 50 * 1024 * 1024,
@@ -221,6 +253,10 @@ export const ImageShowTool = buildTool({
221
  success: false,
222
  } satisfies ImageShowOutput,
223
  }
 
 
 
 
224
  }
225
  },
226
  mapToolResultToToolResultBlockParam(output: ImageShowOutput, toolUseID: string) {
 
1
  import { execFileSync } from 'child_process'
2
+ import crypto from 'node:crypto'
3
  import fs from 'node:fs'
4
  import { z } from 'zod/v4'
5
  import { Jimp } from "jimp";
6
  import { buildTool, type ToolDef } from '../../Tool.js'
 
7
  import { lazySchema } from '../../utils/lazySchema.js'
8
  import { whichSync } from '../../utils/which.js'
9
  import type { PermissionDecision } from '../../utils/permissions/PermissionResult.js'
 
48
  return path.startsWith("http://") || path.startsWith("https://");
49
  }
50
 
51
+ /**
52
+ * Download a URL to a temporary file, return the path.
53
+ * Caller should delete the file after use.
54
+ */
55
+ export async function downloadToTemp(url: string): Promise<string> {
56
+ const res = await fetch(url, {
57
+ headers: {
58
+ 'User-Agent': 'Mozilla/5.0 (compatible; ImageShowTool/1.0)',
59
+ 'Accept': 'image/*',
60
+ },
61
+ })
62
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
63
+ const buffer = Buffer.from(await res.arrayBuffer())
64
+ const hash = crypto.createHash('md5').update(url).digest('hex')
65
+ const tmp = `/tmp/img_${hash}`
66
+ fs.writeFileSync(tmp, buffer)
67
+ return tmp
68
+ }
69
+
70
  export async function loadImage(path: string) {
71
  if (isUrl(path)) {
72
+ // Download to temp file, then read with Jimp
73
+ const tmp = await downloadToTemp(path)
74
+ try {
75
+ return await Jimp.read(fs.readFileSync(tmp))
76
+ } finally {
77
+ fs.unlinkSync(tmp)
78
+ }
79
  } else {
80
  // Read file to Buffer first, then pass to Jimp.read().
81
  // Jimp v1.x has inconsistent path resolution in some runtimes
 
168
  get outputSchema(): OutputSchema {
169
  return outputSchema()
170
  },
171
+ // Kitty 协议序列是带内数据,并发写入终端会互相覆盖导致图片不显示
172
  isConcurrencySafe() {
173
+ return false
174
  },
175
  isReadOnly() {
176
  return true
 
200
  renderToolUseMessage,
201
  renderToolResultMessage,
202
  async call({ src }) {
203
+ let tmpFile: string | undefined
204
  try {
205
+ // For URLs: download to temp file so timg can read it directly
206
+ let imagePath = src
207
+ if (isUrl(src)) {
208
+ tmpFile = await downloadToTemp(src)
209
+ imagePath = tmpFile
210
+ }
211
+
212
+ // Read with Jimp for dimension calculation (from buffer, no MIME restriction)
213
+ const image = await Jimp.read(fs.readFileSync(imagePath))
214
  const rows = process.stdout.rows ?? 24
215
  const dims = calculateDimensions(
216
  image.bitmap.width,
 
223
  try {
224
  const timgPath = whichSync('timg')
225
  if (timgPath) {
 
 
226
  kittySequence = execFileSync(
227
  timgPath,
228
+ ['-p', 'kitty', '-g', `${dims.width}x${dims.height}`, imagePath],
229
  {
 
230
  encoding: 'utf8',
231
  timeout: 30000,
232
  maxBuffer: 50 * 1024 * 1024,
 
253
  success: false,
254
  } satisfies ImageShowOutput,
255
  }
256
+ } finally {
257
+ if (tmpFile) {
258
+ try { fs.unlinkSync(tmpFile) } catch {}
259
+ }
260
  }
261
  },
262
  mapToolResultToToolResultBlockParam(output: ImageShowOutput, toolUseID: string) {