zblog-article-publisher - 文章发布任务路由器
最高优先级要求
本文件不是完整执行手册,只负责判断文章发布任务类型,并选择对应的小技能文件。
收到文章发布任务后,必须先完成任务分流,然后只读取同目录下对应的一个小技能文件,严格按照该小技能文件执行。
禁止只根据本文件自行发布文章。
禁止把两类任务流程混用。
禁止把两个小技能全部读入上下文后再选择。判断任务类型只能根据用户输入和本文件规则完成。
每个发布任务开始时的第一步:先检查同目录 config.local.md 是否存在且包含 SITE / USERNAME / PASSWORD 三项必填值。不满足时立即执行「全局配置」章节的「首次配置流程」,向用户确认站点信息后再继续任务,禁止跳过检查直接发布,禁止用猜测值或示例值充当配置。
全局配置(唯一配置点)
站点与账号配置存放在技能目录下的 config.local.md(私有文件,不随技能公开分发),所有子技能和脚本从这里读取,任何文件中不得写死站点地址、用户名、密码。
首次配置流程(config.local.md 不存在或缺少必填项时,作为任务第一步执行)
- 一次性向用户询问以下信息,不得猜测、不得使用示例值:
- 站点地址(例如 https://example.com)
- 用户名
- 密码
- 发布格式:html 或 md(用户未明确时默认 html)
- 将用户提供的值写入技能目录下的
config.local.md,格式:
| 配置项 | 变量名 | 值 |
|---|---|---|
| 站点地址 | SITE | <站点地址> |
| 用户名 | USERNAME | <用户名> |
| 密码 | PASSWORD | <密码> |
| 发布格式 | POST_FORMAT | html |
- 写入后继续执行本次任务
约定
- API 地址固定为
{SITE}/zb_system/api.php - 子技能文档中的
{API}代表 API 地址(即{SITE}/zb_system/api.php),{SITE}代表站点地址 - 发布文章统一调用技能目录下的
publish.py,脚本自动从config.local.md读取配置(含 POST_FORMAT),代码里不得写具体网址和明文账号 - 上传图片等手动 API 操作沿用「通用代码开头模板」中的 SITE / USERNAME / PASSWORD 常量,API 常量由 SITE 拼接
- 发布格式 POST_FORMAT 只能是
html或md,发布时同样放入 Content 字段:html(默认):按各子技能排版章节要求生成 HTML 正文md:正文用 Markdown 语法书写(标题##、加粗**、引用>、列表、图片),子技能排版章节中 HTML 专属的图片居中样式和版权说明样式不再强制
- 修改站点、账号或发布格式时只改
config.local.md;用户说"重新配置"时按首次配置流程重新询问
API 操作规范(唯一技术手册)
所有子技能共用的 Z-Blog API 操作只在本节定义一次,子技能文件不得重复。子技能只补充本任务特有的发布前检查、配图/截图等任务要求。
通用要求
- 所有 API 操作统一使用 Python
requests.Session(),Session 自动管理 Cookie - HTTPS 请求默认走正常证书校验(verify=True);遇到 SSLError(自签名证书、本地代理环境)才降级
verify=False重试,并屏蔽 InsecureRequestWarning。不要一开始就全局 verify=False(公网正常站点应正常校验),本地 http 站点无此问题 - 除分类查询外,所有 API 请求的 token 都放在 POST body 中,不要放在 URL 参数中(URL 参数方式会返回 419"非法访问")
- 密码必须用变量拼接,禁止在代码中出现明文密码或
***字符串 - token 有时效性,过期后需重新登录
登录获取 token
POST {API}?mod=member&act=login
Content-Type: application/x-www-form-urlencoded
Body: username={USERNAME}&password={PASSWORD}(取值见上方全局配置)
返回: data.token
图片搜索与下载要求
- 搜索图片统一用 agent-browser 打开搜索引擎(必应/搜狗/百度等),浏览器自带正常 User-Agent
- 用 requests 直接下载候选图片或页面时,必须带浏览器 User-Agent 请求头,否则搜索引擎和图床会拒绝请求、返回验证页或异常内容:
UA = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'}
r = requests.get(图片URL, headers=UA, timeout=30)
- 下载完成后再做真实图片格式识别(
file命令或 PIL),HTML/验证页内容不得当作图片使用
上传图片
统一调用技能目录下的 upload_image.py(登录、预处理、auto_rename、解析 URL 都由脚本完成):
python upload_image.py 图1.jpg 图2.png
- 上传前自动预处理:宽度超过 600px 等比缩放到 600,统一转 WebP 格式(质量 80);已是 WebP 且宽度达标的不重编码
- 依赖 Pillow(
python -m pip install --user pillow) - 输出每行"图片路径 -> 站内URL (尺寸, image/webp)",多张上传时逐行记录图片与段落的对应关系。API 细节:
POST {API}?mod=upload&act=post
Content-Type: multipart/form-data
Body: 字段名 File(传图片二进制数据,显式三元组指定图片 Content-Type 如 image/jpeg)+ auto_rename=on + token(放在 POST body 中)
返回: data.upload.Url 即为站内图片地址,写入文章 HTML 用
- ⚠️ 必须显式指定图片 Content-Type(
files={'File': (文件名, 文件对象, 'image/jpeg')}三元组):本机 requests/urllib3 对两元组不自动补 type,缺失会导致后台附件"类型"列为空 - 必须带 auto_rename=on,由 Z-Blog 自动重命名为"日期+时间戳+随机数"文件名,客户端不得自定义文件名
- 不带 auto_rename 时按原文件名保存,同月内文件重名会返回 500"文件已存在"错误
通用代码开头模板(上传图片等手动 API 操作用)
分类查询、发布文章、上传图片已分别固化为技能目录下的 publish.py 和 upload_image.py(见下文),无需手写。仅其他手动 API 操作(如编辑文章)需要按此模板写代码:
import requests, urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# 全局配置常量(SITE/USERNAME/PASSWORD 取值见「全局配置」章节,API 由 SITE 拼接)
SITE = '<站点地址>'
API = SITE + '/zb_system/api.php'
USERNAME = '<用户名>'
PASSWORD = '<密码>'
# SSL 自适应:默认正常证书校验,SSLError(自签名证书/本地代理)才降级 verify=False 重试
def req(s, method, url, **kw):
try:
return s.request(method, url, verify=True, timeout=30, **kw)
except requests.exceptions.SSLError:
return s.request(method, url, verify=False, timeout=30, **kw)
# 标准开头:登录并获取 token
s = requests.Session()
lr = req(s, 'POST', f'{API}?mod=member&act=login',
data={'username': USERNAME, 'password': PASSWORD})
token = lr.json()['data']['token']
分类查询
**每次发布前都必须确定分类,不使用硬编码的分类 ID。**分类查询已内置在 publish.py 中,发布时按以下方式传分类:
- 用户已明确提供分类 ID →
--cate-id - 用户已指定分类名 →
--cate "分类名"(脚本自动查询并匹配,查不到时报错并列出所有现有分类) - 用户未指定分类 → 不传分类参数,脚本自动选「未分类」(不存在则选第一个分类)
- 多个分类都可能匹配或没有明显匹配分类 → 先询问用户,再用
--cate/--cate-id发布
发布文章
各任务的发布前检查清单见对应子技能文件,检查通过后统一调用技能目录下的 publish.py 发布(登录、分类查询、发布、前台自检都由脚本完成):
python publish.py --title "文章标题" --content-file body.html --cate "分类名" --tags "标签1,标签2" --intro "摘要"
参数:
- --title(必需):文章标题
- --content-file(必需):正文文件路径,内容为 HTML 或 Markdown 源码(按全局配置 POST_FORMAT)
- --cate / --cate-id:分类名 / 分类 ID(见「分类查询」)
- --tags:标签,英文逗号分隔
- --intro:摘要
- --format:html/md,不传则使用全局配置 POST_FORMAT
脚本成功后直接输出文章链接;失败时输出错误信息并退出。正文文件放本次任务的临时子目录,随清理删除。发布成功后按子技能要求做前台验证。
HTML 实体编码问题
通过 API(mod=post&act=get)获取文章内容时,返回的 HTML 是实体编码的(如 <h2> 而不是 <h2>)。如果需要基于现有内容做修改再提交,必须先用 html.unescape() 解码,否则会被双重编码导致页面显示异常。
# 获取文章内容后,必须先解码再使用
content = html.unescape(article['Content'])
# 如需多层解码(罕见情况),循环解码直到不再变化:
for _ in range(3):
new = html.unescape(content)
if new == content: break
content = new
通用失败处理
- 发布失败:记录错误 → 立即停止 → 不重试 → 告诉用户失败原因 → 等待用户决定是否修改后重试。发布失败后绝对不要自动重试,防止产生重复文章
- 登录失败:停止发布 → 告诉用户 API 鉴权失败 → 不猜密码、不爆破
图片处理失败的处理方式因任务而异,见各子技能文件。
清理
发布完成后清理本次任务产生的临时文件。
优先为每次文章发布创建独立临时子目录,例如:
当前工作区的 downloads/liubo_YYYYMMDD_HHMMSS/
清理要求:
- 只清理本次任务创建的文件或本次任务独立子目录
- 不得直接清空整个当前工作区的
downloads/目录 - 不得删除用户手动放入、其他任务产生、或无法确认归属的无关文件
默认清理:临时图片、临时HTML、临时脚本。
子技能文件
两个子技能文件必须和本文件放在同一个技能目录下。引用子技能时只使用同目录文件名,不写任何机器绝对路径。
- 标题任务:同目录下的
biaotirenwu-SKILL.md - 一段文章任务:同目录下的
yi-duanwenzhang-SKILL.md
任务类型判定
1. 一段文章任务
如果用户直接粘贴一段正文,让你发布、整理后发布、编辑后发布,且没有要求原创扩写,判定为一段文章任务。
常见表达:
- “把下面这段发到网站”
- “整理一下发布”
- “这篇文章帮我发出去”
- “编辑后发布”
- 用户直接粘贴正文内容
执行方式:只读取同目录下的 yi-duanwenzhang-SKILL.md,并严格按该文件执行。
2. 标题任务
如果用户只给标题、主题、关键词、写作方向,或者说“写一篇……发布到网站”,判定为标题任务。
常见表达:
- “写一篇关于……的文章发布”
- “帮我写一篇……”
- “以……为主题写文章”
- “关键词是……”
- “写一篇……发到我的网站”
执行方式:只读取同目录下的 biaotirenwu-SKILL.md,并严格按该文件执行。
冲突处理规则
如果一条任务同时像多个类型,按以下优先级判断:
一段文章任务 > 标题任务
但如果用户明确提出特殊要求,以用户明确要求为准。
例如:
- 用户给了正文,但说"在这个基础上扩写成一篇原创文章",应根据实际要求判定为标题任务或一段文章深度编辑任务;不确定时先问用户一次。
如果仍然无法判断,只允许问用户一次,不要自行猜测。
执行要求
判断出任务类型后,必须执行以下步骤:
- 只读取同目录下对应的一个小技能文件
- 禁止同时读取另一个小技能文件
- 禁止把两个小技能全部读入上下文后再选择
- 如果判断不清,只问用户一次,不要通过读取全部小技能来判断
- 读取对应小技能后,严格按该小技能文件里的完整流程执行
- 不得跳过小技能文件中的任何强制步骤
- 不得混用其他小技能流程
- 不得只根据本路由文件直接发布文章
- 发布、上传图片、编辑文章等网站数据变更,只在用户明确要求发布文章时执行
- 执行完成后,按小技能文件要求回复发布结果
文件缺失处理
如果对应的小技能文件不存在、无法读取,或内容明显不完整,必须停止执行,并告知用户缺少哪个文件。
不要凭记忆补流程,不要自行拼接其他技能流程。
微信扫一扫