vibego 0.2.32__py3-none-any.whl → 0.2.34__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 CHANGED
@@ -69,6 +69,7 @@ from tasks.fsm import (
69
69
  TaskNoteStates,
70
70
  TaskPushStates,
71
71
  )
72
+ from telegram_markdown import render_markdown_to_telegram
72
73
 
73
74
  # --- 简单 .env 加载 ---
74
75
  def load_env(p: str = ".env"):
@@ -470,32 +471,6 @@ def _normalize_legacy_markdown(text: str) -> str:
470
471
  return "".join(pieces)
471
472
 
472
473
 
473
- def _normalize_to_telegram_markdown(text: str) -> str:
474
- """将标准 Markdown 格式转换为 Telegram MarkdownV2 支持的格式
475
-
476
- 转换规则:
477
- - **text** → *text* (加粗)
478
- - __text__ → _text_ (斜体)
479
- - #### 标题 → *标题* (标题转为加粗)
480
- """
481
- # 先转换加粗和下划线语法
482
- text = _normalize_legacy_markdown(text)
483
-
484
- # 转换标题:移除 # 符号,将标题文本转为加粗
485
- def _replace_heading(match: re.Match[str]) -> str:
486
- # match.group(1) 是 # 符号
487
- # match.group(2) 是标题文本
488
- heading_text = match.group(2)
489
- # 标题转为加粗文本(如果文本已经是加粗的,避免重复)
490
- if heading_text.startswith("*") and heading_text.endswith("*"):
491
- return heading_text # 已经是加粗,保持不变
492
- return f"*{heading_text}*"
493
-
494
- text = MARKDOWN_HEADING.sub(_replace_heading, text)
495
-
496
- return text
497
-
498
-
499
474
  # MarkdownV2 转义字符模式(用于检测已转义文本)
