vibego 0.2.12__py3-none-any.whl → 0.2.13__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
@@ -1203,6 +1203,118 @@ def _resolve_worker_target_chat_ids() -> List[int]:
1203
1203
  return sorted(targets)
1204
1204
 
1205
1205
 
1206
+ def _auto_record_chat_id(chat_id: int) -> None:
1207
+ """首次收到消息时自动将 chat_id 记录到 state 文件。
1208
+
1209
+ 仅在以下条件同时满足时写入:
1210
+ 1. STATE_FILE 环境变量已配置
1211
+ 2. state 文件存在
1212
+ 3. 当前项目在 state 中的 chat_id 为空
1213
+ """
1214
+ state_file_env = os.environ.get("STATE_FILE")
1215
+ if not state_file_env:
1216
+ return
1217
+
1218
+ state_path = Path(state_file_env).expanduser()
1219
+ if not state_path.exists():
1220
+ worker_log.debug(
1221
+ "STATE_FILE 不存在,跳过自动记录 chat_id",
1222
+ extra={**_session_extra(), "path": str(state_path)},
1223
+ )
1224
+ return
1225
+
1226
+ # 使用文件锁保证并发安全
1227
+ lock_path = state_path.with_suffix(state_path.suffix + ".lock")
1228
+ import fcntl
1229
+
1230
+ try:
1231
+ with open(lock_path, "w", encoding="utf-8") as lock_file:
1232
+ fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
1233
+
1234
+ try:
1235
+ # 读取当前 state
1236
+ raw_state = json.loads(state_path.read_text(encoding="utf-8"))
1237
+ if not isinstance(raw_state, dict):
1238
+ worker_log.warning(
1239
+ "STATE_FILE 格式异常,跳过自动记录",
1240
+ extra=_session_extra(),
1241
+ )
1242
+ return
1243
+
1244
+ # 检查当前项目的 chat_id
1245
+ project_key = PROJECT_SLUG or PROJECT_NAME
1246
+ if not project_key:
1247
+ worker_log.warning(
1248
+ "PROJECT_SLUG 和 PROJECT_NAME 均未设置,跳过自动记录",
1249
+ extra=_session_extra(),
1250
+ )
1251
+ return
1252
+
1253
+ project_state = raw_state.get(project_key)
1254
+ if not isinstance(project_state, dict):
1255
+ # 项目不存在,创建新条目
1256
+ raw_state[project_key] = {
1257
+ "chat_id": chat_id,
1258
+ "model": ACTIVE_MODEL or "codex",
1259
+ "status": "running",
1260
+ }
1261
+ need_write = True
1262
+ elif project_state.get("chat_id") is None:
1263
+ # chat_id 为空,更新
1264
+ project_state["chat_id"] = chat_id
1265
+ need_write = True
1266
+ else:
1267
+ # chat_id 已存在,无需更新
1268
+ need_write = False
1269
+
1270
+ if need_write:
1271
+ # 写入更新后的 state
1272
+ tmp_path = state_path.with_suffix(state_path.suffix + ".tmp")
1273
+ tmp_path.write_text(
1274
+ json.dumps(raw_state, ensure_ascii=False, indent=4),
1275
+ encoding="utf-8",
1276
+ )
1277
+ tmp_path.replace(state_path)
1278
+ worker_log.info(
1279
+ "已自动记录 chat_id=%s 到 state 文件",
1280
+ chat_id,
1281
+ extra={**_session_extra(), "project": project_key},
1282
+ )
1283
+ else:
1284
+ worker_log.debug(
1285
+ "chat_id 已存在,跳过自动记录",
1286
+ extra={**_session_extra(), "existing_chat_id": project_state.get("chat_id")},
1287
+ )
1288
+
1289
+ except json.JSONDecodeError as exc:
1290
+ worker_log.error(
1291
+ "STATE_FILE 解析失败,跳过自动记录:%s",
1292
+ exc,
1293
+ extra=_session_extra(),
1294
+ )
1295
+ except Exception as exc:
1296
+ worker_log.error(
1297
+ "自动记录 chat_id 失败:%s",
1298
+ exc,
1299
+ extra={**_session_extra(), "chat": chat_id},
1300
+ )
1301
+ finally:
1302
+ fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
1303
+ except Exception as exc:
1304
+ worker_log.error(
1305
+ "获取文件锁失败:%s",
1306
+ exc,
1307
+ extra=_session_extra(),
1308
+ )
1309
+ finally:
1310
+ # 清理锁文件
1311
+ try:
1312
+ if lock_path.exists():
1313
+ lock_path.unlink()
1314
+ except Exception:
1315
+ pass
1316
+
1317
+
1206
1318
  async def _broadcast_worker_keyboard(bot: Bot) -> None:
1207
1319
  """启动时主动推送菜单,确保 Telegram 键盘同步。"""
1208
1320
  targets = _resolve_worker_target_chat_ids()
@@ -2657,7 +2769,8 @@ def _build_summary_prompt(
2657
2769
  ) -> str:
