vibego 0.2.11__py3-none-any.whl → 0.2.12__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
@@ -219,7 +219,7 @@ SEND_RETRY_BASE_DELAY = float(os.environ.get("SEND_RETRY_BASE_DELAY", "0.5"))
219
219
  SEND_FAILURE_NOTICE_COOLDOWN = float(os.environ.get("SEND_FAILURE_NOTICE_COOLDOWN", "30"))
220
220
  SESSION_INITIAL_BACKTRACK_BYTES = int(os.environ.get("SESSION_INITIAL_BACKTRACK_BYTES", "16384"))
221
221
  ENABLE_PLAN_PROGRESS = (os.environ.get("ENABLE_PLAN_PROGRESS", "1").strip().lower() not in {"0", "false", "no", "off"})
222
- AUTO_COMPACT_THRESHOLD = max(_env_int("AUTO_COMPACT_THRESHOLD", 10), 0)
222
+ AUTO_COMPACT_THRESHOLD = max(_env_int("AUTO_COMPACT_THRESHOLD", 0), 0)
223
223
 
224
224
  PLAN_STATUS_LABELS = {
225
225
  "completed": "✅",
@@ -967,6 +967,9 @@ async def _dispatch_prompt_to_model(
967
967
  return True, session_path
968
968
  await asyncio.sleep(0.3)
969
969
 
970
+ # 中断旧的延迟轮询(如果存在)
971
+ await _interrupt_long_poll(chat_id)
972
+
970
973
  watcher_task = asyncio.create_task(
971
974
  _watch_and_notify(
972
975
  chat_id,
@@ -3050,6 +3053,9 @@ CHAT_DELIVERED_HASHES: Dict[int, Dict[str, set[str]]] = {}
3050
3053
  CHAT_DELIVERED_OFFSETS: Dict[int, Dict[str, set[int]]] = {}
3051
3054
  CHAT_REPLY_COUNT: Dict[int, Dict[str, int]] = {}
3052
3055
  CHAT_COMPACT_STATE: Dict[int, Dict[str, Dict[str, Any]]] = {}
3056
+ # 长轮询状态:用于延迟轮询机制
3057
+ CHAT_LONG_POLL_STATE: Dict[int, Dict[str, Any]] = {}
3058
+ CHAT_LONG_POLL_LOCK: Optional[asyncio.Lock] = None # 在事件循环启动后初始化
3053
3059
  SUMMARY_REQUEST_TIMEOUT_SECONDS = 300.0
3054
3060
 
3055
3061
 
@@ -3992,6 +3998,9 @@ async def _ensure_session_watcher(chat_id: int) -> Optional[Path]:
3992
3998
  if watcher is not None and watcher.done():
3993
3999
  CHAT_WATCHERS.pop(chat_id, None)
3994
4000
 
4001
+ # 中断旧的延迟轮询(如果存在)
4002
+ await _interrupt_long_poll(chat_id)
4003
+
3995
4004
  CHAT_WATCHERS[chat_id] = asyncio.create_task(
3996
4005
  _watch_and_notify(
3997
4006
  chat_id,
@@ -4151,23 +4160,175 @@ async def _finalize_plan_progress(chat_id: int) -> None:
4151
4160
 
4152
4161
 
4153
4162
 
4163
+ async def _interrupt_long_poll(chat_id: int) -> None:
4164
+ """
4165
+ 中断指定 chat 的延迟轮询。
4166
+
4167
+ 当用户发送新消息时调用,确保旧的延迟轮询被终止,
4168
+ 为新的监听任务让路。
4169
+
4170
+ 线程安全:使用 asyncio.Lock 保护状态访问。
4171
+ """
4172
+ if CHAT_LONG_POLL_LOCK is None:
4173
+ # 锁未初始化(测试环境或启动早期)
4174
+ return
4175
+
4176
+ async with CHAT_LONG_POLL_LOCK:
4177
+ state = CHAT_LONG_POLL_STATE.get(chat_id)
4178
+ if state is not None:
4179
+ state["interrupted"] = True
4180
+ worker_log.info(
4181
+ "标记延迟轮询为待中断",
4182
+ extra={"chat": chat_id},
4183
+ )
4184
+
4185
+
4154
4186
  async def _watch_and_notify(chat_id: int, session_path: Path,
4155
4187
  max_wait: float, interval: float):
4188
+ """
4189
+ 监听会话文件并发送消息。
4190
+
4191
+ 两阶段轮询机制:
4192
+ - 阶段1(快速轮询):interval 间隔(通常 0.3 秒),直到首次发送成功
4193
+ - 阶段2(延迟轮询):180 秒间隔,最多 10 次,捕获长时间任务的后续输出
4194
+
4195
+ 异常安全:使用 try...finally 确保状态清理。
4196
+ """
4156
4197
  start = time.monotonic()
4157
- while True:
4158
- await asyncio.sleep(interval)
4159
- if max_wait > 0 and time.monotonic() - start > max_wait:
4160
- worker_log.warning(
4161
- "[session-map] chat=%s 长时间未获取到 Codex 输出,停止轮询",
4162
- chat_id,
4163
- extra=_session_extra(path=session_path),
4164
- )
4165
- return
4166
- if not session_path.exists():
4167
- continue
4168
- delivered = await _deliver_pending_messages(chat_id, session_path)
4169
- if delivered:
4170
- return
4198
+ first_delivery_done = False
4199
+ current_interval = interval # 初始为快速轮询间隔(0.3 秒)
4200
+ long_poll_rounds = 0
4201
+ long_poll_max_rounds = 10
4202
+ long_poll_interval = 180.0 # 3 分钟
4203
+
4204
+ try:
4205
+ while True:
4206
+ # 检查是否被新消息中断(使用锁保护)
4207
+ if CHAT_LONG_POLL_LOCK is not None:
4208
+ async with CHAT_LONG_POLL_LOCK:
4209
+ state = CHAT_LONG_POLL_STATE.get(chat_id)
4210
+ if state is not None and state.get("interrupted", False):
4211
+ worker_log.info(
4212
+ "延迟轮询被新消息中断",
4213
+ extra={
4214
+ **_session_extra(path=session_path),
4215
+ "chat": chat_id,
4216
+ "round": long_poll_rounds,
4217
+ },
4218
+ )
4219
+ return
4220
+
4221
+ await asyncio.sleep(current_interval)
4222
+
4223
+ # 检查超时(仅在快速轮询阶段)
4224
+ if not first_delivery_done and max_wait > 0 and time.monotonic() - start > max_wait:
4225
+ worker_log.warning(
4226
+ "[session-map] chat=%s 长时间未获取到 Codex 输出,停止轮询",
4227
+ chat_id,
4228
+ extra=_session_extra(path=session_path),
4229
+ )
4230
+ return
4231
+
4232
+ if not session_path.exists():
4233
+ continue
4234
+
4235
+ try:
4236
+ delivered = await _deliver_pending_messages(chat_id, session_path)
4237
+ except Exception as exc:
4238
+ worker_log.error(
4239
+ "消息发送时发生未预期异常",
4240
+ exc_info=exc,
4241
+ extra={
4242
+ **_session_extra(path=session_path),
4243
+ "chat": chat_id,
4244
+ },
4245
+ )
4246
+ delivered = False
4247
+
4248
+ # 首次发送成功,切换到延迟轮询模式
4249
+ if delivered and not first_delivery_done:
4250
+ first_delivery_done = True
4251
+ current_interval = long_poll_interval
4252
+ if CHAT_LONG_POLL_LOCK is not None:
4253
+ async with CHAT_LONG_POLL_LOCK:
4254
+ CHAT_LONG_POLL_STATE[chat_id] = {
4255
+ "active": True,
4256
+ "round": 0,
4257
+ "max_rounds": long_poll_max_rounds,
4258
+ "interrupted": False,
4259
+ }
4260
+ worker_log.info(
4261
+ "首次发送成功,启动延迟轮询模式",
4262
+ extra={
4263
+ **_session_extra(path=session_path),
4264
+ "chat": chat_id,
4265
+ "interval": long_poll_interval,
4266
+ "max_rounds": long_poll_max_rounds,
4267
+ },
4268
+ )
4269
+ continue
4270
+
4271
+ # 延迟轮询阶段
4272
+ if first_delivery_done:
4273
+ if delivered:
4274
+ # 又收到新消息,重置轮询计数
4275
+ long_poll_rounds = 0
4276
+ if CHAT_LONG_POLL_LOCK is not None:
4277
+ async with CHAT_LONG_POLL_LOCK:
4278
+ state = CHAT_LONG_POLL_STATE.get(chat_id)
4279
+ if state is not None:
4280
+ state["round"] = 0
4281
+ worker_log.info(
4282
+ "延迟轮询中收到新消息,重置计数",
4283
+ extra={
4284
+ **_session_extra(path=session_path),
4285
+ "chat": chat_id,
4286
+ },
4287
+ )
4288
+ else:
4289
+ # 无新消息,增加轮询计数
4290
+ long_poll_rounds += 1
4291
+ if CHAT_LONG_POLL_LOCK is not None:
4292
+ async with CHAT_LONG_POLL_LOCK:
4293
+ state = CHAT_LONG_POLL_STATE.get(chat_id)
4294
+ if state is not None:
4295
+ state["round"] = long_poll_rounds
4296
+
4297
+ if long_poll_rounds >= long_poll_max_rounds:
4298
+ worker_log.info(
4299
+ "延迟轮询达到最大次数,停止监听",
4300
+ extra={
4301
+ **_session_extra(path=session_path),
4302
+ "chat": chat_id,
4303
+ "total_rounds": long_poll_rounds,
4304
+ },
4305
+ )
4306
+ return
4307
+
4308
+ worker_log.debug(
4309
+ "延迟轮询中无新消息",
4310
+ extra={
4311
+ **_session_extra(path=session_path),
4312
+ "chat": chat_id,
4313
+ "round": f"{long_poll_rounds}/{long_poll_max_rounds}",
4314
+ },
4315
+ )
4316
+ continue
4317
+
4318
+ # 快速轮询阶段:如果已发送消息,退出
4319
+ if delivered:
4320
+ return
4321
+
4322
+ finally:
4323
+ # 确保无论如何都清理延迟轮询状态
4324
+ if CHAT_LONG_POLL_LOCK is not None:
4325
+ async with CHAT_LONG_POLL_LOCK:
4326
+ if chat_id in CHAT_LONG_POLL_STATE:
4327
+ CHAT_LONG_POLL_STATE.pop(chat_id, None)
4328
+ worker_log.debug(
4329
+ "监听任务退出,已清理延迟轮询状态",
4330
+ extra={"chat": chat_id},
4331
+ )
4171
4332
 
4172
4333
 
4173
4334
  def _read_pointer_path(pointer: Path) -> Optional[Path]:
@@ -5422,7 +5583,7 @@ async def on_task_desc_input(message: Message, state: FSMContext) -> None:
5422
5583
 
5423
5584
  @router.message(TaskDescriptionStates.waiting_confirm)
5424
5585
  async def on_task_desc_confirm_stage_text(message: Message, state: FSMContext) -> None:
5425
- """处理任务描述确认阶段的菜单指令。"""
5586
+ """处理任务描述确认阶段的菜单指令。支持按钮点击、数字编号和直接文本输入。"""
5426
5587
 
5427
5588
  data = await state.get_data()
5428
5589
  task_id = data.get("task_id")
@@ -5431,13 +5592,19 @@ async def on_task_desc_confirm_stage_text(message: Message, state: FSMContext) -
5431
5592
  await message.answer("会话已失效,请重新操作。", reply_markup=_build_worker_main_keyboard())
5432
5593
  return
5433
5594
 
5434
- token = _normalize_choice_token(message.text or "")
5435
- if _is_cancel_message(token) or token == _normalize_choice_token(TASK_DESC_CANCEL_TEXT):
5595
+ # 使用 _resolve_reply_choice() 智能解析用户输入,支持数字编号、按钮文本和直接文本
5596
+ options = [TASK_DESC_CONFIRM_TEXT, TASK_DESC_RETRY_TEXT, TASK_DESC_CANCEL_TEXT]
5597
+ resolved = _resolve_reply_choice(message.text, options=options)
5598
+ stripped = _strip_number_prefix((message.text or "").strip()).lower()
5599
+
5600
+ # 处理取消操作
5601
+ if resolved == options[2] or _is_cancel_message(resolved) or stripped in {"取消"}:
5436
5602
  await state.clear()
5437
5603
  await message.answer("已取消编辑任务描述。", reply_markup=_build_worker_main_keyboard())
5438
5604
  return
5439
5605
 
5440
- if token == _normalize_choice_token(TASK_DESC_RETRY_TEXT):
5606
+ # 处理重新输入操作
5607
+ if resolved == options[1] or stripped in {"重新输入"}:
5441
5608
  task = await TASK_SERVICE.get_task(task_id)
5442
5609
  if task is None:
5443
5610
  await state.clear()
@@ -5455,7 +5622,8 @@ async def on_task_desc_confirm_stage_text(message: Message, state: FSMContext) -
5455
5622
  )
5456
5623
  return
5457
5624
 
5458
- if token == _normalize_choice_token(TASK_DESC_CONFIRM_TEXT):
5625
+ # 处理确认更新操作
5626
+ if resolved == options[0] or stripped in {"确认", "确认更新"}:
5459
5627
  new_description = data.get("new_description")
5460
5628
  if new_description is None:
5461
5629
  await state.set_state(TaskDescriptionStates.waiting_content)
@@ -5486,8 +5654,9 @@ async def on_task_desc_confirm_stage_text(message: Message, state: FSMContext) -
5486
5654
  )
5487
5655
  return
5488
5656
 
5657
+ # 无效输入,提示用户
5489
5658
  await message.answer(
5490
- "当前处于确认阶段,请使用菜单中的按钮确认或重新输入。",
5659
+ "当前处于确认阶段,请选择确认、重新输入或取消,可直接输入编号或点击键盘按钮:",
5491
5660
  reply_markup=_build_task_desc_confirm_keyboard(),
5492
5661
  )
5493
5662
 
@@ -6869,7 +7038,9 @@ async def _ensure_worker_menu_button(bot: Bot) -> None:
6869
7038
  )
6870
7039
 
6871
7040
  async def main():
6872
- global _bot
7041
+ global _bot, CHAT_LONG_POLL_LOCK
7042
+ # 初始化长轮询锁
7043
+ CHAT_LONG_POLL_LOCK = asyncio.Lock()
6873
7044
  _bot = build_bot()
6874
7045
  try:
6875
7046
  await ensure_telegram_connectivity(_bot)
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env bash
2
+ # 版本管理便捷脚本
3
+ # 使用方式:
4
+ # ./scripts/bump_version.sh patch
5
+ # ./scripts/bump_version.sh minor
6
+ # ./scripts/bump_version.sh major
7
+ # ./scripts/bump_version.sh show
8
+ # ./scripts/bump_version.sh --help
9
+
10
+ set -e
11
+
12
+ # 项目根目录
13
+ PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
14
+ cd "$PROJECT_ROOT"
15
+
16
+ # bump-my-version 路径
17
+ BUMP_CMD="/Users/david/.config/vibego/runtime/.venv/bin/bump-my-version"
18
+
19
+ # 检查 bump-my-version 是否存在
20
+ if [ ! -f "$BUMP_CMD" ]; then
21
+ echo "错误:找不到 bump-my-version"
22
+ echo "请先安装:pip install bump-my-version"
23
+ exit 1
24
+ fi
25
+
26
+ # 如果没有参数,显示帮助
27
+ if [ $# -eq 0 ]; then
28
+ echo "用法:"
29
+ echo " $0 patch 递增补丁版本 (0.2.11 → 0.2.12)"
30
+ echo " 自动提交:fix: bugfixes"
31
+ echo " $0 minor 递增次版本 (0.2.11 → 0.3.0)"
32
+ echo " 自动提交:feat: 添加新功能"
33
+ echo " $0 major 递增主版本 (0.2.11 → 1.0.0)"
34
+ echo " 自动提交:feat!: 重大变更"
35
+ echo " $0 show 显示当前版本"
36
+ echo " $0 --dry-run 预览变更(添加在 patch/minor/major 后)"
37
+ echo ""
38
+ echo "说明:"
39
+ echo " 脚本会自动提交当前未提交的修改,然后递增版本号。"
40
+ echo " 如果不想自动提交,请在参数中添加 --no-auto-commit"
41
+ echo ""
42
+ echo "示例:"
43
+ echo " $0 patch # 自动提交修改并递增补丁版本"
44
+ echo " $0 patch --dry-run # 预览补丁版本递增(不会提交)"
45
+ echo " $0 minor --no-auto-commit # 仅递增版本,不自动提交当前修改"
46
+ exit 0
47
+ fi
48
+
49
+ # 处理 show 命令
50
+ if [ "$1" = "show" ]; then
51
+ "$BUMP_CMD" show current_version
52
+ exit 0
53
+ fi
54
+
55
+ # 处理 --help
56
+ if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
57
+ "$BUMP_CMD" --help
58
+ exit 0
59
+ fi
60
+
61
+ # 检查是否禁用自动提交
62
+ AUTO_COMMIT=true
63
+ if [[ "$*" =~ "--no-auto-commit" ]]; then
64
+ AUTO_COMMIT=false
65
+ fi
66
+
67
+ # 检查是否是 dry-run
68
+ DRY_RUN=false
69
+ if [[ "$*" =~ "--dry-run" ]]; then
70
+ DRY_RUN=true
71
+ fi
72
+
73
+ # 获取版本类型
74
+ VERSION_TYPE="$1"
75
+
76
+ # 获取对应版本类型的 commit 消息
77
+ get_commit_message() {
78
+ case "$1" in
79
+ patch)
80
+ echo "fix: bugfixes"
81
+ ;;
82
+ minor)
83
+ echo "feat: 添加新功能"
84
+ ;;
85
+ major)
86
+ echo "feat!: 重大变更"
87
+ ;;
88
+ *)
89
+ echo ""
90
+ ;;
91
+ esac
92
+ }
93
+
94
+ # 检查版本类型是否有效
95
+ COMMIT_MSG=$(get_commit_message "$VERSION_TYPE")
96
+ if [ -z "$COMMIT_MSG" ]; then
97
+ # 如果不是有效的版本类型,直接传递给 bump-my-version
98
+ "$BUMP_CMD" bump "$@"
99
+ exit 0
100
+ fi
101
+
102
+ # 显示当前版本
103
+ echo "📦 当前版本:$("$BUMP_CMD" show current_version)"
104
+ echo ""
105
+
106
+ # 检查是否有未提交的修改
107
+ if [ "$AUTO_COMMIT" = true ] && [ "$DRY_RUN" = false ]; then
108
+ if ! git diff-index --quiet HEAD -- 2>/dev/null; then
109
+ echo "📝 检测到未提交的修改,准备创建 commit..."
110
+ echo ""
111
+
112
+ echo "Commit 消息:$COMMIT_MSG"
113
+ echo ""
114
+
115
+ # 显示将要提交的文件
116
+ echo "将要提交的文件:"
117
+ git status --short
118
+ echo ""
119
+
120
+ # 提交所有修改
121
+ git add .
122
+ git commit -m "$COMMIT_MSG"
123
+
124
+ echo "✅ 代码修改已提交"
125
+ echo ""
126
+ else
127
+ echo "ℹ️ 没有未提交的修改,跳过自动 commit"
128
+ echo ""
129
+ fi
130
+ fi
131
+
132
+ # 执行版本递增
133
+ echo "🚀 开始递增版本..."
134
+ echo ""
135
+
136
+ "$BUMP_CMD" bump "$@"
137
+
138
+ echo ""
139
+ echo "✅ 版本管理完成!"
140
+ echo ""
141
+ echo "📋 操作摘要:"
142
+ if [ "$AUTO_COMMIT" = true ] && [ "$DRY_RUN" = false ]; then
143
+ echo " 1. 已提交代码修改(如有)"
144
+ echo " 2. 已递增版本号"
145
+ echo " 3. 已创建版本 commit 和 tag"
146
+ else
147
+ echo " 1. 已递增版本号"
148
+ echo " 2. 已创建版本 commit 和 tag"
149
+ fi
150
+ echo ""
151
+ echo "💡 提示:如需推送到远程,请执行:"
152
+ echo " git push && git push --tags"
@@ -20,6 +20,10 @@ claude_project_key_from_workdir() {
20
20
  model_configure() {
21
21
  MODEL_NAME="ClaudeCode"
22
22
  MODEL_WORKDIR="${CLAUDE_WORKDIR:-${MODEL_WORKDIR:-$ROOT_DIR}}"
23
+ # 默认关闭文件快照,避免孤儿 CLI 持续写入 jsonl
24
+ CLAUDE_DISABLE_FILE_CHECKPOINTING="${CLAUDE_DISABLE_FILE_CHECKPOINTING:-1}"
25
+ CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING="${CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING:-$CLAUDE_DISABLE_FILE_CHECKPOINTING}"
26
+ export CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING
23
27
  local project_key
24
28
  if [[ -n "${CLAUDE_PROJECT_KEY:-}" ]]; then
25
29
  project_key="$CLAUDE_PROJECT_KEY"
scripts/run_bot.sh CHANGED
@@ -151,6 +151,9 @@ export TMUX_LOG="$MODEL_LOG"
151
151
  export PROJECT_NAME="$PROJECT_NAME"
152
152
  export LOG_DIR="$LOG_DIR"
153
153
  export ROOT_DIR="$SOURCE_ROOT"
154
+ if [[ -n "${CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING:-}" ]]; then
155
+ export CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING
156
+ fi
154
157
 
155
158
  "$SOURCE_ROOT/scripts/start_tmux_codex.sh" --kill >/dev/null
156
159
 
@@ -117,13 +117,20 @@ run_tmux pipe-pane -o -t "$SESSION_NAME" "$PIPE_CMD"
117
117
 
118
118
  # 同步环境变量到 tmux 服务端,避免复用旧会话时丢失设置
119
119
  run_tmux set-environment -t "$SESSION_NAME" DISABLE_UPDATE_PROMPT "${DISABLE_UPDATE_PROMPT:-true}"
120
+ if [[ -n "${CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING:-}" ]]; then
121
+ run_tmux set-environment -t "$SESSION_NAME" CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING "${CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING}"
122
+ fi
120
123
 
121
124
  if (( RESTART )); then
122
125
  run_tmux send-keys -t "$SESSION_NAME" C-c
123
126
  sleep 1
124
127
  fi
125
128
 
126
- printf -v FINAL_CMD 'env DISABLE_UPDATE_PROMPT=%q %s' "${DISABLE_UPDATE_PROMPT:-true}" "$MODEL_CMD"
129
+ env_prefix="env $(printf '%q' "DISABLE_UPDATE_PROMPT=${DISABLE_UPDATE_PROMPT:-true}")"
130
+ if [[ -n "${CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING:-}" ]]; then
131
+ env_prefix+=" $(printf '%q' "CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING=${CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING}")"
132
+ fi
133
+ printf -v FINAL_CMD '%s %s' "$env_prefix" "$MODEL_CMD"
127
134
 
128
135
  if (( SESSION_CREATED )) || (( FORCE_START )); then
129
136
  run_tmux send-keys -t "$SESSION_NAME" "$FINAL_CMD" C-m
scripts/stop_bot.sh CHANGED
@@ -47,6 +47,62 @@ fi
47
47
 
48
48
  POINTER_BASENAME="${MODEL_POINTER_BASENAME:-current_session.txt}"
49
49
 
50
+ graceful_shutdown_claudecode() {
51
+ local session="$1"
52
+ local timeout="${2:-10}"
53
+ if ! command -v tmux >/dev/null 2>&1; then
54
+ return 0
55
+ fi
56
+ if ! tmux -u has-session -t "$session" >/dev/null 2>&1; then
57
+ return 0
58
+ fi
59
+ local pane_ids=()
60
+ while IFS= read -r pane; do
61
+ [[ -z "$pane" ]] && continue
62
+ pane_ids+=("$pane")
63
+ done < <(tmux -u list-panes -t "$session" -F "#{pane_id}" 2>/dev/null || true)
64
+ (( ${#pane_ids[@]} )) || return 0
65
+
66
+ local current_cmd has_claude=0
67
+ for pane in "${pane_ids[@]}"; do
68
+ current_cmd=$(tmux -u display-message -p -t "$pane" '#{pane_current_command}' 2>/dev/null || echo "")
69
+ if [[ "$current_cmd" == claude* ]]; then
70
+ has_claude=1
71
+ break
72
+ fi
73
+ done
74
+ (( has_claude )) || return 0
75
+
76
+ printf '[stop-bot] 检测到 ClaudeCode 会话,尝试发送 /exit (session=%s)\n' "$session"
77
+ for pane in "${pane_ids[@]}"; do
78
+ tmux -u send-keys -t "$pane" Escape
79
+ tmux -u send-keys -t "$pane" C-u
80
+ tmux -u send-keys -t "$pane" "/exit" C-m
81
+ done
82
+
83
+ local end_time=$(( $(date +%s) + timeout ))
84
+ while tmux -u has-session -t "$session" >/dev/null 2>&1; do
85
+ local still_running=0
86
+ for pane in "${pane_ids[@]}"; do
87
+ current_cmd=$(tmux -u display-message -p -t "$pane" '#{pane_current_command}' 2>/dev/null || echo "")
88
+ if [[ "$current_cmd" == claude* ]]; then
89
+ still_running=1
90
+ break
91
+ fi
92
+ done
93
+ if (( still_running == 0 )); then
94
+ printf '[stop-bot] ClaudeCode 会话已响应 /exit (session=%s)\n' "$session"
95
+ break
96
+ fi
97
+ if (( $(date +%s) >= end_time )); then
98
+ printf '[stop-bot] ClaudeCode /exit 超时,将继续执行强制关闭 (session=%s)\n' "$session" >&2
99
+ break
100
+ fi
101
+ sleep 0.5
102
+ done
103
+ return 0
104
+ }
105
+
50
106
  kill_tty_sessions() {
51
107
  local session="$1"
52
108
  if command -v tmux >/dev/null 2>&1 && tmux -u has-session -t "$session" >/dev/null 2>&1; then
@@ -64,8 +120,10 @@ kill_pid_file() {
64
120
  if [[ ! -f "$pid_file" ]]; then
65
121
  local fallback_dir
66
122
  fallback_dir="$(dirname "$pid_file")"
67
- [[ -d "$fallback_dir" ]] && clear_session_files "$fallback_dir"
68
- return
123
+ if [[ -d "$fallback_dir" ]]; then
124
+ clear_session_files "$fallback_dir"
125
+ fi
126
+ return 0
69
127
  fi
70
128
  local bot_pid
71
129
  bot_pid=$(cat "$pid_file")
@@ -79,7 +137,10 @@ kill_pid_file() {
79
137
  rm -f "$pid_file"
80
138
  local pid_dir
81
139
  pid_dir="$(dirname "$pid_file")"
82
- [[ -d "$pid_dir" ]] && clear_session_files "$pid_dir"
140
+ if [[ -d "$pid_dir" ]]; then
141
+ clear_session_files "$pid_dir"
142
+ fi
143
+ return 0
83
144
  }
84
145
 
85
146
  stop_single_worker() {
@@ -88,9 +149,11 @@ stop_single_worker() {
88
149
  log_dir="$(log_dir_for "$model_name" "$project_name")"
89
150
  pid_file="$log_dir/bot.pid"
90
151
  tmux_session="$(tmux_session_for "$project_name")"
152
+ graceful_shutdown_claudecode "$tmux_session" 15 || true
91
153
  kill_tty_sessions "$tmux_session"
92
154
  kill_pid_file "$pid_file"
93
155
  clear_session_files "$log_dir"
156
+ return 0
94
157
  }
95
158
 
96
159
  stop_all_workers() {
@@ -109,6 +172,7 @@ stop_all_workers() {
109
172
  if [[ -n "$sessions" ]]; then
110
173
  while IFS= read -r sess; do
111
174
  [[ -z "$sess" ]] && continue
175
+ graceful_shutdown_claudecode "$sess" 15 || true
112
176
  tmux -u kill-session -t "$sess" >/dev/null 2>&1 || true
113
177
  stopped=1
114
178
  done <<<"$sessions"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vibego
3
- Version: 0.2.11
3
+ Version: 0.2.12
4
4
  Summary: vibego CLI:用于初始化与管理 Telegram Master Bot 的工具
5
5
  Author: Hypha
6
6
  License-Expression: LicenseRef-Proprietary
@@ -1,16 +1,17 @@
1
- bot.py,sha256=q_aRgSBen4XThKdExfa-6NeX9R_Mgs0UjXYx32rsGzo,249827
1
+ bot.py,sha256=NPa3-Zb_20O8AjpQTUBDFfTZesh5kVEXRK17SbNpu_0,256916
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
5
5
  scripts/__init__.py,sha256=LVrXUkvWKoc6Sb47X5G0gbIxu5aJ2ARW-qJ14vwi5vM,65
6
+ scripts/bump_version.sh,sha256=a4uB8V8Y5LPsoqTCdzQKsEE8HhwpBmqRaQInG52LDig,4089
6
7
  scripts/log_writer.py,sha256=8euoMlRo7cbtHApbcEoJnwzLABxti-ovJWFLRN1oDQw,3843
7
8
  scripts/master_healthcheck.py,sha256=-X0VVsZ0AXaOb7izxTO_oyu23g_1jsirNdGIcP8nrSI,8321
8
9
  scripts/requirements.txt,sha256=QSt30DSSSHtfucTFPpc7twk9kLS5rVLNTcvDiagxrZg,62
9
- scripts/run_bot.sh,sha256=rM2Op_l4mP9asT3VoTwGQuHocwaxiGyQp2l1PLegsY4,4481
10
+ scripts/run_bot.sh,sha256=rN4K1nz041XBaUJmnBBKHS2cHmQf11vPNX8wf1hbVR4,4596
10
11
  scripts/start.sh,sha256=IsNjFWXX3qmFxx_7iiVEidTj9VHBUTkNNbsCPoNSL0E,6782
11
- scripts/start_tmux_codex.sh,sha256=4ko72SK7EDJmzXVuLBOz1_S1lLEmgIpLAVuD7mZhpdk,4018
12
+ scripts/start_tmux_codex.sh,sha256=xyLv29p924q-ysxvZYAP3T6VrqLPBPMBWo9QP7cuL50,4438
12
13
  scripts/stop_all.sh,sha256=FOz07gi2CI9sMHxBb8XkqHtxRYs3jt1RYgGrEi-htVg,4086
13
- scripts/stop_bot.sh,sha256=b9vvrke5jeyO18SEBFZxQD09Gf65BgGzeKtpM8sa60Y,3677
14
+ scripts/stop_bot.sh,sha256=ot6Sm0IYoXuRNslUVEflQmJKu5Agm-5xIUXXudJWyTM,5588
14
15
  scripts/test_deps_check.sh,sha256=AeSTucbNuNdOPSGvqVp0m31TVFDb0DmU8gSKg-Y5z2I,1696
15
16
  scripts/.venv/lib/python3.14/site-packages/pip/__init__.py,sha256=_lgs5Mfp0t7AGtI7sTVwxKWquz_vahWWH9kKO1cJusA,353
16
17
  scripts/.venv/lib/python3.14/site-packages/pip/__main__.py,sha256=WzbhHXTbSE6gBY19mNN9m4s5o_365LOvTYSgqgbdBhE,854
@@ -414,7 +415,7 @@ scripts/.venv/lib/python3.14/site-packages/pip/_vendor/urllib3/util/ssltransport
414
415
  scripts/.venv/lib/python3.14/site-packages/pip/_vendor/urllib3/util/timeout.py,sha256=cwq4dMk87mJHSBktK1miYJ-85G-3T3RmT20v7SFCpno,10168
415
416
  scripts/.venv/lib/python3.14/site-packages/pip/_vendor/urllib3/util/url.py,sha256=lCAE7M5myA8EDdW0sJuyyZhVB9K_j38ljWhHAnFaWoE,14296
416
417
  scripts/.venv/lib/python3.14/site-packages/pip/_vendor/urllib3/util/wait.py,sha256=fOX0_faozG2P7iVojQoE1mbydweNyTcm-hXEfFrTtLI,5403
417
- scripts/models/claudecode.sh,sha256=tCyvGVPUtewVPimC7q_QoDNkXypvZoSPTiRk8mf7DAw,1212
418
+ scripts/models/claudecode.sh,sha256=k8153NH6O925VvhXh2rywVRje9CyUyIurn42EetZHEI,1526
418
419
  scripts/models/codex.sh,sha256=_vp-v4L4eNz20JC2JIw1npE0Qz-HBUpdzyxLAtqridU,457
419
420
  scripts/models/common.sh,sha256=Xx6BaVqWSxpZUUHf6s6u_rkfbNts0UePeA9J9uoC_HA,2168
420
421
  scripts/models/gemini.sh,sha256=0fychnJL4uQJ1_wuc8O2sAQ4K0q3v7_Ku_9EOgUdocI,449
@@ -424,14 +425,14 @@ tasks/constants.py,sha256=tS1kZxBIUm3JJUMHm25XI-KHNUZl5NhbbuzjzL_rF-c,299
424
425
  tasks/fsm.py,sha256=rKXXLEieQQU4r2z_CZUvn1_70FXiZXBBugF40gpe_tQ,1476
425
426
  tasks/models.py,sha256=N_qqRBo9xMSV0vbn4k6bLBXT8C_dp_oTFUxvdx16ZQM,2459
426
427
  tasks/service.py,sha256=w_S_aWiVqRXzXEpimLDsuCCCX2lB5uDkff9aKThBw9c,41916
427
- vibego_cli/__init__.py,sha256=9Jk4Uc0BhWjiJHoQ1kI0WHFB-tawfl43z4PnmF8S1Gg,310
428
+ vibego_cli/__init__.py,sha256=qgEUIxQ50ynuK_Dcun8OGqFZe0ICJWheDr1_-6tT9bU,311
428
429
  vibego_cli/__main__.py,sha256=qqTrYmRRLe4361fMzbI3-CqpZ7AhTofIHmfp4ykrrBY,158
429
430
  vibego_cli/config.py,sha256=33WSORCfUIxrDtgASPEbVqVLBVNHh-RSFLpNy7tfc0s,2992
430
431
  vibego_cli/deps.py,sha256=1nRXI7Dd-S1hYE8DligzK5fIluQWETRUj4_OKL0DikQ,1419
431
432
  vibego_cli/main.py,sha256=e2W5Pb9U9rfmF-jNX9uIA3222lhM0GgcvSdFTDBZd2s,12086
432
433
  vibego_cli/data/worker_requirements.txt,sha256=QSt30DSSSHtfucTFPpc7twk9kLS5rVLNTcvDiagxrZg,62
433
- vibego-0.2.11.dist-info/METADATA,sha256=qdYmMPXO440tHnQCLv6SojrTvBsPqj2Pbi1sUrKp9KI,10475
434
- vibego-0.2.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
435
- vibego-0.2.11.dist-info/entry_points.txt,sha256=Lsy_zm-dlyxt8-9DL9blBReIwU2k22c8-kifr46ND1M,48
436
- vibego-0.2.11.dist-info/top_level.txt,sha256=R56CT3nW5H5v3ce0l3QDN4-C4qxTrNWzRTwrxnkDX4U,69
437
- vibego-0.2.11.dist-info/RECORD,,
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,,
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.1.0"
10
+ __version__ = "0.2.12"
11
11
 
12
12
  from .main import main # noqa: E402