@acedatacloud/skills 2026.805.1 → 2026.806.0
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 +1 -1
- package/skills/tiktok/SKILL.md +10 -1
- package/skills/tiktok/scripts/tiktok.py +111 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acedatacloud/skills",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.806.0",
|
|
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",
|
package/skills/tiktok/SKILL.md
CHANGED
|
@@ -46,7 +46,16 @@ they will fail with `scope_not_authorized`.
|
|
|
46
46
|
|
|
47
47
|
This is the working path. The video lands in the creator's TikTok **inbox /
|
|
48
48
|
drafts**; they open the TikTok app to add caption, sound and privacy, then post.
|
|
49
|
-
|
|
49
|
+
|
|
50
|
+
Required order:
|
|
51
|
+
|
|
52
|
+
1. Download the exact video locally, but do not upload it yet.
|
|
53
|
+
2. Call `request_action_confirmation` with `kind: "generic"`, the real video
|
|
54
|
+
preview, and a summary that says this uploads to drafts rather than publishing.
|
|
55
|
+
3. If cancelled, stop. If confirmed, run exactly one upload:
|
|
56
|
+
`python3 skills/tiktok/scripts/tiktok.py upload video.mp4`.
|
|
57
|
+
4. Poll with `python3 skills/tiktok/scripts/tiktok.py status PUBLISH_ID` until
|
|
58
|
+
terminal. Never call `upload` again while polling.
|
|
50
59
|
|
|
51
60
|
Two source modes. **`FILE_UPLOAD` is the reliable one** — `PULL_FROM_URL` only
|
|
52
61
|
works from a domain verified in the developer portal, and ours is not verified
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Upload one video to TikTok inbox using FILE_UPLOAD, or fetch status."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import urllib.error
|
|
11
|
+
import urllib.request
|
|
12
|
+
|
|
13
|
+
BASE_URL = "https://open.tiktokapis.com/v2"
|
|
14
|
+
MAX_CHUNK_SIZE = 64 * 1024 * 1024
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def output(value: object) -> None:
|
|
18
|
+
print(json.dumps(value, ensure_ascii=False, indent=2))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def fail(message: str) -> None:
|
|
22
|
+
output({"ok": False, "error": message})
|
|
23
|
+
raise SystemExit(1)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def token() -> str:
|
|
27
|
+
value = os.environ.get("TIKTOK_TOKEN")
|
|
28
|
+
if not value:
|
|
29
|
+
fail("TIKTOK_TOKEN is not set — reconnect TikTok and retry")
|
|
30
|
+
return value
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def api(path: str, body: dict) -> dict:
|
|
34
|
+
request = urllib.request.Request(
|
|
35
|
+
f"{BASE_URL}{path}",
|
|
36
|
+
data=json.dumps(body).encode(),
|
|
37
|
+
headers={"Authorization": f"Bearer {token()}", "Content-Type": "application/json"},
|
|
38
|
+
method="POST",
|
|
39
|
+
)
|
|
40
|
+
try:
|
|
41
|
+
with urllib.request.urlopen(request, timeout=30) as response:
|
|
42
|
+
payload = json.loads(response.read().decode())
|
|
43
|
+
except urllib.error.HTTPError as error:
|
|
44
|
+
raw = error.read().decode("utf-8", "replace")
|
|
45
|
+
try:
|
|
46
|
+
payload = json.loads(raw)
|
|
47
|
+
except json.JSONDecodeError:
|
|
48
|
+
fail(f"TikTok returned HTTP {error.code}")
|
|
49
|
+
except urllib.error.URLError as error:
|
|
50
|
+
fail(f"network error reaching TikTok: {error.reason}")
|
|
51
|
+
api_error = payload.get("error") or {}
|
|
52
|
+
if api_error.get("code") not in (None, "ok"):
|
|
53
|
+
fail(f"TikTok API error {api_error.get('code')}: {api_error.get('message') or 'request failed'}")
|
|
54
|
+
return payload.get("data") or {}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def upload_file(path: str) -> dict:
|
|
58
|
+
size = os.path.getsize(path)
|
|
59
|
+
if size <= 0:
|
|
60
|
+
fail("video file is empty")
|
|
61
|
+
if size > MAX_CHUNK_SIZE:
|
|
62
|
+
fail("video exceeds the 64 MB single-upload limit")
|
|
63
|
+
init = api(
|
|
64
|
+
"/post/publish/inbox/video/init/",
|
|
65
|
+
{"source_info": {"source": "FILE_UPLOAD", "video_size": size, "chunk_size": size, "total_chunk_count": 1}},
|
|
66
|
+
)
|
|
67
|
+
upload_url = init.get("upload_url")
|
|
68
|
+
publish_id = init.get("publish_id")
|
|
69
|
+
if not upload_url or not publish_id:
|
|
70
|
+
fail("TikTok init response did not contain upload_url and publish_id")
|
|
71
|
+
with open(path, "rb") as video:
|
|
72
|
+
body = video.read()
|
|
73
|
+
request = urllib.request.Request(
|
|
74
|
+
upload_url,
|
|
75
|
+
data=body,
|
|
76
|
+
headers={
|
|
77
|
+
"Content-Type": "video/mp4",
|
|
78
|
+
"Content-Length": str(size),
|
|
79
|
+
"Content-Range": f"bytes 0-{size - 1}/{size}",
|
|
80
|
+
},
|
|
81
|
+
method="PUT",
|
|
82
|
+
)
|
|
83
|
+
try:
|
|
84
|
+
with urllib.request.urlopen(request, timeout=120) as response:
|
|
85
|
+
status = response.status
|
|
86
|
+
except urllib.error.HTTPError as error:
|
|
87
|
+
status = error.code
|
|
88
|
+
except urllib.error.URLError as error:
|
|
89
|
+
fail(f"network error uploading video: {error.reason}")
|
|
90
|
+
if status != 201:
|
|
91
|
+
fail(f"TikTok upload returned HTTP {status}, expected 201")
|
|
92
|
+
return {"publish_id": publish_id, "status": "UPLOADED"}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def main() -> None:
|
|
96
|
+
parser = argparse.ArgumentParser()
|
|
97
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
98
|
+
upload = sub.add_parser("upload")
|
|
99
|
+
upload.add_argument("file")
|
|
100
|
+
status = sub.add_parser("status")
|
|
101
|
+
status.add_argument("publish_id")
|
|
102
|
+
args = parser.parse_args()
|
|
103
|
+
if args.command == "upload":
|
|
104
|
+
data = upload_file(args.file)
|
|
105
|
+
else:
|
|
106
|
+
data = api("/post/publish/status/fetch/", {"publish_id": args.publish_id})
|
|
107
|
+
output({"ok": True, "data": data})
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
main()
|