@acedatacloud/skills 2026.926.0 → 2026.926.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.926.0",
3
+ "version": "2026.926.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",
@@ -286,7 +286,17 @@ def main() -> None:
286
286
  args = build_parser().parse_args()
287
287
 
288
288
  if args.cmd == "status":
289
- _json(request("GET", "/api/status"))
289
+ status = request("GET", "/api/status")
290
+ logged_in = status.get("logged_in") is True
291
+ ready = status.get("status") == "ready" or status.get("ready") is True
292
+ if not logged_in or not ready:
293
+ status["scheduled_outcome"] = {
294
+ "version": 1,
295
+ "status": "skipped",
296
+ "code": "WECHAT_NOT_LOGGED_IN",
297
+ "side_effect_performed": False,
298
+ }
299
+ _json(status)
290
300
  elif args.cmd == "account":
291
301
  _json(request("GET", "/api/account"))
292
302
  elif args.cmd == "contacts":
@@ -198,3 +198,14 @@ def test_unattended_confirm_allows_a_preauthorized_skill(monkeypatch) -> None:
198
198
  calls, _ = run(["send", "Alice", "hi", "--unattended-confirm"], task_result={"sent": True})
199
199
 
200
200
  assert calls[0]["path"] == "/api/messages/send"
201
+
202
+
203
+ def test_status_reports_not_logged_in_as_structured_skip() -> None:
204
+ _, stdout = run(["status"], responses={"/api/status": {"status": "ready", "logged_in": False}})
205
+
206
+ assert stdout[0]["scheduled_outcome"] == {
207
+ "version": 1,
208
+ "status": "skipped",
209
+ "code": "WECHAT_NOT_LOGGED_IN",
210
+ "side_effect_performed": False,
211
+ }
@@ -300,10 +300,21 @@ async def cmd_whoami(client, args):
300
300
  if expected:
301
301
  expected_screen_name = normalize_screen_name(expected)
302
302
  if authenticated_screen_name.casefold() != expected_screen_name.casefold():
