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.
- package/lark_client/lark_handler.py +70 -0
- package/lark_client/main.py +14 -0
- package/package.json +1 -1
|
@@ -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:
|
package/lark_client/main.py
CHANGED
|
@@ -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
|
|