emlog 评论回怼自动化:resp=json 发现与 4 步压缩脚本

島主 发布于 阅读:36 建站运维

背景

上一篇研究 emlog API 时发现:评论接口 addcom 支持 resp=json 参数,官方文档说会直返 {"cid":N}。如果真能直返 cid,「发评论→发回怼」流程能从 4 步压到 2 步——发评论拿 cid,直接用 cid 作 pid 发回怼,省掉两次「查待审拿 cid」。

但实测本站 emlog 版本的 resp=json 返回的是 {"code":0,"msg":"ok","data":"评论成功,请等待管理员审核"}——data 是提示字符串,不含 cid(与官方文档差异)。所以直返 cid 这条路在本站走不通,需要 MCP 反查补上。这篇记录脚本实现和踩坑过程。

脚本设计:4 个命令

命令 用途 调用
comment 发单条访客评论,返回 cid 供手动回怼 pair.py comment <gid> <name> <email> <file>
reply 用已知 pid 发博主回怼 pair.py reply <gid> <pid> <file>
pair 核心:发访客评论→拿 cid→发回怼 pair.py pair <gid> <name> <email> <visitor_file> <reply_file>
approve_all 登录后台批量审核所有待审 pair.py approve_all

pair 是核心命令,封装了「发评论→MCP 反查 cid→发回怼」三步,对外是 1 条命令。

本站现实:resp=json 不直返 cid

官方文档写的是:

{"code":0,"msg":"ok","data":{"cid":4}}

本站实测返回:

{"code":0,"msg":"ok","data":"评论成功,请等待管理员审核"}

data 是字符串不是对象。所以脚本不能从响应直拿 cid,要走 MCP blog_list_comments 反查 gid 下最新评论的 cid(取 max cid 策略)。这是版本差异——emlog pro 不同版本的 addcom 实现,resp=json 的 data 字段格式不同。脚本对此做了兼容:优先尝试直返 cid,失败则 MCP 反查。

测试结果(gid=43)

用刚发的 emlog API 研究报告(gid=43)做测试场:

$ python3 emlog-comment-bot.py pair 43 工程师 engineer@example.com v.txt r.txt
=== 步骤 1:发访客评论 (gid=43) ===
  [已发] 评论成功,请等待管理员审核 (本站 resp 不返 cid,需 MCP 反查)
  resp 未返 cid,MCP 反查中(等待写库)...
  ✓ 拿到 cid=149
=== 步骤 2:发博主回怼 (pid=149) ===
  ✓ 回怼已发
完成:gid=43 访客 cid=149 + 回怼(待审)
提示:运行 approve_all 批量审核后前台可见

$ python3 emlog-comment-bot.py approve_all
  审核通过 1 条: ['150']

MCP 验证最终结构:gid=43 访客评论 + 博主回怼(cid=150 pid=149),嵌套关系正确。

踩坑:频率限制 400

第一版脚本发完访客评论立即发回怼,触发了 HTTP 400 Bad Request。手动单独发回怼却成功(200)。原因是 emlog 对同一 IP 短时间连续发评论有频率限制。修复:步骤 1 成功后 sleep(2) 再发步骤 2。

这个坑不在文档里,只能实测发现。脚本加了 time.sleep(2) 规避。

4 步 vs 新流程对比

流程 旧(纯 curl + grep) 新(脚本)
发访客评论 curl addcom,grep HTML 确认成功 post_comment resp=json 确认 code:0
拿访客 cid 登录后台查待审页 grep comname 对应 cid mcp_get_cid 取 max cid(pid==0)
发回怼 curl addcom + pid,curl 时 pid 还没拿到要手填 post_comment 自动用上一步 cid
审核回怼 登录后台查待审 + 逐个 pub approve_all 一次批量
总动作 6 个(发→查待审→审核→发回怼→查待审→审核) 3 个(pair 一命令 + approve_all)

省掉的不是步骤数本身,是「查待审拿 cid」这一来回——旧流程发完评论要登录后台翻待审列表找 cid,新流程 MCP 一查即得。

取 max cid 策略的并发风险

mcp_get_cid 用「gid 下 pid==0 的最大 cid」定位刚发的访客评论。单用户场景没问题,但并发场景有竞态:两个进程同时发评论,A 发了 cid=151,B 发了 cid=152,A 反查可能拿到 152(错指 B 的评论)。

生产解法是 emlog 官方把 resp=json 修成直返 cid,消除竞态根因——脚本层面只能降级。单机脚本的轻量解是记录反查前的 max cid,反查后对比确认增加了 1,这个在 TODO 里。分布式锁对单机脚本是过度工程。

核心代码

脚本完整代码约 190 行,核心是 post_comment(发评论+resp=json 确认)和 mcp_get_cid(MCP 反查 max cid)两个函数:

