---
name: mushroom-forecast
description: "通过局域网或 VPN 隧道内网接口（如 Tailscale）操作用户手机上的蘑菇地图 APP（Mushroom Forecast），包括查看状态、地图定位与筛选、查询情报、收藏、导航，以及打开设置中的页面（出菇热区可带物种、地区和日期）；在用户要求规划采蘑菇时间和地点、比较候选林块或出行窗口时使用。"
---

# 蘑菇地图 APP 控制与采蘑菇规划

只操作用户授权的手机。使用本技能目录内的 `scripts/guiji_app.py`；路径均相对本技能目录，与工作仓库无关。只需 Python 3 标准库。动作参数、响应和例外按需读 [references/api.md](references/api.md)，不要调用未列出的动作。

## 连接

1. 手机和电脑处于同一可互访的局域网或可互访的 VPN（如 Tailscale）。用户打开 APP 设置页“允许 Agent 控制 App”，复制地址和 token，或扫描该页二维码。设置页列出全部成功监听的局域网地址和 VPN 隧道上的 100.64.0.0/10 内网地址（如 Tailscale），标签分别为“局域网”和“VPN / Tailscale”。其它 VPN 若使用同一地址段和匹配的隧道网卡名也会被开放，入口仍需 token。配对文本每行一个 URL，末行 `Authorization: Bearer <token>`，所有地址共用同一 token，不是带 token 的 URL。蜂窝网卡上的运营商 CGNAT 不提供入口。
2. 让用户在运行 Agent/客户端的环境中设置 `GUIJI_APP_URL` 为其中一个可达的纯 HTTP 地址（如 `http://192.168.1.20:8765` 或 `http://100.64.0.8:8765`，不要把全部 URL 一起填入），`GUIJI_APP_TOKEN` 为原始 token（不含 `Bearer `）。token 只能通过环境变量进入客户端请求头；不要写入技能、配置、脚本、文件或日志，不要回显、放进 URL、命令参数或上传服务端。安全输入方式见 [README.md](README.md)。
3. 地址未知时先运行 `python3 scripts/guiji_app.py discover`，通过 Bonjour 服务 `_guiji-agent._tcp` 在局域网发现；不通过 VPN 隧道广播，VPN 调用方使用设置页地址直连。macOS 浏览结果后用 `discover --name "实际服务名"` 解析主机与端口；Linux 使用 avahi-browse 的解析结果。发现只运行一次、最多 5 秒，不提供 token。多台手机时请用户核对目标。没有 dns-sd/avahi-browse、无结果或广播残留时请用户手动给设置页地址，不扫描网络猜设备。
4. APP 保持前台；相机/筛选/聚焦动作还需打开地图页并等待引擎就绪；closeDetail、清单查询、page.open 和 page.close 只要求前台。可读 `state` 做连通测试。关闭开关、重置 token 或 APP 重启后需重新配对（旧 token 在所有地址上同时失效）；切换 Wi-Fi 或 VPN 断开重连后关闭再打开开关以重新扫描地址。请求不会通过环境代理或跟随重定向。

```bash
# 在本技能目录执行；真实地址和 token 已由用户设置在环境里
python3 scripts/guiji_app.py state
python3 scripts/guiji_app.py command account.status
python3 scripts/guiji_app.py command intel.patches --params '{"patchIds":[],"lng":117.2,"lat":40.2}'
```

通过 token 鉴权的 state 或 command 请求会让手机在 APP 前台期间常亮，地图右侧第一个用户按钮变蓝并提示“Agent 已连接”。只有局域网 token 缺失、错误或已撤销导致的 401 不计入连接；token 通过后，即使命令返回 400（参数错误、未知动作）或上游服务返回 401/403，仍算 Agent 已连接（连接表示 Agent 在通信，不表示命令成功）。120 秒无鉴权请求、关闭开关、重置 token 或服务销毁后恢复原按钮与系统默认息屏，退后台也会释放常亮。

示例坐标必须替换为真实出发点（优先 `location.get`，其次用户说出的地点），不要拿示例值查询。一次命令只发一次请求，无缓存、守护进程或自动重试。

## 与人沟通（先做事，少打扰）

