# Mushroom Forecast 内网 API 精简参考

仅下列 26 个动作。地址来自设置页列出的全部成功监听的局域网地址和 VPN 隧道上的 100.64.0.0/10 内网地址（如 Tailscale；例如 `http://192.168.1.20:8765` 或 `http://100.64.0.8:8765`），任选一个可达地址；或通过仅在局域网广播的 `_guiji-agent._tcp` 发现。其它 VPN 若使用同一地址段和匹配的隧道网卡名也会被开放，入口仍需 token。VPN 隧道不支持此 Bonjour 广播，请直连设置页地址；蜂窝 CGNAT 不提供入口。复制 / 二维码文本每行一个 URL、末行鉴权，全部地址共享 token；重置在所有地址上撤销旧 token，关闭会关闭全部监听。切网后重新打开开关以扫描地址。所有请求带 `Authorization: Bearer <token>`，JSON 请求带 `Content-Type: application/json`。局域网 token 不转发给情报服务；查询使用 APP 共享账号/设备凭据。保持 APP 前台，标记“地图”的动作另需地图页可见和引擎就绪。`page.open` / `page.close` 只要求前台。

## 端点、状态与错误

- `GET /api/v1/state`：HTTP 200 返回状态本身（无 ok 包装）。含 `camera`（实际相机或 null）、`focusPatchId`、`patchDetail`（实际展示的 patchId/date/species 或 null）、`filters`（forecastDate/minP/species/searchRadiusKm/forestFams）、`login`、`searchPin`、`location`（lng/lat/accuracy/at 或 null）、`page`。`page` 在设置工作区不可见时为 null；热区为 `{id,species,region,date,loading,failed,count}`，其它打开的页面为 `{id}`，用户推进的未命名页面为 `{id:"unknown"}`。读状态不启动 GPS。未登录时 `login.planId:null`。
- `POST /api/v1/command`：`{"action":"动作名","params":{}}`，params 必须是对象。
- 成功 HTTP 200：`{ok:true, action, result, settled, state}`，无动作数据时 result 为 null。相机/普通筛选观察 5 秒，focusPatch 独立观察 10 秒，`settled:false` 表示已提交但未确认，可一次短等后只读一次 state，不能自动重发。筛选 settled 只确认值应用；查询成功 settled 为 true，超时 504。客户端等候 30 秒（情报一般 20 秒、weekly 20 秒），不自动重试。
- 失败对象：`{ok:false,error,message,reason?}`，可含 `code/requiredPlan/currentLimit/requested` 等，全部保留。客户端在 stderr 打印 HTTP 状态与原响应正文，退出 1。
- 本地错误：401 unauthorized、400 invalid_params、403 forbidden（定位权限）、409 not_ready、422 unsupported、502 upstream_failed、504 timeout。not_ready 按 reason 指示动作，不循环写重试。
- 上游 4xx 保留原 HTTP 状态及业务字段（如会员 403、限流 429）；其他上游状态映射 502，优先保留原 error/message。401 也可能来自 APP 登录凭据，不一定是局域网 token。hotspots 的 404 与收藏的回退例外见下文。
- account/state 显示权益包含 maxRadiusKm、forecastDays、maxFollowedRegions、alertMode、topPatchLimit、marketWide；不能替代服务端授权。

`not_ready.reason`：`app_not_foreground` → 回 APP 前台；`map_not_visible` → 地图动作本会自动关闭当前林块详情、图层面板或隐藏设置页，走到这一步多为其它页面盖住地图，回地图处理；`map_loading` → 等地图加载；`detail_covered` → 先关详情上方页面；`detail_not_ready` → 等详情转场。相机读失败可以只是 camera:null，不代表地图加载中。

## 参数约定

`?` 表示可省略，不表示可显式传 null；只接受列出的键。数值有限，经度 [-180,180]、纬度默认 [-90,90]，WGS84；相机纬度例外见下文。字符串非空白（address 例外），日期为实际存在的 YYYY-MM-DD。最终物种 ID、日期、半径、数量和会员限制由服务端决定。不要臆造未列出的动作（如删除收藏）。

### `map.setCamera`

