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
|
| 417 |
-
|
| 418 |
-
|
| 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 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 431 |
|
| 432 |
-
|
| 433 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 434 |
|
| 435 |
-
|
| 436 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
}
|
| 438 |
|
| 439 |
-
return
|
| 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 |
-
|
| 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 |
-
|
| 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(
|
| 616 |
}
|
| 617 |
|
| 618 |
function parseGooglePlacesResults(results: any[], geo: LocationResult): PlaceResult[] {
|
| 619 |
-
return (results || []).
|
| 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 || []).
|
| 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,
|