默认像熟人帮忙：先自己推断、自己查、直接给出结果并展示到地图上，不把选择题推给用户。内部动作名、参数、原始字段和执行过程不讲给用户听，只讲结论和用户下一步要做的事。

**能推断的不要问**（用户已授权在本任务内按常识补全）：

- **日期**：没说就按今天；“周末/最近几天”按用户时区换算成具体日期，并在预报窗口内顺带看哪天更好，今天不是好日子时一句话点明更好的日子。
- **地点/出发点**：没说就用 `location.get` 的当前位置作出发点；说了城市/区域就用该区域作中心，不要求用户再报坐标。
- **范围**：没说就默认 100 km；结果太少或太远时可自行在 30–150 km 内调整并在结论里一句话交代，不逐档询问。
- **物种**：没指定时不要让用户从清单里选。先 `species.list` 拿有效 ID，再用 `intel.topPatches` 顺序试本地当季常见物种（一般 3–4 个够了），按今天指数、距离和面积替用户选定一个；编造“综合”ID 或把中文名当 ID 仍禁止。用户指定了物种则唯一匹配直接用。
- **展示**：用户说“查一下/哪里适合/帮我看看”即视为同意在地图上展示，直接 `map.focusPatch` 跳到最佳林块，不再问“要不要展示”。

**只有必要时才问一句**（问完就停，不要连环问）：连不上/未配对需用户操作手机；定位拿不到且用户也没给地点；同名地点有多个且选错会跑偏；要扩大到非公共土地（`publicOnly:false`）；要打开外部导航离开 APP 或写入收藏，而用户没提过。用户说“你定/只要结果”时，上述非安全类选择一律自己定。

回话简短口语：一句话给结论（去哪、哪天、找什么），一句话给最关键的理由即可。不要复述套餐、定位精度、数据版本、字段名等技术细节，除非用户追问或数据不可靠到影响结论。不预告“我接下来要调用…”，做完再说结果。

**安全提醒只说一次**：同一会话里，第一次给出具体采点时用一句话提醒（土地能不能进自己核实、蘑菇不能凭指数判定能不能吃，合并成一句）。之后同一会话的追问、换地点、切林块、放大地图等都不再重复这句，哪怕换了城市或物种。只有用户直接问“能不能吃/这是什么菇/帮我鉴定”时，才再简短提醒一次不能凭此鉴定、不可凭此食用。

## 规划流程（顺序执行）

每次等上一动作响应并核对结果后再做下一步，不并发写地图。用户已明确的意图直接复用；未明确的按上节默认补全，不停下来等确认。

1. **先 `account.status`**，了解 APP 登录、套餐及显示权益。它不一定经过网络刷新，最终权限以服务端响应为准；未登录不代表必然不能查询情报。对用户不必汇报套餐细节，除非权限挡住了查询。
2. 按“与人沟通”的默认规则定好日期、中心、半径和物种，直接查候选，不先发问卷。只有触发上节“只有必要时才问”的情形才暂停询问。
3. `location.get` 获取出发点，检查 `accuracy` 与 `at`（可能是已有采样，不保证新定位）；用户说了其他目的地区域时以该区域作查询中心，不擅自改口。无定位或权限拒绝且用户也没给任何地点时，才简短请用户给一个地点；必要时 `search.query` 搜索，同名多结果且影响结论才让用户选。传入手动中心不代表可以绕过服务端定位或半径限制。
4. `intel.topPatches` 按定好的日期、中心、半径、数量、物种找候选。先调用 `species.list` 用当前语言 `name`、`aliases` 与稳定 `id` 匹配；该清单不改筛选，也不要求地图就绪。需要林型编码时用 `forestTypes.list`，不猜字典。有效日期、半径、数量及权限由服务端检查。可选 `publicOnly` 仅接受 boolean；省略时不发送，服务端缺省 true，只返回公共土地/保护地林块（`prot=true`）。`next_private_count` 表示不限公共土地时前 count 名中被折叠的非公共土地林块（`prot=false`）数量，`publicOnly:false` 时为 0。如果 `items` 为空但 `next_private_count > 0`，这是“必要时才问”的一句：简短说明候选都在非公共土地上、需用户同意才查，用户同意后用相同条件加 `publicOnly:false` 再查；已有候选时不为凑数扩大范围。采集前核实土地权属和准入是内部底线，对用户只按“安全提醒只说一次”的规则合并交代，不每次重复。
5. `intel.patches` 传候选 ID 与成对的中心 `lng/lat`，比较未来几天指数。用返回 `days` 对齐每日数组，不硬编码天数；结合目标物种指数、`confidence`、`stale`、天气、`no_forecast`、空值及不可用项判断。`locked_items` 只有锁定项公开显示名/空值，没有完整数值；不得补算或假装已知。若没有可用数据，直说无法可靠择时，不捏造推荐。对用户只提炼成“今天能去/某天更好”的人话，不搬运逐日数组。
6. 选定最佳林块后直接用 `map.focusPatch` 展示（会切换日期/物种，这包含在“查一下”的授权里），明确传 date/species；省略 species 是综合，不是保持筛选。中心点用 topPatches 返回的 `lon/lat`，请求键用 `lng/lat`。聚焦下一林块时上一块详情会被自动关掉，不用先发 `map.closeDetail`，但默认只跳最终推荐的那一块。检查 `state.patchDetail` 是否实际显示对应内容；`focusPatch` 最多观察 10 秒，`settled:false` 可短暂等候一次（例如 1 秒）后只读一次 `state`，不重发写动作、不声称已完成。
7. 外部导航和收藏放在最后，用户提了才做：提过就直接执行，没提就不问、不做，在结论里留一句“需要导航或收藏再说一声”即可（也可省略）。`nav.toPatch` 返回 `initiated:true` 仅表示发起（省略 app 时是打开选择器），不是已选路线、已抵达或保证启动。`favorites.add` 只保存真实林块属性快照（可为 `{}`）。

