@acedatacloud/skills 2026.721.3 → 2026.722.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.722.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/ai-chat/SKILL.md
CHANGED
|
@@ -56,11 +56,11 @@ models include:
|
|
|
56
56
|
|
|
57
57
|
| Family | Current examples |
|
|
58
58
|
|--------|------------------|
|
|
59
|
-
| OpenAI / reasoning | `gpt-5.5`, `gpt-5.5-pro`, `gpt-5.4`, `gpt-5.4-pro`, `gpt-5.2`, `gpt-5.1`, `gpt-5`, `gpt-5-mini`, `gpt-5-nano`, `gpt-4.1`, `gpt-4.1-mini`, `gpt-4.1-nano`, `gpt-4o`, `gpt-4o-mini`, `o1`, `o3`, `o4-mini` |
|
|
59
|
+
| OpenAI / reasoning | `gpt-5.6-luna`, `gpt-5.6-terra`, `gpt-5.6-sol`, `gpt-5.5`, `gpt-5.5-pro`, `gpt-5.4`, `gpt-5.4-pro`, `gpt-5.2`, `gpt-5.1`, `gpt-5`, `gpt-5-mini`, `gpt-5-nano`, `gpt-4.1`, `gpt-4.1-mini`, `gpt-4.1-nano`, `gpt-4o`, `gpt-4o-mini`, `o1`, `o3`, `o4-mini` |
|
|
60
60
|
| OpenAI free-tier chat-completions | `gpt-5.5:free`, `gpt-5:free`, `gpt-4.1:free`, `gpt-4o:free`, `gpt-4o-mini:free`, `gpt-oss:free` |
|
|
61
61
|
| Claude | `claude-fable-5`, `claude-sonnet-5`, `claude-opus-4-8`, `claude-opus-4-7`, `claude-opus-4-6`, `claude-opus-4-5-20251101`, `claude-sonnet-4-6`, `claude-sonnet-4-5-20250929`, `claude-sonnet-4-20250514`, `claude-haiku-4-5-20251001`, `claude-3-7-sonnet-20250219` |
|
|
62
|
-
| Gemini | `gemini-3.1-pro`, `gemini-3.1-pro-preview`, `gemini-3.1-flash-image-preview`, `gemini-3.1-flash-lite-preview`, `gemini-3-pro-preview`, `gemini-2.5-flash-lite`, `gemini-2.0-flash-lite` |
|
|
63
|
-
| Grok | `grok-4`, `grok-4-0709`, `grok-3`, `grok-3-fast` |
|
|
62
|
+
| Gemini | `gemini-3.5-flash`, `gemini-3.1-pro`, `gemini-3.1-pro-preview`, `gemini-3.1-flash-image-preview`, `gemini-3.1-flash-lite-preview`, `gemini-3-pro-preview`, `gemini-2.5-flash-lite`, `gemini-2.0-flash-lite` |
|
|
63
|
+
| Grok | `grok-4.5`, `grok-4`, `grok-4-0709`, `grok-3`, `grok-3-fast` |
|
|
64
64
|
| DeepSeek | `deepseek-r1`, `deepseek-r1-0528`, `deepseek-v3`, `deepseek-v3-250324`, `deepseek-v3.2-exp`, `deepseek-v4-flash` |
|
|
65
65
|
| Kimi | `kimi-k3`, `kimi-k2.6`, `kimi-k2.5`, `kimi-k2-thinking-turbo`, `kimi-k2-thinking`, `kimi-k2-instruct-0905`, `kimi-k2-0905-preview`, `kimi-k2-turbo-preview`, `kimi-k2-0711-preview` |
|
|
66
66
|
| GLM | `glm-5.2`, `glm-5.1`, `glm-5`, `glm-5-turbo`, `glm-4.7`, `glm-4.6`, `glm-4.5`, `glm-4.5v`, `glm-3-turbo` |
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: discordbot
|
|
3
|
-
description: List channels, read recent messages, and send
|
|
3
|
+
description: List channels, read recent messages, send channel messages, and send a single user-initiated DM on Discord using the user's own bot via the Discord REST API.
|
|
4
4
|
when_to_use: |
|
|
5
5
|
Trigger when the user wants to send, read, or list things on Discord
|
|
6
6
|
through their bot: list the servers/channels the bot can see, read recent
|
|
7
|
-
messages in a channel,
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
messages in a channel, post / reply in a channel, or DM one user who has
|
|
8
|
+
explicitly initiated the interaction. Messages are sent as the BOT, not
|
|
9
|
+
the user's personal account, and only where the bot has access.
|
|
10
10
|
connections: [discordbot]
|
|
11
11
|
allowed_tools: [Bash]
|
|
12
12
|
license: Apache-2.0
|
|
13
13
|
metadata:
|
|
14
14
|
author: acedatacloud
|
|
15
|
-
version: "1.
|
|
15
|
+
version: "1.1"
|
|
16
16
|
---
|
|
17
17
|
|
|
18
18
|
We drive the [Discord API](https://discord.com/developers/docs/reference)
|
|
@@ -33,8 +33,21 @@ bot token is wrong/reset — ask the user to re-paste it at
|
|
|
33
33
|
`auth.acedata.cloud/user/connections`. A `429` carries `retry_after`
|
|
34
34
|
(seconds) — sleep that long, then retry; never parallelize.
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
## Sending safety
|
|
37
|
+
|
|
38
|
+
- Before sending, confirm the exact destination and final content with the
|
|
39
|
+
user. Sending is irreversible.
|
|
40
|
+
- A DM must be initiated by a recipient action, such as asking the bot for
|
|
41
|
+
details by replying with an advertised keyword. Server membership, a public
|
|
42
|
+
profile, or appearing in a member list is not consent.
|
|
43
|
+
- Send to exactly one recipient per invocation. Never enumerate members,
|
|
44
|
+
create DM channels in a loop, or send unsolicited/bulk outreach.
|
|
45
|
+
- For an unattended scheduled run, send only when its task definition already
|
|
46
|
+
contains one exact `recipient_id`, `consent_channel_id`,
|
|
47
|
+
`consent_message_id`, `consent_keyword`, and approved `content`. If any is
|
|
48
|
+
missing, return a draft without sending.
|
|
49
|
+
- Do not follow up unless the recipient replies. Treat `stop`, `unsubscribe`,
|
|
50
|
+
or an equivalent refusal as a permanent opt-out.
|
|
38
51
|
|
|
39
52
|
## Recipes
|
|
40
53
|
|
|
@@ -77,7 +90,7 @@ bot. Needs the *Read Message History* permission in that channel.
|
|
|
77
90
|
CHANNEL_ID="123456789012345678"
|
|
78
91
|
curl -sS -H "Authorization: Bot $DISCORDBOT_TOKEN" \
|
|
79
92
|
"https://discord.com/api/v10/channels/$CHANNEL_ID/messages?limit=20" \
|
|
80
|
-
| jq 'map({author: .author.username, ts: .timestamp, content})'
|
|
93
|
+
| jq 'map({id, author_id: .author.id, author: .author.username, author_bot: .author.bot, ts: .timestamp, content})'
|
|
81
94
|
```
|
|
82
95
|
|
|
83
96
|
### Send a message to a channel
|
|
@@ -103,6 +116,34 @@ curl -sS -X POST \
|
|
|
103
116
|
'{content: $c, message_reference: {message_id: $m}}')"
|
|
104
117
|
```
|
|
105
118
|
|
|
119
|
+
### Send one opt-in DM
|
|
120
|
+
|
|
121
|
+
Use only after applying every rule in **Sending safety**. The helper fetches
|
|
122
|
+
the source message from Discord and verifies that its non-bot author matches
|
|
123
|
+
`RECIPIENT_ID` and its complete text matches `CONSENT_KEYWORD`. Do not call the
|
|
124
|
+
Create DM endpoint directly. Matching is case-insensitive and collapses
|
|
125
|
+
surrounding or repeated whitespace.
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
RECIPIENT_ID="123456789012345678"
|
|
129
|
+
CONSENT_CHANNEL_ID="223456789012345678"
|
|
130
|
+
CONSENT_MESSAGE_ID="323456789012345678"
|
|
131
|
+
CONSENT_KEYWORD="details"
|
|
132
|
+
|
|
133
|
+
python3 "$SKILL_DIR/scripts/discordbot.py" send-opt-in-dm \
|
|
134
|
+
--recipient-id "$RECIPIENT_ID" \
|
|
135
|
+
--consent-channel-id "$CONSENT_CHANNEL_ID" \
|
|
136
|
+
--consent-message-id "$CONSENT_MESSAGE_ID" \
|
|
137
|
+
--consent-keyword "$CONSENT_KEYWORD" \
|
|
138
|
+
--content "Here are the details you requested."
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The command is a dry-run unless the final argument is `--confirm`. In a
|
|
142
|
+
pre-authorized scheduled task, use final `--unattended-confirm` instead. The
|
|
143
|
+
helper validates all IDs, verifies the opt-in message, and skips an identical
|
|
144
|
+
message already present in the DM history. Discord may block or rate-limit bots
|
|
145
|
+
that open many DMs; do not retry by targeting another account.
|
|
146
|
+
|
|
106
147
|
## Notes
|
|
107
148
|
|
|
108
149
|
- A "server" in the UI is a "guild" in the API; messages live in channels
|
|
@@ -113,5 +154,7 @@ curl -sS -X POST \
|
|
|
113
154
|
URL → pick a server (the user needs *Manage Server* there).
|
|
114
155
|
- This is a bot, not the user's account — it cannot read the user's DMs or
|
|
115
156
|
the user's full server list, only what the bot itself can access.
|
|
157
|
+
- A bot can open its own DM channel with one consenting user; that does not
|
|
158
|
+
grant access to the connected user's personal DMs.
|
|
116
159
|
- Mentions: `<@USER_ID>` pings a user, `<#CHANNEL_ID>` links a channel. Plain
|
|
117
160
|
text is fine for normal messages.
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Permission-first Discord Bot actions."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
import urllib.error
|
|
12
|
+
import urllib.parse
|
|
13
|
+
import urllib.request
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
API_BASE = "https://discord.com/api/v10"
|
|
17
|
+
SNOWFLAKE_RE = re.compile(r"[0-9]{17,20}")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DiscordBotError(RuntimeError):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def out(payload: dict[str, Any]) -> None:
|
|
25
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def normalize_text(value: str) -> str:
|
|
29
|
+
return " ".join(value.split()).casefold()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def validate_snowflake(name: str, value: str) -> str:
|
|
33
|
+
value = value.strip()
|
|
34
|
+
if not SNOWFLAKE_RE.fullmatch(value):
|
|
35
|
+
raise DiscordBotError(f"{name} must be a 17-20 digit Discord snowflake")
|
|
36
|
+
return value
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def validate_send_input(
|
|
40
|
+
recipient_id: str,
|
|
41
|
+
consent_channel_id: str,
|
|
42
|
+
consent_message_id: str,
|
|
43
|
+
consent_keyword: str,
|
|
44
|
+
content: str,
|
|
45
|
+
) -> dict[str, str]:
|
|
46
|
+
values = {
|
|
47
|
+
"recipient_id": validate_snowflake("recipient_id", recipient_id),
|
|
48
|
+
"consent_channel_id": validate_snowflake("consent_channel_id", consent_channel_id),
|
|
49
|
+
"consent_message_id": validate_snowflake("consent_message_id", consent_message_id),
|
|
50
|
+
"consent_keyword": consent_keyword.strip(),
|
|
51
|
+
"content": content.strip(),
|
|
52
|
+
}
|
|
53
|
+
if not values["consent_keyword"] or len(values["consent_keyword"]) > 64:
|
|
54
|
+
raise DiscordBotError("consent_keyword must contain 1-64 characters")
|
|
55
|
+
if not values["content"] or len(values["content"]) > 2000:
|
|
56
|
+
raise DiscordBotError("content must contain 1-2000 characters")
|
|
57
|
+
return values
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def api_request(method: str, path: str, payload: dict[str, Any] | None = None) -> Any:
|
|
61
|
+
token = os.environ.get("DISCORDBOT_TOKEN", "").strip()
|
|
62
|
+
if not token:
|
|
63
|
+
raise DiscordBotError(
|
|
64
|
+
"DISCORDBOT_TOKEN is not set; connect Discord Bot at "
|
|
65
|
+
"https://auth.acedata.cloud/user/connections"
|
|
66
|
+
)
|
|
67
|
+
body = json.dumps(payload).encode() if payload is not None else None
|
|
68
|
+
request = urllib.request.Request(
|
|
69
|
+
f"{API_BASE}{path}",
|
|
70
|
+
data=body,
|
|
71
|
+
method=method,
|
|
72
|
+
headers={
|
|
73
|
+
"Authorization": f"Bot {token}",
|
|
74
|
+
"Accept": "application/json",
|
|
75
|
+
"Content-Type": "application/json",
|
|
76
|
+
"User-Agent": "AceDataCloud-DiscordBot/1.1",
|
|
77
|
+
},
|
|
78
|
+
)
|
|
79
|
+
try:
|
|
80
|
+
with urllib.request.urlopen(request, timeout=15) as response:
|
|
81
|
+
raw = response.read()
|
|
82
|
+
except urllib.error.HTTPError as exc:
|
|
83
|
+
raw = exc.read()
|
|
84
|
+
try:
|
|
85
|
+
detail = json.loads(raw.decode())
|
|
86
|
+
message = str(detail.get("message") or detail.get("code") or "request rejected")
|
|
87
|
+
retry_after = detail.get("retry_after")
|
|
88
|
+
except (UnicodeDecodeError, json.JSONDecodeError, AttributeError):
|
|
89
|
+
message = "request rejected"
|
|
90
|
+
retry_after = None
|
|
91
|
+
suffix = f"; retry_after={retry_after}" if retry_after is not None else ""
|
|
92
|
+
raise DiscordBotError(f"Discord API {exc.code}: {message}{suffix}") from None
|
|
93
|
+
except urllib.error.URLError as exc:
|
|
94
|
+
raise DiscordBotError(f"Discord API unavailable: {exc.reason}") from None
|
|
95
|
+
|
|
96
|
+
if not raw:
|
|
97
|
+
return None
|
|
98
|
+
try:
|
|
99
|
+
return json.loads(raw.decode())
|
|
100
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
101
|
+
raise DiscordBotError("Discord API returned invalid JSON") from exc
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def send_opt_in_dm(
|
|
105
|
+
recipient_id: str,
|
|
106
|
+
consent_channel_id: str,
|
|
107
|
+
consent_message_id: str,
|
|
108
|
+
consent_keyword: str,
|
|
109
|
+
content: str,
|
|
110
|
+
) -> dict[str, Any]:
|
|
111
|
+
values = validate_send_input(
|
|
112
|
+
recipient_id,
|
|
113
|
+
consent_channel_id,
|
|
114
|
+
consent_message_id,
|
|
115
|
+
consent_keyword,
|
|
116
|
+
content,
|
|
117
|
+
)
|
|
118
|
+
recipient_id = values["recipient_id"]
|
|
119
|
+
consent_channel_id = values["consent_channel_id"]
|
|
120
|
+
consent_message_id = values["consent_message_id"]
|
|
121
|
+
consent_keyword = values["consent_keyword"]
|
|
122
|
+
content = values["content"]
|
|
123
|
+
|
|
124
|
+
bot = api_request("GET", "/users/@me")
|
|
125
|
+
bot_id = validate_snowflake("bot id", str(bot.get("id", "")))
|
|
126
|
+
if not bot.get("bot"):
|
|
127
|
+
raise DiscordBotError("connected credential is not a Discord bot token")
|
|
128
|
+
|
|
129
|
+
source = api_request("GET", f"/channels/{consent_channel_id}/messages/{consent_message_id}")
|
|
130
|
+
if str(source.get("id", "")) != consent_message_id:
|
|
131
|
+
raise DiscordBotError("Discord returned a different consent message")
|
|
132
|
+
author = source.get("author") or {}
|
|
133
|
+
if str(author.get("id", "")) != recipient_id:
|
|
134
|
+
raise DiscordBotError("consent message author does not match recipient_id")
|
|
135
|
+
if author.get("bot"):
|
|
136
|
+
raise DiscordBotError("bot-authored messages cannot grant DM consent")
|
|
137
|
+
if normalize_text(str(source.get("content", ""))) != normalize_text(consent_keyword):
|
|
138
|
+
raise DiscordBotError("consent message does not exactly match consent_keyword")
|
|
139
|
+
|
|
140
|
+
dm = api_request("POST", "/users/@me/channels", {"recipient_id": recipient_id})
|
|
141
|
+
dm_channel_id = validate_snowflake("DM channel id", str(dm.get("id", "")))
|
|
142
|
+
|
|
143
|
+
history = api_request("GET", f"/channels/{dm_channel_id}/messages?limit=100")
|
|
144
|
+
if not isinstance(history, list):
|
|
145
|
+
raise DiscordBotError("Discord returned invalid DM history")
|
|
146
|
+
duplicate = next(
|
|
147
|
+
(
|
|
148
|
+
message
|
|
149
|
+
for message in history
|
|
150
|
+
if str((message.get("author") or {}).get("id", "")) == bot_id
|
|
151
|
+
and str(message.get("content", "")) == content
|
|
152
|
+
),
|
|
153
|
+
None,
|
|
154
|
+
)
|
|
155
|
+
if duplicate:
|
|
156
|
+
return {
|
|
157
|
+
"status": "duplicate_skipped",
|
|
158
|
+
"recipient_id": recipient_id,
|
|
159
|
+
"consent_message_id": consent_message_id,
|
|
160
|
+
"dm_channel_id": dm_channel_id,
|
|
161
|
+
"message_id": duplicate.get("id"),
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
sent = api_request("POST", f"/channels/{dm_channel_id}/messages", {"content": content})
|
|
165
|
+
return {
|
|
166
|
+
"status": "sent",
|
|
167
|
+
"recipient_id": recipient_id,
|
|
168
|
+
"consent_message_id": consent_message_id,
|
|
169
|
+
"dm_channel_id": dm_channel_id,
|
|
170
|
+
"message_id": sent.get("id"),
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
175
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
176
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
177
|
+
send = commands.add_parser("send-opt-in-dm", help="send one DM after verifying an exact opt-in message")
|
|
178
|
+
send.add_argument("--recipient-id", required=True)
|
|
179
|
+
send.add_argument("--consent-channel-id", required=True)
|
|
180
|
+
send.add_argument("--consent-message-id", required=True)
|
|
181
|
+
send.add_argument("--consent-keyword", required=True)
|
|
182
|
+
send.add_argument("--content", required=True)
|
|
183
|
+
send.add_argument("--confirm", action="store_true", help=argparse.SUPPRESS)
|
|
184
|
+
send.add_argument("--unattended-confirm", action="store_true", help=argparse.SUPPRESS)
|
|
185
|
+
return parser
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def parse_cli(raw: list[str] | None = None) -> tuple[argparse.Namespace, str]:
|
|
189
|
+
raw = list(sys.argv[1:] if raw is None else raw)
|
|
190
|
+
parser = build_parser()
|
|
191
|
+
args = parser.parse_args(raw)
|
|
192
|
+
if args.confirm and args.unattended_confirm:
|
|
193
|
+
parser.error("choose only one confirmation mode")
|
|
194
|
+
confirm_mode = "--confirm" if args.confirm else "--unattended-confirm" if args.unattended_confirm else ""
|
|
195
|
+
if confirm_mode and (not raw or raw[-1] != confirm_mode):
|
|
196
|
+
parser.error(f"{confirm_mode} must be the final argument")
|
|
197
|
+
return args, confirm_mode
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def main() -> None:
|
|
201
|
+
args, confirm_mode = parse_cli()
|
|
202
|
+
try:
|
|
203
|
+
values = validate_send_input(
|
|
204
|
+
args.recipient_id,
|
|
205
|
+
args.consent_channel_id,
|
|
206
|
+
args.consent_message_id,
|
|
207
|
+
args.consent_keyword,
|
|
208
|
+
args.content,
|
|
209
|
+
)
|
|
210
|
+
if not confirm_mode:
|
|
211
|
+
out(
|
|
212
|
+
{
|
|
213
|
+
"status": "dry_run",
|
|
214
|
+
"recipient_id": values["recipient_id"],
|
|
215
|
+
"consent_channel_id": values["consent_channel_id"],
|
|
216
|
+
"consent_message_id": values["consent_message_id"],
|
|
217
|
+
"consent_keyword": values["consent_keyword"],
|
|
218
|
+
"content": values["content"],
|
|
219
|
+
"next": "rerun with trailing --confirm after user approval",
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
return
|
|
223
|
+
out(send_opt_in_dm(**values))
|
|
224
|
+
except DiscordBotError as exc:
|
|
225
|
+
out({"error": str(exc)})
|
|
226
|
+
raise SystemExit(1) from exc
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
if __name__ == "__main__":
|
|
230
|
+
main()
|