remote-claude 0.1.7 → 0.2.0

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.
@@ -261,6 +261,7 @@ def _build_menu_button_row(session_name: Optional[str] = None, disconnected: boo
261
261
  "width": "auto",
262
262
  "elements": [{
263
263
  "tag": "button",
264
+ "name": "enter_submit",
264
265
  "text": {"tag": "plain_text", "content": "Enter ↵"},
265
266
  "type": "primary",
266
267
  "action_type": "form_submit",
@@ -290,6 +291,7 @@ def _build_menu_button_row(session_name: Optional[str] = None, disconnected: boo
290
291
  "width": "auto",
291
292
  "elements": [{
292
293
  "tag": "button",
294
+ "name": "enter_submit",
293
295
  "text": {"tag": "plain_text", "content": "Enter ↵"},
294
296
  "type": "primary",
295
297
  "action_type": "form_submit",
@@ -312,6 +314,7 @@ def _build_menu_button_row(session_name: Optional[str] = None, disconnected: boo
312
314
  ("Ctrl+O", {"action": "send_key", "key": "ctrl_o"}),
313
315
  ("Shift+Tab", {"action": "send_key", "key": "shift_tab"}),
314
316
  ("ESC", {"action": "send_key", "key": "esc"}),
317
+ ("(↹)×3", {"action": "send_key", "key": "shift_tab", "times": 3}),
315
318
  ]
316
319
 
317
320
  def _make_key_column(label, value):
@@ -871,7 +874,7 @@ def _dir_session_name(path: str) -> str:
871
874
  return name or "session"
872
875
 
873
876
 
874
- def build_dir_card(target, entries: List[Dict], sessions: List[Dict], tree: bool = False, session_groups: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
877
+ def build_dir_card(target, entries: List[Dict], sessions: List[Dict], tree: bool = False, session_groups: Optional[Dict[str, str]] = None, page: int = 0) -> Dict[str, Any]:
875
878
  """构建目录浏览卡片
876
879
 
877
880
  顶层目录(depth==0)带两个操作按钮:
@@ -906,9 +909,17 @@ def build_dir_card(target, entries: List[Dict], sessions: List[Dict], tree: bool
906
909
  })
907
910
  elements.append({"tag": "hr"})
908
911
 
909
- cap = 20
912
+ PER_PAGE = 12
910
913
  total = len(entries)
911
- shown = entries[:cap]
914
+ if tree:
915
+ # tree 模式不分页,直接展示全部(已有 max_items 上限)
916
+ shown = entries
917
+ page = 0
918
+ total_pages = 1
919
+ else:
920
+ total_pages = max(1, (total + PER_PAGE - 1) // PER_PAGE)
921
+ page = max(0, min(page, total_pages - 1))
922
+ shown = entries[page * PER_PAGE : (page + 1) * PER_PAGE]
912
923
 
913
924
  for entry in shown:
914
925
  name = entry["name"]
@@ -920,6 +931,22 @@ def build_dir_card(target, entries: List[Dict], sessions: List[Dict], tree: bool
920
931
 
921
932
  if is_dir and depth == 0:
922
933
  auto_session = _dir_session_name(full_path)
934
+ group_btn = {
935
+ "tag": "button",
936
+ "text": {"tag": "plain_text", "content": "进入群聊" if (session_groups and auto_session in session_groups) else "创建群聊"},
937
+ "type": "default",
938
+ "behaviors": [{"type": "open_url",
939
+ "default_url": f"https://applink.feishu.cn/client/chat/open?openChatId={session_groups[auto_session]}",
940
+ "android_url": f"https://applink.feishu.cn/client/chat/open?openChatId={session_groups[auto_session]}",
941
+ "ios_url": f"https://applink.feishu.cn/client/chat/open?openChatId={session_groups[auto_session]}",
942
+ "pc_url": f"https://applink.feishu.cn/client/chat/open?openChatId={session_groups[auto_session]}"}]
943
+ if (session_groups and auto_session in session_groups) else
944
+ [{"type": "callback", "value": {
945
+ "action": "dir_new_group",
946
+ "path": full_path,
947
+ "session_name": auto_session
948
+ }}]
949
+ }
923
950
  elements.append({
924
951
  "tag": "column_set",
925
952
  "flex_mode": "none",
@@ -927,65 +954,77 @@ def build_dir_card(target, entries: List[Dict], sessions: List[Dict], tree: bool
927
954
  {
928
955
  "tag": "column",
929
956
  "width": "weighted",
930
- "weight": 3,
931
- "elements": [{"tag": "markdown", "content": f"{icon} **{name}**"}]
932
- },
933
- {
934
- "tag": "column",
935
- "width": "weighted",
936
- "weight": 2,
957
+ "weight": 4,
937
958
  "elements": [{
938
- "tag": "button",
939
- "text": {"tag": "plain_text", "content": "📂 进入"},
940
- "type": "default",
959
+ "tag": "interactive_container",
960
+ "width": "fill",
961
+ "height": "auto",
941
962
  "behaviors": [{"type": "callback", "value": {
942
963
  "action": "dir_browse", "path": full_path
943
- }}]
964
+ }}],
965
+ "elements": [{"tag": "markdown", "content": f"📁 **{name}**"}]
944
966
  }]
945
967
  },
946
968
  {
947
969
  "tag": "column",
948
970
  "width": "weighted",
949
971
  "weight": 2,
950
- "elements": [{
951
- "tag": "button",
952
- "text": {"tag": "plain_text", "content": "🚀 在此启动"},
953
- "type": "primary",
954
- "behaviors": [{"type": "callback", "value": {
955
- "action": "dir_start",
956
- "path": full_path,
957
- "session_name": auto_session
958
- }}]
959
- }]
960
- },
961
- {
962
- "tag": "column",
963
- "width": "weighted",
964
- "weight": 2,
965
- "elements": [{
966
- "tag": "button",
967
- "text": {"tag": "plain_text", "content": "进入群聊" if (session_groups and auto_session in session_groups) else "创建群聊"},
968
- "type": "default",
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]}"}]
974
- if (session_groups and auto_session in session_groups) else
975
- [{"type": "callback", "value": {
976
- "action": "dir_new_group",
977
- "path": full_path,
978
- "session_name": auto_session
979
- }}]
980
- }]
972
+ "elements": [
973
+ {
974
+ "tag": "button",
975
+ "text": {"tag": "plain_text", "content": "Claude"},
976
+ "type": "primary",
977
+ "behaviors": [{"type": "callback", "value": {
978
+ "action": "dir_start",
979
+ "path": full_path,
980
+ "session_name": auto_session
981
+ }}]
982
+ },
983
+ group_btn
984
+ ]
981
985
  }
982
986
  ]
983
987
  })
988
+ elements.append({"tag": "hr"})
984
989
  else:
985
990
  elements.append({"tag": "markdown", "content": f"{indent}{icon} {name}"})
986
991
 
987
- if total > cap:
988
- elements.append({"tag": "markdown", "content": f"*...(共 {total} 项,仅显示前 {cap} 项)*"})
992
+ if not tree and total > PER_PAGE:
993
+ page_cols = []
994
+ if page > 0:
995
+ page_cols.append({
996
+ "tag": "column",
997
+ "width": "auto",
998
+ "elements": [{
999
+ "tag": "button",
1000
+ "text": {"tag": "plain_text", "content": "⬅️ 上一页"},
1001
+ "type": "default",
1002
+ "behaviors": [{"type": "callback", "value": {
1003
+ "action": "dir_page", "path": target_str, "page": page - 1
1004
+ }}]
1005
+ }]
1006
+ })
1007
+ page_cols.append({
1008
+ "tag": "column",
1009
+ "width": "weighted",
1010
+ "weight": 2,
1011
+ "vertical_align": "center",
1012
+ "elements": [{"tag": "markdown", "content": f"第 {page + 1}/{total_pages} 页"}]
1013
+ })
1014
+ if page < total_pages - 1:
1015
+ page_cols.append({
1016
+ "tag": "column",
1017
+ "width": "auto",
1018
+ "elements": [{
1019
+ "tag": "button",
1020
+ "text": {"tag": "plain_text", "content": "下一页 ➡️"},
1021
+ "type": "default",
1022
+ "behaviors": [{"type": "callback", "value": {
1023
+ "action": "dir_page", "path": target_str, "page": page + 1
1024
+ }}]
1025
+ }]
1026
+ })
1027
+ elements.append({"tag": "column_set", "flex_mode": "none", "columns": page_cols})
989
1028
 
990
1029
  elements.append({"tag": "hr"})
991
1030
  elements.append(_build_menu_button_only())
@@ -1093,17 +1132,6 @@ def build_menu_card(sessions: List[Dict], current_session: Optional[str] = None,
1093
1132
  "tag": "column_set",
1094
1133
  "flex_mode": "none",
1095
1134
  "columns": [
1096
- {
1097
- "tag": "column",
1098
- "width": "weighted",
1099
- "weight": 1,
1100
- "elements": [{
1101
- "tag": "button",
1102
- "text": {"tag": "plain_text", "content": "📖 帮助"},
1103
- "type": "default",
1104
- "behaviors": [{"type": "callback", "value": {"action": "menu_help"}}]
1105
- }]
1106
- },
1107
1135
  {
1108
1136
  "tag": "column",
1109
1137
  "width": "weighted",
@@ -22,6 +22,23 @@ from lark_oapi.api.cardkit.v1 import (
22
22
 
23
23
  from . import config
24
24
 
25
+
26
+ def _is_element_limit_error(msg: str) -> bool:
27
+ """判断飞书 API 返回的错误是否为元素超限"""
28
+ if not msg:
29
+ return False
30
+ lower = msg.lower()
31
+ return "element exceeds" in lower or "超限" in lower
32
+
33
+
34
+ class _ElementLimitResult:
35
+ """元素超限的哨兵返回值,__bool__ 为 False 兼容现有 if not success 逻辑"""
36
+ is_element_limit = True
37
+
38
+ def __bool__(self):
39
+ return False
40
+
41
+
25
42
  import sys as _sys
26
43
  _sys.path.insert(0, str(__import__('pathlib').Path(__file__).parent.parent))
27
44
  try:
@@ -193,6 +210,10 @@ class CardService:
193
210
  return True
194
211
  else:
195
212
  logger.warning(f"更新卡片失败(attempt={attempt+1}): card_id={card_id} seq={sequence} code={response.code} msg={response.msg}")
213
+ if _is_element_limit_error(response.msg):
214
+ # 元素超限是内容问题,重试无意义,直接返回哨兵值
215
+ logger.warning(f"检测到元素超限错误,跳过重试: card_id={card_id}")
216
+ return _ElementLimitResult()
196
217
  except Exception as e:
197
218
  logger.error(f"更新卡片异常(attempt={attempt+1}): card_id={card_id} seq={sequence} error={e}")
198
219
 
@@ -531,7 +531,7 @@ class LarkHandler:
531
531
  await self._send_or_update_card(chat_id, card, message_id)
532
532
 
533
533
  async def _cmd_ls(self, user_id: str, chat_id: str, args: str,
534
- tree: bool = False, message_id: Optional[str] = None):
534
+ tree: bool = False, message_id: Optional[str] = None, page: int = 0):
535
535
  """查看目录文件结构"""
536
536
  all_sessions = list_active_sessions()
537
537
  sessions_info = []
@@ -571,7 +571,7 @@ class LarkHandler:
571
571
  return
572
572
 
573
573
  session_groups = {sname: cid for cid, sname in self._chat_bindings.items() if cid.startswith("oc_")}
574
- card = build_dir_card(target, entries, sessions_info, tree=tree, session_groups=session_groups)
574
+ card = build_dir_card(target, entries, sessions_info, tree=tree, session_groups=session_groups, page=page)
575
575
  await self._send_or_update_card(chat_id, card, message_id)
576
576
 
577
577
  async def _cmd_new_group(self, user_id: str, chat_id: str, args: str,
@@ -854,7 +854,7 @@ class LarkHandler:
854
854
  })
855
855
  except PermissionError:
856
856
  pass
857
- return entries[:30]
857
+ return entries
858
858
 
859
859
  @staticmethod
860
860
  def _collect_tree_entries(target, max_depth: int = 2, max_items: int = 60) -> list:
@@ -176,6 +176,14 @@ def handle_card_action(event: P2CardActionTrigger) -> P2CardActionTriggerRespons
176
176
  asyncio.create_task(handler._cmd_ls(user_id, chat_id, path, message_id=message_id))
177
177
  return None
178
178
 
179
+ # 目录卡片:翻页
180
+ if action_type == "dir_page":
181
+ path = action_value.get("path", "")
182
+ page = int(action_value.get("page", 0))
183
+ print(f"[Lark] dir_page: path={path}, page={page}")
184
+ asyncio.create_task(handler._cmd_ls(user_id, chat_id, path, message_id=message_id, page=page))
185
+ return None
186
+
179
187
  # 目录卡片:在该目录创建新 Claude 会话
180
188
  if action_type == "dir_start":
181
189
  path = action_value.get("path", "")
@@ -230,8 +238,13 @@ def handle_card_action(event: P2CardActionTrigger) -> P2CardActionTriggerRespons
230
238
  # 快捷键按钮(callback 模式)
231
239
  if action_type == "send_key":
232
240
  key_name = action_value.get("key", "")
233
- print(f"[Lark] send_key: key={key_name}")
234
- asyncio.create_task(handler.send_raw_key(user_id, chat_id, key_name))
241
+ times = action_value.get("times", 1)
242
+ print(f"[Lark] send_key: key={key_name}" + (f" ×{times}" if times > 1 else ""))
243
+ async def _multi_send(k=key_name, t=times):
244
+ for _ in range(t):
245
+ await handler.send_raw_key(user_id, chat_id, k)
246
+ await asyncio.sleep(0.15)
247
+ asyncio.create_task(_multi_send())
235
248
  return None
236
249
 
237
250
  # 各卡片底部菜单按钮:辅助卡片就地→菜单,流式卡片降级新卡
@@ -247,7 +247,13 @@ class SharedMemoryPoller:
247
247
  card_content=card_dict,
248
248
  )
249
249
 
250
- if not success:
250
+ if getattr(success, 'is_element_limit', False):
251
+ # 元素超限:冻结旧卡 + 推新流式卡
252
+ await self._handle_element_limit(
253
+ tracker, blocks, status_line, bottom_bar, agent_panel, option_block
254
+ )
255
+ return
256
+ elif not success:
251
257
  # 降级:创建新卡片替代
252
258
  logger.warning(
253
259
  f"update_card 失败 card_id={active.card_id} seq={active.sequence},降级为新卡片"
@@ -305,6 +311,48 @@ class SharedMemoryPoller:
305
311
  else:
306
312
  logger.warning(f"create_card 失败 session={tracker.session_name}")
307
313
 
314
+ async def _handle_element_limit(
315
+ self, tracker: StreamTracker, blocks: List[dict],
316
+ status_line: Optional[dict], bottom_bar: Optional[dict],
317
+ agent_panel: Optional[dict] = None,
318
+ option_block: Optional[dict] = None,
319
+ ) -> None:
320
+ """元素超限:冻结旧卡片 + 推送新流式卡片"""
321
+ active = tracker.cards[-1]
322
+ logger.warning(f"元素超限,冻结卡片 {active.card_id} 并推新卡")
323
+
324
+ # 1. 冻结旧卡片(灰色 header,无状态区和按钮)
325
+ from .card_builder import build_stream_card
326
+ blocks_slice = blocks[active.start_idx:]
327
+ frozen_card = build_stream_card(blocks_slice, None, None, is_frozen=True)
328
+ active.sequence += 1
329
+ await self._card_service.update_card(active.card_id, active.sequence, frozen_card)
330
+ active.frozen = True
331
+ _track_stats('card', 'freeze', session_name=tracker.session_name,
332
+ chat_id=tracker.chat_id)
333
+
334
+ # 2. 创建新流式卡片,从最近 INITIAL_WINDOW 个 blocks 开始(重置窗口)
335
+ new_start = max(0, len(blocks) - INITIAL_WINDOW)
336
+ new_blocks = blocks[new_start:]
337
+ if not new_blocks and not status_line and not bottom_bar:
338
+ return
339
+ new_card_dict = build_stream_card(
340
+ new_blocks, status_line, bottom_bar,
341
+ agent_panel=agent_panel, option_block=option_block,
342
+ session_name=tracker.session_name,
343
+ )
344
+ new_card_id = await self._card_service.create_card(new_card_dict)
345
+ if new_card_id:
346
+ await self._card_service.send_card(tracker.chat_id, new_card_id)
347
+ tracker.cards.append(CardSlice(card_id=new_card_id, start_idx=new_start))
348
+ tracker.content_hash = self._compute_hash(new_blocks, status_line, bottom_bar, agent_panel, option_block)
349
+ _track_stats('card', 'create', session_name=tracker.session_name,
350
+ chat_id=tracker.chat_id)
351
+ logger.info(
352
+ f"[ELEMENT_LIMIT_SPLIT] session={tracker.session_name} "
353
+ f"new_start={new_start} blocks={len(new_blocks)} card_id={new_card_id}"
354
+ )
355
+
308
356
  async def _freeze_and_split(
309
357
  self, tracker: StreamTracker, blocks: List[dict],
310
358
  status_line: Optional[dict], bottom_bar: Optional[dict],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remote-claude",
3
- "version": "0.1.7",
3
+ "version": "0.2.0",
4
4
  "description": "双端共享 Claude CLI 工具",
5
5
  "bin": {
6
6
  "remote-claude": "bin/remote-claude",