`intel.weekly`、`intel.hotspots`（及 `intel.status`）只作用户需要时的可选概览，不打开页面，也不替代 topPatches → patches → focusPatch 的规划主链路。用户要在手机上看到热区或本周情报页时，用下一节的 `page.open`。

## 打开用户页面

用户要在手机上看到某个设置页时才发 `page.open`。看完要离开该页、回到地图，且下一步不是地图动作时，才发 `page.close`。查数仍用 `intel.hotspots`、`intel.weekly`，这两个动作不打开页面。参数和结果字段以 [references/api.md](references/api.md) 为准。

页面对应：设置首页 `settings`，情报中心 `intelCenter`，本周情报 `weekly`，出菇热区 `hotspots`，关注的蘑菇 `speciesSubscriptions`，提醒 `notifications`，轨迹 `tracks`，地点库 `spots`，收藏林块 `favorites`，好友 `friends`，组队 `outings`，离线地图 `offlineMaps`。协议、语言、单位、账号、引导回放没有打开动作；用户要看这些，就说只能自己在设置里点。

出菇热区是唯一能带筛选的页面。用户指定了才传 `species`、`region`、`date`，没指定的键不要传，也不要传 null。省略就是页面上的「全部」。一次把能确定的筛选传进去，不要先打开再改。

- `species`：先 `species.list`，用稳定 `id`。不要用中文名，也不要编造「综合」。
- `region`：美国是 `US-XX`（两位大写），中国是两位省码，并且必须是当前市场有的地区。这个市场没有地区筛选时不要传。拿不准就省略，不要猜编码再连试。
- `date`：峰值日，`YYYY-MM-DD`，从今天起、不超过 `account.status` 里 `intelEntitlements.forecastDays` 天（含今天）。这不是查询用的数据日期。用户没指定某一天就省略，看的是整段窗口，不要擅自填今天。

热区命令会等到这次搜索结束再返回。用返回的 `items` 向用户讲结果。`failed` 为 true 时页面已经是失败态，不要自动重开。`stale` 为 true 时用半句说明数据旧了。打开前校验失败是 400，页面停在原地，向用户说明哪个条件不对，不要改着参数连发。打开后看 `state.page`：热区的 `id` 应为 `hotspots`，筛选与刚才传入的一致；设置工作区不在时 `page` 为 null。

地点库、收藏、关注的蘑菇、好友、组队需要 APP 已登录。401 且 `reason` 为 `login_required` 时，请用户在 APP 里登录。这不是局域网 token 失效，不要让用户重新配对，也不要重试。轨迹、地点库、离线地图返回 422 `unsupported`，就是这个功能关着，告诉用户即可。

