@acedatacloud/skills 2026.726.3 → 2026.726.5

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.5",
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",
@@ -5,8 +5,8 @@ when_to_use: |
5
5
  Trigger when the user wants Google Ads reporting — list accessible
6
6
  customers, campaign / ad-group / keyword performance, spend and
7
7
  conversions. Read via GAQL. Needs the OAuth token PLUS a platform
8
- developer token and a login-customer-id; if those env vars are
9
- absent the connector isn't fully provisioned yet — tell the user.
8
+ developer token; if the developer token is absent the connector
9
+ isn't fully provisioned yet — tell the user.
10
10
  connections: [google/ads]
11
11
  allowed_tools: [Bash]
12
12
  license: Apache-2.0
@@ -15,26 +15,31 @@ metadata:
15
15
  version: "1.0"
16
16
  ---
17
17
 
18
- Query the **Google Ads API** via `curl + jq`. Three credentials are needed:
18
+ Query the **Google Ads API** via `curl + jq`. Two credentials plus one context
19
+ header:
19
20
 
20
21
  - `$GOOGLE_ADS_TOKEN` — the user's OAuth bearer (`adwords` scope) →
21
22
  `Authorization: Bearer $GOOGLE_ADS_TOKEN`
22
23
  - `$GOOGLE_ADS_DEVELOPER_TOKEN` — the platform's developer token (injected
23
24
  server-side) → header `developer-token: $GOOGLE_ADS_DEVELOPER_TOKEN`
24
- - `login-customer-id` — the manager (MCC) id under which calls are made; use the
25
- target customer id, or `$GOOGLE_ADS_LOGIN_CUSTOMER_ID` if set (digits only, no
26
- dashes).
25
+ - `login-customer-id` — **only for manager (MCC) scoped calls**: the manager id
26
+ the request runs under, or `$GOOGLE_ADS_LOGIN_CUSTOMER_ID` if set (digits
27
+ only, no dashes). Not needed by `customers:listAccessibleCustomers`.
27
28
 
28
29
  > **API version:** the base is `https://googleads.googleapis.com/<vNN>`. Google
29
- > ships a new `vNN` every ~4 months and retires old ones — set `VER` to the
30
- > **current** supported version (check developers.google.com/google-ads/api
31
- > release notes); the example uses `v18`.
30
+ > ships a new `vNN` every ~4 months and retires old ones, so a hardcoded version
31
+ > eventually stops working. The example uses `v25` (supported as of 2026-07). If
32
+ > **every** call 404s, the version is unavailable — either retired or not yet
33
+ > released. Probe `customers:listAccessibleCustomers`: a supported version
34
+ > answers 401/200, an unavailable one 404. Step **down** one version at a time
35
+ > from the example, and check Google's supported-versions table before assuming
36
+ > a newer `vNN` exists.
32
37
 
33
38
  If `$GOOGLE_ADS_DEVELOPER_TOKEN` is empty, the connector isn't fully provisioned —
34
39
  say so rather than calling the API (it would 401/DEVELOPER_TOKEN_NOT_APPROVED).
35
40
 
36
41
  ```bash
37
- VER="v18"; BASE="https://googleads.googleapis.com/$VER"
42
+ VER="v25"; BASE="https://googleads.googleapis.com/$VER"
38
43
  AUTH="Authorization: Bearer $GOOGLE_ADS_TOKEN"; DEV="developer-token: $GOOGLE_ADS_DEVELOPER_TOKEN"
39
44
  # Customers the OAuth user can access (ids are returned as customers/<id>)
40
45
  curl -sS -H "$AUTH" -H "$DEV" "$BASE/customers:listAccessibleCustomers" | jq '.resourceNames'
@@ -44,7 +49,10 @@ curl -sS -H "$AUTH" -H "$DEV" "$BASE/customers:listAccessibleCustomers" | jq '.r
44
49
 
45
50
  ```bash
46
51
  CID="1234567890" # target customer id, digits only
47
- curl -sS -H "$AUTH" -H "$DEV" -H "login-customer-id: ${GOOGLE_ADS_LOGIN_CUSTOMER_ID:-$CID}" \
52
+ # Manager (MCC) access → send login-customer-id; direct access → drop the header.
53
+ # Array, not a plain string: the header value contains a space and would split.
54
+ LOGIN=(); [ -n "$GOOGLE_ADS_LOGIN_CUSTOMER_ID" ] && LOGIN=(-H "login-customer-id: $GOOGLE_ADS_LOGIN_CUSTOMER_ID")
55
+ curl -sS -H "$AUTH" -H "$DEV" "${LOGIN[@]}" \
48
56
  -H "Content-Type: application/json" -d '{
49
57
  "query":"SELECT campaign.name, metrics.cost_micros, metrics.clicks, metrics.conversions FROM campaign WHERE segments.date DURING LAST_30_DAYS ORDER BY metrics.cost_micros DESC"
50
58
  }' "$BASE/customers/$CID/googleAds:searchStream" \
@@ -56,8 +64,14 @@ GAQL resources: `campaign`, `ad_group`, `ad_group_criterion` (keywords),
56
64
 
57
65
  ## Gotchas
58
66
 
59
- - **Three headers, not one.** Missing `developer-token` or `login-customer-id`
60
- is the #1 cause of 401/403 here.
67
+ - **A 404 on *every* endpoint points at `VER`, not at credentials** — the
68
+ version is retired or unreleased. A 404 on a *single* call is about that call:
69
+ a missing resource (bad customer id) or a wrong path. Sanity-check the version
70
+ with `customers:listAccessibleCustomers` before debugging auth.
71
+ - **Two credentials plus a context header.** `developer-token` is required on
72
+ every call; `login-customer-id` is only needed when acting under a manager
73
+ (MCC) account — `customers:listAccessibleCustomers` works without it. Omitting
74
+ it on a manager-scoped call is the #1 cause of 401/403 here.
61
75
  - Customer ids are **digits only** in URLs/headers (strip the dashes from
62
76
  `123-456-7890`).
63
77
  - `searchStream` returns an array of chunks each with `.results[]` — flatten with
@@ -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