@acedatacloud/skills 2026.726.3 → 2026.726.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acedatacloud/skills",
3
- "version": "2026.726.3",
3
+ "version": "2026.726.4",
4
4
  "description": "Agent Skills for AceDataCloud AI services — music, image, video generation, LLM chat, web search. Compatible with Claude Code, GitHub Copilot, Gemini CLI, OpenAI Codex, and 30+ AI coding agents.",
5
5
  "keywords": [
6
6
  "agent-skills",
@@ -9,11 +9,11 @@ when_to_use: |
9
9
  This skill is for self-hosted WordPress (Application Password auth),
10
10
  not WordPress.com.
11
11
  connections: [wordpress]
12
- allowed_tools: [Bash]
12
+ allowed_tools: [Bash, publish_artifact]
13
13
  license: Apache-2.0
14
14
  metadata:
15
15
  author: acedatacloud
16
- version: "1.0"
16
+ version: "1.1"
17
17
  ---
18
18
 
19
19
  Drive the **WordPress REST API** (`/wp-json/wp/v2`) with `curl + jq`.
@@ -36,27 +36,54 @@ API="$SITE/wp-json/wp/v2"
36
36
  WP=(curl -sS --fail-with-body -u "$WORDPRESS_USERNAME:$WORDPRESS_APP_PASSWORD")
37
37
  ```
38
38
 
39
+ > **Don't add `-L`/`--location` to these calls.** A redirect usually means the
40
+ > site URL is wrong. curl strips the `Authorization` header when a redirect
41
+ > crosses host or scheme (e.g. `http`→`https`, apex→`www`) — exactly the cases
42
+ > that occur here — so the followed request runs **unauthenticated** and can
43
+ > return the site's HTML page with HTTP 200: it looks like success but wrote
44
+ > nothing. Fix the URL instead (see the `/wp-json` gotcha below).
45
+
46
+
39
47
  Errors come back as `{"code": "...", "message": "...", "data": {"status": 401}}` —
40
48
  show `message` verbatim. Common codes:
41
49
 
42
50
  | HTTP | Meaning | What to tell the user |
43
51
  |------|---------|-----------------------|
44
52
  | 401 | `incorrect_password` / bad Basic auth | Application Password wrong or revoked → regenerate it and reconnect the WordPress connector |
53
+ | 401 | `rest_not_logged_in` on a plain-`http://` site | Application Passwords are disabled without HTTPS → the user must enable HTTPS |
45
54
  | 403 | `rest_cannot_create` / insufficient role | The user's role can't publish; needs Author/Editor/Admin, or Application Passwords are disabled on the site |
46
55
  | 404 | `rest_no_route` | REST API disabled or a security plugin blocks `/wp-json` → the user must re-enable it |
47
56
  | 400 | `rest_invalid_param` | Bad field (e.g. unknown category id) → fix and retry |
57
+ | 500 | `rest_upload_sideload_error` | `wp-content/uploads` isn't writable by the web server → the user must fix directory permissions |
58
+
59
+ > **`content` is HTML, not Markdown.** Raw Markdown renders literally. `pandoc` is
60
+ > **not installed** — convert with Python's `markdown` package (preinstalled in the
61
+ > sandbox). If the import ever fails, `pip install markdown` first, then:
62
+ >
63
+ > ```bash
64
+ > HTML=$(python3 -c "
65
+ > import sys, markdown
66
+ > print(markdown.markdown(sys.stdin.read(), extensions=['fenced_code','tables']))
67
+ > " <<'MD'
68
+ > ## 标题
69
+ >
70
+ > 正文 **粗体**
71
+ > MD
72
+ > )
73
+ > ```
48
74
 
49
- > **`content` is HTML, not Markdown.** Convert Markdown to HTML first
50
- > (`pandoc -f markdown -t html`, or a simple converter). Raw Markdown renders literally.
51
75
 
52
76
  ## Step 0 — verify the connection first
53
77
 
54
78
  ```bash
55
- "${WP[@]}" "$API/users/me" | jq '{id, name, slug, roles: (.roles // [])}'
79
+ "${WP[@]}" "$API/users/me?context=edit" | jq '{id, name, slug, roles}'
56
80
  ```
57
81
 
58
- A 200 with your user object confirms the site URL, username, and Application
59
- Password all work. If this fails, stop and surface the error — don't attempt writes.
82
+ `context=edit` is required — without it WP omits `roles`/`capabilities` entirely,
83
+ so you cannot tell whether the account may publish. A 200 with `roles` containing
84
+ `administrator`, `editor` or `author` confirms the site URL, username, Application
85
+ Password **and** publish permission. If this fails, stop and surface the error —
86
+ don't attempt writes.
60
87
 
61
88
  ## Publish or draft a post
62
89
 
@@ -87,6 +114,62 @@ jq -n --arg t "标题" --arg c "<p>正文</p>" --arg e "一句话摘要" \
87
114
  - Update a post: `POST $API/posts/<id>` with any subset of fields (WP REST uses
88
115
  POST, not PUT, for updates).
89
116
  - Delete (trash) a post: `"${WP[@]}" -X DELETE "$API/posts/<id>"`.
