新增小绿书/图片帖支持(WeChat newspic 格式)
publisher.py 新增 create_image_post(): - article_type="newspic",横滑轮播 3:4 比例 - 1-20 张图片,第一张自动当封面 - 标题 ≤ 32 字,纯文本描述 ~1000 字 - 使用 upload_thumb(永久素材)上传图片 cli.py 新增 image-post 子命令: python3 cli.py image-post img1.jpg img2.jpg -t "标题" -c "描述" SKILL.md Step 8c 新增"做一个小绿书"触发入口。 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9bcb9bdd23
commit
52cff3dd8f
3 changed files with 144 additions and 1 deletions
1
SKILL.md
1
SKILL.md
|
|
@ -284,6 +284,7 @@ python3 {skill_dir}/toolkit/cli.py preview {markdown} --theme {theme} --no-open
|
|||
| 换成 XX 主题 | 重新渲染 |
|
||||
| 看看文章数据 | `读取: {skill_dir}/references/effect-review.md` |
|
||||
| 学习我的修改 | `读取: {skill_dir}/references/learn-edits.md` |
|
||||
| 做一个小绿书/图片帖 | `python3 {skill_dir}/toolkit/cli.py image-post img1.jpg img2.jpg -t "标题"` |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import yaml
|
|||
from converter import WeChatConverter, preview_html
|
||||
from theme import load_theme, list_themes
|
||||
from wechat_api import get_access_token, upload_image, upload_thumb
|
||||
from publisher import create_draft
|
||||
from publisher import create_draft, create_image_post
|
||||
|
||||
# Config file search order
|
||||
CONFIG_PATHS = [
|
||||
|
|
@ -142,6 +142,62 @@ def cmd_themes(args):
|
|||
print(f" {name:24s} {theme.description}")
|
||||
|
||||
|
||||
def cmd_image_post(args):
|
||||
"""Create a WeChat image post (小绿书) from image files."""
|
||||
cfg = load_config()
|
||||
wechat_cfg = cfg.get("wechat", {})
|
||||
|
||||
appid = args.appid or wechat_cfg.get("appid")
|
||||
secret = args.secret or wechat_cfg.get("secret")
|
||||
|
||||
if not appid or not secret:
|
||||
print("Error: --appid and --secret required (or set in config.yaml)", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
images = args.images
|
||||
if not images:
|
||||
print("Error: at least 1 image required", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if len(images) > 20:
|
||||
print(f"Error: max 20 images, got {len(images)}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
token = get_access_token(appid, secret)
|
||||
print(f"Uploading {len(images)} images as permanent materials...")
|
||||
|
||||
media_ids = []
|
||||
for img_path in images:
|
||||
p = Path(img_path)
|
||||
if not p.exists():
|
||||
print(f"Error: image not found: {img_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print(f" Uploading: {p.name}")
|
||||
mid = upload_thumb(token, str(p))
|
||||
media_ids.append(mid)
|
||||
print(f" -> {mid}")
|
||||
|
||||
title = args.title
|
||||
if len(title) > 32:
|
||||
print(f"Warning: title truncated to 32 chars (was {len(title)})")
|
||||
title = title[:32]
|
||||
|
||||
content = args.content or ""
|
||||
|
||||
result = create_image_post(
|
||||
access_token=token,
|
||||
title=title,
|
||||
image_media_ids=media_ids,
|
||||
content=content,
|
||||
open_comment=True,
|
||||
)
|
||||
|
||||
print(f"\nImage post draft created!")
|
||||
print(f" media_id: {result.media_id}")
|
||||
print(f" images: {result.image_count}")
|
||||
print(f" title: {title}")
|
||||
print(f" 请到公众号后台草稿箱检查并发布")
|
||||
|
||||
|
||||
def cmd_gallery(args):
|
||||
"""Render all themes side by side in a browser gallery."""
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
|
@ -327,6 +383,14 @@ def main():
|
|||
# themes
|
||||
sub.add_parser("themes", help="List available themes")
|
||||
|
||||
# image-post (小绿书)
|
||||
p_imgpost = sub.add_parser("image-post", help="Create WeChat image post (小绿书)")
|
||||
p_imgpost.add_argument("images", nargs="+", help="Image file paths (1-20, first = cover)")
|
||||
p_imgpost.add_argument("-t", "--title", required=True, help="Post title (max 32 chars)")
|
||||
p_imgpost.add_argument("-c", "--content", default="", help="Plain text description (max ~1000 chars)")
|
||||
p_imgpost.add_argument("--appid", default=None, help="WeChat AppID")
|
||||
p_imgpost.add_argument("--secret", default=None, help="WeChat AppSecret")
|
||||
|
||||
# gallery
|
||||
p_gallery = sub.add_parser("gallery", help="Open theme gallery in browser")
|
||||
p_gallery.add_argument("input", nargs="?", default=None, help="Markdown file (optional, uses sample if omitted)")
|
||||
|
|
@ -342,6 +406,8 @@ def main():
|
|||
cmd_publish(args)
|
||||
elif args.command == "themes":
|
||||
cmd_themes(args)
|
||||
elif args.command == "image-post":
|
||||
cmd_image_post(args)
|
||||
elif args.command == "gallery":
|
||||
cmd_gallery(args)
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -10,6 +10,12 @@ class DraftResult:
|
|||
media_id: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImagePostResult:
|
||||
media_id: str
|
||||
image_count: int
|
||||
|
||||
|
||||
def create_draft(
|
||||
access_token: str,
|
||||
title: str,
|
||||
|
|
@ -60,3 +66,73 @@ def create_draft(
|
|||
raise ValueError(f"WeChat create_draft error: missing media_id in response: {data}")
|
||||
|
||||
return DraftResult(media_id=data["media_id"])
|
||||
|
||||
|
||||
def create_image_post(
|
||||
access_token: str,
|
||||
title: str,
|
||||
image_media_ids: list[str],
|
||||
content: str = "",
|
||||
open_comment: bool = False,
|
||||
fans_only_comment: bool = False,
|
||||
) -> ImagePostResult:
|
||||
"""
|
||||
Create a WeChat image post (小绿书/图片帖) draft.
|
||||
|
||||
This uses article_type="newspic" which displays as a horizontal
|
||||
swipe carousel (3:4 ratio), similar to Xiaohongshu.
|
||||
|
||||
Args:
|
||||
access_token: WeChat access token.
|
||||
title: Post title, max 32 characters.
|
||||
image_media_ids: List of permanent media_ids from upload_thumb().
|
||||
Min 1, max 20. First image becomes the cover.
|
||||
content: Plain text description, max ~1000 chars. No HTML.
|
||||
open_comment: Allow comments.
|
||||
fans_only_comment: Only followers can comment.
|
||||
|
||||
Returns ImagePostResult with media_id of created draft.
|
||||
"""
|
||||
if not image_media_ids:
|
||||
raise ValueError("At least 1 image is required for image post")
|
||||
if len(image_media_ids) > 20:
|
||||
raise ValueError(f"Max 20 images allowed, got {len(image_media_ids)}")
|
||||
if len(title) > 32:
|
||||
raise ValueError(f"Title max 32 chars for image post, got {len(title)}")
|
||||
|
||||
article = {
|
||||
"article_type": "newspic",
|
||||
"title": title,
|
||||
"content": content,
|
||||
"image_info": {
|
||||
"image_list": [
|
||||
{"image_media_id": mid} for mid in image_media_ids
|
||||
]
|
||||
},
|
||||
"need_open_comment": 1 if open_comment else 0,
|
||||
"only_fans_can_comment": 1 if fans_only_comment else 0,
|
||||
}
|
||||
|
||||
body = {"articles": [article]}
|
||||
|
||||
resp = requests.post(
|
||||
"https://api.weixin.qq.com/cgi-bin/draft/add",
|
||||
params={"access_token": access_token},
|
||||
data=json.dumps(body, ensure_ascii=False).encode("utf-8"),
|
||||
headers={"Content-Type": "application/json; charset=utf-8"},
|
||||
)
|
||||
|
||||
data = resp.json()
|
||||
|
||||
errcode = data.get("errcode", 0)
|
||||
if errcode != 0:
|
||||
errmsg = data.get("errmsg", "unknown error")
|
||||
raise ValueError(f"WeChat create_image_post error: errcode={errcode}, errmsg={errmsg}")
|
||||
|
||||
if "media_id" not in data:
|
||||
raise ValueError(f"WeChat create_image_post: missing media_id in response: {data}")
|
||||
|
||||
return ImagePostResult(
|
||||
media_id=data["media_id"],
|
||||
image_count=len(image_media_ids),
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue