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
|
-
|
|
912
|
+
PER_PAGE = 12
|
|
910
913
|
total = len(entries)
|
|
911
|
-
|
|
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":
|
|
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": "
|
|
939
|
-
"
|
|
940
|
-
"
|
|
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
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
"
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
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 >
|
|
988
|
-
|
|
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
|
|
857
|
+
return entries
|
|
858
858
|
|
|
859
859
|
@staticmethod
|
|
860
860
|
def _collect_tree_entries(target, max_depth: int = 2, max_items: int = 60) -> list:
|
package/lark_client/main.py
CHANGED
|
@@ -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
|
-
|
|
234
|
-
|
|
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
|
|
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],
|