remote-claude 0.2.7 → 0.2.8
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/.env.example +4 -0
- package/init.sh +21 -23
- package/lark_client/card_builder.py +110 -40
- package/lark_client/config.py +3 -0
- package/lark_client/lark_handler.py +26 -16
- package/lark_client/main.py +7 -0
- package/lark_client/shared_memory_poller.py +29 -7
- package/package.json +1 -2
- package/remote_claude.py +9 -1
- package/scripts/completion.sh +5 -3
- package/server/component_parser.py +33 -1193
- package/server/server.py +54 -7
- package/server/shared_state.py +5 -0
- package/utils/components.py +17 -1
- package/uv.lock +0 -703
package/.env.example
CHANGED
package/init.sh
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
|
|
3
|
-
set -e
|
|
4
|
-
|
|
5
3
|
# 颜色定义
|
|
6
4
|
RED=$'\033[0;31m'
|
|
7
5
|
GREEN=$'\033[0;32m'
|
|
@@ -128,7 +126,7 @@ check_uv() {
|
|
|
128
126
|
# 方式五:brew(macOS 备用)
|
|
129
127
|
if ! command -v uv &> /dev/null && [[ "$OS" == "Darwin" ]] && command -v brew &> /dev/null; then
|
|
130
128
|
print_warning "尝试 brew install uv..."
|
|
131
|
-
brew install uv
|
|
129
|
+
brew install uv 2>/dev/null || true
|
|
132
130
|
fi
|
|
133
131
|
|
|
134
132
|
if command -v uv &> /dev/null; then
|
|
@@ -181,18 +179,18 @@ check_tmux() {
|
|
|
181
179
|
fi
|
|
182
180
|
print_success "Homebrew 安装成功"
|
|
183
181
|
fi
|
|
184
|
-
brew install tmux
|
|
182
|
+
brew install tmux 2>/dev/null || true
|
|
185
183
|
elif [[ "$OS" == "Linux" ]]; then
|
|
186
184
|
if command -v apt-get &> /dev/null; then
|
|
187
|
-
sudo apt-get update && sudo apt-get install -y tmux
|
|
185
|
+
sudo apt-get update && sudo apt-get install -y tmux || true
|
|
188
186
|
elif command -v yum &> /dev/null; then
|
|
189
|
-
sudo yum install -y tmux
|
|
187
|
+
sudo yum install -y tmux || true
|
|
190
188
|
elif command -v pacman &> /dev/null; then
|
|
191
|
-
sudo pacman -Sy --noconfirm tmux
|
|
189
|
+
sudo pacman -Sy --noconfirm tmux || true
|
|
192
190
|
elif command -v apk &> /dev/null; then
|
|
193
|
-
sudo apk add --no-cache tmux
|
|
191
|
+
sudo apk add --no-cache tmux || true
|
|
194
192
|
elif command -v zypper &> /dev/null; then
|
|
195
|
-
sudo zypper install -y tmux
|
|
193
|
+
sudo zypper install -y tmux || true
|
|
196
194
|
else
|
|
197
195
|
print_warning "无法识别包管理器,尝试从源码编译 tmux..."
|
|
198
196
|
install_tmux_from_source
|
|
@@ -212,10 +210,10 @@ check_tmux() {
|
|
|
212
210
|
if [[ "$OS" == "Darwin" ]]; then
|
|
213
211
|
brew install libevent ncurses pkg-config bison 2>/dev/null || true
|
|
214
212
|
elif command -v apt-get &> /dev/null; then
|
|
215
|
-
sudo apt-get install -y build-essential libevent-dev libncurses5-dev libncursesw5-dev bison pkg-config
|
|
213
|
+
sudo apt-get install -y build-essential libevent-dev libncurses5-dev libncursesw5-dev bison pkg-config || true
|
|
216
214
|
elif command -v yum &> /dev/null; then
|
|
217
|
-
sudo yum groupinstall -y "Development Tools"
|
|
218
|
-
sudo yum install -y libevent-devel ncurses-devel bison
|
|
215
|
+
sudo yum groupinstall -y "Development Tools" || true
|
|
216
|
+
sudo yum install -y libevent-devel ncurses-devel bison || true
|
|
219
217
|
fi
|
|
220
218
|
|
|
221
219
|
# 确定安装前缀
|
|
@@ -249,9 +247,9 @@ check_tmux() {
|
|
|
249
247
|
fi
|
|
250
248
|
|
|
251
249
|
if [[ "$PREFIX" == "/usr/local" ]]; then
|
|
252
|
-
sudo make -C "$SRC_DIR" install
|
|
250
|
+
sudo make -C "$SRC_DIR" install || { WARNINGS+=("tmux make install 失败,请手动安装 tmux ${REQUIRED_MAJOR}.${REQUIRED_MINOR}+"); return; }
|
|
253
251
|
else
|
|
254
|
-
make -C "$SRC_DIR" install
|
|
252
|
+
make -C "$SRC_DIR" install || { WARNINGS+=("tmux make install 失败,请手动安装 tmux ${REQUIRED_MAJOR}.${REQUIRED_MINOR}+"); return; }
|
|
255
253
|
# 若 $HOME/.local/bin 不在 PATH 中,自动写入 shell 配置
|
|
256
254
|
if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then
|
|
257
255
|
export PATH="$HOME/.local/bin:$PATH"
|
|
@@ -363,9 +361,9 @@ install_dependencies() {
|
|
|
363
361
|
|
|
364
362
|
print_info "正在通过 uv 同步依赖..."
|
|
365
363
|
if $NPM_MODE; then
|
|
366
|
-
uv sync
|
|
364
|
+
uv sync || { print_error "依赖安装失败"; exit 1; }
|
|
367
365
|
else
|
|
368
|
-
uv sync
|
|
366
|
+
uv sync || { print_error "依赖安装失败"; exit 1; }
|
|
369
367
|
fi
|
|
370
368
|
|
|
371
369
|
print_success "依赖安装完成"
|
|
@@ -451,7 +449,7 @@ configure_shell() {
|
|
|
451
449
|
print_header "安装快捷命令"
|
|
452
450
|
|
|
453
451
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
454
|
-
chmod +x "$SCRIPT_DIR/bin/cla" "$SCRIPT_DIR/bin/cl" "$SCRIPT_DIR/bin/remote-claude"
|
|
452
|
+
chmod +x "$SCRIPT_DIR/bin/cla" "$SCRIPT_DIR/bin/cl" "$SCRIPT_DIR/bin/remote-claude" 2>/dev/null || true
|
|
455
453
|
|
|
456
454
|
# 优先 /usr/local/bin,权限不够则选 ~/bin 或 ~/.local/bin 中已在 PATH 里的
|
|
457
455
|
BIN_DIR="/usr/local/bin"
|
|
@@ -479,12 +477,12 @@ configure_shell() {
|
|
|
479
477
|
fi
|
|
480
478
|
fi
|
|
481
479
|
mkdir -p "$BIN_DIR"
|
|
482
|
-
ln -sf "$SCRIPT_DIR/bin/cla"
|
|
483
|
-
ln -sf "$SCRIPT_DIR/bin/cl"
|
|
484
|
-
ln -sf "$SCRIPT_DIR/bin/remote-claude" "$BIN_DIR/remote-claude"
|
|
480
|
+
ln -sf "$SCRIPT_DIR/bin/cla" "$BIN_DIR/cla" 2>/dev/null || true
|
|
481
|
+
ln -sf "$SCRIPT_DIR/bin/cl" "$BIN_DIR/cl" 2>/dev/null || true
|
|
482
|
+
ln -sf "$SCRIPT_DIR/bin/remote-claude" "$BIN_DIR/remote-claude" 2>/dev/null || true
|
|
485
483
|
else
|
|
486
|
-
ln -sf "$SCRIPT_DIR/bin/cl"
|
|
487
|
-
ln -sf "$SCRIPT_DIR/bin/remote-claude" "$BIN_DIR/remote-claude"
|
|
484
|
+
ln -sf "$SCRIPT_DIR/bin/cl" "$BIN_DIR/cl" 2>/dev/null || true
|
|
485
|
+
ln -sf "$SCRIPT_DIR/bin/remote-claude" "$BIN_DIR/remote-claude" 2>/dev/null || true
|
|
488
486
|
fi
|
|
489
487
|
|
|
490
488
|
print_success "已安装 cla、cl 和 remote-claude 到 $BIN_DIR"
|
|
@@ -525,7 +523,7 @@ restart_lark_client() {
|
|
|
525
523
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
526
524
|
print_info "正在重启飞书客户端..."
|
|
527
525
|
cd "$SCRIPT_DIR"
|
|
528
|
-
uv run python3 remote_claude.py lark restart
|
|
526
|
+
uv run python3 remote_claude.py lark restart || { WARNINGS+=("飞书客户端重启失败,请手动运行: uv run python3 remote_claude.py lark restart"); return; }
|
|
529
527
|
print_success "飞书客户端已重启"
|
|
530
528
|
}
|
|
531
529
|
|
|
@@ -17,6 +17,12 @@ from typing import Dict, Any, List, Optional
|
|
|
17
17
|
|
|
18
18
|
_cb_logger = logging.getLogger('CardBuilder')
|
|
19
19
|
|
|
20
|
+
# CLI 类型 → 显示名称映射(用于卡片标题中的"就绪"文案)
|
|
21
|
+
CLI_NAMES: Dict[str, str] = {
|
|
22
|
+
"claude": "Claude",
|
|
23
|
+
"codex": "Codex",
|
|
24
|
+
}
|
|
25
|
+
|
|
20
26
|
# 版本号:从 package.json 读取,import 时只读一次
|
|
21
27
|
try:
|
|
22
28
|
_pkg = _pl.Path(__file__).parent.parent / "package.json"
|
|
@@ -539,6 +545,17 @@ def _render_block_colored(block_dict: dict) -> Optional[str]:
|
|
|
539
545
|
parts.append("🔐 权限确认")
|
|
540
546
|
return "\n".join(parts)
|
|
541
547
|
|
|
548
|
+
elif typ == "SystemBlock":
|
|
549
|
+
content = block_dict.get("content", "")
|
|
550
|
+
if not content:
|
|
551
|
+
return None
|
|
552
|
+
ansi_content = block_dict.get("ansi_content", "")
|
|
553
|
+
ansi_ind = block_dict.get("ansi_indicator", "")
|
|
554
|
+
indicator = block_dict.get("indicator", "✻")
|
|
555
|
+
ind_md = _ansi_to_lark_md(ansi_ind) if ansi_ind else _escape_md(indicator)
|
|
556
|
+
content_md = _ansi_to_lark_md(ansi_content) if ansi_content else _escape_md(content)
|
|
557
|
+
return f"{ind_md} {content_md}"
|
|
558
|
+
|
|
542
559
|
return None
|
|
543
560
|
|
|
544
561
|
|
|
@@ -565,6 +582,7 @@ def _determine_header(
|
|
|
565
582
|
is_frozen: bool,
|
|
566
583
|
option_block: Optional[dict] = None,
|
|
567
584
|
disconnected: bool = False,
|
|
585
|
+
cli_type: str = "claude",
|
|
568
586
|
) -> tuple:
|
|
569
587
|
"""确定卡片标题和颜色模板,返回 (title, template)"""
|
|
570
588
|
if disconnected:
|
|
@@ -601,7 +619,8 @@ def _determine_header(
|
|
|
601
619
|
return "🔐 等待权限确认", "red"
|
|
602
620
|
return "🤔 等待选择", "blue"
|
|
603
621
|
|
|
604
|
-
|
|
622
|
+
cli_name = CLI_NAMES.get(cli_type, "Claude")
|
|
623
|
+
return f"✅ {cli_name} 就绪", "green"
|
|
605
624
|
|
|
606
625
|
|
|
607
626
|
def _extract_buttons(blocks: List[dict], option_block: Optional[dict] = None) -> List[Dict[str, str]]:
|
|
@@ -626,6 +645,7 @@ def build_stream_card(
|
|
|
626
645
|
option_block: Optional[dict] = None,
|
|
627
646
|
session_name: Optional[str] = None,
|
|
628
647
|
disconnected: bool = False,
|
|
648
|
+
cli_type: str = "claude",
|
|
629
649
|
) -> Dict[str, Any]:
|
|
630
650
|
"""从共享内存 blocks 流构建飞书卡片
|
|
631
651
|
|
|
@@ -637,7 +657,8 @@ def build_stream_card(
|
|
|
637
657
|
"""
|
|
638
658
|
title, template = _determine_header(
|
|
639
659
|
blocks, status_line, bottom_bar, is_frozen,
|
|
640
|
-
option_block=option_block, disconnected=disconnected
|
|
660
|
+
option_block=option_block, disconnected=disconnected,
|
|
661
|
+
cli_type=cli_type,
|
|
641
662
|
)
|
|
642
663
|
|
|
643
664
|
# === 第一层:内容区 ===
|
|
@@ -749,12 +770,17 @@ def build_stream_card(
|
|
|
749
770
|
|
|
750
771
|
# === 辅助卡片(保留不变)===
|
|
751
772
|
|
|
752
|
-
def _build_session_list_elements(sessions: List[Dict], current_session: Optional[str], session_groups: Optional[Dict[str, str]]) -> List[Dict]:
|
|
773
|
+
def _build_session_list_elements(sessions: List[Dict], current_session: Optional[str], session_groups: Optional[Dict[str, str]], page: int = 0) -> List[Dict]:
|
|
753
774
|
"""构建会话列表元素(供 build_menu_card 复用)"""
|
|
754
775
|
import os
|
|
755
776
|
elements = []
|
|
756
777
|
if sessions:
|
|
757
|
-
|
|
778
|
+
PER_PAGE = 8
|
|
779
|
+
total = len(sessions)
|
|
780
|
+
total_pages = max(1, (total + PER_PAGE - 1) // PER_PAGE)
|
|
781
|
+
page = max(0, min(page, total_pages - 1))
|
|
782
|
+
shown = sessions[page * PER_PAGE : (page + 1) * PER_PAGE]
|
|
783
|
+
for s in shown:
|
|
758
784
|
name = s["name"]
|
|
759
785
|
cwd = s.get("cwd", "")
|
|
760
786
|
start_time = s.get("start_time", "")
|
|
@@ -846,6 +872,41 @@ def _build_session_list_elements(sessions: List[Dict], current_session: Optional
|
|
|
846
872
|
|
|
847
873
|
if elements and elements[-1].get("tag") == "hr":
|
|
848
874
|
elements.pop()
|
|
875
|
+
|
|
876
|
+
if total > PER_PAGE:
|
|
877
|
+
prev_disabled = page == 0
|
|
878
|
+
next_disabled = page >= total_pages - 1
|
|
879
|
+
prev_btn = {
|
|
880
|
+
"tag": "button",
|
|
881
|
+
"text": {"tag": "plain_text", "content": "⬅ 上一页"},
|
|
882
|
+
"type": "default",
|
|
883
|
+
**({"disabled": True} if prev_disabled else {"behaviors": [{"type": "callback", "value": {
|
|
884
|
+
"action": "menu_page", "page": page - 1
|
|
885
|
+
}}]})
|
|
886
|
+
}
|
|
887
|
+
next_btn = {
|
|
888
|
+
"tag": "button",
|
|
889
|
+
"text": {"tag": "plain_text", "content": "下一页 ➡"},
|
|
890
|
+
"type": "default",
|
|
891
|
+
**({"disabled": True} if next_disabled else {"behaviors": [{"type": "callback", "value": {
|
|
892
|
+
"action": "menu_page", "page": page + 1
|
|
893
|
+
}}]})
|
|
894
|
+
}
|
|
895
|
+
elements.append({"tag": "hr"})
|
|
896
|
+
elements.append({
|
|
897
|
+
"tag": "column_set",
|
|
898
|
+
"flex_mode": "none",
|
|
899
|
+
"horizontal_spacing": "small",
|
|
900
|
+
"columns": [
|
|
901
|
+
{"tag": "column", "width": "weighted", "weight": 1, "elements": [{"tag": "markdown", "content": " "}]},
|
|
902
|
+
{"tag": "column", "width": "auto", "elements": [prev_btn]},
|
|
903
|
+
{"tag": "column", "width": "auto", "vertical_align": "center", "elements": [
|
|
904
|
+
{"tag": "markdown", "content": f"第 {page + 1}/{total_pages} 页"}
|
|
905
|
+
]},
|
|
906
|
+
{"tag": "column", "width": "auto", "elements": [next_btn]},
|
|
907
|
+
{"tag": "column", "width": "weighted", "weight": 1, "elements": [{"tag": "markdown", "content": " "}]},
|
|
908
|
+
]
|
|
909
|
+
})
|
|
849
910
|
else:
|
|
850
911
|
elements.append({
|
|
851
912
|
"tag": "markdown",
|
|
@@ -1001,41 +1062,38 @@ def build_dir_card(target, entries: List[Dict], sessions: List[Dict], tree: bool
|
|
|
1001
1062
|
elements.append({"tag": "markdown", "content": f"{indent}{icon} {name}"})
|
|
1002
1063
|
|
|
1003
1064
|
if not tree and total > PER_PAGE:
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
"
|
|
1065
|
+
prev_disabled = page == 0
|
|
1066
|
+
next_disabled = page >= total_pages - 1
|
|
1067
|
+
prev_btn = {
|
|
1068
|
+
"tag": "button",
|
|
1069
|
+
"text": {"tag": "plain_text", "content": "⬅ 上一页"},
|
|
1070
|
+
"type": "default",
|
|
1071
|
+
**({"disabled": True} if prev_disabled else {"behaviors": [{"type": "callback", "value": {
|
|
1072
|
+
"action": "dir_page", "path": target_str, "page": page - 1
|
|
1073
|
+
}}]})
|
|
1074
|
+
}
|
|
1075
|
+
next_btn = {
|
|
1076
|
+
"tag": "button",
|
|
1077
|
+
"text": {"tag": "plain_text", "content": "下一页 ➡"},
|
|
1078
|
+
"type": "default",
|
|
1079
|
+
**({"disabled": True} if next_disabled else {"behaviors": [{"type": "callback", "value": {
|
|
1080
|
+
"action": "dir_page", "path": target_str, "page": page + 1
|
|
1081
|
+
}}]})
|
|
1082
|
+
}
|
|
1083
|
+
elements.append({
|
|
1084
|
+
"tag": "column_set",
|
|
1085
|
+
"flex_mode": "none",
|
|
1086
|
+
"horizontal_spacing": "small",
|
|
1087
|
+
"columns": [
|
|
1088
|
+
{"tag": "column", "width": "weighted", "weight": 1, "elements": [{"tag": "markdown", "content": " "}]},
|
|
1089
|
+
{"tag": "column", "width": "auto", "elements": [prev_btn]},
|
|
1090
|
+
{"tag": "column", "width": "auto", "vertical_align": "center", "elements": [
|
|
1091
|
+
{"tag": "markdown", "content": f"第 {page + 1}/{total_pages} 页"}
|
|
1092
|
+
]},
|
|
1093
|
+
{"tag": "column", "width": "auto", "elements": [next_btn]},
|
|
1094
|
+
{"tag": "column", "width": "weighted", "weight": 1, "elements": [{"tag": "markdown", "content": " "}]},
|
|
1095
|
+
]
|
|
1024
1096
|
})
|
|
1025
|
-
if page < total_pages - 1:
|
|
1026
|
-
page_cols.append({
|
|
1027
|
-
"tag": "column",
|
|
1028
|
-
"width": "auto",
|
|
1029
|
-
"elements": [{
|
|
1030
|
-
"tag": "button",
|
|
1031
|
-
"text": {"tag": "plain_text", "content": "下一页 ➡️"},
|
|
1032
|
-
"type": "default",
|
|
1033
|
-
"behaviors": [{"type": "callback", "value": {
|
|
1034
|
-
"action": "dir_page", "path": target_str, "page": page + 1
|
|
1035
|
-
}}]
|
|
1036
|
-
}]
|
|
1037
|
-
})
|
|
1038
|
-
elements.append({"tag": "column_set", "flex_mode": "none", "columns": page_cols})
|
|
1039
1097
|
|
|
1040
1098
|
elements.append({"tag": "hr"})
|
|
1041
1099
|
elements.append(_build_menu_button_only())
|
|
@@ -1124,12 +1182,13 @@ def build_session_closed_card(session_name: str) -> Dict[str, Any]:
|
|
|
1124
1182
|
|
|
1125
1183
|
|
|
1126
1184
|
def build_menu_card(sessions: List[Dict], current_session: Optional[str] = None,
|
|
1127
|
-
session_groups: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
|
|
1185
|
+
session_groups: Optional[Dict[str, str]] = None, page: int = 0) -> Dict[str, Any]:
|
|
1128
1186
|
"""构建快捷操作菜单卡片(/menu 和 /list 共用):内嵌会话列表 + 快捷操作"""
|
|
1129
1187
|
elements = []
|
|
1130
1188
|
|
|
1131
1189
|
elements.append({"tag": "markdown", "content": "**会话管理**"})
|
|
1132
|
-
elements.
|
|
1190
|
+
elements.append({"tag": "hr"})
|
|
1191
|
+
elements.extend(_build_session_list_elements(sessions, current_session, session_groups, page=page))
|
|
1133
1192
|
|
|
1134
1193
|
elements.append({"tag": "hr"})
|
|
1135
1194
|
elements.append({"tag": "markdown", "content": "**快捷操作**"})
|
|
@@ -1159,6 +1218,17 @@ def build_menu_card(sessions: List[Dict], current_session: Optional[str] = None,
|
|
|
1159
1218
|
"behaviors": [{"type": "callback", "value": {"action": "menu_tree"}}]
|
|
1160
1219
|
}]
|
|
1161
1220
|
},
|
|
1221
|
+
{
|
|
1222
|
+
"tag": "column",
|
|
1223
|
+
"width": "weighted",
|
|
1224
|
+
"weight": 1,
|
|
1225
|
+
"elements": [{
|
|
1226
|
+
"tag": "button",
|
|
1227
|
+
"text": {"tag": "plain_text", "content": "🔄 刷新"},
|
|
1228
|
+
"type": "default",
|
|
1229
|
+
"behaviors": [{"type": "callback", "value": {"action": "menu_open"}}]
|
|
1230
|
+
}]
|
|
1231
|
+
},
|
|
1162
1232
|
]
|
|
1163
1233
|
})
|
|
1164
1234
|
|
package/lark_client/config.py
CHANGED
|
@@ -103,7 +103,13 @@ class LarkHandler:
|
|
|
103
103
|
except Exception as e:
|
|
104
104
|
logger.warning(f"保存群聊 ID 失败: {e}")
|
|
105
105
|
|
|
106
|
-
def _remove_binding_by_chat(self, chat_id: str):
|
|
106
|
+
def _remove_binding_by_chat(self, chat_id: str, force: bool = False):
|
|
107
|
+
"""移除 chat_id 的绑定。
|
|
108
|
+
群聊绑定默认不移除(避免断开后无法解散群);
|
|
109
|
+
force=True 时强制移除(用于会话终止/解散群场景)。
|
|
110
|
+
"""
|
|
111
|
+
if not force and chat_id in self._group_chat_ids:
|
|
112
|
+
return
|
|
107
113
|
self._chat_bindings.pop(chat_id, None)
|
|
108
114
|
self._save_chat_bindings()
|
|
109
115
|
|
|
@@ -416,7 +422,14 @@ class LarkHandler:
|
|
|
416
422
|
if active_slice:
|
|
417
423
|
await self._update_card_disconnected(cid, sname, active_slice)
|
|
418
424
|
await self._detach(cid)
|
|
419
|
-
self._remove_binding_by_chat(cid)
|
|
425
|
+
self._remove_binding_by_chat(cid, force=True)
|
|
426
|
+
|
|
427
|
+
# 清理所有残留绑定(包括已断开的群聊,其绑定在断开时被保留)
|
|
428
|
+
for cid in [c for c, s in list(self._chat_bindings.items()) if s == session_name]:
|
|
429
|
+
self._group_chat_ids.discard(cid)
|
|
430
|
+
self._chat_bindings.pop(cid, None)
|
|
431
|
+
self._save_chat_bindings()
|
|
432
|
+
self._save_group_chat_ids()
|
|
420
433
|
|
|
421
434
|
if tmux_session_exists(session_name):
|
|
422
435
|
tmux_kill_session(session_name)
|
|
@@ -542,7 +555,7 @@ class LarkHandler:
|
|
|
542
555
|
await self._send_or_update_card(chat_id, card, message_id)
|
|
543
556
|
|
|
544
557
|
async def _cmd_menu(self, user_id: str, chat_id: str,
|
|
545
|
-
message_id: Optional[str] = None):
|
|
558
|
+
message_id: Optional[str] = None, page: int = 0):
|
|
546
559
|
"""显示快捷操作菜单(内嵌会话列表)"""
|
|
547
560
|
sessions = list_active_sessions()
|
|
548
561
|
current = self._chat_sessions.get(chat_id)
|
|
@@ -551,7 +564,7 @@ class LarkHandler:
|
|
|
551
564
|
for cid in self._group_chat_ids
|
|
552
565
|
if cid in self._chat_bindings
|
|
553
566
|
}
|
|
554
|
-
card = build_menu_card(sessions, current_session=current, session_groups=session_groups)
|
|
567
|
+
card = build_menu_card(sessions, current_session=current, session_groups=session_groups, page=page)
|
|
555
568
|
await self._send_or_update_card(chat_id, card, message_id)
|
|
556
569
|
|
|
557
570
|
async def _cmd_ls(self, user_id: str, chat_id: str, args: str,
|
|
@@ -665,11 +678,6 @@ class LarkHandler:
|
|
|
665
678
|
# 立即 attach,让新群即刻开始接收 Claude 输出
|
|
666
679
|
await self._attach(group_chat_id, session_name)
|
|
667
680
|
|
|
668
|
-
await card_service.send_text(
|
|
669
|
-
chat_id,
|
|
670
|
-
f"✅ 已创建专属群「{group_name}」并已连接\n"
|
|
671
|
-
f"在群内直接发消息即可与 Claude 交互"
|
|
672
|
-
)
|
|
673
681
|
# 刷新会话列表卡片,使"创建群聊"按钮变为"进入群聊"
|
|
674
682
|
await self._cmd_list(user_id, chat_id, message_id=message_id)
|
|
675
683
|
except Exception as e:
|
|
@@ -727,16 +735,16 @@ class LarkHandler:
|
|
|
727
735
|
logger.error(f"解散群 API 失败: {feishu_msg}")
|
|
728
736
|
|
|
729
737
|
# 无论 Feishu delete 是否成功,都清理本地绑定
|
|
730
|
-
self._remove_binding_by_chat(group_chat_id)
|
|
731
738
|
self._group_chat_ids.discard(group_chat_id)
|
|
732
739
|
self._save_group_chat_ids()
|
|
740
|
+
self._remove_binding_by_chat(group_chat_id, force=True)
|
|
733
741
|
await self._detach(group_chat_id)
|
|
734
742
|
|
|
735
|
-
if feishu_ok:
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
743
|
+
if not feishu_ok:
|
|
744
|
+
await card_service.send_text(
|
|
745
|
+
chat_id,
|
|
746
|
+
f"⚠️ Feishu 群解散失败({feishu_msg}),已解除本地绑定。如需彻底解散请在飞书群内手动操作"
|
|
747
|
+
)
|
|
740
748
|
await self._cmd_list(user_id, chat_id, message_id=message_id)
|
|
741
749
|
except Exception as e:
|
|
742
750
|
logger.error(f"解散群失败: {e}")
|
|
@@ -755,7 +763,9 @@ class LarkHandler:
|
|
|
755
763
|
logger.info(f"自动恢复绑定: chat_id={chat_id[:8]}..., session={saved_session}")
|
|
756
764
|
ok = await self._attach(chat_id, saved_session)
|
|
757
765
|
if not ok:
|
|
758
|
-
self.
|
|
766
|
+
self._group_chat_ids.discard(chat_id)
|
|
767
|
+
self._save_group_chat_ids()
|
|
768
|
+
self._remove_binding_by_chat(chat_id, force=True)
|
|
759
769
|
await card_service.send_text(
|
|
760
770
|
chat_id, f"会话 '{saved_session}' 已不存在,请重新 /attach"
|
|
761
771
|
)
|
package/lark_client/main.py
CHANGED
|
@@ -176,6 +176,13 @@ 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 == "menu_page":
|
|
181
|
+
page = int(action_value.get("page", 0))
|
|
182
|
+
print(f"[Lark] menu_page: page={page}")
|
|
183
|
+
asyncio.create_task(handler._cmd_menu(user_id, chat_id, message_id=message_id, page=page))
|
|
184
|
+
return None
|
|
185
|
+
|
|
179
186
|
# 目录卡片:翻页
|
|
180
187
|
if action_type == "dir_page":
|
|
181
188
|
path = action_value.get("path", "")
|
|
@@ -37,7 +37,7 @@ except Exception:
|
|
|
37
37
|
|
|
38
38
|
# ── 常量 ──────────────────────────────────────────────────────────────────────
|
|
39
39
|
INITIAL_WINDOW = 30 # 首次 attach 最多显示最近 30 个 blocks
|
|
40
|
-
|
|
40
|
+
from .config import MAX_CARD_BLOCKS # 单张卡片最多 N 个 blocks → 超限冻结(可通过 .env 配置)
|
|
41
41
|
POLL_INTERVAL = 1.0 # 轮询间隔(秒)
|
|
42
42
|
RAPID_INTERVAL = 0.2 # 快速轮询间隔(秒)
|
|
43
43
|
RAPID_DURATION = 2.0 # 快速轮询持续时间(秒)
|
|
@@ -210,6 +210,7 @@ class SharedMemoryPoller:
|
|
|
210
210
|
bottom_bar = state.get("bottom_bar")
|
|
211
211
|
agent_panel = state.get("agent_panel")
|
|
212
212
|
option_block = state.get("option_block")
|
|
213
|
+
cli_type = state.get("cli_type", "claude")
|
|
213
214
|
|
|
214
215
|
# 获取活跃卡片(最后一张且未冻结)
|
|
215
216
|
active = None
|
|
@@ -221,14 +222,24 @@ class SharedMemoryPoller:
|
|
|
221
222
|
|
|
222
223
|
if active is None:
|
|
223
224
|
# 需要创建新卡片
|
|
224
|
-
await self._create_new_card(tracker, blocks, status_line, bottom_bar, agent_panel, option_block)
|
|
225
|
+
await self._create_new_card(tracker, blocks, status_line, bottom_bar, agent_panel, option_block, cli_type=cli_type)
|
|
225
226
|
else:
|
|
226
227
|
# 有活跃卡片,检查是否需要更新
|
|
227
228
|
blocks_slice = blocks[active.start_idx:]
|
|
228
229
|
|
|
230
|
+
# blocks 骤降检测(compact/重启导致 blocks 从头累积)
|
|
231
|
+
if len(blocks) < active.start_idx:
|
|
232
|
+
logger.warning(
|
|
233
|
+
f"[blocks regression] len(blocks)={len(blocks)} < start_idx={active.start_idx}, "
|
|
234
|
+
f"resetting start_idx to 0 (session={tracker.session_name})"
|
|
235
|
+
)
|
|
236
|
+
active.start_idx = 0
|
|
237
|
+
blocks_slice = blocks[0:]
|
|
238
|
+
tracker.content_hash = "" # 强制刷新
|
|
239
|
+
|
|
229
240
|
# 超限检查
|
|
230
241
|
if len(blocks_slice) > MAX_CARD_BLOCKS:
|
|
231
|
-
await self._freeze_and_split(tracker, blocks, status_line, bottom_bar, agent_panel, option_block)
|
|
242
|
+
await self._freeze_and_split(tracker, blocks, status_line, bottom_bar, agent_panel, option_block, cli_type=cli_type)
|
|
232
243
|
return
|
|
233
244
|
|
|
234
245
|
# hash diff
|
|
@@ -238,7 +249,7 @@ class SharedMemoryPoller:
|
|
|
238
249
|
|
|
239
250
|
# 更新卡片
|
|
240
251
|
from .card_builder import build_stream_card
|
|
241
|
-
card_dict = build_stream_card(blocks_slice, status_line, bottom_bar, agent_panel=agent_panel, option_block=option_block, session_name=tracker.session_name)
|
|
252
|
+
card_dict = build_stream_card(blocks_slice, status_line, bottom_bar, agent_panel=agent_panel, option_block=option_block, session_name=tracker.session_name, cli_type=cli_type)
|
|
242
253
|
|
|
243
254
|
active.sequence += 1
|
|
244
255
|
success = await self._card_service.update_card(
|
|
@@ -250,7 +261,8 @@ class SharedMemoryPoller:
|
|
|
250
261
|
if getattr(success, 'is_element_limit', False):
|
|
251
262
|
# 元素超限:冻结旧卡 + 推新流式卡
|
|
252
263
|
await self._handle_element_limit(
|
|
253
|
-
tracker, blocks, status_line, bottom_bar, agent_panel, option_block
|
|
264
|
+
tracker, blocks, status_line, bottom_bar, agent_panel, option_block,
|
|
265
|
+
cli_type=cli_type,
|
|
254
266
|
)
|
|
255
267
|
return
|
|
256
268
|
elif not success:
|
|
@@ -280,6 +292,7 @@ class SharedMemoryPoller:
|
|
|
280
292
|
status_line: Optional[dict], bottom_bar: Optional[dict],
|
|
281
293
|
agent_panel: Optional[dict] = None,
|
|
282
294
|
option_block: Optional[dict] = None,
|
|
295
|
+
cli_type: str = "claude",
|
|
283
296
|
) -> None:
|
|
284
297
|
"""创建新卡片(首次 attach 或冻结后)"""
|
|
285
298
|
if not tracker.cards:
|
|
@@ -289,13 +302,19 @@ class SharedMemoryPoller:
|
|
|
289
302
|
# 冻结后:从上张冻结卡片的结束位置开始
|
|
290
303
|
last_frozen = tracker.cards[-1]
|
|
291
304
|
start_idx = last_frozen.start_idx + MAX_CARD_BLOCKS
|
|
305
|
+
if start_idx >= len(blocks):
|
|
306
|
+
start_idx = 0
|
|
307
|
+
logger.warning(
|
|
308
|
+
f"[_create_new_card] start_idx overflow, reset to 0 "
|
|
309
|
+
f"(frozen.start_idx={last_frozen.start_idx}, total blocks={len(blocks)})"
|
|
310
|
+
)
|
|
292
311
|
|
|
293
312
|
blocks_slice = blocks[start_idx:]
|
|
294
313
|
if not blocks_slice and not status_line and not bottom_bar and not agent_panel and not option_block:
|
|
295
314
|
return
|
|
296
315
|
|
|
297
316
|
from .card_builder import build_stream_card
|
|
298
|
-
card_dict = build_stream_card(blocks_slice, status_line, bottom_bar, agent_panel=agent_panel, option_block=option_block, session_name=tracker.session_name)
|
|
317
|
+
card_dict = build_stream_card(blocks_slice, status_line, bottom_bar, agent_panel=agent_panel, option_block=option_block, session_name=tracker.session_name, cli_type=cli_type)
|
|
299
318
|
card_id = await self._card_service.create_card(card_dict)
|
|
300
319
|
|
|
301
320
|
if card_id:
|
|
@@ -316,6 +335,7 @@ class SharedMemoryPoller:
|
|
|
316
335
|
status_line: Optional[dict], bottom_bar: Optional[dict],
|
|
317
336
|
agent_panel: Optional[dict] = None,
|
|
318
337
|
option_block: Optional[dict] = None,
|
|
338
|
+
cli_type: str = "claude",
|
|
319
339
|
) -> None:
|
|
320
340
|
"""元素超限:冻结旧卡片 + 推送新流式卡片"""
|
|
321
341
|
active = tracker.cards[-1]
|
|
@@ -340,6 +360,7 @@ class SharedMemoryPoller:
|
|
|
340
360
|
new_blocks, status_line, bottom_bar,
|
|
341
361
|
agent_panel=agent_panel, option_block=option_block,
|
|
342
362
|
session_name=tracker.session_name,
|
|
363
|
+
cli_type=cli_type,
|
|
343
364
|
)
|
|
344
365
|
new_card_id = await self._card_service.create_card(new_card_dict)
|
|
345
366
|
if new_card_id:
|
|
@@ -358,6 +379,7 @@ class SharedMemoryPoller:
|
|
|
358
379
|
status_line: Optional[dict], bottom_bar: Optional[dict],
|
|
359
380
|
agent_panel: Optional[dict] = None,
|
|
360
381
|
option_block: Optional[dict] = None,
|
|
382
|
+
cli_type: str = "claude",
|
|
361
383
|
) -> None:
|
|
362
384
|
"""冻结当前卡片 + 开新卡"""
|
|
363
385
|
active = tracker.cards[-1]
|
|
@@ -382,7 +404,7 @@ class SharedMemoryPoller:
|
|
|
382
404
|
if not new_blocks:
|
|
383
405
|
return
|
|
384
406
|
|
|
385
|
-
new_card_dict = build_stream_card(new_blocks, status_line, bottom_bar, agent_panel=agent_panel, option_block=option_block, session_name=tracker.session_name)
|
|
407
|
+
new_card_dict = build_stream_card(new_blocks, status_line, bottom_bar, agent_panel=agent_panel, option_block=option_block, session_name=tracker.session_name, cli_type=cli_type)
|
|
386
408
|
new_card_id = await self._card_service.create_card(new_card_dict)
|
|
387
409
|
if new_card_id:
|
|
388
410
|
await self._card_service.send_card(tracker.chat_id, new_card_id)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "remote-claude",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "双端共享 Claude CLI 工具",
|
|
5
5
|
"bin": {
|
|
6
6
|
"remote-claude": "bin/remote-claude",
|
|
@@ -24,7 +24,6 @@
|
|
|
24
24
|
"stats/__init__.py",
|
|
25
25
|
"stats/*.py",
|
|
26
26
|
"pyproject.toml",
|
|
27
|
-
"uv.lock",
|
|
28
27
|
".env.example"
|
|
29
28
|
],
|
|
30
29
|
"os": [
|
package/remote_claude.py
CHANGED
|
@@ -79,6 +79,8 @@ def cmd_start(args):
|
|
|
79
79
|
claude_args_str = " ".join(f"'{arg}'" for arg in claude_args)
|
|
80
80
|
debug_flag = " --debug-screen" if getattr(args, "debug_screen", False) else ""
|
|
81
81
|
debug_verbose_flag = " --debug-verbose" if getattr(args, "debug_verbose", False) else ""
|
|
82
|
+
cli_type = getattr(args, "cli", "claude")
|
|
83
|
+
cli_type_flag = f" --cli-type {cli_type}" if cli_type != "claude" else ""
|
|
82
84
|
|
|
83
85
|
# 捕获用户终端环境变量(tmux 会覆盖这些值,导致 Claude CLI 无法启用 kitty keyboard protocol)
|
|
84
86
|
env_prefix = ""
|
|
@@ -87,7 +89,7 @@ def cmd_start(args):
|
|
|
87
89
|
if val:
|
|
88
90
|
env_prefix += f"{key}='{val}' "
|
|
89
91
|
|
|
90
|
-
server_cmd = f"{env_prefix}uv run --project '{SCRIPT_DIR}' python3 '{server_script}'{debug_flag}{debug_verbose_flag} -- '{session_name}' {claude_args_str}"
|
|
92
|
+
server_cmd = f"{env_prefix}uv run --project '{SCRIPT_DIR}' python3 '{server_script}'{debug_flag}{debug_verbose_flag}{cli_type_flag} -- '{session_name}' {claude_args_str}"
|
|
91
93
|
|
|
92
94
|
print(f"启动会话: {session_name}")
|
|
93
95
|
|
|
@@ -500,6 +502,12 @@ def main():
|
|
|
500
502
|
action="store_true",
|
|
501
503
|
help="debug 日志输出完整诊断信息(indicator、repr 等),默认只输出 ansi_render"
|
|
502
504
|
)
|
|
505
|
+
start_parser.add_argument(
|
|
506
|
+
"--cli",
|
|
507
|
+
default="claude",
|
|
508
|
+
choices=["claude", "codex"],
|
|
509
|
+
help="后端 CLI 类型(默认 claude)"
|
|
510
|
+
)
|
|
503
511
|
start_parser.set_defaults(func=cmd_start)
|
|
504
512
|
|
|
505
513
|
# attach 命令
|