500
475
  _ESCAPED_MARKDOWN_PATTERN = re.compile(
501
476
  r"\\[_*\[\]()~`>#+=|{}.!:-]" # 添加了冒号
@@ -623,12 +598,8 @@ def _unescape_if_already_escaped(text: str) -> str:
623
598
 
624
599
  def _prepare_model_payload(text: str) -> str:
625
600
  if _IS_MARKDOWN_V2:
626
- # 先反转义已转义的内容,避免双重转义
627
601
  cleaned = _unescape_if_already_escaped(text)
628
- # 转换标准 Markdown 格式为 Telegram 支持的格式
629
- normalized = _normalize_to_telegram_markdown(cleaned)
630
- # 再进行 MarkdownV2 转义
631
- return _escape_markdown_v2(normalized)
602
+ return render_markdown_to_telegram(cleaned)
632
603
  if _IS_MARKDOWN:
633
604
  return _normalize_legacy_markdown(text)
634
605
  return text
scripts/requirements.txt CHANGED
@@ -1,3 +1,4 @@
1
1
  aiogram>=3.0.0,<4.0.0
2
2
  aiohttp-socks>=0.10.0
3
3
  aiosqlite>=0.19.0
4
+ markdown-it-py>=3.0.0,<4.0.0
@@ -0,0 +1,269 @@
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vibego
3
- Version: 0.2.32
3
+ Version: 0.2.34
4
4
  Summary: vibego CLI:用于初始化与管理 Telegram Master Bot 的工具
5
5
  Author: Hypha
6
6
  License-Expression: LicenseRef-Proprietary
@@ -15,6 +15,7 @@ Description-Content-Type: text/markdown
15
15
  Requires-Dist: aiogram<4.0.0,>=3.0.0
16
16
  Requires-Dist: aiohttp-socks>=0.10.0
17
17
  Requires-Dist: aiosqlite>=0.19.0
18
+ Requires-Dist: markdown-it-py<4.0.0,>=3.0.0
18
19
 
19
20
  # vibe-bot(Telegram → Mac CLI → Telegram 回推)
20
21
 
@@ -1,4 +1,4 @@
1
- bot.py,sha256=uJcmMJmj5SlpwUXSgtnIEeNiUGkBg-lG5g6kM9BobP4,272046
1
+ bot.py,sha256=fOlusQvWmjL8z6w-QY9IQ3RxbsEgCL5weaYSiDl6Tdw,270925
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
@@ -7,7 +7,7 @@ scripts/bump_version.sh,sha256=a4uB8V8Y5LPsoqTCdzQKsEE8HhwpBmqRaQInG52LDig,4089
7
7
  scripts/log_writer.py,sha256=8euoMlRo7cbtHApbcEoJnwzLABxti-ovJWFLRN1oDQw,3843
8
8
  scripts/master_healthcheck.py,sha256=-X0VVsZ0AXaOb7izxTO_oyu23g_1jsirNdGIcP8nrSI,8321
9
9
  scripts/publish.sh,sha256=ehLfMedcXuGKJ87jpZy3kuiFszG9Cpavp3zXPfR4h-g,3511
10
- scripts/requirements.txt,sha256=QSt30DSSSHtfucTFPpc7twk9kLS5rVLNTcvDiagxrZg,62
10
+ scripts/requirements.txt,sha256=ukJbFLJyzqnQYMz6j07O-IOrG87IwXg0oikmn1nfJ9M,91
11
11
  scripts/run_bot.sh,sha256=rN4K1nz041XBaUJmnBBKHS2cHmQf11vPNX8wf1hbVR4,4596
12
12
  scripts/start.sh,sha256=w1Q35NB-9FRZAez5I5veqgYIJ8XnVukGt8TTE_ad248,13608
13
13
  scripts/start_tmux_codex.sh,sha256=xyLv29p924q-ysxvZYAP3T6VrqLPBPMBWo9QP7cuL50,4438
@@ -426,14 +426,15 @@ 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
- vibego_cli/__init__.py,sha256=yIxuD2p_Dd2qwaOZwbuf-21aP6wKVdMFcPi5wJfYlos,311
429
+ telegram_markdown/__init__.py,sha256=bG3H9fWn5GfTqC6xvd49xbVdYWfSFeaX2nefweOYcWY,9757
430
+ vibego_cli/__init__.py,sha256=YiGf4wB8hakiJXIr-AwQ78eW9g7s9QwQN7KTX7AWhAA,311
430
431
  vibego_cli/__main__.py,sha256=qqTrYmRRLe4361fMzbI3-CqpZ7AhTofIHmfp4ykrrBY,158
431
432
  vibego_cli/config.py,sha256=VxkPJMq01tA3h3cOkH-z_tiP7pMgfSGGicRvUnCWkhI,3054
432
433
  vibego_cli/deps.py,sha256=1nRXI7Dd-S1hYE8DligzK5fIluQWETRUj4_OKL0DikQ,1419
433
434
  vibego_cli/main.py,sha256=X__NXwZnIDIFbdKSTbNyZgZHKcPlN0DQz9sqTI1aQ9E,12158
434
435
  vibego_cli/data/worker_requirements.txt,sha256=QSt30DSSSHtfucTFPpc7twk9kLS5rVLNTcvDiagxrZg,62
435
- vibego-0.2.32.dist-info/METADATA,sha256=Dm8z912xz2fEhlZLca0kzY-tsZEzWqinWmsEB2ycaGQ,10475
436
- vibego-0.2.32.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
437
- vibego-0.2.32.dist-info/entry_points.txt,sha256=Lsy_zm-dlyxt8-9DL9blBReIwU2k22c8-kifr46ND1M,48
438
- vibego-0.2.32.dist-info/top_level.txt,sha256=R56CT3nW5H5v3ce0l3QDN4-C4qxTrNWzRTwrxnkDX4U,69
439
- vibego-0.2.32.dist-info/RECORD,,
436
+ vibego-0.2.34.dist-info/METADATA,sha256=eFQD8C0DvvCHPbvuvAI5h56kh2iqp8Ty5LN-ILJG2aY,10519
437
+ vibego-0.2.34.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
438
+ vibego-0.2.34.dist-info/entry_points.txt,sha256=Lsy_zm-dlyxt8-9DL9blBReIwU2k22c8-kifr46ND1M,48
439
+ vibego-0.2.34.dist-info/top_level.txt,sha256=rWDj9KERtbJL6Lar9Xa0O6dthaFSY_jc1WNpQgUrXCM,87
440
+ vibego-0.2.34.dist-info/RECORD,,
@@ -4,4 +4,5 @@ master
4
4
  project_repository
5
5
  scripts
6
6
  tasks
7
+ telegram_markdown
7
8
  vibego_cli
vibego_cli/__init__.py CHANGED
@@ -7,6 +7,6 @@ from __future__ import annotations
7
7
 
8
8
  __all__ = ["main", "__version__"]
9
9
 
10
- __version__ = "0.2.32"
10
+ __version__ = "0.2.34"
11
11
 
12
12
  from .main import main # noqa: E402