release: cut 1.0.47 — connector supervision + bot-to-bot @-mention (#548)

This commit is contained in:
修雨
2026-07-06 00:46:47 +08:00
committed by GitHub
parent bba94c0092
commit b63e1b4469
39 changed files with 2859 additions and 144 deletions
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env sh
# connect-watchdog.sh — keep a `dws dev connect` connector alive using the
# health contract from `dws dev connect status --json`.
#
# This is the "脚本" side of the 0701 review: a small, inspectable local watchdog
# that (1) asks dws whether the connection is actually healthy — not just whether
# a process exists — and (2) relaunches it when it is down/degraded. Drop it into
# cron or launchd; it is idempotent, so running it every few minutes is safe.
#
# It consumes the machine-readable contract, so it never parses `ps` or guesses.
#
# Usage:
# connect-watchdog.sh --client-id <clientId> [--dry-run] -- <launch command...>
#
# Example (relaunch a daemon connector if it is not healthy):
# connect-watchdog.sh --client-id ding123 -- \
# dws dev connect --robot-client-id ding123 --channel opencode --daemon
#
# cron (every 5 minutes):
# */5 * * * * /path/to/connect-watchdog.sh --client-id ding123 -- \
# dws dev connect --robot-client-id ding123 --channel opencode --daemon >> ~/.dws/connect/ding123/watchdog.log 2>&1
#
# Exit codes: 0 = healthy (no action) or relaunch issued; 1 = usage error.
set -eu
DWS="${DWS_BIN:-dws}"
CLIENT_ID=""
DRY_RUN=0
while [ $# -gt 0 ]; do
case "$1" in
--client-id) CLIENT_ID="$2"; shift 2 ;;
--dry-run) DRY_RUN=1; shift ;;
--) shift; break ;;
*) echo "unknown arg: $1" >&2; exit 1 ;;
esac
done
if [ -z "$CLIENT_ID" ]; then
echo "usage: connect-watchdog.sh --client-id <clientId> [--dry-run] -- <launch command...>" >&2
exit 1
fi
if [ $# -eq 0 ]; then
echo "error: missing launch command after --" >&2
exit 1
fi
ts() { date '+%Y-%m-%d %H:%M:%S'; }
# Ask dws for the health verdict. `--json` is the stable contract.
STATUS_JSON="$("$DWS" dev connect status --robot-client-id "$CLIENT_ID" --json 2>/dev/null || true)"
# Extract "state" without a hard jq dependency (fall back to jq when present).
if command -v jq >/dev/null 2>&1; then
STATE="$(printf '%s' "$STATUS_JSON" | jq -r '.state // empty' 2>/dev/null)"
else
STATE="$(printf '%s' "$STATUS_JSON" | sed -n 's/.*"state"[[:space:]]*:[[:space:]]*"\([a-z_]*\)".*/\1/p' | head -n1)"
fi
# Treat empty, "null", or any non-word value as unknown so the watchdog relaunches.
case "$STATE" in ""|null) STATE="unknown" ;; esac
case "$STATE" in
healthy)
echo "$(ts) [watchdog] $CLIENT_ID healthy — no action"
exit 0
;;
not_running|down|degraded|unknown)
echo "$(ts) [watchdog] $CLIENT_ID state=$STATE — relaunching: $*"
if [ "$DRY_RUN" -eq 1 ]; then
echo "$(ts) [watchdog] dry-run, not executing"
exit 0
fi
# For down/degraded, stop the old connector first so we do not fight the
# single-instance lock; not_running has nothing to stop (ignore failures).
if [ "$STATE" = "down" ] || [ "$STATE" = "degraded" ]; then
"$DWS" dev connect stop --robot-client-id "$CLIENT_ID" >/dev/null 2>&1 || true
fi
exec "$@"
;;
*)
echo "$(ts) [watchdog] $CLIENT_ID unexpected state=$STATE — no action" >&2
exit 0
;;
esac
+80
View File
@@ -0,0 +1,80 @@
# connect supervision recipes
Keep a `dws dev connect` connector alive across crashes, logout, and reboots —
**without** marrying one process manager. Every recipe here consumes the same
health contract, `dws dev connect status --json` (see PR #543), so they restart
on a *real* health verdict instead of guessing from the process table.
Two layers, pick per host:
| Host | Recipe | Boot persistence | Extra dependency |
|------|--------|------------------|------------------|
| anywhere (default) | `dws dev connect --daemon` | no | none (built-in) |
| macOS | `launchd.dws-connect.plist` | yes | none (OS built-in) |
| Linux | `systemd.dws-connect.service` | yes | none (OS built-in) |
| Windows | NSSM (see below) | yes | NSSM |
| Node teams | `pm2.ecosystem.config.js` | yes | Node + pm2 |
All of them run `connect-watchdog.sh`, which is the "脚本" from the 0701 review:
poll `status --json`, and relaunch when `state` is `down`/`degraded`/`not_running`.
## Why a watchdog on top of a supervisor?
`--daemon`, launchd, systemd, and pm2 all restart on **process death**. The
failure mode the review flagged — a connection that is *alive but deaf* — never
kills the process, so none of them see it. The watchdog closes that by asking
dws for the health verdict, not the OS for the pid.
## The watchdog
```sh
connect-watchdog.sh --client-id <clientId> [--dry-run] -- <launch command...>
```
- `--client-id` — the robot clientId (how the connector is keyed on disk).
- everything after `--` — the command to run when unhealthy.
- reads `dws dev connect status --robot-client-id <id> --json`, relaunches only
when needed; on `down`/`degraded` it stops the old connector first to avoid the
single-instance lock. Idempotent, safe to run every few minutes.
## macOS (launchd)
Edit `launchd.dws-connect.plist` (clientId, channel, absolute paths), then:
```sh
cp launchd.dws-connect.plist ~/Library/LaunchAgents/com.dingtalk.dws.connect.plist
launchctl load ~/Library/LaunchAgents/com.dingtalk.dws.connect.plist
```
## Linux (systemd --user)
Edit `systemd.dws-connect.service`, then:
```sh
mkdir -p ~/.config/systemd/user
cp systemd.dws-connect.service ~/.config/systemd/user/dws-connect.service
systemctl --user daemon-reload
systemctl --user enable --now dws-connect.service
loginctl enable-linger "$USER" # keep running after logout
```
## Windows (NSSM)
`--daemon` is not supported on Windows; run the foreground connector as a
service. Install NSSM, then:
```
nssm install dws-connect "C:\path\to\dws.exe" dev connect --robot-client-id <id> --channel opencode
nssm set dws-connect AppExit Default Restart
nssm start dws-connect
```
## Node teams (pm2)
Only if you already run pm2 — it is **not** a dependency of dws. It supervises
the foreground connector (not `--daemon`, to avoid double supervision):
```sh
pm2 start pm2.ecosystem.config.js
pm2 save && pm2 startup # boot persistence
```
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- macOS launchd agent: keep a dws connector alive + watchdog every 5 min.
Edit: <clientId>, --channel, and the absolute paths, then load with
launchctl (see recipes/README.md). Consumes `dws dev connect status --json`. -->
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.dingtalk.dws.connect</string>
<key>ProgramArguments</key>
<array>
<string>/absolute/path/to/scripts/dev/connect-watchdog.sh</string>
<string>--client-id</string>
<string>REPLACE_CLIENT_ID</string>
<string>--</string>
<string>/usr/local/bin/dws</string>
<string>dev</string>
<string>connect</string>
<string>--robot-client-id</string>
<string>REPLACE_CLIENT_ID</string>
<string>--channel</string>
<string>opencode</string>
<string>--daemon</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>DWS_BIN</key>
<string>/usr/local/bin/dws</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>StartInterval</key>
<integer>300</integer>
<key>StandardOutPath</key>
<string>REPLACE_HOME/.dws/connect/REPLACE_CLIENT_ID/watchdog.log</string>
<key>StandardErrorPath</key>
<string>REPLACE_HOME/.dws/connect/REPLACE_CLIENT_ID/watchdog.log</string>
</dict>
</plist>
@@ -0,0 +1,23 @@
// pm2 ecosystem for teams already running pm2. pm2 is NOT a dependency of dws —
// this is one optional host, not the default. It supervises the FOREGROUND
// connector (not --daemon) to avoid double supervision. pm2 restarts on process
// death; pair with connect-watchdog (a pm2 cron_restart or an OS cron) for the
// alive-but-deaf case, which only `dws dev connect status --json` can detect.
//
// pm2 start pm2.ecosystem.config.js
// pm2 save && pm2 startup // boot persistence
module.exports = {
apps: [
{
name: 'dws-connect',
script: 'dws',
args: 'dev connect --robot-client-id REPLACE_CLIENT_ID --channel opencode',
autorestart: true,
restart_delay: 5000,
max_restarts: 50,
// Optional: periodic health-driven restart. A cleaner setup runs
// connect-watchdog.sh from cron so restarts key off `status --json`.
// cron_restart: '*/30 * * * *',
},
],
}
@@ -0,0 +1,19 @@
# Linux systemd --user unit: keep a dws connector alive. systemd restarts on
# process death; connect-watchdog (run via a companion timer, or fold the poll
# into ExecStart) catches the alive-but-deaf case via `status --json`.
# Edit ExecStart (clientId, channel, absolute dws path), install per README.
[Unit]
Description=dws dev connect (DingTalk robot connector)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
Environment=DWS_BIN=/usr/local/bin/dws
# Foreground connector (NOT --daemon: systemd is the supervisor here).
ExecStart=/usr/local/bin/dws dev connect --robot-client-id REPLACE_CLIENT_ID --channel opencode
Restart=always
RestartSec=5
[Install]
WantedBy=default.target