vibego 0.2.39__py3-none-any.whl → 0.2.42__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,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 "MarkdownV2").strip()
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 render_markdown_to_telegram(cleaned)
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
- """返回首选与备用的 MarkdownV2 内容,首选尽量减少转义。"""
603
-
604
- strict_payload = _prepare_model_payload(text)
605
- if not _IS_MARKDOWN_V2:
606
- return strict_payload, None
589
+ """返回首选与备用内容,默认为单一格式。"""
607
590
 
608
- optimized_payload = _optimize_markdown_v2_payload(strict_payload)
609
- if optimized_payload != strict_payload:
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(fallback_payload)
730
- return fallback_payload
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vibego
3
- Version: 0.2.39
3
+ Version: 0.2.42
4
4
  Summary: vibego CLI:用于初始化与管理 Telegram Master Bot 的工具
5
5
  Author: Hypha
6
6
  License-Expression: LicenseRef-Proprietary
@@ -1,4 +1,4 @@
1
- bot.py,sha256=1SThFpCzn0Wfj0ovGHJRowK_uZL_0alQGu2zQU4CN-g,273819
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
- telegram_markdown/__init__.py,sha256=bG3H9fWn5GfTqC6xvd49xbVdYWfSFeaX2nefweOYcWY,9757
430
- vibego_cli/__init__.py,sha256=JrNDd69m19jqQtCE-skg54kfgY00_IW_NusHO-VuUZg,311
429
+ vibego_cli/__init__.py,sha256=dM5R5cK5kft5hzz0-VxwpKzzUMt5ZpQqfPLQaDyAILg,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.39.dist-info/METADATA,sha256=vkwbG6_GDiGr3NTCK9PD8nxAT45wBTJiknjcveqK-AY,10519
437
- vibego-0.2.39.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
438
- vibego-0.2.39.dist-info/entry_points.txt,sha256=Lsy_zm-dlyxt8-9DL9blBReIwU2k22c8-kifr46ND1M,48
439
- vibego-0.2.39.dist-info/top_level.txt,sha256=rWDj9KERtbJL6Lar9Xa0O6dthaFSY_jc1WNpQgUrXCM,87
440
- vibego-0.2.39.dist-info/RECORD,,
435
+ vibego-0.2.42.dist-info/METADATA,sha256=Nw9HJPv7fH0iQmxjRx7wQ_mDoODZ_1hcwmVwJC5TvTI,10519
436
+ vibego-0.2.42.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
437
+ vibego-0.2.42.dist-info/entry_points.txt,sha256=Lsy_zm-dlyxt8-9DL9blBReIwU2k22c8-kifr46ND1M,48
438
+ vibego-0.2.42.dist-info/top_level.txt,sha256=R56CT3nW5H5v3ce0l3QDN4-C4qxTrNWzRTwrxnkDX4U,69
439
+ vibego-0.2.42.dist-info/RECORD,,
@@ -4,5 +4,4 @@ master
4
4
  project_repository
5
5
  scripts
6
6
  tasks
7
- telegram_markdown
8
7
  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.39"
10
+ __version__ = "0.2.42"
11
11
 
12
12
  from .main import main # noqa: E402
@@ -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)