@acedatacloud/skills 2026.622.3 → 2026.622.4

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.622.3",
3
+ "version": "2026.622.4",
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",
@@ -25,6 +25,7 @@ from __future__ import annotations
25
25
  import argparse
26
26
  import gzip
27
27
  import hashlib
28
+ import html as _html
28
29
  import ipaddress
29
30
  import json
30
31
  import os
@@ -287,7 +288,7 @@ _IMG_SKIP = ("hdslb.com", "bilibili.com", "biliimg.com")
287
288
  # matches both HTML <img src="..."> and markdown ![alt](...)
288
289
  # <img ... src = "..."> / '...' (tolerates spaces and either quote style)
289
290
  _HTML_IMG = re.compile(r"""(<img\b[^>]*?\bsrc\s*=\s*)(["'])(https?://[^"']+)(\2)""", re.IGNORECASE)
290
- _MD_IMG = re.compile(r"!\[([^\]]*)\]\((https?://[^)\s]+)\)")
291
+ _MD_IMG = re.compile(r"!\[([^\]]{0,500})\]\((https?://[^)\s]{0,2000})\)") # bounded: no O(n²) on "![" * N
291
292
  UPCOVER = "https://api.bilibili.com/x/article/creative/article/upcover"
292
293
 
293
294
 
@@ -429,6 +430,121 @@ def rehost_images(jar, csrf, content):
429
430
  return content
430
431
 
431
432
 