- 参数：`lng:number`、`lat:number`、`zoom:number`、`bearing?:number`、`pitch?:number`。
- 地图；lat [-85.0511287798,85.0511287798]，zoom [0,22]，bearing [-360,360]，pitch [0,85]；省略 bearing/pitch 保持当前值。中心与 zoom 不变、只改 bearing/pitch 时会像双指旋转一样平滑转过去（时长随转角，约 0.3–2 秒），其余情况仍直接跳切。result:null。实际相机匹配目标且停止移动才 settled（经纬度误差 ≤0.0001、zoom ≤0.05、pitch/bearing ≤0.1，bearing 环形比较）。
- 地图动作（相机类 setCamera/flyTo/set3D/search.select/focusPatch 及筛选类）执行前，若当前林块详情、图层面板或用户设置页正盖住地图，会自动关闭它再执行（设置页只是隐藏，停留位置保留），不要先发 map.closeDetail；被其它页面盖住或正在转场时自动关闭不适用，仍返回 map_not_visible。

### `map.flyTo`

- 参数：`lng:number`、`lat:number`、`zoom?:number`。
- 地图；相机范围/观察同 setCamera，省略 zoom 保持当前值，不接受 bearing/pitch。result:null。

### `map.set3D`

- 参数：`enabled:boolean`。
- 地图；result:null，不支持返回 unsupported；引擎 is3D 匹配且开启时 pitch>0、关闭时 |pitch|≤0.1 才 settled。

### `map.focusPatch`

- 参数：`patchId:string`、`lng:number`、`lat:number`、`date?:string`、`species?:string`。
- 地图；result:null，切换日期/物种、飞到林块并展示详情，在用户已授权范围内执行。省略 date 使用 filters.forecastDate，省略 species 为综合而非保持当前值；显式 null 不接受。完成证据是 state.patchDetail 实际展示对应 patchId/date/species，不是相机或 focus 方法返回。加载失败/取消也可能 settled:false，不说明详情服务成功；无本地套餐日期门禁。

### `map.closeDetail`

- 参数：`{}`。只在用户明确要求关闭当前林块详情时使用；不要拿它给相机、聚焦、筛选等其它动作铺路（相机类动作会自动关闭详情，其它弹窗由用户自己关）。
- 仅要求 APP 前台，不要求地图可见/引擎 ready。复用 Today 当前林块详情的关闭流程；只关闭已登记且在顶层的详情路由，等退场完成才返回 `result:{closed:true}, settled:true`，不退出聚焦或恢复筛选。
- 无详情是安全 no-op：`result:{closed:false}, settled:false`。关闭观察超时同样 false，不代表核心关闭被取消；可一次短等后只读确认。详情被设置/会员/其他路由覆盖时返回 409 `not_ready`、`reason:detail_covered`；详情尚未展示或正在转场为 `detail_not_ready`。不关闭根页面或任意其他页面。

### `species.list`

- 参数：`{}`。
- 只要求 APP 前台；不改筛选/相机，不要求地图可见或 ready。结果 `[{id:string,name:string,aliases:string[]},...]`，直接使用 UI 的 SpeciesListClient 清单：id 稳定、name 为 APP 当前语言、aliases 为同源别名。源错误按现有错误映射透传；不增加 resolve 动作。

### `forestTypes.list`

- 参数：`{}`。
- 只要求 APP 前台；不改筛选/相机，不要求地图可见或 ready。结果 `[{code:integer,name:string},...]`，按 UI 的 forestFamCodes 顺序与 forestFamName 当前语言命名（50/60/70/80/90）；不改变 forestTypes.set 接受整数数组的语义。

### `map.forecastDate.set`

- 参数：`date:string`。
- 地图；result:null，在用户已授权范围内执行。无本地套餐日期限制，settled 只说明控制器应用该日期，以 state.filters 为准，不说明该日数据已授权/加载。

### `map.minP.set`

- 参数：`minP:number`。
- 地图；[0,1]，result:null，在用户已授权范围内执行；settled 只确认筛选应用。

### `map.species.set`

- 参数：`id:string|null` 或 `name:string`（两者互斥）。
- 地图；`{"id":null}` 清除。共享物种清单按 ID、当前语言名称/别名匹配，未知物种 invalid_params。result:null，在用户已授权范围内执行；settled 只确认筛选应用，可读 state.filters.species 获取解析后的 ID。

### `map.searchRadiusKm.set`

- 参数：`km:number|null`。
- 地图；键必传，仅 30/100/300/500/null，null 清除。result:null，在用户已授权范围内执行；地图搜索范围不改变服务端半径权限，settled 只确认筛选应用。

