vibego 0.2.39__py3-none-any.whl → 0.2.40__py3-none-any.whl
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.
Potentially problematic release.
This version of vibego might be problematic. Click here for more details.
- bot.py +37 -35
- {vibego-0.2.39.dist-info → vibego-0.2.40.dist-info}/METADATA +1 -1
- {vibego-0.2.39.dist-info → vibego-0.2.40.dist-info}/RECORD +7 -8
- {vibego-0.2.39.dist-info → vibego-0.2.40.dist-info}/top_level.txt +0 -1
- vibego_cli/__init__.py +1 -1
- telegram_markdown/__init__.py +0 -269
- {vibego-0.2.39.dist-info → vibego-0.2.40.dist-info}/WHEEL +0 -0
- {vibego-0.2.39.dist-info → vibego-0.2.40.dist-info}/entry_points.txt +0 -0
bot.py
CHANGED
|
@@ -69,8 +69,6 @@ from tasks.fsm import (
|
|
|
69
69
|
TaskNoteStates,
|
|
70
70
|
TaskPushStates,
|
|
71
71
|
)
|
|
72
|
-
from telegram_markdown import render_markdown_to_telegram
|
|
73
|
-
|
|
74
72
|
# --- 简单 .env 加载 ---
|
|
75
73
|
def load_env(p: str = ".env"):
|
|
76
74
|
"""从指定路径加载 dotenv 格式的键值对到进程环境变量。"""
|
|
@@ -128,7 +126,7 @@ TEST_PHASE_PROMPT = f"进入测试阶段{AGENTS_PHASE_SUFFIX}"
|
|
|
128
126
|
# 报告缺陷时的专用前缀,插入在统一提示语之前
|
|
129
127
|
BUG_REPORT_PREFIX = "报告一个缺陷,详见底部最新的缺陷描述。"
|
|
130
128
|
|
|
131
|
-
_parse_mode_env = (os.environ.get("TELEGRAM_PARSE_MODE") or "
|
|
129
|
+
_parse_mode_env = (os.environ.get("TELEGRAM_PARSE_MODE") or "Markdown").strip()
|
|
132
130
|
_parse_mode_key = _parse_mode_env.replace("-", "").replace("_", "").lower()
|
|
133
131
|
MODEL_OUTPUT_PARSE_MODE: Optional[ParseMode]
|
|
134
132
|
if _parse_mode_key in _PARSE_MODE_CANDIDATES:
|
|
@@ -142,11 +140,6 @@ if _parse_mode_key in _PARSE_MODE_CANDIDATES:
|
|
|
142
140
|
else str(MODEL_OUTPUT_PARSE_MODE)
|
|
143
141
|
)
|
|
144
142
|
worker_log.info("模型输出 parse_mode:%s", mode_value)
|
|
145
|
-
if MODEL_OUTPUT_PARSE_MODE != ParseMode.MARKDOWN_V2:
|
|
146
|
-
worker_log.warning(
|
|
147
|
-
"当前 parse_mode=%s,将按配置发送原始转义符号(推荐 MarkdownV2)",
|
|
148
|
-
mode_value,
|
|
149
|
-
)
|
|
150
143
|
else:
|
|
151
144
|
MODEL_OUTPUT_PARSE_MODE = ParseMode.MARKDOWN_V2
|
|
152
145
|
worker_log.warning(
|
|
@@ -586,29 +579,17 @@ def _unescape_if_already_escaped(text: str) -> str:
|
|
|
586
579
|
def _prepare_model_payload(text: str) -> str:
|
|
587
580
|
if _IS_MARKDOWN_V2:
|
|
588
581
|
cleaned = _unescape_if_already_escaped(text)
|
|
589
|
-
return
|
|
582
|
+
return _escape_markdown_v2(cleaned)
|
|
590
583
|
if _IS_MARKDOWN:
|
|
591
584
|
return _normalize_legacy_markdown(text)
|
|
592
585
|
return text
|
|
593
586
|
|
|
594
587
|
|
|
595
|
-
def _optimize_markdown_v2_payload(payload: str) -> str:
|
|
596
|
-
"""占位函数:保留严格转义版本,确保 MarkdownV2 合规。"""
|
|
597
|
-
|
|
598
|
-
return payload
|
|
599
|
-
|
|
600
|
-
|
|
601
588
|
def _prepare_model_payload_variants(text: str) -> tuple[str, Optional[str]]:
|
|
602
|
-
"""
|
|
603
|
-
|
|
604
|
-
strict_payload = _prepare_model_payload(text)
|
|
605
|
-
if not _IS_MARKDOWN_V2:
|
|
606
|
-
return strict_payload, None
|
|
589
|
+
"""返回首选与备用内容,默认为单一格式。"""
|
|
607
590
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
return optimized_payload, strict_payload
|
|
611
|
-
return strict_payload, None
|
|
591
|
+
payload = _prepare_model_payload(text)
|
|
592
|
+
return payload, None
|
|
612
593
|
|
|
613
594
|
|
|
614
595
|
def _extract_bad_request_message(exc: TelegramBadRequest) -> str:
|
|
@@ -714,20 +695,12 @@ async def _send_with_markdown_guard(
|
|
|
714
695
|
if raw_sender is None:
|
|
715
696
|
raise
|
|
716
697
|
|
|
717
|
-
# Markdown 彻底失败时退回纯文本发送,需要先清理 MarkdownV2 转义符号
|
|
718
|
-
fallback_payload = _force_unescape_markdown(text)
|
|
719
|
-
if fallback_payload != text:
|
|
720
|
-
worker_log.debug(
|
|
721
|
-
"Markdown 降级为纯文本发送,已移除转义符号",
|
|
722
|
-
extra={"original_length": len(text), "fallback_length": len(fallback_payload)},
|
|
723
|
-
)
|
|
724
|
-
|
|
725
698
|
worker_log.warning(
|
|
726
699
|
"Markdown 解析仍失败,将以纯文本发送",
|
|
727
700
|
extra={"length": len(text)},
|
|
728
701
|
)
|
|
729
|
-
await raw_sender(
|
|
730
|
-
return
|
|
702
|
+
await raw_sender(text)
|
|
703
|
+
return text
|
|
731
704
|
|
|
732
705
|
|
|
733
706
|
async def _notify_send_failure_message(chat_id: int) -> None:
|
|
@@ -4276,6 +4249,7 @@ async def _deliver_pending_messages(
|
|
|
4276
4249
|
"chat": chat_id,
|
|
4277
4250
|
},
|
|
4278
4251
|
)
|
|
4252
|
+
return False
|
|
4279
4253
|
else:
|
|
4280
4254
|
worker_log.info(
|
|
4281
4255
|
"模型输出已发送且计划完成",
|
|
@@ -4602,7 +4576,13 @@ async def _interrupt_long_poll(chat_id: int) -> None:
|
|
|
4602
4576
|
线程安全:使用 asyncio.Lock 保护状态访问。
|
|
4603
4577
|
"""
|
|
4604
4578
|
if CHAT_LONG_POLL_LOCK is None:
|
|
4605
|
-
|
|
4579
|
+
state = CHAT_LONG_POLL_STATE.get(chat_id)
|
|
4580
|
+
if state is not None:
|
|
4581
|
+
state["interrupted"] = True
|
|
4582
|
+
worker_log.info(
|
|
4583
|
+
"标记延迟轮询为待中断",
|
|
4584
|
+
extra={"chat": chat_id},
|
|
4585
|
+
)
|
|
4606
4586
|
return
|
|
4607
4587
|
|
|
4608
4588
|
async with CHAT_LONG_POLL_LOCK:
|
|
@@ -4695,6 +4675,13 @@ async def _watch_and_notify(chat_id: int, session_path: Path,
|
|
|
4695
4675
|
"max_rounds": long_poll_max_rounds,
|
|
4696
4676
|
"interrupted": False,
|
|
4697
4677
|
}
|
|
4678
|
+
else:
|
|
4679
|
+
CHAT_LONG_POLL_STATE[chat_id] = {
|
|
4680
|
+
"active": True,
|
|
4681
|
+
"round": 0,
|
|
4682
|
+
"max_rounds": long_poll_max_rounds,
|
|
4683
|
+
"interrupted": False,
|
|
4684
|
+
}
|
|
4698
4685
|
worker_log.info(
|
|
4699
4686
|
"首次发送成功,启动延迟轮询模式",
|
|
4700
4687
|
extra={
|
|
@@ -4716,6 +4703,10 @@ async def _watch_and_notify(chat_id: int, session_path: Path,
|
|
|
4716
4703
|
state = CHAT_LONG_POLL_STATE.get(chat_id)
|
|
4717
4704
|
if state is not None:
|
|
4718
4705
|
state["round"] = 0
|
|
4706
|
+
else:
|
|
4707
|
+
state = CHAT_LONG_POLL_STATE.get(chat_id)
|
|
4708
|
+
if state is not None:
|
|
4709
|
+
state["round"] = 0
|
|
4719
4710
|
worker_log.info(
|
|
4720
4711
|
"延迟轮询中收到新消息,重置计数",
|
|
4721
4712
|
extra={
|
|
@@ -4731,6 +4722,10 @@ async def _watch_and_notify(chat_id: int, session_path: Path,
|
|
|
4731
4722
|
state = CHAT_LONG_POLL_STATE.get(chat_id)
|
|
4732
4723
|
if state is not None:
|
|
4733
4724
|
state["round"] = long_poll_rounds
|
|
4725
|
+
else:
|
|
4726
|
+
state = CHAT_LONG_POLL_STATE.get(chat_id)
|
|
4727
|
+
if state is not None:
|
|
4728
|
+
state["round"] = long_poll_rounds
|
|
4734
4729
|
|
|
4735
4730
|
if long_poll_rounds >= long_poll_max_rounds:
|
|
4736
4731
|
worker_log.info(
|
|
@@ -4767,6 +4762,13 @@ async def _watch_and_notify(chat_id: int, session_path: Path,
|
|
|
4767
4762
|
"监听任务退出,已清理延迟轮询状态",
|
|
4768
4763
|
extra={"chat": chat_id},
|
|
4769
4764
|
)
|
|
4765
|
+
else:
|
|
4766
|
+
if chat_id in CHAT_LONG_POLL_STATE:
|
|
4767
|
+
CHAT_LONG_POLL_STATE.pop(chat_id, None)
|
|
4768
|
+
worker_log.debug(
|
|
4769
|
+
"监听任务退出,已清理延迟轮询状态",
|
|
4770
|
+
extra={"chat": chat_id},
|
|
4771
|
+
)
|
|
4770
4772
|
|
|
4771
4773
|
|
|
4772
4774
|
def _read_pointer_path(pointer: Path) -> Optional[Path]:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
bot.py,sha256=
|
|
1
|
+
bot.py,sha256=TthqY_Rvndzd3pgLH6lvPcsDXQbwkuMfB4W9qt_Li8I,273832
|
|
2
2
|
logging_setup.py,sha256=gvxHi8mUwK3IhXJrsGNTDo-DR6ngkyav1X-tvlBF_IE,4613
|
|
3
3
|
master.py,sha256=ZW4A3Gh0MUKFnfZX-VJ7OCNnBzlEcOWkPKYH92OfyKA,112967
|
|
4
4
|
project_repository.py,sha256=UcthtSGOJK0cTE5bQCneo3xkomRG-kyc1N1QVqxeHIs,17577
|
|
@@ -426,15 +426,14 @@ tasks/constants.py,sha256=tS1kZxBIUm3JJUMHm25XI-KHNUZl5NhbbuzjzL_rF-c,299
|
|
|
426
426
|
tasks/fsm.py,sha256=rKXXLEieQQU4r2z_CZUvn1_70FXiZXBBugF40gpe_tQ,1476
|
|
427
427
|
tasks/models.py,sha256=N_qqRBo9xMSV0vbn4k6bLBXT8C_dp_oTFUxvdx16ZQM,2459
|
|
428
428
|
tasks/service.py,sha256=w_S_aWiVqRXzXEpimLDsuCCCX2lB5uDkff9aKThBw9c,41916
|
|
429
|
-
|
|
430
|
-
vibego_cli/__init__.py,sha256=JrNDd69m19jqQtCE-skg54kfgY00_IW_NusHO-VuUZg,311
|
|
429
|
+
vibego_cli/__init__.py,sha256=mkajWsGOmqjBWy1uSiLsUbptT642DZPYn7LoFD0LU7o,311
|
|
431
430
|
vibego_cli/__main__.py,sha256=qqTrYmRRLe4361fMzbI3-CqpZ7AhTofIHmfp4ykrrBY,158
|
|
432
431
|
vibego_cli/config.py,sha256=VxkPJMq01tA3h3cOkH-z_tiP7pMgfSGGicRvUnCWkhI,3054
|
|
433
432
|
vibego_cli/deps.py,sha256=1nRXI7Dd-S1hYE8DligzK5fIluQWETRUj4_OKL0DikQ,1419
|
|
434
433
|
vibego_cli/main.py,sha256=X__NXwZnIDIFbdKSTbNyZgZHKcPlN0DQz9sqTI1aQ9E,12158
|
|
435
434
|
vibego_cli/data/worker_requirements.txt,sha256=QSt30DSSSHtfucTFPpc7twk9kLS5rVLNTcvDiagxrZg,62
|
|
436
|
-
vibego-0.2.
|
|
437
|
-
vibego-0.2.
|
|
438
|
-
vibego-0.2.
|
|
439
|
-
vibego-0.2.
|
|
440
|
-
vibego-0.2.
|
|
435
|
+
vibego-0.2.40.dist-info/METADATA,sha256=XiZcCoOO5eGJ6nG9Xt92Bt9R-zViJfjf3In31pzE78M,10519
|
|
436
|
+
vibego-0.2.40.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
437
|
+
vibego-0.2.40.dist-info/entry_points.txt,sha256=Lsy_zm-dlyxt8-9DL9blBReIwU2k22c8-kifr46ND1M,48
|
|
438
|
+
vibego-0.2.40.dist-info/top_level.txt,sha256=R56CT3nW5H5v3ce0l3QDN4-C4qxTrNWzRTwrxnkDX4U,69
|
|
439
|
+
vibego-0.2.40.dist-info/RECORD,,
|
vibego_cli/__init__.py
CHANGED
telegram_markdown/__init__.py
DELETED
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
自定义 Markdown → Telegram MarkdownV2 渲染器。
|
|
3
|
-
|
|
4
|
-
依赖 markdown-it-py 提供的 CommonMark 解析能力,将常见 Markdown
|
|
5
|
-
语法(段落、标题、列表、引用、加粗、斜体、代码块、行内代码、链接等)
|
|
6
|
-
转换为符合 Telegram MarkdownV2 语法的文本。
|
|
7
|
-
|
|
8
|
-
Telegram 对 MarkdownV2 的语法要求非常严格,特殊字符必须转义,否则会直接
|
|
9
|
-
返回 “can't parse entities” 错误。借助解析树,我们可以准确地区分格式化标记
|
|
10
|
-
与普通文本,做到“格式保留 + 无额外反斜杠”。
|
|
11
|
-
"""
|
|
12
|
-
from __future__ import annotations
|
|
13
|
-
|
|
14
|
-
import re
|
|
15
|
-
from typing import Iterable, List, Optional, Tuple
|
|
16
|
-
|
|
17
|
-
from markdown_it import MarkdownIt
|
|
18
|
-
from markdown_it.token import Token
|
|
19
|
-
|
|
20
|
-
_TELEGRAM_SPECIAL_CHARS = re.compile(r"([_*\[\]()~`>#+\-=|{}.!\\])")
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def _escape_plain_text(value: str) -> str:
|
|
24
|
-
"""转义 Telegram MarkdownV2 的特殊字符。"""
|
|
25
|
-
if not value:
|
|
26
|
-
return ""
|
|
27
|
-
return _TELEGRAM_SPECIAL_CHARS.sub(r"\\\1", value)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def _escape_url(value: str) -> str:
|
|
31
|
-
"""URL 中统一转义括号、空格等特殊字符。"""
|
|
32
|
-
if not value:
|
|
33
|
-
return ""
|
|
34
|
-
escaped = value.replace("\\", "\\\\")
|
|
35
|
-
escaped = escaped.replace(")", r"\)").replace("(", r"\(")
|
|
36
|
-
return escaped
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def _escape_inline_code(value: str) -> str:
|
|
40
|
-
"""行内代码仅需要处理反斜杠与反引号。"""
|
|
41
|
-
if not value:
|
|
42
|
-
return "``"
|
|
43
|
-
escaped = value.replace("\\", "\\\\").replace("`", r"\`")
|
|
44
|
-
return f"`{escaped}`"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def _escape_code_block(value: str, language: str = "") -> str:
|
|
48
|
-
"""多行代码块同样保留原样,只需保护反引号。"""
|
|
49
|
-
content = (value or "").rstrip("\n")
|
|
50
|
-
content = content.replace("```", r"\`\`\`")
|
|
51
|
-
header = f"```{language.strip()}\n" if language else "```\n"
|
|
52
|
-
return f"{header}{content}\n```"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
class _TelegramMarkdownRenderer:
|
|
56
|
-
"""对 markdown-it 解析出的 Token 序列进行 Telegram MarkdownV2 序列化。"""
|
|
57
|
-
|
|
58
|
-
def __init__(self) -> None:
|
|
59
|
-
self._parser = MarkdownIt("commonmark", {"linkify": True})
|
|
60
|
-
|
|
61
|
-
# ---- 外部接口 ---------------------------------------------------------
|
|
62
|
-
def render(self, markdown_text: str) -> str:
|
|
63
|
-
tokens = self._parser.parse(markdown_text or "")
|
|
64
|
-
return self._render_block_tokens(tokens, 0, len(tokens)).strip()
|
|
65
|
-
|
|
66
|
-
# ---- Block 级渲染 -----------------------------------------------------
|
|
67
|
-
def _render_block_tokens(self, tokens: List[Token], start: int, end: int) -> str:
|
|
68
|
-
blocks: List[str] = []
|
|
69
|
-
index = start
|
|
70
|
-
while index < end:
|
|
71
|
-
token = tokens[index]
|
|
72
|
-
token_type = token.type
|
|
73
|
-
|
|
74
|
-
if token_type == "paragraph_open":
|
|
75
|
-
inline = tokens[index + 1]
|
|
76
|
-
blocks.append(self._render_inline(inline.children or []))
|
|
77
|
-
index += 3 # 跳过 inline + paragraph_close
|
|
78
|
-
continue
|
|
79
|
-
|
|
80
|
-
if token_type == "heading_open":
|
|
81
|
-
inline = tokens[index + 1]
|
|
82
|
-
content = self._render_inline(inline.children or [])
|
|
83
|
-
blocks.append(f"*{content}*")
|
|
84
|
-
index += 3
|
|
85
|
-
continue
|
|
86
|
-
|
|
87
|
-
if token_type == "bullet_list_open":
|
|
88
|
-
close_idx = self._find_matching(tokens, index, "bullet_list_close")
|
|
89
|
-
blocks.append(self._render_list(tokens, index + 1, close_idx, ordered=False))
|
|
90
|
-
index = close_idx + 1
|
|
91
|
-
continue
|
|
92
|
-
|
|
93
|
-
if token_type == "ordered_list_open":
|
|
94
|
-
close_idx = self._find_matching(tokens, index, "ordered_list_close")
|
|
95
|
-
start_number = int(token.attrGet("start") or "1")
|
|
96
|
-
blocks.append(
|
|
97
|
-
self._render_list(tokens, index + 1, close_idx, ordered=True, start_number=start_number)
|
|
98
|
-
)
|
|
99
|
-
index = close_idx + 1
|
|
100
|
-
continue
|
|
101
|
-
|
|
102
|
-
if token_type == "blockquote_open":
|
|
103
|
-
close_idx = self._find_matching(tokens, index, "blockquote_close")
|
|
104
|
-
quoted = self._render_block_tokens(tokens, index + 1, close_idx)
|
|
105
|
-
lines = quoted.splitlines()
|
|
106
|
-
blocks.append("\n".join(f"> {line}" if line else ">" for line in lines))
|
|
107
|
-
index = close_idx + 1
|
|
108
|
-
continue
|
|
109
|
-
|
|
110
|
-
if token_type in {"fence", "code_block"}:
|
|
111
|
-
language = token.info or "" if token_type == "fence" else ""
|
|
112
|
-
blocks.append(_escape_code_block(token.content, language))
|
|
113
|
-
index += 1
|
|
114
|
-
continue
|
|
115
|
-
|
|
116
|
-
if token_type == "inline":
|
|
117
|
-
blocks.append(self._render_inline(token.children or []))
|
|
118
|
-
index += 1
|
|
119
|
-
continue
|
|
120
|
-
|
|
121
|
-
if token_type == "hr":
|
|
122
|
-
blocks.append("―" * 3)
|
|
123
|
-
index += 1
|
|
124
|
-
continue
|
|
125
|
-
|
|
126
|
-
# 其他 Block token(如 html_block)直接忽略或转换为原始文本
|
|
127
|
-
if token_type == "html_block":
|
|
128
|
-
blocks.append(_escape_plain_text(token.content))
|
|
129
|
-
index += 1
|
|
130
|
-
continue
|
|
131
|
-
|
|
132
|
-
index += 1
|
|
133
|
-
|
|
134
|
-
return "\n\n".join(part for part in blocks if part)
|
|
135
|
-
|
|
136
|
-
def _render_list(
|
|
137
|
-
self,
|
|
138
|
-
tokens: List[Token],
|
|
139
|
-
start: int,
|
|
140
|
-
end: int,
|
|
141
|
-
*,
|
|
142
|
-
ordered: bool,
|
|
143
|
-
start_number: int = 1,
|
|
144
|
-
) -> str:
|
|
145
|
-
parts: List[str] = []
|
|
146
|
-
index = start
|
|
147
|
-
counter = start_number
|
|
148
|
-
while index < end:
|
|
149
|
-
token = tokens[index]
|
|
150
|
-
if token.type != "list_item_open":
|
|
151
|
-
index += 1
|
|
152
|
-
continue
|
|
153
|
-
|
|
154
|
-
close_idx = self._find_matching(tokens, index, "list_item_close")
|
|
155
|
-
body = self._render_block_tokens(tokens, index + 1, close_idx)
|
|
156
|
-
lines = body.splitlines()
|
|
157
|
-
prefix = f"{counter}. " if ordered else "- "
|
|
158
|
-
if lines:
|
|
159
|
-
rendered = [prefix + lines[0]]
|
|
160
|
-
rendered.extend((" " if ordered else " ") + line for line in lines[1:])
|
|
161
|
-
parts.append("\n".join(rendered))
|
|
162
|
-
else:
|
|
163
|
-
parts.append(prefix.rstrip())
|
|
164
|
-
|
|
165
|
-
if ordered:
|
|
166
|
-
counter += 1
|
|
167
|
-
index = close_idx + 1
|
|
168
|
-
|
|
169
|
-
return "\n".join(parts)
|
|
170
|
-
|
|
171
|
-
# ---- Inline 渲染 ------------------------------------------------------
|
|
172
|
-
def _render_inline(self, tokens: Iterable[Token]) -> str:
|
|
173
|
-
rendered, _ = self._render_inline_segment(list(tokens), 0, None)
|
|
174
|
-
return rendered
|
|
175
|
-
|
|
176
|
-
def _render_inline_segment(
|
|
177
|
-
self,
|
|
178
|
-
tokens: List[Token],
|
|
179
|
-
start: int,
|
|
180
|
-
closing_type: Optional[str],
|
|
181
|
-
) -> Tuple[str, int]:
|
|
182
|
-
parts: List[str] = []
|
|
183
|
-
index = start
|
|
184
|
-
|
|
185
|
-
while index < len(tokens):
|
|
186
|
-
token = tokens[index]
|
|
187
|
-
token_type = token.type
|
|
188
|
-
|
|
189
|
-
if closing_type and token_type == closing_type:
|
|
190
|
-
return "".join(parts), index + 1
|
|
191
|
-
|
|
192
|
-
if token_type == "text":
|
|
193
|
-
parts.append(_escape_plain_text(token.content))
|
|
194
|
-
index += 1
|
|
195
|
-
continue
|
|
196
|
-
|
|
197
|
-
if token_type in {"softbreak", "hardbreak"}:
|
|
198
|
-
parts.append("\n")
|
|
199
|
-
index += 1
|
|
200
|
-
continue
|
|
201
|
-
|
|
202
|
-
if token_type == "code_inline":
|
|
203
|
-
parts.append(_escape_inline_code(token.content))
|
|
204
|
-
index += 1
|
|
205
|
-
continue
|
|
206
|
-
|
|
207
|
-
if token_type == "strong_open":
|
|
208
|
-
inner, next_index = self._render_inline_segment(tokens, index + 1, "strong_close")
|
|
209
|
-
parts.append(f"*{inner}*")
|
|
210
|
-
index = next_index
|
|
211
|
-
continue
|
|
212
|
-
|
|
213
|
-
if token_type == "em_open":
|
|
214
|
-
inner, next_index = self._render_inline_segment(tokens, index + 1, "em_close")
|
|
215
|
-
parts.append(f"_{inner}_")
|
|
216
|
-
index = next_index
|
|
217
|
-
continue
|
|
218
|
-
|
|
219
|
-
if token_type == "link_open":
|
|
220
|
-
href = token.attrGet("href") or ""
|
|
221
|
-
inner, next_index = self._render_inline_segment(tokens, index + 1, "link_close")
|
|
222
|
-
parts.append(f"[{inner}]({_escape_url(href)})")
|
|
223
|
-
index = next_index
|
|
224
|
-
continue
|
|
225
|
-
|
|
226
|
-
if token_type == "image":
|
|
227
|
-
alt_text = self._render_inline(token.children or [])
|
|
228
|
-
parts.append(f"[{alt_text}]")
|
|
229
|
-
index += 1
|
|
230
|
-
continue
|
|
231
|
-
|
|
232
|
-
if token_type == "html_inline":
|
|
233
|
-
parts.append(_escape_plain_text(token.content))
|
|
234
|
-
index += 1
|
|
235
|
-
continue
|
|
236
|
-
|
|
237
|
-
if token_type == "strikethrough_open":
|
|
238
|
-
inner, next_index = self._render_inline_segment(tokens, index + 1, "strikethrough_close")
|
|
239
|
-
parts.append(f"~{inner}~")
|
|
240
|
-
index = next_index
|
|
241
|
-
continue
|
|
242
|
-
|
|
243
|
-
index += 1
|
|
244
|
-
|
|
245
|
-
return "".join(parts), len(tokens)
|
|
246
|
-
|
|
247
|
-
# ---- 工具方法 ---------------------------------------------------------
|
|
248
|
-
@staticmethod
|
|
249
|
-
def _find_matching(tokens: List[Token], start_idx: int, closing_type: str) -> int:
|
|
250
|
-
"""使用 nesting 深度寻找匹配的关闭 token。"""
|
|
251
|
-
open_type = tokens[start_idx].type
|
|
252
|
-
depth = 0
|
|
253
|
-
for idx in range(start_idx, len(tokens)):
|
|
254
|
-
token = tokens[idx]
|
|
255
|
-
if token.type == open_type and token.nesting == 1:
|
|
256
|
-
depth += 1
|
|
257
|
-
elif token.type == closing_type and token.nesting == -1:
|
|
258
|
-
depth -= 1
|
|
259
|
-
if depth == 0:
|
|
260
|
-
return idx
|
|
261
|
-
raise ValueError(f"未找到匹配的 {closing_type}")
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
_RENDERER = _TelegramMarkdownRenderer()
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
def render_markdown_to_telegram(markdown_text: str) -> str:
|
|
268
|
-
"""将 Markdown 文本转换为 Telegram MarkdownV2。"""
|
|
269
|
-
return _RENDERER.render(markdown_text)
|
|
File without changes
|
|
File without changes
|