什么是 Hermes

Hermes 是一个 AI 代理框架,可以通过飞书、Telegram、微信等平台跟你对话。它能操作终端、编辑文件、控制浏览器,甚至派出子代理并行工作。

今天我想说的是它的一个用法:自动写博客、发博客。

原理:Typecho 的 XML-RPC 接口

Typecho 自带一个 XML-RPC 接口,地址是 /index.php/action/xmlrpc。这个接口遵循 MetaWeblog 协议,支持以下操作:

方法功能
metaWeblog.newPost发布文章
metaWeblog.editPost编辑文章
metaWeblog.deletePost删除文章
metaWeblog.getRecentPosts获取最近文章
metaWeblog.newMediaObject上传图片/文件
metaWeblog.getCategories获取分类列表

Python 标准库里自带 xmlrpc.client,不需要安装任何第三方库,几行代码就能调用。

核心代码

发布一篇文章只需要三步:

1. 连接服务器

import xmlrpc.client
server = xmlrpc.client.ServerProxy('https://你的博客/index.php/action/xmlrpc')

2. 上传图片(可选)

with open('image.png', 'rb') as f:
    data = xmlrpc.client.Binary(f.read())
media = {'name': 'image.png', 'type': 'image/png', 'bits': data}
result = server.metaWeblog.newMediaObject(1, '用户名', '密码', media)
print(result['url'])  # 返回图片地址

3. 发布文章

post = {
    'title': '文章标题',
    'description': '文章内容(Markdown 或 HTML)',
    'categories': ['默认分类'],
    'mt_keywords': '标签1,标签2',
}
post_id = server.metaWeblog.newPost(1, '用户名', '密码', post, True)

第四个参数 True 表示立即发布,False 表示存为草稿。

完整脚本

以下是脱敏后的完整脚本,把顶部三个配置项改成你自己的就能用:

#!/usr/bin/env python3
# Typecho 博客自动发文章脚本 (XML-RPC)
#
# 用法:
#   python3 typecho-post.py --title "标题" --content "内容" [--category "分类"] [--tags "标签"] [--draft]
#   python3 typecho-post.py --list
#   python3 typecho-post.py --delete <文章ID>
#   python3 typecho-post.py --edit <文章ID> --title "新标题"
#   python3 typecho-post.py --upload <文件路径>

import xmlrpc.client
import argparse
import os

BLOG_URL = 'https://你的博客/index.php/action/xmlrpc'
BLOG_ID = 1
USERNAME = '你的用户名'
PASSWORD = '你的密码'

def get_server():
    return xmlrpc.client.ServerProxy(BLOG_URL)

def post_article(title, content, category=None, tags=None, draft=False):
    server = get_server()
    post = {'title': title, 'description': content}
    if category:
        post['categories'] = [category]
    if tags:
        post['mt_keywords'] = tags
    publish = not draft
    post_id = server.metaWeblog.newPost(BLOG_ID, USERNAME, PASSWORD, post, publish)
    status = "草稿" if draft else "已发布"
    print(f"成功! ID: {post_id}")
    return post_id

def list_posts(number=10):
    server = get_server()
    posts = server.metaWeblog.getRecentPosts(BLOG_ID, USERNAME, PASSWORD, number)
    for p in posts:
        print(f"  [{p['postid']}] {p['title']}")

def edit_post(cid, title=None, content=None):
    server = get_server()
    post = server.metaWeblog.getPost(cid, USERNAME, PASSWORD)
    if title: post['title'] = title
    if content: post['description'] = content
    server.metaWeblog.editPost(cid, USERNAME, PASSWORD, post, True)
    print(f"编辑成功! ID: {cid}")

def delete_post(cid):
    server = get_server()
    server.blogger.deletePost('', cid, USERNAME, PASSWORD, True)
    print(f"删除成功! ID: {cid}")

def upload_media(file_path):
    server = get_server()
    with open(file_path, 'rb') as f:
        data = xmlrpc.client.Binary(f.read())
    ext = os.path.splitext(file_path)[1].lower()
    mime = {'.jpg':'image/jpeg','.png':'image/png','.gif':'image/gif',
            '.webp':'image/webp','.mp4':'video/mp4'}.get(ext, 'application/octet-stream')
    media = {'name': os.path.basename(file_path), 'type': mime, 'bits': data}
    result = server.metaWeblog.newMediaObject(BLOG_ID, USERNAME, PASSWORD, media)
    print(f"上传成功: {result['url']}")
    return result['url']

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--title', '-t')
    parser.add_argument('--content', '-c')
    parser.add_argument('--category')
    parser.add_argument('--tags')
    parser.add_argument('--draft', '-d', action='store_true')
    parser.add_argument('--list', '-l', action='store_true')
    parser.add_argument('--number', '-n', type=int, default=10)
    parser.add_argument('--edit', '-e', type=int)
    parser.add_argument('--delete', type=int)
    parser.add_argument('--upload', '-u')
    args = parser.parse_args()

    if args.list:
        list_posts(args.number)
    elif args.delete:
        delete_post(args.delete)
    elif args.edit:
        edit_post(args.edit, title=args.title, content=args.content)
    elif args.upload:
        upload_media(args.upload)
    elif args.title and args.content:
        post_article(args.title, args.content, args.category, args.tags, args.draft)
    else:
        parser.print_help()

if __name__ == '__main__':
    main()

和 Hermes 配合的用法

脚本只是工具,Hermes 做的是把整个流程自动化:

  1. 你:说"帮我发一篇关于 XXX 的博客"
  2. Hermes:回顾上下文、撰写文章内容
  3. Hermes:用浏览器自动截图作为配图
  4. Hermes:调用脚本上传图片,拿到 URL
  5. Hermes:调用脚本发布文章,返回链接给你

全程不需要你打开浏览器、登录后台、手动操作任何东西。这篇文章就是这么发出来的。

注意事项

  • 图片上传用 metaWeblog.newMediaObject,不要用 wp.newMediaObject,Typecho 不支持后者
  • 文章内容格式取决于后台设置:开启了 Markdown 解析就传 Markdown,否则传 HTML
  • 密码明文写在脚本里,注意文件权限(chmod 600),别提交到公开仓库
  • XML-RPC 接口默认开启,如果关了可以在 Typecho 后台或源码中重新启用

本文由 Hermes Agent 自动撰写、截图、上传图片并发布。