@acedatacloud/skills 2026.714.1 → 2026.714.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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acedatacloud/skills",
|
|
3
|
-
"version": "2026.714.
|
|
3
|
+
"version": "2026.714.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",
|
package/skills/telegram/SKILL.md
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: telegram
|
|
3
|
-
description: Full personal Telegram control over MTProto (Telethon) with the user's own account — list/search chats, read & summarize history, see unread, look up contacts & chat info, download media, and send / reply / forward / edit / delete / react / send files / mark read. Use when the user mentions Telegram, a Telegram chat/group/contact, "我的 Telegram", reading/replying/forwarding/summarizing Telegram messages, their unread Telegram, or sending a file/message on Telegram.
|
|
3
|
+
description: Full personal Telegram control over MTProto (Telethon) with the user's own account — list/search chats, read & summarize history, see unread, look up contacts & chat info, download media, and send / reply / forward / edit / delete / react / send files / mark read / join & leave groups. Use when the user mentions Telegram, a Telegram chat/group/contact, "我的 Telegram", reading/replying/forwarding/summarizing Telegram messages, their unread Telegram, joining or leaving a Telegram group/channel, or sending a file/message on Telegram.
|
|
4
4
|
when_to_use: |
|
|
5
5
|
Trigger for anything on the user's personal Telegram account: list recent
|
|
6
6
|
conversations or just the unread ones, read / summarize a chat or group,
|
|
7
7
|
search one chat or across all chats, look up a contact or a chat's info,
|
|
8
8
|
download a photo/file from a message, or take an action — send, reply,
|
|
9
|
-
forward, edit, delete, react, send a file,
|
|
10
|
-
the user's OWN account over MTProto (not a
|
|
9
|
+
forward, edit, delete, react, send a file, mark a chat read, or join / leave
|
|
10
|
+
a group or channel. This drives the user's OWN account over MTProto (not a
|
|
11
|
+
bot), so it sees everything they see.
|
|
11
12
|
connections: [telegram]
|
|
12
13
|
allowed_tools: [Bash]
|
|
13
14
|
license: Apache-2.0
|
|
14
15
|
metadata:
|
|
15
16
|
author: acedatacloud
|
|
16
|
-
version: "1.
|
|
17
|
+
version: "1.3"
|
|
17
18
|
---
|
|
18
19
|
|
|
19
20
|
We drive **personal** Telegram over MTProto with [Telethon](https://docs.telethon.dev/) —
|
|
@@ -42,7 +43,7 @@ python3 "$TG" whoami
|
|
|
42
43
|
```
|
|
43
44
|
|
|
44
45
|
Every state-changing command (`send`, `reply`, `send-file`, `forward`, `edit`, `delete`,
|
|
45
|
-
`react`, `mark-read`) is **gated**: without a trailing `--confirm` it only DRY-RUNS (prints what
|
|
46
|
+
`react`, `mark-read`, `join`, `leave`) is **gated**: without a trailing `--confirm` it only DRY-RUNS (prints what
|
|
46
47
|
it would do, changes nothing). Read commands run directly. `--confirm` is honored **only as the
|
|
47
48
|
last argument** so a message/caption that merely contains "--confirm" can never silently confirm.
|
|
48
49
|
|
|
@@ -106,8 +107,16 @@ python3 "$TG" edit <target> <msg_id> "new text" --confirm # own messages
|
|
|
106
107
|
python3 "$TG" delete <target> <msg_id> --confirm # destructive
|
|
107
108
|
python3 "$TG" react <target> <msg_id> "👍" --confirm
|
|
108
109
|
python3 "$TG" mark-read <target> --confirm # sends read receipts
|
|
110
|
+
python3 "$TG" join <@username|t.me/link|t.me/+invite> --confirm # join a public group/channel or a private invite
|
|
111
|
+
python3 "$TG" leave <target> --confirm # leave a group/channel (not private chats)
|
|
109
112
|
```
|
|
110
113
|
|
|
114
|
+
`join` accepts a public `@username` / `t.me/<name>` (→ JoinChannelRequest) or a private
|
|
115
|
+
invite link `t.me/+HASH` / `t.me/joinchat/HASH` (→ ImportChatInviteRequest). Joining someone
|
|
116
|
+
else's group is a real membership change on the account — treat it like any other write:
|
|
117
|
+
dry-run, confirm, then `--confirm`. Never auto-join then bulk-message; that gets accounts
|
|
118
|
+
spam-limited.
|
|
119
|
+
|
|
111
120
|
The dry run returns `{"dry_run": true, "command": ..., "args": [...]}` — present that to the
|
|
112
121
|
user verbatim as the confirmation prompt.
|
|
113
122
|
|
|
@@ -19,10 +19,10 @@ import tempfile
|
|
|
19
19
|
import urllib.request
|
|
20
20
|
from urllib.parse import urlparse
|
|
21
21
|
|
|
22
|
-
from telethon import TelegramClient
|
|
22
|
+
from telethon import TelegramClient, errors, utils
|
|
23
23
|
from telethon.sessions import StringSession
|
|
24
24
|
from telethon.tl import functions
|
|
25
|
-
from telethon.tl.types import ReactionEmoji
|
|
25
|
+
from telethon.tl.types import ReactionEmoji, User
|
|
26
26
|
|
|
27
27
|
API_ID = int(os.environ["TELEGRAM_API_ID"])
|
|
28
28
|
API_HASH = os.environ["TELEGRAM_API_HASH"]
|
|
@@ -35,7 +35,7 @@ CONFIRM = bool(_raw) and _raw[-1] == "--confirm"
|
|
|
35
35
|
a = _raw[:-1] if CONFIRM else list(_raw)
|
|
36
36
|
cmd = a[0] if a else "help"
|
|
37
37
|
args = a[1:]
|
|
38
|
-
GATED = {"send", "reply", "send-file", "forward", "edit", "delete", "react", "mark-read"}
|
|
38
|
+
GATED = {"send", "reply", "send-file", "forward", "edit", "delete", "react", "mark-read", "join", "leave"}
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
def out(o):
|
|
@@ -252,6 +252,33 @@ async def run():
|
|
|
252
252
|
await cl.send_read_acknowledge(ent)
|
|
253
253
|
out({"marked_read": True})
|
|
254
254
|
|
|
255
|
+
elif cmd == "join":
|
|
256
|
+
need(1); target = args[0]
|
|
257
|
+
# A public @username / t.me/<user> resolves + JoinChannelRequest; a
|
|
258
|
+
# private invite (t.me/+HASH, joinchat/HASH, tg://join?invite=HASH)
|
|
259
|
+
# can't be resolved without joining, so import it by hash directly.
|
|
260
|
+
link_hash, is_invite = utils.parse_username(target)
|
|
261
|
+
try:
|
|
262
|
+
if is_invite:
|
|
263
|
+
res = await cl(functions.messages.ImportChatInviteRequest(link_hash))
|
|
264
|
+
chat = (getattr(res, "chats", None) or [None])[0]
|
|
265
|
+
out({"joined": True, "via": "invite",
|
|
266
|
+
"id": getattr(chat, "id", None), "title": getattr(chat, "title", None)})
|
|
267
|
+
else:
|
|
268
|
+
ent = await resolve(cl, target)
|
|
269
|
+
await cl(functions.channels.JoinChannelRequest(ent))
|
|
270
|
+
out({"joined": True, "via": "public", "id": ent.id,
|
|
271
|
+
"title": getattr(ent, "title", None), "username": getattr(ent, "username", None)})
|
|
272
|
+
except errors.UserAlreadyParticipantError:
|
|
273
|
+
out({"joined": True, "already_member": True, "target": target})
|
|
274
|
+
|
|
275
|
+
elif cmd == "leave":
|
|
276
|
+
need(1); ent = await resolve(cl, args[0])
|
|
277
|
+
if isinstance(ent, User):
|
|
278
|
+
out({"error": "leave applies to groups/channels, not private chats"}); return
|
|
279
|
+
await cl.delete_dialog(ent)
|
|
280
|
+
out({"left": True, "id": ent.id})
|
|
281
|
+
|
|
255
282
|
else:
|
|
256
283
|
out({"error": f"unknown command: {cmd}"}); sys.exit(1)
|
|
257
284
|
|
package/skills/tgstat/SKILL.md
CHANGED
|
@@ -12,7 +12,7 @@ allowed_tools: [Bash, web_search, web_fetch]
|
|
|
12
12
|
license: Apache-2.0
|
|
13
13
|
metadata:
|
|
14
14
|
author: acedatacloud
|
|
15
|
-
version: "2.
|
|
15
|
+
version: "2.3"
|
|
16
16
|
---
|
|
17
17
|
|
|
18
18
|
# TGStat Research
|
|
@@ -105,6 +105,12 @@ mean TGStat did not expose them on the public page; do not call that full
|
|
|
105
105
|
statistics. Use `rankings` when you need reach / citation index for large
|
|
106
106
|
public sources.
|
|
107
107
|
|
|
108
|
+
Individual channel/chat pages are more aggressively bot-protected than the
|
|
109
|
+
ranking pages, so an `info`/`stat` lookup may occasionally return a
|
|
110
|
+
`render blocked` / `could not render` error. It fails fast (well under the
|
|
111
|
+
Bash timeout) — retry once, or fall back to `rankings` and `search` for
|
|
112
|
+
discovery, which are far more reliable.
|
|
113
|
+
|
|
108
114
|
For ad selection, rank by relevant reach/activity rather than subscriber count
|
|
109
115
|
alone. High subscribers with weak reach or chat MAU can indicate an inactive or
|
|
110
116
|
inflated audience. Present metrics as evidence, not a guarantee of lead quality.
|
|
@@ -316,24 +316,44 @@ def _render_token() -> str:
|
|
|
316
316
|
# 422 antibot_blocked, and the render service can transiently 429/5xx.
|
|
317
317
|
_RETRYABLE_RENDER_STATUS = frozenset({422, 425, 429, 500, 502, 503, 504})
|
|
318
318
|
_RENDER_ATTEMPTS = 3
|
|
319
|
+
# Total wall-time budget for one lookup. The agent sandbox kills a Bash command
|
|
320
|
+
# at ~60s, so keep every render (incl. anti-bot retries) safely under that: a
|
|
321
|
+
# stubborn page fails fast instead of being killed mid-retry.
|
|
322
|
+
_RENDER_BUDGET_SECONDS = 46
|
|
323
|
+
# Shared deadline across every render in a single command invocation, so a
|
|
324
|
+
# multi-render command (info tries channel + chat + a ranking fallback) can
|
|
325
|
+
# never exceed the sandbox cap by stacking per-render budgets.
|
|
326
|
+
_OPERATION_DEADLINE: Optional[float] = None
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _op_deadline() -> float:
|
|
330
|
+
global _OPERATION_DEADLINE
|
|
331
|
+
if _OPERATION_DEADLINE is None:
|
|
332
|
+
_OPERATION_DEADLINE = time.monotonic() + _RENDER_BUDGET_SECONDS + 2
|
|
333
|
+
return _OPERATION_DEADLINE
|
|
319
334
|
|
|
320
335
|
|
|
321
336
|
def _request_with_url(url: str, timeout: int) -> Tuple[str, str]:
|
|
322
337
|
# Public tgstat.com is behind Cloudflare; render it through the platform
|
|
323
338
|
# WebExtrator service (headless Chromium) rather than fetching directly.
|
|
324
|
-
# Anti-bot blocks are intermittent, so retry with a
|
|
325
|
-
#
|
|
339
|
+
# Anti-bot blocks are intermittent, so retry with a cache bypass — but stay
|
|
340
|
+
# inside a wall-time budget so the caller never times out mid-retry.
|
|
326
341
|
_validate_request_url(url)
|
|
327
|
-
render_timeout = max(
|
|
342
|
+
render_timeout = max(15, min(timeout, 30))
|
|
328
343
|
token = _render_token()
|
|
329
|
-
|
|
344
|
+
deadline = min(time.monotonic() + _RENDER_BUDGET_SECONDS, _op_deadline())
|
|
345
|
+
last_reason = "render timed out"
|
|
330
346
|
for attempt in range(_RENDER_ATTEMPTS):
|
|
347
|
+
remaining = deadline - time.monotonic()
|
|
348
|
+
if remaining < 8:
|
|
349
|
+
break
|
|
350
|
+
nav_timeout = max(6, int(min(render_timeout, remaining - 5)))
|
|
331
351
|
payload = json.dumps(
|
|
332
352
|
{
|
|
333
353
|
"url": url,
|
|
334
354
|
"wait_until": "domcontentloaded",
|
|
335
|
-
"delay": RENDER_DELAY_SECONDS +
|
|
336
|
-
"timeout":
|
|
355
|
+
"delay": RENDER_DELAY_SECONDS + attempt,
|
|
356
|
+
"timeout": nav_timeout,
|
|
337
357
|
"block_resources": RENDER_BLOCK_RESOURCES,
|
|
338
358
|
"bypass_cache": attempt > 0,
|
|
339
359
|
}
|
|
@@ -348,18 +368,20 @@ def _request_with_url(url: str, timeout: int) -> Tuple[str, str]:
|
|
|
348
368
|
},
|
|
349
369
|
)
|
|
350
370
|
try:
|
|
351
|
-
|
|
371
|
+
# `remaining` bounds the socket op; the render service returns one
|
|
372
|
+
# JSON body, so this single read is what the wall budget relies on.
|
|
373
|
+
with urllib.request.urlopen(request, timeout=remaining) as response:
|
|
352
374
|
envelope = json.loads(response.read().decode("utf-8", "replace"))
|
|
353
375
|
except urllib.error.HTTPError as exc:
|
|
354
376
|
body = exc.read().decode("utf-8", "replace")
|
|
355
377
|
if exc.code in _RETRYABLE_RENDER_STATUS or "antibot" in body.lower():
|
|
356
378
|
last_reason = f"render blocked (HTTP {exc.code}): {_safe_error_body(body)}"
|
|
357
|
-
_render_backoff(attempt)
|
|
379
|
+
_render_backoff(attempt, deadline)
|
|
358
380
|
continue
|
|
359
381
|
raise TGStatError(f"render service HTTP {exc.code}: {_safe_error_body(body)}") from exc
|
|
360
382
|
except urllib.error.URLError as exc:
|
|
361
383
|
last_reason = f"render service unreachable: {_safe_error_body(str(exc.reason))}"
|
|
362
|
-
_render_backoff(attempt)
|
|
384
|
+
_render_backoff(attempt, deadline)
|
|
363
385
|
continue
|
|
364
386
|
except json.JSONDecodeError as exc:
|
|
365
387
|
raise TGStatError(f"render service returned invalid JSON: {_safe_error_body(str(exc))}") from exc
|
|
@@ -375,14 +397,18 @@ def _request_with_url(url: str, timeout: int) -> Tuple[str, str]:
|
|
|
375
397
|
last_reason = f"empty or interstitial render (upstream status {render.get('status')})"
|
|
376
398
|
else:
|
|
377
399
|
last_reason = "render service returned no page data"
|
|
378
|
-
_render_backoff(attempt)
|
|
400
|
+
_render_backoff(attempt, deadline)
|
|
379
401
|
raise TGStatError(f"could not render {_safe_url(url)}: {last_reason}")
|
|
380
402
|
|
|
381
403
|
|
|
382
|
-
def _render_backoff(attempt: int) -> None:
|
|
383
|
-
#
|
|
384
|
-
if attempt
|
|
385
|
-
|
|
404
|
+
def _render_backoff(attempt: int, deadline: float) -> None:
|
|
405
|
+
# Short settle between attempts, but never sleep past the wall-time budget.
|
|
406
|
+
if attempt >= _RENDER_ATTEMPTS - 1:
|
|
407
|
+
return
|
|
408
|
+
remaining = deadline - time.monotonic()
|
|
409
|
+
if remaining <= 8:
|
|
410
|
+
return
|
|
411
|
+
time.sleep(min(1.5 + attempt, 3.0, remaining - 8))
|
|
386
412
|
|
|
387
413
|
|
|
388
414
|
def _request(url: str, timeout: int) -> str:
|
|
@@ -588,7 +614,7 @@ def command_stat(args: argparse.Namespace) -> None:
|
|
|
588
614
|
def build_parser() -> argparse.ArgumentParser:
|
|
589
615
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
590
616
|
parser.add_argument("--host", default=os.environ.get("TGSTAT_PUBLIC_HOST", DEFAULT_PUBLIC_BASE))
|
|
591
|
-
parser.add_argument("--timeout", type=int, default=
|
|
617
|
+
parser.add_argument("--timeout", type=int, default=30)
|
|
592
618
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
593
619
|
|
|
594
620
|
sub.add_parser("profile").set_defaults(func=command_profile)
|
|
@@ -616,6 +642,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
616
642
|
|
|
617
643
|
|
|
618
644
|
def main() -> None:
|
|
645
|
+
global _OPERATION_DEADLINE
|
|
646
|
+
_OPERATION_DEADLINE = None # fresh render budget per command invocation
|
|
619
647
|
args = build_parser().parse_args()
|
|
620
648
|
try:
|
|
621
649
|
args.func(args)
|