feat: opt ai group

This commit is contained in:
2026-10-03 19:33:03 +08:00
parent b95cb4a813
commit 7358008936
13 changed files with 382 additions and 8 deletions

View File

@@ -0,0 +1,91 @@
---
name: update-db
description: 对 LA 生产机上的 proxy-station 数据做规范化变更——增删节点、导入分享链接、调整策略组与成员、规则、规则集引用、订阅 token、Surge 设置段落。凡是用户要动线上数据的(「线上加/删个节点」「把这个组改成 url-test」「调一下规则」「建个订阅 token」「删掉那个 token」「改下生产上的设置」),都用这个 Skill,即使没提到「数据库」三个字。它只改数据不碰代码:代码更新与部署归 deploy Skill。
---
# 变更 proxy-station 生产数据
线上数据(节点、策略组、规则、token、设置)住在 la 机的 SQLite(`/opt/proxy-station/data/proxy-station.db`),由常驻的 `proxy-station` 服务直连。本 Skill 的做法是:**走服务自己的管理 API 做变更,变更前用 sqlite3 在线备份做快照**,不直接写库、不重启服务。
## 为什么必须走 API,不能直写 SQLite
API 不是简单转发,直写会绕过它的全部防护:
- zod 校验拦住坏数据(VMess 必有 UUID、WS 必有 path、SS 必有 method)
- 节点名唯一冲突返回 409;规则的 policy 必须指向存在的组或内置策略,否则规则整条失效
- 策略组改名会级联更新引用它的组成员与规则;members 是「整组替换」语义,API 帮你做对
- 订阅 token 由 crypto 随机生成、常量时间比对——手工 INSERT 塞进 null 或坏格式,`/sub` 全部当场 500
生成器每次请求实时读库,**API 改完订阅即刻生效**,不需要重启服务。直接动 SQLite 文件仅限两种只读场景:`api.sh counts` 的行数盘点,以及从备份快照里查旧值。
## 用法
```bash
bash .claude/skills/update-db/scripts/api.sh api GET /api/nodes
bash .claude/skills/update-db/scripts/api.sh api POST /api/nodes '{"name":"…","protocol":"…",…}'
bash .claude/skills/update-db/scripts/api.sh api GET /sub/<token>/clash # 拉订阅做校验(公开端点)
bash .claude/skills/update-db/scripts/api.sh backup <context> # context 用小写连字符 slug,如 add-jp-hy2
bash .claude/skills/update-db/scripts/api.sh counts
bash .claude/skills/update-db/scripts/api.sh health
```
- `api` 经 ssh 在**远端本机回环**调接口:`ADMIN_TOKEN` 只在远端 `/etc/proxy-station/env` 里展开,不经本地 shell、不落对话记录;JSON 走 stdin,无需转义
- 响应末尾带 `__HTTP_STATUS:200__` 一行。**非 2xx 时先读响应体里的错误信息再决定下一步**,不要盲目重试;要管道给 jq 时先剥掉这行:`| sed '$d' | jq …`
- 连不上 la(解析出 198.18.x.x)是本地 fake-IP 劫持了 ssh,与远端无关,等网络恢复再试
- 环境变量可覆盖 `DEPLOY_HOST`/`PS_DATA_DIR`/`PS_ENV_FILE`,默认即生产值
## 标准流程
1. **摸现状**(只读):`api GET /api/groups`、`/api/rules`、`/api/nodes`、`/api/tokens`,把要动的对象当前值拿到手。改组成员、调规则顺序这类「全量替换」操作,不先拿全量必出错。
2. **备份**:`api.sh backup <context>`。一个变更主题一个备份,同主题内的多条变更共用一份。备份落在远端 `data/backups/`(root 属主),不要清理。
3. **执行**:逐条调 API,每条核对 HTTP_STATUS 与返回体。某条失败就停下排查,不要跳过继续。
4. **校验**:
- `counts` 与变更前对比,行数增减要能对上意图
- `api GET` 变更对象,确认落库的是新值
- 涉及下发面的变更(节点/组/规则/token 范围):拉一次 `api GET /sub/<token>/clash` 确认 200;本地装了 mihomo 的话,`mihomo -t -f <文件>` 是最有说服力的端到端验证
- `excluded`/`skippedRules` 只存在于生成器内部、不在 HTTP 响应里;要核对静默排除时,用 `/api/nodes` 列表对比 yaml 里的 `proxies`
5. **汇报**:向用户复述三件事——变更清单、备份文件名、校验结果。三者齐了才算完成。
## 各实体的操作要点
### 节点 `/api/nodes`
- 新节点来自分享链接:先 `POST /api/nodes/import` 带 `{"text":"…","commit":false}` 预览(返回每条的解析结果与自动去重后的名字),确认后再 `commit:true` 真正入库
- 节点名是分流的关键:必须保留出口词尾缀(客户端里没有标签,用户靠名字区分直连/WARP 线路);地区组按名称正则收编,涉及新地区先读 `docs/ADDING-REGIONS.md`
- 临时下线用 `PATCH /:id/enabled {"enabled":false}`,不要删——IR 层会自动剔除停用节点,删了凭据就没了
- 真要删除:先 GET 全量字段向用户展示将消失的内容,确认后 DELETE
### 策略组 `/api/groups`
- `members` 是**整体替换**:GET 全量 → 改数组 → PUT 回去,别只发改动项
- 成员三态:`{kind:"node", nodeId}`/`{kind:"group", refName}`/`{kind:"builtin", refName}`
- `filterRegex` 与 `includeAllNodes` 可共存,**filterRegex 优先**(动态筛选用它,新增同地区节点自动进组)
- 改名后端会级联引用,但已导入客户端的旧组名会失配——改名前先向用户点明
- url-test/fallback 组别忘了 `testUrl`/`interval`/`tolerance`
### 规则 `/api/rules`
- 顺序由 `PUT /api/rules/reorder` 的**全量 ids 数组**决定(GET 拿现有顺序 → 调整 → 全量提交);自定义规则置顶是既有约定
- FINAL 规则永远保留;其策略组失效时 IR 层降级 DIRECT——兜底存在,但别依赖
- 新规则的 policy 必须已存在(内置 DIRECT/REJECT… 或现有组名),后端会校验,报错就先建组
- `POST /api/rules/reset` 会重置回模板初版并**清掉全部自定义规则**——除非用户明说「重置」,否则永远不调用
### 规则集 `/api/rulesets`(只读)
- `GET /api/rulesets` 看清单,`GET /:id/content` 看自托管转换结果
- `ruleset_cache` 是 24h 缓存(etag + 陈旧回落)自动管理,不要手动碰;「改规则集」实际是改引用它的规则
### 订阅 token `/api/tokens`
- 输入 `{name, nodeIds}`:`nodeIds` 空数组 = 下发全部启用节点;非空 = 只下发这些节点(给不同设备发不同范围的入口就在这里配)
- token 值由后端随机生成,创建响应里拿一次;`lastAccessAt`/`accessCount` 可判断是否仍在被使用
- **删除 token 会即刻断供所有用它的订阅 URL**——删除前必须向用户点明后果
### 设置 `/api/settings`
- `surgeSections` 是 Surge 原文段落透传,改错直接毁掉 Surge 配置;动它之前先 GET 存档 + backup,改完拉 `/sub/<token>/surge` 全文核对
## 回滚
- 单条改错:用 API 反向改回。旧值可以从备份快照只读查出:远端 `sqlite3 data/backups/<file>.db 'SELECT …'`
- 整体回滚:`api.sh restore data/backups/<file>.db --confirm`——停服换库再拉起,订阅中断约 10–20 秒,只在用户明确要求时执行
## 边界
- 不改代码、不做部署(那是 deploy Skill 的事);不动 nginx/证书/`ADMIN_TOKEN`/systemd 单元
- 删除类操作与 reset:先列出将删除/重置的完整清单,用户确认后再执行
- 节点凭据是真凭据:汇报里不要粘贴密码/UUID 全文,用节点名指代

