mcpField
G
gmaps

gmaps

github.com/monotykamary生产力454 次安装2 天前更新
代码仓库

关于 gmaps

gmaps 是通过 CDP 搜索 Google Maps 并获取路线的命令行技能。它支持多种出行方式,也可按直线距离估算固定起点的站点顺序,再查询实际路线。

主要功能

  1. 01搜索 Google Maps 本地商家
  2. 02获取多种出行方式的路线
  3. 03估算固定起点的站点顺序
  4. 04以文本或 JSON 返回结果
  5. 05通过浏览器标签页并行执行

适用场景

  1. 01查找地点附近的商家
  2. 02比较路线和出行方式
  3. 03规划多个地点的访问顺序

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/monotykamary/browser-harness-js --skill gmaps

安装后在 Claude Code 中输入/gmaps确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

scripts/
gmaps
setup
test

详细介绍

gmaps — 通过 CDP 使用 Google Maps(搜索、路线规划、尽力而为的 TSP)

通过 CDP 免费、免密钥地访问 Google Maps——直接从渲染后的页面获取数据,与付费的 Google Places 和 Directions API 所售的数据相同。无需 API 密钥、无需配额、无需 jq。有三种模式:

  • search(默认)——根据查询查找本地商家。
  • --route——为按指定顺序排列的地点提供真实驾车路线(总时间和距离,含当前路况)。
  • --optimize——尽力计算最快的访问顺序(开放路径 TSP,起点固定为第一个地点)。

每次调用都会单独打开一个后台标签页和 WebSocket 会话,可安全并行使用。

命令

搜索(默认)
gmaps "coffee shops in Austin TX"            # up to 10 results, pretty
gmaps "sushi near Times Square" 5            # 5 results
gmaps "pharmacy open now Berlin" 20          # 20 (scrolls the feed)
gmaps --json "coffee shops in Austin TX" 5   # raw JSON
参数含义
--json输出原始 JSON,而不是排版后的文本
<query>Google Maps 搜索内容,例如 coffee shops in Austin TX
[count]结果数量(默认 10,最多 30——会滚动信息流以加载更多结果)
路线规划(--route)
gmaps --route "Austin, TX" "Houston, TX"                       # real time + distance (driving)
gmaps --route --mode transit "London, UK" "Paris, France"     # by travel mode
gmaps --route "Austin, TX" "Houston, TX" "Dallas, TX"           # ordered multi-stop, total
gmaps --route --json --mode cycling "Austin, TX" "Houston, TX" # raw JSON

按给定顺序和出行 --mode(默认 driving)规划真实路线。读取总时间和距离(含当前路况)以及解析后的途经点名称。第一个地点为起点。最多支持 25 个地点。折叠的路线摘要中不含每段路程的时间(Maps 只显示总时间)——打开 url 可查看完整的逐向导航。

尽力计算顺序(--optimize)
gmaps --optimize "Austin, TX" "Houston, TX" "Dallas, TX" "San Antonio, TX"
gmaps --optimize --mode transit "Austin, TX" "Houston, TX" "Dallas, TX"
gmaps --optimize --json "Austin, TX" "Houston, TX" "Dallas, TX"

计算最快的访问顺序,作为起点固定(第一个地点)的开放路径 TSP。第一个地点是起点;其余地点按总直线距离最短的顺序排列,然后针对该顺序进行一次真实路线规划调用(使用所选的 --mode,默认 driving)——因此,尽管顺序是根据直线距离估算的,报告的时间和距离仍然是真实值。

  • 并行将每个地点解析为精确的经纬度(后台标签页轮询解析后地点 URL 中的 !3d!4d 坐标)——共 N 次查询,而非 N² 次。
  • 路段距离虚拟化为直线距离(haversine),并报告每段路程的罗盘方位。出行 --mode 只影响最后一次真实路线规划调用,不影响 TSP 顺序。
  • 最多 12 个地点时,使用 Held-Karp 算法精确求解 TSP。
  • 浏览器调用总数:N + 1(N 次地点解析 + 1 次路线渲染)。
