数字我 Digital Me:全量入库(大文件走 Git LFS)
nginx serve /uploads direct (audio speed) / patch-nginx (push) Failing after 1m34s
nginx serve /uploads direct (audio speed) / patch-nginx (push) Failing after 1m34s
This commit is contained in:
Executable
+55
@@ -0,0 +1,55 @@
|
||||
name: Check Auth Flags (只读诊断)
|
||||
|
||||
# 只读:打印生产 .env 里【登录相关开关】的值。不打印任何密钥/AppSecret/AccessKey。
|
||||
#
|
||||
# 要确认的事:生产上 ALLOW_STUB_LOGIN 是不是 true。
|
||||
# 如果是 true 且没接真实短信服务商,那么 POST /api/auth/sms/send 会把 6 位验证码
|
||||
# 【直接放进 HTTP 响应里返回】——任何人填上别人的手机号,就能拿到验证码,
|
||||
# 再调 /sms/verify 直接登录成那个人的账号。这不是"测试便利",这是一扇开着的门。
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Print auth flags (no secrets)
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ secrets.ECS_HOST }}
|
||||
username: ${{ secrets.ECS_USER }}
|
||||
password: ${{ secrets.ECS_PASSWORD }}
|
||||
command_timeout: 3m
|
||||
script: |
|
||||
set -e
|
||||
cd /var/www/digitalme-api.invoratec.cn
|
||||
[ -f .env ] || { echo "❌ 生产 .env 缺失"; exit 1; }
|
||||
|
||||
echo "===== 生产登录相关开关(只看开关,不看密钥)====="
|
||||
for k in NODE_ENV ALLOW_STUB_LOGIN SMS_PROVIDER EMAIL_PROVIDER FEATURE_MARKETPLACE; do
|
||||
v=$(grep -E "^${k}=" .env | head -1 | cut -d= -f2- || true)
|
||||
printf '%-22s = %s\n' "$k" "${v:-(未设置)}"
|
||||
done
|
||||
|
||||
echo
|
||||
echo "===== 是否配了真实短信/微信(只判断有没有,不打印值)====="
|
||||
for k in ALIYUN_ACCESS_KEY_ID WECHAT_APP_ID; do
|
||||
if grep -qE "^${k}=.+" .env; then echo "$k = 已配置"; else echo "$k = 未配置"; fi
|
||||
done
|
||||
|
||||
echo
|
||||
echo "===== 结论 ====="
|
||||
STUB=$(grep -E '^ALLOW_STUB_LOGIN=' .env | head -1 | cut -d= -f2- || true)
|
||||
SMSP=$(grep -E '^SMS_PROVIDER=' .env | head -1 | cut -d= -f2- || true)
|
||||
if [ "$STUB" = "true" ] && [ "$SMSP" != "aliyun" ]; then
|
||||
echo "🔴 危险:ALLOW_STUB_LOGIN=true 且没接真实短信服务商。"
|
||||
echo " → /api/auth/sms/send 会把验证码放进响应里返回。"
|
||||
echo " → 任何人填别人的手机号即可拿到验证码,然后登录成那个人。"
|
||||
echo " → 手机注册的账号目前全部可被任意接管。"
|
||||
elif [ "$STUB" = "true" ]; then
|
||||
echo "🟡 注意:ALLOW_STUB_LOGIN=true,但短信走的是 aliyun(真实发送)。"
|
||||
echo " 短信这条路是安全的;但微信仍是 mock 扫码,建议一并关掉 stub。"
|
||||
else
|
||||
echo "🟢 安全:ALLOW_STUB_LOGIN 未开启,验证码不会出现在响应里。"
|
||||
fi
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
name: Cleanup + Disk Guard + claw source
|
||||
|
||||
# A) 读 claw-api(aiplatform-api) 源码定位「反复重载 CustomApps」根因
|
||||
# B) 安全清理可再生空间(conda 包缓存 / docker 悬空镜像 / dnf 缓存 / npm 缓存)——不碰 docker 卷、/data、媒体
|
||||
# C) 装磁盘看门狗 cron:根盘>=90% 自动清失控日志,防再次满盘全站瘫
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: SSH cleanup + guard + claw scan
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ secrets.ECS_HOST }}
|
||||
username: ${{ secrets.ECS_USER }}
|
||||
password: ${{ secrets.ECS_PASSWORD }}
|
||||
command_timeout: 12m
|
||||
script: |
|
||||
D=/var/www/aiplatform-api.invoratec.cn
|
||||
echo "############ A. claw-api 反复重载 CustomApps 根因(读源码) ############"
|
||||
echo "=== 打印该日志的源码位置 ==="
|
||||
grep -rn "CustomApps" "$D/src" 2>/dev/null | grep -iE "Loaded|log|console" | grep -v node_modules | head -5
|
||||
echo "=== 谁触发重载(setInterval/fs.watch/chokidar/reload/loadAll/每请求加载) ==="
|
||||
grep -rniE "setInterval|fs\.watch|chokidar|reloadApps|loadCustomApps|loadAllApps|loadApps\(|scanApps|refreshApps" "$D/src" 2>/dev/null | grep -v node_modules | head -20
|
||||
echo "=== .env 是否有 LOG_LEVEL/DEBUG 可降噪 ==="
|
||||
grep -iE "LOG_LEVEL|LOG_|DEBUG|VERBOSE|NODE_ENV" "$D/.env" 2>/dev/null | sed -E 's/=.*/=***/' | head
|
||||
echo ""
|
||||
echo "############ B. 安全清理(仅可再生垃圾) ############"
|
||||
echo "=== before ==="; df -h / | tail -1
|
||||
echo "--- conda 包缓存 ---"
|
||||
if [ -x /opt/miniconda3/bin/conda ]; then /opt/miniconda3/bin/conda clean -a -y 2>&1 | tail -4; else echo "(无 conda)"; fi
|
||||
echo "--- docker 悬空镜像(只清 dangling,安全) ---"
|
||||
(command -v docker >/dev/null && docker image prune -f 2>&1 | tail -2) || echo "(无 docker)"
|
||||
echo "--- dnf 缓存 ---"
|
||||
(command -v dnf >/dev/null && dnf clean all 2>&1 | tail -1) || true
|
||||
echo "--- npm 缓存 ---"
|
||||
npm cache clean --force 2>/dev/null && echo "npm cache cleaned" || true
|
||||
echo "=== after ==="; df -h / | tail -1
|
||||
echo ""
|
||||
echo "############ C. 装磁盘看门狗 cron ############"
|
||||
echo 'IyEvYmluL2Jhc2gKIyDno4Hnm5jnnIvpl6jni5fvvJrmoLnliIbljLogPj05MCUg5pe257Sn5oCl5riF56m65aSx5o6nIHBtMiDml6Xlv5cgKyDmlLbnvKkgam91cm5hbO+8jOiusOW9leWIsCAvdmFyL2xvZy9kaXNrLWd1YXJkLmxvZ+OAggojIOeUsSBjcm9uIOavjyAxMCDliIbpkp/ot5HkuIDmrKHjgILpmLLmraLlho3mrKHmu6Hnm5jlr7zoh7QgbW9uZ29kIOatu+OAgeWFqOermeeYq+OAggpUSFJFU0g9OTAKVVNFPSQoZGYgLyB8IGF3ayAnTlI9PTJ7Z3N1YigiJSIsIiIsJDUpO3ByaW50ICQ1fScpCkxPRz0vdmFyL2xvZy9kaXNrLWd1YXJkLmxvZwpbICIke1VTRTotMH0iIC1sdCAiJFRIUkVTSCIgXSAmJiBleGl0IDAKZWNobyAiJChkYXRlICcrJUYgJVQnKSBkaXNrICR7VVNFfSUgPj0gJHtUSFJFU0h9JSAtPiBlbWVyZ2VuY3kgY2xlYW51cCIgPj4gIiRMT0ciCmZvciBkIGluIC9yb290Ly5wbTIvbG9ncyAvaG9tZS8qLy5wbTIvbG9ncyAiJEhPTUUvLnBtMi9sb2dzIjsgZG8KICBmb3IgZiBpbiAiJGQiLyoubG9nOyBkbwogICAgWyAtZiAiJGYiIF0gfHwgY29udGludWUKICAgIHN6PSQoc3RhdCAtYyVzICIkZiIgMj4vZGV2L251bGwgfHwgZWNobyAwKQogICAgaWYgWyAiJHN6IiAtZ3QgMjA5NzE1MjAwIF07IHRoZW4gZWNobyAiICB0cnVuY2F0ZSAkZiAoJCgoc3ovMTA0ODU3NikpTSkiID4+ICIkTE9HIjsgOiA+ICIkZiI7IGZpCiAgZG9uZQpkb25lCmpvdXJuYWxjdGwgLS12YWN1dW0tc2l6ZT0xMDBNID4+ICIkTE9HIiAyPiYxCmVjaG8gIiAgYWZ0ZXI6ICQoZGYgLWggLyB8IGF3ayAnTlI9PTJ7cHJpbnQgJDUiIHVzZWQsICIkNCIgZnJlZSJ9JykiID4+ICIkTE9HIgo=' | base64 -d > /usr/local/bin/disk-guard.sh
|
||||
chmod +x /usr/local/bin/disk-guard.sh
|
||||
( crontab -l 2>/dev/null | grep -v 'disk-guard.sh'; echo '*/10 * * * * /usr/local/bin/disk-guard.sh' ) | crontab -
|
||||
systemctl enable --now crond 2>/dev/null || systemctl enable --now cron 2>/dev/null || true
|
||||
echo "cron 已注册:"; crontab -l 2>/dev/null | grep disk-guard || echo " ⚠ 未见 cron 条目"
|
||||
echo "看门狗立即空跑一次(当前<90%应无动作):"; /usr/local/bin/disk-guard.sh; echo " exit=$?"
|
||||
echo "############ 完成 ############"
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
name: Cleanup Test Data (manual)
|
||||
|
||||
# 手动触发:清理上线前自测累积的数据(课堂/排期/孤儿自习/测试申请与预约/孤儿通知)。
|
||||
# 大师本体与真实用户分身一律不动。前置:后端已部署(scripts/cleanup-test-data.js 已在服务器)。
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Run cleanup on ECS
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ secrets.ECS_HOST }}
|
||||
username: ${{ secrets.ECS_USER }}
|
||||
password: ${{ secrets.ECS_PASSWORD }}
|
||||
command_timeout: 5m
|
||||
script: |
|
||||
cd /var/www/digitalme-api.invoratec.cn
|
||||
[ -f .env ] || { echo "❌ 生产 .env 缺失,中止"; exit 1; }
|
||||
[ -f scripts/cleanup-test-data.js ] || { echo "❌ scripts/cleanup-test-data.js 未部署,请先合并后端部署"; exit 1; }
|
||||
node scripts/cleanup-test-data.js
|
||||
echo "=== 校验:课堂应清空 ==="
|
||||
curl -sf "http://localhost:3603/api/classroom" | head -c 200 || echo "(接口检查失败)"
|
||||
Executable
+74
@@ -0,0 +1,74 @@
|
||||
name: Deploy Backend to ECS
|
||||
|
||||
# 数字我后端(纯成人版)。真正的生产目录 = /var/www/digitalme-api.invoratec.cn
|
||||
# (它有正确 .env: creator_cn 共享库+前缀 / DASHSCOPE / JWT),nginx digitalme-api → :3603。
|
||||
on:
|
||||
workflow_dispatch: {} # ★ 允许在 Actions 页面手动触发部署(老板要求 2026-07-28)
|
||||
push:
|
||||
# ⚠ 只监听 master。原来 [master, main] 是一把上膛的枪:
|
||||
# main 是落后 169 个提交的僵尸分支,往它推任何一行代码,
|
||||
# 都会把旧代码盖到生产上。(已发生过一次。main 现已删除。)
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'digital-me-backend/**'
|
||||
- '.github/workflows/deploy-adult-backend.yml'
|
||||
|
||||
jobs:
|
||||
# 说明:原「测试门 test job」(node --test + mongo service)已按要求移除。
|
||||
# 它总是报错、且 deploy 本就未 needs 它(不阻断部署),只让每次运行显示一个红叉、
|
||||
# 看起来像"部署失败"。移除后部署运行恢复全绿。
|
||||
# 要恢复 CI 测试:另建一个独立 workflow(如 test.yml)跑 node --test,不要再挂进部署流程误报。
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Copy files to ECS
|
||||
uses: appleboy/scp-action@v0.1.7
|
||||
with:
|
||||
host: ${{ secrets.ECS_HOST }}
|
||||
username: ${{ secrets.ECS_USER }}
|
||||
password: ${{ secrets.ECS_PASSWORD }}
|
||||
source: 'digital-me-backend/src,digital-me-backend/scripts,digital-me-backend/package.json,digital-me-backend/package-lock.json'
|
||||
target: /var/www/digitalme-api.invoratec.cn
|
||||
strip_components: 1
|
||||
overwrite: true
|
||||
rm: false
|
||||
timeout: 60s
|
||||
command_timeout: 5m
|
||||
|
||||
- name: Install + restart PM2 (:3603)
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ secrets.ECS_HOST }}
|
||||
username: ${{ secrets.ECS_USER }}
|
||||
password: ${{ secrets.ECS_PASSWORD }}
|
||||
command_timeout: 10m
|
||||
script: |
|
||||
cd /var/www/digitalme-api.invoratec.cn
|
||||
[ -f .env ] || { echo "❌ 生产 .env 缺失,中止"; exit 1; }
|
||||
# ★ 2026-07-28 上线前【关闭 stub 登录】:不再强制写 ALLOW_STUB_LOGIN=true。
|
||||
# 代码层(config.js)已保证生产环境即使 .env 误设 true 也强制忽略(防账号接管);
|
||||
# 这里再把历史遗留的 =true 就地改成 false,彻底消除。要放开消费端登录 = 接真实
|
||||
# 阿里云短信(SMS_PROVIDER=aliyun + ALIYUN_SMS_*) / 微信开放平台(WECHAT_APP_ID/SECRET)。
|
||||
if grep -q '^ALLOW_STUB_LOGIN=true' .env; then sed -i 's/^ALLOW_STUB_LOGIN=true/ALLOW_STUB_LOGIN=false/' .env; fi
|
||||
# ★ 万相文生图开关(老板拍板开):开通后 inkArt/漫画轻版/二创样板间美术视频样板渲成真图=成品。
|
||||
# 复用 DASHSCOPE key·国内 DashScope;用户点击才生成、结果缓存,非自动批量。
|
||||
# ⚠ 若 DASHSCOPE 账号未开通万相文生图(账号层,同 TTS 曾遇),置 true 也不出图——那是账号问题非代码。
|
||||
grep -q '^INK_ART_ENABLED=' .env || echo 'INK_ART_ENABLED=true' >> .env
|
||||
npm install --production --no-audit --no-fund --prefer-offline --no-progress 2>&1 | tail -8 || \
|
||||
npm install --production --no-audit --no-fund 2>&1 | tail -20
|
||||
# 收敛为单进程 digitalme-api on :3603(清掉错误的 adult/teen 空目录进程)。
|
||||
pm2 delete digitalme-adult-api 2>/dev/null || true
|
||||
pm2 delete digitalme-teen-api 2>/dev/null || true
|
||||
pm2 delete digitalme-api 2>/dev/null || true
|
||||
fuser -k 3603/tcp 2>/dev/null || true
|
||||
sleep 2
|
||||
PORT=3603 pm2 start src/index.js --name digitalme-api --node-args="--max-old-space-size=512" --update-env
|
||||
pm2 save
|
||||
sleep 6
|
||||
echo "=== pm2 ==="; pm2 list | grep digitalme || true
|
||||
echo "=== 崩溃日志(如有) ==="; pm2 logs digitalme-api --lines 25 --nostream 2>/dev/null | tail -30 || true
|
||||
H=$(curl -sf http://localhost:3603/health || true); echo "health:[$H]"
|
||||
echo "$H" | grep -q '"ok":true' || { echo "❌ 后端没起来"; exit 1; }
|
||||
echo "$H" | grep -q '"edition"' && { echo "❌ 旧代码"; exit 1; } || echo "✓ 新代码已上线"
|
||||
Executable
+98
@@ -0,0 +1,98 @@
|
||||
name: Deploy Web to ECS
|
||||
|
||||
# 数字我前端(纯成人版)
|
||||
on:
|
||||
workflow_dispatch: {} # ★ 允许在 Actions 页面手动触发部署(老板要求 2026-07-28)
|
||||
push:
|
||||
# ⚠ 只监听 master。原来 [master, main] 是一把上膛的枪:
|
||||
# main 是落后 169 个提交的僵尸分支,往它推任何一行代码,
|
||||
# 都会把旧代码盖到生产上。(已发生过一次。main 现已删除。)
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'digital-me-web/**'
|
||||
- '.github/workflows/deploy-adult-web.yml'
|
||||
|
||||
# ★ 并发锁:同一时刻只跑一个前端部署,多次快连推【串行、不重叠】。
|
||||
# 2026-07-20 事故:连推三次(80ed3f8/379b671/ed91b14)三个部署乱序上传 chunk,
|
||||
# 服务器一度新旧混装,懒加载的天下几章刷到「新 index.html 配已删旧 chunk」→ 打不开。
|
||||
# cancel-in-progress: false ⇒ 不中途掐(scp/rsync 掐一半更糟),排队跑完;
|
||||
# GitHub 会把中间那个 pending 顶掉,最终落定的永远是最新一次 push 的构建。
|
||||
concurrency:
|
||||
group: deploy-adult-web
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: digital-me-web
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
cache-dependency-path: digital-me-web/package-lock.json
|
||||
- run: npm ci
|
||||
- run: npm run build:fast
|
||||
env:
|
||||
VITE_API_BASE_URL: https://digitalme-api.invoratec.cn
|
||||
|
||||
- name: Upload dist to staging
|
||||
uses: appleboy/scp-action@v0.1.7
|
||||
with:
|
||||
host: ${{ secrets.ECS_HOST }}
|
||||
username: ${{ secrets.ECS_USER }}
|
||||
password: ${{ secrets.ECS_PASSWORD }}
|
||||
source: 'digital-me-web/dist/*'
|
||||
target: /tmp/dm-web-dist
|
||||
strip_components: 2
|
||||
overwrite: true
|
||||
|
||||
- name: Publish to nginx root + reload
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ secrets.ECS_HOST }}
|
||||
username: ${{ secrets.ECS_USER }}
|
||||
password: ${{ secrets.ECS_PASSWORD }}
|
||||
script: |
|
||||
# ★★ 2026-07-28:写死本站 SPA 目录,【绝不】扫描 nginx root、【绝不】按 index.html 内容猜目录。
|
||||
# 起因:同源同品牌(数字我家族)多站同服务器时,按 "index.html 含 数字我" 猜目录,会把隔壁
|
||||
# superdigitme("超级数字我")的前端一起覆盖(2026-07-28 事故)。数字我(成人)前端唯一落点就是这里。
|
||||
# 要改路径,只改这一行 ROOT。
|
||||
ROOT=/var/www/digitalme-spa
|
||||
RESOLVE="--resolve digitalme.invoratec.cn:443:127.0.0.1"
|
||||
NEW=$(grep -oE 'index-[A-Za-z0-9_-]+\.js' /tmp/dm-web-dist/index.html 2>/dev/null | head -1)
|
||||
if [ -z "$NEW" ]; then echo "::error::上传的 dist 里没有 index.html——上传步骤有问题,中止,不写盘。"; exit 1; fi
|
||||
mkdir -p "$ROOT"
|
||||
cp -rf /tmp/dm-web-dist/* "$ROOT"/
|
||||
WROTE="$ROOT"
|
||||
echo "✓ 已写入固定目录 $ROOT (指纹 $NEW)"
|
||||
rm -rf /tmp/dm-web-dist
|
||||
# 清理上一次误传进 acme 目录的 SPA 文件(不动真正的 challenge 文件)。
|
||||
rm -rf /var/www/acme-challenge/assets /var/www/acme-challenge/index.html 2>/dev/null || true
|
||||
# ── 缓存头(幂等【替换】,不再跳过): index.html 改 no-store —— 浏览器永不缓存首页,每次强制拿最新,
|
||||
# 根治「老用户/旧标签页一直加载旧 bundle、看不到新版」(no-cache 会被 bfcache 绕过,no-store 不会)。
|
||||
# hash 资源仍长缓存(immutable)。带 nginx -t 校验, 失败即回滚。 ──
|
||||
CONF=/etc/nginx/conf.d/digitalme.invoratec.cn.conf
|
||||
if [ -f "$CONF" ]; then
|
||||
cp "$CONF" "$CONF.dmbak"
|
||||
sed -i '/# dm-cache-headers/d' "$CONF" # 先删旧头(如有), 再注入新的
|
||||
sed -i '/index index\.html;/a location = /index.html { add_header Cache-Control "no-store, no-cache, must-revalidate"; } location /assets/ { add_header Cache-Control "public, max-age=31536000, immutable"; } # dm-cache-headers' "$CONF"
|
||||
if nginx -t 2>/dev/null; then echo "✓ nginx 缓存头已更新(index.html=no-store)"; else echo "❌ nginx -t 失败, 已回滚缓存头"; cp "$CONF.dmbak" "$CONF"; fi
|
||||
else
|
||||
echo "✓ conf 缺失, 跳过缓存头"
|
||||
fi
|
||||
nginx -s reload 2>/dev/null || true
|
||||
sleep 2
|
||||
# ★★ 发布后自检:以【公网真实 URL】(老板看到的那个)为准;不一致就把整张诊断表写进红色 ::error:: 标注
|
||||
# ——GitHub 会把它显示在运行页顶部(就是老板一直能看到、能贴回来的那种),这样病根直接暴露、不用再猜。
|
||||
LOCAL=$(curl -sk $RESOLVE https://digitalme.invoratec.cn/ 2>/dev/null | grep -oE 'index-[A-Za-z0-9_-]+\.js' | head -1)
|
||||
PUB=$(curl -s "https://digitalme.invoratec.cn/?cc=$$" 2>/dev/null | grep -oE 'index-[A-Za-z0-9_-]+\.js' | head -1)
|
||||
echo "=== 发布后自检 === 应发布=$NEW 本机served=$LOCAL 公网served=$PUB 写入目录=[$WROTE]"
|
||||
if [ "$NEW" != "$PUB" ]; then
|
||||
echo "::error::已写入 $WROTE 并 reload,但公网仍非最新。应发=$NEW 本机=$LOCAL 公网=$PUB。请确认 digitalme.invoratec.cn 的 nginx root 是否为 $WROTE。"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓✓ 自检通过:公网已是最新 $NEW"
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
name: Diagnose Cleanup + claw-api (read-only)
|
||||
|
||||
# 只读:①查 claw-api 疯狂刷日志的根因 ②盘点还能安全清理的空间。不删任何东西。
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
scan:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: SSH read-only scan
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ secrets.ECS_HOST }}
|
||||
username: ${{ secrets.ECS_USER }}
|
||||
password: ${{ secrets.ECS_PASSWORD }}
|
||||
command_timeout: 9m
|
||||
script: |
|
||||
echo "###################### 一、claw-api 刷日志根因 ######################"
|
||||
echo "===== pm2 describe claw-api ====="
|
||||
pm2 describe claw-api 2>/dev/null | grep -iE "status|restart|uptime|script path|exec cwd|pid|unstable" | head -12
|
||||
echo "===== claw-api 日志当前大小(截断后是否又涨) ====="
|
||||
ls -lh ~/.pm2/logs/claw-api-out.log ~/.pm2/logs/claw-api-error.log 2>/dev/null
|
||||
echo "===== out.log 最近 30 行(它在打印啥) ====="
|
||||
tail -30 ~/.pm2/logs/claw-api-out.log 2>/dev/null | cut -c1-240
|
||||
echo "===== out.log 最重复的 15 种行(定位刷屏内容) ====="
|
||||
tail -4000 ~/.pm2/logs/claw-api-out.log 2>/dev/null | sed -E 's/[0-9]{4}-[0-9:.TZ -]+//g; s/[0-9]+/N/g' | sort | uniq -c | sort -rn | head -15 | cut -c1-200
|
||||
echo "===== error.log 最近 20 行 ====="
|
||||
tail -20 ~/.pm2/logs/claw-api-error.log 2>/dev/null | cut -c1-240
|
||||
echo ""
|
||||
echo "###################### 二、还能安全清理什么 ######################"
|
||||
echo "===== df ====="; df -h / | tail -1
|
||||
echo "===== docker 可回收(images/build cache/volumes) ====="
|
||||
(command -v docker >/dev/null && docker system df 2>/dev/null) || echo "(无 docker)"
|
||||
echo " --- dangling 悬空镜像 ---"; (command -v docker >/dev/null && docker images -f dangling=true -q 2>/dev/null | wc -l | xargs echo " 悬空镜像数=") || true
|
||||
echo "===== 包管理器缓存 ====="
|
||||
du -sh /var/cache/apt 2>/dev/null; du -sh /var/cache/yum 2>/dev/null; du -sh /var/cache/dnf 2>/dev/null
|
||||
echo "===== conda 包缓存(可 conda clean) ====="; du -sh /opt/miniconda3/pkgs 2>/dev/null
|
||||
echo "===== 各种 cache / pm2日志 / journal ====="
|
||||
du -sh ~/.cache 2>/dev/null; du -sh ~/.npm 2>/dev/null; du -sh ~/.pm2/logs 2>/dev/null; journalctl --disk-usage 2>/dev/null
|
||||
echo "===== 旧构建备份 / 我留的 .dmbak / *.bak / dist.old ====="
|
||||
ls -lh /etc/nginx/conf.d/*.dmbak 2>/dev/null
|
||||
find /var/www -maxdepth 3 -type d \( -name "dist.old" -o -name "*.bak" -o -name "backup" \) 2>/dev/null | head
|
||||
find /var/www -maxdepth 3 -type f \( -name "*.bak" -o -name "*.tar.gz" -o -name "*.zip" \) -size +50M 2>/dev/null -printf '%s\t%p\n' | sort -rn | head -10 | awk '{printf " %.2fG\t%s\n",$1/1073741824,$2}'
|
||||
echo "===== /data/iot 里是否有 logs/node_modules/缓存(可清) ====="
|
||||
du -xh --max-depth=2 /data/iot 2>/dev/null | sort -rh | head -12
|
||||
find /data/iot -maxdepth 3 -type d -name node_modules 2>/dev/null -printf '%p\n' | head -8
|
||||
echo "===== 其它 >100M 的 .log(别的失控日志) ====="
|
||||
find / -xdev -type f -name "*.log" -size +100M 2>/dev/null -printf '%s\t%p\n' | sort -rn | head -12 | awk '{printf " %.2fG\t%s\n",$1/1073741824,$2}'
|
||||
echo "===== coredump / crash ====="
|
||||
du -sh /var/lib/systemd/coredump 2>/dev/null; du -sh /var/crash 2>/dev/null; ls /var/crash 2>/dev/null | head
|
||||
echo "===== 全盘 Top 25 大目录(复核) ====="
|
||||
du -xh --max-depth=3 / 2>/dev/null | sort -rh | head -25
|
||||
echo "###################### 扫描完成 ######################"
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
name: Diagnose Disk (read-only)
|
||||
|
||||
# 只读:根分区 100% 满,定位大占用者,判断哪些可安全清理。不删任何东西。
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
disk:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: SSH disk usage (read-only)
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ secrets.ECS_HOST }}
|
||||
username: ${{ secrets.ECS_USER }}
|
||||
password: ${{ secrets.ECS_PASSWORD }}
|
||||
command_timeout: 8m
|
||||
script: |
|
||||
echo "===== df -h ====="; df -h / /boot/efi 2>/dev/null
|
||||
echo "===== 根下一级目录占用(du --max-depth=1 /) ====="
|
||||
du -xh --max-depth=1 / 2>/dev/null | sort -rh | head -20
|
||||
echo "===== 全盘 Top 30 大目录(max-depth=3) ====="
|
||||
du -xh --max-depth=3 / 2>/dev/null | sort -rh | head -30
|
||||
echo "===== MongoDB 数据目录 ====="
|
||||
du -sh /var/lib/mongodb 2>/dev/null || du -sh /data/db 2>/dev/null || echo "(未找到常规 mongo 数据目录)"
|
||||
echo "===== pm2 日志目录(常见爆盘元凶) ====="
|
||||
du -sh ~/.pm2/logs 2>/dev/null; ls -lhS ~/.pm2/logs 2>/dev/null | head -15
|
||||
echo "===== /var/log 占用 ====="; du -sh /var/log 2>/dev/null; du -xh /var/log 2>/dev/null | sort -rh | head -12
|
||||
echo "===== journal 占用 ====="; journalctl --disk-usage 2>/dev/null || echo "(无 journalctl)"
|
||||
echo "===== npm/缓存 ====="; du -sh ~/.npm 2>/dev/null; du -sh /root/.cache 2>/dev/null; du -sh /tmp 2>/dev/null
|
||||
echo "===== docker 占用(如有) ====="; (command -v docker >/dev/null && docker system df 2>/dev/null) || echo "(无 docker)"
|
||||
echo "===== /var/www 各站点占用(含媒体上传) ====="
|
||||
du -xh --max-depth=2 /var/www 2>/dev/null | sort -rh | head -25
|
||||
echo "===== 单个大文件 Top 20(>200M) ====="
|
||||
find / -xdev -type f -size +200M 2>/dev/null -printf '%s\t%p\n' | sort -rn | head -20 | awk '{printf "%.1fG\t%s\n",$1/1073741824,$2}'
|
||||
echo "===== 完成 ====="
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
name: Diagnose ECS (read-only)
|
||||
|
||||
# 只读诊断:排查数字我后端 DB 接口卡死(MongoDB 挂/连不上)。不重启任何服务。
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
diagnose:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: SSH read-only diagnostics
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ secrets.ECS_HOST }}
|
||||
username: ${{ secrets.ECS_USER }}
|
||||
password: ${{ secrets.ECS_PASSWORD }}
|
||||
command_timeout: 8m
|
||||
script: |
|
||||
echo "===== 时间 ====="; date
|
||||
echo "===== 磁盘 df -h(磁盘满会让 mongo 卡/只读)====="; df -h | grep -vE "tmpfs|udev|overlay" | head -20
|
||||
echo "===== 内存 free -h ====="; free -h
|
||||
echo "===== pm2 list ====="; pm2 list 2>/dev/null | grep -iE "name|digitalme|online|errored|stopped" || pm2 list 2>/dev/null | head -20
|
||||
echo "===== pm2 describe digitalme-api ====="; pm2 describe digitalme-api 2>/dev/null | grep -iE "status|restart|uptime|memory|cpu|pid|script path|unstable" | head -20
|
||||
echo "===== 本地 /health(不读库)====="; curl -s -m 8 http://localhost:3603/health; echo
|
||||
echo "===== 本地 /api/plaza(读库,8s 看是否卡)====="; curl -s -m 8 -o /dev/null -w "plaza -> code=%{http_code} time=%{time_total}s\n" http://localhost:3603/api/plaza 2>&1
|
||||
echo "===== MongoDB systemd 服务 ====="
|
||||
systemctl is-active mongod 2>/dev/null && systemctl status mongod --no-pager 2>/dev/null | head -14 || echo "(mongod systemd 非 active 或不存在)"
|
||||
echo "===== MongoDB docker 容器 ====="
|
||||
(command -v docker >/dev/null && docker ps -a --format '{{.Names}} | {{.Status}} | {{.Image}}' 2>/dev/null | grep -i mongo) || echo "(无 docker mongo 或无 docker)"
|
||||
echo "===== 27017 端口监听 ====="; ss -tlnp 2>/dev/null | grep 27017 || echo "⚠ 27017 未监听"
|
||||
echo "===== .env 里 Mongo 主机(已脱敏密码) ====="; grep -iE "MONGO|MONGODB" /var/www/digitalme-api.invoratec.cn/.env 2>/dev/null | sed -E 's#://[^@ ]*@#://***:***@#g' | head -5 || echo "(未读到)"
|
||||
echo "===== mongo ping(8s) ====="
|
||||
if command -v mongosh >/dev/null; then timeout 8 mongosh --quiet --eval "db.adminCommand('ping')" 2>&1 | head -6; \
|
||||
elif command -v mongo >/dev/null; then timeout 8 mongo --quiet --eval "db.adminCommand('ping')" 2>&1 | head -6; \
|
||||
else echo "(无 mongosh/mongo 客户端)"; fi
|
||||
echo "===== API 近 45 行日志(找 Mongo 报错/超时) ====="; pm2 logs digitalme-api --lines 45 --nostream 2>/dev/null | tail -50
|
||||
echo "===== mongod 日志尾部(如有) ====="; tail -25 /var/log/mongodb/mongod.log 2>/dev/null || echo "(无 /var/log/mongodb/mongod.log)"
|
||||
echo "===== 系统负载/OOM 迹象 ====="; uptime; dmesg 2>/dev/null | grep -iE "out of memory|killed process|oom" | tail -5 || echo "(dmesg 无 OOM 或无权限)"
|
||||
echo "===== 诊断完成 ====="
|
||||
Executable
+84
@@ -0,0 +1,84 @@
|
||||
name: Fix 唐杰→鲁杰 (线上残留文本一次性改名)
|
||||
|
||||
# 背景:大师改名(唐杰→鲁杰)是通过后台 PATCH 改【大师档案的 displayName/honorific】完成的,
|
||||
# 大师页已显示鲁杰。但当初一次性种进库的【文旅行程文本】(itinerary/masterMoments/highlights)、
|
||||
# 以及 twin 的 characterPrompt/tagline/masterworks、master.intro 里的"唐杰"字面串,改名不会重写。
|
||||
# 本工作流做一次【外科手术式】替换:只把这些文本字段里的"唐杰"换成"鲁杰",不动其它任何数据、
|
||||
# 不重建种子(不会丢头像/作品/点赞)。只匹配含"唐杰"的文档,已经干净则自动零改动(可反复安全重跑)。
|
||||
#
|
||||
# ⚠ 需要 mongosh 支持 $replaceAll(MongoDB 4.4+)。生产为本地 Mongo,一般满足。
|
||||
# 跑完会打印每个集合改了几条;若都为 0,说明线上早已干净。
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
confirm:
|
||||
description: '输入 RENAME 确认执行(把线上残留“唐杰”改为“鲁杰”)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
fix:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Replace 唐杰 → 鲁杰 in tours / twins / masters text fields
|
||||
if: ${{ inputs.confirm == 'RENAME' }}
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ secrets.ECS_HOST }}
|
||||
username: ${{ secrets.ECS_USER }}
|
||||
password: ${{ secrets.ECS_PASSWORD }}
|
||||
command_timeout: 5m
|
||||
script: |
|
||||
set -e
|
||||
cd /var/www/digitalme-api.invoratec.cn
|
||||
[ -f .env ] || { echo "❌ 生产 .env 缺失,中止"; exit 1; }
|
||||
MONGO_URI="$(grep '^MONGO_URI=' .env | cut -d= -f2- || echo 'mongodb://localhost:27017')"
|
||||
MONGO_DB="$(grep '^MONGO_DB=' .env | cut -d= -f2- || echo 'creator_cn')"
|
||||
echo "──── 目标库:$MONGO_DB ────"
|
||||
mongosh "$MONGO_URI/$MONGO_DB" --quiet --eval '
|
||||
const FIND = "唐杰", REP = "鲁杰";
|
||||
const rp = (f) => ({ $replaceAll: { input: { $ifNull: [f, ""] }, find: FIND, replacement: REP } });
|
||||
|
||||
// 出现前计数
|
||||
const before = {
|
||||
tours: db.tours.countDocuments({ $or: [ {title:/唐杰/}, {masterMoments:/唐杰/}, {highlights:/唐杰/}, {"itinerary.title":/唐杰/}, {"itinerary.desc":/唐杰/} ] }),
|
||||
twins: db.twins.countDocuments({ $or: [ {displayName:/唐杰/}, {honorific:/唐杰/}, {characterPrompt:/唐杰/}, {tagline:/唐杰/}, {masterworks:/唐杰/} ] }),
|
||||
masters: db.masters.countDocuments({ $or: [ {intro:/唐杰/}, {offlineCommit:/唐杰/} ] }),
|
||||
};
|
||||
print("命中(改前):tours=" + before.tours + " twins=" + before.twins + " masters=" + before.masters);
|
||||
|
||||
const rt = db.tours.updateMany(
|
||||
{ $or: [ {title:/唐杰/}, {masterMoments:/唐杰/}, {highlights:/唐杰/}, {"itinerary.title":/唐杰/}, {"itinerary.desc":/唐杰/} ] },
|
||||
[ { $set: {
|
||||
title: rp("$title"),
|
||||
masterMoments: { $map: { input: { $ifNull: ["$masterMoments", []] }, as: "m", in: rp("$$m") } },
|
||||
highlights: { $map: { input: { $ifNull: ["$highlights", []] }, as: "h", in: rp("$$h") } },
|
||||
itinerary: { $map: { input: { $ifNull: ["$itinerary", []] }, as: "d", in: { $mergeObjects: [ "$$d", { title: rp("$$d.title"), desc: rp("$$d.desc") } ] } } }
|
||||
} } ]
|
||||
);
|
||||
const rw = db.twins.updateMany(
|
||||
{ $or: [ {displayName:/唐杰/}, {honorific:/唐杰/}, {characterPrompt:/唐杰/}, {tagline:/唐杰/}, {masterworks:/唐杰/} ] },
|
||||
[ { $set: {
|
||||
displayName: rp("$displayName"), honorific: rp("$honorific"),
|
||||
characterPrompt: rp("$characterPrompt"), tagline: rp("$tagline"),
|
||||
masterworks: { $map: { input: { $ifNull: ["$masterworks", []] }, as: "w", in: rp("$$w") } }
|
||||
} } ]
|
||||
);
|
||||
const rm = db.masters.updateMany(
|
||||
{ $or: [ {intro:/唐杰/}, {offlineCommit:/唐杰/} ] },
|
||||
[ { $set: { intro: rp("$intro"), offlineCommit: rp("$offlineCommit") } } ]
|
||||
);
|
||||
print("已改:tours=" + rt.modifiedCount + " twins=" + rw.modifiedCount + " masters=" + rm.modifiedCount);
|
||||
|
||||
const leftT = db.tours.countDocuments({ $or: [ {title:/唐杰/}, {masterMoments:/唐杰/}, {highlights:/唐杰/}, {"itinerary.title":/唐杰/}, {"itinerary.desc":/唐杰/} ] });
|
||||
const leftW = db.twins.countDocuments({ $or: [ {displayName:/唐杰/}, {honorific:/唐杰/}, {characterPrompt:/唐杰/}, {tagline:/唐杰/}, {masterworks:/唐杰/} ] });
|
||||
const leftM = db.masters.countDocuments({ $or: [ {intro:/唐杰/}, {offlineCommit:/唐杰/} ] });
|
||||
print("残留(改后):tours=" + leftT + " twins=" + leftW + " masters=" + leftM + " (应全为 0)");
|
||||
' || { echo "❌ mongosh 执行失败(可能 Mongo < 4.4 不支持 \$replaceAll,或连接串不对)"; exit 1; }
|
||||
echo "✅ 完成。刷新文旅路线详情页,D4 应显示「鲁杰·营造之道」。"
|
||||
- name: Confirmation missing
|
||||
if: ${{ inputs.confirm != 'RENAME' }}
|
||||
run: |
|
||||
echo "未执行:confirm 必须精确输入 RENAME。"
|
||||
exit 1
|
||||
Executable
+106
@@ -0,0 +1,106 @@
|
||||
name: Grant Super Admin (设超管 · 可带外建号)
|
||||
|
||||
# 超管不是"某个预置账号+密码"——代码里没有种子管理员、密码全是 salt+hash 不可反推。
|
||||
# 超管 = .env 的 SUPER_ADMIN_EMAILS 白名单:【白名单里的邮箱,谁拥有这个账号,谁就是超管】。
|
||||
#
|
||||
# ⚠ 重要(安全策略变更后的正确顺序):
|
||||
# 为堵住"管理员邮箱可被任何人抢注→拿全部后台权限"的漏洞,后端【已禁止用白名单邮箱自助注册】。
|
||||
# 所以【先加白名单、再去注册】这条老路已经走不通(注册会被 403 挡掉)。
|
||||
# 现在有两条路:
|
||||
# A) 已有账号:你的邮箱【已经】在平台注册过 → 直接跑本工作流加白名单 → 重新登录即为超管。
|
||||
# B) 没有账号:勾选 create_or_reset_account → 本工作流【带外】在库里建号/重置密码(密码取自
|
||||
# 仓库 Secret: ADMIN_INIT_PASSWORD)→ 加白名单 → 用该邮箱+该密码登录即为超管。
|
||||
#
|
||||
# 走 B 之前请先在 GitHub 仓库 Settings → Secrets and variables → Actions 添加:
|
||||
# ADMIN_INIT_PASSWORD = 你的强密码(≥8位)。用 Secret 而不是工作流输入框——输入框的值会明文
|
||||
# 留在 Actions 运行记录里,Secret 会在日志中被自动打码。
|
||||
#
|
||||
# 前置:后端需已部署过(含 scripts/create-admin.js)。若提示脚本不存在,先跑一次后端部署工作流。
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
admin_email:
|
||||
description: '要设为超管的邮箱(用你自己的)'
|
||||
required: true
|
||||
type: string
|
||||
create_or_reset_account:
|
||||
description: '带外建号/重置密码(账号还不存在、或忘了密码时勾选;密码取自 Secret ADMIN_INIT_PASSWORD)'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
delete_orphan_admin:
|
||||
description: '同时删除误建的孤儿账号 admin@test.com(密码随机无人知道,建议删)'
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
grant:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set SUPER_ADMIN_EMAILS (+ optional out-of-band account provisioning)
|
||||
uses: appleboy/ssh-action@v1
|
||||
env:
|
||||
ADMIN_EMAIL: ${{ inputs.admin_email }}
|
||||
ADMIN_PASSWORD: ${{ secrets.ADMIN_INIT_PASSWORD }}
|
||||
DO_CREATE: ${{ inputs.create_or_reset_account }}
|
||||
DO_DELETE_ORPHAN: ${{ inputs.delete_orphan_admin }}
|
||||
with:
|
||||
host: ${{ secrets.ECS_HOST }}
|
||||
username: ${{ secrets.ECS_USER }}
|
||||
password: ${{ secrets.ECS_PASSWORD }}
|
||||
command_timeout: 5m
|
||||
envs: ADMIN_EMAIL,ADMIN_PASSWORD,DO_CREATE,DO_DELETE_ORPHAN
|
||||
script: |
|
||||
set -e
|
||||
cd /var/www/digitalme-api.invoratec.cn
|
||||
[ -f .env ] || { echo "❌ 生产 .env 缺失,中止"; exit 1; }
|
||||
|
||||
echo "──── 当前 SUPER_ADMIN_EMAILS ────"
|
||||
grep '^SUPER_ADMIN_EMAILS=' .env || echo "(未设置 → 现在没有任何人是超管)"
|
||||
|
||||
EMAIL="$(echo "$ADMIN_EMAIL" | tr '[:upper:]' '[:lower:]' | tr -d ' ')"
|
||||
if [ -z "$EMAIL" ]; then echo "❌ 邮箱为空"; exit 1; fi
|
||||
|
||||
# ── 带外建号/重置密码(账号不存在时的唯一入口,因为白名单邮箱已禁止自助注册) ──
|
||||
if [ "$DO_CREATE" = "true" ]; then
|
||||
if [ -z "$ADMIN_PASSWORD" ]; then
|
||||
echo "❌ 勾了「带外建号」但仓库 Secret ADMIN_INIT_PASSWORD 未设置。"
|
||||
echo " 去 GitHub → Settings → Secrets and variables → Actions 添加 ADMIN_INIT_PASSWORD(≥8位) 再重跑。"
|
||||
exit 1
|
||||
fi
|
||||
[ -f scripts/create-admin.js ] || { echo "❌ 服务器上没有 scripts/create-admin.js —— 请先跑一次【后端部署】工作流再来。"; exit 1; }
|
||||
echo "──── 带外建号/重置密码 ────"
|
||||
ADMIN_EMAIL="$EMAIL" ADMIN_PASSWORD="$ADMIN_PASSWORD" node scripts/create-admin.js
|
||||
fi
|
||||
|
||||
# ── 写入/覆盖超管白名单(只放这一个邮箱,最小权限) ──
|
||||
if grep -q '^SUPER_ADMIN_EMAILS=' .env; then
|
||||
sed -i "s|^SUPER_ADMIN_EMAILS=.*|SUPER_ADMIN_EMAILS=$EMAIL|" .env
|
||||
else
|
||||
echo "SUPER_ADMIN_EMAILS=$EMAIL" >> .env
|
||||
fi
|
||||
echo "──── 写入后 ────"
|
||||
grep '^SUPER_ADMIN_EMAILS=' .env
|
||||
|
||||
# 删掉被误建的孤儿账号 admin@test.com(随机密码、无人知道、白占邮箱)
|
||||
if [ "$DO_DELETE_ORPHAN" = "true" ] && [ "$EMAIL" != "admin@test.com" ]; then
|
||||
MONGO_URI="$(grep '^MONGO_URI=' .env | cut -d= -f2- || echo 'mongodb://localhost:27017')"
|
||||
MONGO_DB="$(grep '^MONGO_DB=' .env | cut -d= -f2- || echo 'creator_cn')"
|
||||
echo "──── 删除孤儿账号 admin@test.com ────"
|
||||
mongosh "$MONGO_URI/$MONGO_DB" --quiet --eval '
|
||||
const u = db.users.findOne({ email: "admin@test.com" });
|
||||
if (!u) { print("admin@test.com 不存在,无需清理"); }
|
||||
else {
|
||||
const r = db.users.deleteOne({ _id: u._id });
|
||||
print("已删除 admin@test.com,deletedCount=" + r.deletedCount);
|
||||
}
|
||||
' || echo "⚠ mongosh 执行失败(可手动清理),不影响超管设置"
|
||||
fi
|
||||
|
||||
# 重启后端让新的 .env 生效
|
||||
pm2 restart digitalme-api --update-env
|
||||
pm2 describe digitalme-api | grep -E 'status|restarts' || true
|
||||
echo "✅ 完成。用 $EMAIL 登录 → 即为超管(右上角出现盾牌图标 / 可进 /admin)。"
|
||||
echo " 若刚带外建号:用你在 Secret ADMIN_INIT_PASSWORD 里设的密码登录,登录后建议立刻改密。"
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
name: nginx serve /uploads direct (audio speed)
|
||||
|
||||
# 让 nginx 直接吐 /uploads(音频),绕过 Node → 首播更快、sendfile 高效、长效不可变缓存。
|
||||
# 只影响公开的【试听】音频(满血走 Node 的签名/令牌口,不在 /uploads)。
|
||||
# ★安全:保留首次原始备份;精确定位 443 server 块插入;幂等(先删旧再插);nginx -t + 自检失败一律还原,绝不弄坏线上。
|
||||
on:
|
||||
push:
|
||||
paths: ['.github/workflows/nginx-uploads-direct.yml']
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
patch-nginx:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Add location /uploads to API nginx (robust+guarded)
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ secrets.ECS_HOST }}
|
||||
username: ${{ secrets.ECS_USER }}
|
||||
password: ${{ secrets.ECS_PASSWORD }}
|
||||
command_timeout: 5m
|
||||
script: |
|
||||
set -e
|
||||
CONF=$(grep -rl "digitalme-api.invoratec.cn" /etc/nginx 2>/dev/null | head -1)
|
||||
echo "conf=$CONF"; [ -n "$CONF" ] || { echo "❌ 找不到 API nginx conf"; exit 1; }
|
||||
UP=/var/www/digitalme-api.invoratec.cn/data/uploads
|
||||
[ -d "$UP" ] || { echo "❌ 上传目录不存在: $UP"; exit 1; }
|
||||
BAK="$CONF.orig-bak"; [ -f "$BAK" ] || cp "$CONF" "$BAK" # 只保留【首次】原始备份
|
||||
SAFE="$CONF.pre-uploads.$(date +%s)"; cp "$CONF" "$SAFE" # 本次回滚点
|
||||
python3 - "$CONF" "$UP" <<'PY'
|
||||
import sys, re
|
||||
conf, up = sys.argv[1], sys.argv[2]
|
||||
s = open(conf, encoding='utf-8').read()
|
||||
# 1) 幂等:先删掉已有的 location /uploads/ {...}(本模块的块无嵌套花括号)
|
||||
s = re.sub(r'\n?\s*location\s+/uploads/\s*\{[^{}]*\}\n?', '\n', s)
|
||||
# 2) 精确定位含 listen ... 443 的 server 块:找到 443 的 listen,再回溯它所属的 server 的 '{'
|
||||
m = re.search(r'listen[^;]*443', s)
|
||||
if not m: print('NO_443'); sys.exit(2)
|
||||
srv = s.rfind('server', 0, m.start())
|
||||
if srv < 0: print('NO_SERVER'); sys.exit(3)
|
||||
brace = s.find('{', srv)
|
||||
if brace < 0: print('NO_BRACE'); sys.exit(4)
|
||||
block = ('\n location /uploads/ {\n'
|
||||
' alias %s/;\n'
|
||||
' add_header Cache-Control "public, max-age=31536000, immutable";\n'
|
||||
' access_log off;\n'
|
||||
' try_files $uri =404;\n'
|
||||
' }\n') % up
|
||||
open(conf, 'w', encoding='utf-8').write(s[:brace+1] + block + s[brace+1:])
|
||||
print('inserted into 443 server block at', brace)
|
||||
PY
|
||||
if ! nginx -t; then echo "❌ nginx -t 失败,还原本次"; cp "$SAFE" "$CONF"; nginx -t; exit 1; fi
|
||||
nginx -s reload; sleep 1
|
||||
F=$(ls -1 "$UP"/songprev-* 2>/dev/null | head -1 | xargs -r basename)
|
||||
if [ -n "$F" ]; then
|
||||
H=$(curl -sI "https://digitalme-api.invoratec.cn/uploads/$F" 2>/dev/null)
|
||||
CODE=$(echo "$H" | head -1)
|
||||
echo "自检: $CODE"; echo "$H" | grep -iq "x-request-id" && echo "⚠ 仍是 Node 在吐(未直吐)" || echo "✓ 已由 nginx 直吐(无 x-request-id)"
|
||||
echo "$CODE" | grep -qE " 200| 206" || { echo "❌ 自检非2xx,还原"; cp "$SAFE" "$CONF"; nginx -s reload; exit 1; }
|
||||
fi
|
||||
echo "✓ done"
|
||||
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);
|
||||
'
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
name: Recover DB (conditional mongo restart)
|
||||
|
||||
# 磁盘已腾出,但满盘期间 MongoDB 可能卡进坏状态。先用应用自带 driver ping:
|
||||
# 卡死/失败 → 重启本机 mongod(共享库,恢复所有连库产品);健康 → 只重启本产品 API 清连接池。
|
||||
# 顺带装 pm2-logrotate,防止失控日志再次撑爆磁盘。
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
recover:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: SSH recover DB + prevent log blowup
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ secrets.ECS_HOST }}
|
||||
username: ${{ secrets.ECS_USER }}
|
||||
password: ${{ secrets.ECS_PASSWORD }}
|
||||
command_timeout: 10m
|
||||
script: |
|
||||
echo "========== 当前磁盘 =========="; df -h / | tail -1
|
||||
echo "========== 0) 装 pm2-logrotate 防再次爆盘(幂等) =========="
|
||||
pm2 install pm2-logrotate 2>&1 | tail -2 || echo " (logrotate 安装跳过)"
|
||||
pm2 set pm2-logrotate:max_size 50M 2>/dev/null
|
||||
pm2 set pm2-logrotate:retain 3 2>/dev/null
|
||||
pm2 set pm2-logrotate:compress true 2>/dev/null
|
||||
echo " pm2-logrotate: max_size=50M retain=3 compress=on"
|
||||
echo "========== 1) 用应用 driver ping MongoDB(8s 超时) =========="
|
||||
cd /var/www/digitalme-api.invoratec.cn
|
||||
PING=$(timeout 14 node -e '
|
||||
const { MongoClient } = require("mongodb");
|
||||
(async () => {
|
||||
const c = new MongoClient("mongodb://localhost:27017", { serverSelectionTimeoutMS: 8000 });
|
||||
try { await c.connect(); await c.db("admin").command({ ping: 1 }); console.log("MONGO_OK"); }
|
||||
catch (e) { console.log("MONGO_FAIL " + e.message); }
|
||||
finally { try { await c.close(); } catch (_) {} }
|
||||
})();
|
||||
' 2>&1 | grep -aE "MONGO_OK|MONGO_FAIL" | head -1)
|
||||
echo " ping 结果: [$PING]"
|
||||
if echo "$PING" | grep -q "MONGO_OK"; then
|
||||
echo "========== 2) Mongo 健康 → 只重启本产品 API 清连接池 =========="
|
||||
pm2 restart digitalme-api --update-env 2>/dev/null | tail -2
|
||||
else
|
||||
echo "========== 2) Mongo 卡死/无响应 → 重启本机 mongod =========="
|
||||
if systemctl list-units --type=service 2>/dev/null | grep -q mongod; then
|
||||
systemctl restart mongod 2>&1 | tail -3; sleep 8
|
||||
systemctl is-active mongod 2>/dev/null && echo " mongod active" || echo " ⚠ mongod 未 active"
|
||||
elif command -v docker >/dev/null && docker ps -a --format '{{.Names}}' 2>/dev/null | grep -qi mongo; then
|
||||
MC=$(docker ps -a --format '{{.Names}}' | grep -i mongo | head -1); echo " docker restart $MC"; docker restart "$MC" 2>&1 | tail -2; sleep 8
|
||||
else
|
||||
echo " ❌ 未识别 mongod 服务(既非 systemd 也非 docker),需人工处理"
|
||||
fi
|
||||
echo "========== 3) 重启本产品 API =========="
|
||||
pm2 restart digitalme-api --update-env 2>/dev/null | tail -2
|
||||
fi
|
||||
sleep 8
|
||||
echo "========== 4) 验证 =========="
|
||||
curl -s -m 8 -o /dev/null -w " /health -> code=%{http_code} time=%{time_total}s\n" http://localhost:3603/health
|
||||
curl -s -m 12 -o /dev/null -w " /api/plaza -> code=%{http_code} time=%{time_total}s\n" http://localhost:3603/api/plaza
|
||||
curl -s -m 12 -o /dev/null -w " /api/masters-> code=%{http_code} time=%{time_total}s\n" http://localhost:3603/api/masters
|
||||
echo "========== pm2 关键进程状态 =========="
|
||||
pm2 list 2>/dev/null | grep -iE "digitalme-api|mongo" || true
|
||||
echo "========== 最终磁盘 =========="; df -h / | tail -1
|
||||
echo "========== 完成 =========="
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
name: Repair Disk (safe cleanup)
|
||||
|
||||
# 根分区 100% 满导致 MongoDB 写卡死。只清「可再生垃圾」:失控日志/pm2日志/journal/npm缓存/旧nginx日志。
|
||||
# 绝不触碰:数据库文件、用户媒体、/var/www 站点、/var/lib/docker、/data。清完测 DB 恢复,必要时只重启本产品 API。
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
repair:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: SSH safe disk cleanup + verify recovery
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ secrets.ECS_HOST }}
|
||||
username: ${{ secrets.ECS_USER }}
|
||||
password: ${{ secrets.ECS_PASSWORD }}
|
||||
command_timeout: 10m
|
||||
script: |
|
||||
echo "========== 清理前 df =========="; df -h / | tail -1
|
||||
echo "========== 1) 截断失控的超大 pm2 stdout 日志(>500M,只清空不删文件) =========="
|
||||
for f in ~/.pm2/logs/*.log; do
|
||||
[ -f "$f" ] || continue
|
||||
sz=$(stat -c%s "$f" 2>/dev/null || echo 0)
|
||||
if [ "$sz" -gt 524288000 ]; then echo " 清空 $(du -h "$f" | cut -f1) $f"; : > "$f"; fi
|
||||
done
|
||||
echo "========== 2) pm2 flush(清其余各产品日志) =========="
|
||||
pm2 flush 2>/dev/null && echo " pm2 flush done" || echo " (pm2 flush 跳过)"
|
||||
echo "========== 3) journal 收缩到 100M =========="
|
||||
journalctl --vacuum-size=100M 2>&1 | tail -2 || echo " (无 journalctl)"
|
||||
echo "========== 4) npm 缓存清理(可再生) =========="
|
||||
npm cache clean --force 2>/dev/null && echo " npm cache cleaned" || echo " (npm cache 跳过)"
|
||||
echo "========== 5) 旧的 rotated nginx 日志(*.gz / *.N) =========="
|
||||
find /var/log/nginx -type f \( -name "*.gz" -o -regex ".*\.[0-9]+" \) -delete 2>/dev/null && echo " 旧 nginx 日志已清" || echo " (无)"
|
||||
echo "========== 清理后 df =========="; df -h / | tail -1
|
||||
echo "========== 6) MongoDB ping =========="
|
||||
if command -v mongosh >/dev/null; then timeout 10 mongosh --quiet --eval "db.adminCommand('ping')" 2>&1 | head -4; \
|
||||
elif command -v mongo >/dev/null; then timeout 10 mongo --quiet --eval "db.adminCommand('ping')" 2>&1 | head -4; \
|
||||
else echo " (无 mongo 客户端,跳过 ping)"; fi
|
||||
echo "========== 7) 本地读库接口测试(10s) =========="
|
||||
curl -s -m 10 -o /dev/null -w " plaza -> code=%{http_code} time=%{time_total}s\n" http://localhost:3603/api/plaza 2>&1
|
||||
CODE=$(curl -s -m 10 -o /dev/null -w "%{http_code}" http://localhost:3603/api/plaza 2>/dev/null)
|
||||
if [ "$CODE" != "200" ]; then
|
||||
echo "========== 8) plaza 仍非200,重启本产品 API 清连接池 =========="
|
||||
pm2 restart digitalme-api --update-env 2>/dev/null | tail -3; sleep 7
|
||||
curl -s -m 15 -o /dev/null -w " plaza(重启后) -> code=%{http_code} time=%{time_total}s\n" http://localhost:3603/api/plaza 2>&1
|
||||
else
|
||||
echo " ✓ 读库接口已恢复 200,无需重启"
|
||||
fi
|
||||
echo "========== 完成 =========="
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
name: Seed Master Wall (manual)
|
||||
|
||||
# 手动触发:把「数字大师墙」三城样板(成都/苏州/徽州 · 7 位大师 + 3 条路线)
|
||||
# 灌进生产库。幂等(只清同 seedTag 的旧种子,不动其它数据)。
|
||||
# 前置:后端已部署(scripts/seed-masters.js 已在服务器)。
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
seed:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Run seed on ECS
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ secrets.ECS_HOST }}
|
||||
username: ${{ secrets.ECS_USER }}
|
||||
password: ${{ secrets.ECS_PASSWORD }}
|
||||
command_timeout: 5m
|
||||
script: |
|
||||
cd /var/www/digitalme-api.invoratec.cn
|
||||
[ -f .env ] || { echo "❌ 生产 .env 缺失,中止"; exit 1; }
|
||||
[ -f scripts/seed-masters.js ] || { echo "❌ scripts/seed-masters.js 未部署,请先合并后端部署"; exit 1; }
|
||||
node scripts/seed-masters.js
|
||||
echo "=== 校验:大师墙接口 ==="
|
||||
curl -sf "http://localhost:3603/api/masters" | head -c 400 || echo "(接口检查失败)"
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
name: set admin password (manual)
|
||||
|
||||
# 已于 2026-08-02 执行一次:把生产超管口令从默认 admin123 改成强口令(堵公网默认口令洞)。
|
||||
# ★口令已抹除、只保留手动触发(不再随推送自动跑,避免误改回)。要再改口令:
|
||||
# 把下面 PW='...' 填成新口令,在 Actions 页手动 Run 一次,跑完再抹掉。
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
setpw:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Write ADMIN_PASS/ADMIN_USER to prod .env + restart
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ secrets.ECS_HOST }}
|
||||
username: ${{ secrets.ECS_USER }}
|
||||
password: ${{ secrets.ECS_PASSWORD }}
|
||||
command_timeout: 5m
|
||||
script: |
|
||||
set -e
|
||||
cd /var/www/digitalme-api.invoratec.cn
|
||||
[ -f .env ] || { echo "❌ 生产 .env 缺失"; exit 1; }
|
||||
PW='__FILL_NEW_PASSWORD__'
|
||||
[ "$PW" = '__FILL_NEW_PASSWORD__' ] && { echo "未填新口令,退出(避免误改)"; exit 0; }
|
||||
if grep -q '^ADMIN_PASS=' .env; then sed -i "s#^ADMIN_PASS=.*#ADMIN_PASS=$PW#" .env; else echo "ADMIN_PASS=$PW" >> .env; fi
|
||||
grep -q '^ADMIN_USER=' .env || echo "ADMIN_USER=admin" >> .env
|
||||
pm2 restart digitalme-api --update-env
|
||||
sleep 5
|
||||
H=$(curl -sf http://localhost:3603/health || true); echo "$H" | grep -q '"ok":true' && echo "✓ 已带新口令重启" || { echo "❌ 后端没起来"; exit 1; }
|
||||
Reference in New Issue
Block a user