目录
[七、QQ 通知](#七、QQ 通知)
[八、常驻运行与 2G 内存错峰](#八、常驻运行与 2G 内存错峰)
本文面向需要在自有云主机上运行抖音评论自动回复的开发者,解决个人开发者拿不到官方 item.comment 权限、又不想维护小程序审核链路的问题。方案基于无头 Chrome 读取自己作品下的评论,用页面上下文接口回评,DeepSeek 生成回复,NapCat 把异常和结果推到 QQ。
一、为什么不用官方评论接口
抖音开放平台的 item.comment 权限要求应用具备企业或站点资质,个人开发者通常无法创建网站应用。小程序方案虽然能拿到 ma.item.comment,但需要额外的审核、包体和账号体系,维护成本远高于一个自用机器人。
在"只处理自己作品、回复量不大、可接受人工处理滑块"的前提下,浏览器自动化是更现实的路径:
- 用无头 Chrome 登录网页版,读取作品评论和楼中楼;
- 在页面上下文里调用评论接口,复用页面已有的签名;
- 用本地 SQLite 做去重、配额和审计;
- 遇到滑块验证立即停止写操作,转人工处理。
代价是依赖网页结构、需要维护登录态,并且平台风控会不定期触发验证。这一点会在后文的踩坑部分展开。
二、整体架构

整条链路分为六块:
douyin-runner:systemd常驻,默认 180 秒轮询一次,负责调度采集、筛选、生成和发送。dsh-douyin-watch:浏览器自动化插件,提供work-comments.mjs、comment-api.mjs、comment-delete.mjs等命令行工具,Chrome 通过CDP 9334暴露。SQLite + triage:保存作品、评论、回复记录,按comment_id去重,并过滤自己发的、已回过的、观众互聊的评论。DeepSeek:只根据评论上下文生成 10--40 字回复,失败时走固定模板。NapCat:本地 OneBot HTTP 服务,负责把通知发到指定的 QQ。dsh-web + Remotion:预留给视频生成和渲染,评论任务和渲染任务错峰使用同一台 2C2G 机器。
数据流是:网页评论 → work-comments → SQLite → triage → DeepSeek → comment-api → 回评;异常、高风险评论和发送结果另走 NapCat → QQ。
三、服务器与基础环境
测试环境为阿里云 2 vCPU / 2 GB 内存 / 40 GB 磁盘,Ubuntu 24.04。2 GB 内存跑 Chrome 比较紧,先补 2 GB swap。
bash
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
安装 Node.js 24、Chrome、Docker、中文字体和 ffmpeg:
bash
curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
apt-get install -y nodejs google-chrome-stable docker.io ffmpeg fonts-noto-cjk
NapCat 使用 Docker 运行,只监听本机端口:
bash
docker run -d --name napcat-douyin \
-e ACCOUNT=<QQ号> \
-p 127.0.0.1:5701:5700 \
-p 127.0.0.1:6100:6099 \
-v /opt/napcat-douyin/config:/app/napcat/config \
-v /opt/napcat-douyin/qq:/app/.config/QQ \
--restart unless-stopped \
mlikiowa/napcat-docker:latest
QQ 登录需要扫码,登录态保存在挂载目录里。容器重启后通常可以自动恢复,但会话失效时仍要重新扫码。
四、评论采集与去重

评论由 work-comments.mjs 采集。它会在已登录的页面里读取作品列表、根评论和每条根评论下的回复,并返回结构化的 JSON。核心字段包括 cid、uid、nick、text、create_time,以及"我是否已经回过"的判断结果。
本地用 SQLite 保存三类数据:
sql
CREATE TABLE comments (
cid TEXT PRIMARY KEY,
aweme_id TEXT,
root_cid TEXT,
parent_cid TEXT,
depth INTEGER DEFAULT 0,
uid TEXT,
nick TEXT,
text TEXT,
create_time INTEGER DEFAULT 0,
answered_by_me INTEGER DEFAULT 0,
risk TEXT DEFAULT '',
seen_at TEXT
);
CREATE TABLE replies (
target_cid TEXT PRIMARY KEY,
aweme_id TEXT,
root_cid TEXT,
target_uid TEXT,
target_nick TEXT,
text TEXT,
our_cid TEXT,
created_at TEXT,
status TEXT DEFAULT 'sent'
);
cid 是主键,天然去重。即使某次采集重复返回同一条评论,也只会更新状态,不会重复回复。
候选评论的筛选条件如下:
sql
SELECT c.* FROM comments c
WHERE c.answered_by_me = 0
AND c.risk = ''
AND c.uid != :own_uid
AND NOT EXISTS (SELECT 1 FROM replies r WHERE r.target_cid = c.cid)
ORDER BY c.create_time ASC;
这里有三层保护:不回复自己、不重复回复、不碰已标记风险的评论。楼中楼还会额外判断父评论或根评论是否属于自己,避免在观众之间的对话里插话。
五、风控与回复策略
回复策略以"短、具体、不引流"为原则。发送前会做一次清洗:
- 去掉 URL;
- 去掉
@,避免误触发提醒; - 压缩空白字符;
- 超过 60 字直接截断;
- 生成失败时使用固定模板兜底。
限额只按用户计算:同一用户 24 小时内最多 100 条,不设全局上限和小时上限。查询逻辑如下:
python
def per_user_count(conn, uid, hours=24):
cutoff = (datetime.now(timezone.utc) - timedelta(hours=hours)).isoformat(timespec="seconds")
row = conn.execute(
"SELECT COUNT(*) AS n FROM replies WHERE target_uid=? AND created_at>=?",
(str(uid), cutoff),
).fetchone()
return int(row["n"] if row else 0)
发送间隔随机 3--8 秒,避免在短时间内形成明显的固定节奏。评论内容命中广告、引战、敏感词等规则时只通知、不自动回复。
六、楼中楼的正确回法
楼中楼最容易踩的坑是评论 ID 用错。接口需要两个 ID:
reply_id:根评论的cid;reply_to_reply_id:真正要回复的那条子评论的cid。
如果两个参数都填子评论 cid,接口可能返回成功,但服务端不会把回复挂到目标位置。桥接层按下面的方式组装参数:
python
args = ["--aweme", aweme_id, "--file", text_file]
if root_cid:
args.extend(["--reply", root_cid])
if target_cid and target_cid != root_cid:
args.extend(["--reply-to-reply", target_cid])
发送时在页面上下文里执行 fetch,复用页面已有的签名和 Cookie,不直接调用外部 HTTP 接口。下面的截图来自创作者中心评论管理页,展示了根评论、一级回复和楼中楼三层结构(昵称和头像已打码):

七、QQ 通知
通知走新服务器本地的 NapCat,不和 B 站机器人共用 QQ。Python 侧只需要向 127.0.0.1:5701 发一个 send_private_msg:
python
body = json.dumps({
"user_id": int(target_qq),
"message": [{"type": "text", "data": {"text": text[:1500]}}],
}, ensure_ascii=False).encode("utf-8")
req = request.Request(
"http://127.0.0.1:5701/send_private_msg",
data=body,
headers={"Authorization": f"Bearer {onebot_token}", "Content-Type": "application/json"},
method="POST",
)
OneBot 端口和 WebUI 端口都只绑定 127.0.0.1,通过 SSH 隧道访问,不直接暴露公网。Token 放在 .env 里,权限设为 600,日志里不打印 Token 和 Cookie。
八、常驻运行与 2G 内存错峰
douyin-runner 用 systemd 管理,限制内存并降低调度优先级:
ini
[Service]
Type=simple
User=douyin
WorkingDirectory=/opt/douyin-operator
EnvironmentFile=/opt/douyin-operator/.env
ExecStart=/usr/bin/python3 /opt/douyin-operator/scripts/runner.py --interval 180 --notify-risk
Restart=on-failure
RestartPreventExitStatus=2
MemoryHigh=800M
MemoryMax=1000M
Nice=10
RestartPreventExitStatus=2 用于滑块验证:脚本以状态码 2 退出后,systemd 不会无限重启。
2 GB 内存同时跑 Chrome、NapCat、dsh-web 和渲染任务会非常紧张,因此评论和渲染必须错峰:

渲染前停止评论和网页相关服务,只保留必要的系统进程:
bash
systemctl stop douyin-runner dsh-web
pkill -f work-comments.mjs
pkill -f '/opt/google/chrome/chrome'
渲染时固定单并发:
bash
npx remotion render <入口文件> <输出文件> --concurrency 1
渲染完成后再依次启动 dsh-web、Chrome 和 douyin-runner。评论不会丢,暂停期间的评论会在恢复后按轮询补回,只是通知会延迟。
九、实际运行效果
创作者中心内容管理页可以看到已发布作品和播放、评论、收藏等数据。下面的截图来自真实环境,账号信息已打码:

评论管理页可以按作品查看评论、回复和删除。根评论、一级回复、楼中楼都会在同一页面展示,方便核对机器人是否回复到了正确的位置。
十、踩坑记录
- 无头 Chrome 的 UA 带
HeadlessChrome:评论写接口会返回200和空响应,看起来成功、实际没发出去。发送前需要把 UA 覆盖为普通 Chrome。 comments: null被当成失败 :没有评论时接口返回null,早期脚本会报错退出。现在把total = 0视为正常空列表。- Node 进程不退出 :部分工具执行完后仍保持 CDP 连接,导致轮询挂起。工具结束时应显式
process.exit。 - 楼中楼 ID 用错 :两个参数都填子评论
cid时,接口返回成功但服务端不落点。必须分别传根评论和子评论 ID。 - 2 GB 内存下同时加载多个重页面:Creator Center 和视频页同时打开会让内存和 CPU 迅速打满,SSH 也可能短暂无响应。做法是一次只加载一个页面,先停掉 dsh-web,再重启 Chrome。
- 滑块验证 :
work-comments.mjs会返回verify_required。此时不要继续自动回复,切换到有头浏览器人工完成验证,再恢复轮询。 - Cookie 过期 :网页登录态失效后所有采集都会失败。Cookie 文件按密码级凭据管理,权限
600,不要提交到 Git。
十一、安全与维护
- 评论 Cookie、OneBot Token、DeepSeek Key 全部放在
.env,文件权限600; - 所有管理端口只监听
127.0.0.1,通过 SSH 隧道访问; - 发送前做内容清洗,不输出 URL、不
@任何人; - 同一用户 24 小时最多 100 条,不设全局上限;
- 高风险评论只通知、不自动回复;
- 定期备份
douyin.db,并检查operator.log中是否有连续失败; - 平台风控策略会变化,浏览器自动化方案需要持续维护。
十二、小结
这套方案的核心不是"绕过接口权限",而是把官方接口不可用的部分换成可控的浏览器自动化,再用 SQLite、triage 和限额把风险收住。2C2G 的机器可以跑通评论链路,但必须把渲染和评论错峰,并接受滑块验证需要人工处理一次的现实。
标签:抖音 Chrome CDP DeepSeek NapCat SQLite 自动化
背景说明:本文基于自建抖音账号的评论回复实践整理,截图中的昵称、头像和账号信息已做打码处理;公网 IP、QQ 号和 Token 已隐去,请替换为自己的环境。