vibego 0.2.16__py3-none-any.whl → 0.2.19__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
@@ -391,11 +391,45 @@ async def _send_with_retry(coro_factory, *, attempts: int = SEND_RETRY_ATTEMPTS)
391
391
 
392
392
 
393
393
  def _escape_markdown_v2(text: str) -> str:
394
- escaped = Text(text).as_markdown()
395
- escaped = re.sub(r"(?<=\w)\\-(?=\w)", "-", escaped)
396
- escaped = escaped.replace("\\-", "-")
397
- escaped = escaped.replace("\\/", "/")
398
- return escaped
394
+ """转义 MarkdownV2 特殊字符,保护代码块内容。
395
+
396
+ 注意:
397
+ - 使用分段处理,保护代码块(```...``` `...`)
398
+ - Text().as_markdown() 会转义所有 MarkdownV2 特殊字符
399
+ - 只移除纯英文单词之间的连字符转义(如 "pre-release")
400
+ - 保留数字、时间戳等其他情况的连字符转义(如 "2025-10-23")
401
+ - 代码块内容不被转义,保持原样
402
+ """
403
+
404
+ def _escape_segment(segment: str) -> str:
405
+ """转义单个文本段落(非代码块)"""
406
+ escaped = Text(segment).as_markdown()
407
+ # 只移除纯英文字母之间的连字符转义
408
+ escaped = re.sub(r"(?<=[a-zA-Z])\\-(?=[a-zA-Z])", "-", escaped)
409
+ # 移除斜杠的转义
410
+ escaped = escaped.replace("\\/", "/")
411
+ return escaped
412
+
413
+ # 分段处理:代码块保持原样,普通文本转义
414
+ pieces: list[str] = []
415
+ last_index = 0
416
+
417
+ for match in CODE_SEGMENT_RE.finditer(text):
418
+ # 处理代码块之前的普通文本
419
+ normal_part = text[last_index:match.start()]
420
+ if normal_part:
421
+ pieces.append(_escape_segment(normal_part))
422
+
423
+ # 代码块保持原样,不转义
424
+ pieces.append(match.group(0))
425
+ last_index = match.end()
426
+
427
+ # 处理最后一段普通文本
428
+ if last_index < len(text):
429
+ remaining = text[last_index:]
430
+ pieces.append(_escape_segment(remaining))
431
+
432
+ return "".join(pieces) if pieces else _escape_segment(text)
399
433
 
400
434
 
401
435
  LEGACY_DOUBLE_BOLD = re.compile(r"\*\*(.+?)\*\*", re.DOTALL)
@@ -432,6 +466,131 @@ def _normalize_legacy_markdown(text: str) -> str:
432
466
  return "".join(pieces)
433
467
 
434
468
 
