@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.
|
|
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
|
-
|
|
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
|
+
}
|
package/skills/x/scripts/x.py
CHANGED
|
@@ -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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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),
|
package/skills/youtube/SKILL.md
CHANGED
|
@@ -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
|
-
|
|
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 '
|
|
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())
|