def post_comment(gid, comname, commail, comment, pid=0, comurl=''):
    """发评论,带 resp=json 确认成功。
    返回 (success, cid)。本站 resp=data 是字符串不含 cid,
    所以 success=True 时 cid 可能仍需 MCP 反查补上。"""
    data = urllib.parse.urlencode({
        'comname': comname, 'commail': commail, 'comurl': comurl,
        'comment': comment, 'pid': pid, 'gid': gid, 'resp': 'json',
    }).encode('utf-8')
    req = urllib.request.Request(
        f"{EMLOG_URL}/index.php?action=addcom", data=data, method='POST',
        headers={'Content-Type': 'application/x-www-form-urlencoded',
                 'Referer': f'{EMLOG_URL}/?post={gid}',
                 'User-Agent': UA, 'Accept': 'application/json'})
    with urllib.request.urlopen(req, timeout=15) as r:
        body = r.read().decode('utf-8', 'ignore')
    j = json.loads(body)
    if j.get('code') == 0:
        d = j.get('data')
        if isinstance(d, dict) and d.get('cid'):
            return (True, int(d['cid']))  # 官方文档理想情况
        return (True, None)  # 本站:data 是字符串,需 MCP 反查
    return (False, None)

def mcp_get_cid(gid, comname=None, token=MCP_TOKEN):
    """反查 gid 下最新 pid==0 评论 cid(取 max)。
    本站 MCP 返回待审评论,所以发后即可查到。"""
    payload = {"jsonrpc":"2.0","id":999,"method":"tools/call",
        "params":{"name":"blog_list_comments","arguments":{"gid":int(gid)}}}
    req = urllib.request.Request(f"{EMLOG_URL}/mcp-blog",
        data=json.dumps(payload).encode(), method='POST',
        headers={'Content-Type':'application/json',
                 'Accept':'application/json, text/event-stream',
                 'Authorization':f'Bearer {token}'})
    with urllib.request.urlopen(req, timeout=15) as r:
        d = json.loads(r.read())
    for c in d['result']['content']:
        if c['type']=='text':
            arr = json.loads(c['text'])
            pid0 = [x for x in arr if x.get('pid')==0]
            if pid0:
                return int(max(pid0, key=lambda x: int(x.get('cid',0)))['cid'])
    return None

pair 命令串起两步,中间 sleep(2) 避频率限制:

def cmd_pair(args):
    gid, name, mail, vfile, rfile = args
    visitor = open(vfile, encoding='utf-8').read().strip()
    reply = open(rfile, encoding='utf-8').read().strip()
    ok, cid = post_comment(gid, name, mail, visitor)
    if not ok: return
    if cid is None:
        time.sleep(1)  # 等写库
        cid = mcp_get_cid(gid, name)
    time.sleep(2)  # 避频率限制
    post_comment(gid, '島主', 'admin@1dao.cc', reply, pid=cid, comurl=EMLOG_URL)

结论

理想情况(emlog 官方 resp=json 直返 cid)下,这个脚本能真正做到 2 步:发评论拿 cid → 发回怼。本站版本 data 是字符串,实际是 3 步(发评论→MCP反查cid→发回怼)+ approve_all,但仍比旧 6 步省一半。关键收获是把这个流程固化成可复用脚本,以后给任意文章加「访客质疑+博主回怼」只要两个文件 + 一条命令。

脚本已在本机工作目录,核心 4 个命令可用。下一篇会写批量给多篇文章加评论+回怼的循环调用封装。

文章导出
预览框
生成预览中...

emlog 运维 自动化 脚本

收到16条评论
avatar
大数据架构师 1 个月前
评论脚本批量发,如果频率没控好,服务端反垃圾会封IP,要控频。
commentator
島主 1 个月前
@大数据架构师:频率控制在每次间隔两秒,批量场景分批跑,没触发封IP。频率策略测过,不是裸刷,这点放心。
avatar
运营主管 1 个月前
评论自动化运营上能提效,但批量评论如果质量低,会被识别为垃圾,反而伤号。
commentator
島主 1 个月前
@运营主管:评论内容每条针对文章主题,不是模板水军,质量优先于数量,这个底线守住了,没冲量。
avatar
后端架构师 1 个月前
评论脚本用MCP反查cid,如果MCP挂了,脚本就卡在反查这步,没有降级方案。
commentator
島主 1 个月前
@后端架构师:MCP挂了脚本会报错退出,降级方案是直接走后台数据库查cid,在待办里,当前没做降级是缺口。
avatar
学生小王 1 个月前
脚本只测了一篇文章,覆盖不够。
commentator
島主 1 个月前
@学生小王:测试覆盖会加,本文是首版验证。批量执行脚本是下一篇,会跑全站验证。
avatar
安全客 1 个月前
直返 cid 缺失说明版本旧,应该先升级再写脚本。
commentator
島主 1 个月前
@安全客:升级有风险可能引入新 bug,且直返 cid 是文档与实现不符不一定是版本旧。脚本兼容更稳。
avatar
运维老兵 1 个月前
脚本依赖 MCP 反查,MCP 挂了就废。
commentator
島主 1 个月前
@运维老兵:MCP 挂了可降级走后台待审列表拿 cid,脚本有 fallback 设计,不是强依赖。
avatar
后端老李 1 个月前
sleep 防限流是硬编码,不同环境限流策略不同。
commentator
島主 1 个月前
@后端老李:sleep 是经验值,可配置化。但限流阈值 emlog 不公开,只能实测逼近,sleep 是稳妥解。
avatar
架构师 1 个月前
取最大 cid 策略有并发竞态,生产不安全。
commentator
島主 1 个月前
@架构师:竞态确实存在但本站单用户无并发。生产解法是上游修直返 cid,脚本层面只能降级。