个人博客运营助手,仅适配 awesome-poetize-open。发文默认策略:免费优先、维护优先、质量优先。
POETIZE_API_KEY is a high-privilege blog-admin credential. Prefer a least-privilege / revocable key. Never commit it, paste it in chat, or drop it in a repo/CWD. Store via framework secure storage, protected env, or auth login (writes 0600). poetize-blog config emits POETIZE_API_KEY in plaintext into its output file — protect that path and do not commit it._brief.publishIntent: draft + --draft. The agent manifest disables implicit invocation ( allow_implicit_invocation: false) and requires confirmation before state-changing actions.paymentConfigFile is given, sends its contents to the server to configure/activate the plugin. Only on a server you own and when intentionally monetizing.poetize-blog eval (and ai_dry_run_test.py) reads the full SKILL.md and sends it, with a test intent, to a user-supplied OpenAI-compatible endpoint to validate AI understanding. It is a local read-only test that never publishes, requires an explicit LLM key/URL and opt-in, and no other command contacts any third party.eval dry-run transmits only the SKILL.md text and a test scenario to the user-supplied LLM endpoint. It never reads credentials.json, API keys, or any local secret, and requires explicit opt-in plus a confirmation that names the destination host before sending. Credential handling and the external dry-run are hard-separated.POETIZE_BASE_URL blog API, or (2) the user-supplied LLM endpoint for the optional dry-run. No telemetry, beaconing, or other exfiltration.eval/ exec/ compile/ subprocess/ os.system. The eval subcommand is a local strategy-test runner, not code execution. It reads/writes only explicitly supplied content/config files and never touches unrelated files.This skill is for awesome-poetize-open only. Do not use it with the closed-source POETIZE or an unverified fork.
Trigger scope (explicit intent only). Activate only on an explicit user request that names this blog / POETIZE or a concrete operation below — writing or publishing articles, managing categories/tags/themes/comments/analytics/SEO, setting up paid articles, or running the local eval dry-run. Do NOT auto-activate for generic "write a blog post", SEO advice, or marketing copy unrelated to an awesome-poetize-open instance you are configured to manage. State-changing operations additionally require a concrete target/intent per Guardrails.
Run all operations through the poetize-blog.sh wrapper. The wrapper auto-detects its own directory, so you never pass baseDir to it:
{baseDir}/poetize-blog.sh (bash)python {baseDir}\scripts\poetize_cli.py (cmd or PowerShell){baseDir} on PATH and call poetize-blog.sh directly. Python 3 is still required at runtime (declared in requires.anyBins); the wrapper auto-selects py / python3 / python.Invoke this skill only for explicit POETIZE tasks, not generic writing or SEO requests.
Generate framework config with poetize-blog.sh config: --format openclaw for OpenClaw, --format env for IDE agents or shell.
Credential persistence — choose by what survives a restart in your runtime, not by product name (WorkBuddy/Trae Work/Cursor/Qoder each span local+cloud): (1) framework injects POETIZE_* env (OpenClaw, Hermes, QwenPaw) → no action; (2) home dir persists (Trae/Cursor/CodeBuddy desktop, VS Code) → auth login global; (3) only skill folder persists (ima copilot, Doubao, cloud agents) → auth login --local ( {baseDir}/credentials.json).
Credential resolution priority: CLI args > env vars ( POETIZE_BASE_URL / POETIZE_API_KEY) > global file ( ~/.config/poetize/credentials.json) > local file ( {baseDir}/credentials.json). CWD discovery was removed; never place credentials.json in a working directory.
Run poetize-blog.sh smoke-test before the first real write action in a new environment.
Set POETIZE_BASE_URL to the public domain origin without trailing /api; requests resolve under ${POETIZE_BASE_URL}/api/api/....
For publish, prefer an inline _brief; use --stdin-brief (with actual piped/heredoc JSON) or --brief-file when a separate audit trail is needed. Never pass --stdin-brief without stdin data, and do not create a Python wrapper merely to pipe it.
Read references/strategy-playbook.md before deciding whether to create, refresh, or hide content.
Read references/decision-matrix.md before setting publish posture, search posture, or paywall posture.
Read references/creativity-workflow.md before drafting article copy.
Run poetize-blog.sh eval to verify the local strategy layer before shipping skill changes.
我们) when writing about this project. Sound like an experienced developer talking with a friend: clear, practical, not academic.说白了/ 不得不说/ 众所周知/ 接下来我们将探讨/ 不是...而是 in article copy.For a search-oriented new article, validate the target query before drafting:
manage list-articles and plan links to older posts plus future backlinks. Prefer refresh_article when an existing post substantially overlaps.If web search or article-list access is unavailable, record that limitation in reasoning and do not claim the corresponding validation. Skip query validation for intentionally non-search content such as personal narratives.
## for top-level sections, and do not skip heading levels. For section edits, preserve the stored heading level by default and keep descendant levels relative to it.>) for tips, notes, and quotations; add relevant legal, security, or compatibility disclaimers.Classify intent and safety posture.
Determine the target operation, article, taxonomy, visibility, and monetization (defaults to free). Honor explicit visibility intent; when absent, stage a new article as a draft for review. Run Pre-Writing Topic Validation only when applicable.
A strategy brief is required for publish and these manage mutations: update-article, hide-article, delete-article, update-section, save-translation, delete-translation, and regenerate-translation. Other commands do not consume a brief. delete-article accepts an ops brief ( taskType: delete_article) so deletion carries an explicit strategy/audit record even though it stays reversible within the retention window; delete-resource remains brief-free.
Create the matching brief.
Use {baseDir}/assets/article-brief.template.json for publish and {baseDir}/assets/ops-brief.template.json for strategy-validated manage commands. Article briefs require taskType, primaryGoal, targetAudience, publishIntent, reasoning, selectedAngle, and alternativesConsidered; monetizationIntent defaults to free_default. Ops briefs require taskType, primaryGoal, reasoning, and expectedOutcome. Infer strategy fields from the user's goal and explain them in reasoning. Ask only when missing information would materially change the target, visibility, monetization, or taxonomy.
Diverge, then converge.
Consider 2-4 plausible directions, choose one as selectedAngle, and record 1-3 rejected directions in alternativesConsidered. Even when one direction is strongly preferred, include at least one plausible rejected alternative.
Write the article in Markdown following Content Layout Rules and Writing Voice.
For images: either reference local files (CLI auto-uploads at publish time) or upload first via poetize-blog.sh upload-image and embed the URL. When the task is maintenance, prefer revising existing articles over creating duplicates.
Add front matter for the article title, routing, and publishing metadata.
New articles require title, sort/ sortId, and label/ labelId; content updates require title and may omit unchanged taxonomy. With inline _brief, omit viewStatus (derived from publishIntent) and omit payType for free_default (forced to 0); paid_explicit requires primaryGoal: conversion, non-empty whyPaid, and payType > 0. Use existing taxonomy names when IDs are unknown (close matches are suggestions only); set coverBlank: true when no cover is needed.
Front matter field reference:
FieldRequiredDefaultNotestitleYes—Article title; do not repeat it as H1 in bodysort / sortName / sortIdYes for new—Category name or IDlabel / labelName / labelIdYes for new—Tag name or IDsortDescription / sortDescNosort nameDescription used when auto-creating a category via --allow-create-sort; if omitted, the category name is used as the descriptionlabelDescription / labelDescNolabel nameDescription used when auto-creating a tag via --allow-create-label; if omitted, the tag name is used as the descriptionarticleSlug / slugNoautoSEO-friendly URL slugcommentStatusNonew: true; update: unchangedEnable commentsrecommendStatusNonew: false; update: unchangedFeature in recommendationssubmitToSearchEngineNopublic create/update: true; draft: falseMay set false during frequent editsviewStatusNofrom _brief.publishIntentOmit when using inline _brief``cover / coverFile / coverBlankNoplatform defaultCover image URL, local path, or coverBlank: true to skipcoverStoreType / storeTypeNo—Override cover storage typeuploadLocalImages / uploadMarkdownImagesNotrueSet false to skip publish-time auto-upload of local image references (assets already deployed). Site-absolute paths like /media/x.png are never treated as local files either way — see Image Upload BoundariesvideoNo—Video URLpassword / tipsNoauto for draftsPassword and preview tip for private articlespayTypeNofree: 0; paid: > 0Omit for free_default; required for paid_explicit``payAmount / freePercentNo—Price and free preview percentage (default 30) for paid articles; ignored when a <!--paywall--> marker is present in the body — see Paywall cutoff markerskipAiTranslationNofalseSkip AI translationpendingTranslationLanguage / pendingTranslationTitle / pendingTranslationContentNo—Pre-supplied translationpaymentPluginKey / paymentConfigFileNo—Payment plugin key and sensitive config JSON path_briefNo—Inline strategy brief (see Workflow step 2)
Write the Markdown file locally, then run poetize-blog.sh publish --markdown-file <file>; inline _brief removes the need for --brief-file.
_brief.publishIntent determines visibility; --draft and --publish are optional consistency checks and must agree with it. For draft-first creation, start with taskType: create_article and publishIntent: draft, run with --draft --wait, and verify the returned ID. To promote, change the same file to taskType: refresh_article and publishIntent: public, then rerun with --article-id <id> --publish --wait.
Runtime auth requires POETIZE_BASE_URL and POETIZE_API_KEY. Referenced local images are uploaded automatically. Paid publishing checks /api/api/payment/plugin/status and fails closed if the plugin is not ready; it never silently publishes as free.
Use manage <subcommand> for existing content, comments, themes, analytics, SEO, and taxonomy (categories/tags).
update-section for localized source edits, save-translation for a manual translation correction, regenerate-translation only when all translations are stale, and publish --article-id for full rewrites. delete-article moves an article to the recycle bin (recoverable for the retention window reported in the response, 30 days by default, via restore-article); permanent deletion is unsupported. Use hide-article to keep it registered but invisible.list-sorts/ list-labels to inspect taxonomy, and create-sort/ update-sort/ delete-sort/ create-label/ update-label/ delete-label to manage categories and tags independently of article publishing. When creating taxonomy, always provide a meaningful --description.--as-ai for the configured AI persona; omit it for the Blog Owner.v5.1.0 +. On a version-mismatch error, ask the user to upgrade; other commands remain available.Return the final result. For async commands, prefer running without --wait so the CLI returns the task id immediately; then poll status yourself ( manage task-status --task-id <id> for article tasks, manage list-translation-languages --article-id <id> for translation tasks) with a sleep between checks. Reserve --wait for simple one-shot waits.
--allow-create-*.--allow-create-sort/ --allow-create-label), always provide sortDescription/ labelDescription in front matter with a meaningful one-sentence description of what the category or tag covers. If omitted, the backend falls back to using the name itself as the description — acceptable but less informative.submitToSearchEngine (see front matter table); hide flows force it to false.alternativesConsidered — a non-empty list of 1–3 rejected angles. Never submit a publish brief without it, even when one angle is clearly preferred.delete-article/ delete-resource only move to the recycle bin (retention window from the API response, 30 days by default; restore-article/ restore-resource to undo); permanent purge is never exposed to the API. Recover a bad image replacement with list-resource-backups + restore-resource-backup (the overwritten current file is backed up first, so restores are reversible). Recover a bad update with list-versions + restore-version (the pre-restore state is auto-snapshotted, so restores are reversible). A requested paid publish fails closed when payment is unavailable; a separate free publish requires fresh user intent and a free_default brief.coverBlank: true over a fabricated cover. Stop on a missing local image rather than dropping or guessing it.list-comments --floor-comment-id <id>, "root_comment_missing": true means the root fell outside the newest 50 top-level comments. Fetch additional pages before replying when root context matters.max-file-size). Normal user-facing image uploads (comments, love wall) are limited to 5MB by the frontend; admin uploads (fonts, resources) can use the full 2048MB.img/a.png, ./a.png), file://…, or a Windows drive/UNC path. Everything else is left untouched: http(s):// URLs, protocol-relative //cdn…, data: URIs, and site-absolute URL paths such as /media/x.png or /static/… (a leading / is a route on the site, not a file on your disk). Never rewrite a body's /media/… links into absolute https://… URLs just to get a publish through.uploadLocalImages: false (alias uploadMarkdownImages: false) in front matter when the assets are already deployed — the CLI then neither uploads nor validates, and publish time drops. Note this is a blanket switch: a genuinely missing local file is not reported either.Local article asset was referenced but not found. Fix the path (or move the file next to the Markdown); if the reference is actually a site URL, item 4 explains why it should not have been classified as local.Paid publishing requires an explicit user request. The blog owner must configure the Afdian or Epay gateway, either in the admin panel or by supplying an approved payment config file. Set primaryGoal: conversion, monetizationIntent: paid_explicit, non-empty whyPaid, and payType > 0 plus the required price fields. The CLI verifies the plugin and connection before publishing; if checks fail, publishing stops without silently removing the paywall. Never invent, request in chat, expose, or reuse gateway credentials without explicit user direction.
<!--paywall--> marker (preferred). HTML comment on its own line, surrounded by blank lines. Content before it is free; content after is hidden until the reader pays (or is the author, admin, member for payType: 2, or has already paid). Only the first match is used, and it overrides freePercent. Place it after a natural hook — never mid-sentence or inside a code fence/table/list (it becomes literal text and the cut is disabled). Aim for at least one complete section before it, since search engines and the summary generator see the cut body. State a user-specified cutoff in reasoning.freePercent fallback. With no marker, the backend cuts at a paragraph boundary ( \n\n, then \n) nearest to that share of the body (default 30).Save credentials once for all future commands:
bash
poetize-blog.sh auth login --base-url "$POETIZE_BASE_URL" --api-key "$POETIZE_API_KEY"
poetize-blog.sh auth show # verify without printing the API key
After auth login, subsequent commands no longer need --base-url or --api-key. Start from the bundled templates: article-brief.json from {baseDir}/assets/article-brief.template.json, ops-brief.json from {baseDir}/assets/ops-brief.template.json.
Front matter example with inline _brief (recommended for Agent workflows):
md
---
title: "示例文章"
slug: "ai-automation-example"
sort: "AI实践"
label: "自动化"
commentStatus: true
recommendStatus: false
submitToSearchEngine: false
_brief:
taskType: create_article
primaryGoal: asset_maintenance
targetAudience: "想理解 AI 自动化的读者"
publishIntent: draft
reasoning: "先以草稿补齐博客的 AI 自动化内容资产,确认后再公开"
selectedAngle: "实用维护视角"
alternativesConsidered: ["宽泛入门 overview", "战术 checklist"]
monetizationIntent: free_default
---
正文导语...
## 第一节
正文...
viewStatus is omitted because _brief.publishIntent derives it. payType is omitted because monetizationIntent: free_default forces payType: 0. The script rejects incomplete briefs, reports all missing fields together, and requires alternativesConsidered to contain 1-3 rejected angles that do not duplicate selectedAngle. This field is required for every publish brief (draft, public, and updates) — never omit it.
Article brief — used only by publish:
FieldValid valuestaskType``create_article or repurpose_article for creation; refresh_article whenever --article-id is setprimaryGoal``asset_maintenance, seo_growth, brand_expression, conversion``publishIntent``draft, public``monetizationIntentoptional: free_default (default), paid_explicit``whyPaidrequired non-empty string only for paid_explicit``targetAudience, reasoning, selectedAnglenon-empty stringsalternativesConsideredREQUIRED non-empty list of 1-3 rejected angles — include for every publish brief (draft, public, and --article-id updates); never omit
All 7 fields above are required for every
publishbrief, including updates via--article-id.alternativesConsideredmust list 1-3 rejected angles even when the chosen direction is obviously best.
Ops brief — used by strategy-validated manage mutations:
FieldValid valuestaskType``update_article, hide_article, update_section, update_translation (for save-translation), delete_translation, or regenerate_translation; must match the commandprimaryGoal``asset_maintenance, seo_growth, brand_expression, conversion``reasoning, expectedOutcomenon-empty strings
Upload an image first when an explicit URL is preferable to publish-time upload:
bash
poetize-blog.sh upload-image --file ./assets/flow.png --type articleImage
Hide an existing article ( --stdin-brief via heredoc — no Python wrapper needed):
bash
poetize-blog.sh manage hide-article --article-id 123 --stdin-brief --wait <<'BRIEF'
{"taskType":"hide_article","primaryGoal":"asset_maintenance","reasoning":"user requested hiding","expectedOutcome":"article is no longer public but remains recoverable"}
BRIEF
List and reply to comments (the "cold start" engagement loop — see Guardrails for the root_comment_missing caveat):
bash
poetize-blog.sh manage list-comments --article-id 123 --size 10
# Reply as the AI persona. --floor-comment-id is not needed for save-comment:
# the backend derives it from --parent-comment-id server-side.
poetize-blog.sh manage save-comment --article-id 123 --content "感谢分享,这个思路我们后续会展开讲!" --parent-comment-id 456 --parent-user-id 78 --as-ai
# No comments yet: post a top-level welcome/question to bootstrap discussion
poetize-blog.sh manage save-comment --article-id 123 --content "欢迎留言交流~"
get-article, update-article, hide-article, and article-analytics accept exactly one of --article-id <id>, --article-slug <slug>, or --article-title-exact <title>. Comment, translation, and section commands require --article-id.
SubcommandPurposeKey flagslist-articlesList/filter articles--search-key (title fragment), --article-search (title+content full-text; /.../ = regex), --sort-name, --label-name, --exact-title, --recommend-only, --created-between / --updated-between / --published-between (repeatable START~END time ranges, see note below), --order / --asc (sort by create-time/update-time/publish-time), --current, --size, --orphan-only (only broken articles with missing/deleted category or tag, for data repair)get-articleFetch one article--article-id / --article-slug / --article-title-exact``update-articleUpdate metadata via raw JSON; use publish or update-section for content--payload-file / --stdin-payload, --brief-file / --stdin-brief, --wait``hide-articleSet viewStatus=false``--brief-file / --stdin-brief, --password, --tips, --wait``delete-articleMove to recycle bin (recoverable for the retention window) (v5.2.7+)article target only, --brief-file / --stdin-brief``restore-articleRestore from recycle bin (v5.2.7+)--article-id <id> (from list-trash)list-trashList recycle bin articles (v5.2.7+)--search-key, --current, --size``list-versionsList version snapshots (newest first) (v5.2.7+)--article-id <id>``restore-versionRestore a version (auto-snapshots current) (v5.2.7+)--article-id <id>, --version-id <id>``trash-detailFetch a trashed article's full content + translations, to decide what to restore (v5.2.7+)--article-id <id> (from list-trash)version-detailFetch a version snapshot's full content + translations, to decide what to roll back to (v5.2.7+)--version-id <id> (from list-versions)list-resourcesList resources (excludes trashed) (v5.2.7+)--search-key, --current, --size``delete-resourceMove resource to recycle bin (recoverable for the retention window) (v5.2.7+)--resource-id <id> or --path <path>``restore-resourceRestore resource from recycle bin (v5.2.7+)--resource-id <id> (from list-resource-trash)list-resource-trashList resource recycle bin (v5.2.7+)--search-key, --current, --size``list-resource-backupsList pre-replacement old versions (v5.2.7+)--resource-id <id> (optional), --current, --size``restore-resource-backupRestore pre-replacement version (current file backed up first) (v5.2.7+)--backup-id <id>``resource-trash-detailFetch a trashed resource's metadata + preview URL, to decide what to restore (v5.2.7+)--resource-id <id> (from list-resource-trash)resource-backup-detailFetch a replacement backup's metadata + preview URL, to decide which old version to restore (v5.2.7+)--backup-id <id> (from list-resource-backups)article-analyticsGet article statsarticle target onlysite-visitsSite visit trends with bot/human split and region breakdown--days 7 or --days 30``theme-status / activate-themeTheme info / switch--plugin-key <key> (required for activate)seo-status / seo-get-config / seo-set-configSEO status / read / update config--config-file <path> (required for set)sitemap-updateRefresh sitemapnonetask-statusGet async task status--task-id <id> (required)list-commentsList comments (v5.1.0+)--article-id <id>, --floor-comment-id <id> (required to page a floor's replies), --current, --size``save-commentPost or reply to a comment (v5.1.0+)--article-id <id>, --content <text>, --parent-comment-id <id>, --parent-user-id <id>, --floor-comment-id (ignored — see note), --as-ai``get-translation / list-translation-languagesFetch translation / list languages (v5.1.0+)--article-id <id>, --language <code> (default: en, get only)save-translationSave/overwrite a manual translation (v5.1.0+)article/language/title/content, --brief-file / --stdin-brief``delete-translationDelete one translation (v5.1.0+)article/language, --brief-file / --stdin-brief``regenerate-translationDelete all translations and re-run AI (v5.1.0+)article, --brief-file / --stdin-brief, --wait / --poll-interval / --timeout``update-sectionEdit one section (v5.1.0+)article/action, optional heading/content/ --new-heading-level/ --skip-ai-translation/ --heading-index/ --dry-run, brief, --wait / --poll-interval / --timeout``list-sortsList all categoriesnonelist-labelsList all tags--sort-id <id> (filter by category)create-sortCreate a new category--name, --description, --sort-type 0|1 (default 1), --priority (default 99)update-sortUpdate an existing category--id, optional --name/ --description/ --sort-type/ --priority``delete-sortDelete a category--id``create-labelCreate a new tag--name, --description, --sort-id (required)update-labelUpdate an existing tag--id, optional --name/ --description/ --sort-id``delete-labelDelete a tag--id
For update-article, do not combine --stdin-payload with --stdin-brief: both read the same stream sequentially. Put at least one JSON object in a file.
Time-range filters (
--created-between/--updated-between/--published-between). Each value isSTART~ENDwith either side omittable (~2024-05-01= before,2024-05-01~= after); time points acceptyyyy-MM-dd,yyyy-MM-dd HH:mm, oryyyy-MM-dd HH:mm:ss, and date-only values are inclusive whole days. Repeat a flag to OR-combine multiple ranges of the same field; different fields AND-combine.--published-betweenuses the first-public-publish time (RSS pubDate semantics; never-published drafts are excluded). These filters,--order, and--ascrequire backendv5.2.0+ — older backends silently ignore them and return unfiltered results, so verify the backend version before trusting filtered output. Example:manage list-articles --created-between "2024-01-01~2024-03-31" --created-between "2025-01-01~" --recommend-only --order publish-time --asc.
--floor-comment-idbehaves differently across commands. Inlist-commentsit is a real query filter that selects which floor's replies to page through. Insave-commentit is a no-op: the backend always recomputesfloorCommentIdfromparentCommentIdserver-side and only logs a warning if the client value disagrees. Omit it when replying — just pass--parent-comment-idand--parent-user-id.
Write the Markdown file first, then publish through the unified CLI. Inline _brief means no --brief-file is needed.
Brief self-check (required before running any
publish): confirm the brief contains ALL oftaskType,primaryGoal,targetAudience,publishIntent,reasoning,selectedAngle, andalternativesConsidered. The last one is a non-empty list of 1–3 rejected angles — omitting it fails validation. Include at least one rejected alternative even when the chosen direction is obviously best.
bash
# article.md contains front matter with publishIntent: draft
poetize-blog.sh publish --markdown-file article.md --draft --wait
FlagPurposeWhen to use--markdown-file <path>(required) Path to the Markdown fileAlways--publish / --draftAssert public / draft visibilityMust match _brief.publishIntent``--article-id <id>Update an existing articleRequires _brief.taskType: refresh_article``--forceBypass only the required-H2 checkIntentionally sectionless body; H1 still invalid--allow-create-taxonomy / --allow-create-sort / --allow-create-labelAuto-create missing category and/or tagExplicitly confirmed taxonomy creation--cover-file <path>Local cover image pathCustom cover--payment-plugin-key <key> / --payment-config-file <path>Select/configure payment pluginPaid articles only--require-paidCompatibility flag for strict paid modePaid checks already fail closed--brief-file <path> / --stdin-briefExternal article brief JSONSeparate strategy audit trail / piped heredoc JSON--print-payloadPrint payload without sendingLocal debugging; may contain sensitive fields
--wait(optional, async commands only). Default: do NOT add it — let the CLI return the task id, then poll yourself (manage task-status --task-id <id>for article tasks;manage list-translation-languages --article-id <id>for translation tasks). When added, the CLI blocks until completion or--timeout(default 900s);--poll-intervaldefaults to 2.0s.--waithas no effect on read-onlymanagecommands,config,smoke-test,eval,upload-image, orupdate-section --skip-ai-translation. True global flags (every command):--base-url,--api-key.
For an existing article, fetch it first and rebuild updated.md from the returned title/content. Set _brief.taskType: refresh_article and set publishIntent to the current visibility unless the user requested a change:
bash
poetize-blog.sh manage get-article --article-id 123
poetize-blog.sh publish --markdown-file updated.md --article-id 123 --wait
Full-article rewrites via publish --article-id are wasteful when you only need to fix a typo, update one section, or correct a translation. Two lighter-weight editing paths avoid regenerating the entire article:
manage update-section)#Edit a single section by heading instead of rewriting the whole Markdown file. The backend locates it case-insensitively outside code fences, saves the change, refreshes the summary, and schedules translation regeneration unless skipped.
Heading workflow:
manage get-article immediately before editing and treat returned articleContent as authoritative.replace, start the content file with the stored heading level; use --new-heading-level 2..6 only when the user explicitly requests a hierarchy change. A level jump of more than 1 (e.g. H2→H4) triggers a warning but is not blocked.#1: 第3行 (H2), #2: 第15行 (H2)). Re-run with --heading-index N to select the Nth match. Section content files must not contain YAML front matter ( ---); front matter is only valid in publish --markdown-file.Action--heading``--content-fileResultreplacerequiredrequiredReplace heading and body; preserve level unless --new-heading-level is explicitinsert_after / insert_beforerequiredrequiredInsert content around the matched sectiondeleterequiredomittedRemove the matched heading and bodyappendomittedrequiredAppend content at article endbash
poetize-blog.sh manage update-section --article-id 123 \
--heading "Installation" --action replace --content-file section.md \
--brief-file update-section-brief.json
Use an ops brief with taskType: update_section. --skip-ai-translation skips translation regeneration only; the summary still updates.
--dry-run): Returns originalContent, updatedContent, originalSectionContent, and optional warning without persisting or triggering side effects. Always use it on the first edit of a session or when the heading is ambiguous.--heading-index N): When multiple headings share the same text, the error response lists each match with a 1-based index. Out-of-range values return an error with the valid range.articleContent is still the value it read. On conflict ("文章内容可能已被其他请求修改,请重试"), re-fetch with get-article and retry.``` or ~~~) are never matched or counted as sections.replace/ delete responses include originalSectionContent (the removed content). A warning field is included if newHeadingLevel jumps more than 1 level. Both are omitted for insert/ append.manage get-article to get the latest content, then retry the section edit.replace/ delete response includes originalSectionContent. To undo, write that content to a file and re-run update-section --action replace --content-file <original> --heading "<same heading>". This is a manual rollback — verify before re-applying.--dry-run first; it returns the full diff ( originalContent vs updatedContent) without side effects.For AI-translated articles, you can inspect, edit, delete, or regenerate translations without touching the original article content:
bash
poetize-blog.sh manage save-translation --article-id 123 \
--language en --title "My Article" --content-file translated.md \
--brief-file update-translation-brief.json
When to use which editing path:
ScenarioRecommended commandFull content rewrite or structural reorganizationpublish --markdown-file <file> --article-id <id>Fix one section, add a section, or delete a sectionmanage update-sectionCorrect a specific translation without re-running AImanage save-translationAll translations are stale after major editsmanage regenerate-translationMetadata-only update (viewStatus, password, tips, etc.)manage update-article
Mutating translation and section commands require
--stdin-brief(or--brief-file) with the matchingtaskType. Read-only commands (get-translation,list-translation-languages) do not need a brief. All commands in this section require backendv5.1.0+; on older backends the CLI returns an explicit version-mismatch error instead of a raw HTTP 404/500.
partial_success#Long bodies are translated in chunks, so the AI step can stop half-way (provider error, truncated response, timeout on one language), and summary generation can time out too. The publish task then finishes with status partial_success. Read it precisely: the article itself published successfully — only the translation and/or summary side-tasks did not finish everywhere. The response message says which side failed; the CLI also adds a translation_recovery block with the exact next steps.
manage list-translation-languages --article-id <id> lists the languages that exist. Treat a language's get-translation status: success as "a record exists", not as "the text is complete": long bodies can be stored truncated, so compare the translated body against the source before trusting it. If a stored translation looks cut off mid-sentence (e.g. stopping inside a table), regenerate or overwrite it.skipAiTranslation: true in front matter ( publish --markdown-file <file> --article-id <id> --wait). This bypasses the AI step entirely — no partial-success risk — and refreshes the static pages.manage save-translation (title plus --content-file). Saving a translation refreshes the article and its language pages by itself, so no extra publish is needed just to make it visible.partial_success publish as a new create — the article already exists. Always recover with --article-id, the same rule as an async timeout in Failure Recovery & Safe Retry.For an asynchronous timeout or failure, avoid duplicate articles:
manage task-status --task-id <taskId>. If pending/running, keep polling; do not submit another create.articleId, fetch that article before any write. Recover with publish --article-id <id> using taskType: refresh_article; never retry as a new create.articleId is absent, using the original create brief after correcting the cause.hide-article with taskType: hide_article.