出行模式(--mode)
--mode返回内容
driving (默认)时间 + 距离 + 途经点 + 过路费(常规 Maps 路线)
transit最佳公交车/火车/渡轮/地铁选项的时间(无距离/途经点)
walking时间 + 距离(时长以小时显示,例如 62 hr)
cycling时间 + 距离(可能包含渡轮/汽车运输说明)
flights仅直飞时间(尽力而为——航班卡片位于路线面板之外)
bestMaps“最佳”标签页选择的路线(大多数路线为驾车;更快时会选择公共交通/航班)
  • 不支持 motorcycle 或 ferry 模式。 Maps 顶层标签页只有上述六种;摩托车路线通过 driving 规划,渡轮则作为 driving/transit 路线中的路段出现。(在越南或美国,Maps 不会显示专门的两轮车标签页——只有部分地区/国家会显示。)
  • 页面默认以 best 模式加载;对于其他模式,gmaps 会点击对应的出行模式标签页。不可用的模式(例如岛屿路线的 cycling)会返回明确的错误。
  • 适用于 --route 和 --optimize。
并行使用

每次调用都会单独打开一个后台标签页,可安全并发运行:

gmaps "coffee Austin" 5 &  gmaps "coffee Seattle" 5 &  wait
gmaps --route "Austin, TX" "Houston, TX" &  gmaps --route "Dallas, TX" "San Antonio, TX" &  wait

返回结果格式

搜索 — 排版模式(默认)
coffee shops in Austin TX  ·  5 results

1. Jo's Coffee – South Congress   ★4.4 (1972)   $1–10   Coffee shop
   1300 S Congress Ave   ·   Open · Closes 7 PM
   30.2510458, -97.7493717   ·   ChIJ1S_Bov20RIYRe7MiR8
   https://www.google.com/maps/place/Jo%27s+Coffee...
搜索 — --json
{
  "query": "coffee shops in Austin TX",
  "count": 5,
  "results": [
    {
      "name": "Jo's Coffee – South Congress",
      "rating": 4.4,
      "review_count": 1972,
      "price": "$1–10",
      "category": "Coffee shop",
      "address": "1300 S Congress Ave",
      "hours": "Open · Closes 7 PM",
      "lat": 30.2510458,
      "lng": -97.7493717,
      "place_id": "ChIJ1S_Bov20RIYRe7MiR8",
      "url": "https://www.google.com/maps/place/Jo%27s+Coffee…"
    }
  ]
}

每条结果包含:name, rating, review_count, price, category, address, hours, lat, lng, place_id, url。缺失字段为 null(排版模式下为 -)——例如,没有价格等级或街道地址的地点。

路线 — 排版模式
Route (driving): Austin, TX  →  Houston, TX

  2 hr 31 min   165 miles   via State Hwy 71 E and I-10 E
  Fastest route, the usual traffic  ·  tolls

  1. Austin, Texas, USA
  2. Houston, Texas, USA

  https://www.google.com/maps/dir/Austin,+TX/Houston,+TX/

(driving) 表示 --mode(默认为该值)。对于非常短的路线以及 transit/flights(无距离),distance(和 via)可能为 null。只有路线包含收费路段时才会出现 tolls。

路线 — --json
{
  "mode": "route",
  "travel_mode": "driving",
  "places": ["Austin, TX", "Houston, TX"],
  "route": {
    "duration": "2 hr 31 min",
    "distance": "165 miles",
    "via": "via State Hwy 71 E and I-10 E",
    "label": "Fastest route, the usual traffic",
    "tolls": true,
    "waypoints": ["Austin, Texas, USA", "Houston, Texas, USA"],
    "url": "https://www.google.com/maps/dir/Austin,+TX/Houston,+TX/"
  }
}
优化 — pretty
Optimized route  ·  best-effort (straight-line TSP)  ·  driving  ·  fixed start: Austin, TX, USA

  1. Austin, TX, USA  (30.267153, -97.7430608)   start
  2. San Antonio, TX, USA  (29.4251905, -98.4945922)   ← 118.4 km SW (218°)
  3. Houston, TX, USA  (29.7600771, -95.3701108)   ← 304.4 km E (82°)
  4. Dallas, TX, USA  (32.7766642, -96.7969879)   ← 361.8 km N (338°)

  Straight-line total: 784.6 km

Real driving for this order:
  7 hr 50 min   516 miles   via I-35 S
  Fastest route now due to traffic conditions  ·  tolls
  Austin, TX, USA  →  San Antonio, TX, USA  →  Houston, TX, USA  →  Dallas, TX, USA

  https://www.google.com/maps/dir/Austin,+TX/San+Antonio,+TX/Houston,+TX/Dallas,+TX/

