@acedatacloud/skills 2026.726.6 → 2026.726.7

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.6",
3
+ "version": "2026.726.7",
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",
@@ -42,6 +42,8 @@ python3 "$BILI" whoami # who is logged in (mid, name)
42
42
  python3 "$BILI" articles --limit 20 # my 专栏 articles + stats
43
43
  python3 "$BILI" article <cvid> # one article's stats (cv id)
44
44
  python3 "$BILI" drafts --limit 50 # list saved drafts (aid + title)
45
+ python3 "$BILI" status --limit 10 # review state of recent submissions
46
+ python3 "$BILI" categories # 分类 names accepted by --category
45
47
  ```
46
48
 
47
49
  Stats come straight from Bilibili: `view` (阅读), `like` (点赞), `reply` (评论),
@@ -69,12 +71,41 @@ BILI="$SKILL_DIR/scripts/bilibili.py"; [ -f "$BILI" ] || BILI=$(find /tmp -maxde
69
71
  python3 "$BILI" publish --title "标题" --content-file a.html # dry-run
70
72
  python3 "$BILI" publish --title "标题" --content-file a.html --draft-only --confirm # save a draft
71
73
  python3 "$BILI" publish --title "标题" --content-file a.html --confirm # save draft + submit (publish)
74
+ python3 "$BILI" publish --title "标题" --content-file a.html --category 数码 --confirm # pick the 分类
72
75
  ```
73
76
 
74
77
  - `--draft-only` saves a draft (no submit) — safe; finish/publish in the editor.
78
+ Prefer it when the user has not clearly asked to go public: a submitted
79
+ article enters a review queue this CLI cannot withdraw it from.
75
80
  - The **submit** (go public) step is frequently rate-limited by Bilibili
76
81
  risk-control (HTTP 412). When that happens the CLI reports the saved draft +
77
- edit URL so the user can publish from the web editor. Default to `--draft-only`.
82
+ edit URL so the user can publish from the web editor.
83
+ - `--category` picks the 分类 (default **数码**, which suits technical posts).
84
+ Run `categories` for the accepted names. The result echoes the 分类 actually
85
+ used as `category` / `category_id` — report it when it wasn't the user's pick.
86
+
87
+ ### Publishing is not instant — it enters a review queue
88
+
89
+ A successful `submit` returns `state: -2` (**待审核**) and `pending_review: true`.
90
+ The returned `url` **404s for everyone until Bilibili approves it** (usually
91
+ minutes to hours). Tell the user it is pending; do not claim it is live, and do
92
+ not re-submit. Check later with `status`, which is the only view that shows
93
+ pending/rejected articles (the public list omits them):
94
+
95
+ ```sh
96
+ python3 "$BILI" status --limit 5 # state_desc: 待审核 / 已发布 / 未通过 (+ reason)
97
+ ```
98
+
99
+ Other states are **not** pending and will never go live on their own — the CLI
100
+ returns them with `ok: false` and `published: false` (`-1` 未通过 rejected, `-3`
101
+ 锁定, `-4` 已删除). Read `state_desc`, tell the user plainly, and check `status`
102
+ for the `reason` rather than re-submitting.
103
+
104
+ The `id` in the publish result is the **article id** (use `cv<id>`), which is a
105
+ different number from `draft_aid`. Only ever share the `url` field. If Bilibili
106
+ returned no article id, `url` and `id` are `null` and `id_unverified: true` is
107
+ set — there is nothing shareable yet, so find the article with `status` instead
108
+ of constructing a link from `draft_aid`.
78
109
 
79
110
  ## Managing drafts (the 999-draft cap)
80
111
 
@@ -108,8 +139,8 @@ skips this.
108
139
  - **This is the user's real Bilibili account.** Confirm before any publish.
109
140
  - **submit may 412** (anti-bot) even when the draft saved fine — the draft is the
110
141
  reliable result; don't loop-retry submit.
