vibego 0.2.30__py3-none-any.whl → 0.2.32__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.
master.py CHANGED
@@ -21,6 +21,7 @@ import stat
21
21
  import textwrap
22
22
  import re
23
23
  import threading
24
+ import unicodedata
24
25
  from datetime import datetime, timezone
25
26
  from zoneinfo import ZoneInfo
26
27
  from dataclasses import dataclass, field
@@ -128,6 +129,43 @@ SWITCHABLE_MODELS: Tuple[Tuple[str, str], ...] = (
128
129
  ("claudecode", "⚙️ ClaudeCode"),
129
130
  )
130
131
 
132
+ # Telegram 在不同客户端可能插入零宽字符或额外空白,提前归一化按钮文本。
133
+ ZERO_WIDTH_CHARACTERS: Tuple[str, ...] = ("\u200b", "\u200c", "\u200d", "\ufeff")
134
+
135
+
136
+ def _normalize_button_text(text: str) -> str:
137
+ """归一化项目按钮文本,剔除零宽字符并统一大小写。"""
138
+
139
+ filtered = "".join(ch for ch in text if ch not in ZERO_WIDTH_CHARACTERS)
140
+ compacted = re.sub(r"\s+", " ", filtered).strip()
141
+ return unicodedata.normalize("NFKC", compacted).casefold()
142
+
143
+
144
+ MASTER_MENU_BUTTON_CANONICAL_NORMALIZED = _normalize_button_text(MASTER_MENU_BUTTON_TEXT)
145
+ MASTER_MENU_BUTTON_ALLOWED_NORMALIZED = {
146
+ _normalize_button_text(value) for value in MASTER_MENU_BUTTON_ALLOWED_TEXTS
147
+ }
148
+ MASTER_MENU_BUTTON_KEYWORDS: Tuple[str, ...] = ("项目列表", "project", "projects")
149
+
150
+
151
+ def _is_projects_menu_trigger(text: Optional[str]) -> bool:
152
+ """判断消息文本是否可触发项目列表展示。"""
153
+
154
+ if not text:
155
+ return False
156
+ normalized = _normalize_button_text(text)
157
+ if not normalized:
158
+ return False
159
+ if normalized in MASTER_MENU_BUTTON_ALLOWED_NORMALIZED:
160
+ return True
161
+ return any(keyword in normalized for keyword in MASTER_MENU_BUTTON_KEYWORDS)
162
+
163
+
164
+ def _text_equals_master_button(text: str) -> bool:
165
+ """判断文本是否等同于当前主按钮文案(允许空白差异)。"""
166
+
167
+ return _normalize_button_text(text) == MASTER_MENU_BUTTON_CANONICAL_NORMALIZED
168
+
131
169
 
132
170
  def _build_master_main_keyboard() -> ReplyKeyboardMarkup:
133
171
  """构造 Master Bot 主键盘,提供项目列表与管理入口。"""
@@ -1700,9 +1738,14 @@ def _read_restart_signal() -> Optional[dict]:
1700
1738
 
1701
1739
 
1702
1740
  async def _notify_restart_success(bot: Bot) -> None:
1703
- """在新 master 启动时读取 signal 并通知触发者"""
1741
+ """在新 master 启动时读取 signal 并通知触发者(改进版:支持超时检测和详细诊断)"""
1704
1742
  restart_expected = os.environ.pop("MASTER_RESTART_EXPECTED", None)
1705
1743
  payload = _read_restart_signal()
1744
+
1745
+ # 定义重启健康检查阈值(2 分钟)
1746
+ RESTART_HEALTHY_THRESHOLD = 120 # 秒
1747
+ RESTART_WARNING_THRESHOLD = 60 # 超过 1 分钟发出警告
1748
+
1706
1749
  if not payload:
1707
1750
  if restart_expected:
1708
1751
  targets = _collect_admin_targets()
@@ -1710,6 +1753,18 @@ async def _notify_restart_success(bot: Bot) -> None:
1710
1753
  "启动时未检测到重启信号文件,将向管理员发送兜底提醒", extra={"targets": targets}
1711
1754
  )
1712
1755
  if targets:
