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.
@@ -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 _cmd_disband_group(self, user_id: str, chat_id: str, session_name: str,
688
- message_id: Optional[str] = None):
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
- feishu_ok = disband_data.get("code") == 0
725
- feishu_msg = disband_data.get("msg", "")
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
- feishu_ok = False
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
- feishu_ok = False
734
- feishu_msg = f"HTTP {e.code}"
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 是否成功,都清理本地绑定
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remote-claude",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "双端共享 Claude CLI 工具",
5
5
  "bin": {
6
6
  "remote-claude": "bin/remote-claude",
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
- # 使用 asyncio 读取
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(data)
736
+ self.history.append(coalesced)
724
737
  # 广播给所有客户端
725
- await self._broadcast_output(data)
738
+ await self._broadcast_output(coalesced)
726
739
  elif data is None:
727
740
  # 暂时无数据(BlockingIOError),稍等继续
728
741
  await asyncio.sleep(0.01)