C

Cloudflare Weather DingTalk Bot

作者:董保全v4

Deploy a Cloudflare Workers weather bot that pushes daily weather + cycling safety reminders to DingTalk groups, with an optional Amap (Gaode) secondary-source cross-check that suppresses single-run false alarms (e.g. hail). Use when the user wants to create a weather push bot, DingTalk weather robot, Cloudflare Workers scheduled weather notifications, daily cycling safety alerts for delivery riders, or to add/verify/tune the Amap cross-check (二源复核) on the existing bot.

下载量
414
点赞
99
价格
免费

技能文档

---
name: cloudflare-weather-dingtalk-bot
title: Cloudflare Weather DingTalk Bot
description: Deploy a Cloudflare Workers weather bot that pushes daily weather + cycling safety reminders to DingTalk groups, with an optional Amap (Gaode) secondary-source cross-check that suppresses single-run false alarms (e.g. hail). Use when the user wants to create a weather push bot, DingTalk weather robot, Cloudflare Workers scheduled weather notifications, daily cycling safety alerts for delivery riders, or to add/verify/tune the Amap cross-check (二源复核) on the existing bot.
version: 1.4.1
category: 工具
---

# Cloudflare Weather DingTalk Bot

A serverless weather bot running on Cloudflare Workers with Cron triggers. Pushes daily weather + cycling safety advice to DingTalk groups via Webhook. Designed for field teams (e.g. BD) who commute by electric scooter daily.

## Prerequisites

- Node.js installed (on THIS machine node/python are NOT on PATH — use the full path `/c/Users/TBSG/AppData/Local/openclaw-bundle/node/node.exe`, v22; see Environment pitfalls)
- Wrangler CLI installed (`npm install -g wrangler`)
- Cloudflare account (free tier sufficient)
- DingTalk group robot with Webhook URL and signing secret
- (Optional) An Amap Open Platform **Web service key** (高德开放平台 Web服务 key) — only needed to enable the secondary-source cross-check (二源复核). Stored as a Wrangler secret `AMAP_KEY`, never in plaintext `[vars]`. Without it the bot runs Open-Meteo-only and fail-open.

## Project Structure

```
weather-bot/
├── src/index.js       # Worker entry point (Open-Meteo + optional Amap cross-check)
├── wrangler.toml      # Config + Cron + env vars
├── package.json       # Project metadata
├── local-trigger.mjs  # Local resend (imports built worker, calls fetch in Node v22)
├── render-test.mjs    # Format stub: intercepts oapi.dingtalk.com, renders without sending
└── test-xcheck.mjs    # Cross-check stub test: 6 scenarios / 17 assertions for crossCheckCode
```

## Setup Steps

### 1. Login to Cloudflare

> **All `wrangler` commands need a PATH prefix on this machine** — `npx`/`node` are NOT on PATH (see Environment pitfalls). Prepend the bundled node dir before any wrangler call (`login`, `deploy`, `secret put`, `deployments list`, `tail`):
>
> ```bash
> export PATH="/c/Users/TBSG/AppData/Local/openclaw-bundle/node:$PATH"
> ```

```bash
export PATH="/c/Users/TBSG/AppData/Local/openclaw-bundle/node:$PATH" && npx -y wrangler login
```

The browser will open for OAuth authorization. Click **Allow**.

If `wrangler deploy` later fails with "register a workers.dev subdomain", open the Cloudflare Dashboard Workers & Pages page once to auto-create the subdomain.

### 2. Create Project Files

**wrangler.toml:**

```toml
name = "weather-bot"
main = "src/index.js"
compatibility_date = "2026-05-19"

[triggers]
crons = ["0 23 * * *"]  # UTC 23:00 = Beijing 07:00

[vars]
DINGTALK_WEBHOOK = "https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN"
DINGTALK_SECRET = "SECyour_secret_here"
TRIGGER_TOKEN = "a-strong-random-token"  # guards the /trigger manual resend endpoint
```

**package.json:**

```json
{
  "name": "weather-bot",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "deploy": "wrangler deploy"
  }
}
```

### 3. Worker Code (src/index.js)

Use the template from the current project. Core features:

- **Weather source**: Open-Meteo API (free, no API key)
- **Cities**: configurable `CITIES` array with name + lat/lng
- **Weather map**: WMO code to Chinese label + emoji
- **Data validation**: shower/thunderstorm codes with precipitation < 2mm are downgraded to "cloudy" to avoid Open-Meteo false alarms
- **Cycling advice**: safety reminders by weather type, not ride/don't-ride binary
  - Only "don't ride" when ALL cities have real thunderstorms (code 95-99 + precip >= 2mm)
  - All other weather shows "cycling safety reminder" with specific tips