### `forestTypes.set`

- 参数：`fams:integer[]`。
- 地图；可为空、去重，不按林型枚举限制，浮点元素不接受。result:null，在用户已授权范围内执行；settled 只确认筛选应用，state.filters.forestFams 顺序不保证。

### `favorites.list`

- 参数：`{}`。
- result:收藏数组；未登录或账号变更可能 []。先补传待同步队列；网络失败时可回退本机副本并合并待同步项，按 patch_id 去重。仅此动作增加兼容的顶层 `offline:boolean|null`：true 为缓存/待同步回退，false 为仓库网络列表（可能合并待同步项），null 为来源未知（未登录/账号变更等）。离线或未知的空数组不证明云端无收藏；false 也不承诺所有队列已同步。
- 收藏对象用 snake_case：patch_id/note/forest_version/status、lon/lat/anchor_lon/anchor_lat、props；可含 resolved_props、created_at/resolved_at（UTC ISO 8601）。注意响应是 lon，不是 lng。

### `favorites.add`

- 参数：`patchId:string`、`lng:number`、`lat:number`、`props:object`。
- 用户提过收藏才执行，没提就不做、不追问；props 为真实林块属性快照，可为空对象。result:`{favorite:收藏对象,queued:boolean}`。幂等 upsert，未登录 401（code:login_required）；服务端 4xx 不加入离线队列。离线/上游不可用时可写本机和队列，queued:true 不代表服务端保存。settled 仅业务入口完成，不等待地图标记渲染。

### `search.query`

- 参数：`query:string`、`nearLng?:number`、`nearLat?:number`。
- 两个 near 不强制成对。result:`[{name,address,lng,lat},...]`，可为空；不改地图。使用 APP 当前底图/市场，不接受 basemapProvider。

### `search.select`

- 参数：`lng:number`、`lat:number`、`name:string`、`address?:string`。
- 地图；address 可空串，省略为 ""。更新搜索标记并飞到 zoom 15，result:null；实际相机与标记匹配才 settled，否则 settled:false。

### `intel.topPatches`

- 参数：`species:string`、`date:string`、`lng:number`、`lat:number`、`radiusKm:number`、`count:number`、`publicOnly?:boolean`。
- 只读，species 必填有效 ID，radiusKm>0、count≥1 且整数值。result 为对象（不是数组）：run_date/stale/day/items/next_private_count，保留新增字段。请求映射 lng→lon、radiusKm→radius_km、count→limit、publicOnly→public_only。publicOnly 省略时不发送，保持服务端缺省 true，只返回公共土地/保护地林块（prot=true）；显式 true/false 原样透传，非 boolean 或显式 null 返回 400 invalid_params。权限仍由服务端判断，无本地门禁。
- next_private_count：只查公共土地时，若不限公共土地，前 count 名中非公共土地林块（prot=false）的数量，即被折叠的块数；publicOnly:false 时恒为 0。国内通常需要 publicOnly:false 才能取得候选。如果 items 为空但 next_private_count>0，向用户说明这些是非公共土地林块，征得同意后用相同条件加 publicOnly:false 再查；提醒采集前核实土地权属和准入，返回候选不代表允许进入或采集。
- items[]：id、lon、lat、area_ha（公顷）、years、elev_m（米或 null）、distance_km（或 null）、idx、top_species（或 null）、peak_day、peak_idx。不承诺名称，海拔 null 是未知不是 0。

### `intel.patches`

- 参数：`patchIds:string[]`、`lng?:number`、`lat?:number`。
- 只读，数组可空，ID 非空白，坐标必须成对。省略坐标不自动用 state.location，服务端可能返回 intel_location_required。退休 ID 会被过滤，非空输入全部被过滤时 404 code:patch_not_found。
- result 对象：days（日期索引）、items（按 ID 索引）、locked_items 等。items 每块可含逐日 p/top/species（id/name/name_lat/idx）、confidence、no_forecast、weather（t_mean_c/t_min_c/rh_pct/precip_mm/cloud_pct），数组与 days 对齐，值可为 null，天数不固定。
- run_date/stale/source 为版本信息，errors 为异常 ID，unavailable_cells 为不可用格。locked_items 是 ID→逐日显示名或 null，无完整数值，不能把显示名转换为已知指数。保留新增数据；behavior_request_id 仅保留与本次请求匹配的回显，否则 null。

