chenbhao Claude Big Pickle commited on
Commit
0c8a99f
·
1 Parent(s): 5b13359

feat: remove result limits on LocationTool place searches (Amap/Google)

Browse files

- Amap (高德): offset 25 → 1000 per page with pagination loop (≤20 pages)
- Google Places: remove .slice(0,20), add next_page_token pagination (≤3 pages)
- Photos list: remove .slice(0,3) limit

Co-Authored-By: Claude Big Pickle <noreply@anthropic.com>

src/tools/LocationTool/LocationTool.ts CHANGED
@@ -413,30 +413,45 @@ async function amapSearchPlaces(
413
  // Use pre-resolved coordinates if available, otherwise geocode
414
  const resolved = geo ?? await amapGeocode(location)
415
 
416
- const params = new URLSearchParams({
417
- key,
418
- location: `${resolved.lng},${resolved.lat}`,
419
- radius: String(radius || 5000),
420
- offset: '25',
421
- page: '1',
422
- extensions: 'all',
423
- })
424
- if (query) params.set('keywords', query)
425
- if (type) params.set('types', type)
426
 
427
- const res = await fetch(`https://restapi.amap.com/v3/place/around?${params}`, {
428
- headers: { 'Accept': 'application/json' },
429
- signal: AbortSignal.timeout(15000),
430
- })
 
 
 
 
 
 
 
431
 
432
- if (!res.ok) throw new Error(`Amap around search HTTP ${res.status}`)
433
- const data = await res.json() as any
 
 
 
 
 
 
 
 
 
434
 
435
- if (data.status !== '1') {
436
- throw new Error(`Amap search failed: ${data.info}`)
 
 
 
 
 
 
437
  }
438
 
439
- return (data.pois || []).map((poi: any) => ({
440
  name: poi.name,
441
  address: poi.address || '',
442
  lat: parseFloat(poi.location?.split(',')[1] || '0'),
@@ -574,16 +589,30 @@ async function googleSearchPlaces(
574
  ): Promise<PlaceResult[]> {
575
  const key = getGoogleKey()
576
  const resolved = geo ?? await googleGeocode(location, language)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
577
 
578
- const params = new URLSearchParams({
579
- key,
580
- language: language || 'zh-CN',
581
- })
582
-
583
- if (query) {
584
- // Text search for specific queries
585
- params.set('query', `${query} in ${location}`)
586
- const res = await fetch(`https://maps.googleapis.com/maps/api/place/textsearch/json?${params}`, {
587
  headers: { 'Accept': 'application/json' },
588
  signal: AbortSignal.timeout(15000),
589
  })
@@ -591,32 +620,20 @@ async function googleSearchPlaces(
591
  if (!res.ok) throw new Error(`Google places search HTTP ${res.status}`)
592
  const data = await res.json() as any
593
  if (data.status !== 'OK') {
 
594
  throw new Error(`Google places search failed: ${data.status} — ${data.error_message || ''}`)
595
  }
596
 
597
- return parseGooglePlacesResults(data.results, resolved)
598
- }
599
-
600
- // Nearby search
601
- params.set('location', `${resolved.lat},${resolved.lng}`)
602
- params.set('radius', String(radius || 5000))
603
-
604
- const res = await fetch(`https://maps.googleapis.com/maps/api/place/nearbysearch/json?${params}`, {
605
- headers: { 'Accept': 'application/json' },
606
- signal: AbortSignal.timeout(15000),
607
- })
608
-
609
- if (!res.ok) throw new Error(`Google nearby search HTTP ${res.status}`)
610
- const data = await res.json() as any
611
- if (data.status !== 'OK') {
612
- throw new Error(`Google nearby search failed: ${data.status} — ${data.error_message || ''}`)
613
  }
614
 
615
- return parseGooglePlacesResults(data.results, resolved)
616
  }
617
 
618
  function parseGooglePlacesResults(results: any[], geo: LocationResult): PlaceResult[] {
619
- return (results || []).slice(0, 20).map((place: any) => ({
620
  name: place.name,
621
  address: place.formatted_address || place.vicinity || '',
622
  lat: place.geometry?.location?.lat || geo.lat,
@@ -626,7 +643,7 @@ function parseGooglePlacesResults(results: any[], geo: LocationResult): PlaceRes
626
  phone: place.formatted_phone_number,
627
  website: place.website,
628
  openingHours: place.opening_hours?.open_now ? 'Open now' : undefined,
629
- photos: (place.photos || []).slice(0, 3).map((p: any) =>
630
  `https://maps.googleapis.com/maps/api/place/photo?maxwidth=800&photoreference=${p.photo_reference}&key=${getGoogleKey()}`
631
  ),
632
  cost: place.price_level ? '💰'.repeat(place.price_level) : undefined,
 
413
  // Use pre-resolved coordinates if available, otherwise geocode
414
  const resolved = geo ?? await amapGeocode(location)
415
 
416
+ const allPois: any[] = []
417
+ const maxPages = 20 // 高德最多返回 10000 条 (offset 10000 × page 1 就夠了,分頁當備用)
418
+ const pageSize = 1000
 
 
 
 
 
 
 
419
 
420
+ for (let page = 1; page <= maxPages; page++) {
421
+ const params = new URLSearchParams({
422
+ key,
423
+ location: `${resolved.lng},${resolved.lat}`,
424
+ radius: String(radius || 5000),
425
+ offset: String(pageSize),
426
+ page: String(page),
427
+ extensions: 'all',
428
+ })
429
+ if (query) params.set('keywords', query)
430
+ if (type) params.set('types', type)
431
 
432
+ const res = await fetch(`https://restapi.amap.com/v3/place/around?${params}`, {
433
+ headers: { 'Accept': 'application/json' },
434
+ signal: AbortSignal.timeout(15000),
435
+ })
436
+
437
+ if (!res.ok) throw new Error(`Amap around search HTTP ${res.status}`)
438
+ const data = await res.json() as any
439
+
440
+ if (data.status !== '1') {
441
+ throw new Error(`Amap search failed: ${data.info}`)
442
+ }
443
 
444
+ const pois = data.pois || []
445
+ allPois.push(...pois)
446
+
447
+ // 如果這頁沒滿 pageSize,或已達總數,停止翻頁
448
+ if (pois.length < pageSize || allPois.length >= Number(data.count || 0)) break
449
+
450
+ // 高德限制每秒 3 次請求,稍微等一下
451
+ await new Promise(r => setTimeout(r, 350))
452
  }
453
 
454
+ return allPois.map((poi: any) => ({
455
  name: poi.name,
456
  address: poi.address || '',
457
  lat: parseFloat(poi.location?.split(',')[1] || '0'),
 
589
  ): Promise<PlaceResult[]> {
590
  const key = getGoogleKey()
591
  const resolved = geo ?? await googleGeocode(location, language)
592
+ const allPlaces: any[] = []
593
+ const baseUrl = query
594
+ ? 'https://maps.googleapis.com/maps/api/place/textsearch/json'
595
+ : 'https://maps.googleapis.com/maps/api/place/nearbysearch/json'
596
+
597
+ let nextPageToken: string | undefined
598
+
599
+ for (let page = 0; page < 3; page++) { // Google 最多 60 條(3 頁 × 20)
600
+ const params = new URLSearchParams({ key, language: language || 'zh-CN' })
601
+
602
+ if (page === 0) {
603
+ if (query) {
604
+ params.set('query', `${query} in ${location}`)
605
+ } else {
606
+ params.set('location', `${resolved.lat},${resolved.lng}`)
607
+ params.set('radius', String(radius || 5000))
608
+ }
609
+ } else if (nextPageToken) {
610
+ // 翻頁需要等 2 秒讓 token 生效
611
+ await new Promise(r => setTimeout(r, 2000))
612
+ params.set('pagetoken', nextPageToken)
613
+ }
614
 
615
+ const res = await fetch(`${baseUrl}?${params}`, {
 
 
 
 
 
 
 
 
616
  headers: { 'Accept': 'application/json' },
617
  signal: AbortSignal.timeout(15000),
618
  })
 
620
  if (!res.ok) throw new Error(`Google places search HTTP ${res.status}`)
621
  const data = await res.json() as any
622
  if (data.status !== 'OK') {
623
+ if (allPlaces.length > 0) break // 翻頁失敗但已有結果,繼續
624
  throw new Error(`Google places search failed: ${data.status} — ${data.error_message || ''}`)
625
  }
626
 
627
+ allPlaces.push(...(data.results || []))
628
+ nextPageToken = data.next_page_token
629
+ if (!nextPageToken) break
 
 
 
 
 
 
 
 
 
 
 
 
 
630
  }
631
 
632
+ return parseGooglePlacesResults(allPlaces, resolved)
633
  }
634
 
635
  function parseGooglePlacesResults(results: any[], geo: LocationResult): PlaceResult[] {
636
+ return (results || []).map((place: any) => ({
637
  name: place.name,
638
  address: place.formatted_address || place.vicinity || '',
639
  lat: place.geometry?.location?.lat || geo.lat,
 
643
  phone: place.formatted_phone_number,
644
  website: place.website,
645
  openingHours: place.opening_hours?.open_now ? 'Open now' : undefined,
646
+ photos: (place.photos || []).map((p: any) =>
647
  `https://maps.googleapis.com/maps/api/place/photo?maxwidth=800&photoreference=${p.photo_reference}&key=${getGoogleKey()}`
648
  ),
649
  cost: place.price_level ? '💰'.repeat(place.price_level) : undefined,