release: cut 1.0.47 — connector supervision + bot-to-bot @-mention (#548)
This commit is contained in:
Executable
+85
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user