@acedatacloud/skills 2026.622.0 → 2026.622.1

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.622.0",
3
+ "version": "2026.622.1",
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",
@@ -38,6 +38,7 @@ BILI=$SKILL_DIR/scripts/bilibili.py
38
38
  python3 $BILI whoami # who is logged in (mid, name)
39
39
  python3 $BILI articles --limit 20 # my 专栏 articles + stats
40
40
  python3 $BILI article <cvid> # one article's stats (cv id)
41
+ python3 $BILI drafts --limit 50 # list saved drafts (aid + title)
41
42
  ```
42
43
 
43
44
  Stats come straight from Bilibili: `view` (阅读), `like` (点赞), `reply` (评论),
@@ -70,6 +71,22 @@ python3 $BILI publish --title "标题" --content-file a.html --confirm
70
71
  risk-control (HTTP 412). When that happens the CLI reports the saved draft +
71
72
  edit URL so the user can publish from the web editor. Default to `--draft-only`.
72
73
 
74
+ ## Managing drafts (the 999-draft cap)
75
+
76
+ Bilibili caps 专栏 drafts at **999**; once full, saving a new draft fails with
77
+ `code 37106 草稿数已达最大上限`. List drafts and delete the ones you don't need:
78
+
79
+ ```sh
80
+ python3 $BILI drafts --limit 50 # list (aid + title)
81
+ python3 $BILI delete-draft <aid> <aid2> ... # dry-run (shows what would delete)
82
+ python3 $BILI delete-draft <aid> <aid2> ... --confirm # PERMANENTLY delete those drafts
83
+ ```
84
+
85
+ - `delete-draft` is **GATED** (dry-run unless trailing `--confirm`) and deletion
86
+ is **permanent** — always show the dry-run + the titles and get an explicit
87
+ "yes" before `--confirm`. Pass multiple aids to batch a few per call.
88
+ - Never bulk-delete blindly: list first, confirm the titles are junk/duplicates.
89
+
73
90
  ## Gotchas
74
91
 
75
92
  - **This is the user's real Bilibili account.** Confirm before any publish.
@@ -354,11 +354,60 @@ def cmd_publish(jar, args):
354
354
  f"web editor. Detail: {(r or {}).get('message') if r else text[:200]}"})
355
355
 
356
356
 
357
+ def _drafts_of(d: dict) -> list:
358
+ al = (d.get("data") or {}).get("artlist") or d.get("artlist") or {}
359
+ return al.get("drafts") or []
360
+
361
+
362
+ def cmd_drafts(jar, args):
363
+ # 专栏 drafts are capped at 999; this lists them so they can be pruned.
364
+ d = get_json(f"{API}/x/article/creative/draft/list?pn={args.page}&ps={args.limit}",
365
+ jar, referer="https://member.bilibili.com/")
366
+ if d.get("code"): # 0 = ok; anything truthy is an auth/API error
367
+ die(f"draft list error (code={d.get('code')}): {d.get('message')} — "
368
+ f"cookie may be expired; reconnect at https://auth.acedata.cloud/user/connections.")
369
+ items = _drafts_of(d)
370
+ out({"page": args.page, "count": len(items), "drafts": [{
371
+ "aid": x.get("id"),
372
+ "title": x.get("title"),
373
+ "edit_url": f"https://member.bilibili.com/article-text/home?aid={x.get('id')}",
374
+ } for x in items]})
375
+
376
+
377
+ def cmd_delete_draft(jar, args):
378
+ if not args.aids:
379
+ die("provide one or more draft aids: delete-draft <aid> [<aid> ...] --confirm")
380
+ bad = [a for a in args.aids if not str(a).isdigit()]
381
+ if bad:
382
+ die(f"invalid draft aid(s) — must be numeric: {bad}")
383
+ csrf = cookie_value(jar, "bili_jct")
384
+ if not csrf:
385
+ die("no bili_jct cookie (CSRF token) — reconnect Bilibili.")
386
+ if not CONFIRM:
387
+ out({"dry_run": True, "command": "delete-draft", "platform": "bilibili",
388
+ "aids": args.aids, "note": "Deletion is PERMANENT. Re-run with --confirm "
389
+ "as the LAST argument to actually delete these draft(s)."})
390
+ return
391
+ results = []
392
+ for aid in args.aids:
393
+ _, text = request("POST", f"{API}/x/article/creative/draft/delete", jar,
394
+ referer="https://member.bilibili.com/", form={"aid": aid, "csrf": csrf})
395
+ try:
396
+ code = json.loads(text).get("code")
397
+ except json.JSONDecodeError:
398
+ code = None
399
+ results.append({"aid": aid, "deleted": code == 0, "code": code})
400
+ out({"command": "delete-draft", "deleted": sum(1 for r in results if r["deleted"]),
401
+ "results": results})
402
+
403
+
357
404
  COMMANDS = {
358
405
  "whoami": cmd_whoami,
359
406
  "articles": cmd_articles,
360
407
  "article": cmd_article,
361
408
  "publish": cmd_publish,
409
+ "drafts": cmd_drafts,
410
+ "delete-draft": cmd_delete_draft,
362
411
  }
363
412
 
364
413
 
@@ -375,6 +424,11 @@ def main() -> None:
375
424
  sp.add_argument("--content", help="HTML content inline")
376
425
  sp.add_argument("--content-file", help="path to an HTML file")
377
426
  sp.add_argument("--draft-only", action="store_true", help="save a draft; do NOT submit")
427
+ sp = sub.add_parser("drafts", help="list 专栏 drafts (id+title); use to prune the 999-draft cap")
428
+ sp.add_argument("--limit", type=int, default=50)
429
+ sp.add_argument("--page", type=int, default=1)
430
+ sp = sub.add_parser("delete-draft", help="delete draft(s) by aid (GATED by trailing --confirm)")
431
+ sp.add_argument("aids", nargs="*", help="one or more draft aids to delete")
378
432
  args = p.parse_args(ARGV)
379
433
  jar = load_cookies()
380
434
  COMMANDS[args.command](jar, args)
@@ -71,6 +71,8 @@ publish). `--draft-only` stops at the draft (visible only at the user's
71
71
  - Markdown→Medium conversion is paragraph-level (headings, quotes, code, body);
72
72
  complex inline formatting / images aren't converted — the user can polish in
73
73
  the Medium editor before going public.
74
- - Medium sits behind Cloudflare; an occasional 403/429 is transient.
74
+ - Medium sits behind Cloudflare; an occasional 403/429 is transient — the CLI
75
+ auto-retries once after a short pause. A *persistent* 403 means the cookie is
76
+ genuinely expired (reconnect).
75
77
  - **Never print `MEDIUM_COOKIES`** — it is full account access.
76
78
  - **ToS**: acts only on the user's own account with their own captured cookie.
@@ -30,6 +30,7 @@ import json
30
30
  import os
31
31
  import re
32
32
  import sys
33
+ import time
33
34
  import urllib.error
34
35
  import urllib.parse
35
36
  import urllib.request
@@ -147,8 +148,15 @@ def request(method, url, jar, *, headers=None, body=None, accept="application/js
147
148
  die(f"network error reaching {url}: {e.reason}")
148
149
 
149
150
 
150
- def api(method, url, jar, *, body=None):
151
+ def api(method, url, jar, *, body=None, _retried=False):
151
152
  status, text = request(method, url, jar, body=body)
153
+ # Medium sits behind Cloudflare, which intermittently 403s/429s an otherwise
154
+ # valid session; one retry after a short pause clears the transient block.
155
+ # Only retry idempotent GETs — never replay a POST (new-story/deltas/publish),
156
+ # which could duplicate a write if the origin already processed it.
157
+ if status in (403, 429) and method == "GET" and not _retried:
158
+ time.sleep(1.5)
159
+ return api(method, url, jar, body=body, _retried=True)
152
160
  if status in (401, 403):
153
161
  die(f"auth failed ({status}) on {url} — cookie likely expired. "
154
162
  f"Reconnect at https://auth.acedata.cloud/user/connections.")