### `intel.hotspots`

- 参数：`date:string`、`species?:string`、`lat?:number`、`lon?:number`。
- 只读，可选概览；经度键是 lon，两坐标都提供才发送附近原点，单边忽略。省略 species 查综合；不接受 followed/state/limit（默认 followed=false、limit=20）。
- result 对象含 run_date（可 null）、stale、items；条目可含 state/block_lon/block_lat/place_name/place_lat/place_lon/species_id/top_species_id/peak_idx/peak_day/hot_cells/focus，附加字段保留。沿用客户端 404→HTTP 200 result:`{run_date:null,stale:false,items:[]}`，其他错误正常映射；behavior_request_id 只保留匹配的回显。本动作不打开页面；打开出菇热区用 `page.open`。

### `intel.weekly`

- 参数：`lat?:number`、`lng?:number`。
- 只读，可选概览；坐标成对，省略不自动用 state.location。HTTP 动作用 lng，转到上游为 lon。result 原样保留：week/items/needStates/upsell? 等；items 按州含 state/stateId/enteredAreas/bestAreas，后者含 place/date/days，days 含 date/focus（可 null 或锁定）。不要把权益锁定 focus 当完整数据。沿用 20 秒查询超时；behavior_request_id 仅保留匹配的响应头回显，否则 null。

### `intel.status`

- 参数：`{}`。
- 只读，可选版本概览；result 对象含 run_date（可 null）、stale、source（通常 table/live）、market/rows/unavailable（可 null）等，保留新增字段；live 时版本/行数可 null。无 hotspots 的 404 空结果特例。behavior_request_id 沿用共享客户端响应头回显行为。

### `location.get`

- 参数：`{}`。
- result:`{lng,lat,accuracy,at}`，accuracy 米、at UTC ISO 8601。无权限 forbidden reason:location_permission_denied，不主动弹权限申请；已有共享位置直接用（未必新采样），否则等待定位，无 fix 为 504，取到后再查权限。

### `account.status`

- 参数：`{}`。
- result:`{loggedIn,planId,planLabel,intelEntitlements}`。现有 APP 显示状态，不保证网络刷新；未登录时 result.planId 仍是默认套餐 ID（通常 free），而 state.login.planId 为 null。最终数据访问权限仍看服务端。

### `nav.toPatch`

- 参数：`lng:number`、`lat:number`、`app?:string`。
- 用户提过导航才执行（会离开 APP 打开外部导航），没提就不做、不追问；同目标已有授权直接复用。app 仅 amap/appleMaps/googleMaps/amapWeb（精确大小写）；省略打开选择器并返回 initiated:true，不等选择/取消。result:`{initiated:boolean}`，指定 app 启动失败为 false。settled:true 不代表已抵达或用户选择/取消；切后台后 command.state.camera/location 可 null。

### `page.open`

- 参数：`page:string`、`species?:string`、`region?:string`、`date?:string`。
- 只要求前台。page 为 settings、intelCenter、weekly、hotspots、speciesSubscriptions、notifications、tracks、spots、favorites、friends、outings、offlineMaps。协议、语言、单位、账号、引导回放不可开。species/region/date 只有 hotspots 能带，省略即「全部」；其它页面带这些键为 400。显式 null 不接受。
- species 是 species.list 的 id。region 是当前区域目录里的州或省（美国 US-XX，中国两位省码）；该市场没有地区筛选时传 region 为 400。date 是峰值日 YYYY-MM-DD，范围今天起、套餐天数内，对应页面 peakDate；请求数据日期仍是今天。未知物种、未知地区、超出窗口的日期在打开前 400，页面不动。
- 先显示设置工作区，弹回设置首页，再打开目标页。settings 停在首页。热区已打开时也是先回首页再开一页，等这次搜索结束再返回（超时与情报请求相同）。未登录打开地点库、收藏、关注蘑菇、好友、组队返回 401 reason:login_required（APP 登录，不是局域网 token），不弹登录框。轨迹、地点库、离线地图功能关闭时 422 unsupported。
- 非热区 result:`{page}`。热区 result:`{page,species,region,date,stale,failed,count,items}`。失败时页面保持失败态，failed 为 true，不自动重开。

### `page.close`

- 参数：`{}`。
- 隐藏设置工作区回到地图。result:`{closed:true}`。之后 state.page 为 null。