- **Three-part reminder format**: [Must-do] -> [Weather] -> [Tips by type]
- **Clothing advice**: based on max temperature
- **UV advice**: based on daily max UV index
- **DingTalk signing**: HMAC-SHA256 with timestamp

### 4. Deploy

```bash
# node/npx are NOT on PATH — prefix it, cd into the project, then deploy
export PATH="/c/Users/TBSG/AppData/Local/openclaw-bundle/node:$PATH" \
  && cd "<...>/outputs/weather-bot" \
  && npx -y wrangler deploy
```

Deploying without an `AMAP_KEY` secret is safe: the cross-check stays dormant (fail-open), so production behavior is identical to the pre-cross-check version and the morning push is unaffected.

Verify in Cloudflare Dashboard -> Workers & Pages that the Cron schedule is active. On this machine the `*.workers.dev` URL is unreachable (network block), so verify the version programmatically instead — see the `deployments list` caveat under "Local resend without wrangler dev".

## Reliability (Retry + Manual Resend + Monitoring)

After a missed 07:00 push (2026-08-14, transient Worker execution failure), three safety layers were added. Keep all three when modifying the bot:

### 1. Built-in retry (`runWithRetry`)

`scheduled()` calls `runWithRetry(env)` via `ctx.waitUntil`. It runs `handleRequest` once; if ALL groups fail (errors present and zero successes), it waits 3 minutes and retries once:

```javascript
async function runWithRetry(env) {
  let results = await handleRequest(env);
  const failed = results.errors.length > 0 && results.success.length === 0;
  if (failed) {
    await new Promise((r) => setTimeout(r, 18e4));
    results = await handleRequest(env);
  }
  return results;
}
```

### 2. Token-guarded manual resend endpoint (`/trigger`)

The `fetch` handler accepts `GET /trigger?token=<TRIGGER_TOKEN>` and runs `handleRequest` synchronously, returning JSON `{ triggered, success, errors }`. Wrong or missing token → 403. Any other path returns a static status JSON. Never expose the token in reports or logs.

### 3. Daily monitoring cron (QoderWork scheduled task)

A QoderWork cron task runs daily at 07:30 (Asia/Shanghai): search today's DingTalk messages for the push (e.g. `dws chat message search --query "早安天气"`); if today's message is missing, resend via `node local-trigger.mjs` from the weather-bot folder, re-verify arrival, and append a status line to `check-log.md`. The task must not modify code/config or redeploy.

### Local resend without wrangler dev (workerd blocked)

On this Windows machine `workerd.exe` (used by `wrangler dev`) is blocked by system policy (`spawn UNKNOWN`, errno -4094; Unblock-File does not fix). Workaround: `local-trigger.mjs` parses `[vars]` from `wrangler.toml`, imports the built `src/index.js` directly in Node v22 (which natively provides fetch/crypto.subtle/btoa/Request/Response), and calls `worker.fetch(new Request('http://localhost/trigger?token=...'), vars, { waitUntil(){} })`. Same code path as production. Verify with a wrong token first (expect 403) before the real resend.

Note: the deployed `*.workers.dev` URL is also unreachable from this machine (network block) — verify deployments with `npx wrangler deployments list` (PATH-prefixed like all wrangler calls) and confirm push arrival via DingTalk message history, not HTTP calls. **`deployments list` ordering gotcha:** the newest deployment is NOT reliably at the top — `head` can show a stale entry (e.g. an old version at 100% from months ago) and mislead you. Read the **tail** (`... | tail -12`) to find today's version, and confirm it shows the expected Version ID at **100% traffic**.

## Amap Secondary-Source Cross-Check (高德二源复核)

Open-Meteo's early-morning single run occasionally emits an implausible code — e.g. on 2026-09-16 the 07:00 run returned code 96 (雷暴伴冰雹) + 6.4mm for 广州, which reached the group verbatim because `downgradeCode` only fires at precip < 2mm. Hail in 广州 in September is climatologically near-impossible; later Open-Meteo runs retracted it to drizzle and 高德 only ever reported 雷阵雨. The cross-check adds a **second data source (Amap) inside the Worker** to veto/adjust the storm codes *before* pushing.

### Key architecture decision — do NOT reuse the MCP

