两个程序(微软积分、微信读书签到)从本机 Windows 计划任务搬到一台 7×24 在线的云主机上,
用 Docker 容器跑、由宿主的 systemd timer 顺序触发。本文是这套部署的设计与运维说明;
落地文件在 ../scripts/linux/,实测记录见 0-Note 的
记录-服务器部署pi与config-ai-2026-10.md(本机文档,不进仓库)。
本机的痛点是机器不一定在线:出门、断电、系统更新重启、02:00 的 AutoShutdown0200,
当天那一次就没了(Windows 计划任务不补跑)。云主机 7×24 在线,定时器一定触发。
| 层 | 放在哪 | 为什么 |
|---|---|---|
| 触发 | 宿主 systemd timer(08:00 主力 + 12:00 兜底,Persistent=true) |
重启/关机错过的触发开机后补跑;不写调度代码 |
| 顺序 | 宿主 run-all.sh(flock 单实例) |
「一项结束才启动下一项」由它保证,不是靠错峰时刻去赌 |
| 守卫 / 通知 / 看门狗 | 宿主 rewards/run.sh、weread/run.sh |
与 Windows 版运行器一一对应,状态与日志留在宿主,容器随时可换 |
| 程序本体 | 容器(一次性,跑完即退) | 环境固定、依赖不污染宿主、升级就是换镜像 |
| 凭据 / 状态 | 宿主目录挂进容器 | 备份与迁移只搬目录,不进镜像 |
被否定的两条:① 两个常驻容器各自 cron(上游默认形态)—— 微软积分实测一次要 40 分钟以上, 固定槽位必然重叠,与「一项结束一项启动」冲突;② 单容器内顺序循环 —— 要把 Node+Python+Chromium 两套运行时塞进一个镜像,还得自己写调度循环,收益不抵复杂度。
/srv/apps/automation/
├── compose.yaml 两个服务的定义(都是一次性容器)
├── suite.env 可调参数(systemd EnvironmentFile)
├── run-all.sh 顺序编排(先微软积分,跑完再微信读书)
├── rewards/
│ ├── src/ 上游源码(构建上下文,含三个本地补丁)
│ ├── deploy/ run-once.sh(容器内入口)+ run-config.js(内存闸门/并行集群)
│ ├── config/config.json 程序配置(挂进容器)
│ ├── sessions/ 登录态 SQLite(挂进容器)
│ ├── logs/ 程序日志、runner.log、last-run.state、run.lock(同伴判定用)
│ ├── wechat-bridge/ 企业微信通知层(宿主侧调用,含 data/wecom-webhook.txt)
│ ├── .env 账号(ACCOUNT_N_EMAIL / PASSWORD)
│ └── run.sh 宿主侧运行器
├── weread/
│ ├── src/ vendor/ 程序本体 + Python 底座(打镜像用)
│ ├── deploy/ run-once.sh(容器内入口)
│ ├── Dockerfile 程序镜像(底座依赖装进 venv)
│ ├── secrets/ 网页 cookie、只读 API Key、App 凭据、企业微信 webhook
│ ├── data/ logs/ 状态与日志
│ ├── .env config.yaml 守卫参数与底座配置
│ └── run.sh 宿主侧运行器
├── tieba/
│ ├── src/ scripts/ 零依赖 Node 薄壳 + 同伴查询入口(run-state.js)
│ ├── deploy/ Dockerfile run-once.sh 与镜像定义
│ ├── secrets/ 企业微信 webhook(贴吧那个群)
│ ├── data/ logs/ 状态、历史与单实例锁(logs/run.lock)
│ ├── .env ACCOUNT_n_BDUSS 与开关
│ └── run.sh 宿主侧运行器
├── netease/
│ ├── src/ scripts/ 自研 weapi/eapi 加密层 + 薄壳 + 同伴查询入口
│ ├── deploy/ Dockerfile 同上
│ ├── secrets/ 企业微信 webhook(网易云那个群)
│ ├── data/ logs/ 同上
│ ├── .env ACCOUNT_n_MUSIC_U 与开关
│ └── run.sh 宿主侧运行器
├── state/ flock 锁与套件级状态
└── logs/suite.log 编排日志
镜像名:automation-rewards:local、automation-weread:local(本地构建,不走仓库)。
触发:automation-suite.timer → 08:00 与 12:00(Persistent=true,主机本地时区 Asia/Shanghai)。
12:00 那次是兜底:上午失败或没跑成时还有一次机会;两次都触发也不会重复跑 —— 幂等守卫一秒跳过。
顺序:run-all.sh 拿 state/suite.flock 排他锁 → rewards/run.sh → 等它返回 → weread/run.sh。
任一失败不影响另一个(各自有通知与当天重试机会)。
微软积分守卫(rewards/run.sh,逐条对应 Windows 版 run-daily.bat):
| 顺序 | 条件 | 结果 |
|---|---|---|
| 0 | 已有同一次运行(flock) | 跳过 |
| 1 | 逻辑日(04:00 为界)当天已完成(状态 9)或已尝试 3 次 | 跳过;跳过原因当天只推一次 |
| 2 | .env 还是模板账号 |
跳过并提醒(需要人工处理) |
| 3 | 可用内存低于 REWARDS_MIN_FREE_MB(1200MB) |
跳过,不消耗当天尝试次数 |
| 4 | 看门狗超时(REWARDS_RUN_TIMEOUT_MIN,150 分钟) |
强杀容器,记为失败,当天剩余触发重试 |
去掉的只有「关机避让」与「安静时段」(云主机没有 02:00 关机,也没人在这台机器上阅读);
换掉的只有单实例锁:Windows 用「是否有 node 在跑」判残锁,Linux 用 flock —— 进程一死锁自动释放,
不存在残锁。另外仍会写一份 logs/run.lock(JSON,含 pid 与开始时间)给微信读书的错峰判定用。
微信读书守卫:程序自带(src/guards.js)—— 已达标 / 尝试次数用尽 / 同伴在跑 / 安静时段 /
凭据失效,加联网段的凭据体检与官方统计。云端 .env 的两处改动:SHUTDOWN_TIME 留空
(关机避让不生效,minutesToShutdown 返回 Infinity)、BUSY_PEERS=/peers/rewards/run.lock
(容器里挂进来的同伴锁)。
沿用本机同一套企业微信机器人 key(两个程序各自的 wecom-webhook.txt 直接搬过去),文案与频率规则
不变(见 notification-convention.md):真跑一次开始 + 结束两条;
跳过同一天同一种原因最多一条;凭据失效、读不到数据这类需要人工处理的不限次数。
| 措施 | 效果 |
|---|---|
| 镜像预先构建(代码、依赖、Chromium 都在镜像里) | 触发时零构建、零下载 |
预置 chrome-headless-shell(npmmirror 下好放进构建上下文) |
构建从 40+ 分钟降到 5 分钟:Playwright CDN 实测 ~130 KB/s 且会停住,npmmirror 是 13 MB/s |
SKIP_RANDOM_SLEEP=true |
上游默认会随机等 5–50 分钟再跑,这里直接开跑 |
RUN_ON_START=false + CRON_SCHEDULE 置为永不触发 |
容器只跑我们要求的那一次 |
REWARDS_FREE_MB_FOR_PARALLEL=1800(上游默认 2500) |
云主机独占 2C4G,可用内存约 2.3GB → 开 2 集群并行跑账号,约省一半时间 |
一次性容器 + mem_limit |
跑完即退,不占常驻内存;超限只 OOM 容器自己 |
- 容器内 apt 走
deb.debian.org只有几百 KB/s:42 个包下 20 多分钟还没完;换成mirrors.cloud.tencent.com后 97 个包 28 秒。两个 Dockerfile 构建前都改。 - Playwright CDN 下载
chrome-headless-shell会卡住(114 MB,~130 KB/s): 改用scripts/linux/fetch-cft-browser.sh从 npmmirror 预下(13 MB/s、9 秒)放进构建上下文, Dockerfile 里解压到node_modules/patchright-core/.local-browsers/chromium_headless_shell-<revision>/并写INSTALLATION_COMPLETE/DEPENDENCIES_VALIDATEDmarker,让 patchright 跳过下载只做校验。 注意保留chrome-headless-shell-linux64/这一层目录 —— 铺平会报「Executable doesn't exist」。 顺带:node:24-slim 里没有unzip,apt 列表要加。
| 文件 | 来源 | 失效后 |
|---|---|---|
rewards/.env |
本机 %REWARDS_DIR%\.env(5 个账号) |
改密码就改这里,不用重建镜像 |
rewards/config/config.json |
本机 %REWARDS_DIR%\config.json |
配置项改动直接改文件 |
rewards/sessions/sessions.db |
本机 %REWARDS_DIR%\sessions\ |
失效会退回密码登录;删掉即强制重登 |
rewards/wechat-bridge/data/wecom-webhook.txt |
本机同路径 | 机器人被移出群就重新复制 |
weread/secrets/read-request.curl |
本机 %WEREAD_DIR%\secrets\ |
wr_skey 约 1.5 小时,程序自动续期;超 360 天或别处退出登录才需重抓 |
weread/secrets/weread-api-key.txt |
同上 | 换新的后跑一次只读校验 |
weread/secrets/app-*.json |
同上 | App token 过期时重新扫码(app-login.js qr + wait) |
tieba/.env |
浏览器里 tieba.baidu.com 的 BDUSS |
每个账号一行 ACCOUNT_n_BDUSS;失效时程序推一条「需要你处理」,重新抓一份写进去即可 |
tieba/secrets/wecom-webhook.txt |
贴吧那个群的群机器人地址 | 机器人被移出群就重新复制 |
netease/.env |
浏览器里 music.163.com 的 MUSIC_U |
同上;程序不做登录,失效只能人工换 |
netease/secrets/wecom-webhook.txt |
网易云那个群的群机器人地址 | 同上 |
两个程序各自一个群机器人,与其它程序分开的群与 key —— 消息按程序分开,出问题时不互相淹没。
所有凭据只在宿主目录里(600 权限),不进镜像、不进仓库。同一账号不要两边同时跑: 微软的登录态与微信读书的 cookie 都会被对方顶掉,所以迁到云主机后本机计划任务要停掉 (见第 8 节)。
# 宿主
systemctl list-timers automation-suite.timer # 下一次触发时刻
sudo systemctl start automation-suite.service # 手动触发一次(看日志)
journalctl -u automation-suite.service -n 50 --no-pager
tail -f /srv/apps/automation/logs/suite.log
tail -f /srv/apps/automation/rewards/logs/runner.log
# 单程序(跳过编排)
/srv/apps/automation/rewards/run.sh
/srv/apps/automation/weread/run.sh
# 容器层面
docker compose -f /srv/apps/automation/compose.yaml run --rm -T weread-run # 真跑一次
docker compose -f /srv/apps/automation/compose.yaml run --rm -T rewards-run
# 想跑程序自己的子命令(status / plan / verify / auth / pause)必须显式覆盖 entrypoint:
docker compose -f /srv/apps/automation/compose.yaml run --rm -T --entrypoint node weread-run src/index.js status
# 自检脚本(不进编排,手动跑)
docker compose -f /srv/apps/automation/compose.yaml run --rm -T --entrypoint bash rewards-run /opt/deploy/smoke.sh
docker compose -f /srv/apps/automation/compose.yaml run --rm -T --entrypoint node weread-run /opt/deploy/test-peer.mjs
docker compose -f /srv/apps/automation/compose.yaml run --rm -T --entrypoint node weread-run /opt/deploy/test-welfare.mjs判定「今天那次算不算成功」:微软积分看 rewards/logs/last-run.state(<逻辑日> 9 = 已完成;
3..8 = 当天尝试都失败);微信读书看 weread/data/state.json 的 done 与 attempts。
| 症状 | 根因 | 修法 |
|---|---|---|
| 永远只开 1 个集群(慢一倍) | os.freemem() 在 Linux 上返回 MemFree,把可回收页缓存算作已用;同一时刻 MemAvailable 2362MB 而它给出不到 1000MB |
run-config.js 优先读 /proc/meminfo 的 MemAvailable,读不到才退回 os.freemem() |
| 阈值改了不生效 | suite.env 用 . 载入但没 set -a,变量只存在于当前 shell,node 子进程读不到 |
三个脚本都改成 set -a; . suite.env; set +a |
| 看门狗超时后容器还在跑 | docker rm -f rewards-run 删不掉 compose run 的一次性容器(真名是 automation-rewards-run-run-<hash>) |
按标签删:docker ps -aq --filter "label=com.docker.compose.service=rewards-run" | xargs -r docker rm -f |
| 底座报「缺少依赖: PyYAML, requests, httpx」 | venv 用 ln -s /opt/venv/bin/python /usr/local/bin/python 暴露:符号链让 Python 把 sys.executable 解析成 /usr/bin/python3,找不到 pyvenv.cfg → 看不到 venv 的 site-packages |
改用 ENV PATH="/opt/venv/bin:$PATH"(并加 /etc/profile.d/venv.sh 兜住登录 shell) |
子命令(status/plan/auth)跑了却像在跑 run |
服务的 entrypoint 是 run-once.sh,后面跟的命令被忽略 |
显式 --entrypoint node |
容器里 check 报 EISDIR: illegal operation on a directory, read |
compose 把 ./<程序>/.env 以文件挂进去,而宿主上这个文件不存在时,Docker 会自动建一个同名目录(2026-10-14 实测:贴吧与网易云首次部署各撞一次) |
先建 .env 再起容器(部署步骤里那步别跳);两个程序已让「读不动就当空配置」,现在报的是「没有配置任何账号」 |
| 当天第一次真跑,退出码 1,企业微信里一条消息都没有 | config.yaml 以文件形式 bind mount 到容器里,而程序改配置的写法是「写 .tmp 再 rename 覆盖」—— rename 覆盖一个挂载点必然 EBUSY(2026-10-05 实测:1 秒内退出,执行失败:EBUSY: resource busy or locked, rename '/opt/weread/config.yaml.tmp' -> '/opt/weread/config.yaml') |
挂成模板:./weread/config.yaml:/opt/weread/config.template.yaml:ro,由 run-once.sh 复制到容器可写层再用(补丁只影响本次运行,不需要落回宿主) |
| 崩溃时完全静默(只能靠"今天没消息"发现) | 程序死在发消息之前,result 消息根本没机会生成 | 两个运行器都加兜底:alert-fail.mjs —— 退出码非 0 且日志里从未出现「企业微信」时,补一条「需要你处理」提醒(出现过说明程序至少试过发送,不重复打扰) |
第三个程序,容器化时比前两个多两件事:引擎刻意用可见窗口(上游 epic-games.js 里写死
headless: false,注释说明 headless 更容易触发 hCaptcha)→ 容器里用 Xvfb 提供虚拟显示;
以及结账环节的人机验证只能人工完成(程序会把预置结账链接推给企业微信)。
| 项 | 值 |
|---|---|
| 镜像 | automation-epic:local(1.55 GB) |
| 容器 | epic-run,一次性;入口 deploy/run-once.sh 先起 Xvfb 再跑 node src/cli.js run |
| 编排位置 | 第三步(微软积分 → 微信读书 → Epic),宿主看门狗 EPIC_RUN_TIMEOUT_MIN=40 |
| 浏览器 | 两个都预置:chromium(可见窗口用)+ chromium-headless-shell(注入 cookie 用 headless) |
| 凭据 | epic/secrets/epic-tokens.json(设备授权登录生成,自动续期)+ wecom-webhook.txt |
| 状态 | epic/data/state.json(已领记录、当天尝试次数);epic/vendor-data/ 存上游引擎结果 |
| 守卫 | 程序自己判(src/guards.js):已暂停 / 次数用尽 / 同伴在跑 / 安静时段 / 内存不足 |
接入时踩到的六个坑(全在部署层,项目代码只改了两处):
| 症状 | 根因 | 修法 |
|---|---|---|
构建直接失败 "/vendor": not found |
暂存布局与 Dockerfile 的 COPY 路径不一致 | 把项目摊平到构建上下文根(与 COPY package.json/src/vendor 对齐) |
构建日志里只有一行空的 exit code: 1 |
read -r A B < <(node -e '...'):node 输出不带换行 → read 返回 1 → set -e 静默退出 |
改用 herestring read -r A B <<< "$(...)",并补参数校验 |
curl: command not found |
node:24-slim 里没有 curl,apt 清单漏了 |
加进 apt 清单 |
Executable doesn't exist at /root/.cache/ms-playwright/... |
浏览器铺到了 node_modules/patchright-core/.local-browsers,但 patchright 默认去用户缓存找 |
ENV PLAYWRIGHT_BROWSERS_PATH=0 |
会话注入报 Cannot read properties of undefined (reading '_playwright') |
项目代码把 chromium.launchPersistentContext 拆下来单独调用,丢了 this |
项目侧修:.bind(chromium) |
| 引擎跑不起来、一直等到超时 | 上一次崩溃在 profile 里留下 SingletonLock,而容器每次都是新 hostname → Chromium 判成「被另一台电脑占用」 |
run-once.sh 每次先 rm -f /opt/epic/data/browser/Singleton* |
引擎崩溃 ENOENT .../vendor/free-games-claimer/data/.epic-games.json.tmp |
镜像里没有那个 data 目录 | Dockerfile mkdir -p + compose 挂 epic/vendor-data 留档 |
已知会走到人工的环节:结账时的 hCaptcha。程序识别到就把「需要你处理」+ 每款一条结账链接推出来,
点开在浏览器里完成即可。2026-10-05 首次真跑就撞上了它(引擎日志:Got hcaptcha challenge! Lost trust due to too many login attempts?),属于设计内的退化路径,不是故障。
| 症状 / 诉求 | 原来 | 现在 |
|---|---|---|
| 微信读书官方进度差一点就停了,只能等下一次触发 | 一次运行 = 一个底座会话,不达标只记一次尝试 | 会话循环:不达标就在同一次运行内再跑一个会话(最多 3 个),每轮按最新官方进度重算目标并写回底座配置;宿主看门狗 100 → 330 分钟;程序侧 MAX_SESSIONS_PER_RUN=3、RUN_BUDGET_MINUTES=330 |
Epic 消息里的预置结账链接打开报 Account id is missing |
消息只给 .../store/purchase?offers=1-... |
消息改推商店页链接(点 Get);预置结账链接仍可用 node src/cli.js link 取 |
| Epic 人机验证频繁出现 | 每天 2 次尝试 | 每天 1 次、失败不重试 —— 验证码多由"登录尝试过多"触发,保持上游"不刷"的定调 |
| 无可领取游戏时不想收到消息 | nothing-new 本来就不推送 |
加回归测试固化,防止以后改坏 |
微信读书的 attempts 现在按会话累加(一次触发内可能 2-3 个),每日上限 3 → 6;
循环结束后用 recordMinutes() 补记最终进度,不重复累加 attempts。
仓库里的「基础模块」(scripts/ + scripts/lib/)多数是给 Windows 本机用的;真正在服务器上有价值的是
四个纯 Node 脚本(零 npm 依赖):check-wecom-drift.mjs、check-privacy.mjs、apply-schedule.mjs --emit=systemd、
backup.mjs。它们被放进一个按需跑的容器(compose 服务 suite-check,镜像 automation-suite-check:local),
宿主上什么都不用装,平时零进程 —— 更新工具只要在宿主的仓库克隆里 git pull,不必重建镜像。
gh repo clone Zzz210s/scripts-hub ~/scripts-hub # 只做一次
suite-check # 四项全跑;也可 drift|privacy|units|backup 单跑服务器侧备份清单:config/backup.server.json(本地那份的 sets 是 Windows 路径)。restic 已装进镜像但
仓库/密码故意留空 —— --apply 会拒绝执行,现在只做 --dry-run 盘点(18/18 个来源、720 个文件、169MB)。
首次跑抓到两件真事:demo 脚本里写着真实账号昵称(已脱敏,注意它在公开仓库的历史里);
/etc/systemd/system/ 的 unit 比仓库权威版旧(已按 scripts/linux/systemd/ 重装)。
该清的只有构建缓存。docker system df 会把"没有正在运行的容器在用"的镜像算进 RECLAIMABLE ——
这台上三个自动化镜像都是一次性容器用完即退,照那个数字跑 docker image prune -a 会把它们全删掉。
真正可回收的是构建缓存(反复重建攒下来的):
docker system df # 先看:Build Cache 的 Reclaimable 才是可清的
docker builder prune -f # 清(不影响镜像,只让下次重建慢一点)2026-10-05 实测:构建缓存 9.46GB(可回收 6.59GB)→ 清掉后根分区 21G→16G(37%→28%)。
swap 已加到 4GB(/swap.img 2G + /swap2.img 2G,后者 pri=10,都写进 /etc/fstab)。
加第二个文件而不是扩容第一个:当时只有 ~390MB 可用内存,swapoff 会把在用的 1.5GB 换页挤回内存,
大概率 OOM。跑批时的峰值内存 1.6-2.6GB,多一档 swap 是保险。
内存水位与自动化无关:这台上常驻的是 singbox-relay(1.6 MiB)与三个一次性容器;把可用内存吃掉的
是 VS Code 远端与 pi 会话(约 2.7GB)。今早 08:00 微软积分开跑时实测 free 2806MB、clusters=2。
suite-check resources 会在宿主上打一份水位(镜像/构建缓存/内存/swap/根分区),只在真该清时提示。
B站任务的上游 Verbose 日志(2026-10-11 接入):容器内 Console 把日志写到 /app/Logs/log<日期>.txt
(Serilog File sink,rollingInterval: Day)。compose.yaml 已把 bilibili/logs/console 挂到该路径,
容器退出不丢;bilibili/run.sh 每次运行前清 30 天前的旧文件。任务「什么都没做」时先看它。
先量后调:24 小时均值 %idle 86.2、磁盘 %util 6.3 —— 这台机器平时很闲,尖峰来自 VS Code 的全盘
搜索与后台构建,不是自动化。所以只做了"隔离与保护",没有堆监控。
| 项 | 改前 | 改后 | 位置 |
|---|---|---|---|
vm.swappiness |
60 | 20 | /etc/sysctl.d/99-automation-tuning.conf |
net.ipv4.tcp_fastopen |
1(仅客户端) | 3(含服务端) | 同上 |
net.ipv4.tcp_max_syn_backlog |
256 | 1024 | 同上 |
| 根分区挂载 | relatime |
noatime |
/etc/fstab(备份 .bak-20261005) |
| 拥塞控制 | cubic | bbr(先 modprobe tcp_bbr 确认内核支持) |
/etc/sysctl.d/99-bbr.conf + /etc/modules-load.d/bbr.conf |
| 队列规则 | fq_codel | fq | 同上 |
| OOM 保护 | 无 | earlyoom(mem<5% 且 swap<5% 才动手;avoid sshd/systemd/dockerd/containerd,prefer code/node/chrome) | /etc/default/earlyoom |
| 自动化优先级 | Nice=5(比后台任务还低) |
Nice=0 + CPUWeight/IOWeight=200 |
automation-suite.service.d/10-priority.conf |
| 后台构建优先级 | Nice=0、无限制 |
Nice=10 + IOSchedulingClass=idle + IO/CPUWeight=50 + MemoryMax=1G |
blog-rebuild.service.d/10-priority.conf(drop-in,不动原 unit) |
| Docker 日志 | 10m×3 |
加 compress: true |
/etc/docker/daemon.json(备份 .bak-20261005) |
| 容器资源上限 | 无 | relay 256m/0.5cpu、blog 512m/0.5cpu | 各自 compose.yaml + 运行中 docker update |
| VS Code 搜索/监听排除 | 7 条 | 19 条(/usr /var /opt /snap /boot /tmp node_modules .next .cache .vscode-server …) |
~/.vscode-server/data/Machine/settings.json(备份 .bak-20261005c) |
两条实测踩坑记下来:
- 改
/etc/fstab的 sed 把选项写进了文件系统类型字段(/ ext4,noatime defaults 0 1),findmnt --verify能查出来。改 fstab 一律先备份再findmnt --verify。 EARLYOOM_ARGS里的引号在 heredoc 里写成了'字面量,earlyoom 只解析到-r 3600(其余参数全丢)。 写完要看/proc/<pid>/cmdline确认。
BBR 生效的验证:ss -tin state established 里能看到用 bbr 的连接;注意入向连接显示的是对端的算法,
要看本机作发送方的那条。
这台上真正吃内存的是编辑器的扩展宿主,不是自动化:
| 进程 | 实测 |
|---|---|
扩展宿主(VSCODE_EXTHOST_WILL_SEND_SOCKET=1) |
1.78 GB RSS + 2.59 GB swap,14 线程;两小时内从 1.34 GB 长到 1.78 GB |
| VS Code 全部 13 个进程 | 合计 1765 MB RSS |
用户 slice(user-1000.slice) |
~2.15 GB(编辑器 + pi + shell) |
| 自动化 | 常驻 0(一次性容器);跑批峰值 1.6-2.6 GB |
远端只装了 2 个扩展(containers + 中文包),没有 TS 语言服务 —— 所以 1.78 GB 属于累积而不是某个扩展本身。
内存账算不平:跑批峰值 2.6 GB + 编辑器 2.1 GB + 系统 0.5 GB = 5.2 GB,而机器只有 3.7 GB。所以
微软积分运行器自带的内存闸门(REWARDS_MIN_FREE_MB=1200)才是实际的保护 —— 编辑器占着内存时,那次跑批会
跳过(当天还有 12:00 兜底与 Persistent=true 补跑)。今早 08:00 实测 free 2806MB,闸门没拦。
已做的两件事:
user-1000.slice加MemoryHigh=2G(/etc/systemd/system/user-1000.slice.d/10-memory.conf): 超了就压它(回收/放慢),不杀进程。自动化是 system 服务,不在这个 slice 里,不受影响。 写入时该 slice 已到 2.15 GB —— 正好卡在上限。- 远端 settings 关掉后台churn:
telemetry.telemetryLevel=off、extensions.autoUpdate/autoCheckUpdates=false、remote.autoForwardPorts=false、update.mode=none;搜索/监听排除 19 条(见 7.6)。
其余可选项:重启远端服务(Remote-SSH: Kill VS Code Server on Host,立即回收 ~1.8 GB,窗口会重连)、
不用时禁用 containers 扩展(它在轮询 docker socket)、收窄工作区根(现在是 /,排除项只是缓解)、
试 NODE_OPTIONS=--max-old-space-size=1024(需实测远端服务是否尊重它)。
真源是 GitHub Zzz210s/note(分支 master),两端都按"远端为主"同步。
| 端 | 触发 | 脚本 | 动作 |
|---|---|---|---|
本机 F:\0-Note |
Windows 计划任务 NoteSync,每 5 分钟 |
scripts/note-sync/note-sync.sh |
pull --rebase --autostash → 只提交 2 分钟没被改过的文件 → 脱敏闸门 → 提交 → push(重试 3 次) |
服务器 /srv/apps/note/repo |
systemd note-sync.timer,每 5 分钟 |
/srv/apps/automation/note-sync.sh |
同上;拉取成功后博客重建按 sha 变化自动重建 |
配置:本机 ~/.note-sync/config.env;服务器复用 suite.env。冲突一律停下 + 企业微信通知,绝不 force。
四个坑(都实测踩过):
git commit默认提交整个索引 —— 脚本判定"刚被改过"而跳过的文件,仍被别的会话暂存着,于是被一起提交。 必须git commit -m ... -- "${READY[@]}",失败回滚也只用git reset -- "${READY[@]}"。- 未跟踪同名文件挡住 merge —— 另一个会话在服务器上新建了
app_static.py,远端也有同名文件, git 直接Aborting。脚本现在拉取前把这些文件移到备份目录(不是删除)再拉。 - github 在这台机器上时通时断 —— 只用
timeout不够(它杀的是直接子进程,git fetch子进程会活下来), 必须同时用 git 自己的低速超时:git -c http.lowSpeedLimit=1000 -c http.lowSpeedTime=30。 - HTTPS + gh 凭据会把 token 放进 URL ——
ps与 journald 里能看到https://x-access-token:gho_***@github.com/...。服务器侧已改为 deploy key + SSH (~/.ssh/id_ed25519_note,仓库 deploy keyserver-note-sync,写权限;core.sshCommand指定密钥)。
现象:ping 通(8ms)、22/443 端口开、relay 正常返回 200,但 SSH 认证卡死登不上;load 39(2 核), swap 4095/4095 全满。
根因:VS Code 远端扩展宿主一个进程占 3.45 GB swap(另有 1.1 GB RSS)—— 编辑器把交换空间吃干净了。
earlyoom 为什么没救:它的判据是"内存与 swap 同时低于阈值"(AND)。当时 swap 剩 0%,但内存还有 40%, 所以不触发。
处理:
| 动作 | 说明 |
|---|---|
撤掉 user-1000.slice 的 MemoryHigh=2G |
它只是加剧了压力(不是唯一原因),撤掉后恢复 infinity |
新增 mem-guard(每 5 分钟) |
swap 剩余 <10% 且 load > 2×核数 → 杀掉 vscode-server 里 RSS 最大的进程并通知;绝不碰 pi/tmux |
装 zram-tools(lz4,25%,pri=100) |
换页走压缩内存,thrash 不再致命 |
结果:swap 4095 → 500 MB,可用内存 1131 → 2438 MB,load 39 → 8。
教训:小内存机器上"编辑器 + 跑批"共存,必须有基于 swap/负载 的守卫;earlyoom 的 AND 判据覆盖不了 "swap 满了但内存还没到底"。另外:VS Code 扩展宿主会累积(实测两小时 1.34 → 1.78 GB), 长期开着远端窗口就会走到这一步 —— 不用时关窗口,或定期重启远端服务。
每个程序/用途一条独立通道,key 只落在服务器上的文件里(仓库外,600),文档只记路径。
| 通道 | 用途 | 文件 |
|---|---|---|
| Epic 限免 | 领取开始 / 结果 / 需要你处理 | /srv/apps/automation/epic/secrets/wecom-webhook.txt |
| 服务器 | 基础设施告警:内存守卫、note 同步冲突、部署与体检异常 | /srv/apps/automation/secrets/wecom-server.txt |
| 微软积分 | 不变 | /srv/apps/automation/rewards/wechat-bridge/data/wecom-webhook.txt |
| 微信读书签到 | 不变 | /srv/apps/automation/weread/secrets/wecom-webhook.txt |
本机侧的 0-Note 同步失败通知也走"服务器"通道(本机文件 ~/.note-sync/wecom-server.txt)。
改通道只需换文件内容,不用动代码:mem-guard.sh 用 MEM_GUARD_WEBHOOK、note-sync.sh 用
NOTE_WECOM_WEBHOOK、alert-fail.mjs 用内置的"程序 → 文件"表(含 服务器 这一项,可被别的脚本复用)。
提醒:webhook key 属于凭据。仓库是公开的,任何把 key 写进笔记/文档的动作都会被 push 前的脱敏闸门拦下 (规则名「企业微信 webhook 真 key」)。
早上两条消息:一条是程序自己的「需要你处理」(带商店页链接),紧接着又来了第二条兜底提醒 「运行在发出任何消息之前就退出了」—— 程序明明报过结果,却又被兜底逻辑补了一刀。
根因与修法(都有实测证据)
| # | 现象 | 根因 | 修法 |
|---|---|---|---|
| 1 | [警告] 会话注入失败 … Storage.setCookies: Invalid cookie fields |
浏览器单个 cookie 上限 4096 字节。容器内实测:value 4096 → 报错,4000 → 成功;而 Epic 的 access token 是 4529 字节 —— 这条注入路对它根本不成立,不是偶发失败 | inject.js 先量尺寸:超限就不开浏览器、直接返回 skipped,日志写清原因([登录] 跳过 token 注入:… 4529 字节 …) |
| 2 | 同一天连推两条(程序自己一条 + 兜底一条) | 兜底判据是「退出码非 0 且 日志里没有『企业微信』」,而 Epic 的 last-run.log 里根本没有『企业微信』字样(推送在子进程里)→ 必然误判 |
alert-fail.mjs 与三个 run.sh 都补上「日志里有 [完成] = 程序自己报过结果 → 不补发」;顺带修掉 fs.existsSync 传非字符串触发的 DEP0187 |
| 3 | 跑完并已推送,退出码却是 1 | run.js 用 summary.ok ? 0 : 1 —— 把业务结果「有未领取」当成了崩溃 |
退出码语义改为:跑完并已把结果推给人 = 0;真崩溃仍由 main() 的 catch 给 1。同时失败时把引擎输出末 15 行写进日志(原来引擎自己的报错根本不进 last-run.log,第二天无从判断原因) |
遗留的设计问题(没有动手改,先记录):EPIC_BEARER_TOKEN 这条注入路对 Epic 是死路(token 4529 字节)。
上游引擎本来也不靠它 —— vendor/free-games-claimer/epic-games.js 只注入两个小 cookie,登录态来自
持久化 profile。实测该 profile 里确实有会话(_epicSID、EPIC_EG1、REFRESH_EPIC_EG1),所以引擎能继续跑;
至于 10-06 这次为什么仍然「有未领取」,日志里没有任何线索 —— 这也是上面第 3 条要补引擎输出的原因。
真因(修完第 3 条留证功能后立刻抓到的):
browserType.launchPersistentContext: Executable doesn't exist at
/root/.cache/ms-playwright/chromium-1243/chrome-linux64/chrome
engineEnv() 返回的是一个全新对象,而 spawn(..., { env }) 是整体替换而不是合并 ——
于是引擎子进程丢掉了 PLAYWRIGHT_BROWSERS_PATH=0(浏览器装在包目录里,不在 ~/.cache/ms-playwright),
patchright 找不到浏览器,每天都「有未领取」。修法:engineEnv 先 ...process.env,显式覆盖项排在后面
(这样外部的 EG_PASSWORD 之类仍会被清空)。
修完实跑:浏览器正常启动,走到了结账页,然后上游自己报
Failed to claim! To avoid captchas try to get a new IP address. —— 等 It's all yours /
Download the Epic Games Launcher... / Is Epic Games Launcher installed? 三个成功标记超时。
这是机房 IP 被 Epic 反欺诈拦下,不是代码问题(上游在源码里也明说了)。
顺带做的两件:
- 公开历史脱敏:scripts-hub 历史里的真实昵称(
855972f/216779c/dbb36a2三个提交)用git filter-repo --replace-text清掉 → 全新克隆验证 0 残留,111 个提交保留;服务器上的克隆已重克隆 (本地克隆git fetch && git reset --hard origin/main对齐)。 - 提交前脱敏闸门:全局 pre-commit hook(
config-ai的host/git/hooks/pre-commit→~/.githooks/pre-commit), 只在仓库自带scripts/check-privacy.mjs时生效 —— 手动提交以前不经过闸门,这次就是这么漏的。 实测:暂存一个假密码 → 提交被拦下;逃生舱PRIVACY_GATE=0 git commit ...。
迁到云主机后,停掉本机的 Windows 计划任务(否则两边同一天都跑:微软账号会互相顶掉登录态, 微信读书的时长上报也会重复):
Disable-ScheduledTask -TaskName MicrosoftRewardsScript
Disable-ScheduledTask -TaskName WeReadSignIn想留兜底就先观察几天:确认云主机连续几天都跑成、企业微信每天都收到推送,再停本机任务。 智慧树 Autovisor 仍只在本机(需要图形会话),不受影响。
| 动作 | 命令 |
|---|---|
| 看今天跑没跑 | cat rewards/logs/last-run.state、cat weread/data/state.json |
| 手动补跑一次 | sudo systemctl start automation-suite.service |
| 暂停微信读书 | 容器外创建 weread/data/paused 文件(或 docker compose run --rm -T weread-run node src/index.js pause) |
| 升级微软积分 | cd rewards/src && git pull(或换上游版本 + git apply 补丁)→ docker build -t automation-rewards:local . |
| 升级微信读书 | 更新 weread/src(从本机工作区同步)→ docker build -f weread/Dockerfile -t automation-weread:local weread/ |
| 看容器有没有残留 | `docker ps -a |
| 日志占空间 | logs/ 里每次运行会轮转一份 previous-run.log;磁盘 48GB 空闲,暂时不用管 |
| 项 | Windows 本机 | 云主机(本文) |
|---|---|---|
| 触发 | 计划任务:登录后 + 08:00 起每 2 小时(14 小时窗口) | systemd timer:08:00 + 12:00,Persistent=true |
| 补跑 | 不补跑,靠窗口内多次触发 | 错过的触发开机后补跑 |
| 顺序 | 错峰时刻 + 同伴互查 | 编排脚本严格顺序 |
| 单实例锁 | logs\run.lock + 进程实况判残锁 |
flock(自动释放)+ 同伴用的 JSON 锁 |
| 关机避让 | 有(02:00 关机前 30 分钟不启动) | 无(云主机不关机) |
| 安静时段 | 有(20:00-23:00) | 微信读书保留;微软积分不看这个 |
| 程序形态 | 裸装 Node/Python | 容器(镜像本地构建) |
sudo install -m 755 scripts/linux/check-layout.sh /srv/apps/automation/check-layout.sh
/srv/apps/automation/check-layout.sh # 退出码 0 才算铺完它会硬查三件「一定让调度失败」的事:每个程序的 <程序>/run.sh 是否存在且可执行、run.sh 里的 docker compose run 是否断了 stdin、compose.yaml 里是否有对应服务;另外提示缺 Dockerfile 或缺状态目录的程序。
这条是补一个真实事故:贴吧与网易云部署时只铺了 deploy/、Dockerfile、src/ 与 verify.sh,漏了宿主入口 run.sh,于是 run-all.sh 每次调用都是退出码 127,suite.log 里只有一行「退出码 127」,看起来像程序自身故障,而 data/state.json 停在上一次手工验证的日期。同类前例是 B站的宿主入口丢了可执行位(退出码 126)。
一天的启动只有一条链路,不要在别处再加定时:
systemd automation-suite.timer(08:00 主力 + 12:00 兜底,Persistent=true)
-> automation-suite.service(Type=oneshot,User=ubuntu,TimeoutStartSec=infinity)
-> /srv/apps/automation/run-all.sh(flock 单实例)
-> 启动前布局体检(check-layout.sh):入口缺失/不可执行的程序当场跳过并提醒,不再拿退出码 127
-> 按 schedule.json 的 order 串行跑 6 个 <程序>/run.sh
-> 中途被打断(TERM/INT/HUP 或异常退出)时记一条中断日志并发套件级提醒
要点:
- 各程序的时刻只对 Windows 有意义(
programs.*.startTime那套是 Windows 计划任务的重复触发窗口)。Linux 侧是「一项跑完再下一项」,由run-all.sh的顺序保证,不靠错峰时刻去赌。 - 开关只有一处:
config/schedule.json的linuxEnabled(缺省回落enabled);run-all.sh用program-enabled.mjs读它,读不到时 fail-closed 只跑固定四程序并把贴吧/网易云排除。 - 12:00 那次是兜底:当天已完成的程序会一秒跳过,所以重复触发不产生重复动作。
- 核对方法:
/srv/apps/automation/check-layout.sh(布局)、node program-enabled.mjs list schedule.json(启用清单)、systemctl list-timers automation-suite.timer(下次触发),以及logs/suite.log里每天那两段「编排开始 / 编排结束」。
六个程序的 <程序>/run.sh 不再各抄一份骨架:轮转日志、flock 单实例、docker compose run
(断 stdin、锁 fd 不继承)、看门狗(先杀 sleep 再杀子 shell)、兜底提醒都在套件根目录的
run-lib.sh。单个 run.sh 只剩声明:程序名、目录、compose 服务名、看门狗超时。
run.sh 18 行:SUITE_DIR + PROG_* 四个变量 + source run-lib.sh + run_program "$@"
run-lib.sh 93 行:rp_prepare / rp_run_container / rp_report / run_program
<程序>/run.sh check 这类只读子命令仍可用 —— run_program "$@" 会把子命令透传进容器。
check-layout.sh 现在检查「run.sh 是否 source 了 run-lib.sh」以及「run-lib.sh 是否仍在位」,
不再逐个 grep </dev/null(那已经收进库了)。
服务器上 github.com:443 的 git 走不通(2026-10-29 实测 TLS 被中断),但 SSH 与 codeload 通。
统一入口 github.sh,手术级说明见 config-ai 的 docs/git-github-index.md:
/srv/apps/automation/github.sh status # 四条通路逐条探测
/srv/apps/automation/github.sh pull ~/config-ai # 公开仓走镜像、私有仓走 SSH服务器已生成账号级 SSH key 并加入 GitHub(tencent-cloud-automation-20261029),origin 已改为
git@github.com:...,所以 git pull / git push 直接可用,不需要任何代理。
时长目标的口径是全缺口:签到每天一次,时长不是每天领一点配额,而是一有空就把挑战总缺口补到 0。
所以它没有走 run-all.sh 的串行顺序 —— 那样会把后面的贴吧/网易云/Epic 全堵住。
weread-reader.timer:每 30 分钟触发一次weread/run.sh;一次运行内会连跑多个会话, 会话之间隔SESSION_GAP_MINUTES(默认 5 分钟),直到缺口补完、用完尝试次数或窗口关闭。- 让路:
BUSY_PEERS列出全部同伴锁(/peers/{rewards,epic,bilibili,tieba,netease}/run.lock), 任何同伴在跑,它就在两个会话之间直接退出(timer 半小时后再来),不抢 CPU 与网络。 - 窗口:
QUIET_START/QUIET_END(20:00-23:00)不读;DAILY_CAP_MINUTES默认 600 分钟兜底。 - 套件里
weread-signin的linuxEnabled已置 false,program-enabled.mjs is-enabled返回 1; 日报与福利仍由 reader 那次运行负责,通知按天去重,不会刷屏。 - 部署:
sudo cp scripts/linux/systemd/weread-reader.{service,timer} /etc/systemd/system/→sudo systemctl daemon-reload→sudo systemctl enable --now weread-reader.timer。
所有容器的启停判断都收进 scripts/linux/runner.sh(服务器上是 /srv/apps/automation/runner.sh),
systemd 只剩一个定时器 automation-runner.timer,每 30 分钟问它一次。
- 改程序看表:文件顶部的
PROGRAMS数组一行一个程序,字段依次是id | 显示名 | compose 服务 | 类别 | 钟点 | 星期 | 看门狗分钟 | 启用 | 一天几次。 加一个程序 = 加一行;临时停掉 = 把那行的启用改成 0。 - 类别:
daily是每日任务,一跳里按表顺序串行跑完(与旧run-all.sh一致);reader是补时长的活(微信读书),只有 daily 都没得跑时才跑,一次一个会话。 - 钟点:
08:00,12:00,19:00表示到点后两小时内补跑;00:00-19:59/30,23:00-23:59/30表示这两个区间内每 30 分钟一跳都算触发。 - 两道锁:
state/runner.lock保证同时只有一个控制进程;state/<id>.flock保证同一个程序 不会有两个容器并跑(名字与旧 run.sh 一致,手工调用也受同一把锁保护)。 - 自检:
./runner.sh --plan只打印这一跳会启动谁;./runner.sh weread强制跑某一个。 - 旧的
automation-suite.timer与weread-reader.timer已 disable,run-all.sh已标注停用, 想回滚把旧的 enable 回来即可。