remote-claude 0.2.8 → 0.2.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/card_builder.py +12 -0
- package/lark_client/lark_handler.py +34 -21
- package/lark_client/main.py +7 -0
- package/package.json +1 -1
- package/server/server.py +16 -3
|
@@ -850,6 +850,18 @@ def _build_session_list_elements(sessions: List[Dict], current_session: Optional
|
|
|
850
850
|
"action": "list_disband_group", "session": name
|
|
851
851
|
}}]
|
|
852
852
|
})
|
|
853
|
+
right_buttons.append({
|
|
854
|
+
"tag": "button",
|
|
855
|
+
"text": {"tag": "plain_text", "content": "🗑️ 关闭"},
|
|
856
|
+
"type": "danger",
|
|
857
|
+
"confirm": {
|
|
858
|
+
"title": {"tag": "plain_text", "content": "确认关闭会话"},
|
|
859
|
+
"text": {"tag": "plain_text", "content": f"确定要关闭「{name}」吗?此操作不可撤销。"}
|
|
860
|
+
},
|
|
861
|
+
"behaviors": [{"type": "callback", "value": {
|
|
862
|
+
"action": "list_kill", "session": name
|
|
863
|
+
}}]
|
|
864
|
+
})
|
|
853
865
|
elements.append({
|
|
854
866
|
"tag": "column_set",
|
|
855
867
|
"flex_mode": "none",
|
|
@@ -401,7 +401,8 @@ class LarkHandler:
|
|
|
401
401
|
logger.error(f"启动并创建群聊失败: {e}")
|
|
402
402
|
await card_service.send_text(chat_id, f"操作失败:{e}")
|
|
403
403
|
|
|
404
|
-
async def _cmd_kill(self, user_id: str, chat_id: str, args: str
|
|
404
|
+
async def _cmd_kill(self, user_id: str, chat_id: str, args: str,
|
|
405
|
+
message_id: Optional[str] = None):
|
|
405
406
|
"""终止会话"""
|
|
406
407
|
from utils.session import cleanup_session, tmux_session_exists, tmux_kill_session
|
|
407
408
|
|
|
@@ -415,6 +416,13 @@ class LarkHandler:
|
|
|
415
416
|
await card_service.send_text(chat_id, f"错误: 会话 '{session_name}' 不存在")
|
|
416
417
|
return
|
|
417
418
|
|
|
419
|
+
# 解散绑定该会话的专属群聊(必须在断开连接之前,否则 _chat_bindings 已被清除)
|
|
420
|
+
for cid in list(self._group_chat_ids):
|
|
421
|
+
if self._chat_bindings.get(cid) == session_name:
|
|
422
|
+
ok, err = await self._disband_group_via_api(cid)
|
|
423
|
+
if not ok:
|
|
424
|
+
logger.warning(f"关闭会话时解散群 {cid} 失败: {err}")
|
|
425
|
+
|
|
418
426
|
# 断开所有连接到此会话的 chat
|
|
419
427
|
for cid, sname in list(self._chat_sessions.items()):
|
|
420
428
|
if sname == session_name:
|
|
@@ -436,6 +444,7 @@ class LarkHandler:
|
|
|
436
444
|
cleanup_session(session_name)
|
|
437
445
|
|
|
438
446
|
await card_service.send_text(chat_id, f"✅ 会话 '{session_name}' 已终止")
|
|
447
|
+
await self._cmd_list(user_id, chat_id, message_id=message_id)
|
|
439
448
|
|
|
440
449
|
async def _handle_list_detach(self, user_id: str, chat_id: str,
|
|
441
450
|
message_id: Optional[str] = None):
|
|
@@ -684,17 +693,8 @@ class LarkHandler:
|
|
|
684
693
|
logger.error(f"创建群失败: {e}")
|
|
685
694
|
await card_service.send_text(chat_id, f"创建群失败:{e}")
|
|
686
695
|
|
|
687
|
-
async def
|
|
688
|
-
|
|
689
|
-
"""解散与指定会话绑定的专属群聊"""
|
|
690
|
-
group_chat_id = next(
|
|
691
|
-
(cid for cid, sname in self._chat_bindings.items() if sname == session_name and cid.startswith("oc_")),
|
|
692
|
-
None
|
|
693
|
-
)
|
|
694
|
-
if not group_chat_id:
|
|
695
|
-
await card_service.send_text(chat_id, f"会话 '{session_name}' 没有绑定群聊")
|
|
696
|
-
return
|
|
697
|
-
|
|
696
|
+
async def _disband_group_via_api(self, group_chat_id: str) -> tuple:
|
|
697
|
+
"""调用飞书 API 解散群聊,返回 (ok: bool, err_msg: str)"""
|
|
698
698
|
import json as _json
|
|
699
699
|
import urllib.request
|
|
700
700
|
import urllib.error
|
|
@@ -709,9 +709,6 @@ class LarkHandler:
|
|
|
709
709
|
), timeout=10
|
|
710
710
|
)
|
|
711
711
|
token = _json.loads(token_resp.read())["tenant_access_token"]
|
|
712
|
-
|
|
713
|
-
feishu_ok = False
|
|
714
|
-
feishu_msg = ""
|
|
715
712
|
try:
|
|
716
713
|
disband_resp = urllib.request.urlopen(
|
|
717
714
|
urllib.request.Request(
|
|
@@ -721,17 +718,33 @@ class LarkHandler:
|
|
|
721
718
|
), timeout=10
|
|
722
719
|
)
|
|
723
720
|
disband_data = _json.loads(disband_resp.read())
|
|
724
|
-
|
|
725
|
-
|
|
721
|
+
if disband_data.get("code") == 0:
|
|
722
|
+
return True, ""
|
|
723
|
+
return False, disband_data.get("msg", "")
|
|
726
724
|
except urllib.error.HTTPError as e:
|
|
727
725
|
err_body = e.read().decode("utf-8", errors="replace")
|
|
728
726
|
try:
|
|
729
727
|
err_data = _json.loads(err_body)
|
|
730
|
-
|
|
731
|
-
feishu_msg = f"code={err_data.get('code')} {err_data.get('msg', '')}"
|
|
728
|
+
return False, f"code={err_data.get('code')} {err_data.get('msg', '')}"
|
|
732
729
|
except Exception:
|
|
733
|
-
|
|
734
|
-
|
|
730
|
+
return False, f"HTTP {e.code}"
|
|
731
|
+
except Exception as e:
|
|
732
|
+
return False, str(e)
|
|
733
|
+
|
|
734
|
+
async def _cmd_disband_group(self, user_id: str, chat_id: str, session_name: str,
|
|
735
|
+
message_id: Optional[str] = None):
|
|
736
|
+
"""解散与指定会话绑定的专属群聊"""
|
|
737
|
+
group_chat_id = next(
|
|
738
|
+
(cid for cid, sname in self._chat_bindings.items() if sname == session_name and cid.startswith("oc_")),
|
|
739
|
+
None
|
|
740
|
+
)
|
|
741
|
+
if not group_chat_id:
|
|
742
|
+
await card_service.send_text(chat_id, f"会话 '{session_name}' 没有绑定群聊")
|
|
743
|
+
return
|
|
744
|
+
|
|
745
|
+
try:
|
|
746
|
+
feishu_ok, feishu_msg = await self._disband_group_via_api(group_chat_id)
|
|
747
|
+
if not feishu_ok:
|
|
735
748
|
logger.error(f"解散群 API 失败: {feishu_msg}")
|
|
736
749
|
|
|
737
750
|
# 无论 Feishu delete 是否成功,都清理本地绑定
|
package/lark_client/main.py
CHANGED
|
@@ -169,6 +169,13 @@ def handle_card_action(event: P2CardActionTrigger) -> P2CardActionTriggerRespons
|
|
|
169
169
|
asyncio.create_task(handler._cmd_disband_group(user_id, chat_id, session_name, message_id=message_id))
|
|
170
170
|
return None
|
|
171
171
|
|
|
172
|
+
# 列表卡片:关闭会话
|
|
173
|
+
if action_type == "list_kill":
|
|
174
|
+
session_name = action_value.get("session", "")
|
|
175
|
+
print(f"[Lark] list_kill: session={session_name}")
|
|
176
|
+
asyncio.create_task(handler._cmd_kill(user_id, chat_id, session_name, message_id=message_id))
|
|
177
|
+
return None
|
|
178
|
+
|
|
172
179
|
# 目录卡片:进入子目录(继续浏览,就地更新原卡片)
|
|
173
180
|
if action_type == "dir_browse":
|
|
174
181
|
path = action_value.get("path", "")
|
package/package.json
CHANGED
package/server/server.py
CHANGED
|
@@ -708,21 +708,34 @@ class ProxyServer:
|
|
|
708
708
|
cli_label = self.cli_type.capitalize()
|
|
709
709
|
print(f"[Server] {cli_label} 已启动 (PID: {pid}, PTY: {self.PTY_COLS}×{self.PTY_ROWS})")
|
|
710
710
|
|
|
711
|
+
_COALESCE_MAX = 64 * 1024 # 64KB,防止单次广播过大
|
|
712
|
+
|
|
711
713
|
async def _read_pty(self):
|
|
712
714
|
"""读取 PTY 输出并广播"""
|
|
713
715
|
loop = asyncio.get_event_loop()
|
|
714
716
|
|
|
715
717
|
while self.running and self.master_fd is not None:
|
|
716
718
|
try:
|
|
717
|
-
#
|
|
719
|
+
# 第一次 read(在线程池中,可能阻塞等待数据)
|
|
718
720
|
data = await loop.run_in_executor(
|
|
719
721
|
None, self._read_pty_sync
|
|
720
722
|
)
|
|
721
723
|
if data:
|
|
724
|
+
# 贪婪合并:非阻塞读取紧接数据,合并为一次广播
|
|
725
|
+
buf = bytearray(data)
|
|
726
|
+
while len(buf) < self._COALESCE_MAX:
|
|
727
|
+
try:
|
|
728
|
+
more = os.read(self.master_fd, 4096)
|
|
729
|
+
if not more:
|
|
730
|
+
break
|
|
731
|
+
buf.extend(more)
|
|
732
|
+
except (BlockingIOError, OSError):
|
|
733
|
+
break
|
|
734
|
+
coalesced = bytes(buf)
|
|
722
735
|
# 保存到历史
|
|
723
|
-
self.history.append(
|
|
736
|
+
self.history.append(coalesced)
|
|
724
737
|
# 广播给所有客户端
|
|
725
|
-
await self._broadcast_output(
|
|
738
|
+
await self._broadcast_output(coalesced)
|
|
726
739
|
elif data is None:
|
|
727
740
|
# 暂时无数据(BlockingIOError),稍等继续
|
|
728
741
|
await asyncio.sleep(0.01)
|