背景
上一篇研究 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 个命令可用。下一篇会写批量给多篇文章加评论+回怼的循环调用封装。