469
+ # MarkdownV2 转义字符模式(用于检测已转义文本)
470
+ _ESCAPED_MARKDOWN_PATTERN = re.compile(
471
+ r"\\[_*\[\]()~`>#+=|{}.!:-]" # 添加了冒号
472
+ )
473
+
474
+ # 已转义的代码块模式(转义的反引号)
475
+ _ESCAPED_CODE_BLOCK_PATTERN = re.compile(
476
+ r"(\\\`\\\`\\\`.*?\\\`\\\`\\\`|\\\`[^\\\`]*?\\\`)",
477
+ re.DOTALL
478
+ )
479
+
480
+
481
+ def _is_already_escaped(text: str) -> bool:
482
+ """检测文本是否已经包含 MarkdownV2 转义字符。
483
+
484
+ 通过统计转义字符的出现频率来判断:
485
+ - 如果转义字符数量 >= 文本长度的 3%,认为已被转义(降低阈值)
486
+ - 或者如果有 2 个以上的连续转义模式(如 \*\*),也认为已被转义
487
+ - 或者包含已转义的代码块标记
488
+ """
489
+ if not text:
490
+ return False
491
+
492
+ # 检查是否有已转义的代码块标记
493
+ if _ESCAPED_CODE_BLOCK_PATTERN.search(text):
494
+ return True
495
+
496
+ matches = _ESCAPED_MARKDOWN_PATTERN.findall(text)
497
+ if not matches:
498
+ return False
499
+
500
+ # 对于短文本,放宽检测条件
501
+ if len(text) < 20:
502
+ # 短文本只要有 2 个以上转义字符就认为已被转义
503
+ if len(matches) >= 2:
504
+ return True
505
+ else:
506
+ # 检查转义字符密度(降低到 3%)
507
+ escape_count = len(matches)
508
+ text_length = len(text)
509
+ density = escape_count / text_length
510
+
511
+ if density >= 0.03: # 3% 以上认为已被转义
512
+ return True
513
+
514
+ # 检查是否有连续转义模式(如 \#\#\# 或 \*\*)
515
+ consecutive_pattern = re.compile(r"(?:\\[_*\[\]()~`>#+=|{}.!:-]){2,}")
516
+ if consecutive_pattern.search(text):
517
+ return True
518
+
519
+ return False
520
+
521
+
522
+ def _unescape_markdown_v2(text: str) -> str:
523
+ """反转义 MarkdownV2 特殊字符。
524
+
525
+ 将 \*, \_, \#, \[, \], \: 等转义字符还原为原始字符。
526
+ """
527
+ # 移除所有 MarkdownV2 转义的反斜杠
528
+ # 匹配模式:反斜杠 + 特殊字符(添加了冒号)
529
+ return re.sub(r"\\([_*\[\]()~`>#+=|{}.!:-])", r"\1", text)
530
+
531
+
532
+ def _unescape_if_already_escaped(text: str) -> str:
533
+ """智能检测并清理预转义文本,特别保护代码块。
534
+
535
+ 改进的分段处理策略:
536
+ 1. 先识别已转义的代码块(\`\`\`...\`\`\` 和 \`...\`)
537
+ 2. 对这些代码块先反转义边界反引号,变成正常代码块
538
+ 3. 然后用正常的 CODE_SEGMENT_RE 识别代码块
539
+ 4. 只对非代码块的普通文本进行反转义
540
+ 5. 代码块内容保持转义状态(因为是代码本身)
541
+ 6. 重新组合所有段落
542
+
543
+ Args:
544
+ text: 待处理的文本
545
+
546
+ Returns:
547
+ 处理后的文本(如未检测到预转义,返回原文本)
548
+ """
549
+ if not text:
550
+ return text
551
+
552
+ # 快速检测:如果没有任何转义字符,直接返回
553
+ if not _is_already_escaped(text):
554
+ return text
555
+
556
+ # 第一步:处理已转义的代码块,将边界反引号反转义
557
+ # 这样后续可以用正常的 CODE_SEGMENT_RE 识别它们
558
+ processed = text
559
+
560
+ # 先标记所有已转义的代码块,用占位符替换
561
+ code_blocks: list[str] = []
562
+
563
+ def _preserve_code_block(match: re.Match[str]) -> str:
564
+ """保存代码块并返回占位符"""
565
+ block = match.group(0)
566
+ # 代码块边界的反引号需要反转义,但内容保持不变
567
+ # 例如:\`\`\`bash\npython -m vibego\_cli\`\`\`
568
+ # -> ```bash\npython -m vibego\_cli```
569
+ if block.startswith(r"\`\`\`"):
570
+ # 多行代码块
571
+ unescaped_block = block.replace(r"\`", "`", 6) # 只替换前后各3个反引号
572
+ else:
573
+ # 单行代码块
574
+ unescaped_block = block.replace(r"\`", "`", 2) # 只替换前后各1个反引号
575
+
576
+ placeholder = f"__CODE_BLOCK_{len(code_blocks)}__"
577
+ code_blocks.append(unescaped_block)
578
+ return placeholder
579
+
580
+ processed = _ESCAPED_CODE_BLOCK_PATTERN.sub(_preserve_code_block, processed)
581
+
582
+ # 第二步:对非代码块部分进行反转义
583
+ if _is_already_escaped(processed):
584
+ processed = _unescape_markdown_v2(processed)
585
+
586
+ # 第三步:恢复代码块
587
+ for i, block in enumerate(code_blocks):
588
+ placeholder = f"__CODE_BLOCK_{i}__"
589
+ processed = processed.replace(placeholder, block)
590
+
591
+ return processed
592
+
593
+
435
594
  def _prepare_model_payload(text: str) -> str:
436
595
  if _IS_MARKDOWN_V2:
437
596
  return _escape_markdown_v2(text)