111
- - A wrong cover layout (`tid`) / category returns `-17`; the CLI auto-retries
112
- common `tid` values.
142
+ - A 分类 the account can't post to returns `-17`; without `--category` the CLI
143
+ auto-retries the fallback list.
113
144
  - **Never print `BILIBILI_COOKIES`** — it is full account access.
114
145
  - **ToS**: acts only on the user's own account with their own captured cookie.
115
146
 
@@ -123,6 +154,14 @@ After you successfully publish and obtain the live result URL, call the built-in
123
154
  publish_artifact(kind="article", channel="bilibili", title="<title>", url="<the REAL returned URL>", status="delivered")
124
155
  ```
125
156
 
126
- Use the real returned URL — never fabricate one. Call it once per published item,
127
- only after delivery is confirmed; skip it (or use `status="failed"`) if publishing failed.
157
+ Use the real returned URL — never fabricate one. That is the `url` field of the
158
+ publish result (built from the article `id`, **not** `draft_aid`). Call it once
159
+ per published item, only after delivery is confirmed.
160
+
161
+ - `state: 0` or `1` (已发布, live) → `status="delivered"`.
162
+ - `pending_review: true` (待审核) → `status="draft"`; the URL is not live yet.
163
+ Re-record as delivered later if the user asks you to re-check with `status`.
164
+ - rejected/locked states, a 412-blocked submit, `id_unverified: true`, or
165
+ `state_unknown: true` → `status="failed"` (or skip it).
166
+
128
167
  See `_shared/artifacts.md`.
@@ -277,8 +277,30 @@ def cmd_article(jar, args):
277
277
  })
278
278
 
279
279
 
280
- # tid = cover layout; a wrong one returns code -17 / "分类". Try common ones.
281
- _TID_CANDIDATES = ["4", "3", "6", "7", "2", "17", "28", "41"]
280
+ # The article's 分类 (from /x/article/categories). This is the `category` form
281
+ # field — `tid` alone does NOT set it (a hardcoded category=0 always lands in
282
+ # 生活 regardless of tid). Sub-category ids only; a parent id is rejected.
283
+ _CATEGORIES = {
284
+ "数码": "26", "科技": "26", "tech": "26",
285
+ "学习": "34", "人文历史": "25", "自然": "33", "汽车": "27",
286
+ "日常": "15", "生活": "15", "美食": "13", "时尚": "14", "运动": "22", "萌宠": "21",
287
+ "绘画": "23", "手工": "24", "摄影": "38", "音乐舞蹈": "39", "模型手办": "11",
288
+ "动漫杂谈": "4", "动漫资讯": "5", "动画技术": "31",
289
+ "单机游戏": "6", "电子竞技": "7", "手机游戏": "8", "网络游戏": "9", "桌游棋牌": "10",
290
+ "电影": "12", "电视剧": "35", "纪录片": "36", "综艺": "37",
291
+ "原创连载": "18", "同人连载": "19", "短篇小说": "32", "小说杂谈": "20",
292
+ }
293
+ # Fallback order when --category is not given: 数码 first (most of our content
294
+ # is technical), then broad ones. A category the account can't post to → -17.
295
+ _TID_CANDIDATES = ["26", "34", "15", "4", "6", "12", "23"]
296
+
297
+ # A creative article's `state` (what 投稿管理 shows). Only 0/1 are actually live.
298
+ _STATES = {0: "已发布", 1: "已发布", -1: "未通过", -2: "待审核", -3: "锁定", -4: "已删除"}
299
+
300
+ # id → canonical display name (first alias wins; 数码/科技/tech all map to 26).
301
+ _CATEGORY_NAMES = {}
302
+ for _name, _cid in _CATEGORIES.items():
303
+ _CATEGORY_NAMES.setdefault(_cid, _name)
282
304
 
283
305
 
284
306
  # ── image upload (Bilibili hotlink-blocks external imgs; re-host via
@@ -545,6 +567,19 @@ def md_to_html(src):
545
567
  return "\n".join(out)
546
568
 
547
569
 
570
+ def _resolve_categories(raw):
571
+ """--category → the single 分类 id to use, or the fallback list if unset."""
572
+ if not raw:
573
+ return _TID_CANDIDATES, None
574
+ name = raw.strip()
575
+ cid = _CATEGORIES.get(name)
576
+ if not cid:
577
+ if not name.isdigit():
578
+ die(f"unknown --category {raw!r}; known: " + ", ".join(sorted(_CATEGORIES)))
579
+ cid = name
580
+ return [cid], cid # explicit choice: don't silently fall back to another 分类
581
+
582
+
548
583
  def cmd_publish(jar, args):
549
584
  if not args.title:
550
585
  die("--title is required")
@@ -562,14 +597,22 @@ def cmd_publish(jar, args):
562
597
  if not csrf:
563
598
  die("no bili_jct cookie (CSRF token) — reconnect Bilibili.")
564
599
 
600
+ # Resolve before the dry-run so a typo'd 分类 is caught in the preview,
601
+ # and before rehost_images so a bad one can't burn uploads then abort.
602
+ cids, explicit = _resolve_categories(args.category)
603
+
565
604
  if not CONFIRM:
566
605
  out({
567
606
  "dry_run": True, "command": "publish", "platform": "bilibili",
568
607
  "title": args.title, "draft_only": args.draft_only,
608
+ "category": (_CATEGORY_NAMES.get(explicit, explicit) if explicit
609
+ else "(auto: 数码)"),
569
610
  "content_bytes": len(content),
570
611
  "note": "Bilibili 专栏 content is HTML. Re-run with --confirm as the "
571
- "LAST argument to write. The submit step is often 412-limited; "
572
- "the saved draft is the reliable result.",
612
+ "LAST argument to write. Published articles enter Bilibili's "
613
+ "review queue (state -2) before going public; the submit step "
614
+ "is also often 412-limited, in which case the saved draft is "
615
+ "the reliable result.",
573
616
  })
574
617
  return
575
618
 
@@ -585,13 +628,15 @@ def cmd_publish(jar, args):
585
628
  ref = "https://member.bilibili.com/"
586
629
  base = {
587
630
  "title": args.title, "content": content, "csrf": csrf,
588
- "category": "0", "list_id": "0", "reprint": "0", "original": "1",
631
+ "list_id": "0", "reprint": "0", "original": "1",
589
632
  "media_id": "0", "spoiler": "0", "save": "0", "pgc_id": "0",
590
633
  }
591
- # 1. save draft, retrying tid until the category is accepted
592
- aid, last = None, None
593
- for tid in _TID_CANDIDATES:
594
- body = dict(base, tid=tid)
634
+ # 1. save draft, retrying the 分类 until one is accepted
635
+ aid, chosen, last = None, None, None
636
+ for cid in cids:
637
+ # `category` is what actually sets 分类; `tid` must match or the article
638
+ # silently lands in 生活.
639
+ body = dict(base, tid=cid, category=cid)
595
640
  status, text = request("POST", f"{API}/x/article/creative/draft/addupdate",
596
641
  jar, referer=ref, form=body)
597
642
  try:
@@ -601,21 +646,22 @@ def cmd_publish(jar, args):
601
646
  continue
602
647
  if r.get("code") == 0:
603
648
  aid = (r.get("data") or {}).get("aid")
604
- chosen_tid = tid
649
+ chosen = cid
605
650
  break
606
651
  last = f"code={r.get('code')} {r.get('message')}"
607
652
  if "分类" not in str(r.get("message", "")) and r.get("code") != -17:
608
- break # a non-category error won't be fixed by another tid
653
+ break # a non-category error won't be fixed by another one
609
654
  if not aid:
610
655
  die(f"save-draft failed: {last}")
611
656
 
612
657
  if args.draft_only:
613
658
  out({"ok": True, "draft_only": True, "aid": str(aid),
659
+ "category_id": chosen, "category": _CATEGORY_NAMES.get(chosen, chosen),
614
660
  "edit_url": f"https://member.bilibili.com/article-text/home?aid={aid}"})
615
661
  return
616
662
 
617
663
  # 2. submit (publish) — may be 412 risk-controlled; report draft if so
618
- body = dict(base, tid=chosen_tid, aid=aid)
664
+ body = dict(base, tid=chosen, category=chosen, aid=aid)
619
665
  status, text = request("POST", f"{API}/x/article/creative/article/submit",
620
666
  jar, referer=ref, form=body)
621
667
  try:
@@ -623,8 +669,64 @@ def cmd_publish(jar, args):
623
669
  except json.JSONDecodeError:
624
670
  r = None
625
671
  if r and r.get("code") == 0:
626
- out({"ok": True, "published": True, "aid": str(aid),
627
- "url": f"https://www.bilibili.com/read/cv{aid}"})
672
+ # The published article gets a NEW id — the draft aid is not a cv id, so
673
+ # cv<draft_aid> 404s. Only trust an id the submit response actually gave.
674
+ data = r.get("data") or {}
675
+ try:
676
+ cvid = int(data.get("aid"))
677
+ except (TypeError, ValueError):
678
+ cvid = None
679
+ if cvid is not None and cvid <= 0:
680
+ cvid = None # article ids are positive; 0 means "not returned"
681
+ res = {"ok": True, "published": True,
682
+ "id": str(cvid) if cvid is not None else None,
683
+ "url": (f"https://www.bilibili.com/read/cv{cvid}"
684
+ if cvid is not None else None),
685
+ "draft_aid": str(aid),
686
+ "category_id": chosen,
687
+ "category": _CATEGORY_NAMES.get(chosen, chosen)}
688
+ notes = []
689
+ if cvid is None:
690
+ # Never hand back a draft-aid URL dressed up as the published one.
691
+ res["id_unverified"] = True
692
+ notes.append("submit succeeded but returned no article id — there is "
693
+ f"no shareable URL yet (draft aid {aid} is NOT a cv id). "
694
+ "Find the article with `status`.")
695
+ raw_state = data.get("state")
696
+ if isinstance(raw_state, bool):
697
+ state = None
698
+ else:
699
+ try:
700
+ state = int(raw_state)
701
+ except (TypeError, ValueError):
702
+ state = None
703
+ if state is None and raw_state is not None:
704
+ res["state_unknown"] = True
705
+ notes.append(f"submit returned an unreadable state ({raw_state!r}); "
706
+ "confirm with `status` before reporting this as live.")
707
+ if state is not None and state not in (0, 1):
708
+ res["state"] = state
709
+ res["state_desc"] = _STATES.get(state, str(state))
710
+ if state == -2:
711
+ # 待审核: the url 404s for everyone until Bilibili approves it.
712
+ res["pending_review"] = True
713
+ notes.append("submitted, now in Bilibili's review queue — the URL "
714
+ "goes live once approved (usually minutes to hours). "
715
+ "Re-check with `status`; do not re-submit.")
716
+ elif state < 0:
717
+ # -1 未通过 / -3 锁定 / -4 已删除 — NOT pending, will never go live.
718
+ res["ok"] = False
719
+ res["published"] = False
720
+ notes.append(f"submit returned state {state} "
721
+ f"({_STATES.get(state, 'unknown')}) — the article is "
722
+ "not live and is not queued. Check `status` for the "
723
+ "reason; do not report this as published.")
724
+ elif state in (0, 1):
725
+ res["state"] = state
726
+ res["state_desc"] = _STATES[state]
727
+ if notes:
728
+ res["note"] = " ".join(notes)
729
+ out(res)
628
730
  else:
629
731
  out({"ok": False, "published": False, "draft_saved": True, "aid": str(aid),
630
732
  "edit_url": f"https://member.bilibili.com/article-text/home?aid={aid}",
@@ -637,6 +739,41 @@ def _drafts_of(d: dict) -> list:
637
739
  return al.get("drafts") or []
638
740
 
639
741
 
742
+ def cmd_categories(jar, args):
743
+ by_id = {}
744
+ for name, cid in _CATEGORIES.items():
745
+ by_id.setdefault(cid, []).append(name)
746
+ cats = [{"id": cid, "names": names} for cid, names in by_id.items()]
747
+ out({"count": len(cats), "categories": cats,
748
+ "note": "pass any of the names (or the raw id) to `publish --category`; "
749
+ "the default is 数码."})
750
+
751
+
752
+ def cmd_status(jar, args):
753
+ # The creative list is the ONLY view that shows pending/rejected articles —
754
+ # the public space list omits anything not yet approved.
755
+ d = get_json(f"{API}/x/article/creative/article/list?pn=1&ps={args.limit}",
756
+ jar, referer="https://member.bilibili.com/")
757
+ if d.get("code"):
758
+ die(f"status error (code={d.get('code')}): {d.get('message')} — cookie expired?")
759
+ al = (d.get("data") or {}).get("artlist") or d.get("artlist") or {}
760
+ rows = []
761
+ for a in (al.get("articles") or [])[: args.limit]:
762
+ st = a.get("state")
763
+ cvid = a.get("id")
764
+ rows.append({
765
+ "id": str(cvid) if cvid else None,
766
+ "title": a.get("title"),
767
+ "state": st,
768
+ "state_desc": _STATES.get(st, str(st)) if isinstance(st, int) else None,
769
+ "live": isinstance(st, int) and st >= 0,
770
+ "reason": a.get("reason") or None,
771
+ "category": (a.get("category") or {}).get("name"),
772
+ "url": f"https://www.bilibili.com/read/cv{cvid}" if cvid else None,
773
+ })
774
+ out({"count": len(rows), "articles": rows})
775
+
776
+
640
777
  def cmd_drafts(jar, args):
641
778
  # 专栏 drafts are capped at 999; this lists them so they can be pruned.
642
779
  d = get_json(f"{API}/x/article/creative/draft/list?pn={args.page}&ps={args.limit}",
@@ -686,6 +823,8 @@ COMMANDS = {
686
823
  "publish": cmd_publish,
687
824
  "drafts": cmd_drafts,
688
825
  "delete-draft": cmd_delete_draft,
826
+ "categories": cmd_categories,
827
+ "status": cmd_status,
689
828
  }
690
829
 
691
830
 
@@ -702,8 +841,13 @@ def main() -> None:
702
841
  sp.add_argument("--content", help="HTML content inline")
703
842
  sp.add_argument("--content-file", help="path to an HTML file")
704
843
  sp.add_argument("--draft-only", action="store_true", help="save a draft; do NOT submit")
844
+ sp.add_argument("--category", help="分类 name (e.g. 数码, 学习, 日常) or a raw tid; "
845
+ "default tries 数码 first")
705
846
  sp.add_argument("--no-rehost-images", action="store_true",
706
847
  help="keep external image URLs as-is (skip Bilibili CDN re-host)")
848
+ sp = sub.add_parser("categories", help="list the 分类 names accepted by --category")
849
+ sp = sub.add_parser("status", help="review state of the user's recent submissions")
850
+ sp.add_argument("--limit", type=int, default=10)
707
851
  sp = sub.add_parser("drafts", help="list 专栏 drafts (id+title); use to prune the 999-draft cap")
708
852
  sp.add_argument("--limit", type=int, default=50)
709
853
  sp.add_argument("--page", type=int, default=1)