433
+ # ── Markdown → HTML (专栏 body is HTML; convert if the caller passed Markdown) ──
434
+
435
+ # Bounded so a single token can't scan to EOF — keeps these linear (no O(n²)
436
+ # backtracking on malformed input like "![" * N). Real alt < 500 / URL < 2000.
437
+ _IMG_RE = re.compile(r"!\[([^\]]{0,500})\]\(([^)\s]{0,2000})\)")
438
+ _LINK_RE = re.compile(r"(?<!!)\[([^\]]{0,500})\]\(([^)\s]{0,2000})\)")
439
+
440
+
441
+ def _looks_like_markdown(s):
442
+ # Three-way classification (no full parser): block-level HTML ⇒ already an
443
+ # HTML document → pass through (even if it also contains markdown-looking
444
+ # text like **bold**); else markdown markers ⇒ render; else inline-only HTML
445
+ # ⇒ pass through; else plain text ⇒ render (md_to_html just wraps/escapes it).
446
+ s = s or ""
447
+ block_html = re.search(
448
+ r"</?(?:p|div|h[1-6]|ul|ol|li|table|thead|tbody|tr|td|th|blockquote|"
449
+ r"pre|figure|figcaption|section|article|header|footer|nav|aside|hr|"
450
+ r"main|details|summary)\b",
451
+ s, re.I,
452
+ )
453
+ if block_html:
454
+ return False
455
+ # ^-anchored (re.M) + horizontal-only whitespace + bounded {0,N} repeats so
456
+ # the scan can't backtrack across newlines or to EOF (no quadratic scanning).
457
+ has_md = re.search(
458
+ r"^#{1,6}\s|!\[[^\]]{0,500}\]\([^)]{0,2000}\)|^[ \t]*[-*+]\s|^[ \t]*\d+\.\s"
459
+ r"|\[[^\]]{0,500}\]\([^)]{0,2000}\)|`[^`]{1,500}`|\*\*[^*]{1,500}\*\*",
460
+ s, re.M,
461
+ )
462
+ if has_md:
463
+ return True
464
+ inline_html = re.search(
465
+ r"</?(?:a|strong|em|b|i|u|s|span|code|br|img|small|mark|sup|sub|"
466
+ r"video|audio|iframe)\b",
467
+ s, re.I,
468
+ )
469
+ return not inline_html
470
+
471
+
472
+ def _attr_url(u):
473
+ # Strip C0 controls/space FIRST, then allow only http/https/mailto — otherwise
474
+ # `\x01javascript:` would slip past a literal scheme check yet be re-normalised
475
+ # to javascript: by the browser. Finally neutralise attribute-breaking quotes.
476
+ u = re.sub(r"[\x00-\x20\x7f]", "", u or "")
477
+ m = re.match(r"(?i)([a-z][a-z0-9+.\-]*):", u)
478
+ if m and m.group(1).lower() not in ("http", "https", "mailto"):
479
+ return "#"
480
+ return u.replace('"', "%22").replace("'", "%27")
481
+
482
+
483
+ def _alt(s):
484
+ return (s or "").replace('"', "&quot;").replace("'", "&#x27;")
485
+
486
+
487
+ def _inline_md(t):
488
+ t = _html.escape(t, quote=False).replace("\x00", "")
489
+ # Stash code spans FIRST so emphasis/link/image markup inside `…` stays
490
+ # literal (e.g. `**x**` must render as text, not bold).
491
+ spans = []
492
+
493
+ def _stash(m):
494
+ spans.append("<code>" + m.group(1) + "</code>")
495
+ return f"\x00{len(spans) - 1}\x00"
496
+
497
+ t = re.sub(r"`([^`]+)`", _stash, t)
498
+ t = _IMG_RE.sub(lambda m: f'<img src="{_attr_url(m.group(2))}" alt="{_alt(m.group(1))}">', t)
499
+ t = _LINK_RE.sub(lambda m: f'<a href="{_attr_url(m.group(2))}">{m.group(1)}</a>', t)
500
+ t = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", t)
501
+ t = re.sub(r"(?<!\*)\*([^*\n]+)\*(?!\*)", r"<em>\1</em>", t)
502
+ t = re.sub(r"\x00(\d+)\x00", lambda m: spans[int(m.group(1))], t)
503
+ return t
504
+
505
+
506
+ def _md_block_to_html(b):
507
+ b = b.strip("\n")
508
+ if not b.strip():
509
+ return None
510
+ f = b.lstrip()
511
+ m = re.match(r"(#{1,6})\s+(.*)", f)
512
+ if m:
513
+ lvl = len(m.group(1))
514
+ return f"<h{lvl}>{_inline_md(m.group(2).strip())}</h{lvl}>"
515
+ if f.startswith(">"):
516
+ inner = re.sub(r"^>\s?", "", b, flags=re.M).replace("\n", " ")
517
+ return "<blockquote><p>" + _inline_md(inner) + "</p></blockquote>"
518
+ if re.match(r"[-*+]\s+", f):
519
+ items = [_inline_md(re.sub(r"^[-*+]\s+", "", ln)) for ln in b.split("\n") if ln.strip()]
520
+ return "<ul>" + "".join(f"<li>{x}</li>" for x in items) + "</ul>"
521
+ if re.match(r"\d+\.\s+", f):
522
+ items = [_inline_md(re.sub(r"^\d+\.\s+", "", ln)) for ln in b.split("\n") if ln.strip()]
523
+ return "<ol>" + "".join(f"<li>{x}</li>" for x in items) + "</ol>"
524
+ only = re.fullmatch(r"!\[([^\]]{0,500})\]\(([^)\s]{0,2000})\)", f)
525
+ if only:
526
+ return f'<img src="{_attr_url(only.group(2))}" alt="{_alt(_html.escape(only.group(1), quote=False))}">'
527
+ return "<p>" + _inline_md(b.replace("\n", " ").strip()) + "</p>"
528
+
529
+
530
+ def md_to_html(src):
531
+ src = (src or "").strip()
532
+ out = []
533
+ # Pull fenced code out FIRST (it may itself contain blank lines) so the
534
+ # blank-line block splitter below can never tear a ``` … ``` fence apart.
535
+ for i, part in enumerate(re.split(r"(?ms)^(```.*?(?:\n```[ \t]*$|\Z))", src)):
536
+ if i % 2 == 1: # fenced code segment
537
+ code = re.sub(r"\A```[^\n]*\n?", "", part)
538
+ code = re.sub(r"\n?```[ \t]*\Z", "", code)
539
+ out.append("<pre><code>" + _html.escape(code) + "</code></pre>")
540
+ continue
541
+ for b in re.split(r"\n[ \t]*\n", part): # blank-line split (no cross-newline \s*)
542
+ block = _md_block_to_html(b)
543
+ if block:
544
+ out.append(block)
545
+ return "\n".join(out)
546
+
547
+
432
548
  def cmd_publish(jar, args):
433
549
  if not args.title:
434
550
  die("--title is required")
@@ -461,6 +577,11 @@ def cmd_publish(jar, args):
461
577
  if not args.no_rehost_images:
462
578
  content = rehost_images(jar, csrf, content)
463
579
 
580
+ # 专栏 content is HTML; if the caller passed Markdown, render it (else the
581
+ # article shows literal `##`/`![]()` and runs together as one paragraph).
582
+ if _looks_like_markdown(content):
583
+ content = md_to_html(content)
584
+
464
585
  ref = "https://member.bilibili.com/"