303
- die(
304
- f"connected X account is @{authenticated_screen_name}, not @{expected_screen_name}; "
305
- "stopped without performing any write"
303
+ out(
304
+ {
305
+ "error": (
306
+ f"connected X account is @{authenticated_screen_name}, not @{expected_screen_name}; "
307
+ "stopped without performing any write"
308
+ ),
309
+ "scheduled_outcome": {
310
+ "version": 1,
311
+ "status": "skipped",
312
+ "code": "ACCOUNT_MISMATCH_SAFE_STOP",
313
+ "side_effect_performed": False,
314
+ },
315
+ }
306
316
  )
317
+ raise SystemExit(1)
307
318
  u, client = await retry_flaky_not_found(
308
319
  client,
309
320
  lambda c: c.get_user_by_screen_name(authenticated_screen_name),
@@ -64,20 +64,14 @@ YouTube has no "list my videos" call directly — read the channel's
64
64
  **uploads playlist**, then page its items.
65
65
 
66
66
  ```bash
67
- # 1. Get the uploads playlist id (UU... ) — same as channels call above.
68
- UPLOADS=$(curl -sS -H "Authorization: Bearer $GOOGLE_YOUTUBE_TOKEN" \
69
- "https://www.googleapis.com/youtube/v3/channels?part=contentDetails&mine=true" \
70
- | jq -r '.items[0].contentDetails.relatedPlaylists.uploads')
71
-
72
- # 2. List recent uploads (50/page; follow .nextPageToken for more).
73
- curl -sS -H "Authorization: Bearer $GOOGLE_YOUTUBE_TOKEN" \
74
- -G "https://www.googleapis.com/youtube/v3/playlistItems" \
75
- --data-urlencode "part=snippet,contentDetails" \
76
- --data-urlencode "playlistId=$UPLOADS" \
77
- --data-urlencode "maxResults=50" \
78
- | jq '.items[] | {videoId: .contentDetails.videoId, title: .snippet.title, published: .snippet.publishedAt}'
67
+ python3 skills/youtube/scripts/list_uploads.py --max-pages 20
79
68
  ```
80
69
 
70
+ The helper follows every `nextPageToken` until the upload history is complete
71
+ (or the explicit safety limit is reached). Its compact JSON keeps `videoId`,
72
+ `title`, and `published` for each item, plus `complete`, `pages_fetched`, and
73
+ `next_page_token`. Do not claim a video is absent when `complete` is false.
74
+
81
75
  Paginate by passing `--data-urlencode "pageToken=$PAGE_TOKEN"` with the
82
76
  `.nextPageToken` from the previous response.
83
77
 
@@ -175,7 +169,20 @@ RESULT=$(curl -sS -H "Authorization: Bearer $GOOGLE_YOUTUBE_TOKEN" \
175
169
  -X PUT --upload-file "$FILE" "$UPLOAD_URL")
176
170
  echo "$RESULT" | jq -e .id >/dev/null 2>&1 \
177
171
  || { echo "upload failed: $(echo "$RESULT" | jq -r '.error.message' 2>/dev/null || echo "$RESULT")"; exit 1; }
178
- echo "$RESULT" | jq '{id: .id, url: ("https://www.youtube.com/watch?v=" + .id), privacy: .status.privacyStatus}'
172
+ VIDEO_ID=$(echo "$RESULT" | jq -r '.id')
173
+ VERIFY=$(curl -sS -H "Authorization: Bearer $GOOGLE_YOUTUBE_TOKEN" \
174
+ -G "https://www.googleapis.com/youtube/v3/videos" \
175
+ --data-urlencode "part=snippet,status,processingDetails" \
176
+ --data-urlencode "id=$VIDEO_ID")
177
+ echo "$VERIFY" | jq -e '.items[0].id == "'"$VIDEO_ID"'"' >/dev/null \
178
+ || { echo "upload readback failed for $VIDEO_ID"; exit 1; }
179
+ echo "$VERIFY" | jq '.items[0] | {
180
+ video_id: .id,
181
+ canonical_url: ("https://www.youtube.com/watch?v=" + .id),
182
+ privacy: .status.privacyStatus,
183
+ upload_status: .status.uploadStatus,
184
+ processing_status: .processingDetails.processingStatus
185
+ }'
179
186
  ```
180
187
 
181
188
  Delete a downloaded temp file only **after** you've confirmed an id came back
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env python3
2
+ """List uploaded YouTube videos while preserving pagination evidence."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import os
9
+ import sys
10
+ from typing import Any
11
+ from urllib import error, parse, request
12
+
13
+ API_ROOT = "https://www.googleapis.com/youtube/v3"
14
+
15
+
16
+ def api_get(path: str, token: str, params: dict[str, str]) -> dict[str, Any]:
17
+ url = f"{API_ROOT}/{path}?{parse.urlencode(params)}"
18
+ req = request.Request(url, headers={"Authorization": f"Bearer {token}"})
19
+ try:
20
+ with request.urlopen(req, timeout=30) as response:
21
+ return json.load(response)
22
+ except error.HTTPError as exc:
23
+ body = exc.read().decode("utf-8", errors="replace")
24
+ raise RuntimeError(f"YouTube API HTTP {exc.code}: {body}") from exc
25
+
26
+
27
+ def list_uploads(token: str, *, max_pages: int = 20) -> dict[str, Any]:
28
+ channel = api_get("channels", token, {"part": "contentDetails", "mine": "true"})
29
+ items = channel.get("items") or []
30
+ if not items:
31
+ raise RuntimeError("YouTube channel not found for the connected account")
32
+ uploads = items[0]["contentDetails"]["relatedPlaylists"]["uploads"]
33
+
34
+ videos: list[dict[str, Any]] = []
35
+ page_token = ""
36
+ pages = 0
37
+ while pages < max_pages:
38
+ params = {
39
+ "part": "snippet,contentDetails",
40
+ "playlistId": uploads,
41
+ "maxResults": "50",
42
+ }
43
+ if page_token:
44
+ params["pageToken"] = page_token
45
+ payload = api_get("playlistItems", token, params)
46
+ pages += 1
47
+ for item in payload.get("items") or []:
48
+ videos.append(
49
+ {
50
+ "videoId": item.get("contentDetails", {}).get("videoId"),
51
+ "title": item.get("snippet", {}).get("title"),
52
+ "published": item.get("snippet", {}).get("publishedAt"),
53
+ }
54
+ )
55
+ page_token = str(payload.get("nextPageToken") or "")
56
+ if not page_token:
57
+ break
58
+
59
+ return {
60
+ "uploads_playlist": uploads,
61
+ "pages_fetched": pages,
62
+ "complete": not page_token,
63
+ "next_page_token": page_token or None,
64
+ "items": videos,
65
+ }
66
+
67
+
68
+ def main() -> int:
69
+ parser = argparse.ArgumentParser()
70
+ parser.add_argument("--max-pages", type=int, default=20)
71
+ args = parser.parse_args()
72
+ token = os.environ.get("GOOGLE_YOUTUBE_TOKEN", "")
73
+ if not token:
74
+ print(json.dumps({"error": "GOOGLE_YOUTUBE_TOKEN is not set"}))
75
+ return 2
76
+ try:
77
+ print(json.dumps(list_uploads(token, max_pages=max(1, min(args.max_pages, 100))), ensure_ascii=False))
78
+ except Exception as exc:
79
+ print(json.dumps({"error": str(exc)}, ensure_ascii=False))
80
+ return 1
81
+ return 0
82
+
83
+
84
+ if __name__ == "__main__":
85
+ sys.exit(main())