1756
+ # 检查启动日志是否有错误信息
1757
+ error_log_dir = ROOT_DIR / "logs"
1758
+ error_log_hint = ""
1759
+ try:
1760
+ error_logs = sorted(error_log_dir.glob("master_error_*.log"), key=lambda p: p.stat().st_mtime, reverse=True)
1761
+ if error_logs:
1762
+ latest_error_log = error_logs[0]
1763
+ if latest_error_log.stat().st_size > 0:
1764
+ error_log_hint = f"\n⚠️ 发现错误日志:{latest_error_log}"
1765
+ except Exception:
1766
+ pass
1767
+
1713
1768
  text_lines = [
1714
1769
  "⚠️ Master 已重新上线,但未找到重启触发者信息。",
1715
1770
  "",
@@ -1717,12 +1772,16 @@ async def _notify_restart_success(bot: Bot) -> None:
1717
1772
  "1. 重启信号文件写入失败",
1718
1773
  "2. 信号文件已超时被清理(TTL=30分钟)",
1719
1774
  "3. 文件系统权限问题",
1775
+ "4. start.sh 启动失败后被清理",
1720
1776
  "",
1721
1777
  "建议检查:",
1722
1778
  f"- 启动日志: {ROOT_DIR}/logs/start.log",
1723
1779
  f"- 运行日志: {ROOT_DIR}/vibe.log",
1724
1780
  f"- 信号文件: {RESTART_SIGNAL_PATH}",
1725
1781
  ]
1782
+ if error_log_hint:
1783
+ text_lines.append(error_log_hint)
1784
+
1726
1785
  text = "\n".join(text_lines)
1727
1786
  for chat in targets:
1728
1787
  try:
@@ -1746,6 +1805,9 @@ async def _notify_restart_success(bot: Bot) -> None:
1746
1805
  user_id = payload.get("user_id")
1747
1806
  timestamp = payload.get("timestamp")
1748
1807
  timestamp_fmt: Optional[str] = None
1808
+ restart_duration: Optional[int] = None
1809
+
1810
+ # 计算重启耗时
1749
1811
  if timestamp:
1750
1812
  try:
1751
1813
  ts = datetime.fromisoformat(timestamp)
@@ -1753,6 +1815,10 @@ async def _notify_restart_success(bot: Bot) -> None:
1753
1815
  ts = ts.replace(tzinfo=LOCAL_TZ)
1754
1816
  ts_local = ts.astimezone(LOCAL_TZ)
1755
1817
  timestamp_fmt = ts_local.strftime("%Y-%m-%d %H:%M:%S %Z")
1818
+
1819
+ # 计算重启耗时(秒)
1820
+ now = datetime.now(LOCAL_TZ)
1821
+ restart_duration = int((now - ts_local).total_seconds())
1756
1822
  except Exception as exc:
1757
1823
  log.warning("解析重启时间失败: %s", exc)
1758
1824
 
@@ -1764,7 +1830,23 @@ async def _notify_restart_success(bot: Bot) -> None:
1764
1830
  if timestamp_fmt:
1765
1831
  details.append(f"请求时间:{timestamp_fmt}")
1766
1832
 
1767
- message_lines = ["master 已重新上线 ✅"]
1833
+ # 添加重启耗时信息和健康状态
1834
+ message_lines = []
1835
+ if restart_duration is not None:
1836
+ if restart_duration <= RESTART_WARNING_THRESHOLD:
1837
+ message_lines.append(f"master 已重新上线 ✅(耗时 {restart_duration}秒)")
1838
+ elif restart_duration <= RESTART_HEALTHY_THRESHOLD:
1839
+ message_lines.append(f"⚠️ master 已重新上线(耗时 {restart_duration}秒,略慢)")
1840
+ details.append("💡 建议:检查依赖安装是否触发了重新下载")
1841
+ else:
1842
+ message_lines.append(f"⚠️ master 已重新上线(耗时 {restart_duration}秒,异常缓慢)")
1843
+ details.append("⚠️ 重启耗时过长,建议检查:")
1844
+ details.append(" - 网络连接是否正常")
1845
+ details.append(" - 依赖安装是否卡住")
1846
+ details.append(f" - 启动日志: {ROOT_DIR}/logs/start.log")
1847
+ else:
1848
+ message_lines.append("master 已重新上线 ✅")
1849
+
1768
1850
  if details:
1769
1851
  message_lines.extend(details)
1770
1852
 
@@ -1776,7 +1858,7 @@ async def _notify_restart_success(bot: Bot) -> None:
1776
1858
  log.error("发送重启成功通知失败: %s", exc, extra={"chat": chat_id})
1777
1859
  else:
1778
1860
  # 重启成功后不再附带项目列表,避免高频重启时产生额外噪音
1779
- log.info("重启成功通知已发送", extra={"chat": chat_id})
1861
+ log.info("重启成功通知已发送", extra={"chat": chat_id, "duration": restart_duration})
1780
1862
  finally:
1781
1863
  _safe_remove(RESTART_SIGNAL_PATH)
1782
1864
 
@@ -2491,7 +2573,7 @@ async def on_project_wizard_skip(callback: CallbackQuery) -> None:
2491
2573
  )