Note: order is a best-effort estimate from straight-line (haversine) distances;
the driving time above is the real route for that order.
优化 — --json
{
  "mode": "optimize",
  "travel_mode": "driving",
  "places": [{ "query": "Austin, TX", "name": "Austin, TX, USA", "lat": 30.267153, "lng": -97.7430608, "url": "…" }, …],
  "order": ["Austin, TX, USA", "San Antonio, TX, USA", "Houston, TX, USA", "Dallas, TX, USA"],
  "order_indices": [0, 3, 1, 2],
  "legs": [{ "from": "Austin, TX, USA", "to": "San Antonio, TX, USA", "km": 118.4, "bearing": 218, "dir": "SW" }, …],
  "straight_line_total_km": 784.6,
  "route": { "duration": "7 hr 50 min", "distance": "516 miles", "via": "via I-35 S", "label": "…", "tolls": true, "waypoints": […], "url": "…" },
  "note": "Order is a best-effort estimate from straight-line (haversine) distances; the driving time/distance is the real route for that order."
}

工作原理

搜索
步骤执行内容
1session.connect()(一次)——连接浏览器的共享 WebSocket
2Target.createTarget({ background: true }) + attachToTarget ——隔离的后台标签页 + 每次调用专属的 sessionId
3导航至 https://www.google.com/maps/search/<query>(URI 编码;空格用 + 表示)
4轮询 a[href*='/maps/place/'] 的数量,直到大于 0——这是真正的就绪信号。Chrome 不会为 Maps 触发 networkIdle(持续的 XHR 轮询会让 500ms 静默窗口始终无法开启),因此等待生命周期事件会超时。轮询信息流更快也更可靠。
5滚动信息流,加载最多 count 条结果。信息流最初会渲染约 7 张卡片;继续滚动至底部时会加载更多。达到 count、连续两次滚动后数量没有增长(结果已到底),或达到 25 次滚动上限时停止。
6使用一次 Runtime.evaluate 解析所有结果卡片并返回 JSON 数组。
7在 finally 中以即发即弃方式调用 closeTab——清理操作不会阻塞响应。
路线(--route / --optimize)
步骤执行内容
1导航至 https://www.google.com/maps/dir/<p0>/<p1>/…/(路径形式,空格用 + 表示)。?query= 形式不会渲染。页面会以 Maps 的“最佳”模式加载。
2**轮询路线面板,直到出现路线耗时。**与信息流一样,networkIdle 永远不会触发。路线列表中的耗时以 min/hr 结尾;出行方式标签页使用简写形式(2h 30m、16 hr),不含 min 标记,因此在 [aria-label="Directions"] 面板中轮询 /\d+\s*min\b/ 可等待真实路线,同时忽略出行方式标签页。也接受距离叶节点(miles/km),因此整小时路线(2 hr,不含 min)也能触发。地图比例尺中的 50 km 位于面板之外。
3选择出行 --mode(除非为 best)。出行方式标签页是包含 [aria-label] 图标的 button[role=radio],顺序固定为 [Best, Driving, Transit, Walking, Cycling, Flights](地图类型的 Default/Satellite 单选按钮没有图标 aria-label,因此会被排除)。标签页的 aria-label 会本地化(“Driving” → “Lái xe”),因此按索引而不是标签选择标签页;会等待标签页出现(它们可能晚于初始绘制),并检测不可用的标签页(DOM disabled 属性;备用判断方式:点击后没有路线 ⇒“不可用”)。实际点击后,面板会清空(耗时 → null),然后重新填充新模式的耗时,因此通过轮询 null → 非 null 等待重渲染,避免与先前模式的耗时发生竞争。
4使用一次 Runtime.evaluate 提取主要路线:第一个不在出行方式 BUTTON[role=radio] 内的完整格式耗时叶节点(其中存放各出行方式的简写最佳耗时);最近祖先节点中的距离叶节点;以及唯一的 via … / Fastest route … / This route has tolls. 叶节点(公交的备用路线,如 RedCoach/FlixBus,不包含这些内容,因此不会造成干扰)。解析出的途经点名称来自 input[aria-label](Starting point … / Destination …)。对于 flights,耗时从整个页面正文中读取(航班卡片位于 Directions 面板之外),距离/via/收费信息均为 null。
5在 finally 中以即发即弃方式调用 closeTab。
优化额外选项
步骤发生的情况
A在并行后台标签页中解析每个地点:地点名称搜索不会渲染信息流,而是解析为一个地点页面,其 URL 会更新(约 3-7 s 后)为 /maps/place/<name>/@<view>/data=…!8m2!3d<lat>!4d<lng>…。每个标签页都会轮询 location.href,查找 !3d!4d 标记(地点的精确坐标——@lat,lng 仅表示视口)。地点名称取自 /maps/place/<name>/ 路径段。
B构建 haversine 距离矩阵,并求解固定起点 = place[0] 的开放路径 TSP(精确 Held-Karp,最多 12 个地点)。
C计算每段的直线距离(km)和罗盘方位角。
D按选定顺序进行一次真实的 --route 调用(使用选定的 --mode,默认为 driving)→ 报告的时间/距离是真实数据;只有顺序是直线距离估算。

