remote-claude 0.1.6 → 0.1.7
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/card_builder.py +47 -44
- package/lark_client/lark_handler.py +91 -28
- package/lark_client/main.py +24 -5
- package/package.json +1 -1
- package/server/server.py +9 -9
|
@@ -773,58 +773,57 @@ def _build_session_list_elements(sessions: List[Dict], current_session: Optional
|
|
|
773
773
|
btn_label = "进入会话"
|
|
774
774
|
btn_type = "primary"
|
|
775
775
|
btn_action = "list_attach"
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
"weight": 5,
|
|
781
|
-
"elements": [{"tag": "markdown", "content": header_text}]
|
|
782
|
-
},
|
|
776
|
+
has_group = bool(session_groups and name in session_groups)
|
|
777
|
+
|
|
778
|
+
# 右列按钮(纵向堆叠)
|
|
779
|
+
right_buttons = [
|
|
783
780
|
{
|
|
784
|
-
"tag": "
|
|
785
|
-
"
|
|
786
|
-
"
|
|
787
|
-
"
|
|
788
|
-
"
|
|
789
|
-
|
|
790
|
-
"type": btn_type,
|
|
791
|
-
"behaviors": [{"type": "callback", "value": {
|
|
792
|
-
"action": btn_action, "session": name
|
|
793
|
-
}}]
|
|
794
|
-
}]
|
|
781
|
+
"tag": "button",
|
|
782
|
+
"text": {"tag": "plain_text", "content": btn_label},
|
|
783
|
+
"type": btn_type,
|
|
784
|
+
"behaviors": [{"type": "callback", "value": {
|
|
785
|
+
"action": btn_action, "session": name
|
|
786
|
+
}}]
|
|
795
787
|
},
|
|
796
788
|
{
|
|
797
|
-
"tag": "
|
|
798
|
-
"
|
|
799
|
-
"
|
|
800
|
-
"
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
}]
|
|
789
|
+
"tag": "button",
|
|
790
|
+
"text": {"tag": "plain_text", "content": "进入群聊" if has_group else "创建群聊"},
|
|
791
|
+
"type": "default",
|
|
792
|
+
"behaviors": [{"type": "open_url",
|
|
793
|
+
"default_url": f"https://applink.feishu.cn/client/chat/open?openChatId={session_groups[name]}",
|
|
794
|
+
"android_url": f"https://applink.feishu.cn/client/chat/open?openChatId={session_groups[name]}",
|
|
795
|
+
"ios_url": f"https://applink.feishu.cn/client/chat/open?openChatId={session_groups[name]}",
|
|
796
|
+
"pc_url": f"https://applink.feishu.cn/client/chat/open?openChatId={session_groups[name]}"}]
|
|
797
|
+
if has_group else
|
|
798
|
+
[{"type": "callback", "value": {"action": "list_new_group", "session": name}}]
|
|
808
799
|
},
|
|
809
800
|
]
|
|
810
|
-
if
|
|
811
|
-
|
|
812
|
-
"tag": "
|
|
813
|
-
"
|
|
814
|
-
"
|
|
815
|
-
"
|
|
816
|
-
"
|
|
817
|
-
|
|
818
|
-
"type": "danger",
|
|
819
|
-
"behaviors": [{"type": "callback", "value": {
|
|
820
|
-
"action": "list_disband_group", "session": name
|
|
821
|
-
}}]
|
|
822
|
-
}]
|
|
801
|
+
if has_group:
|
|
802
|
+
right_buttons.append({
|
|
803
|
+
"tag": "button",
|
|
804
|
+
"text": {"tag": "plain_text", "content": "解散群聊"},
|
|
805
|
+
"type": "danger",
|
|
806
|
+
"behaviors": [{"type": "callback", "value": {
|
|
807
|
+
"action": "list_disband_group", "session": name
|
|
808
|
+
}}]
|
|
823
809
|
})
|
|
824
810
|
elements.append({
|
|
825
811
|
"tag": "column_set",
|
|
826
812
|
"flex_mode": "none",
|
|
827
|
-
"columns":
|
|
813
|
+
"columns": [
|
|
814
|
+
{
|
|
815
|
+
"tag": "column",
|
|
816
|
+
"width": "weighted",
|
|
817
|
+
"weight": 3,
|
|
818
|
+
"elements": [{"tag": "markdown", "content": header_text}]
|
|
819
|
+
},
|
|
820
|
+
{
|
|
821
|
+
"tag": "column",
|
|
822
|
+
"width": "weighted",
|
|
823
|
+
"weight": 2,
|
|
824
|
+
"elements": right_buttons
|
|
825
|
+
},
|
|
826
|
+
]
|
|
828
827
|
})
|
|
829
828
|
elements.append({"tag": "hr"})
|
|
830
829
|
|
|
@@ -967,7 +966,11 @@ def build_dir_card(target, entries: List[Dict], sessions: List[Dict], tree: bool
|
|
|
967
966
|
"tag": "button",
|
|
968
967
|
"text": {"tag": "plain_text", "content": "进入群聊" if (session_groups and auto_session in session_groups) else "创建群聊"},
|
|
969
968
|
"type": "default",
|
|
970
|
-
"behaviors": [{"type": "open_url",
|
|
969
|
+
"behaviors": [{"type": "open_url",
|
|
970
|
+
"default_url": f"https://applink.feishu.cn/client/chat/open?openChatId={session_groups[auto_session]}",
|
|
971
|
+
"android_url": f"https://applink.feishu.cn/client/chat/open?openChatId={session_groups[auto_session]}",
|
|
972
|
+
"ios_url": f"https://applink.feishu.cn/client/chat/open?openChatId={session_groups[auto_session]}",
|
|
973
|
+
"pc_url": f"https://applink.feishu.cn/client/chat/open?openChatId={session_groups[auto_session]}"}]
|
|
971
974
|
if (session_groups and auto_session in session_groups) else
|
|
972
975
|
[{"type": "callback", "value": {
|
|
973
976
|
"action": "dir_new_group",
|
|
@@ -26,7 +26,6 @@ from .card_builder import (
|
|
|
26
26
|
build_help_card,
|
|
27
27
|
build_dir_card,
|
|
28
28
|
build_menu_card,
|
|
29
|
-
build_session_closed_card,
|
|
30
29
|
)
|
|
31
30
|
from .shared_memory_poller import SharedMemoryPoller, CardSlice
|
|
32
31
|
|
|
@@ -92,11 +91,17 @@ class LarkHandler:
|
|
|
92
91
|
|
|
93
92
|
async def _attach(self, chat_id: str, session_name: str) -> bool:
|
|
94
93
|
"""统一 attach 逻辑(私聊/群聊共用)"""
|
|
94
|
+
# 在断开旧连接之前,先更新旧流式卡片为已断开状态
|
|
95
|
+
old_session = self._chat_sessions.get(chat_id)
|
|
96
|
+
old_slice = self._poller.stop_and_get_active_slice(chat_id)
|
|
97
|
+
if old_slice and old_session:
|
|
98
|
+
await self._update_card_disconnected(chat_id, old_session, old_slice)
|
|
99
|
+
|
|
95
100
|
# 断开旧 bridge
|
|
96
101
|
old = self._bridges.pop(chat_id, None)
|
|
97
102
|
if old:
|
|
98
103
|
await old.disconnect()
|
|
99
|
-
|
|
104
|
+
# _poller.stop 已通过 stop_and_get_active_slice 完成
|
|
100
105
|
self._chat_sessions.pop(chat_id, None)
|
|
101
106
|
self._detached_slices.pop(chat_id, None)
|
|
102
107
|
|
|
@@ -132,19 +137,8 @@ class LarkHandler:
|
|
|
132
137
|
self._detached_slices.pop(chat_id, None)
|
|
133
138
|
self._remove_binding_by_chat(chat_id)
|
|
134
139
|
|
|
135
|
-
card = build_session_closed_card(session_name)
|
|
136
140
|
if active_slice:
|
|
137
|
-
|
|
138
|
-
success = await card_service.update_card(
|
|
139
|
-
card_id=active_slice.card_id,
|
|
140
|
-
sequence=active_slice.sequence + 1,
|
|
141
|
-
card_content=card,
|
|
142
|
-
)
|
|
143
|
-
if success:
|
|
144
|
-
return
|
|
145
|
-
except Exception as e:
|
|
146
|
-
logger.warning(f"_on_disconnect 就地更新失败: {e}")
|
|
147
|
-
await card_service.create_and_send_card(chat_id, card)
|
|
141
|
+
await self._update_card_disconnected(chat_id, session_name, active_slice)
|
|
148
142
|
|
|
149
143
|
# ── 消息入口 ────────────────────────────────────────────────────────────
|
|
150
144
|
|
|
@@ -398,6 +392,9 @@ class LarkHandler:
|
|
|
398
392
|
# 断开所有连接到此会话的 chat
|
|
399
393
|
for cid, sname in list(self._chat_sessions.items()):
|
|
400
394
|
if sname == session_name:
|
|
395
|
+
active_slice = self._poller.stop_and_get_active_slice(cid)
|
|
396
|
+
if active_slice:
|
|
397
|
+
await self._update_card_disconnected(cid, sname, active_slice)
|
|
401
398
|
await self._detach(cid)
|
|
402
399
|
self._remove_binding_by_chat(cid)
|
|
403
400
|
|
|
@@ -410,10 +407,44 @@ class LarkHandler:
|
|
|
410
407
|
async def _handle_list_detach(self, user_id: str, chat_id: str,
|
|
411
408
|
message_id: Optional[str] = None):
|
|
412
409
|
"""会话列表卡片中断开连接,就地刷新列表"""
|
|
410
|
+
session_name = self._chat_sessions.get(chat_id, "")
|
|
411
|
+
# 更新流式卡片为已断开状态
|
|
412
|
+
active_slice = self._poller.stop_and_get_active_slice(chat_id)
|
|
413
|
+
if active_slice and session_name:
|
|
414
|
+
await self._update_card_disconnected(chat_id, session_name, active_slice)
|
|
415
|
+
|
|
413
416
|
self._remove_binding_by_chat(chat_id)
|
|
414
|
-
await self._detach(chat_id)
|
|
417
|
+
await self._detach(chat_id) # bridge.disconnect + _poller.stop(幂等)
|
|
415
418
|
await self._cmd_list(user_id, chat_id, message_id=message_id)
|
|
416
419
|
|
|
420
|
+
async def _update_card_disconnected(self, chat_id: str, session_name: str,
|
|
421
|
+
active_slice: 'CardSlice') -> bool:
|
|
422
|
+
"""读取最新 blocks 并就地更新卡片为断开状态(disconnected=True)。Best-effort,不降级发新卡。"""
|
|
423
|
+
blocks = []
|
|
424
|
+
try:
|
|
425
|
+
import sys as _sys
|
|
426
|
+
_sys.path.insert(0, str(Path(__file__).parent.parent / "server"))
|
|
427
|
+
from shared_state import SharedStateReader, get_mq_path
|
|
428
|
+
mq_path = get_mq_path(session_name)
|
|
429
|
+
if mq_path.exists():
|
|
430
|
+
reader = SharedStateReader(session_name)
|
|
431
|
+
state = reader.read()
|
|
432
|
+
reader.close()
|
|
433
|
+
blocks = state.get("blocks", [])
|
|
434
|
+
except Exception:
|
|
435
|
+
pass
|
|
436
|
+
blocks_slice = blocks[active_slice.start_idx:]
|
|
437
|
+
card = build_stream_card(blocks_slice, disconnected=True, session_name=session_name)
|
|
438
|
+
try:
|
|
439
|
+
return await card_service.update_card(
|
|
440
|
+
card_id=active_slice.card_id,
|
|
441
|
+
sequence=active_slice.sequence + 1,
|
|
442
|
+
card_content=card,
|
|
443
|
+
)
|
|
444
|
+
except Exception as e:
|
|
445
|
+
logger.warning(f"_update_card_disconnected 失败 ({chat_id[:8]}...): {e}")
|
|
446
|
+
return False
|
|
447
|
+
|
|
417
448
|
async def _handle_stream_detach(self, user_id: str, chat_id: str,
|
|
418
449
|
session_name: str, message_id: Optional[str] = None):
|
|
419
450
|
"""流式卡片中断开连接,就地更新卡片为已断开状态"""
|
|
@@ -543,7 +574,8 @@ class LarkHandler:
|
|
|
543
574
|
card = build_dir_card(target, entries, sessions_info, tree=tree, session_groups=session_groups)
|
|
544
575
|
await self._send_or_update_card(chat_id, card, message_id)
|
|
545
576
|
|
|
546
|
-
async def _cmd_new_group(self, user_id: str, chat_id: str, args: str
|
|
577
|
+
async def _cmd_new_group(self, user_id: str, chat_id: str, args: str,
|
|
578
|
+
message_id: Optional[str] = None):
|
|
547
579
|
"""创建专属群聊并绑定 Claude 会话"""
|
|
548
580
|
session_name = args.strip()
|
|
549
581
|
if not session_name:
|
|
@@ -606,6 +638,8 @@ class LarkHandler:
|
|
|
606
638
|
f"✅ 已创建专属群「【{dir_label}】{config.BOT_NAME}」并已连接\n"
|
|
607
639
|
f"在群内直接发消息即可与 Claude 交互"
|
|
608
640
|
)
|
|
641
|
+
# 刷新会话列表卡片,使"创建群聊"按钮变为"进入群聊"
|
|
642
|
+
await self._cmd_list(user_id, chat_id, message_id=message_id)
|
|
609
643
|
except Exception as e:
|
|
610
644
|
logger.error(f"创建群失败: {e}")
|
|
611
645
|
await card_service.send_text(chat_id, f"创建群失败:{e}")
|
|
@@ -623,6 +657,7 @@ class LarkHandler:
|
|
|
623
657
|
|
|
624
658
|
import json as _json
|
|
625
659
|
import urllib.request
|
|
660
|
+
import urllib.error
|
|
626
661
|
from . import config
|
|
627
662
|
try:
|
|
628
663
|
token_resp = urllib.request.urlopen(
|
|
@@ -635,20 +670,39 @@ class LarkHandler:
|
|
|
635
670
|
)
|
|
636
671
|
token = _json.loads(token_resp.read())["tenant_access_token"]
|
|
637
672
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
673
|
+
feishu_ok = False
|
|
674
|
+
feishu_msg = ""
|
|
675
|
+
try:
|
|
676
|
+
disband_resp = urllib.request.urlopen(
|
|
677
|
+
urllib.request.Request(
|
|
678
|
+
f"https://open.feishu.cn/open-apis/im/v1/chats/{group_chat_id}",
|
|
679
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
680
|
+
method="DELETE"
|
|
681
|
+
), timeout=10
|
|
682
|
+
)
|
|
683
|
+
disband_data = _json.loads(disband_resp.read())
|
|
684
|
+
feishu_ok = disband_data.get("code") == 0
|
|
685
|
+
feishu_msg = disband_data.get("msg", "")
|
|
686
|
+
except urllib.error.HTTPError as e:
|
|
687
|
+
err_body = e.read().decode("utf-8", errors="replace")
|
|
688
|
+
try:
|
|
689
|
+
err_data = _json.loads(err_body)
|
|
690
|
+
feishu_ok = False
|
|
691
|
+
feishu_msg = f"code={err_data.get('code')} {err_data.get('msg', '')}"
|
|
692
|
+
except Exception:
|
|
693
|
+
feishu_ok = False
|
|
694
|
+
feishu_msg = f"HTTP {e.code}"
|
|
695
|
+
logger.error(f"解散群 API 失败: {feishu_msg}")
|
|
696
|
+
|
|
697
|
+
# 无论 Feishu delete 是否成功,都清理本地绑定
|
|
650
698
|
self._remove_binding_by_chat(group_chat_id)
|
|
651
699
|
await self._detach(group_chat_id)
|
|
700
|
+
|
|
701
|
+
if feishu_ok:
|
|
702
|
+
notice = "✅ 群聊已解散,绑定已解除"
|
|
703
|
+
else:
|
|
704
|
+
notice = f"⚠️ Feishu 群解散失败({feishu_msg}),已解除本地绑定。如需彻底解散请在飞书群内手动操作"
|
|
705
|
+
await card_service.send_text(chat_id, notice)
|
|
652
706
|
await self._cmd_list(user_id, chat_id, message_id=message_id)
|
|
653
707
|
except Exception as e:
|
|
654
708
|
logger.error(f"解散群失败: {e}")
|
|
@@ -830,6 +884,15 @@ class LarkHandler:
|
|
|
830
884
|
_walk(target, 0)
|
|
831
885
|
return entries
|
|
832
886
|
|
|
887
|
+
async def disconnect_all_for_shutdown(self) -> None:
|
|
888
|
+
"""lark stop 时清理所有活跃流式卡片(更新为已断开状态)"""
|
|
889
|
+
chat_ids = list(self._bridges.keys())
|
|
890
|
+
for chat_id in chat_ids:
|
|
891
|
+
session_name = self._chat_sessions.get(chat_id, "")
|
|
892
|
+
active_slice = self._poller.stop_and_get_active_slice(chat_id)
|
|
893
|
+
if active_slice and session_name:
|
|
894
|
+
await self._update_card_disconnected(chat_id, session_name, active_slice)
|
|
895
|
+
|
|
833
896
|
@staticmethod
|
|
834
897
|
def _get_pid_cwd(pid: int) -> Optional[str]:
|
|
835
898
|
"""获取进程的工作目录(macOS/Linux)"""
|
package/lark_client/main.py
CHANGED
|
@@ -30,6 +30,17 @@ from lark_oapi.event.callback.model.p2_card_action_trigger import (
|
|
|
30
30
|
from . import config
|
|
31
31
|
from .lark_handler import handler
|
|
32
32
|
|
|
33
|
+
|
|
34
|
+
async def _graceful_shutdown() -> None:
|
|
35
|
+
"""优雅关闭:更新所有活跃流式卡片为已断开状态后退出"""
|
|
36
|
+
try:
|
|
37
|
+
await handler.disconnect_all_for_shutdown()
|
|
38
|
+
except Exception as e:
|
|
39
|
+
print(f"[Lark] graceful shutdown 异常: {e}")
|
|
40
|
+
finally:
|
|
41
|
+
import os
|
|
42
|
+
os._exit(0)
|
|
43
|
+
|
|
33
44
|
def check_user_allowed(user_id: str) -> bool:
|
|
34
45
|
"""检查用户是否在白名单中"""
|
|
35
46
|
if not config.ENABLE_USER_WHITELIST:
|
|
@@ -148,7 +159,7 @@ def handle_card_action(event: P2CardActionTrigger) -> P2CardActionTriggerRespons
|
|
|
148
159
|
if action_type == "list_new_group":
|
|
149
160
|
session_name = action_value.get("session", "")
|
|
150
161
|
print(f"[Lark] list_new_group: session={session_name}")
|
|
151
|
-
asyncio.create_task(handler._cmd_new_group(user_id, chat_id, session_name))
|
|
162
|
+
asyncio.create_task(handler._cmd_new_group(user_id, chat_id, session_name, message_id=message_id))
|
|
152
163
|
return None
|
|
153
164
|
|
|
154
165
|
# 列表卡片:解散群聊
|
|
@@ -288,12 +299,20 @@ class LarkBot:
|
|
|
288
299
|
self.ws_client.start()
|
|
289
300
|
|
|
290
301
|
def _signal_handler(self, signum, frame):
|
|
291
|
-
"""
|
|
302
|
+
"""处理退出信号(SIGTERM / SIGINT)"""
|
|
292
303
|
print("\n正在关闭...")
|
|
293
304
|
self.running = False
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
305
|
+
# 调度异步清理(更新所有活跃卡片为已断开状态后退出)
|
|
306
|
+
try:
|
|
307
|
+
loop = asyncio.get_event_loop()
|
|
308
|
+
if loop.is_running():
|
|
309
|
+
loop.call_soon_threadsafe(
|
|
310
|
+
lambda: asyncio.ensure_future(_graceful_shutdown())
|
|
311
|
+
)
|
|
312
|
+
return
|
|
313
|
+
except Exception:
|
|
314
|
+
pass
|
|
315
|
+
sys.exit(0)
|
|
297
316
|
|
|
298
317
|
|
|
299
318
|
def main():
|
package/package.json
CHANGED
package/server/server.py
CHANGED
|
@@ -624,22 +624,22 @@ class ProxyServer:
|
|
|
624
624
|
raise
|
|
625
625
|
|
|
626
626
|
if pid == 0:
|
|
627
|
-
#
|
|
628
|
-
for k, v in _extra_env.items():
|
|
629
|
-
os.environ[k] = v
|
|
630
|
-
# 环境已加载到内存,立即删除快照文件(execvp 前销毁)
|
|
627
|
+
# 环境已加载到内存,立即删除快照文件(exec 前销毁)
|
|
631
628
|
try:
|
|
632
629
|
env_snapshot_path.unlink()
|
|
633
630
|
except Exception:
|
|
634
631
|
pass
|
|
632
|
+
# 以快照为权威来源完整替换子进程环境,确保 unset 的变量也消失
|
|
633
|
+
# 若 snapshot 加载失败(_extra_env 为空),降级使用当前进程环境
|
|
634
|
+
child_env = dict(_extra_env) if _extra_env else dict(os.environ)
|
|
635
635
|
# 恢复 TERM 以支持 kitty keyboard protocol(Shift+Enter 等扩展键)
|
|
636
636
|
# tmux 会将 TERM 改为 tmux-256color,导致 Claude CLI 不启用 kitty protocol
|
|
637
|
-
|
|
637
|
+
child_env['TERM'] = 'xterm-256color'
|
|
638
638
|
# 清除 tmux 标识变量(PTY 数据不经过 tmux,不应让 Claude CLI 误判终端环境)
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
os.
|
|
642
|
-
os._exit(1) #
|
|
639
|
+
child_env.pop('TMUX', None)
|
|
640
|
+
child_env.pop('TMUX_PANE', None)
|
|
641
|
+
os.execvpe("claude", ["claude"] + self.claude_args, child_env)
|
|
642
|
+
os._exit(1) # execvpe 失败时兜底退出
|
|
643
643
|
else:
|
|
644
644
|
# 父进程
|
|
645
645
|
self.master_fd = fd
|