chenbhao Claude Opus 4.6 commited on
Commit
8ed2aac
·
1 Parent(s): 603297b

Add inline image utilities and integrate image handling into Location, WebFetch, and WebSearch tools.

Browse files
src/tools/LocationTool/LocationTool.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
  renderToolUseMessage,
10
  renderToolUseProgressMessage,
11
  } from './UI.js'
 
12
 
13
  // ── Region detection ────────────────────────────────────────────────────────
14
 
@@ -365,6 +366,7 @@ type Output = {
365
  error?: string
366
  rawData?: unknown
367
  ipGeo?: IpGeoInfo
 
368
  }
369
 
370
  // ── Amap API helpers ─────────────────────────────────────────────────────────
@@ -912,6 +914,7 @@ export const LocationTool = buildTool({
912
  let geocoding: LocationResult | undefined
913
  let places: PlaceResult[] | undefined
914
  let directions: DirectionsResult | undefined
 
915
 
916
  switch (action) {
917
  case 'geocode':
@@ -995,6 +998,15 @@ export const LocationTool = buildTool({
995
  })
996
  }
997
 
 
 
 
 
 
 
 
 
 
998
  return {
999
  data: {
1000
  action,
@@ -1003,6 +1015,7 @@ export const LocationTool = buildTool({
1003
  places,
1004
  directions,
1005
  ipGeo,
 
1006
  } satisfies Output,
1007
  }
1008
  } catch (error) {
@@ -1094,7 +1107,17 @@ export const LocationTool = buildTool({
1094
  return {
1095
  tool_use_id: toolUseID,
1096
  type: 'tool_result',
1097
- content: lines.join('\n'),
 
 
 
 
 
 
 
 
 
 
1098
  }
1099
  },
1100
  }) satisfies ToolDef<ReturnType<typeof inputSchema>, Output, LocationToolProgress>
 
9
  renderToolUseMessage,
10
  renderToolUseProgressMessage,
11
  } from './UI.js'
12
+ import { fetchImagesAsInline } from '../../utils/inlineImageUtils.js'
13
 
14
  // ── Region detection ────────────────────────────────────────────────────────
15
 
 
366
  error?: string
367
  rawData?: unknown
368
  ipGeo?: IpGeoInfo
369
+ images?: Array<{ url: string; base64: string; mediaType: string }>
370
  }
371
 
372
  // ── Amap API helpers ─────────────────────────────────────────────────────────
 
914
  let geocoding: LocationResult | undefined
915
  let places: PlaceResult[] | undefined
916
  let directions: DirectionsResult | undefined
917
+ let images: Output['images']
918
 
919
  switch (action) {
920
  case 'geocode':
 
998
  })
999
  }
1000
 
1001
+ if (places && places.length > 0) {
1002
+ const photoUrls = places
1003
+ .flatMap((p) => p.photos ?? [])
1004
+ .slice(0, 6)
1005
+ if (photoUrls.length > 0) {
1006
+ images = await fetchImagesAsInline(photoUrls, 6)
1007
+ }
1008
+ }
1009
+
1010
  return {
1011
  data: {
1012
  action,
 
1015
  places,
1016
  directions,
1017
  ipGeo,
1018
+ images,
1019
  } satisfies Output,
1020
  }
1021
  } catch (error) {
 
1107
  return {
1108
  tool_use_id: toolUseID,
1109
  type: 'tool_result',
1110
+ content: [
1111
+ { type: 'text', text: lines.join('\n') },
1112
+ ...(output.images?.map((img) => ({
1113
+ type: 'image' as const,
1114
+ source: {
1115
+ type: 'base64' as const,
1116
+ data: img.base64,
1117
+ media_type: img.mediaType as 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp',
1118
+ },
1119
+ })) ?? []),
1120
+ ],
1121
  }
1122
  },
1123
  }) satisfies ToolDef<ReturnType<typeof inputSchema>, Output, LocationToolProgress>
src/tools/LocationTool/prompt.ts CHANGED
@@ -24,7 +24,12 @@ This tool provides location-based services using two map providers:
24
  **Combining with other tools:**
25
  - After getting place/POI results from LocationTool, you can use **WebSearchTool** to search for travel guides, reviews, or additional information about specific places
26
  - Use **WebFetchTool** to fetch detailed information from URLs found in search results (e.g., attraction pages, hotel booking sites, travel blog posts)
27
- - Example: Search places WebSearch for reviews WebFetch for detailed info
 
 
 
 
 
28
 
29
  **Transport modes for directions:**
