@acedatacloud/skills 2026.704.1 → 2026.704.3
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/_shared/artifacts.md +28 -0
- package/skills/bilibili/SKILL.md +14 -0
- package/skills/blogger/SKILL.md +14 -0
- package/skills/bluesky/SKILL.md +14 -0
- package/skills/csdn/SKILL.md +14 -0
- package/skills/devto/SKILL.md +14 -0
- package/skills/hashnode/SKILL.md +14 -0
- package/skills/juejin/SKILL.md +14 -0
- package/skills/linkedin/SKILL.md +14 -0
- package/skills/mastodon/SKILL.md +14 -0
- package/skills/medium/SKILL.md +14 -0
- package/skills/notion/SKILL.md +14 -0
- package/skills/reddit/SKILL.md +14 -0
- package/skills/substack/SKILL.md +14 -0
- package/skills/telegram/SKILL.md +14 -0
- package/skills/wordpress/SKILL.md +14 -0
- package/skills/x/SKILL.md +123 -0
- package/skills/x/scripts/x.py +417 -0
- package/skills/zhihu/SKILL.md +14 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acedatacloud/skills",
|
|
3
|
-
"version": "2026.704.
|
|
3
|
+
"version": "2026.704.3",
|
|
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",
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Recording produced outputs (`publish_artifact`)
|
|
2
|
+
|
|
3
|
+
Write/publish-capable Skills should record their concrete deliverable so the
|
|
4
|
+
user can later find and aggregate everything the agent produced (in the "My
|
|
5
|
+
Outputs" view at `/chatgpt/artifacts`).
|
|
6
|
+
|
|
7
|
+
After a publish/send/generate action **succeeds and you have the live result
|
|
8
|
+
URL**, call the built-in `publish_artifact` tool exactly ONCE for that
|
|
9
|
+
deliverable:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
publish_artifact(
|
|
13
|
+
kind="article", # article | image | video | audio | document | email | message | dataset | link | other
|
|
14
|
+
channel="<platform>", # e.g. zhihu, csdn, medium, x, mastodon
|
|
15
|
+
title="<human title>",
|
|
16
|
+
url="<the REAL returned URL>",
|
|
17
|
+
status="delivered" # delivered | draft | failed
|
|
18
|
+
)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Rules:
|
|
22
|
+
|
|
23
|
+
- Use the **real URL returned by the publish step** — never fabricate a URL.
|
|
24
|
+
- Call it once per distinct deliverable, right after delivery is confirmed.
|
|
25
|
+
- Do NOT call it for intermediate steps, drafts you didn't submit, or narration.
|
|
26
|
+
- If publishing failed, either skip it or record `status="failed"`.
|
|
27
|
+
|
|
28
|
+
The tool is non-interactive and works in unattended (scheduled-task) runs.
|
package/skills/bilibili/SKILL.md
CHANGED
|
@@ -106,3 +106,17 @@ skips this.
|
|
|
106
106
|
common `tid` values.
|
|
107
107
|
- **Never print `BILIBILI_COOKIES`** — it is full account access.
|
|
108
108
|
- **ToS**: acts only on the user's own account with their own captured cookie.
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
## Record the output
|
|
112
|
+
|
|
113
|
+
After you successfully publish and obtain the live result URL, call the built-in
|
|
114
|
+
`publish_artifact` tool ONCE so the user can track this deliverable in **My Outputs**:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
publish_artifact(kind="article", channel="bilibili", title="<title>", url="<the REAL returned URL>", status="delivered")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Use the real returned URL — never fabricate one. Call it once per published item,
|
|
121
|
+
only after delivery is confirmed; skip it (or use `status="failed"`) if publishing failed.
|
|
122
|
+
See `_shared/artifacts.md`.
|
package/skills/blogger/SKILL.md
CHANGED
|
@@ -78,3 +78,17 @@ curl -sS -H "Authorization: Bearer $GOOGLE_BLOGGER_TOKEN" \
|
|
|
78
78
|
or reconnect with the account that has the blog.
|
|
79
79
|
- `content` must be HTML; passing raw Markdown will render literally.
|
|
80
80
|
- Paginate with `&pageToken=$PAGE_TOKEN` from the previous `.nextPageToken`.
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
## Record the output
|
|
84
|
+
|
|
85
|
+
After you successfully publish and obtain the live result URL, call the built-in
|
|
86
|
+
`publish_artifact` tool ONCE so the user can track this deliverable in **My Outputs**:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
publish_artifact(kind="article", channel="blogger", title="<title>", url="<the REAL returned URL>", status="delivered")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Use the real returned URL — never fabricate one. Call it once per published item,
|
|
93
|
+
only after delivery is confirmed; skip it (or use `status="failed"`) if publishing failed.
|
|
94
|
+
See `_shared/artifacts.md`.
|
package/skills/bluesky/SKILL.md
CHANGED
|
@@ -112,3 +112,17 @@ extracts the `rkey`. An empty result / `deleted:true` is success.
|
|
|
112
112
|
there; all XRPC calls target that host, not `bsky.social`.
|
|
113
113
|
- The CLI creates a fresh short-lived session on **every** invocation, so an
|
|
114
114
|
expiring `accessJwt` is never a concern — just run the command again.
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
## Record the output
|
|
118
|
+
|
|
119
|
+
After you successfully publish and obtain the live result URL, call the built-in
|
|
120
|
+
`publish_artifact` tool ONCE so the user can track this deliverable in **My Outputs**:
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
publish_artifact(kind="message", channel="bluesky", title="<title>", url="<the REAL returned URL>", status="delivered")
|
|
124
|
+
```
|
|
125
|
+
|
|
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.
|
|
128
|
+
See `_shared/artifacts.md`.
|
package/skills/csdn/SKILL.md
CHANGED
|
@@ -91,3 +91,17 @@ image that fails to upload keeps its original URL (never blocks the post).
|
|
|
91
91
|
- **Never print `CSDN_COOKIES`** — it is full account access.
|
|
92
92
|
- **ToS**: cookie automation acts only on the user's own account with their own
|
|
93
93
|
captured cookie; the user owns that risk.
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
## Record the output
|
|
97
|
+
|
|
98
|
+
After you successfully publish and obtain the live result URL, call the built-in
|
|
99
|
+
`publish_artifact` tool ONCE so the user can track this deliverable in **My Outputs**:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
publish_artifact(kind="article", channel="csdn", title="<title>", url="<the REAL returned URL>", status="delivered")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Use the real returned URL — never fabricate one. Call it once per published item,
|
|
106
|
+
only after delivery is confirmed; skip it (or use `status="failed"`) if publishing failed.
|
|
107
|
+
See `_shared/artifacts.md`.
|
package/skills/devto/SKILL.md
CHANGED
|
@@ -76,3 +76,17 @@ curl -sS -H "api-key: $DEVTO_API_KEY" -H "Accept: application/vnd.forem.api-v1+j
|
|
|
76
76
|
bulk publishes or you'll get `429`.
|
|
77
77
|
- `body_markdown` is the source of truth — if you put a `---` front-matter block
|
|
78
78
|
at the top, its fields override the JSON `article` fields.
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
## Record the output
|
|
82
|
+
|
|
83
|
+
After you successfully publish and obtain the live result URL, call the built-in
|
|
84
|
+
`publish_artifact` tool ONCE so the user can track this deliverable in **My Outputs**:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
publish_artifact(kind="article", channel="devto", title="<title>", url="<the REAL returned URL>", status="delivered")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Use the real returned URL — never fabricate one. Call it once per published item,
|
|
91
|
+
only after delivery is confirmed; skip it (or use `status="failed"`) if publishing failed.
|
|
92
|
+
See `_shared/artifacts.md`.
|
package/skills/hashnode/SKILL.md
CHANGED
|
@@ -145,3 +145,17 @@ gql 'query GetPost($id: ObjectId!) { post(id: $id) { title url views reactionCou
|
|
|
145
145
|
via status codes; always surface them.
|
|
146
146
|
- **Idempotency** — re-running `publishPost` creates a *new* post each time; to
|
|
147
147
|
change an existing one use `updatePost` with its `id`.
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
## Record the output
|
|
151
|
+
|
|
152
|
+
After you successfully publish and obtain the live result URL, call the built-in
|
|
153
|
+
`publish_artifact` tool ONCE so the user can track this deliverable in **My Outputs**:
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
publish_artifact(kind="article", channel="hashnode", title="<title>", url="<the REAL returned URL>", status="delivered")
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Use the real returned URL — never fabricate one. Call it once per published item,
|
|
160
|
+
only after delivery is confirmed; skip it (or use `status="failed"`) if publishing failed.
|
|
161
|
+
See `_shared/artifacts.md`.
|
package/skills/juejin/SKILL.md
CHANGED
|
@@ -73,3 +73,17 @@ python3 $JJ publish --title "标题" --content-file a.md \
|
|
|
73
73
|
- Publishing without a category + tag is rejected in 审核; prefer `--draft-only`.
|
|
74
74
|
- **Never print `JUEJIN_COOKIES`** — it is full account access.
|
|
75
75
|
- **ToS**: acts only on the user's own account with their own captured cookie.
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
## Record the output
|
|
79
|
+
|
|
80
|
+
After you successfully publish and obtain the live result URL, call the built-in
|
|
81
|
+
`publish_artifact` tool ONCE so the user can track this deliverable in **My Outputs**:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
publish_artifact(kind="article", channel="juejin", title="<title>", url="<the REAL returned URL>", status="delivered")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Use the real returned URL — never fabricate one. Call it once per published item,
|
|
88
|
+
only after delivery is confirmed; skip it (or use `status="failed"`) if publishing failed.
|
|
89
|
+
See `_shared/artifacts.md`.
|
package/skills/linkedin/SKILL.md
CHANGED
|
@@ -71,3 +71,17 @@ If the versioned endpoint is unavailable for the app, the older
|
|
|
71
71
|
Page posts need `w_organization_social` + an admin role (not in this connector).
|
|
72
72
|
- The `LinkedIn-Version` header is mandatory for `/rest/*`; a stale value
|
|
73
73
|
returns a version error — bump to the current `YYYYMM`.
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
## Record the output
|
|
77
|
+
|
|
78
|
+
After you successfully publish and obtain the live result URL, call the built-in
|
|
79
|
+
`publish_artifact` tool ONCE so the user can track this deliverable in **My Outputs**:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
publish_artifact(kind="article", channel="linkedin", title="<title>", url="<the REAL returned URL>", status="delivered")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Use the real returned URL — never fabricate one. Call it once per published item,
|
|
86
|
+
only after delivery is confirmed; skip it (or use `status="failed"`) if publishing failed.
|
|
87
|
+
See `_shared/artifacts.md`.
|
package/skills/mastodon/SKILL.md
CHANGED
|
@@ -101,3 +101,17 @@ https://docs.joinmastodon.org/methods/media/
|
|
|
101
101
|
get `429`.
|
|
102
102
|
- `verify_credentials` returns HTML in fields like `note`; the plaintext source
|
|
103
103
|
lives under the `source` object.
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
## Record the output
|
|
107
|
+
|
|
108
|
+
After you successfully publish and obtain the live result URL, call the built-in
|
|
109
|
+
`publish_artifact` tool ONCE so the user can track this deliverable in **My Outputs**:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
publish_artifact(kind="message", channel="mastodon", title="<title>", url="<the REAL returned URL>", status="delivered")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Use the real returned URL — never fabricate one. Call it once per published item,
|
|
116
|
+
only after delivery is confirmed; skip it (or use `status="failed"`) if publishing failed.
|
|
117
|
+
See `_shared/artifacts.md`.
|
package/skills/medium/SKILL.md
CHANGED
|
@@ -93,3 +93,17 @@ to a link paragraph (never blocks the post).
|
|
|
93
93
|
genuinely expired (reconnect).
|
|
94
94
|
- **Never print `MEDIUM_COOKIES`** — it is full account access.
|
|
95
95
|
- **ToS**: acts only on the user's own account with their own captured cookie.
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
## Record the output
|
|
99
|
+
|
|
100
|
+
After you successfully publish and obtain the live result URL, call the built-in
|
|
101
|
+
`publish_artifact` tool ONCE so the user can track this deliverable in **My Outputs**:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
publish_artifact(kind="article", channel="medium", title="<title>", url="<the REAL returned URL>", status="delivered")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Use the real returned URL — never fabricate one. Call it once per published item,
|
|
108
|
+
only after delivery is confirmed; skip it (or use `status="failed"`) if publishing failed.
|
|
109
|
+
See `_shared/artifacts.md`.
|
package/skills/notion/SKILL.md
CHANGED
|
@@ -120,3 +120,17 @@ curl -sS -X POST "https://api.notion.com/v1/pages" \
|
|
|
120
120
|
- Most write failures (400/404) come from a property type mismatch —
|
|
121
121
|
e.g. sending `{"select": "Open"}` instead of `{"select": {"name": "Open"}}`.
|
|
122
122
|
Read the database schema once via `GET /v1/databases/<id>` if unsure.
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
## Record the output
|
|
126
|
+
|
|
127
|
+
After you successfully publish and obtain the live result URL, call the built-in
|
|
128
|
+
`publish_artifact` tool ONCE so the user can track this deliverable in **My Outputs**:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
publish_artifact(kind="document", channel="notion", title="<title>", url="<the REAL returned URL>", status="delivered")
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Use the real returned URL — never fabricate one. Call it once per published item,
|
|
135
|
+
only after delivery is confirmed; skip it (or use `status="failed"`) if publishing failed.
|
|
136
|
+
See `_shared/artifacts.md`.
|
package/skills/reddit/SKILL.md
CHANGED
|
@@ -77,3 +77,17 @@ curl -sS -H "Authorization: Bearer $REDDIT_TOKEN" -H "User-Agent: $UA" \
|
|
|
77
77
|
- Respect rate limits: read the `X-Ratelimit-Remaining` response header; space
|
|
78
78
|
out bulk submits.
|
|
79
79
|
- Use `r/test` as a safe target when validating that the connection works.
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
## Record the output
|
|
83
|
+
|
|
84
|
+
After you successfully publish and obtain the live result URL, call the built-in
|
|
85
|
+
`publish_artifact` tool ONCE so the user can track this deliverable in **My Outputs**:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
publish_artifact(kind="message", channel="reddit", title="<title>", url="<the REAL returned URL>", status="delivered")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Use the real returned URL — never fabricate one. Call it once per published item,
|
|
92
|
+
only after delivery is confirmed; skip it (or use `status="failed"`) if publishing failed.
|
|
93
|
+
See `_shared/artifacts.md`.
|
package/skills/substack/SKILL.md
CHANGED
|
@@ -93,3 +93,17 @@ python3 $SUB publish --title "Title" --content-file post.md --send-email --confi
|
|
|
93
93
|
genuinely expired (reconnect); the CLI never retries a write.
|
|
94
94
|
- **Never print `SUBSTACK_COOKIES`** — it is full account access.
|
|
95
95
|
- **ToS**: acts only on the user's own account with their own captured cookie.
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
## Record the output
|
|
99
|
+
|
|
100
|
+
After you successfully publish and obtain the live result URL, call the built-in
|
|
101
|
+
`publish_artifact` tool ONCE so the user can track this deliverable in **My Outputs**:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
publish_artifact(kind="article", channel="substack", title="<title>", url="<the REAL returned URL>", status="delivered")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Use the real returned URL — never fabricate one. Call it once per published item,
|
|
108
|
+
only after delivery is confirmed; skip it (or use `status="failed"`) if publishing failed.
|
|
109
|
+
See `_shared/artifacts.md`.
|
package/skills/telegram/SKILL.md
CHANGED
|
@@ -122,3 +122,17 @@ user verbatim as the confirmation prompt.
|
|
|
122
122
|
return an error (no shareable link exists).
|
|
123
123
|
- **`edit`/`delete`** generally only apply to the user's own messages (admins can delete others
|
|
124
124
|
in groups they manage).
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
## Record the output
|
|
128
|
+
|
|
129
|
+
After you successfully publish and obtain the live result URL, call the built-in
|
|
130
|
+
`publish_artifact` tool ONCE so the user can track this deliverable in **My Outputs**:
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
publish_artifact(kind="message", channel="telegram", title="<title>", url="<the REAL returned URL>", status="delivered")
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Use the real returned URL — never fabricate one. Call it once per published item,
|
|
137
|
+
only after delivery is confirmed; skip it (or use `status="failed"`) if publishing failed.
|
|
138
|
+
See `_shared/artifacts.md`.
|
|
@@ -141,3 +141,17 @@ echo "media id=$MEDIA_ID"
|
|
|
141
141
|
- **Never publish silently.** Even if the user says "post it", prefer creating a
|
|
142
142
|
draft and returning the `wp-admin` edit link unless they explicitly asked to
|
|
143
143
|
go live.
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
## Record the output
|
|
147
|
+
|
|
148
|
+
After you successfully publish and obtain the live result URL, call the built-in
|
|
149
|
+
`publish_artifact` tool ONCE so the user can track this deliverable in **My Outputs**:
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
publish_artifact(kind="article", channel="wordpress", title="<title>", url="<the REAL returned URL>", status="delivered")
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Use the real returned URL — never fabricate one. Call it once per published item,
|
|
156
|
+
only after delivery is confirmed; skip it (or use `status="failed"`) if publishing failed.
|
|
157
|
+
See `_shared/artifacts.md`.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: x
|
|
3
|
+
description: Read & act on X (Twitter) with the user's own login cookies (BYOC) — post tweets (text / images / video / threads / replies / quotes), search tweets & users, read timelines and single tweets, like / retweet / follow / delete, and see trends. Use when the user mentions X / Twitter, 发推 / 发推特 / 推特, "我的 Twitter", posting to X, searching X, or reading their X timeline.
|
|
4
|
+
when_to_use: |
|
|
5
|
+
Trigger for anything on the user's X (Twitter) account driven by their own
|
|
6
|
+
login cookie: post a tweet / thread / reply / quote (optionally with images or
|
|
7
|
+
a video), search tweets or users, read their home timeline or a user's tweets,
|
|
8
|
+
look up one tweet, like / retweet / follow / delete, or check trends. This acts
|
|
9
|
+
as the user's REAL account, so every write is gated behind an explicit
|
|
10
|
+
confirmation.
|
|
11
|
+
connections: [x]
|
|
12
|
+
allowed_tools: [Bash]
|
|
13
|
+
license: Apache-2.0
|
|
14
|
+
metadata:
|
|
15
|
+
author: acedatacloud
|
|
16
|
+
version: "1.0"
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# x — read & post on X (Twitter) via your own cookies
|
|
20
|
+
|
|
21
|
+
Drives the user's **real** X account through X's internal web API via
|
|
22
|
+
[`twikit`](https://github.com/d60/twikit), authenticated by the login cookie they
|
|
23
|
+
captured with the ACE extension. No official API key, no cost.
|
|
24
|
+
|
|
25
|
+
> ⚠️ **Not yet E2E-verified.** Built against twikit's documented API but not run
|
|
26
|
+
> against a live account at build time. The first live run is the verification —
|
|
27
|
+
> if X's internal API drifted it surfaces as a clear error, not silent breakage.
|
|
28
|
+
|
|
29
|
+
The connector injects the cookie jar as an env var:
|
|
30
|
+
|
|
31
|
+
- `X_COOKIES` — a JSON array of cookies (needs at least `auth_token` + `ct0`).
|
|
32
|
+
**Secret — full account access. Never echo or print it.**
|
|
33
|
+
|
|
34
|
+
## Setup — install twikit once per session
|
|
35
|
+
|
|
36
|
+
`twikit` may not be preinstalled; bootstrap it (same pattern as the telegram
|
|
37
|
+
skill), then call the shipped CLI:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
python3 -c "import twikit" 2>/dev/null || pip install --user --quiet twikit 2>/dev/null || true
|
|
41
|
+
X=$SKILL_DIR/scripts/x.py
|
|
42
|
+
python3 $X whoami # who is logged in
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Read commands (run directly)
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
python3 $X whoami # the logged-in account
|
|
49
|
+
python3 $X search --query "ai agents" --product Latest --limit 20 # Top | Latest | Media
|
|
50
|
+
python3 $X search-users --query "openai" --limit 10
|
|
51
|
+
python3 $X timeline --limit 20 # my home timeline
|
|
52
|
+
python3 $X user-tweets --user elonmusk --type Tweets --limit 20 # Tweets|Replies|Media|Likes
|
|
53
|
+
python3 $X tweet --id 1234567890123456789 # single tweet detail
|
|
54
|
+
python3 $X trends --category trending --limit 20 # trending|for-you|news|sports|entertainment
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`--user` accepts either an `@screen_name` (the `@` is optional) or a numeric id.
|
|
58
|
+
|
|
59
|
+
## Verify the connection first
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
python3 $X whoami
|
|
63
|
+
# → {"id": "...", "screen_name": "...", "followers_count": ...}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
On an auth error the cookie is expired — have the user reconnect at
|
|
67
|
+
<https://auth.acedata.cloud/user/connections>. Do **not** loop-retry.
|
|
68
|
+
|
|
69
|
+
## Write commands — GATED (dry-run unless trailing `--confirm`)
|
|
70
|
+
|
|
71
|
+
Every state-changing command (`post`, `thread`, `like`, `unlike`, `retweet`,
|
|
72
|
+
`unretweet`, `follow`, `unfollow`, `delete`) **dry-runs** without a trailing
|
|
73
|
+
`--confirm`. `--confirm` is honored **only as the last argument**, so a tweet
|
|
74
|
+
body that merely contains "--confirm" can never silently post. Always show the
|
|
75
|
+
dry-run, get an explicit "yes" on the exact text, then re-run with `--confirm`.
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
python3 $X post --text "hello world" # dry-run
|
|
79
|
+
python3 $X post --text "hello world" --confirm # LIVE tweet
|
|
80
|
+
python3 $X post --text "look at this" --media a.jpg,b.png --confirm # up to 4 images (or 1 video)
|
|
81
|
+
python3 $X post --text "great point" --reply-to 123456 --confirm # reply
|
|
82
|
+
python3 $X post --text "worth reading" --quote-url https://x.com/u/status/123 --confirm # quote
|
|
83
|
+
python3 $X thread --text "1/2 first" --text "2/2 second" --confirm # thread (2+ segments)
|
|
84
|
+
python3 $X like --id 123456 --confirm
|
|
85
|
+
python3 $X retweet --id 123456 --confirm
|
|
86
|
+
python3 $X follow --user elonmusk --confirm
|
|
87
|
+
python3 $X delete --id 123456 --confirm # delete one of MY tweets
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- **A confirmed `post` / `thread` is immediately PUBLIC** on the user's real
|
|
91
|
+
account — there is no draft step. Always confirm the exact text first.
|
|
92
|
+
- `--media` takes comma-separated file paths. X allows up to **4 images** OR
|
|
93
|
+
**1 video/GIF** per tweet; for a thread the media attaches to the **first**
|
|
94
|
+
segment only.
|
|
95
|
+
|
|
96
|
+
## Gotchas
|
|
97
|
+
|
|
98
|
+
- **This is the user's real X account.** Confirm before any write — posts are
|
|
99
|
+
immediate and public.
|
|
100
|
+
- **Not E2E-verified** (see the warning above) — expect to validate the first run.
|
|
101
|
+
- **twikit is a scraper of X's non-public API.** It can break when X changes its
|
|
102
|
+
internal endpoints. A `Couldn't get KEY_BYTE indices` / transaction-id error
|
|
103
|
+
means twikit needs upgrading: `pip install --user -U twikit`. An auth error
|
|
104
|
+
means the cookie expired → reconnect.
|
|
105
|
+
- **ToS / rate-limit / ban risk.** This acts through the web API, not the
|
|
106
|
+
official API — high-frequency automation can get the account rate-limited or
|
|
107
|
+
suspended. Keep volume human-like.
|
|
108
|
+
- **Never print `X_COOKIES`** — it is full account access.
|
|
109
|
+
- **DMs are intentionally not exposed** by this skill.
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
## Record the output
|
|
113
|
+
|
|
114
|
+
After you successfully publish and obtain the live result URL, call the built-in
|
|
115
|
+
`publish_artifact` tool ONCE so the user can track this deliverable in **My Outputs**:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
publish_artifact(kind="message", channel="x", title="<title>", url="<the REAL returned URL>", status="delivered")
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Use the real returned URL — never fabricate one. Call it once per published item,
|
|
122
|
+
only after delivery is confirmed; skip it (or use `status="failed"`) if publishing failed.
|
|
123
|
+
See `_shared/artifacts.md`.
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
x — read & act on X (Twitter) with the user's own login cookies (BYOC).
|
|
4
|
+
|
|
5
|
+
Drives X's internal web API through `twikit` (https://github.com/d60/twikit),
|
|
6
|
+
authenticated by the ``auth_token`` + ``ct0`` cookies the user captured with the
|
|
7
|
+
ACE extension. This acts as the user's REAL account, so every state-changing
|
|
8
|
+
command (post / thread / reply / quote / like / retweet / follow / delete) is
|
|
9
|
+
GATED by a trailing ``--confirm`` — without it, the command dry-runs.
|
|
10
|
+
|
|
11
|
+
The connector injects the cookie jar as a JSON env var ``X_COOKIES`` (a JSON list
|
|
12
|
+
of cookie dicts, each with at least ``name`` and ``value``). It is full account
|
|
13
|
+
access — NEVER echo or print it.
|
|
14
|
+
|
|
15
|
+
twikit is a scraper of X's non-public API: it can drift when X changes its
|
|
16
|
+
internal endpoints, and high-frequency use risks rate-limiting or account
|
|
17
|
+
suspension under X's ToS. Errors surface as clear messages rather than silent
|
|
18
|
+
breakage.
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
python3 x.py whoami
|
|
22
|
+
python3 x.py search --query "python" --product Latest --limit 20
|
|
23
|
+
python3 x.py timeline --limit 20
|
|
24
|
+
python3 x.py user-tweets --user elonmusk --type Tweets --limit 20
|
|
25
|
+
python3 x.py tweet --id 1234567890
|
|
26
|
+
python3 x.py trends --category trending
|
|
27
|
+
python3 x.py post --text "hello world" --confirm
|
|
28
|
+
python3 x.py post --text "look" --media a.jpg,b.jpg --confirm
|
|
29
|
+
python3 x.py thread --text "1/2 first" --text "2/2 second" --confirm
|
|
30
|
+
python3 x.py like --id 1234567890 --confirm
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import argparse
|
|
36
|
+
import asyncio
|
|
37
|
+
import json
|
|
38
|
+
import os
|
|
39
|
+
import sys
|
|
40
|
+
|
|
41
|
+
UA = (
|
|
42
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
|
|
43
|
+
"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
_RAW = sys.argv[1:]
|
|
47
|
+
# --confirm is honored ONLY as the last token, and only one is stripped, so a
|
|
48
|
+
# tweet body that merely contains "--confirm" can never silently confirm a write.
|
|
49
|
+
CONFIRM = bool(_RAW) and _RAW[-1] == "--confirm"
|
|
50
|
+
ARGV = _RAW[:-1] if CONFIRM else list(_RAW)
|
|
51
|
+
|
|
52
|
+
# State-changing commands — dry-run unless the invocation ends with --confirm.
|
|
53
|
+
GATED = {
|
|
54
|
+
"post", "thread", "like", "unlike", "retweet", "unretweet",
|
|
55
|
+
"follow", "unfollow", "delete",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def out(obj) -> None:
|
|
60
|
+
print(json.dumps(obj, ensure_ascii=False, indent=2, default=str))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def die(msg: str, code: int = 1) -> None:
|
|
64
|
+
out({"error": msg})
|
|
65
|
+
sys.exit(code)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def load_cookie_dict() -> dict:
|
|
69
|
+
raw = os.environ.get("X_COOKIES")
|
|
70
|
+
if not raw:
|
|
71
|
+
die("X_COOKIES is not set — connect X (Twitter) at "
|
|
72
|
+
"https://auth.acedata.cloud/user/connections, then retry.")
|
|
73
|
+
try:
|
|
74
|
+
jar = json.loads(raw)
|
|
75
|
+
except json.JSONDecodeError as e:
|
|
76
|
+
die(f"X_COOKIES is not valid JSON: {e}")
|
|
77
|
+
if not isinstance(jar, list):
|
|
78
|
+
die(f"X_COOKIES must be a JSON list of cookies, got {type(jar).__name__}")
|
|
79
|
+
cookies = {}
|
|
80
|
+
for c in jar:
|
|
81
|
+
name, value = c.get("name"), c.get("value")
|
|
82
|
+
if name and value is not None:
|
|
83
|
+
cookies[name] = value
|
|
84
|
+
if "auth_token" not in cookies or "ct0" not in cookies:
|
|
85
|
+
die("X_COOKIES is missing auth_token / ct0 — re-capture the cookie on "
|
|
86
|
+
"x.com with the ACE extension, then reconnect at "
|
|
87
|
+
"https://auth.acedata.cloud/user/connections.")
|
|
88
|
+
return cookies
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def make_client():
|
|
92
|
+
from twikit import Client
|
|
93
|
+
proxy = (
|
|
94
|
+
os.environ.get("X_PROXY")
|
|
95
|
+
or os.environ.get("HTTPS_PROXY") or os.environ.get("https_proxy")
|
|
96
|
+
or os.environ.get("ALL_PROXY") or os.environ.get("all_proxy")
|
|
97
|
+
or None
|
|
98
|
+
)
|
|
99
|
+
client = Client("en-US", proxy=proxy, user_agent=UA)
|
|
100
|
+
client.set_cookies(load_cookie_dict())
|
|
101
|
+
return client
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ── formatting ──────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
def fmt_user(u) -> dict:
|
|
107
|
+
return {
|
|
108
|
+
"id": str(getattr(u, "id", "")),
|
|
109
|
+
"name": getattr(u, "name", None),
|
|
110
|
+
"screen_name": getattr(u, "screen_name", None),
|
|
111
|
+
"url": f"https://x.com/{getattr(u, 'screen_name', '')}",
|
|
112
|
+
"followers_count": getattr(u, "followers_count", None),
|
|
113
|
+
"following_count": getattr(u, "following_count", None),
|
|
114
|
+
"statuses_count": getattr(u, "statuses_count", None),
|
|
115
|
+
"verified": getattr(u, "verified", None) or getattr(u, "is_blue_verified", None),
|
|
116
|
+
"description": (getattr(u, "description", None) or "")[:200],
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def fmt_tweet(t) -> dict:
|
|
121
|
+
author = getattr(t, "user", None)
|
|
122
|
+
sn = getattr(author, "screen_name", None) if author else None
|
|
123
|
+
tid = str(getattr(t, "id", ""))
|
|
124
|
+
return {
|
|
125
|
+
"id": tid,
|
|
126
|
+
"text": (getattr(t, "full_text", None) or getattr(t, "text", None) or "")[:280],
|
|
127
|
+
"author": sn,
|
|
128
|
+
"url": f"https://x.com/{sn}/status/{tid}" if sn and tid else None,
|
|
129
|
+
"created_at": getattr(t, "created_at", None),
|
|
130
|
+
"favorite_count": getattr(t, "favorite_count", None),
|
|
131
|
+
"retweet_count": getattr(t, "retweet_count", None),
|
|
132
|
+
"reply_count": getattr(t, "reply_count", None),
|
|
133
|
+
"quote_count": getattr(t, "quote_count", None),
|
|
134
|
+
"view_count": getattr(t, "view_count", None),
|
|
135
|
+
"lang": getattr(t, "lang", None),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def resolve_user(client, target: str):
|
|
140
|
+
t = target.lstrip("@").strip()
|
|
141
|
+
if t.isdigit():
|
|
142
|
+
return await client.get_user_by_id(t)
|
|
143
|
+
return await client.get_user_by_screen_name(t)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ── read commands ───────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
async def cmd_whoami(client, _args):
|
|
149
|
+
u = await client.user()
|
|
150
|
+
out(fmt_user(u))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
async def cmd_search(client, args):
|
|
154
|
+
tweets = await client.search_tweet(args.query, args.product, count=args.limit)
|
|
155
|
+
items = list(tweets)[: args.limit]
|
|
156
|
+
out({"query": args.query, "product": args.product,
|
|
157
|
+
"count": len(items), "tweets": [fmt_tweet(t) for t in items]})
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
async def cmd_search_users(client, args):
|
|
161
|
+
users = await client.search_user(args.query, count=args.limit)
|
|
162
|
+
items = list(users)[: args.limit]
|
|
163
|
+
out({"query": args.query, "count": len(items),
|
|
164
|
+
"users": [fmt_user(u) for u in items]})
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
async def cmd_timeline(client, args):
|
|
168
|
+
tweets = await client.get_latest_timeline(count=args.limit)
|
|
169
|
+
items = list(tweets)[: args.limit]
|
|
170
|
+
out({"count": len(items), "tweets": [fmt_tweet(t) for t in items]})
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
async def cmd_user_tweets(client, args):
|
|
174
|
+
u = await resolve_user(client, args.user)
|
|
175
|
+
tweets = await client.get_user_tweets(u.id, args.type, count=args.limit)
|
|
176
|
+
items = list(tweets)[: args.limit]
|
|
177
|
+
out({"user": fmt_user(u), "type": args.type,
|
|
178
|
+
"count": len(items), "tweets": [fmt_tweet(t) for t in items]})
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
async def cmd_tweet(client, args):
|
|
182
|
+
t = await client.get_tweet_by_id(args.id)
|
|
183
|
+
out(fmt_tweet(t))
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
async def cmd_trends(client, args):
|
|
187
|
+
trends = await client.get_trends(args.category, count=args.limit)
|
|
188
|
+
out({"category": args.category,
|
|
189
|
+
"trends": [{"name": getattr(x, "name", None),
|
|
190
|
+
"tweets_count": getattr(x, "tweets_count", None)}
|
|
191
|
+
for x in list(trends)[: args.limit]]})
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# ── write commands (GATED) ──────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
async def _upload_media(client, media_arg: str) -> list:
|
|
197
|
+
media_ids = []
|
|
198
|
+
for path in [p.strip() for p in media_arg.split(",") if p.strip()]:
|
|
199
|
+
if not os.path.isfile(path):
|
|
200
|
+
die(f"media file not found: {path}")
|
|
201
|
+
media_ids.append(await client.upload_media(path, wait_for_completion=True))
|
|
202
|
+
return media_ids
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _quote_note(args) -> dict:
|
|
206
|
+
return {
|
|
207
|
+
"reply_to": getattr(args, "reply_to", None),
|
|
208
|
+
"quote_url": getattr(args, "quote_url", None),
|
|
209
|
+
"media": getattr(args, "media", None),
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
async def cmd_post(client, args):
|
|
214
|
+
text = (args.text or "").strip()
|
|
215
|
+
if not text and not args.media:
|
|
216
|
+
die("provide --text and/or --media")
|
|
217
|
+
media_ids = await _upload_media(client, args.media) if args.media else None
|
|
218
|
+
tweet = await client.create_tweet(
|
|
219
|
+
text=text, media_ids=media_ids,
|
|
220
|
+
reply_to=args.reply_to, attachment_url=args.quote_url)
|
|
221
|
+
out({"ok": True, "posted": True, **fmt_tweet(tweet)})
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
async def cmd_thread(client, args):
|
|
225
|
+
texts = [t for t in (args.text or []) if t and t.strip()]
|
|
226
|
+
if len(texts) < 2:
|
|
227
|
+
die("a thread needs at least two --text segments")
|
|
228
|
+
media_ids = await _upload_media(client, args.media) if args.media else None
|
|
229
|
+
posted, reply_to = [], None
|
|
230
|
+
for i, text in enumerate(texts):
|
|
231
|
+
tweet = await client.create_tweet(
|
|
232
|
+
text=text.strip(),
|
|
233
|
+
media_ids=media_ids if i == 0 else None,
|
|
234
|
+
reply_to=reply_to)
|
|
235
|
+
reply_to = tweet.id
|
|
236
|
+
posted.append(fmt_tweet(tweet))
|
|
237
|
+
out({"ok": True, "posted": True, "count": len(posted), "tweets": posted})
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
async def cmd_like(client, args):
|
|
241
|
+
await client.favorite_tweet(args.id)
|
|
242
|
+
out({"ok": True, "liked": True, "tweet_id": args.id})
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
async def cmd_unlike(client, args):
|
|
246
|
+
await client.unfavorite_tweet(args.id)
|
|
247
|
+
out({"ok": True, "unliked": True, "tweet_id": args.id})
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
async def cmd_retweet(client, args):
|
|
251
|
+
await client.retweet(args.id)
|
|
252
|
+
out({"ok": True, "retweeted": True, "tweet_id": args.id})
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
async def cmd_unretweet(client, args):
|
|
256
|
+
await client.delete_retweet(args.id)
|
|
257
|
+
out({"ok": True, "unretweeted": True, "tweet_id": args.id})
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
async def cmd_follow(client, args):
|
|
261
|
+
u = await resolve_user(client, args.user)
|
|
262
|
+
await client.follow_user(u.id)
|
|
263
|
+
out({"ok": True, "followed": True, "user": u.screen_name, "user_id": str(u.id)})
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
async def cmd_unfollow(client, args):
|
|
267
|
+
u = await resolve_user(client, args.user)
|
|
268
|
+
await client.unfollow_user(u.id)
|
|
269
|
+
out({"ok": True, "unfollowed": True, "user": u.screen_name, "user_id": str(u.id)})
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
async def cmd_delete(client, args):
|
|
273
|
+
await client.delete_tweet(args.id)
|
|
274
|
+
out({"ok": True, "deleted": True, "tweet_id": args.id})
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def gated_dry_run(args) -> None:
|
|
278
|
+
"""Print what a state-changing command WOULD do, without any network call."""
|
|
279
|
+
cmd = args.command
|
|
280
|
+
fields: dict = {}
|
|
281
|
+
if cmd == "post":
|
|
282
|
+
fields = {"text_preview": (args.text or "")[:280], **_quote_note(args)}
|
|
283
|
+
elif cmd == "thread":
|
|
284
|
+
texts = [t for t in (args.text or []) if t and t.strip()]
|
|
285
|
+
fields = {"segments": len(texts), "preview": [t[:120] for t in texts],
|
|
286
|
+
"media_on_first": args.media}
|
|
287
|
+
elif cmd in ("follow", "unfollow"):
|
|
288
|
+
fields = {"user": args.user}
|
|
289
|
+
else: # like / unlike / retweet / unretweet / delete
|
|
290
|
+
fields = {"tweet_id": args.id}
|
|
291
|
+
out({"dry_run": True, "command": cmd, **fields,
|
|
292
|
+
"note": "Re-run with --confirm as the LAST argument to actually run "
|
|
293
|
+
"this. It acts on the user's REAL X account."})
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
COMMANDS = {
|
|
297
|
+
"whoami": cmd_whoami,
|
|
298
|
+
"search": cmd_search,
|
|
299
|
+
"search-users": cmd_search_users,
|
|
300
|
+
"timeline": cmd_timeline,
|
|
301
|
+
"user-tweets": cmd_user_tweets,
|
|
302
|
+
"tweet": cmd_tweet,
|
|
303
|
+
"trends": cmd_trends,
|
|
304
|
+
"post": cmd_post,
|
|
305
|
+
"thread": cmd_thread,
|
|
306
|
+
"like": cmd_like,
|
|
307
|
+
"unlike": cmd_unlike,
|
|
308
|
+
"retweet": cmd_retweet,
|
|
309
|
+
"unretweet": cmd_unretweet,
|
|
310
|
+
"follow": cmd_follow,
|
|
311
|
+
"unfollow": cmd_unfollow,
|
|
312
|
+
"delete": cmd_delete,
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
317
|
+
p = argparse.ArgumentParser(prog="x.py", description="X (Twitter) cookie CLI")
|
|
318
|
+
sub = p.add_subparsers(dest="command", required=True)
|
|
319
|
+
|
|
320
|
+
sub.add_parser("whoami", help="show the logged-in account")
|
|
321
|
+
|
|
322
|
+
sp = sub.add_parser("search", help="search tweets by keyword")
|
|
323
|
+
sp.add_argument("--query", required=True)
|
|
324
|
+
sp.add_argument("--product", choices=["Top", "Latest", "Media"], default="Latest")
|
|
325
|
+
sp.add_argument("--limit", type=int, default=20)
|
|
326
|
+
|
|
327
|
+
sp = sub.add_parser("search-users", help="search users by keyword")
|
|
328
|
+
sp.add_argument("--query", required=True)
|
|
329
|
+
sp.add_argument("--limit", type=int, default=20)
|
|
330
|
+
|
|
331
|
+
sp = sub.add_parser("timeline", help="my home timeline (latest)")
|
|
332
|
+
sp.add_argument("--limit", type=int, default=20)
|
|
333
|
+
|
|
334
|
+
sp = sub.add_parser("user-tweets", help="a user's tweets")
|
|
335
|
+
sp.add_argument("--user", required=True, help="@screen_name or numeric id")
|
|
336
|
+
sp.add_argument("--type", choices=["Tweets", "Replies", "Media", "Likes"],
|
|
337
|
+
default="Tweets")
|
|
338
|
+
sp.add_argument("--limit", type=int, default=20)
|
|
339
|
+
|
|
340
|
+
sp = sub.add_parser("tweet", help="single tweet detail")
|
|
341
|
+
sp.add_argument("--id", required=True)
|
|
342
|
+
|
|
343
|
+
sp = sub.add_parser("trends", help="trending topics")
|
|
344
|
+
sp.add_argument("--category",
|
|
345
|
+
choices=["trending", "for-you", "news", "sports", "entertainment"],
|
|
346
|
+
default="trending")
|
|
347
|
+
sp.add_argument("--limit", type=int, default=20)
|
|
348
|
+
|
|
349
|
+
sp = sub.add_parser("post", help="publish a tweet (GATED by trailing --confirm)")
|
|
350
|
+
sp.add_argument("--text", default="")
|
|
351
|
+
sp.add_argument("--media", help="comma-separated image/video file paths")
|
|
352
|
+
sp.add_argument("--reply-to", dest="reply_to", help="tweet id to reply to")
|
|
353
|
+
sp.add_argument("--quote-url", dest="quote_url", help="tweet URL to quote")
|
|
354
|
+
|
|
355
|
+
sp = sub.add_parser("thread", help="publish a thread (GATED by trailing --confirm)")
|
|
356
|
+
sp.add_argument("--text", action="append", help="one per tweet; repeat 2+ times")
|
|
357
|
+
sp.add_argument("--media", help="comma-separated paths, attached to the FIRST tweet")
|
|
358
|
+
|
|
359
|
+
for name in ("like", "unlike", "retweet", "unretweet", "delete"):
|
|
360
|
+
sp = sub.add_parser(name, help=f"{name} a tweet (GATED by trailing --confirm)")
|
|
361
|
+
sp.add_argument("--id", required=True)
|
|
362
|
+
|
|
363
|
+
for name in ("follow", "unfollow"):
|
|
364
|
+
sp = sub.add_parser(name, help=f"{name} a user (GATED by trailing --confirm)")
|
|
365
|
+
sp.add_argument("--user", required=True, help="@screen_name or numeric id")
|
|
366
|
+
|
|
367
|
+
return p
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
async def run(args) -> None:
|
|
371
|
+
client = make_client()
|
|
372
|
+
await COMMANDS[args.command](client, args)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def main() -> None:
|
|
376
|
+
args = build_parser().parse_args(ARGV)
|
|
377
|
+
# Gated commands dry-run offline (no cookies, no network) unless --confirm.
|
|
378
|
+
if args.command in GATED and not CONFIRM:
|
|
379
|
+
gated_dry_run(args)
|
|
380
|
+
return
|
|
381
|
+
try:
|
|
382
|
+
from twikit.errors import (
|
|
383
|
+
Unauthorized, Forbidden, TooManyRequests, NotFound,
|
|
384
|
+
TweetNotAvailable, UserNotFound, UserUnavailable,
|
|
385
|
+
AccountLocked, AccountSuspended, TwitterException,
|
|
386
|
+
)
|
|
387
|
+
except Exception as e: # twikit not importable
|
|
388
|
+
die(f"twikit is not available: {e}. Install with "
|
|
389
|
+
f"`pip install --user twikit`.")
|
|
390
|
+
try:
|
|
391
|
+
asyncio.run(run(args))
|
|
392
|
+
except (Unauthorized,) as e:
|
|
393
|
+
die(f"auth failed — cookie likely expired. Reconnect X at "
|
|
394
|
+
f"https://auth.acedata.cloud/user/connections. ({e})")
|
|
395
|
+
except (AccountLocked, AccountSuspended) as e:
|
|
396
|
+
die(f"account locked/suspended by X: {e}")
|
|
397
|
+
except TooManyRequests as e:
|
|
398
|
+
die(f"rate limited by X — wait and retry, or slow down. ({e})")
|
|
399
|
+
except (NotFound, TweetNotAvailable, UserNotFound, UserUnavailable) as e:
|
|
400
|
+
die(f"not found / unavailable: {e}")
|
|
401
|
+
except Forbidden as e:
|
|
402
|
+
die(f"forbidden by X (content rule, protected account, or ToS): {e}")
|
|
403
|
+
except TwitterException as e:
|
|
404
|
+
die(f"X API error: {e}")
|
|
405
|
+
except Exception as e:
|
|
406
|
+
# twikit scrapes X's non-public API; a bare error here usually means an
|
|
407
|
+
# expired cookie OR that X changed its internal endpoints and twikit
|
|
408
|
+
# needs upgrading (e.g. "Couldn't get KEY_BYTE indices" = transaction-id
|
|
409
|
+
# bootstrap drift → `pip install --user -U twikit`).
|
|
410
|
+
die(f"X request failed ({type(e).__name__}: {e}). Likely an expired "
|
|
411
|
+
f"cookie — reconnect at https://auth.acedata.cloud/user/connections "
|
|
412
|
+
f"— or twikit drift vs X's internal API (try `pip install --user -U "
|
|
413
|
+
f"twikit`).")
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
if __name__ == "__main__":
|
|
417
|
+
main()
|
package/skills/zhihu/SKILL.md
CHANGED
|
@@ -218,3 +218,17 @@ python3 $BLOG edit-answer --id <answer-id> --content-file ans.html --confirm #
|
|
|
218
218
|
- **Scope**: Zhihu only. Other Chinese platforms (掘金 / CSDN / …) ship as their
|
|
219
219
|
own per-platform skills (e.g. `csdn`, `juejin`), each with its own connector —
|
|
220
220
|
not a `--platform` switch here.
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
## Record the output
|
|
224
|
+
|
|
225
|
+
After you successfully publish and obtain the live result URL, call the built-in
|
|
226
|
+
`publish_artifact` tool ONCE so the user can track this deliverable in **My Outputs**:
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
publish_artifact(kind="article", channel="zhihu", title="<title>", url="<the REAL returned URL>", status="delivered")
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Use the real returned URL — never fabricate one. Call it once per published item,
|
|
233
|
+
only after delivery is confirmed; skip it (or use `status="failed"`) if publishing failed.
|
|
234
|
+
See `_shared/artifacts.md`.
|