2658
2770
  """构造模型摘要提示词,要求携带请求标识。"""
2659
2771
 
2660
- task_code = _format_task_command(task.id)
2772
+ # 摘要提示词是发送给模型的,使用纯文本格式,不需要 Markdown 转义
2773
+ task_code = f"/{task.id}" if task.id else "-"
2661
2774
  title = task.title or "-"
2662
2775
  status_label = STATUS_LABELS.get(task.status, task.status)
2663
2776
  note_lines: list[str] = []
@@ -2820,8 +2933,9 @@ def _build_task_history_view(
2820
2933
  if total_items == 0:
2821
2934
  raise ValueError("暂无事件记录")
2822
2935
 
2936
+ # 历史记录会被包裹在代码块中显示,使用纯文本格式,不需要 Markdown 转义
2823
2937
  title_text = normalize_newlines(task.title or "").strip() or "-"
2824
- title_display = _escape_markdown_text(title_text) if _IS_MARKDOWN_V2 else title_text
2938
+ title_display = title_text
2825
2939
 
2826
2940
  digit_width = len(str(max(total_items, 1)))
2827
2941
  placeholder_page = "9" * digit_width
@@ -3650,7 +3764,19 @@ def _get_delivered_offsets(chat_id: int, session_key: str) -> set[int]:
3650
3764
  return CHAT_DELIVERED_OFFSETS.setdefault(chat_id, {}).setdefault(session_key, set())
3651
3765
 
3652
3766
 
3653
- async def _deliver_pending_messages(chat_id: int, session_path: Path) -> bool:
3767
+ async def _deliver_pending_messages(
3768
+ chat_id: int,
3769
+ session_path: Path,
3770
+ *,
3771
+ add_completion_header: bool = True
3772
+ ) -> bool:
3773
+ """发送待处理的模型消息。
3774
+
3775
+ Args:
3776
+ chat_id: Telegram 聊天 ID
3777
+ session_path: 会话文件路径
3778
+ add_completion_header: 是否添加"✅模型执行完成"前缀(快速轮询阶段为 True,延迟轮询为 False)
3779
+ """
3654
3780
  session_key = str(session_path)
3655
3781
  previous_offset = SESSION_OFFSETS.get(session_key, 0)
3656
3782
  new_offset, events = _read_session_events(session_path)
@@ -3725,7 +3851,8 @@ async def _deliver_pending_messages(chat_id: int, session_path: Path) -> bool:
3725
3851
  last_committed_offset = event_offset
3726
3852
  SESSION_OFFSETS[session_key] = event_offset
3727
3853
  continue
3728
- formatted_text = _prepend_completion_header(text_to_send)
3854
+ # 根据轮询阶段决定是否添加完成前缀
3855
+ formatted_text = _prepend_completion_header(text_to_send) if add_completion_header else text_to_send
3729
3856
  payload_for_hash = _prepare_model_payload(formatted_text)
3730
3857
  initial_hash = hashlib.sha256(payload_for_hash.encode("utf-8", errors="ignore")).hexdigest()
3731
3858
  if initial_hash in delivered_hashes:
@@ -3836,22 +3963,24 @@ async def _deliver_pending_messages(chat_id: int, session_path: Path) -> bool:
3836
3963
  SESSION_OFFSETS[session_key] = max(last_committed_offset, new_offset)
3837
3964
 
3838
3965
  if delivered_response:
3966
+ # 实际发送了消息,返回 True 表示本次调用成功发送
3967
+ # 这样可以确保延迟轮询机制被正确触发
3839
3968
  if ENABLE_PLAN_PROGRESS and plan_active:
3840
3969
  worker_log.info(
3841
- "模型输出已发送,但计划仍在更新,继续监听",
3970
+ "模型输出已发送,但计划仍在更新",
3971
+ extra={
3972
+ **_session_extra(path=session_path),
3973
+ "chat": chat_id,
3974
+ },
3975
+ )
3976
+ else:
3977
+ worker_log.info(
3978
+ "模型输出已发送且计划完成",
3842
3979
  extra={
3843
3980
  **_session_extra(path=session_path),
3844
3981
  "chat": chat_id,
3845
3982
  },
3846
3983
  )
3847
- return False
3848
- worker_log.info(
3849
- "模型输出已发送且计划完成",
3850
- extra={
3851
- **_session_extra(path=session_path),
3852
- "chat": chat_id,
3853
- },
3854
- )
3855
3984
  return True
3856
3985
 
3857
3986
  if ENABLE_PLAN_PROGRESS and not plan_active and final_response_sent:
@@ -4190,16 +4319,17 @@ async def _watch_and_notify(chat_id: int, session_path: Path,
4190
4319
 
4191
4320
  两阶段轮询机制:
4192
4321
  - 阶段1(快速轮询):interval 间隔(通常 0.3 秒),直到首次发送成功
4193
- - 阶段2(延迟轮询):180 秒间隔,最多 10 次,捕获长时间任务的后续输出
4322
+ - 阶段2(延迟轮询):3 秒间隔,最多 600 次(持续 30 分钟),捕获长时间任务的后续输出
4194
4323
 
4195
4324
  异常安全:使用 try...finally 确保状态清理。
4325
+ 中断机制:收到新 Telegram 消息时会设置 interrupted 标志,轮询自动停止。
4196
4326
  """
4197
4327
  start = time.monotonic()
4198
4328
  first_delivery_done = False
4199
4329
  current_interval = interval # 初始为快速轮询间隔(0.3 秒)
4200
4330
  long_poll_rounds = 0
4201
- long_poll_max_rounds = 10
4202
- long_poll_interval = 180.0 # 3 分钟
4331
+ long_poll_max_rounds = 600 # 30 分钟 / 3 秒 = 600 次
4332
+ long_poll_interval = 3.0 # 3
4203
4333
 
4204
4334
  try:
4205
4335
  while True:
@@ -4233,7 +4363,12 @@ async def _watch_and_notify(chat_id: int, session_path: Path,
4233
4363
  continue
4234
4364
 
4235
4365
  try:
4236
- delivered = await _deliver_pending_messages(chat_id, session_path)
4366
+ # 快速轮询阶段添加前缀,延迟轮询阶段不添加
4367
+ delivered = await _deliver_pending_messages(
4368
+ chat_id,
4369
+ session_path,
4370
+ add_completion_header=not first_delivery_done
4371
+ )
4237
4372
  except Exception as exc:
4238
4373
  worker_log.error(
4239
4374
  "消息发送时发生未预期异常",
@@ -6918,6 +7053,9 @@ async def on_edit_new_value(message: Message, state: FSMContext) -> None:
6918
7053
 
6919
7054
  @router.message(CommandStart())
6920
7055
  async def on_start(m: Message):
7056
+ # 首次收到消息时自动记录 chat_id 到 state 文件
7057
+ _auto_record_chat_id(m.chat.id)
7058
+
6921
7059
  await m.answer(
6922
7060
  (
6923
7061
  f"Hello, {m.from_user.full_name}!\n"
@@ -6933,6 +7071,9 @@ async def on_start(m: Message):
6933
7071
 
6934
7072
  @router.message(F.text)
6935
7073
  async def on_text(m: Message):
7074
+ # 首次收到消息时自动记录 chat_id 到 state 文件
7075
+ _auto_record_chat_id(m.chat.id)
7076
+
6936
7077
  prompt = (m.text or "").strip()
6937
7078
  if not prompt:
6938
7079
  return await m.answer("请输入非空提示词")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vibego
3
- Version: 0.2.12
3
+ Version: 0.2.13
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=NPa3-Zb_20O8AjpQTUBDFfTZesh5kVEXRK17SbNpu_0,256916
1
+ bot.py,sha256=YsMdqha8MaJrnDJQwZ0GgQFrnoYKctV5es-RmbL-VeU,262371
2
2
  logging_setup.py,sha256=gvxHi8mUwK3IhXJrsGNTDo-DR6ngkyav1X-tvlBF_IE,4613
3
3
  master.py,sha256=Qz2NTapUexVvpQz8Y_pVhKd-uXkqp3M6oclzfAzIuGs,106497
4
4
  project_repository.py,sha256=UcthtSGOJK0cTE5bQCneo3xkomRG-kyc1N1QVqxeHIs,17577
@@ -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=qgEUIxQ50ynuK_Dcun8OGqFZe0ICJWheDr1_-6tT9bU,311
428
+ vibego_cli/__init__.py,sha256=HDuRE3BFyJQ0KL0Ozp34-WoYBjtGdy6lzYEt1AS7vyY,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.12.dist-info/METADATA,sha256=rqZTvJDLdYpOe_2FY4rEvGfnVVWC543yh0Nn944vCt8,10475
435
- vibego-0.2.12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
436
- vibego-0.2.12.dist-info/entry_points.txt,sha256=Lsy_zm-dlyxt8-9DL9blBReIwU2k22c8-kifr46ND1M,48
437
- vibego-0.2.12.dist-info/top_level.txt,sha256=R56CT3nW5H5v3ce0l3QDN4-C4qxTrNWzRTwrxnkDX4U,69
438
- vibego-0.2.12.dist-info/RECORD,,
434
+ vibego-0.2.13.dist-info/METADATA,sha256=eaSYWnC-3iRHcqmq3ewae3njV2s53eWMwS1Rggkqrhg,10475
435
+ vibego-0.2.13.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
436
+ vibego-0.2.13.dist-info/entry_points.txt,sha256=Lsy_zm-dlyxt8-9DL9blBReIwU2k22c8-kifr46ND1M,48
437
+ vibego-0.2.13.dist-info/top_level.txt,sha256=R56CT3nW5H5v3ce0l3QDN4-C4qxTrNWzRTwrxnkDX4U,69
438
+ vibego-0.2.13.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.12"
10
+ __version__ = "0.2.13"
11
11
 
12
12
  from .main import main # noqa: E402