ci: 归档 ECS 专用工作流;新增 NAS 自动部署流水线
This commit is contained in:
Executable
+84
@@ -0,0 +1,84 @@
|
||||
name: Purge Twins (删测试分身)
|
||||
|
||||
# 按名字/slug 删掉指定分身(测试号留下的垃圾数据)。
|
||||
#
|
||||
# 为什么要走工作流:生产库在共享 ECS 上、不对外开放,本地连不上。
|
||||
# 为什么不手写 deleteOne:分身的数据散在 12+ 个集合里(知识/记忆/消息/会话/分享/
|
||||
# 作品/问答卡/课堂参与/排期报名/付费解锁/钱包/大师档案…)。这里【复用后端自己的
|
||||
# deleteTwin()】,跟用户在页面上点"删除分身"走完全同一条级联,不留孤儿数据。
|
||||
#
|
||||
# 默认 dry_run=true:先只打印"会删掉谁、连带多少条数据",确认无误再关掉 dry_run 真删。
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
targets:
|
||||
# 已预填老板确认过的 5 个测试分身(竹子=测试号4 的私密分身;其余 4 个是挂在广场上的垃圾)。
|
||||
# ⚠ 不要往里加 夏晚/陈九/林医生/阿May/阿正/大鹏/林小满/小野/苏晴/老周 ——
|
||||
# 那 10 个是 scripts/seed-masters.js 种的【演示分身】,是广场的门面,删了广场就空了。
|
||||
description: '要删的分身,填 slug 或显示名,逗号分隔'
|
||||
required: true
|
||||
default: '竹子,1234,社交测试89tuu7ds,11-2,12'
|
||||
type: string
|
||||
dry_run:
|
||||
description: '试运行:只打印会删什么,不真删(想先看一眼就勾上)'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
purge:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Purge
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ secrets.ECS_HOST }}
|
||||
username: ${{ secrets.ECS_USER }}
|
||||
password: ${{ secrets.ECS_PASSWORD }}
|
||||
command_timeout: 10m
|
||||
envs: TARGETS,DRY_RUN
|
||||
script: |
|
||||
set -e
|
||||
cd /var/www/digitalme-api.invoratec.cn
|
||||
[ -f .env ] || { echo "❌ 生产 .env 缺失,中止"; exit 1; }
|
||||
|
||||
TARGETS="${{ inputs.targets }}" DRY_RUN="${{ inputs.dry_run }}" node --input-type=module -e '
|
||||
import { connectDb, getDb } from "./src/db.js";
|
||||
import { deleteTwin } from "./src/services/twin.js";
|
||||
|
||||
const dry = String(process.env.DRY_RUN) !== "false";
|
||||
const names = String(process.env.TARGETS || "").split(",").map(s => s.trim()).filter(Boolean);
|
||||
if (!names.length) { console.log("没填 targets,什么都不做"); process.exit(0); }
|
||||
|
||||
await connectDb();
|
||||
const db = getDb();
|
||||
|
||||
// slug 或 displayName 精确匹配(不做模糊,免得误伤同名的真实用户)。
|
||||
const twins = await db.collection("twins")
|
||||
.find({ $or: [{ slug: { $in: names } }, { displayName: { $in: names } }] })
|
||||
.toArray();
|
||||
|
||||
const found = new Set(twins.flatMap(t => [t.slug, t.displayName]));
|
||||
for (const n of names) if (!found.has(n)) console.log(`⚠ 没找到:「${n}」(跳过)`);
|
||||
if (!twins.length) { console.log("一个都没匹配上,退出"); process.exit(0); }
|
||||
|
||||
console.log(dry ? "\n=== 试运行(不会真删) ===\n" : "\n=== 真删 ===\n");
|
||||
for (const t of twins) {
|
||||
const owner = t.ownerId ? await db.collection("users").findOne({ _id: t.ownerId }) : null;
|
||||
// 顺带报一下这个分身身上挂了多少数据,好确认"确实是个空壳测试号"。
|
||||
const [kn, msg, mem] = await Promise.all([
|
||||
db.collection("knowledge").countDocuments({ twinId: t._id }),
|
||||
db.collection("messages").countDocuments({ twinId: t._id }),
|
||||
db.collection("memories").countDocuments({ twinId: t._id }),
|
||||
]);
|
||||
console.log(`【${t.displayName}】slug=${t.slug} 公开=${!!t.isPublic}`);
|
||||
console.log(` 主人: ${owner?.email || owner?.phone || "(无主/已注销)"} 建于: ${t.createdAt?.toISOString?.().slice(0, 10) || "?"}`);
|
||||
console.log(` 知识 ${kn} 条 / 对话 ${msg} 条 / 记忆 ${mem} 条`);
|
||||
if (dry) { console.log(" → 试运行,未删\n"); continue; }
|
||||
await deleteTwin(t._id.toString(), t.ownerId);
|
||||
console.log(" → 已删(级联清干净)\n");
|
||||
}
|
||||
console.log(dry ? "试运行结束。确认无误后,把 dry_run 关掉再跑一次即可真删。" : `完成,删除 ${twins.length} 个分身。`);
|
||||
process.exit(0);
|
||||
'
|
||||
Reference in New Issue
Block a user