remote-claude 0.2.7 → 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/.env.example +4 -0
- package/init.sh +21 -23
- package/lark_client/card_builder.py +122 -40
- package/lark_client/config.py +3 -0
- package/lark_client/lark_handler.py +60 -37
- package/lark_client/main.py +14 -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 +70 -10
- 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", "")
|
|
@@ -824,6 +850,18 @@ def _build_session_list_elements(sessions: List[Dict], current_session: Optional
|
|
|
824
850
|
"action": "list_disband_group", "session": name
|
|
825
851
|
}}]
|
|
826
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
|
+
})
|
|
827
865
|
elements.append({
|
|
828
866
|
"tag": "column_set",
|
|
829
867
|
"flex_mode": "none",
|
|
@@ -846,6 +884,41 @@ def _build_session_list_elements(sessions: List[Dict], current_session: Optional
|
|
|
846
884
|
|
|
847
885
|
if elements and elements[-1].get("tag") == "hr":
|
|
848
886
|
elements.pop()
|
|
887
|
+
|
|
888
|
+
if total > PER_PAGE:
|
|
889
|
+
prev_disabled = page == 0
|
|
890
|
+
next_disabled = page >= total_pages - 1
|
|
891
|
+
prev_btn = {
|
|
892
|
+
"tag": "button",
|
|
893
|
+
"text": {"tag": "plain_text", "content": "⬅ 上一页"},
|
|
894
|
+
"type": "default",
|
|
895
|
+
**({"disabled": True} if prev_disabled else {"behaviors": [{"type": "callback", "value": {
|
|
896
|
+
"action": "menu_page", "page": page - 1
|
|
897
|
+
}}]})
|
|
898
|
+
}
|
|
899
|
+
next_btn = {
|
|
900
|
+
"tag": "button",
|
|
901
|
+
"text": {"tag": "plain_text", "content": "下一页 ➡"},
|
|
902
|
+
"type": "default",
|
|
903
|
+
**({"disabled": True} if next_disabled else {"behaviors": [{"type": "callback", "value": {
|
|
904
|
+
"action": "menu_page", "page": page + 1
|
|
905
|
+
}}]})
|
|
906
|
+
}
|
|
907
|
+
elements.append({"tag": "hr"})
|
|
908
|
+
elements.append({
|
|
909
|
+
"tag": "column_set",
|
|
910
|
+
"flex_mode": "none",
|
|
911
|
+
"horizontal_spacing": "small",
|
|
912
|
+
"columns": [
|
|
913
|
+
{"tag": "column", "width": "weighted", "weight": 1, "elements": [{"tag": "markdown", "content": " "}]},
|
|
914
|
+
{"tag": "column", "width": "auto", "elements": [prev_btn]},
|
|
915
|
+
{"tag": "column", "width": "auto", "vertical_align": "center", "elements": [
|
|
916
|
+
{"tag": "markdown", "content": f"第 {page + 1}/{total_pages} 页"}
|
|
917
|
+
]},
|
|
918
|
+
{"tag": "column", "width": "auto", "elements": [next_btn]},
|
|
919
|
+
{"tag": "column", "width": "weighted", "weight": 1, "elements": [{"tag": "markdown", "content": " "}]},
|
|
920
|
+
]
|
|
921
|
+
})
|
|
849
922
|
else:
|
|
850
923
|
elements.append({
|
|
851
924
|
"tag": "markdown",
|
|
@@ -1001,41 +1074,38 @@ def build_dir_card(target, entries: List[Dict], sessions: List[Dict], tree: bool
|
|
|
1001
1074
|
elements.append({"tag": "markdown", "content": f"{indent}{icon} {name}"})
|
|
1002
1075
|
|
|
1003
1076
|
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
|
-
"
|
|
1077
|
+
prev_disabled = page == 0
|
|
1078
|
+
next_disabled = page >= total_pages - 1
|
|
1079
|
+
prev_btn = {
|
|
1080
|
+
"tag": "button",
|
|
1081
|
+
"text": {"tag": "plain_text", "content": "⬅ 上一页"},
|
|
1082
|
+
"type": "default",
|
|
1083
|
+
**({"disabled": True} if prev_disabled else {"behaviors": [{"type": "callback", "value": {
|
|
1084
|
+
"action": "dir_page", "path": target_str, "page": page - 1
|
|
1085
|
+
}}]})
|
|
1086
|
+
}
|
|
1087
|
+
next_btn = {
|
|
1088
|
+
"tag": "button",
|
|
1089
|
+
"text": {"tag": "plain_text", "content": "下一页 ➡"},
|
|
1090
|
+
"type": "default",
|
|
1091
|
+
**({"disabled": True} if next_disabled else {"behaviors": [{"type": "callback", "value": {
|
|
1092
|
+
"action": "dir_page", "path": target_str, "page": page + 1
|
|
1093
|
+
}}]})
|
|
1094
|
+
}
|
|
1095
|
+
elements.append({
|
|
1096
|
+
"tag": "column_set",
|
|
1097
|
+
"flex_mode": "none",
|
|
1098
|
+
"horizontal_spacing": "small",
|
|
1099
|
+
"columns": [
|
|
1100
|
+
{"tag": "column", "width": "weighted", "weight": 1, "elements": [{"tag": "markdown", "content": " "}]},
|
|
1101
|
+
{"tag": "column", "width": "auto", "elements": [prev_btn]},
|
|
1102
|
+
{"tag": "column", "width": "auto", "vertical_align": "center", "elements": [
|
|
1103
|
+
{"tag": "markdown", "content": f"第 {page + 1}/{total_pages} 页"}
|
|
1104
|
+
]},
|
|
1105
|
+
{"tag": "column", "width": "auto", "elements": [next_btn]},
|
|
1106
|
+
{"tag": "column", "width": "weighted", "weight": 1, "elements": [{"tag": "markdown", "content": " "}]},
|
|
1107
|
+
]
|
|
1024
1108
|
})
|
|
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
1109
|
|
|
1040
1110
|
elements.append({"tag": "hr"})
|
|
1041
1111
|
elements.append(_build_menu_button_only())
|
|
@@ -1124,12 +1194,13 @@ def build_session_closed_card(session_name: str) -> Dict[str, Any]:
|
|
|
1124
1194
|
|
|
1125
1195
|
|
|
1126
1196
|
def build_menu_card(sessions: List[Dict], current_session: Optional[str] = None,
|
|
1127
|
-
session_groups: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
|
|
1197
|
+
session_groups: Optional[Dict[str, str]] = None, page: int = 0) -> Dict[str, Any]:
|
|
1128
1198
|
"""构建快捷操作菜单卡片(/menu 和 /list 共用):内嵌会话列表 + 快捷操作"""
|
|
1129
1199
|
elements = []
|
|
1130
1200
|
|
|
1131
1201
|
elements.append({"tag": "markdown", "content": "**会话管理**"})
|
|
1132
|
-
elements.
|
|
1202
|
+
elements.append({"tag": "hr"})
|
|
1203
|
+
elements.extend(_build_session_list_elements(sessions, current_session, session_groups, page=page))
|
|
1133
1204
|
|
|
1134
1205
|
elements.append({"tag": "hr"})
|
|
1135
1206
|
elements.append({"tag": "markdown", "content": "**快捷操作**"})
|
|
@@ -1159,6 +1230,17 @@ def build_menu_card(sessions: List[Dict], current_session: Optional[str] = None,
|
|
|
1159
1230
|
"behaviors": [{"type": "callback", "value": {"action": "menu_tree"}}]
|
|
1160
1231
|
}]
|
|
1161
1232
|
},
|
|
1233
|
+
{
|
|
1234
|
+
"tag": "column",
|
|
1235
|
+
"width": "weighted",
|
|
1236
|
+
"weight": 1,
|
|
1237
|
+
"elements": [{
|
|
1238
|
+
"tag": "button",
|
|
1239
|
+
"text": {"tag": "plain_text", "content": "🔄 刷新"},
|
|
1240
|
+
"type": "default",
|
|
1241
|
+
"behaviors": [{"type": "callback", "value": {"action": "menu_open"}}]
|
|
1242
|
+
}]
|
|
1243
|
+
},
|
|
1162
1244
|
]
|
|
1163
1245
|
})
|
|
1164
1246
|
|
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
|
|
|
@@ -395,7 +401,8 @@ class LarkHandler:
|
|
|
395
401
|
logger.error(f"启动并创建群聊失败: {e}")
|
|
396
402
|
await card_service.send_text(chat_id, f"操作失败:{e}")
|
|
397
403
|
|
|
398
|
-
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):
|
|
399
406
|
"""终止会话"""
|
|
400
407
|
from utils.session import cleanup_session, tmux_session_exists, tmux_kill_session
|
|
401
408
|
|
|
@@ -409,6 +416,13 @@ class LarkHandler:
|
|
|
409
416
|
await card_service.send_text(chat_id, f"错误: 会话 '{session_name}' 不存在")
|
|
410
417
|
return
|
|
411
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
|
+
|
|
412
426
|
# 断开所有连接到此会话的 chat
|
|
413
427
|
for cid, sname in list(self._chat_sessions.items()):
|
|
414
428
|
if sname == session_name:
|
|
@@ -416,13 +430,21 @@ class LarkHandler:
|
|
|
416
430
|
if active_slice:
|
|
417
431
|
await self._update_card_disconnected(cid, sname, active_slice)
|
|
418
432
|
await self._detach(cid)
|
|
419
|
-
self._remove_binding_by_chat(cid)
|
|
433
|
+
self._remove_binding_by_chat(cid, force=True)
|
|
434
|
+
|
|
435
|
+
# 清理所有残留绑定(包括已断开的群聊,其绑定在断开时被保留)
|
|
436
|
+
for cid in [c for c, s in list(self._chat_bindings.items()) if s == session_name]:
|
|
437
|
+
self._group_chat_ids.discard(cid)
|
|
438
|
+
self._chat_bindings.pop(cid, None)
|
|
439
|
+
self._save_chat_bindings()
|
|
440
|
+
self._save_group_chat_ids()
|
|
420
441
|
|
|
421
442
|
if tmux_session_exists(session_name):
|
|
422
443
|
tmux_kill_session(session_name)
|
|
423
444
|
cleanup_session(session_name)
|
|
424
445
|
|
|
425
446
|
await card_service.send_text(chat_id, f"✅ 会话 '{session_name}' 已终止")
|
|
447
|
+
await self._cmd_list(user_id, chat_id, message_id=message_id)
|
|
426
448
|
|
|
427
449
|
async def _handle_list_detach(self, user_id: str, chat_id: str,
|
|
428
450
|
message_id: Optional[str] = None):
|
|
@@ -542,7 +564,7 @@ class LarkHandler:
|
|
|
542
564
|
await self._send_or_update_card(chat_id, card, message_id)
|
|
543
565
|
|
|
544
566
|
async def _cmd_menu(self, user_id: str, chat_id: str,
|
|
545
|
-
message_id: Optional[str] = None):
|
|
567
|
+
message_id: Optional[str] = None, page: int = 0):
|
|
546
568
|
"""显示快捷操作菜单(内嵌会话列表)"""
|
|
547
569
|
sessions = list_active_sessions()
|
|
548
570
|
current = self._chat_sessions.get(chat_id)
|
|
@@ -551,7 +573,7 @@ class LarkHandler:
|
|
|
551
573
|
for cid in self._group_chat_ids
|
|
552
574
|
if cid in self._chat_bindings
|
|
553
575
|
}
|
|
554
|
-
card = build_menu_card(sessions, current_session=current, session_groups=session_groups)
|
|
576
|
+
card = build_menu_card(sessions, current_session=current, session_groups=session_groups, page=page)
|
|
555
577
|
await self._send_or_update_card(chat_id, card, message_id)
|
|
556
578
|
|
|
557
579
|
async def _cmd_ls(self, user_id: str, chat_id: str, args: str,
|
|
@@ -665,28 +687,14 @@ class LarkHandler:
|
|
|
665
687
|
# 立即 attach,让新群即刻开始接收 Claude 输出
|
|
666
688
|
await self._attach(group_chat_id, session_name)
|
|
667
689
|
|
|
668
|
-
await card_service.send_text(
|
|
669
|
-
chat_id,
|
|
670
|
-
f"✅ 已创建专属群「{group_name}」并已连接\n"
|
|
671
|
-
f"在群内直接发消息即可与 Claude 交互"
|
|
672
|
-
)
|
|
673
690
|
# 刷新会话列表卡片,使"创建群聊"按钮变为"进入群聊"
|
|
674
691
|
await self._cmd_list(user_id, chat_id, message_id=message_id)
|
|
675
692
|
except Exception as e:
|
|
676
693
|
logger.error(f"创建群失败: {e}")
|
|
677
694
|
await card_service.send_text(chat_id, f"创建群失败:{e}")
|
|
678
695
|
|
|
679
|
-
async def
|
|
680
|
-
|
|
681
|
-
"""解散与指定会话绑定的专属群聊"""
|
|
682
|
-
group_chat_id = next(
|
|
683
|
-
(cid for cid, sname in self._chat_bindings.items() if sname == session_name and cid.startswith("oc_")),
|
|
684
|
-
None
|
|
685
|
-
)
|
|
686
|
-
if not group_chat_id:
|
|
687
|
-
await card_service.send_text(chat_id, f"会话 '{session_name}' 没有绑定群聊")
|
|
688
|
-
return
|
|
689
|
-
|
|
696
|
+
async def _disband_group_via_api(self, group_chat_id: str) -> tuple:
|
|
697
|
+
"""调用飞书 API 解散群聊,返回 (ok: bool, err_msg: str)"""
|
|
690
698
|
import json as _json
|
|
691
699
|
import urllib.request
|
|
692
700
|
import urllib.error
|
|
@@ -701,9 +709,6 @@ class LarkHandler:
|
|
|
701
709
|
), timeout=10
|
|
702
710
|
)
|
|
703
711
|
token = _json.loads(token_resp.read())["tenant_access_token"]
|
|
704
|
-
|
|
705
|
-
feishu_ok = False
|
|
706
|
-
feishu_msg = ""
|
|
707
712
|
try:
|
|
708
713
|
disband_resp = urllib.request.urlopen(
|
|
709
714
|
urllib.request.Request(
|
|
@@ -713,30 +718,46 @@ class LarkHandler:
|
|
|
713
718
|
), timeout=10
|
|
714
719
|
)
|
|
715
720
|
disband_data = _json.loads(disband_resp.read())
|
|
716
|
-
|
|
717
|
-
|
|
721
|
+
if disband_data.get("code") == 0:
|
|
722
|
+
return True, ""
|
|
723
|
+
return False, disband_data.get("msg", "")
|
|
718
724
|
except urllib.error.HTTPError as e:
|
|
719
725
|
err_body = e.read().decode("utf-8", errors="replace")
|
|
720
726
|
try:
|
|
721
727
|
err_data = _json.loads(err_body)
|
|
722
|
-
|
|
723
|
-
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', '')}"
|
|
724
729
|
except Exception:
|
|
725
|
-
|
|
726
|
-
|
|
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:
|
|
727
748
|
logger.error(f"解散群 API 失败: {feishu_msg}")
|
|
728
749
|
|
|
729
750
|
# 无论 Feishu delete 是否成功,都清理本地绑定
|
|
730
|
-
self._remove_binding_by_chat(group_chat_id)
|
|
731
751
|
self._group_chat_ids.discard(group_chat_id)
|
|
732
752
|
self._save_group_chat_ids()
|
|
753
|
+
self._remove_binding_by_chat(group_chat_id, force=True)
|
|
733
754
|
await self._detach(group_chat_id)
|
|
734
755
|
|
|
735
|
-
if feishu_ok:
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
756
|
+
if not feishu_ok:
|
|
757
|
+
await card_service.send_text(
|
|
758
|
+
chat_id,
|
|
759
|
+
f"⚠️ Feishu 群解散失败({feishu_msg}),已解除本地绑定。如需彻底解散请在飞书群内手动操作"
|
|
760
|
+
)
|
|
740
761
|
await self._cmd_list(user_id, chat_id, message_id=message_id)
|
|
741
762
|
except Exception as e:
|
|
742
763
|
logger.error(f"解散群失败: {e}")
|
|
@@ -755,7 +776,9 @@ class LarkHandler:
|
|
|
755
776
|
logger.info(f"自动恢复绑定: chat_id={chat_id[:8]}..., session={saved_session}")
|
|
756
777
|
ok = await self._attach(chat_id, saved_session)
|
|
757
778
|
if not ok:
|
|
758
|
-
self.
|
|
779
|
+
self._group_chat_ids.discard(chat_id)
|
|
780
|
+
self._save_group_chat_ids()
|
|
781
|
+
self._remove_binding_by_chat(chat_id, force=True)
|
|
759
782
|
await card_service.send_text(
|
|
760
783
|
chat_id, f"会话 '{saved_session}' 已不存在,请重新 /attach"
|
|
761
784
|
)
|
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", "")
|
|
@@ -176,6 +183,13 @@ def handle_card_action(event: P2CardActionTrigger) -> P2CardActionTriggerRespons
|
|
|
176
183
|
asyncio.create_task(handler._cmd_ls(user_id, chat_id, path, message_id=message_id))
|
|
177
184
|
return None
|
|
178
185
|
|
|
186
|
+
# 菜单卡片:会话列表翻页
|
|
187
|
+
if action_type == "menu_page":
|
|
188
|
+
page = int(action_value.get("page", 0))
|
|
189
|
+
print(f"[Lark] menu_page: page={page}")
|
|
190
|
+
asyncio.create_task(handler._cmd_menu(user_id, chat_id, message_id=message_id, page=page))
|
|
191
|
+
return None
|
|
192
|
+
|
|
179
193
|
# 目录卡片:翻页
|
|
180
194
|
if action_type == "dir_page":
|
|
181
195
|
path = action_value.get("path", "")
|