每次调用:搜索约 2-3 s(渲染信息流并滚动);路线约 2-3 s;优化约 5-9 s(并行解析 N 个地点 + 渲染 1 次路线)。后台标签页不会打扰使用(与 ytdl 不同,Maps 不需要播放或 poToken)。

为什么 Maps 需要真实浏览器

  • Google Places 和 Directions API 按量计费且受到限制——需要密钥、计费账户,并且每次调用都会产生成本。Maps 网页会免费渲染相同的数据;使用自己的浏览器读取数据不需要密钥,也没有配额限制。付费 Directions API 唯一能提供、而免费网页确实无法提供的功能是 optimizeWaypoints(针对 ≤9 个停靠点的 TSP)——--optimize 会用尽力而为的直线距离 TSP 取代它。
  • 真实浏览器会渲染信息流和路线面板。 Maps 是一个庞大的 SPA:结果会在滚动时流式载入 div[role=feed],而路线面板会以经过混淆且不断轮换的类名渲染路线时长。单次 Runtime.evaluate 即可通过一次 CDP 调用提取所有卡片/主要路线——无需获取 HTML、代理或应对 Cloudflare 拦截(页面由你自己的浏览器以你的身份渲染)。
  • 按查询使用,而非批量抓取。 这就是 Agent 调用的形式:一次查询(或一条最多包含 25 个地点的路线,或一个最多包含 12 个地点的 TSP),返回结构化 JSON 或文本。它不是批量抓取工具(批量抓取需要轮换代理并管理速率;而每次调用都使用真实浏览器的方式无法扩展到这种规模)。

陷阱

搜索
  • Maps 不会触发 networkIdle。 持续的 XHR 轮询会使网络一直处于繁忙状态,因此 gsearch/findata 使用的生命周期事件等待每次都会超时。gmaps 改为轮询 a[href*='/maps/place/'] 的数量——这是实际的就绪信号。
  • 不使用类名选择器。 Google 的类名经过混淆(.Nv2PK、.bfdHYd、.MW4etd……),并且会随版本更新而变化。解析器只依赖稳定信号:结果链接的 aria-label(名称)、卡片叶子文本的 DOM 顺序(评分/评论/价格/类别/地址/营业时间),以及 href(坐标对应 !3d<lat>!4d<lng>,地点 ID 对应 !19sChIJ… / !1s0x..:0x..)。如果 Maps 改变卡片文本顺序,地址/营业时间启发式规则是首先需要重新检查的部分。
  • ?query= URL 格式不会渲染结果列表(只会返回没有结果的重定向外壳),只有路径格式 /maps/search/<query> 会渲染。空格会转换为 +。
  • 地址是通过启发式规则推断的。 取第一个以街道号码开头的叶子(^\d{1,5}[-A-Za-z]*\s)。没有街道地址的地点(区域/地区结果或具名建筑)会返回 address: null,而不是猜测一个地址。
  • 营业时间是通过启发式规则推断的。 取 Open / Closed / Open 24 hours / Temporarily closed 叶子,以及存在时紧随其后的 · Closes/Opens … 叶子。如果 Open 24 hours 被渲染为两个叶子(Open + 24 hours),则只会读取为 Open。
  • 结果卡片中没有电话、网站和完整的每周营业时间——这些信息位于地点详情页,需要逐个导航到地点页面才能读取。目前尚未实现(可作为 --details 模式);目前可使用 url 打开地点。
  • 可能会出现赞助结果(Maps 有时会显示一个广告,它也会使用 /maps/place/ 链接)。这些结果不会被过滤——如果这很重要,请检查数据。
  • 同意提示/ Cookie 墙(在已预热的浏览器中很少见)会导致结果数量为零。如果返回空结果,请先在浏览器中打开一次搜索 URL 以清除提示。
