@acedatacloud/skills 2026.805.1 → 2026.806.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.
|
|
3
|
+
"version": "2026.806.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",
|
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()
|
|
@@ -36,13 +36,13 @@ After every navigation, upload, tab switch, modal open/close, or submission, the
|
|
|
36
36
|
3. The page opens on `上传视频` by default. Select mode by exact visible tab text — `上传图文`, `上传视频`, or `写长文` — by observing the tab strip and clicking the returned ref. Verify the selected mode after clicking; the upload area text changes (`拖拽视频到此或点击上传` for video, an image dropzone for `上传图文`).
|
|
37
37
|
4. If the tab click reports success but the mode does not change, an onboarding popover is covering the tab strip. Press `Escape`, re-observe, and click again. If it still does not change, stop and ask the user to dismiss the overlay in the visible tab — never try to delete page nodes.
|
|
38
38
|
5. Upload one approved resource at a time and wait until the visible preview count reaches the submitted count before the next resource (up to 60 seconds per image). Re-observe between images: the file input's ref changes after the first upload. For video, wait until processing completes and Publish becomes enabled, up to 10 minutes. If resource resolution is unavailable, wait for the user to select local media and verify the same preview/processing state.
|
|
39
|
-
6. Fill the image/video title using the visible title textbox (recognition hints: placeholder containing `填写标题`, then the single visible title input fallback). Titles are capped at 20 full-width-equivalent characters; a visible `n/20` counter turning over the limit is authoritative, so shorten and re-confirm rather than submitting a truncated title. Fill body in the visible rich-text editor (`输入正文描述` placeholder). Use `browser.fill` for replacement, or `browser.click` followed by `browser.type` for rich text. Read immediately after each field
|
|
39
|
+
6. Fill the image/video title using the visible title textbox (recognition hints: placeholder containing `填写标题`, then the single visible title input fallback). Titles are capped at 20 full-width-equivalent characters; a visible `n/20` counter turning over the limit is authoritative, so shorten and re-confirm rather than submitting a truncated title. Fill body in the visible rich-text editor (`输入正文描述` placeholder). Use `browser.fill` for replacement, or `browser.click` followed by `browser.type` for rich text. Read immediately after each field and compare the complete title/body with the normalized preview. If a write fails or the content differs, do not append more keystrokes: replace the whole field, read it again, and obtain renewed confirmation for any changed preview. Never publish duplicated, missing, or extra text.
|
|
40
40
|
7. Limit tags to the first 10 confirmed tags. Insert them one at a time, close any topic suggestion popover by focusing the title, and verify visible chips/text before continuing.
|
|
41
41
|
8. Configure options one at a time and verify each exact state: schedule (1 hour–14 days), visibility (`公开可见`, `仅自己可见`, `仅互关好友可见`), originality, and products. If originality was requested but cannot be confirmed, abort rather than publishing non-original. Bind a product only when the exact intended product is visibly selected; never accept a first fuzzy match silently.
|
|
42
42
|
9. For long article: choose `写长文` → `新的创作`; fill `输入标题` textarea and the body editor; click `一键排版`; enumerate visible template names; select the confirmed template and verify its selected state; click `下一步`; then fill the separate publish-page description editor.
|
|
43
43
|
10. Before the final action, read or screenshot again and compare media count, title, full body, tags, options, products, and schedule with the confirmed preview. Stop on mismatch.
|
|
44
|
-
11.
|
|
45
|
-
12. Follow [reconciliation](./reconciliation.md). Immediate success requires
|
|
44
|
+
11. After the confirmed preview, run a fresh `browser.find` for `role=button`, `name=发布`. The current declarative creator widget is exposed through that semantic contract even though its page element is custom. Inspect the returned ref and reject `submit-disabled=true`, `disabled`, `aria-disabled=true`, or disabled styling. Click that fresh ref exactly once; do not substitute another visible control when Publish is missing.
|
|
45
|
+
12. Follow [reconciliation](./reconciliation.md). Immediate success requires `/publish/success`, `published=true`, or a newly added same-title entry/note ID in note management. A changed draft count, returning to the initial publish page, or click completion alone is not success. Return the canonical note URL when visible.
|
|
46
46
|
|
|
47
47
|
Never mix image/video media unless the visible current UI explicitly supports it. Bind products only when the account visibly exposes the feature and the exact selected products appear in the final preview.
|
|
48
48
|
|