From b7246c4d471469bd62ebb8f3fb069db6aac59eb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=8F=E7=8A=80=E9=BA=9F?= Date: Sun, 13 Sep 2026 17:27:06 +0800 Subject: [PATCH] =?UTF-8?q?docs(memory):=20Uptime=20Kuma=20=E7=AC=AC?= =?UTF-8?q?=E4=B8=80=E6=89=B9=E7=9B=91=E6=8E=A7=E4=B8=8A=E7=BA=BF=20+=20gi?= =?UTF-8?q?t=20=E8=AF=81=E4=B9=A6=E6=9B=B4=E6=96=B0=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .workbuddy/memory/2026-09-13.md | 3 + .workbuddy/memory/DEPLOY.md | 10 ++++ .workbuddy/tmp/add-monitors.sh | 82 +++++++++++++++++++++++++++ .workbuddy/tmp/fix-service-monitor.sh | 43 ++++++++++++++ .workbuddy/tmp/kuma-schema.sh | 30 ++++++++++ .workbuddy/tmp/probe-service.sh | 18 ++++++ .workbuddy/tmp/update-git-cert.sh | 52 +++++++++++++++++ .workbuddy/tmp/verify-monitors.sh | 28 +++++++++ 8 files changed, 266 insertions(+) create mode 100644 .workbuddy/tmp/add-monitors.sh create mode 100644 .workbuddy/tmp/fix-service-monitor.sh create mode 100644 .workbuddy/tmp/kuma-schema.sh create mode 100644 .workbuddy/tmp/probe-service.sh create mode 100644 .workbuddy/tmp/update-git-cert.sh create mode 100644 .workbuddy/tmp/verify-monitors.sh diff --git a/.workbuddy/memory/2026-09-13.md b/.workbuddy/memory/2026-09-13.md index 122085b..eaadcc4 100644 --- a/.workbuddy/memory/2026-09-13.md +++ b/.workbuddy/memory/2026-09-13.md @@ -1,5 +1,8 @@ # 2026-09-13 +2026-09-13 | CHG | Uptime Kuma 第一批监控上线:9 个站点 HTTP 监控(60/120/300s 三档 + 证书到期通知),因官方 API 不支持创建监控,采用「直写 SQLite monitor 表 + 重启容器」批量添加 +2026-09-13 | FIX | 修正 service.xpcool.com 监控误报:后端纯 POST 无根路由,可接受状态码放宽为 ["200-399","404"](进程挂掉时 nginx 返 502,仍判 DOWN) +2026-09-13 | CFG | git.xpcool.com 证书更新(至 2026-12-12);旧证书备份为 fullchain.pem.bak.202609131719;注意该站证书文件名是 fullchain.pem/privkey.pem,与别的站不一致 2026-09-13 | CFG | status.xpcool.com 切换 HTTPS:腾讯云 DV 证书落到 /data/nginx/ssl/status.xpcool.com/(key 600),80→301→443 生效,公网验证 ssl_verify=0、证书有效期至 2026-12-12 2026-09-13 | FIX | 沉淀 SSH 远程部署两个坑:多行脚本经 ssh.exe 传参会因括号导致 bash 语法错误;多文件 scp 静默丢文件 → 改为「本地 .sh + 单文件 scp + 幂等脚本」 2026-09-13 | CFG | status.xpcool.com:新增 nginx 反代配置(先 HTTP,含 WebSocket 透传),nginx -t 通过并已 reload diff --git a/.workbuddy/memory/DEPLOY.md b/.workbuddy/memory/DEPLOY.md index 8c527bb..38ed0f4 100644 --- a/.workbuddy/memory/DEPLOY.md +++ b/.workbuddy/memory/DEPLOY.md @@ -58,3 +58,13 @@ - **证书**:腾讯云免费 DV,CN/SAN 均为 `status.xpcool.com`,**有效期仅 3 个月(至 2026-12-12),到期必须重新申请并替换**;落点 `/data/nginx/ssl/status.xpcool.com/`(crt/pem/csr 644、key 600),本地源 `E:\CentOS\ssl\status.xpcool.com_nginx\`。部署时用 `openssl x509 -pubkey | md5` 与 `openssl pkey -pubout | md5` 比对,确认证书与私钥配套。 - 访问形态:`http:// → 301 → https://`;公网实测 `ssl_verify=0`、响应约 0.45s(2026-09-13)。 - nginx 配置模板见 `.workbuddy/tmp/status.xpcool.com.conf`。 +- **监控的添加方式(2026-09-13 实测)**:Uptime Kuma 官方 REST API **不支持创建监控**(只能读/暂停/恢复),只能走 Socket.IO(需面板账号密码,社区库 `uptime-kuma-api`)或**直接写 SQLite**。本项目采用后者:向 `monitor` 表 INSERT(字段均有默认值,只需给 `name/type/url/interval/retry_interval/maxretries/weight/description/accepted_statuscodes_json`,`user_id=1` 即 `xpcool`),再 `docker restart uptime-kuma` 让服务端重新加载并开始探测。脚本:`.workbuddy/tmp/add-monitors.sh`(幂等,按 name 去重);**改库前先备份** `/tmp/kuma.db.backup.<时间戳>`。 +- **heartbeat.status 含义**:`0=DOWN`、`1=UP`、`2=PENDING`、`3=MAINTENANCE`;`monitor_tls_info` 表会自动填充各站点证书信息(可用于证书到期监控)。 +- **第一批监控(9 个,均 HTTP(s)、`accepted_statuscodes_json=["200-399"]`、开启证书到期通知)**: + | 站点 | 间隔 | 重试 | 权重 | + |---|---|---|---| + | admin / service / git | 60s | 3 | 1000 | + | file / read / tool / sub2api | 120s | 2 | 2000 | + | code / status | 300s | 2 | 3000 | +- **`service.xpcool.com` 单独放宽为 `["200-399","404"]`**:该后端是纯 POST 接口(遵循本项目"接口全 POST"规范),任何 GET 都 404;返回 404 说明 nginx + GoFrame 存活,**后端进程真挂时 nginx 返回 502,仍会判 DOWN**。 +- **待办**:通知渠道尚未配置(建议接 Bark,可复用现有 `bark` 容器及 `service.xpcool.com/bark/` 反代);「证书到期提前几天通知」需在面板设置里确认。 diff --git a/.workbuddy/tmp/add-monitors.sh b/.workbuddy/tmp/add-monitors.sh new file mode 100644 index 0000000..46070e4 --- /dev/null +++ b/.workbuddy/tmp/add-monitors.sh @@ -0,0 +1,82 @@ +#!/bin/bash +# 批量添加 Uptime Kuma 监控(2026-09-13 第一批) +# 说明:Uptime Kuma 官方 REST API 不支持创建监控,这里直接写入 SQLite(字段均有默认值), +# 写完后重启容器,服务端会从 monitor 表重新加载并开始探测。 +set -e + +DB=/data/uptime-kuma/kuma.db + +echo "===== insert monitors =====" +sudo python3 - <<'PY' +import sqlite3, shutil, datetime + +DB = "/data/uptime-kuma/kuma.db" +USER_ID = 1 + +# 先备份数据库(含 WAL),便于回滚 +stamp = datetime.datetime.now().strftime("%Y%m%d%H%M") +for ext in ("", "-wal", "-shm"): + try: + shutil.copy(DB + ext, "/tmp/kuma.db.backup.%s%s" % (stamp, ext)) + except Exception: + pass +print("backup -> /tmp/kuma.db.backup.%s" % stamp) + +# (名称, URL, 探测间隔秒, 最大重试, 排序权重(越小越前), 描述) +MONITORS = [ + ("管理后台 · admin.xpcool.com", "https://admin.xpcool.com", 60, 3, 1000, "Vue 管理后台前端"), + ("后端服务 · service.xpcool.com", "https://service.xpcool.com", 60, 3, 1000, "Go 后端服务"), + ("代码托管 · git.xpcool.com", "https://git.xpcool.com", 60, 3, 1000, "Gitea 代码托管"), + ("文件下载 · file.xpcool.com", "https://file.xpcool.com", 120, 2, 2000, "FileBrowser Quantum 只读下载"), + ("阅读 · read.xpcool.com", "https://read.xpcool.com", 120, 2, 2000, "legado 自托管阅读"), + ("工具站 · tool.xpcool.com", "https://tool.xpcool.com", 120, 2, 2000, "前端工具站"), + ("sub2api · sub2api.xpcool.com", "https://sub2api.xpcool.com", 120, 2, 2000, "sub2api 服务"), + ("网页 IDE · code.xpcool.com", "https://code.xpcool.com", 300, 2, 3000, "code-server 网页 IDE"), + ("监控面板 · status.xpcool.com", "https://status.xpcool.com", 300, 2, 3000, "Uptime Kuma 自身可用性"), +] + +con = sqlite3.connect(DB, timeout=30) +cur = con.cursor() +existing = {r[0] for r in cur.execute("select name from monitor")} + +added = 0 +for name, url, interval, retries, weight, desc in MONITORS: + if name in existing: + print("SKIP exists:", name) + continue + cur.execute( + "INSERT INTO monitor (" + " name, active, user_id, interval, retry_interval, maxretries, url, type, method," + " weight, description, accepted_statuscodes_json, expiry_notification," + " domain_expiry_notification, upside_down, maxredirects, invert_keyword" + ") VALUES (?, 1, ?, ?, ?, ?, ?, 'http', 'GET', ?, ?, ?, 1, 0, 0, 10, 0)", + (name, USER_ID, interval, interval, retries, url, weight, desc, '["200-399"]'), + ) + print("ADD:", name, "-> id", cur.lastrowid) + added += 1 + +con.commit() +print("added =", added, " total =", cur.execute("select count(*) from monitor").fetchone()[0]) +con.close() +PY + +echo "===== restart uptime-kuma (reload monitors) =====" +sudo docker restart uptime-kuma >/dev/null +sleep 50 + +echo "===== verify =====" +sudo python3 - <<'PY' +import sqlite3 +con = sqlite3.connect("file:/data/uptime-kuma/kuma.db?mode=ro", uri=True) +print("monitors:") +for row in con.execute("select id, name, type, interval, active from monitor order by weight, id"): + print(" ", row) +print("heartbeat rows:", con.execute("select count(*) from heartbeat").fetchone()[0]) +print("latest beats (monitor_id, status, time):") +for row in con.execute("select monitor_id, status, time from heartbeat order by id desc limit 15"): + print(" ", row) +PY + +echo "===== container =====" +sudo docker ps --filter name=uptime-kuma --format '{{.Names}} {{.Status}}' +echo DONE diff --git a/.workbuddy/tmp/fix-service-monitor.sh b/.workbuddy/tmp/fix-service-monitor.sh new file mode 100644 index 0000000..12f3c21 --- /dev/null +++ b/.workbuddy/tmp/fix-service-monitor.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# 修正 service.xpcool.com 监控的判定规则 +# 背景:该后端为纯 POST 接口,任何 GET 都返回 404;404 说明 nginx+GoFrame 存活, +# 真正故障(后端进程挂掉)时 nginx 会返回 502,故把 404 纳入可接受状态码。 +set -e + +echo "===== update monitor #2 =====" +sudo python3 - <<'PY' +import sqlite3 +con = sqlite3.connect("/data/uptime-kuma/kuma.db", timeout=30) +cur = con.cursor() +cur.execute( + "update monitor set accepted_statuscodes_json = ?, description = ? where id = 2", + ('["200-399","404"]', "Go 后端服务(无根路由,404 即存活;502/超时才算故障)"), +) +con.commit() +row = cur.execute("select id, name, accepted_statuscodes_json from monitor where id = 2").fetchone() +print("updated:", row[0], row[2]) +con.close() +PY + +echo "===== restart to reload =====" +sudo docker restart uptime-kuma >/dev/null +sleep 55 + +echo "===== final status =====" +sudo python3 - <<'PY' +import sqlite3 +con = sqlite3.connect("file:/data/uptime-kuma/kuma.db?mode=ro", uri=True) +label = {0: "DOWN", 1: "UP", 2: "PENDING", 3: "MAINTENANCE"} +q = """ +select m.id, m.name, m.interval, m.accepted_statuscodes_json, h.status, h.msg +from monitor m +left join heartbeat h on h.id = (select max(id) from heartbeat where monitor_id = m.id) +order by m.weight, m.id +""" +for mid, name, interval, codes, status, msg in con.execute(q): + print(" id=%s %-4ss [%-11s] %-34s %s" % (mid, interval, label.get(status, status), name[:34], (msg or "")[:40])) +print() +print("heartbeat total:", con.execute("select count(*) from heartbeat").fetchone()[0]) +PY +sudo docker ps --filter name=uptime-kuma --format '{{.Names}} {{.Status}}' +echo DONE diff --git a/.workbuddy/tmp/kuma-schema.sh b/.workbuddy/tmp/kuma-schema.sh new file mode 100644 index 0000000..6021b8b --- /dev/null +++ b/.workbuddy/tmp/kuma-schema.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# 读取 Uptime Kuma 数据库结构(只读),评估批量添加监控的可行路径 +python3 - <<'PY' +import sqlite3 +con = sqlite3.connect("file:/data/uptime-kuma/kuma.db?mode=ro", uri=True) +cur = con.cursor() + +print("===== all tables =====") +for (n,) in cur.execute("select name from sqlite_master where type='table' order by name"): + print(n) + +for name in ("user", "monitor", "monitor_notification", "monitor_tag", "tag", "notification"): + r = cur.execute("select sql from sqlite_master where type='table' and name=?", (name,)).fetchone() + print("\n===== schema: %s =====" % name) + print(r[0] if r else "NOT FOUND") + +print("\n===== users =====") +try: + for row in cur.execute("select id, username, active from user"): + print(row) +except Exception as e: + print("err:", e) + +print("\n===== monitor count =====") +print(cur.execute("select count(*) from monitor").fetchone()) + +print("\n===== monitor columns (pragma) =====") +for row in cur.execute("pragma table_info(monitor)"): + print(row) +PY diff --git a/.workbuddy/tmp/probe-service.sh b/.workbuddy/tmp/probe-service.sh new file mode 100644 index 0000000..28699dd --- /dev/null +++ b/.workbuddy/tmp/probe-service.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# 探测 service.xpcool.com 可用的健康检查路径 +echo "=== service nginx conf ===" +cat /data/nginx/conf.d/service.xpcool.com.conf + +echo "" +echo "=== path probe (via local nginx with Host header) ===" +for p in / /health /healthz /api/health /api/service/health /api/service/admin /api/service/open/health /index.html /favicon.ico; do + code=$(curl -s -o /dev/null -w "%{http_code}" -k -H "Host: service.xpcool.com" "https://127.0.0.1$p") + echo " $p -> $code" +done + +echo "" +echo "=== direct backend probe (127.0.0.1:10100) ===" +for p in / /health /api/health; do + code=$(curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:10100$p") + echo " $p -> $code" +done diff --git a/.workbuddy/tmp/update-git-cert.sh b/.workbuddy/tmp/update-git-cert.sh new file mode 100644 index 0000000..2b9977d --- /dev/null +++ b/.workbuddy/tmp/update-git-cert.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# git.xpcool.com 证书更新(2026-09-13) +# 自适应:从 nginx 配置里读出实际引用的证书路径,覆盖该路径,避免文件名不一致踩空 +set -e + +DOMAIN=git.xpcool.com +CONF=/data/nginx/conf.d/$DOMAIN.conf + +echo "=== conf ssl lines ===" +grep -n "listen\|server_name\|ssl_certificate" "$CONF" + +echo "=== ssl dir (before) ===" +sudo ls -l /data/nginx/ssl/$DOMAIN/ + +# 从配置解析真实路径(ssl_certificate\s 不会误匹配 ssl_certificate_key) +CRT=$(grep -oP "ssl_certificate\s+\K[^;]+" "$CONF" | head -1 | tr -d ' \r') +KEY=$(grep -oP "ssl_certificate_key\s+\K[^;]+" "$CONF" | head -1 | tr -d ' \r') +echo "target crt = $CRT" +echo "target key = $KEY" + +echo "=== old cert ===" +sudo openssl x509 -in "$CRT" -noout -subject -enddate +echo -n "old sha="; sudo sha256sum "$CRT" | cut -c1-16 + +# 备份旧证书(可回滚) +STAMP=$(date +%Y%m%d%H%M) +sudo cp -a "$CRT" "${CRT}.bak.$STAMP" +sudo cp -a "$KEY" "${KEY}.bak.$STAMP" +echo "backup -> ${CRT}.bak.$STAMP" + +# 覆盖为新证书 +sudo cp /tmp/${DOMAIN}_bundle.crt "$CRT" +sudo cp /tmp/${DOMAIN}.key "$KEY" +sudo chmod 644 "$CRT" +sudo chmod 600 "$KEY" + +echo "=== key/cert match check ===" +c=$(sudo openssl x509 -in "$CRT" -noout -pubkey | openssl md5) +k=$(sudo openssl pkey -in "$KEY" -pubout 2>/dev/null | openssl md5) +echo "cert=$c" +echo "key =$k" +if [ "$c" = "$k" ]; then echo "MATCH_OK"; else echo "MATCH_FAIL"; exit 1; fi + +echo "=== new cert ===" +sudo openssl x509 -in "$CRT" -noout -subject -enddate + +sudo nginx -t +sudo systemctl reload nginx + +echo "=== verify served cert ===" +echo | openssl s_client -connect 127.0.0.1:443 -servername $DOMAIN 2>/dev/null | openssl x509 -noout -subject -enddate +echo "DONE" diff --git a/.workbuddy/tmp/verify-monitors.sh b/.workbuddy/tmp/verify-monitors.sh new file mode 100644 index 0000000..7baee5e --- /dev/null +++ b/.workbuddy/tmp/verify-monitors.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# 复核监控状态:状态码含义 0=DOWN 1=UP 2=PENDING 3=MAINTENANCE +sleep 25 +sudo python3 - <<'PY' +import sqlite3 +con = sqlite3.connect("file:/data/uptime-kuma/kuma.db?mode=ro", uri=True) + +print("=== name encoding check (chars, first bytes hex) ===") +for row in con.execute("select id, name, length(name), hex(substr(name,1,4)) from monitor order by id limit 3"): + print(" ", row) + +print() +print("=== latest status per monitor ===") +q = """ +select m.id, m.name, h.status, h.time, h.msg +from monitor m +left join heartbeat h on h.id = (select max(id) from heartbeat where monitor_id = m.id) +order by m.weight, m.id +""" +label = {0: "DOWN", 1: "UP", 2: "PENDING", 3: "MAINTENANCE"} +for mid, name, status, time, msg in con.execute(q): + st = label.get(status, status) + print(" id=%s [%-11s] %s (%s) %s" % (mid, st, name, time, (msg or "")[:60])) + +print() +print("heartbeat total:", con.execute("select count(*) from heartbeat").fetchone()[0]) +print("tls_info rows:", con.execute("select count(*) from monitor_tls_info").fetchone()[0]) +PY