View File

@@ -0,0 +1,23 @@
{
"skill_name": "update-db",
"evals": [
{
"id": 1,
"prompt": "帮我盘一下线上现在一共有多少节点、多少个策略组、多少条规则,节点按地区归归类",
"expected_output": "走 update-db Skill 的只读路径:api GET /api/nodes、/api/groups、/api/rules(或 counts),汇总出准确的行数与按地区分组的节点清单;不产生任何写操作、不建备份也行(纯读)",
"files": []
},
{
"id": 2,
"prompt": "在线上建一个临时测试订阅 token,名字叫 eval-test,确认订阅拉得通,然后把它删掉",
"expected_output": "走标准流程:backup → POST /api/tokens {name:eval-test} → GET /sub/<新token>/clash 验证 200 → DELETE → GET /api/tokens 确认已删;全程经 API,不直写 SQLite,最后汇报变更清单+备份名+校验结果",
"files": []
},
{
"id": 3,
"prompt": "我想把这条分享链接加到线上,先帮我看看它会解析成什么、别真的提交:vmess://eyJ2IjoiMiIsInBzIjoic2VsZi10ZXN0LWR1bW15IiwiYWRkIjoiZXhhbXBsZS5pbnZhbGlkIiwicG9ydCI6IjQ0MyIsImlkIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAwIiwiYWlkIjoiMCIsIm5ldCI6InRjcCIsInR5cGUiOiJub25lIiwidGxzIjoidGxzIn0=",
"expected_output": "POST /api/nodes/import 带 commit:false 干跑预览,展示解析出的节点字段(协议/地址/端口/UUID/TLS),明确说明未入库;不执行 commit:true",
"files": []
}
]
}