The `cloudmap-gateway` MCP (`amap_aggregator_local_weather_query`) goes through QoderWork's OAuth channel. The Worker runs on Cloudflare's public network and **cannot reuse that OAuth token**. The correct approach is to have the Worker call the Amap Open Platform public REST endpoint directly:

```
https://restapi.amap.com/v3/weather/weatherInfo?key=<AMAP_KEY>&city=<adcode>&extensions=all
```

This requires an Amap Open Platform **Web服务 key**. Parse the response at `forecasts[0].casts[0]` and read `dayweather` + `nightweather` (Chinese text like 雷阵雨 / 中雨 / 晴).

### CITIES must carry adcode

Query Amap by **adcode, not city name** (the Chinese name for 佛山 is often not recognized):

```javascript
var CITIES = [
  { name: "广州", adcode: "440100", latitude: 23.1291, longitude: 113.2644 },
  { name: "佛山", adcode: "440600", latitude: 23.0218, longitude: 113.1219 }
];
```

### AMAP_KEY gating + fail-open

- The cross-check runs **only when `env.AMAP_KEY` exists**. Set it as a Wrangler **secret**, not plaintext `[vars]`:

  ```bash
  export PATH="/c/Users/TBSG/AppData/Local/openclaw-bundle/node:$PATH" \
    && cd "<...>/outputs/weather-bot" \
    && npx -y wrangler secret put AMAP_KEY
  ```

  Pipe the key via stdin (never echo it into a report or leave a temp file behind). **Secrets take effect at runtime — no redeploy is required.** So the safe order is: deploy the cross-check code first (it stays dormant without the key), then `secret put AMAP_KEY` whenever the key is available; the cross-check auto-enables on the next scheduled run.

- If the key is missing OR the Amap call throws, the bot **fail-opens**: it keeps the Open-Meteo code unchanged and records a `warnings` entry — the push is never blocked by a cross-check failure.

### crossCheckCode rules (降雹 + 条件升雷)