30
  - For China (Amap): "driving", "walking", "transit" (公交/地铁)
 
24
  **Combining with other tools:**
25
  - After getting place/POI results from LocationTool, you can use **WebSearchTool** to search for travel guides, reviews, or additional information about specific places
26
  - Use **WebFetchTool** to fetch detailed information from URLs found in search results (e.g., attraction pages, hotel booking sites, travel blog posts)
27
+ - **ImageShowTool**: When place results include photo URLs, the tool will automatically display images inline in the terminal. You can also explicitly call ImageShowTool with any image URL (local path or https:// URL) to display images.
28
+ - **Google Street View**: For any location (lat,lng), you can directly call ImageShowTool with a Google Street View Static API URL to display a street view panorama:
29
+ - URL format: \`https://maps.googleapis.com/maps/api/streetview?size=800x400&location=<lat>,<lng>&heading=0&pitch=0&key=<YOUR_API_KEY>\`
30
+ - Note: The free tier of Google Street View Static API requires an API key but has generous quotas
31
+ - Use this when users ask for "street view" or "街景" of a location
32
+ - Example: Search places → WebSearch for reviews → WebFetch for detailed info → ImageShow for photos → ImageShow with Google Street View URL for street panoramas
33
 
34
  **Transport modes for directions:**
35
  - For China (Amap): "driving", "walking", "transit" (公交/地铁)
src/tools/WebFetchTool/WebFetchTool.ts CHANGED
@@ -14,6 +14,7 @@ import {
14
  type FetchedContent,
15
  getURLMarkdownContent,
16
  } from './utils.js' // 抓网页, 处理 markdown
 
17
 
18
  const inputSchema = lazySchema(() =>
19
  z.strictObject({ // 严格对象 不允许多字段
@@ -40,7 +41,9 @@ const outputSchema = lazySchema(() =>
40
  )
41
  type OutputSchema = ReturnType<typeof outputSchema>
42
 
43
- export type Output = z.infer<OutputSchema> // 输出类型推导
 
 
44
 
45
  // Tool 定义开始
46
  export const WebFetchTool = buildTool({
@@ -180,6 +183,13 @@ To complete your request, I need to fetch content from the redirected URL. Pleas
180
  result += `\n\n[Binary content also saved to ${persistedPath}]`
181
  }
182
 
 
 
 
 
 
 
 
183
  const output: Output = {
184
  bytes,
185
  code,
@@ -187,6 +197,7 @@ To complete your request, I need to fetch content from the redirected URL. Pleas
187
  result,
188
  durationMs: Date.now() - start,
189
  url,
 
190
  }
191
 
192
  return {
@@ -210,11 +221,21 @@ To complete your request, I need to fetch content from the redirected URL. Pleas
210
  }
211
  }
212
  },
213
- mapToolResultToToolResultBlockParam({ result }, toolUseID) {
214
  return {
215
  tool_use_id: toolUseID,
216
  type: 'tool_result',
217
- content: result,
 
 
 
 
 
 
 
 
 
 
218
  }
219
  },
220
  } satisfies ToolDef<InputSchema, Output>)
 
14
  type FetchedContent,
15
  getURLMarkdownContent,
16
  } from './utils.js' // 抓网页, 处理 markdown
17
+ import { fetchImagesAsInline } from '../../utils/inlineImageUtils.js'
18
 
19
  const inputSchema = lazySchema(() =>
20
  z.strictObject({ // 严格对象 不允许多字段
 
41
  )
42
  type OutputSchema = ReturnType<typeof outputSchema>
43
 
44
+ export type Output = z.infer<OutputSchema> & {
45
+ images?: Array<{ url: string; base64: string; mediaType: string }>
46
+ }
47
 
48
  // Tool 定义开始
49
  export const WebFetchTool = buildTool({
 
183
  result += `\n\n[Binary content also saved to ${persistedPath}]`
184
  }
185
 
186
+ const imageUrls = result
187
+ ? result.match(/!\[.*?\]\((https?:\/\/[^)\s]+)\)/g)?.map((m) => m.match(/!\[[^\]]*\]\(([^)]+)\)/)?.[1] ?? []) ?? []
188
+ : []
189
+
190
+ const images =
191
+ imageUrls.length > 0 ? await fetchImagesAsInline(imageUrls.slice(0, 4), 4) : undefined
192
+
193
  const output: Output = {
194
  bytes,
195
  code,
 
197
  result,
198
  durationMs: Date.now() - start,
199
  url,
200
+ images,
201
  }
202
 
203
  return {
 
221
  }
222
  }
223
  },
224
+ mapToolResultToToolResultBlockParam(output, toolUseID) {
225
  return {
226
  tool_use_id: toolUseID,
227
  type: 'tool_result',
228
+ content: [
229
+ { type: 'text', text: output.result },
230
+ ...(output.images?.map((img) => ({
231
+ type: 'image' as const,
232
+ source: {
233
+ type: 'base64' as const,
234
+ data: img.base64,
235
+ media_type: img.mediaType as 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp',
236
+ },
237
+ })) ?? []),
238
+ ],
239
  }
240
  },
241
  } satisfies ToolDef<InputSchema, Output>)
src/tools/WebFetchTool/prompt.ts CHANGED
@@ -18,6 +18,7 @@ Usage notes:
18
  - Includes a self-cleaning 15-minute cache for faster responses when repeatedly accessing the same URL
19
  - When a URL redirects to a different host, the tool will inform you and provide the redirect URL in a special format. You should then make a new WebFetch request with the redirect URL to fetch the content.
20
  - For GitHub URLs, prefer using the gh CLI via Bash instead (e.g., gh pr view, gh issue view, gh api).
 
21
  `
22
 
23
  export function makeSecondaryModelPrompt(
 
18
  - Includes a self-cleaning 15-minute cache for faster responses when repeatedly accessing the same URL
19
  - When a URL redirects to a different host, the tool will inform you and provide the redirect URL in a special format. You should then make a new WebFetch request with the redirect URL to fetch the content.
20
  - For GitHub URLs, prefer using the gh CLI via Bash instead (e.g., gh pr view, gh issue view, gh api).
21
+ - **ImageShowTool**: When fetched content contains image URLs (e.g., from a wiki page or travel blog), images will be automatically displayed inline in the terminal. You can also explicitly call ImageShowTool with any image URL to display it.
22
  `
23
 
24
  export function makeSecondaryModelPrompt(
src/tools/WebSearchTool/WebSearchTool.ts CHANGED
@@ -11,6 +11,7 @@ import {
11
  renderToolUseMessage,
12
  renderToolUseProgressMessage,
13
  } from './UI.js'
 
14
 
15
  const inputSchema = lazySchema(() =>
16
  z.strictObject({
@@ -44,7 +45,9 @@ const outputSchema = lazySchema(() =>
44
  }),
45
  )
46
 
47
- export type Output = z.infer<ReturnType<typeof outputSchema>>
 
 
48
 
49
  export type { WebSearchProgress } from '../../types/tools.js'
50
  import type { WebSearchProgress } from '../../types/tools.js'
@@ -257,6 +260,14 @@ export const WebSearchTool = buildTool({
257
  },
258
  ]
259
 
 
 
 
 
 
 
 
 
260
  const duration = (performance.now() - start) / 1000
261
 
262
  return {
@@ -264,6 +275,7 @@ export const WebSearchTool = buildTool({
264
  query: input.query,
265
  results: output,
266
  durationSeconds: duration,
 
267
  },
268
  }
269
  } catch (error) {
@@ -298,8 +310,7 @@ export const WebSearchTool = buildTool({
298
  text += r + '\n\n'
299
  } else {
300
  r.content.forEach((item: any, i: number) => {
301
- const imgLine = item.image ? `\n![${item.title}](${item.image})\n` : ''
302
- text += `${i + 1}. ${item.title}\n${item.url}\n${item.snippet || ''}${imgLine}\n`
303
  })
304
  }
305
  }
@@ -307,7 +318,17 @@ export const WebSearchTool = buildTool({
307
  return {
308
  tool_use_id: toolUseID,
309
  type: 'tool_result',
310
- content: text,
 
 
 
 
 
 
 
 
 
 
311
  }
312
  },
313
  }) satisfies ToolDef<any, Output, WebSearchProgress>
 
11
  renderToolUseMessage,
12
  renderToolUseProgressMessage,
13
  } from './UI.js'
14
+ import { fetchImagesAsInline } from '../../utils/inlineImageUtils.js'
15
 
16
  const inputSchema = lazySchema(() =>
17
  z.strictObject({
 
45
  }),
46
  )
47
 
48
+ export type Output = z.infer<ReturnType<typeof outputSchema>> & {
49
+ images?: Array<{ url: string; base64: string; mediaType: string }>
50
+ }
51
 
52
  export type { WebSearchProgress } from '../../types/tools.js'
53
  import type { WebSearchProgress } from '../../types/tools.js'
 
260
  },
261
  ]
262
 
263
+ const imageUrls = cleaned
264
+ .map((r) => r.image)
265
+ .filter((url): url is string => !!url)
266
+ .slice(0, 4)
267
+
268
+ const images =
269
+ imageUrls.length > 0 ? await fetchImagesAsInline(imageUrls, 4) : undefined
270
+
271
  const duration = (performance.now() - start) / 1000
272
 
273
  return {
 
275
  query: input.query,
276
  results: output,
277
  durationSeconds: duration,
278
+ images,
279
  },
280
  }
281
  } catch (error) {
 
310
  text += r + '\n\n'
311
  } else {
312
  r.content.forEach((item: any, i: number) => {
313
+ text += `${i + 1}. ${item.title}\n${item.url}\n${item.snippet || ''}\n`
 
314
  })
315
  }
316
  }
 
318
  return {
319
  tool_use_id: toolUseID,
320
  type: 'tool_result',
321
+ content: [
322
+ { type: 'text', text },
323
+ ...(output.images?.map((img) => ({
324
+ type: 'image' as const,
325
+ source: {
326
+ type: 'base64' as const,
327
+ data: img.base64,
328
+ media_type: img.mediaType as 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp',
329
+ },
330
+ })) ?? []),
331
+ ],
332
  }
333
  },
334
  }) satisfies ToolDef<any, Output, WebSearchProgress>
src/tools/WebSearchTool/prompt.ts CHANGED
@@ -21,9 +21,8 @@ Search Strategy:
21
 
22
  Search Result Format:
23
  - Each search result includes: title, URL, snippet, and optionally an image URL (thumbnail)
24
- - You can use images in your response via markdown syntax: ![alt text](image_url)
25
- - When search results include images, incorporate them into your answer for a richer,图文混排 experience
26
- - Images make responses much more readable — use them whenever available
27
 
28
  CRITICAL REQUIREMENT - You MUST follow this:
29
  - After answering the user's question, you MUST include a "Sources:" section at the end of your response
 
21
 
22
  Search Result Format:
23
  - Each search result includes: title, URL, snippet, and optionally an image URL (thumbnail)
24
+ - **Images are automatically displayed inline in the terminal** when search results include image thumbnails — no need to manually call ImageShowTool for search result images
25
+ - You can also explicitly call ImageShowTool with any image URL (https://... or local path) to display images
 
26
 
27
  CRITICAL REQUIREMENT - You MUST follow this:
28
  - After answering the user's question, you MUST include a "Sources:" section at the end of your response
src/utils/inlineImageUtils.ts ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { downloadImage } from './imageUrlCache.js'
2
+ import { logForDebugging } from './debug.js'
3
+
4
+ const IMAGE_URL_PATTERNS = [
5
+ /https?:\/\/[^\s<>\"')\]]+\.(?:jpg|jpeg|png|gif|webp)(?:[^\s<>\"'\]]*)/gi,
6
+ /https?:\/\/[^\s<>\"')\]]+(?:store\.is\.autonavi\.com|maps\.google\.com|upload\.wikimedia\.org)[^\s<>\"'\]]*/gi,
7
+ ]
8
+
9
+ export interface InlineImage {
10
+ url: string
11
+ base64: string
12
+ mediaType: string
13
+ }
14
+
15
+ export function detectImageUrls(text: string): string[] {
16
+ const urls = new Set<string>()
17
+ for (const pattern of IMAGE_URL_PATTERNS) {
18
+ const matches = text.matchAll(pattern)
19
+ for (const match of matches) {
20
+ if (match[0]) {
21
+ urls.add(match[0])
22
+ }
23
+ }
24
+ }
25
+ return Array.from(urls)
26
+ }
27
+
28
+ export async function fetchImagesAsInline(
29
+ urls: string[],
30
+ maxImages = 4,
31
+ ): Promise<InlineImage[]> {
32
+ const uniqueUrls = [...new Set(urls)].slice(0, maxImages)
33
+ const results: InlineImage[] = []
34
+
35
+ await Promise.all(
36
+ uniqueUrls.map(async (url) => {
37
+ try {
38
+ const cached = await downloadImage(url)
39
+ if (cached) {
40
+ results.push({
41
+ url,
42
+ base64: cached.buffer.toString('base64'),
43
+ mediaType: cached.mediaType,
44
+ })
45
+ }
46
+ } catch (err) {
47
+ logForDebugging(`fetchImagesAsInline: failed to fetch ${url}: ${err}`)
48
+ }
49
+ }),
50
+ )
51
+
52
+ return results
53
+ }