117
+ - Schedule a post: `{"status":"future","date_gmt":"2030-01-01T00:00:00"}` (UTC,
118
+ no trailing `Z`).
119
+
120
+ ## SEO fields that actually matter
121
+
122
+ WordPress core emits `<link rel="canonical">` on its own but ships **no meta
123
+ description tag at all** — that only appears if the site runs an SEO plugin
124
+ (Yoast, Rank Math, SEOPress) or a theme that renders one. Those consume the
125
+ post's `excerpt`, so setting `excerpt` is what makes a good description possible;
126
+ it does nothing on a bare core install. For an SEO post always set:
127
+
128
+ | Field | Why |
129
+ |---|---|
130
+ | `slug` | The permalink. Set it explicitly to a short ASCII keyword phrase — otherwise a CJK title becomes a percent-encoded URL. |
131
+ | `excerpt` | Source for the SEO plugin's meta description and for list-page summaries. One sentence. |
132
+ | `categories` / `tags` | Internal linking + topic clustering. |
133
+ | `featured_media` | Social/OG card image. |
134
+ | media `alt_text` | Image SEO + accessibility. Set it on the media object (below). |
135
+
136
+
137
+ ```bash
138
+ jq -n --arg s "claude-api-guide" --argjson c 3 --argjson m 9 \
139
+ '{title:"如何稳定调用 Claude API:完整对接指南",
140
+ slug:$s,
141
+ content:"<h2>小标题</h2><p>正文</p>",
142
+ excerpt:"一句话摘要,用于 meta description。",
143
+ status:"publish", categories:[$c], featured_media:$m}' \
144
+ | "${WP[@]}" -X POST "$API/posts" -H "Content-Type: application/json" -d @- \
145
+ | jq '{id, status, slug, link}'
146
+ ```
147
+
148
+ ## Before publishing: check for a duplicate
149
+
150
+ **WordPress will NOT reject a duplicate slug — it silently appends `-2`,** so an
151
+ unattended/scheduled run that reposts the same article creates an endless trail of
152
+ near-identical URLs that compete with each other in search. Always pre-check:
153
+
154
+ ```bash
155
+ SLUG="claude-api-guide"
156
+ # Fail CLOSED at BOTH steps: a failed request, or a 200 that isn't a JSON array
157
+ # (HTML from a redirect/permalink issue, or an error object), must abort — never
158
+ # fall through and create the duplicate this check exists to prevent.
159
+ if ! LOOKUP=$("${WP[@]}" "$API/posts?slug=$SLUG&status=publish,draft,future&_fields=id,link"); then
160
+ echo "duplicate check failed (request error) — aborting" >&2; exit 1
161
+ fi
162
+ if ! EXISTING=$(printf '%s' "$LOOKUP" | jq -er 'if type=="array" then (.[0].id // "") else error("not a JSON array") end'); then
163
+ echo "duplicate check failed (unexpected response) — aborting" >&2; exit 1
164
+ fi
165
+ if [ -n "$EXISTING" ]; then
166
+ echo "already exists as post $EXISTING — update it instead of creating a new one"
167
+ # update: "${WP[@]}" -X POST "$API/posts/$EXISTING" ...
168
+ fi
169
+ ```
170
+
171
+ Prefer **updating** the existing post over creating a near-duplicate. This matters
172
+ most in Scheduled Tasks, where nobody is watching the output.
90
173
 
91
174
  ## List / read posts
92
175
 
@@ -125,22 +208,40 @@ MEDIA_ID=$("${WP[@]}" -X POST "$API/media" \
125
208
  -H "Content-Type: image/png" \
126
209
  --data-binary @"$FILE" | jq -r '.id')
127
210
  echo "media id=$MEDIA_ID"
211
+
212
+ # Set alt text (image SEO + accessibility) — a separate call on the media object.
213
+ jq -n --arg a "Claude API 架构图" '{alt_text:$a}' \
214
+ | "${WP[@]}" -X POST "$API/media/$MEDIA_ID" \
215
+ -H "Content-Type: application/json" -d @- | jq '{id, alt_text}'
216
+
128
217
  # Attach as the post's featured image:
129
218
  # add "featured_media": <MEDIA_ID> to the post body.
130
219
  ```
131
220
 
132
221
  ## Gotchas
133
222
 
134
- - **HTTPS + Application Passwords are required.** On plain `http://`, WordPress
135
- disables Application Passwords → every call 401s. Tell the user to enable HTTPS.
223
+ - **HTTPS is effectively required for Application Passwords.** WP gates them on
224
+ `wp_is_application_passwords_available()`, which is false on a plain-`http://`
225
+ production site → authenticated calls fail with `rest_not_logged_in` (401). (The
226
+ gate is filterable and is bypassed when `WP_ENVIRONMENT_TYPE` is `local`, so a
227
+ dev box may still work — but any real site the user connects must serve HTTPS.)
228
+ - **`$WORDPRESS_SITE_URL` must exactly match the site's configured address.** If
229
+ it differs (missing/extra `www`, `http` vs `https`), WP answers `/wp-json/...`
230
+ with a **301** to the canonical host. Fix the stored site URL in the connector
231
+ rather than papering over it with `-L`.
232
+
136
233
  - **A security plugin / host may block `/wp-json`** (Wordfence, "disable REST
137
234
  API" plugins, some managed hosts). Symptom: 404 `rest_no_route` or an HTML
138
235
  login page instead of JSON. The user must allow REST API access.
236
+ - **Pretty permalinks may be off.** If `/wp-json/wp/v2/...` returns the site's
237
+ HTML instead of JSON, the site is on plain permalinks — use the always-available
238
+ query form instead: `$SITE/?rest_route=/wp/v2/posts`.
139
239
  - **The Application Password contains spaces** (e.g. `abcd efgh ijkl mnop`).
140
240
  Keep them — `curl -u` handles the spaces fine; don't strip them.
141
241
  - **Never publish silently.** Even if the user says "post it", prefer creating a
142
242
  draft and returning the `wp-admin` edit link unless they explicitly asked to
143
- go live.
243
+ go live. (In an unattended Scheduled Task the user has pre-authorized the run,
244
+ so publishing directly is expected there — but still run the duplicate check.)
144
245
 
145
246
 
146
247
  ## Record the output