路线
  • 折叠后的路线摘要中没有每段路程的耗时。 对于有多个有序停靠点的路线,Maps 只显示总耗时。每段耗时位于“详情”展开区域中(尚未实现);请使用 url 查看逐向导航。
  • 对于极短路线,distance 可能为 null(Maps 可能不会渲染距离叶子)。
  • 主要路线是“不在出行方式单选按钮中的第一个完整格式耗时”。 出行方式标签页(BUTTON[role=radio])会显示紧凑的最佳时间(2h 30m),必须排除;路线列表使用完整格式(2 hr 31 min)。公交替代路线(RedCoach/FlixBus)会出现在同一面板中,但没有 via / Fastest route / 距离叶子,因此不会干扰主要路线的提取。
  • 时间反映当前路况(“现在”)。 页面提供的是现在,而不是预测的未来出发时间(付费 API 的 departure_time / traffic_model 是页面无法提供的内容)。标签文本可能不同:“Fastest route, the usual traffic” 或 “Fastest route now due to traffic conditions”——两者都会匹配 ^Fastest route。
  • 地点名称搜索不会渲染结果列表——这就是为什么 --optimize 通过轮询地点页面 URL 中的 !3d!4d 来解析地点,而不是解析结果卡片。后台标签页更新 URL 需要约 3-7 s;resolvePlace 最多轮询 20 s。
出行方式(--mode)
  • best 是 Maps 的默认选项,但不一定是驾车。 对于公共交通或航班更快的路线(例如伦敦→巴黎,Best = 欧洲之星),best 会返回公共交通所需的时间。传入 --mode driving 可强制选择驾车。出于这个原因,此技能的默认选项是 driving(而不是 best)。
  • 方式标签页通过索引选择,而非 aria-label——标签会本地化(在 vi 中,"Driving" → "Lái xe")。顺序 [Best, Driving, Transit, Walking, Cycling, Flights] 在各语言区域中一直保持稳定,但如果 Maps 调整顺序或移除标签页,索引映射就会失效。
  • 通过两种方式检测不可用的出行方式——标签页 DOM 的 disabled 属性(其状态可能落后于渲染),以及备用检测:如果点击后没有路线(面板被清空但未重新填充),则报告该出行方式“此路线不可用”。
  • flights 尽力而为。 航班卡片渲染在路线规划面板之外,因此会在整个页面正文中读取直飞航班时间;没有距离、途经点或过路费信息。不会解析中转航班和预订详情。
  • 没有 motorcycle 或 ferry 出行方式。 Maps 仅提供以上六个标签页;摩托车路线使用 driving,渡轮则显示为 driving/transit 路线中的路段。(在越南或美国,Maps 不会显示两轮车标签页。)
  • 步行/骑行时长在路线列表中以小时显示,例如 62 hr,而不是方式标签页中显示的“天数”——两种格式都会匹配。
优化
  • 路线顺序是直线距离估算,而非真正的驾车最优顺序。 直线距离(haversine)是“虚拟化”的边成本;道路网络的绕行可能会改变真正的最优顺序。报告的驾车时间/距离是真实值(针对选定顺序进行一次路线规划调用)——只有顺序是尽力而为。
  • 起点固定,路径开放。 第一个地点是固定起点;路径会经过每个地点一次,并在任意地点结束(不返回起点)。这不是往返行程。
  • 地点数 ≤12 时保证精确。 Held-Karp 算法可得出精确结果,但复杂度呈指数增长;超过 12 个地点时,命令会报错,而不是猜测。N 个地点的解析会并行运行(N 个后台标签页),再加 1 次路线规划调用,共计 N+1 次浏览器调用。
常规说明
  • 不依赖 jq——解析、TSP 和美化输出均在页面内 / 使用 JS 完成。
  • 输入通过占位符替换传入 heredoc(node JSON.stringify + 函数替换),不会受地点名称中的 & / $ / \ 影响(例如 AT&T Stadium、Café du Monde)——搜索查询也使用相同机制。

评论

支持 Markdown