commit 90d4ab8c397256f9dea1a6d93f2f9c275d2eef34 Author: qwenpaw-skills Date: Sun Aug 9 00:36:04 2026 +0000 Publish emlog via gitea-publish skill diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..e44ef40 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,86 @@ +--- +name: emlog +description: 发布文章到 emlog 博客。当用户提到「发文章」「发布到博客」「blog」「emlog」「article_post」「微语笔记」等时使用。基于 emlog REST API,支持文章发布、文章列表、分类查询、草稿管理、微语笔记等功能。 +--- + +# emlog 博客发布 Skill + +通过 emlog REST API 与博客系统交互,支持发布文章、查询文章、获取分类、管理草稿、发布微语等。 + +## 前置条件 + +- 博客需开启 API 功能(后台:系统-设置-API 设置) +- 需要 API 秘钥(后台可查看) + +## 环境变量 + +| 变量 | 说明 | 默认值 | +|------|------|--------| +| `EMLOG_BASE_URL` | 博客站点地址,如 `https://blog.apescale.com` | `https://blog.apescale.com` | +| `EMLOG_API_KEY` | API 秘钥(后台系统-设置-API 设置) | 无 | + +## 使用方式 + +```bash +# 发布文章(推荐:内容较长时用文件传参) +python3 scripts/emlog.py post \ + --title "文章标题" \ + --content-file /tmp/article.md \ + --sort-id 14 \ + --tags "QwenPaw,TencentDB" + +# 发布文章(内容较短时直接传) +python3 scripts/emlog.py post --title "标题" --content "正文内容" --sort-id 14 + +# 获取分类列表 +python3 scripts/emlog.py sorts + +# 文章列表(可按分类过滤) +python3 scripts/emlog.py list --sort-id 14 --count 10 + +# 文章详情 +python3 scripts/emlog.py detail --id 123 + +# 草稿列表 +python3 scripts/emlog.py drafts + +# 发布微语笔记 +python3 scripts/emlog.py note --text "这是一条微语" +``` + +## 鉴权方式 + +脚本默认使用**免签名鉴权**(传 `api_key` 参数),简单可靠,建议配合 HTTPS 使用。 +如需签名鉴权,可传 `--sign` 参数(自动计算 `md5(req_time + api_key)`)。 + +## 发布参数说明(article_post) + +| 参数 | 必填 | 说明 | +|------|------|------| +| `title` | ✅ | 标题 | +| `content` | ✅ | 内容(支持 Markdown,emlog 会保存 raw 原文) | +| `excerpt` | ⬜ | 摘要 | +| `sort_id` | ⬜ | 分类 ID(`emlog.py sorts` 可查) | +| `tags` | ⬜ | 标签,半角逗号分隔 | +| `draft` | ⬜ | `y` 存草稿 / `n` 发布(默认 `n`) | +| `post_date` | ⬜ | 发布时间 `2026-08-08 12:00:00` | +| `alias` | ⬜ | 文章别名 | +| `top` | ⬜ | 首页置顶 `y`/`n` | +| `allow_remark` | ⬜ | 允许评论 `y`/`n` | + +## 常见错误 + +| 返回 msg | 含义 | 处理 | +|----------|------|------| +| `sign error` | 签名错误 | 检查 API 秘钥 / 时间戳 | +| `api is closed` | 未开启 API | 后台系统-设置-API 设置开启 | +| `API function is not exist` | 接口名错误 | 检查 `rest-api` 参数 | +| `parameter error` | 必填参数缺失 | 检查 title/content | + +## 返回格式 + +```json +{ "code": 0, "msg": "ok", "data": { "article_id": 14 } } +``` + +`code=0` 表示成功,`code=1` 表示失败(看 `msg`)。 diff --git a/scripts/emlog.py b/scripts/emlog.py new file mode 100644 index 0000000..0104c81 --- /dev/null +++ b/scripts/emlog.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +"""emlog REST API 客户端 — 发布文章/查询/分类/草稿/微语。 + +用法示例: + python3 emlog.py post --title "标题" --content "正文" --sort-id 14 --tags "a,b" + python3 emlog.py post --title "标题" --content-file /tmp/a.md --sort-id 14 + python3 emlog.py sorts + python3 emlog.py list --sort-id 14 --count 10 + python3 emlog.py detail --id 123 + python3 emlog.py drafts + python3 emlog.py note --text "微语内容" + +鉴权: + 默认免签名(api_key);加 --sign 走签名鉴权 md5(req_time + api_key) +环境变量: + EMLOG_BASE_URL 默认 https://blog.apescale.com + EMLOG_API_KEY API 秘钥 +""" +import argparse +import hashlib +import json +import os +import sys +import time +import urllib.parse +import urllib.request +import urllib.error + +BASE_URL = os.environ.get("EMLOG_BASE_URL", "https://blog.apescale.com") +API_KEY = os.environ.get("EMLOG_API_KEY", "") + + +def request(rest_api, params, method="POST"): + url = f"{BASE_URL}/?rest-api={rest_api}" + data = urllib.parse.urlencode(params).encode("utf-8") + req = urllib.request.Request(url, data=data, method=method) + try: + with urllib.request.urlopen(req, timeout=30) as resp: + return json.loads(resp.read().decode("utf-8")) + except urllib.error.HTTPError as e: + body = e.read().decode("utf-8", "ignore") + try: + return json.loads(body) + except Exception: + return {"code": 1, "msg": f"HTTP {e.code}: {body}"} + except Exception as e: + return {"code": 1, "msg": str(e)} + + +def build_auth_params(args): + p = {} + if args.sign: + t = str(int(time.time())) + p["req_time"] = t + p["req_sign"] = hashlib.md5((t + args.api_key).encode()).hexdigest() + else: + p["api_key"] = args.api_key + return p + + +def cmd_post(args): + content = args.content + if args.content_file: + with open(args.content_file, "r", encoding="utf-8") as f: + content = f.read() + params = { + "title": args.title, + "content": content, + "sort_id": args.sort_id, + "tags": args.tags, + "draft": args.draft, + "excerpt": args.excerpt, + "alias": args.alias, + "top": args.top, + "allow_remark": args.allow_remark, + } + params = {k: v for k, v in params.items() if v not in (None, "")} + params.update(build_auth_params(args)) + if args.id: + # 更新已有文章:emlog 使用 article_update 接口(id 必填) + params = {"id": args.id, **params} + return request("article_update", params) + return request("article_post", params) + + +def cmd_list(args): + params = {"count": args.count} + if args.sort_id is not None: + params["sort_id"] = args.sort_id + if args.keyword: + params["keyword"] = args.keyword + if args.tag: + params["tag"] = args.tag + return request("article_list", params, method="GET") + + +def cmd_sorts(args): + return request("sort_list", {}, method="GET") + + +def cmd_detail(args): + return request("article_detail", {"id": args.id}, method="GET") + + +def cmd_drafts(args): + params = {} + if args.count: + params["count"] = args.count + params.update(build_auth_params(args)) + return request("draft_list", params) + + +def cmd_note(args): + params = {"t": args.text} + params.update(build_auth_params(args)) + return request("note_post", params) + + +def main(): + global BASE_URL, API_KEY + parser = argparse.ArgumentParser(description="emlog REST API 客户端") + parser.add_argument("--base-url", default=BASE_URL) + parser.add_argument("--api-key", default=API_KEY) + parser.add_argument("--sign", action="store_true", help="使用签名鉴权") + sub = parser.add_subparsers(dest="cmd", required=True) + + p = sub.add_parser("post", help="发布/更新文章") + p.add_argument("--id", default=None, help="文章 ID(更新已有文章时传入)") + p.add_argument("--title", required=True) + p.add_argument("--content", default="") + p.add_argument("--content-file", default=None) + p.add_argument("--sort-id", default=None) + p.add_argument("--tags", default="") + p.add_argument("--draft", default="n") + p.add_argument("--excerpt", default="") + p.add_argument("--alias", default="") + p.add_argument("--top", default="n") + p.add_argument("--allow-remark", default="n") + p.set_defaults(func=cmd_post) + + p = sub.add_parser("list", help="文章列表") + p.add_argument("--sort-id", type=int, default=None) + p.add_argument("--count", type=int, default=10) + p.add_argument("--keyword", default="") + p.add_argument("--tag", default="") + p.set_defaults(func=cmd_list) + + p = sub.add_parser("sorts", help="分类列表") + p.set_defaults(func=cmd_sorts) + + p = sub.add_parser("detail", help="文章详情") + p.add_argument("--id", required=True) + p.set_defaults(func=cmd_detail) + + p = sub.add_parser("drafts", help="草稿列表") + p.add_argument("--count", type=int, default=None) + p.set_defaults(func=cmd_drafts) + + p = sub.add_parser("note", help="发布微语") + p.add_argument("--text", required=True) + p.set_defaults(func=cmd_note) + + args = parser.parse_args() + args.base_url = args.base_url or BASE_URL + args.api_key = args.api_key or API_KEY + BASE_URL, API_KEY = args.base_url, args.api_key + + result = args.func(args) + print(json.dumps(result, ensure_ascii=False, indent=2)) + if result.get("code") != 0: + sys.exit(1) + + +if __name__ == "__main__": + main()