View File

@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# update-db 的唯一执行通道:对生产库的一切操作经它走 ssh(在远端本机回环上调 API / 做快照)。
#
# 用法:
# api.sh api METHOD PATH [JSON] # 调管理接口;PATH 以 /api/ 或 /sub/ 开头;JSON 作第三参
# api.sh backup <context> # 变更前快照 → data/backups/before-<context>-<时间戳>.db
# api.sh backups # 列出最近 20 个备份
# api.sh counts # 各表行数,变更前后对比用
# api.sh health # systemd 状态 + /api/health
# api.sh restore <file> --confirm # 整体回滚:停服换库再拉起(仅在用户明确要求时执行)
#
# 可用环境变量覆盖:DEPLOY_HOST(默认 la)、DEPLOY_SERVICE(默认 proxy-station)、
# PS_DATA_DIR(默认 /opt/proxy-station/data)、PS_ENV_FILE(默认 /etc/proxy-station/env)
set -euo pipefail
# 会话注入的 HTTP 代理只供模型 API 使用,ssh 一律直连(同 deploy.sh)
unset HTTPS_PROXY HTTP_PROXY https_proxy http_proxy ALL_PROXY all_proxy 2>/dev/null || true
HOST="${DEPLOY_HOST:-la}"
SERVICE="${DEPLOY_SERVICE:-proxy-station}"
DATA_DIR="${PS_DATA_DIR:-/opt/proxy-station/data}"
DB_FILE="$DATA_DIR/proxy-station.db"
BACKUP_DIR="$DATA_DIR/backups"
ENV_FILE="${PS_ENV_FILE:-/etc/proxy-station/env}"
SSH=(ssh -o BatchMode=yes -o ConnectTimeout=15 "$HOST")
die() { echo "错误:$*" >&2; exit 1; }
[ $# -ge 1 ] || die "缺少子命令(api/backup/backups/counts/health/restore)"
cmd="$1"; shift
case "$cmd" in
api)
[ $# -ge 2 ] || die "用法:api.sh api METHOD PATH [JSON]"
METHOD="$1"; APIPATH="$2"; BODY="${3:-}"
case "$METHOD" in GET|POST|PUT|PATCH|DELETE) ;; *) die "METHOD 只能是 GET/POST/PUT/PATCH/DELETE";; esac
case "$APIPATH" in /api/*|/sub/*) ;; *) die "PATH 必须以 /api/ 或 /sub/ 开头";; esac
# 单引号段内的 $ADMIN_TOKEN 与 ${PORT:-3000} 留给远端 shell 展开:token 不经本地 shell、不落本地磁盘
RCMD='. '"$ENV_FILE"' 2>/dev/null; curl -sS -m 60 -X '"$METHOD"' -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" -w "\n__HTTP_STATUS:%{http_code}__" http://127.0.0.1:${PORT:-3000}'"$APIPATH"
if [ -n "$BODY" ]; then
printf '%s' "$BODY" | "${SSH[@]}" "$RCMD --data-binary @-"
else
"${SSH[@]}" "$RCMD"
fi
;;
backup)
CTX="${1:-}"; [ -n "$CTX" ] || die "用法:api.sh backup <context>(小写字母/数字/连字符,如 add-jp-hy2)"
case "$CTX" in *[!a-z0-9-]*) die "context 只允许小写字母、数字与连字符";; esac
# .backup 是 SQLite 在线备份,WAL 下安全,产出独立完整快照(无需 wal/shm)
"${SSH[@]}" 'set -e; ts=$(date +%Y%m%d-%H%M%S); f='"$BACKUP_DIR"'/before-'"$CTX"'-$ts.db; sqlite3 '"$DB_FILE"' ".backup $f"; if [ -s "$f" ]; then ls -la "$f"; else echo "备份为空" >&2; exit 1; fi'
;;
backups)
"${SSH[@]}" "ls -la '$BACKUP_DIR' | tail -20"
;;
counts)
# 表清单来自 BOOTSTRAP_SQL;新增表时应同步更新这里
SQL="SELECT 'nodes='||COUNT(*) FROM nodes"
for t in policy_groups group_members rules rulesets ruleset_cache sub_tokens settings; do
SQL="$SQL UNION ALL SELECT '$t='||COUNT(*) FROM $t"
done
"${SSH[@]}" "sqlite3 '$DB_FILE' \"$SQL\""
;;
health)
"${SSH[@]}" "systemctl is-active $SERVICE; curl -sS -m 5 http://127.0.0.1:3000/api/health; echo"
;;
restore)
FILE="${1:-}"; [ -n "$FILE" ] || die "用法:api.sh restore <备份文件> --confirm"
[ "${2:-}" = "--confirm" ] || die "restore 会停服换库(订阅中断约 10–20 秒),必须由用户明确要求后追加 --confirm 执行"
case "$FILE" in "$BACKUP_DIR"/*.db) ;; *) die "备份文件必须是 $BACKUP_DIR 下的 .db 快照";; esac
"${SSH[@]}" 'set -e; f='"$FILE"'; [ -f "$f" ] || { echo "备份不存在:$f" >&2; exit 1; }
systemctl stop '"$SERVICE"'
cp "$f" '"$DB_FILE"'.new && mv '"$DB_FILE"'.new '"$DB_FILE"'
rm -f '"$DB_FILE"'-wal '"$DB_FILE"'-shm
chown '"$SERVICE"':'"$SERVICE"' '"$DB_FILE"'
systemctl start '"$SERVICE"'
for i in $(seq 1 20); do curl -sf -m 2 http://127.0.0.1:3000/api/health >/dev/null && break; sleep 1; done
systemctl is-active '"$SERVICE"' && curl -s http://127.0.0.1:3000/api/health && echo && echo "已回滚自 $(basename "$f")"'
;;
*) die "未知子命令:$cmd(可用:api/backup/backups/counts/health/restore)" ;;
esac