@@ -1986,11 +2145,12 @@ def _build_status_filter_row(current_status: Optional[str], limit: int) -> list[
1986
2145
  def _format_task_list_entry(task: TaskRecord) -> str:
1987
2146
  indent = " " * max(task.depth, 0)
1988
2147
  title_raw = (task.title or "").strip()
1989
- # 修复:避免双重转义
2148
+ # 修复:智能清理预转义文本
1990
2149
  if not title_raw:
1991
2150
  title = "-"
1992
2151
  elif _IS_MARKDOWN_V2:
1993
- title = title_raw
2152
+ # 智能清理预转义文本(保护代码块)
2153
+ title = _unescape_if_already_escaped(title_raw)
1994
2154
  else:
1995
2155
  title = _escape_markdown_text(title_raw)
1996
2156
  type_icon = TASK_TYPE_EMOJIS.get(task.task_type)
@@ -2040,11 +2200,13 @@ def _format_task_detail(
2040
2200
  *,
2041
2201
  notes: Sequence[TaskNoteRecord],
2042
2202
  ) -> str:
2043
- # 修复:仅在非 MarkdownV2 模式下手动转义,避免双重转义
2044
- # MarkdownV2 模式下由 _prepare_model_payload() 统一处理转义
2203
+ # 修复:智能处理预转义文本
2204
+ # - MarkdownV2 模式:先清理可能的预转义,再由 _prepare_model_payload() 统一处理
2205
+ # - 其他模式:手动转义
2045
2206
  title_raw = (task.title or "").strip()
2046
2207
  if _IS_MARKDOWN_V2:
2047
- title_text = title_raw if title_raw else "-"
2208
+ # 智能清理预转义文本(保护代码块)
2209
+ title_text = _unescape_if_already_escaped(title_raw) if title_raw else "-"
2048
2210
  else:
2049
2211
  title_text = _escape_markdown_text(title_raw) if title_raw else "-"
2050
2212
 
@@ -2057,10 +2219,11 @@ def _format_task_detail(
2057
2219
  f"📂 类型:{_format_task_type(task.task_type)}",
2058
2220
  ]
2059
2221
 
2060
- # 修复:描述字段也避免双重转义
2222
+ # 修复:描述字段智能清理预转义
2061
2223
  description_raw = task.description or "暂无"
2062
2224
  if _IS_MARKDOWN_V2:
2063
- description_text = description_raw
2225
+ # 智能清理预转义文本(保护代码块)
2226
+ description_text = _unescape_if_already_escaped(description_raw)
2064
2227
  else:
2065
2228
  description_text = _escape_markdown_text(description_raw)
2066
2229
 
@@ -2068,10 +2231,11 @@ def _format_task_detail(
2068
2231
  lines.append(f"📅 创建时间:{_format_local_time(task.created_at)}")
2069
2232
  lines.append(f"🔁 更新时间:{_format_local_time(task.updated_at)}")
2070
2233
 
2071
- # 修复:父任务ID字段也避免双重转义
2234
+ # 修复:父任务ID字段智能清理预转义
2072
2235
  if task.parent_id:
2073
2236
  if _IS_MARKDOWN_V2:
2074
- parent_text = task.parent_id
2237
+ # 智能清理预转义文本(保护代码块)
2238
+ parent_text = _unescape_if_already_escaped(task.parent_id)
2075
2239
  else:
2076
2240
  parent_text = _escape_markdown_text(task.parent_id)
2077
2241
  lines.append(f"👪 父任务:{parent_text}")
master.py CHANGED
@@ -54,6 +54,7 @@ from aiogram.fsm.storage.memory import MemoryStorage
54
54
  from logging_setup import create_logger
55
55
  from project_repository import ProjectRepository, ProjectRecord
56
56
  from tasks.fsm import ProjectDeleteStates
57
+ from vibego_cli import __version__
57
58
 
58
59
  ROOT_DIR = Path(__file__).resolve().parent
59
60
  CONFIG_PATH = Path(os.environ.get("MASTER_PROJECTS_PATH", ROOT_DIR / "config/projects.json"))
@@ -1792,7 +1793,7 @@ async def cmd_start(message: Message) -> None:
1792
1793
  return
1793
1794
  manager.refresh_state()
1794
1795
  await message.answer(
1795
- "Master bot 已启动。\n"
1796
+ f"Master bot 已启动(v{__version__})。\n"
1796
1797
  f"已登记项目: {len(manager.configs)} 个。\n"
1797
1798
  "使用 /projects 查看状态,/run 或 /stop 控制 worker。",
1798
1799
  reply_markup=_build_master_main_keyboard(),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vibego
3
- Version: 0.2.16
3
+ Version: 0.2.19
4
4
  Summary: vibego CLI:用于初始化与管理 Telegram Master Bot 的工具
5
5
  Author: Hypha
6
6
  License-Expression: LicenseRef-Proprietary
@@ -1,6 +1,6 @@
1
- bot.py,sha256=rOSJ6BmiG2GMatE0xjS89k9nCCALVrOllZXJJRmAKZ4,263647
1
+ bot.py,sha256=aK68QaITXzOoUStLqejg8L6Bd3LuAqNLOw6R3-7SfYw,269638
2
2
  logging_setup.py,sha256=gvxHi8mUwK3IhXJrsGNTDo-DR6ngkyav1X-tvlBF_IE,4613
3
- master.py,sha256=Qz2NTapUexVvpQz8Y_pVhKd-uXkqp3M6oclzfAzIuGs,106497
3
+ master.py,sha256=qw6ZxItpSr6C5YzjVDOGm4jZpnGYLZndsJ-lmJrzGcM,106553
4
4
  project_repository.py,sha256=UcthtSGOJK0cTE5bQCneo3xkomRG-kyc1N1QVqxeHIs,17577
5
5
  scripts/__init__.py,sha256=LVrXUkvWKoc6Sb47X5G0gbIxu5aJ2ARW-qJ14vwi5vM,65
6
6
  scripts/bump_version.sh,sha256=a4uB8V8Y5LPsoqTCdzQKsEE8HhwpBmqRaQInG52LDig,4089
@@ -425,14 +425,14 @@ tasks/constants.py,sha256=tS1kZxBIUm3JJUMHm25XI-KHNUZl5NhbbuzjzL_rF-c,299
425
425
  tasks/fsm.py,sha256=rKXXLEieQQU4r2z_CZUvn1_70FXiZXBBugF40gpe_tQ,1476
426
426
  tasks/models.py,sha256=N_qqRBo9xMSV0vbn4k6bLBXT8C_dp_oTFUxvdx16ZQM,2459
427
427
  tasks/service.py,sha256=w_S_aWiVqRXzXEpimLDsuCCCX2lB5uDkff9aKThBw9c,41916
428
- vibego_cli/__init__.py,sha256=d5pc65g50YjhuctLTvuKUD5rGeT6lfCB3xOLDphrEFs,311
428
+ vibego_cli/__init__.py,sha256=PbHZ3j0Ku7MxszOczRhZnNfiggYU_AB2P91m7bMQqCw,311
429
429
  vibego_cli/__main__.py,sha256=qqTrYmRRLe4361fMzbI3-CqpZ7AhTofIHmfp4ykrrBY,158
430
430
  vibego_cli/config.py,sha256=33WSORCfUIxrDtgASPEbVqVLBVNHh-RSFLpNy7tfc0s,2992
431
431
  vibego_cli/deps.py,sha256=1nRXI7Dd-S1hYE8DligzK5fIluQWETRUj4_OKL0DikQ,1419
432
432
  vibego_cli/main.py,sha256=e2W5Pb9U9rfmF-jNX9uIA3222lhM0GgcvSdFTDBZd2s,12086
433
433
  vibego_cli/data/worker_requirements.txt,sha256=QSt30DSSSHtfucTFPpc7twk9kLS5rVLNTcvDiagxrZg,62
434
- vibego-0.2.16.dist-info/METADATA,sha256=bb23CIEShOSwY-fF0913Eg-aC3gR388opQgFFm1XsGE,10475
435
- vibego-0.2.16.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
436
- vibego-0.2.16.dist-info/entry_points.txt,sha256=Lsy_zm-dlyxt8-9DL9blBReIwU2k22c8-kifr46ND1M,48
437
- vibego-0.2.16.dist-info/top_level.txt,sha256=R56CT3nW5H5v3ce0l3QDN4-C4qxTrNWzRTwrxnkDX4U,69
438
- vibego-0.2.16.dist-info/RECORD,,
434
+ vibego-0.2.19.dist-info/METADATA,sha256=EcNsx_kVnc0gZiqMdbpIHRDYcIFu09gERPQubTVW-mY,10475
435
+ vibego-0.2.19.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
436
+ vibego-0.2.19.dist-info/entry_points.txt,sha256=Lsy_zm-dlyxt8-9DL9blBReIwU2k22c8-kifr46ND1M,48
437
+ vibego-0.2.19.dist-info/top_level.txt,sha256=R56CT3nW5H5v3ce0l3QDN4-C4qxTrNWzRTwrxnkDX4U,69
438
+ vibego-0.2.19.dist-info/RECORD,,
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.16"
10
+ __version__ = "0.2.19"
11
11
 
12
12
  from .main import main # noqa: E402