vibego 0.2.18__py3-none-any.whl → 0.2.20__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.
bot.py CHANGED
@@ -391,19 +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
- """转义 MarkdownV2 特殊字符。
394
+ """转义 MarkdownV2 特殊字符,保护代码块内容。
395
395
 
396
396
  注意:
397
+ - 使用分段处理,保护代码块(```...``` 和 `...`)
397
398
  - Text().as_markdown() 会转义所有 MarkdownV2 特殊字符
398
399
  - 只移除纯英文单词之间的连字符转义(如 "pre-release")
399
400
  - 保留数字、时间戳等其他情况的连字符转义(如 "2025-10-23")
401
+ - 代码块内容不被转义,保持原样
400
402
  """
401
- escaped = Text(text).as_markdown()
402
- # 只移除纯英文字母之间的连字符转义(避免影响数字、时间戳等)
403
- escaped = re.sub(r"(?<=[a-zA-Z])\\-(?=[a-zA-Z])", "-", escaped)
404
- # 移除斜杠的转义(Telegram 不需要转义斜杠)
405
- escaped = escaped.replace("\\/", "/")
406
- return escaped
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)
407
433
 
408
434
 
409
435
  LEGACY_DOUBLE_BOLD = re.compile(r"\*\*(.+?)\*\*", re.DOTALL)
@@ -440,6 +466,131 @@ def _normalize_legacy_markdown(text: str) -> str:
440
466
  return "".join(pieces)
441
467
 
442
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
+
443
594
  def _prepare_model_payload(text: str) -> str:
444
595
  if _IS_MARKDOWN_V2:
445
596
  return _escape_markdown_v2(text)
@@ -1994,11 +2145,12 @@ def _build_status_filter_row(current_status: Optional[str], limit: int) -> list[
1994
2145
  def _format_task_list_entry(task: TaskRecord) -> str:
1995
2146
  indent = " " * max(task.depth, 0)
1996
2147
  title_raw = (task.title or "").strip()
1997
- # 修复:避免双重转义
2148
+ # 修复:智能清理预转义文本
1998
2149
  if not title_raw:
1999
2150
  title = "-"
2000
2151
  elif _IS_MARKDOWN_V2:
2001
- title = title_raw
2152
+ # 智能清理预转义文本(保护代码块)
2153
+ title = _unescape_if_already_escaped(title_raw)
2002
2154
  else:
2003
2155
  title = _escape_markdown_text(title_raw)
2004
2156
  type_icon = TASK_TYPE_EMOJIS.get(task.task_type)
@@ -2048,11 +2200,13 @@ def _format_task_detail(
2048
2200
  *,
2049
2201
  notes: Sequence[TaskNoteRecord],
2050
2202
  ) -> str:
2051
- # 修复:仅在非 MarkdownV2 模式下手动转义,避免双重转义
2052
- # MarkdownV2 模式下由 _prepare_model_payload() 统一处理转义
2203
+ # 修复:智能处理预转义文本
2204
+ # - MarkdownV2 模式:先清理可能的预转义,再由 _prepare_model_payload() 统一处理
2205
+ # - 其他模式:手动转义
2053
2206
  title_raw = (task.title or "").strip()
2054
2207
  if _IS_MARKDOWN_V2:
2055
- title_text = title_raw if title_raw else "-"
2208
+ # 智能清理预转义文本(保护代码块)
2209
+ title_text = _unescape_if_already_escaped(title_raw) if title_raw else "-"
2056
2210
  else:
2057
2211
  title_text = _escape_markdown_text(title_raw) if title_raw else "-"
2058
2212
 
@@ -2065,10 +2219,11 @@ def _format_task_detail(
2065
2219
  f"📂 类型:{_format_task_type(task.task_type)}",
2066
2220
  ]
2067
2221
 
2068
- # 修复:描述字段也避免双重转义
2222
+ # 修复:描述字段智能清理预转义
2069
2223
  description_raw = task.description or "暂无"
2070
2224
  if _IS_MARKDOWN_V2:
2071
- description_text = description_raw
2225
+ # 智能清理预转义文本(保护代码块)
2226
+ description_text = _unescape_if_already_escaped(description_raw)
2072
2227
  else:
2073
2228
  description_text = _escape_markdown_text(description_raw)
2074
2229
 
@@ -2076,10 +2231,11 @@ def _format_task_detail(
2076
2231
  lines.append(f"📅 创建时间:{_format_local_time(task.created_at)}")
2077
2232
  lines.append(f"🔁 更新时间:{_format_local_time(task.updated_at)}")
2078
2233
 
2079
- # 修复:父任务ID字段也避免双重转义
2234
+ # 修复:父任务ID字段智能清理预转义
2080
2235
  if task.parent_id:
2081
2236
  if _IS_MARKDOWN_V2:
2082
- parent_text = task.parent_id
2237
+ # 智能清理预转义文本(保护代码块)
2238
+ parent_text = _unescape_if_already_escaped(task.parent_id)
2083
2239
  else:
2084
2240
  parent_text = _escape_markdown_text(task.parent_id)
2085
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(),
@@ -2160,6 +2161,7 @@ async def on_project_action(callback: CallbackQuery, state: FSMContext) -> None:
2160
2161
  manager=manager,
2161
2162
  )
2162
2163
  log.info("按钮操作成功: user=%s 重启 master", user_id)
2164
+ return # 重启后不刷新项目列表,避免产生额外噪音
2163
2165
  elif action == "run":
2164
2166
  chosen = await manager.run_worker(cfg)
2165
2167
  log.info(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vibego
3
- Version: 0.2.18
3
+ Version: 0.2.20
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=mX0fgXEbm7M5baUvX_Q--NWpwYWzF8Yge6wrp4MoS6U,264070
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=KS6HjQDSq_45HgOEQ2Iwc1UH-NU9Q9qeYr32S1RNiBA,106633
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=M8Oc6o3XOUKq9SL__MXA-SqxrkAPAXJ3RZSCBM_TZlw,311
428
+ vibego_cli/__init__.py,sha256=GXdKUzmHLWX0fdzhz_4ReIVtJpbgH8b1ZtpJluyc9fo,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.18.dist-info/METADATA,sha256=EV6IIf-JRkYtbjZVBMN1e8RCHfX-0vGNmJICXQOcbEY,10475
435
- vibego-0.2.18.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
436
- vibego-0.2.18.dist-info/entry_points.txt,sha256=Lsy_zm-dlyxt8-9DL9blBReIwU2k22c8-kifr46ND1M,48
437
- vibego-0.2.18.dist-info/top_level.txt,sha256=R56CT3nW5H5v3ce0l3QDN4-C4qxTrNWzRTwrxnkDX4U,69
438
- vibego-0.2.18.dist-info/RECORD,,
434
+ vibego-0.2.20.dist-info/METADATA,sha256=gMF6J4m9ERycJ8Z73pYBXj9fWFtt83pK9fVeujLDaTY,10475
435
+ vibego-0.2.20.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
436
+ vibego-0.2.20.dist-info/entry_points.txt,sha256=Lsy_zm-dlyxt8-9DL9blBReIwU2k22c8-kifr46ND1M,48
437
+ vibego-0.2.20.dist-info/top_level.txt,sha256=R56CT3nW5H5v3ce0l3QDN4-C4qxTrNWzRTwrxnkDX4U,69
438
+ vibego-0.2.20.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.18"
10
+ __version__ = "0.2.20"
11
11
 
12
12
  from .main import main # noqa: E402