remote-claude 1.0.8 → 1.0.9

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.
@@ -87,6 +87,8 @@ class LarkHandler:
87
87
  self._detached_slices: Dict[str, CardSlice] = {}
88
88
  # 正在启动中的会话名集合(防止并发点击触发竞态)
89
89
  self._starting_sessions: set = set()
90
+ # Session 健康检查后台任务
91
+ self._health_check_task: Optional[asyncio.Task] = None
90
92
  logger.info(
91
93
  f"LarkHandler 初始化: bindings={len(self._chat_bindings)}, "
92
94
  f"groups={len(self._group_chat_ids)}, "
@@ -161,6 +163,7 @@ class LarkHandler:
161
163
  async def _attach(self, chat_id: str, session_name: str,
162
164
  user_id: Optional[str] = None) -> bool:
163
165
  """统一 attach 逻辑(私聊/群聊共用)"""
166
+ self._ensure_health_check_started()
164
167
  logger.info(
165
168
  f"[ATTACH] 开始: chat_id={chat_id[:8]}..., session={session_name}, "
166
169
  f"old_session={self._chat_sessions.get(chat_id)}, "
@@ -244,6 +247,7 @@ class LarkHandler:
244
247
  async def handle_message(self, user_id: str, chat_id: str, text: str,
245
248
  chat_type: str = "p2p"):
246
249
  """处理用户消息(群聊/私聊统一路由)"""
250
+ self._ensure_health_check_started()
247
251
  logger.info(f"收到消息: user={user_id[:8]}..., chat={chat_id[:8]}..., type={chat_type}, text={text[:50]}")
248
252
  text = text.strip()
249
253
 
@@ -913,6 +917,68 @@ class LarkHandler:
913
917
  self._save_chat_bindings()
914
918
  self._save_group_chat_ids()
915
919
 
920
+ # ── Session 健康检查 ─────────────────────────────────────────────────────
921
+
922
+ HEALTH_CHECK_INTERVAL = 30 # 秒
923
+
924
+ def _ensure_health_check_started(self):
925
+ """懒启动健康检查后台任务(首次收到消息时调用)"""
926
+ if self._health_check_task is None or self._health_check_task.done():
927
+ self._health_check_task = asyncio.create_task(self._health_check_loop())
928
+ logger.info("Session 健康检查后台任务已启动")
929
+
930
+ async def _health_check_loop(self):
931
+ """定期检查绑定的 session 是否仍然存活"""
932
+ while True:
933
+ await asyncio.sleep(self.HEALTH_CHECK_INTERVAL)
934
+ try:
935
+ await self._session_health_check()
936
+ except Exception as e:
937
+ logger.error(f"健康检查异常: {e}", exc_info=True)
938
+
939
+ async def _session_health_check(self):
940
+ """扫描所有绑定关系,对已死亡 session 自动解散群聊、清理私聊绑定"""
941
+ bound_sessions = set(self._chat_bindings.values())
942
+ if not bound_sessions:
943
+ return
944
+
945
+ active = {s["name"] for s in list_active_sessions()}
946
+ dead_sessions = bound_sessions - active
947
+ if not dead_sessions:
948
+ return
949
+
950
+ for session_name in dead_sessions:
951
+ logger.info(f"[health-check] 检测到 session '{session_name}' 已关闭")
952
+
953
+ # 记录受影响的 chat_id(在 disband 修改 dict 之前)
954
+ affected_chats = [
955
+ cid for cid, sname in list(self._chat_bindings.items())
956
+ if sname == session_name
957
+ ]
958
+
959
+ # 解散群聊(复用已有逻辑,内部会清理群聊的绑定/轮询/bridge)
960
+ await self._disband_groups_for_session(session_name, source="health-check")
961
+
962
+ # 处理私聊绑定(非群聊的 chat_id)
963
+ cleaned_private = False
964
+ for cid in affected_chats:
965
+ if cid in self._group_chat_ids:
966
+ continue # 群聊已由 _disband_groups_for_session 处理
967
+ if self._chat_bindings.get(cid) != session_name:
968
+ continue # 已被其他逻辑清理
969
+ logger.info(f"[health-check] 清理私聊绑定: chat_id={cid[:8]}..., session={session_name}")
970
+ self._poller.stop(cid)
971
+ bridge = self._bridges.pop(cid, None)
972
+ if bridge:
973
+ await bridge.disconnect()
974
+ self._chat_sessions.pop(cid, None)
975
+ self._detached_slices.pop(cid, None)
976
+ self._remove_binding_by_chat(cid, force=True)
977
+ cleaned_private = True
978
+
979
+ if cleaned_private:
980
+ self._save_chat_bindings()
981
+
916
982
  async def _cmd_disband_group(self, user_id: str, chat_id: str, session_name: str,
917
983
  message_id: Optional[str] = None):
918
984
  """解散与指定会话绑定的专属群聊"""
@@ -1010,6 +1076,7 @@ class LarkHandler:
1010
1076
  发箭头键导航到目标选项,每步从共享内存读取 selected_value 确认是否到位,
1011
1077
  到位后发 Enter 确认。避免数字键在溢出选项上无效的问题。
1012
1078
  """
1079
+ self._ensure_health_check_started()
1013
1080
  logger.info(f"处理选项选择: user={user_id[:8]}..., option={option_value}, total={option_total}")
1014
1081
  _track_stats('lark', 'option_select',
1015
1082
  session_name=self._chat_sessions.get(chat_id, ''),
@@ -1307,6 +1374,9 @@ class LarkHandler:
1307
1374
 
1308
1375
  async def disconnect_all_for_shutdown(self) -> None:
1309
1376
  """lark stop 时清理所有活跃流式卡片(更新为已断开状态)"""
1377
+ # 停止健康检查
1378
+ if self._health_check_task and not self._health_check_task.done():
1379
+ self._health_check_task.cancel()
1310
1380
  logger.info(f"[SHUTDOWN] 开始清理: bridges={len(self._bridges)}, sessions={len(self._chat_sessions)}")
1311
1381
  chat_ids = list(self._bridges.keys())
1312
1382
  for chat_id in chat_ids:
@@ -11,6 +11,8 @@ import logging
11
11
  import os
12
12
  import signal
13
13
  import sys
14
+ import threading
15
+ import time
14
16
  import urllib.request
15
17
  from pathlib import Path
16
18
 
@@ -155,6 +157,7 @@ def handle_card_action(event: P2CardActionTrigger) -> P2CardActionTriggerRespons
155
157
  action_value = action.value or {}
156
158
 
157
159
  print(f"[Lark] 收到卡片动作: user={user_id[:8]}..., action={action_value}")
160
+ handler._ensure_health_check_started()
158
161
 
159
162
  # 检查用户白名单
160
163
  if not check_user_allowed(user_id):
@@ -399,6 +402,17 @@ class LarkBot:
399
402
  print("\n机器人已启动,等待消息...")
400
403
  print("在飞书中发送 /help 查看使用说明\n")
401
404
 
405
+ # 在 ws_client.start() 阻塞前,保存事件循环引用,通过守护线程延迟启动健康检查
406
+ # (无需等待消息,15 秒后 WS 已连接就自动启动)
407
+ _loop = asyncio.get_event_loop()
408
+ def _deferred_health_check_start():
409
+ time.sleep(15)
410
+ try:
411
+ _loop.call_soon_threadsafe(handler._ensure_health_check_started)
412
+ except Exception as e:
413
+ print(f"[Lark] 健康检查启动失败: {e}")
414
+ threading.Thread(target=_deferred_health_check_start, daemon=True).start()
415
+
402
416
  # 启动 WebSocket(阻塞)
403
417
  self.ws_client.start()
404
418
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remote-claude",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "双端共享 Claude CLI 工具",
5
5
  "bin": {
6
6
  "remote-claude": "bin/remote-claude",