地图动作会自动隐藏设置页。接下来要聚焦林块或移动地图时，不要先发 `page.close`。

## 执行规则与错误

- **不做关弹窗的铺路操作**：不要为了让某个地图动作能执行而先发 `map.closeDetail` 或其它关闭页面/弹窗的操作。地图动作（相机与筛选）由 APP 自动关闭当前林块详情或图层面板、自动隐藏用户设置页回到地图（设置停留位置保留）；遇到其它弹窗挡住地图时，停下来请用户自己关掉（只说一句），不要替用户关。`map.closeDetail` 只在用户明确说“关掉详情/关掉这个林块”时才用。
- “查一下/哪里适合”等说法即包含展示授权，`map.focusPatch` 的日期/物种变化不用再确认；收藏和外部导航是额外的事，用户没提就不做。同范围授权持续有效，不要求重复确认；单纯查询不偷偷收藏。
- 权限和会员以服务端返回为准。遇到 `forbidden` 如实报告 HTTP 状态、`error`、`message`、`code`、`reason` 及附加字段；不改凭据、拆分请求或换中心绕过限制。显示权益不能取代授权。`locked_items` 不能说成已知指数或完整详情。
- `not_ready` 按 `reason` 处理：`app_not_foreground` 提示回 APP；`map_not_visible` 时相机类动作（移动/旋转/缩放/3D/聚焦）本会自动关闭当前林块详情、图层面板或隐藏设置页，走到这一步多是其它页面盖住地图，请用户回地图；`map_loading` 等地图加载；`detail_covered` 请用户先关详情上方的页面；`detail_not_ready` 等详情转场。不给写动作做循环重试，也不把 `camera:null` 擅自判为地图未加载。
- 401：`reason` 为 `login_required` 时是 APP 未登录，请用户在手机里登录，不要重新配对，也不要重试。其它 401 可能是局域网 token 失效或上游凭据问题，结合原 `code/message` 说明，不无条件判断。
- 超时/断连后写入可能已经执行（包括 token 重置/关闭期间）；不要自动重发。相机/筛选/聚焦的 `settled:false` 是已提交未确认，不是失败，可一次短等后只读确认；`closeDetail` 无详情时为 `closed:false, settled:false`，是安全 no-op。筛选的 `settled:true` 仅说明值应用，不代表渲染或情报加载已完成。
- `favorites.add` 的 `queued:true` 表示本机待同步，不说服务端已保存；`favorites.list` 沿用打开收藏页的补传与缓存回退；顶层 `offline:true` 表示缓存/待同步视图，`false` 表示仓库取得网络列表（仍可能合并待同步项），`null` 表示来源未知（如未登录或账号变更）。离线或未知的空列表不证明云端无收藏，`false` 也不承诺队列已全部同步。
- 地图和指数仅作出行参考。不能据此鉴定种类、食用性或保证有蘑菇；不要把高指数等同于安全。

## 每次规划输出格式（简短版）

默认 2–4 行说完，像当面说话，不写报告。无数据/失败时一句说清原因和用户要做的一件事。不承诺林块有名称，可用 ID，但 ID 只在用户可能要复查时才给。

- **结论一句**：哪天去、去哪块（大致方位/距离即可，如“城北 11 公里那块”）、找什么菇；已跳地图的直接说“地图上已经打开了”。
- **理由一句**：只挑最有说服力的一点（如今天指数最高、离得近、面积大），顺带一句天气或“某天会更好”。指数可提数字，但不解释字段和数据版本。
- **安全提醒（仅首次）**：同一会话第一次给具体采点时才加一句，土地能不能进自己核实、蘑菇不能凭这个判断能不能吃，两件事合并说完；之后不再重复（用户问能不能吃/鉴定时例外，见上）。数据过期/可信度低/锁定数据这类影响结论的问题，不算安全提醒，该说明时仍用半句交代结论打了折扣。

仅当用户追问细节时，才展开坐标、面积、海拔、逐日指数、可信度和版本等完整信息（海拔 `null` 写“海拔未知”，不能当作 0；距离是中心直线距离，不是路线距离）。

另一台电脑实际连接手机、Bonjour/二维码及完整规划链路的真机端到端验收：**待用户测试**；mock 测试不能替代。