2492
2574
 
2493
2575
 
2494
- @router.message(F.text.in_(MASTER_MENU_BUTTON_ALLOWED_TEXTS))
2576
+ @router.message(F.text.func(_is_projects_menu_trigger))
2495
2577
  async def on_master_projects_button(message: Message) -> None:
2496
2578
  """处理常驻键盘触发的项目概览请求。"""
2497
2579
  _log_update(message)
@@ -2501,7 +2583,7 @@ async def on_master_projects_button(message: Message) -> None:
2501
2583
  return
2502
2584
  requested_text = message.text or ""
2503
2585
  reply_to_message_id: Optional[int] = message.message_id
2504
- if requested_text != MASTER_MENU_BUTTON_TEXT:
2586
+ if not _text_equals_master_button(requested_text):
2505
2587
  log.info(
2506
2588
  "收到旧版项目列表按钮,准备刷新聊天键盘",
2507
2589
  extra={"text": requested_text, "chat_id": message.chat.id},
scripts/start.sh CHANGED
@@ -186,21 +186,105 @@ check_deps_installed() {
186
186
  return 0
187
187
  }
188
188
 
189
- if pgrep -f "python.*master.py" >/dev/null 2>&1; then
190
- log_info "检测到历史 master 实例,正在终止..."
191
- pkill -f "python.*master.py" || true
192
- sleep 1
193
- if pgrep -f "python.*master.py" >/dev/null 2>&1; then
194
- log_info "残留 master 进程仍在,执行强制结束"
195
- pkill -9 -f "python.*master.py" || true
196
- sleep 1
189
+ # 清理旧 master 进程的健壮函数(改进版:支持 PID 文件 + pgrep 双保险)
190
+ cleanup_old_master() {
191
+ local max_wait=10 # 最多等待10秒优雅退出
192
+ local waited=0
193
+ local old_pids=""
194
+ local master_pid_file="${MASTER_CONFIG_ROOT:-$HOME/.config/vibego}/state/master.pid"
195
+
196
+ # 方案1:优先从 PID 文件读取
197
+ if [[ -f "$master_pid_file" ]]; then
198
+ local pid_from_file
199
+ pid_from_file=$(cat "$master_pid_file" 2>/dev/null || true)
200
+ if [[ "$pid_from_file" =~ ^[0-9]+$ ]]; then
201
+ if kill -0 "$pid_from_file" 2>/dev/null; then
202
+ old_pids="$pid_from_file"
203
+ log_info "从 PID 文件检测到旧 master 实例(PID: $old_pids)"
204
+ else
205
+ log_info "PID 文件存在但进程已不在,清理过期 PID 文件"
206
+ rm -f "$master_pid_file"
207
+ fi
208
+ fi
209
+ fi
210
+
211
+ # 方案2:使用 pgrep 查找(支持多种运行方式)
212
+ if [[ -z "$old_pids" ]]; then
213
+ # 匹配模式:支持源码运行和 pipx 安装的方式
214
+ # - python.*master.py(源码运行)
215
+ # - Python.*master.py(macOS 上的 Python.app)
216
+ # - bot.py(pipx 安装的 master 别名)
217
+ local pgrep_pids
218
+ pgrep_pids=$(pgrep -f "master\.py$" 2>/dev/null || true)
219
+ if [[ -n "$pgrep_pids" ]]; then
220
+ old_pids="$pgrep_pids"
221
+ log_info "通过 pgrep 检测到旧 master 实例(PID: $old_pids)"
222
+ fi
197
223
  fi
198
- if pgrep -f "python.*master.py" >/dev/null 2>&1; then
199
- log_error "仍存在 master 进程,请手动检查后再启动"
224
+
225
+ # 如果两种方式都没找到,说明没有旧进程
226
+ if [[ -z "$old_pids" ]]; then
227
+ log_info "未检测到旧 master 实例"
228
+ return 0
229
+ fi
230
+
231
+ # 开始清理旧进程
232
+ log_info "正在优雅终止旧 master 实例(PID: $old_pids)..."
233
+
234
+ # 发送 SIGTERM 信号优雅终止
235
+ for pid in $old_pids; do
236
+ kill -15 "$pid" 2>/dev/null || true
237
+ done
238
+
239
+ # 循环等待进程退出
240
+ while (( waited < max_wait )); do
241
+ sleep 1
242
+ ((waited++))
243
+
244
+ # 检查所有 PID 是否都已退出
245
+ local all_exited=1
246
+ for pid in $old_pids; do
247
+ if kill -0 "$pid" 2>/dev/null; then
248
+ all_exited=0
249
+ break
250
+ fi
251
+ done
252
+
253
+ if (( all_exited )); then
254
+ log_info "✅ 旧 master 已优雅退出(耗时 ${waited}秒)"
255
+ rm -f "$master_pid_file"
256
+ return 0
257
+ fi
258
+ done
259
+
260
+ # 优雅终止超时,执行强制结束
261
+ log_info "优雅终止超时(${max_wait}秒),执行强制结束..."
262
+ for pid in $old_pids; do
263
+ kill -9 "$pid" 2>/dev/null || true
264
+ done
265
+ sleep 2
266
+
267
+ # 最后检查
268
+ local remaining_pids=""
269
+ for pid in $old_pids; do
270
+ if kill -0 "$pid" 2>/dev/null; then
271
+ remaining_pids="$remaining_pids $pid"
272
+ fi
273
+ done
274
+
275
+ if [[ -n "$remaining_pids" ]]; then
276
+ log_error "❌ 无法清理旧 master 进程(残留 PID:$remaining_pids)"
277
+ log_error "请手动执行: kill -9$remaining_pids"
200
278
  exit 1
201
279
  fi
202
- log_info "历史 master 实例已清理"
203
- fi
280
+
281
+ log_info "✅ 旧 master 实例已强制清理"
282
+ rm -f "$master_pid_file"
283
+ return 0
284
+ }
285
+
286
+ # 调用清理函数
287
+ cleanup_old_master
204
288
 
205
289
  # 智能依赖管理:仅在必要时安装
206
290
  REQUIREMENTS_FILE="${VIBEGO_REQUIREMENTS_PATH:-$ROOT_DIR/scripts/requirements.txt}"
@@ -256,8 +340,34 @@ if [[ -n "${MASTER_RESTART_EXPECTED:-}" ]]; then
256
340
  fi
257
341
 
258
342
  log_info "准备启动 master 进程..."
343
+
344
+ # 清理旧的错误日志(保留最近 10 次)
345
+ cleanup_old_error_logs() {
346
+ local error_log_pattern="$LOG_DIR/master_error_*.log"
347
+ local error_logs
348
+ error_logs=$(ls -t $error_log_pattern 2>/dev/null || true)
349
+ if [[ -n "$error_logs" ]]; then
350
+ local count=0
351
+ while IFS= read -r logfile; do
352
+ ((count++))
353
+ if (( count > 10 )); then
354
+ rm -f "$logfile"
355
+ log_info "已清理旧错误日志: $logfile"
356
+ fi
357
+ done <<< "$error_logs"
358
+ fi
359
+ }
360
+
361
+ cleanup_old_error_logs
362
+
363
+ # 创建带时间戳的错误日志文件
364
+ MASTER_ERROR_LOG="$LOG_DIR/master_error_$(date +%Y%m%d_%H%M%S).log"
365
+ MASTER_STDOUT_LOG="$LOG_DIR/master_stdout.log"
366
+
259
367
  # 显式传递环境变量给 nohup 进程,确保重启信号文件路径正确
260
- MASTER_RESTART_SIGNAL_PATH="$MASTER_RESTART_SIGNAL_PATH" nohup python master.py >> /dev/null 2>&1 &
368
+ # 使用虚拟环境的 Python 解释器,避免版本不匹配导致依赖加载失败
369
+ # 重要:将 stderr 保存到日志文件,方便排查启动失败问题
370
+ MASTER_RESTART_SIGNAL_PATH="$MASTER_RESTART_SIGNAL_PATH" nohup "$ROOT_DIR/.venv/bin/python" master.py > "$MASTER_STDOUT_LOG" 2> "$MASTER_ERROR_LOG" &
261
371
  MASTER_PID=$!
262
372
 
263
373
  # 健壮性检查:确保进程成功启动
@@ -274,7 +384,20 @@ if ! kill -0 "$MASTER_PID" 2>/dev/null; then
274
384
  log_error "请检查:"
275
385
  log_error " - master.py 是否有语法错误: python master.py"
276
386
  log_error " - 依赖是否完整: pip list | grep aiogram"
277
- log_error " - 最近的错误日志: tail -50 $ROOT_DIR/vibe.log"
387
+ log_error " - 错误日志: $MASTER_ERROR_LOG"
388
+
389
+ # 输出错误日志的最后 20 行,帮助快速定位问题
390
+ if [[ -s "$MASTER_ERROR_LOG" ]]; then
391
+ log_error ""
392
+ log_error "=== 错误日志最后 20 行 ==="
393
+ tail -20 "$MASTER_ERROR_LOG" | while IFS= read -r line; do
394
+ log_error " $line"
395
+ done
396
+ log_error "=========================="
397
+ else
398
+ log_error "错误日志文件为空,可能是环境变量或路径问题"
399
+ fi
400
+
278
401
  exit 1
279
402
  fi
280
403
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vibego
3
- Version: 0.2.30
3
+ Version: 0.2.32
4
4
  Summary: vibego CLI:用于初始化与管理 Telegram Master Bot 的工具
5
5
  Author: Hypha
6
6
  License-Expression: LicenseRef-Proprietary
@@ -1,6 +1,6 @@
1
1
  bot.py,sha256=uJcmMJmj5SlpwUXSgtnIEeNiUGkBg-lG5g6kM9BobP4,272046
2
2
  logging_setup.py,sha256=gvxHi8mUwK3IhXJrsGNTDo-DR6ngkyav1X-tvlBF_IE,4613
3
- master.py,sha256=CHCsjOYAhr_gnNb1Yl7NaOsFQTbRL9CdhLLMIlrx3G0,109337
3
+ master.py,sha256=ZW4A3Gh0MUKFnfZX-VJ7OCNnBzlEcOWkPKYH92OfyKA,112967
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
@@ -9,7 +9,7 @@ scripts/master_healthcheck.py,sha256=-X0VVsZ0AXaOb7izxTO_oyu23g_1jsirNdGIcP8nrSI
9
9
  scripts/publish.sh,sha256=ehLfMedcXuGKJ87jpZy3kuiFszG9Cpavp3zXPfR4h-g,3511
10
10
  scripts/requirements.txt,sha256=QSt30DSSSHtfucTFPpc7twk9kLS5rVLNTcvDiagxrZg,62
11
11
  scripts/run_bot.sh,sha256=rN4K1nz041XBaUJmnBBKHS2cHmQf11vPNX8wf1hbVR4,4596
12
- scripts/start.sh,sha256=WxByzBXALfjcjhGPCrLHLkaD8hNYZ2WDQAZ0tpoQSeA,9996
12
+ scripts/start.sh,sha256=w1Q35NB-9FRZAez5I5veqgYIJ8XnVukGt8TTE_ad248,13608
13
13
  scripts/start_tmux_codex.sh,sha256=xyLv29p924q-ysxvZYAP3T6VrqLPBPMBWo9QP7cuL50,4438
14
14
  scripts/stop_all.sh,sha256=FOz07gi2CI9sMHxBb8XkqHtxRYs3jt1RYgGrEi-htVg,4086
15
15
  scripts/stop_bot.sh,sha256=ot6Sm0IYoXuRNslUVEflQmJKu5Agm-5xIUXXudJWyTM,5588
@@ -426,14 +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
- vibego_cli/__init__.py,sha256=cGLGVLdOGT2rC2HZZBaLnLPkoa2ofpzKfDuuMEhgNeY,311
429
+ vibego_cli/__init__.py,sha256=yIxuD2p_Dd2qwaOZwbuf-21aP6wKVdMFcPi5wJfYlos,311
430
430
  vibego_cli/__main__.py,sha256=qqTrYmRRLe4361fMzbI3-CqpZ7AhTofIHmfp4ykrrBY,158
431
431
  vibego_cli/config.py,sha256=VxkPJMq01tA3h3cOkH-z_tiP7pMgfSGGicRvUnCWkhI,3054
432
432
  vibego_cli/deps.py,sha256=1nRXI7Dd-S1hYE8DligzK5fIluQWETRUj4_OKL0DikQ,1419
433
433
  vibego_cli/main.py,sha256=X__NXwZnIDIFbdKSTbNyZgZHKcPlN0DQz9sqTI1aQ9E,12158
434
434
  vibego_cli/data/worker_requirements.txt,sha256=QSt30DSSSHtfucTFPpc7twk9kLS5rVLNTcvDiagxrZg,62
435
- vibego-0.2.30.dist-info/METADATA,sha256=Ksc_G1L5Gn8gIEDXPTVHhQYXereR6HczAjdfEcsAdRc,10475
436
- vibego-0.2.30.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
437
- vibego-0.2.30.dist-info/entry_points.txt,sha256=Lsy_zm-dlyxt8-9DL9blBReIwU2k22c8-kifr46ND1M,48
438
- vibego-0.2.30.dist-info/top_level.txt,sha256=R56CT3nW5H5v3ce0l3QDN4-C4qxTrNWzRTwrxnkDX4U,69
439
- vibego-0.2.30.dist-info/RECORD,,
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,,
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.30"
10
+ __version__ = "0.2.32"
11
11
 
12
12
  from .main import main # noqa: E402