@acedatacloud/skills 2026.714.1 → 2026.714.2

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.1",
3
+ "version": "2026.714.2",
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",
@@ -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.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 longer settle and a
325
- # cache bypass before giving up.
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(20, min(timeout, 45))
342
+ render_timeout = max(15, min(timeout, 30))
328
343
  token = _render_token()
329
- last_reason = ""
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 + 2 * attempt,
336
- "timeout": render_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
- with urllib.request.urlopen(request, timeout=render_timeout + 75) as response:
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
- # Skip the wait after the final attempt; give Cloudflare a moment otherwise.
384
- if attempt < _RENDER_ATTEMPTS - 1:
385
- time.sleep(min(2 + 2 * attempt, 6))
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=45)
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)