465
586
  base = {
466
587
  "title": args.title, "content": content, "csrf": csrf,
@@ -28,6 +28,7 @@ import base64
28
28
  import gzip
29
29
  import hashlib
30
30
  import hmac
31
+ import html as _html
31
32
  import ipaddress
32
33
  import json
33
34
  import mimetypes
@@ -286,7 +287,7 @@ def cmd_article(jar, args):
286
287
  # Signed signature → Huawei OBS multipart → csdnimg.cn URL. Mirrors our
287
288
  # PlatformPublisher csdn.py flow.
288
289
 
289
- _MD_IMG = re.compile(r"!\[([^\]]*)\]\((https?://[^)\s]+)\)")
290
+ _MD_IMG = re.compile(r"!\[([^\]]{0,500})\]\((https?://[^)\s]{0,2000})\)") # bounded: no O(n²) on "![" * N
290
291
  _IMG_SKIP = ("csdnimg.cn", "csdn.net")
291
292
 
292
293
 
@@ -416,6 +417,126 @@ def rehost_images(jar, content):
416
417
  return _MD_IMG.sub(repl, content)
417
418
 
418
419
 
420
+ # ── Markdown → HTML (CSDN renders the `content` HTML field, not the source) ──
421
+
422
+ # Bounded so a single token can't scan to EOF — keeps these linear (no O(n²)
423
+ # backtracking on malformed input like "![" * N). Real alt < 500 / URL < 2000.
424
+ _IMG_RE = re.compile(r"!\[([^\]]{0,500})\]\(([^)\s]{0,2000})\)")
425
+ _LINK_RE = re.compile(r"(?<!!)\[([^\]]{0,500})\]\(([^)\s]{0,2000})\)")
426
+
427
+
428
+ def _attr_url(u):
429
+ # Strip C0 controls/space FIRST, then allow only http/https/mailto — otherwise
430
+ # `\x01javascript:` would slip past a literal scheme check yet be re-normalised
431
+ # to javascript: by the browser. Finally neutralise attribute-breaking quotes.
432
+ u = re.sub(r"[\x00-\x20\x7f]", "", u or "")
433
+ m = re.match(r"(?i)([a-z][a-z0-9+.\-]*):", u)
434
+ if m and m.group(1).lower() not in ("http", "https", "mailto"):
435
+ return "#"
436
+ return u.replace('"', "%22").replace("'", "%27")
437
+
438
+
439
+ def _alt(s):
440
+ # alt text is already &/</>-escaped by the line escape; only the attribute
441
+ # quote can still break out, so neutralise it.
442
+ return (s or "").replace('"', "&quot;").replace("'", "&#x27;")
443
+
444
+
445
+ def _inline_md(t):
446
+ t = _html.escape(t, quote=False).replace("\x00", "")
447
+ # Stash code spans FIRST so emphasis/link/image markup inside `…` stays
448
+ # literal (e.g. `**x**` must render as text, not bold).
449
+ spans = []
450
+
451
+ def _stash(m):
452
+ spans.append("<code>" + m.group(1) + "</code>")
453
+ return f"\x00{len(spans) - 1}\x00"
454
+
455
+ t = re.sub(r"`([^`]+)`", _stash, t)
456
+ t = _IMG_RE.sub(lambda m: f'<img src="{_attr_url(m.group(2))}" alt="{_alt(m.group(1))}">', t)
457
+ t = _LINK_RE.sub(lambda m: f'<a href="{_attr_url(m.group(2))}">{m.group(1)}</a>', t)
458
+ t = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", t)
459
+ t = re.sub(r"(?<!\*)\*([^*\n]+)\*(?!\*)", r"<em>\1</em>", t)
460
+ t = re.sub(r"\x00(\d+)\x00", lambda m: spans[int(m.group(1))], t)
461
+ return t
462
+
463
+
464
+ def _md_block_to_html(b):
465
+ b = b.strip("\n")
466
+ if not b.strip():
467
+ return None
468
+ f = b.lstrip()
469
+ m = re.match(r"(#{1,6})\s+(.*)", f)
470
+ if m:
471
+ lvl = len(m.group(1))
472
+ return f"<h{lvl}>{_inline_md(m.group(2).strip())}</h{lvl}>"
473
+ if f.startswith(">"):
474
+ inner = re.sub(r"^>\s?", "", b, flags=re.M).replace("\n", " ")
475
+ return "<blockquote><p>" + _inline_md(inner) + "</p></blockquote>"
476
+ if re.match(r"[-*+]\s+", f):
477
+ items = [_inline_md(re.sub(r"^[-*+]\s+", "", ln)) for ln in b.split("\n") if ln.strip()]
478
+ return "<ul>" + "".join(f"<li>{x}</li>" for x in items) + "</ul>"
479
+ if re.match(r"\d+\.\s+", f):
480
+ items = [_inline_md(re.sub(r"^\d+\.\s+", "", ln)) for ln in b.split("\n") if ln.strip()]
481
+ return "<ol>" + "".join(f"<li>{x}</li>" for x in items) + "</ol>"
482
+ only = re.fullmatch(r"!\[([^\]]{0,500})\]\(([^)\s]{0,2000})\)", f)
483
+ if only:
484
+ return f'<img src="{_attr_url(only.group(2))}" alt="{_alt(_html.escape(only.group(1), quote=False))}">'
485
+ return "<p>" + _inline_md(b.replace("\n", " ").strip()) + "</p>"
486
+
487
+
488
+ def md_to_html(src):
489
+ """Minimal stdlib Markdown→HTML for the platforms whose body is HTML.
490
+ Covers headings, paragraphs, standalone images, links, bold/italic/code,
491
+ fenced code, blockquotes, and ordered/unordered lists."""
492
+ src = (src or "").strip()
493
+ out = []
494
+ # Pull fenced code out FIRST (it may itself contain blank lines) so the
495
+ # blank-line block splitter below can never tear a ``` … ``` fence apart.
496
+ for i, part in enumerate(re.split(r"(?ms)^(```.*?(?:\n```[ \t]*$|\Z))", src)):
497
+ if i % 2 == 1: # fenced code segment
498
+ code = re.sub(r"\A```[^\n]*\n?", "", part)
499
+ code = re.sub(r"\n?```[ \t]*\Z", "", code)
500
+ out.append("<pre><code>" + _html.escape(code) + "</code></pre>")
501
+ continue
502
+ for b in re.split(r"\n[ \t]*\n", part): # blank-line split (no cross-newline \s*)
503
+ block = _md_block_to_html(b)
504
+ if block:
505
+ out.append(block)
506
+ return "\n".join(out)
507
+
508
+
509
+ def _looks_like_markdown(s):
510
+ # Three-way classification (no full parser): block-level HTML ⇒ already an
511
+ # HTML document → pass through (even if it also contains markdown-looking
512
+ # text like **bold**); else markdown markers ⇒ render; else inline-only HTML
513
+ # ⇒ pass through; else plain text ⇒ render (md_to_html just wraps/escapes it).
514
+ s = s or ""
515
+ block_html = re.search(
516
+ r"</?(?:p|div|h[1-6]|ul|ol|li|table|thead|tbody|tr|td|th|blockquote|"
517
+ r"pre|figure|figcaption|section|article|header|footer|nav|aside|hr|"
518
+ r"main|details|summary)\b",
519
+ s, re.I,
520
+ )
521
+ if block_html:
522
+ return False
523
+ # ^-anchored (re.M) + horizontal-only whitespace + bounded {0,N} repeats so
524
+ # the scan can't backtrack across newlines or to EOF (no quadratic scanning).
525
+ has_md = re.search(
526
+ r"^#{1,6}\s|!\[[^\]]{0,500}\]\([^)]{0,2000}\)|^[ \t]*[-*+]\s|^[ \t]*\d+\.\s"
527
+ r"|\[[^\]]{0,500}\]\([^)]{0,2000}\)|`[^`]{1,500}`|\*\*[^*]{1,500}\*\*",
528
+ s, re.M,
529
+ )
530
+ if has_md:
531
+ return True
532
+ inline_html = re.search(
533
+ r"</?(?:a|strong|em|b|i|u|s|span|code|br|img|small|mark|sup|sub|"
534
+ r"video|audio|iframe)\b",
535
+ s, re.I,
536
+ )
537
+ return not inline_html
538
+
539
+
419
540
  def cmd_publish(jar, args):
420
541
  if not args.title:
421
542
  die("--title is required")
@@ -450,8 +571,12 @@ def cmd_publish(jar, args):
450
571
  status_code = 2 if args.draft_only else 0
451
572
  body = {
452
573
  "title": args.title,
574
+ # markdowncontent = the editor source; content = the RENDERED HTML CSDN
575
+ # actually displays. Sending raw markdown as `content` shows literal
576
+ # `##`/`![]()` and collapses newlines — it MUST be HTML. Already-HTML
577
+ # callers are passed through untouched (don't double-escape).
453
578
  "markdowncontent": content,
454
- "content": content,
579
+ "content": md_to_html(content) if _looks_like_markdown(content) else content,
455
580
  "readType": "public",
456
581
  "status": status_code,
457
582
  "categories": "",