Design principle chosen by the user: **hail = intersection (conservative veto), thunder = union (don't under-report)**. `RAIN_CODES = [51, 53, 55, 61, 63, 65, 80, 81, 82]`.

```javascript
function crossCheckCode(code, precipitation, amap) {
  if (!amap) return code;                 // fail-open: no Amap data → unchanged
  const text = `${amap.day}${amap.night}`;
  const hasHail = text.includes("雹");
  const hasThunder = text.includes("雷");
  if ((code === 96 || code === 99) && !hasHail) {
    code = hasThunder ? 95 : 61;          // hail not corroborated → thunderstorm or light rain
  } else if (code === 95 && !hasThunder) {
    code = 61;                            // thunder not corroborated → light rain
  } else if (hasThunder && !hasHail && precipitation > 1 && RAIN_CODES.includes(code)) {
    code = 95;                            // Amap sees thunder + OM has real rain → upgrade to thunderstorm
  }
  return code;
}
```

- **降雹 (hail intersection):** code 96/99 with no "雹" in Amap → downgrade to 95 (if Amap has "雷") or 61. This is what kills the false "雷暴伴冰雹".
- **降雷:** code 95 with no "雷" in Amap → downgrade to 61.
- **条件升雷 (thunder union):** Amap has "雷" but no "雹", AND Open-Meteo daily precip > 1mm, AND the OM code is a rain type in `RAIN_CODES` → upgrade to 95. Prevents under-reporting when Open-Meteo's latest run drops the thunder that Amap still sees.

### Audit trail (xcheck + warnings)

`handleRequest`'s result object carries `warnings: []` and `xcheck: []`. Each city appends an `xcheck` row `{ city, omCode (original), amapDay, amapNight, finalCode }`. Both are surfaced in the `/trigger` JSON summary and logged via `console.log` at the end of `runWithRetry`, so every scheduled push's cross-check decisions are auditable in Cloudflare logs. Never log the AMAP_KEY itself.

## Local Stub Test (test-xcheck.mjs)

Before deploying any cross-check change, run the local stub harness — it validates `crossCheckCode` behavior **without sending a real DingTalk message**.

- **What it stubs:** Open-Meteo (feeds this morning's 96/95 codes), Amap (雷阵雨 / 中雨 / call-failure variants), and the DingTalk send (intercepts `oapi.dingtalk.com`, renders only).
- **Coverage:** 6 scenarios / 17 assertions — hail downgrade (96→95 and 96→61), thunder downgrade (95→61), conditional thunder upgrade (rain code→95), no-key fail-open, Amap-failure fail-open. All 17 passed on the reference implementation.
- **Run it (note the full node path — node is NOT on PATH on this machine):**

  ```bash
  cd "<...>/outputs/weather-bot"
  /c/Users/TBSG/AppData/Local/openclaw-bundle/node/node.exe test-xcheck.mjs
  ```

- **Deploy discipline:** the user may explicitly halt deployment ("先不部署,先本地验证"). Only deploy after the stub test passes AND the user gives the go-ahead, using the PATH-prefixed form (`export PATH="/c/Users/TBSG/AppData/Local/openclaw-bundle/node:$PATH" && cd "<...>/outputs/weather-bot" && npx -y wrangler deploy`). Confirm with `npx -y wrangler deployments list | tail -12` (newest entry is at the tail, not the head — see the ordering gotcha above). Do not fire a real `/trigger` during verification (avoids spamming the group).

## Core Logic

### Weather Code Downgrade Rule

Open-Meteo sometimes reports thunderstorm/shower codes (80-82, 95-99) with near-zero precipitation. Downgrade to cloudy (code 2) when precipitation < 2mm to avoid false alarms:

```javascript
if ((code >= 80 && code <= 82) || (code >= 95 && code <= 99)) {
  if (precip < 2) {
    code = 2;
  }
}
```

`isRealStorm` check must stay consistent (`precipitation >= 2`):

```javascript
const isRealStorm = [95, 96, 99].includes(f.code) && f.precipitation >= 2;
```

**Two-stage code pipeline:** `downgradeCode` runs first (precip < 2mm guard against near-zero false storms), then — only when `env.AMAP_KEY` is present — `crossCheckCode` applies the Amap二源复核 (降雹 + 条件升雷). The `isRealStorm` / "不建议骑行" check reads the *final* post-cross-check code, so a hail code vetoed by Amap no longer forces "don't ride". See the "Amap Secondary-Source Cross-Check" section for the full rules.

### Cycling Advice Logic

```
All cities real storm  -> "Don't ride" (public transport)
Any other weather      -> "Cycling safety reminder" + specific tips
```

Tips are selected by weather type:
- Rain: brake distance, slow down, stay away from trucks
- Snow/ice: low speed, extra distance, no hard braking
- Fog: headlights on, slow down, keep distance
- Hot (>33C): hydrate, avoid noon rides, battery heat
- Cold (<10C): stay warm, battery range drop, check tire pressure
- Windy (>=39km/h): grip handlebars, avoid falling objects
- Storm: seek shelter immediately
- Good weather: daily safety habits (blind spots, distance, no speeding)

Always includes: **wear 3C-certified helmet, fasten strap**.

### Message Format

```
☀️ 早安天气 | 5月26日 星期二

【广州】 ⛅ 多云
温度:27C ~ 35C | 降水:1mm | 最大风力:15km/h
穿衣建议:短袖短裤,注意防晒
防晒:紫外线极强,尽量避免外出,出门务必涂防晒+戴帽子/太阳镜

【佛山】 ⛅ 多云
温度:27C ~ 35C | 降水:0.8mm | 最大风力:17km/h
穿衣建议:短袖短裤,注意防晒
防晒:紫外线极强,尽量避免外出,出门务必涂防晒+戴帽子/太阳镜

---

🔥 骑行安全提醒
【必做】戴好3C认证头盔,系紧束带
【天气】今天广州高温(35C),佛山高温(35C)
【防暑技巧】注意防暑降温多喝水,避免正午长时间骑行,注意电动车电池散热

💡 今日安全锦囊:大风天骑行身体压低减少受风面积
<font color=#FF0000>**✨ 今日金句:你越努力越幸运,越学习越强大**</font>
```

The quote line is intentionally red bold: DingTalk markdown supports `<font color=#RRGGBB>` and `**bold**`, and the combination renders as red bold text.

## Critical Pitfall: Newline in DingTalk Markdown

**NEVER** use `parts.join('\\n')`. JavaScript parses `\\n` as a literal backslash + n, which displays as `\n` text in DingTalk.

**CORRECT**: `parts.join('\n')` — produces an actual newline character that DingTalk renders properly.

## Push Accuracy Verification (推送准确性核验)

When the user asks "今天的推送准不准 / 天气报得对不对" (often with a screenshot of the morning push), run this reproducible cross-check pipeline. Do NOT redeploy or trigger a real push while verifying. The bot faithfully transcribes its data source, so a "wrong" push is usually a data-source issue, not a code bug — this pipeline is how you tell them apart.

### 4-step cross-check pipeline

**Step 1 — 高德 (Amap) real-time + forecast.** Use MCP `mcp__cloudmap-gateway__amap_aggregator_local_weather_query` with `query_type: "all"` (live + forecast). Query by **adcode, not city name** — the Chinese city name for 佛山 is often not recognized:

| 城市 | adcode |
|------|--------|
| 广州 | `440100` |
| 佛山 | `440600` |

**Step 2 — Open-Meteo current run.** `curl` the latest run for both cities so you compare against the *current* model output, not the 07:00 one the bot used. Coordinates: 广州 `23.1291,113.2644`, 佛山 `23.0218,113.1219`. Request `daily` + `hourly` + UV + precip probability:

```bash
cd "<workspace dir>"
curl -s "https://api.open-meteo.com/v1/forecast?latitude=23.1291&longitude=113.2644&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum,wind_speed_10m_max,uv_index_max,precipitation_probability_max&hourly=weather_code,temperature_2m,precipitation_probability&timezone=Asia%2FShanghai&forecast_days=1" -o gz_wx.json
curl -s "https://api.open-meteo.com/v1/forecast?latitude=23.0218&longitude=113.1219&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum,wind_speed_10m_max,uv_index_max,precipitation_probability_max&hourly=weather_code,temperature_2m,precipitation_probability&timezone=Asia%2FShanghai&forecast_days=1" -o fs_wx.json
```

Key check: does the current run's `weather_code` still contain a thunderstorm/hail code (95/96/99)? If the hourly series has NO storm code but the bot pushed "雷暴/冰雹", the morning run was a single-run false alarm.

**Step 3 — Compare against worker source thresholds.** Read `src/index.js` in the weather-bot folder and confirm the bot's decision rules, so you can attribute a discrepancy correctly:

- `downgradeCode`: a storm/shower code (80-82, 95-99) is downgraded to cloudy ONLY when `precipitation < 2mm`. So any code with precip >= 2mm passes through verbatim (this is how "雷暴伴冰雹" reached the group).
- "不建议骑行" (don't ride) fires only when **ALL** cities are real storms: `[95,96,99].includes(code) && precipitation >= 2` for every city.
- UV advice tiers come from `uv_index_max` (e.g. >=8 → "很强 SPF30+", >=6 → "较强 SPF15+").

**Step 4 — Attribute the deviation and output a comparison table.** Classify each mismatched field into one of three causes:

| 归因类别 | 含义 | 典型信号 |
|----------|------|----------|
| 转写错误 | Bot logic/transcription bug | Pushed value doesn't match ANY source; threshold applied wrongly |
| 模型单跑误报 | The 07:00 run gave a false positive; later runs retracted it | Current Open-Meteo run dropped the storm/hail code; Amap never reported it; climatologically implausible (e.g. hail in 广州 in September) |
| 模型更新 | Normal drift as the model refines over hours | Small numeric changes (wind, temp, UV one tier) with the same weather direction |

Output a `推送项 | 当前核验 | 判定` table (判定 = 准确 / 基本准确 / 高估 / 误报), then state the root cause in one sentence and the practical impact for the day (e.g. "冰雹字眼不必当真,但午后降水概率仍 86%,保守的雨天减速提醒依然有效").

### Environment pitfalls (verified on this Windows/bash machine)

- **No `python3`/`python` in the Bash tool.** Don't pipe curl into python. Instead `curl -o file.json` then parse with the **Read** tool.
- **No `node` on PATH either.** `node script.mjs` fails in the Bash tool. Use the full path `/c/Users/TBSG/AppData/Local/openclaw-bundle/node/node.exe` (v22) to run `local-trigger.mjs`, `render-test.mjs`, and `test-xcheck.mjs`. If that path is missing, `find /c/Users/TBSG/AppData/Local -maxdepth 3 -name node.exe`.
- **`/tmp` is a different filesystem than the Read tool.** Files written to `/tmp/...` in Bash cannot be Read. Always `curl -o` into the **workspace directory** (e.g. `cd "C:\Users\TBSG\.qoderwork\workspace\<id>"` first), then Read the relative/absolute Windows path.
- **`%%2F` is wrong in bash.** `timezone=Asia%%2FShanghai` produces a literal `%%2F` and a ~42-byte error response. In bash use a single percent: `timezone=Asia%2FShanghai`. (The `%%` escaping only applies to cmd/printf.)
- **佛山 by city name fails** on the Amap MCP — always use adcode `440600`.
- The weather-bot project may live in a *different* workspace dir than the current session (e.g. `...\workspace\ms3ghwc7lgp715xr\outputs\weather-bot`). If not found, glob `**/weather-bot/src/index.js` under `.qoderwork\workspace`.

## Customization

### Change Cities

Edit the `CITIES` array in `src/index.js` (keep the `adcode` field — it's required by the Amap cross-check; look up city adcodes at https://lbs.amap.com):

```javascript
const CITIES = [
  { name: '广州', adcode: '440100', latitude: 23.1291, longitude: 113.2644 },
  { name: '佛山', adcode: '440600', latitude: 23.0218, longitude: 113.1219 },
];
```

### Change Push Time

`wrangler.toml` Cron uses UTC. Beijing time = UTC + 8.

| Beijing | UTC Cron |
|---------|----------|
| 07:00 | `0 23 * * *` |
| 08:00 | `0 0 * * *` |
| 09:00 | `0 1 * * *` |
| 10:00 | `0 2 * * *` |

### Update Webhook / Secret

Edit `wrangler.toml` `[vars]` section, then re-run the PATH-prefixed deploy (`export PATH="/c/Users/TBSG/AppData/Local/openclaw-bundle/node:$PATH" && cd "<...>/outputs/weather-bot" && npx -y wrangler deploy`). Note: `AMAP_KEY` is a **secret**, not a `[vars]` entry — set it via `wrangler secret put AMAP_KEY` (runtime effect, no redeploy), never in plaintext `[vars]`.

## Troubleshooting

- **DingTalk errcode 310000**: Check that the signing secret matches the robot's security setting ("加签" mode).
- **Worker not triggering**: Verify Cron schedule in Cloudflare Dashboard; check Workers logs with `npx wrangler tail`.
- **Missed daily push**: Resend with `GET /trigger?token=<TRIGGER_TOKEN>` on the Worker URL, or locally via `node local-trigger.mjs` (the workers.dev URL is blocked on this machine, so local trigger is the reliable path). Then verify arrival in DingTalk and log to `check-log.md`.
- **`wrangler dev` fails with `spawn UNKNOWN` / errno -4094**: `workerd.exe` is blocked by Windows policy and Unblock-File doesn't help. Don't waste time on it — run the worker code directly in Node v22 via `local-trigger.mjs`.
- **Unknown weather emoji**: Add missing WMO codes to `WEATHER_MAP` in `src/index.js`.
- **Weather mismatch with phone app**: Open-Meteo is ECMWF-driven and may have local deviations. The built-in downgrade rule (precip < 2mm) filters most false thunderstorm reports. For serious discrepancies, cross-check with China Weather Network (weather.com.cn).
- **A push looks wrong (e.g. implausible "雷暴伴冰雹", over-stated precipitation/UV)**: Don't assume a code bug — run the "Push Accuracy Verification" pipeline (高德 adcode + Open-Meteo current run + worker source thresholds) to classify it as 转写错误 / 模型单跑误报 / 模型更新 before changing anything. If it's a recurring single-run false alarm, the fix is the Amap二源复核 (crossCheckCode), not the manual pipeline.
- **Cross-check seems inactive (still pushing "冰雹")**: The `AMAP_KEY` secret is probably not set — the cross-check is gated on `env.AMAP_KEY` and fail-opens without it. Set it with the PATH-prefixed `npx -y wrangler secret put AMAP_KEY` (secrets apply at runtime — **no redeploy needed**; it activates on the next scheduled run). Confirm via the `xcheck` array in `/trigger` output or the Cloudflare log line from `runWithRetry`. Remember the key must be an Amap Open Platform **Web服务** key (the `cloudmap-gateway` MCP OAuth token cannot be reused by the Worker).
- **Amap call failing / warnings in logs**: By design this is fail-open — the push still goes out with the raw Open-Meteo code and a `warnings` entry is recorded. Check the key validity/quota and that `city` is the adcode (广州 `440100`, 佛山 `440600`), not the city name. Never let an Amap outage block the morning push.
- **Newlines show as `\n` text**: You used `join('\\n')` instead of `join('\n')`. Fix and redeploy.

如何安装此技能?

访问技能市场,点击「安装」按钮,按提示将技能包放入 AI 编程助手的 skills 目录即可。

浏览技能市场

支持平台:Qoder · QoderWork · Claude · Codex 等 AI 编程助手