remote-claude 1.0.3 → 1.0.4
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/README.md +0 -2
- package/bin/cdx +9 -1
- package/bin/cl +9 -1
- package/bin/cla +9 -1
- package/bin/cx +9 -1
- package/bin/remote-claude +9 -18
- package/client/client.py +1 -1
- package/init.sh +30 -97
- package/lark_client/card_builder.py +53 -33
- package/lark_client/lark_handler.py +118 -6
- package/lark_client/main.py +6 -4
- package/lark_client/shared_memory_poller.py +137 -112
- package/package.json +1 -1
- package/remote_claude.py +226 -0
- package/server/server.py +31 -19
- package/utils/session.py +81 -30
package/.env.example
CHANGED
|
@@ -18,6 +18,10 @@ ALLOWED_USERS=ou_xxxxx,ou_yyyyy
|
|
|
18
18
|
# 支持多词命令,如:ccr code、/usr/local/bin/claude
|
|
19
19
|
# CLAUDE_COMMAND=claude
|
|
20
20
|
|
|
21
|
+
# Codex CLI 命令(可选,默认 codex)
|
|
22
|
+
# 支持多词命令,如:/usr/local/bin/codex
|
|
23
|
+
# CODEX_COMMAND=codex
|
|
24
|
+
|
|
21
25
|
# 流式卡片配置(可选)
|
|
22
26
|
# 单张卡片最多显示的 block 数量,超限自动冻结并创建新卡片(默认 50)
|
|
23
27
|
# MAX_CARD_BLOCKS=50
|
package/README.md
CHANGED
|
@@ -179,7 +179,6 @@ remote-claude attach <会话名>
|
|
|
179
179
|
"im:message:recall",
|
|
180
180
|
"im:message:update",
|
|
181
181
|
"search:docs:read",
|
|
182
|
-
"search:suite_dataset:readonly",
|
|
183
182
|
"sheets:spreadsheet.meta:read",
|
|
184
183
|
"sheets:spreadsheet.meta:write_only",
|
|
185
184
|
"sheets:spreadsheet:create",
|
|
@@ -191,7 +190,6 @@ remote-claude attach <会话名>
|
|
|
191
190
|
"task:task:write",
|
|
192
191
|
"task:task:writeonly",
|
|
193
192
|
"task:tasklist:read",
|
|
194
|
-
"task:tasklist:writeonly",
|
|
195
193
|
"wiki:wiki:readonly"
|
|
196
194
|
]
|
|
197
195
|
}
|
package/bin/cdx
CHANGED
|
@@ -16,5 +16,13 @@ fi
|
|
|
16
16
|
# 检查飞书配置
|
|
17
17
|
source "$SCRIPT_DIR/scripts/check-env.sh" "$SCRIPT_DIR"
|
|
18
18
|
|
|
19
|
+
# 会话名:第一个参数非 - 开头时作为自定义名,否则用 PWD_时间戳
|
|
20
|
+
if [ -n "$1" ] && [[ "$1" != -* ]]; then
|
|
21
|
+
SESSION_NAME="$1"
|
|
22
|
+
shift
|
|
23
|
+
else
|
|
24
|
+
SESSION_NAME="${PWD}_$(date +%m%d_%H%M%S)"
|
|
25
|
+
fi
|
|
26
|
+
|
|
19
27
|
uv run --project "$SCRIPT_DIR" python3 "$SCRIPT_DIR/remote_claude.py" lark start
|
|
20
|
-
uv run --project "$SCRIPT_DIR" python3 "$SCRIPT_DIR/remote_claude.py" start "$
|
|
28
|
+
uv run --project "$SCRIPT_DIR" python3 "$SCRIPT_DIR/remote_claude.py" start "$SESSION_NAME" --cli codex -- "$@"
|
package/bin/cl
CHANGED
|
@@ -16,5 +16,13 @@ fi
|
|
|
16
16
|
# 检查飞书配置
|
|
17
17
|
source "$SCRIPT_DIR/scripts/check-env.sh" "$SCRIPT_DIR"
|
|
18
18
|
|
|
19
|
+
# 会话名:第一个参数非 - 开头时作为自定义名,否则用 PWD_时间戳
|
|
20
|
+
if [ -n "$1" ] && [[ "$1" != -* ]]; then
|
|
21
|
+
SESSION_NAME="$1"
|
|
22
|
+
shift
|
|
23
|
+
else
|
|
24
|
+
SESSION_NAME="${PWD}_$(date +%m%d_%H%M%S)"
|
|
25
|
+
fi
|
|
26
|
+
|
|
19
27
|
uv run --project "$SCRIPT_DIR" python3 "$SCRIPT_DIR/remote_claude.py" lark start
|
|
20
|
-
uv run --project "$SCRIPT_DIR" python3 "$SCRIPT_DIR/remote_claude.py" start "$
|
|
28
|
+
uv run --project "$SCRIPT_DIR" python3 "$SCRIPT_DIR/remote_claude.py" start "$SESSION_NAME" -- --dangerously-skip-permissions --permission-mode=dontAsk "$@"
|
package/bin/cla
CHANGED
|
@@ -16,5 +16,13 @@ fi
|
|
|
16
16
|
# 检查飞书配置
|
|
17
17
|
source "$SCRIPT_DIR/scripts/check-env.sh" "$SCRIPT_DIR"
|
|
18
18
|
|
|
19
|
+
# 会话名:第一个参数非 - 开头时作为自定义名,否则用 PWD_时间戳
|
|
20
|
+
if [ -n "$1" ] && [[ "$1" != -* ]]; then
|
|
21
|
+
SESSION_NAME="$1"
|
|
22
|
+
shift
|
|
23
|
+
else
|
|
24
|
+
SESSION_NAME="${PWD}_$(date +%m%d_%H%M%S)"
|
|
25
|
+
fi
|
|
26
|
+
|
|
19
27
|
uv run --project "$SCRIPT_DIR" python3 "$SCRIPT_DIR/remote_claude.py" lark start
|
|
20
|
-
uv run --project "$SCRIPT_DIR" python3 "$SCRIPT_DIR/remote_claude.py" start "$
|
|
28
|
+
uv run --project "$SCRIPT_DIR" python3 "$SCRIPT_DIR/remote_claude.py" start "$SESSION_NAME" -- "$@"
|
package/bin/cx
CHANGED
|
@@ -16,5 +16,13 @@ fi
|
|
|
16
16
|
# 检查飞书配置
|
|
17
17
|
source "$SCRIPT_DIR/scripts/check-env.sh" "$SCRIPT_DIR"
|
|
18
18
|
|
|
19
|
+
# 会话名:第一个参数非 - 开头时作为自定义名,否则用 PWD_时间戳
|
|
20
|
+
if [ -n "$1" ] && [[ "$1" != -* ]]; then
|
|
21
|
+
SESSION_NAME="$1"
|
|
22
|
+
shift
|
|
23
|
+
else
|
|
24
|
+
SESSION_NAME="${PWD}_$(date +%m%d_%H%M%S)"
|
|
25
|
+
fi
|
|
26
|
+
|
|
19
27
|
uv run --project "$SCRIPT_DIR" python3 "$SCRIPT_DIR/remote_claude.py" lark start
|
|
20
|
-
uv run --project "$SCRIPT_DIR" python3 "$SCRIPT_DIR/remote_claude.py" start "$
|
|
28
|
+
uv run --project "$SCRIPT_DIR" python3 "$SCRIPT_DIR/remote_claude.py" start "$SESSION_NAME" --cli codex -- --dangerously-bypass-approvals-and-sandbox "$@"
|
package/bin/remote-claude
CHANGED
|
@@ -17,25 +17,16 @@ fi
|
|
|
17
17
|
if [ "$1" = "log" ]; then
|
|
18
18
|
LOG_DIR="/tmp/remote-claude"
|
|
19
19
|
if [ -n "$2" ]; then
|
|
20
|
-
# 指定 session 名,/
|
|
21
|
-
|
|
22
|
-
LOG_FILE="$LOG_DIR/${
|
|
20
|
+
# 指定 session 名,/ . 空格替换为 _(与 _log_filename 一致)
|
|
21
|
+
SESSION_LOG=$(echo "$2" | tr '/. ' '___')
|
|
22
|
+
LOG_FILE="$LOG_DIR/${SESSION_LOG}_messages.log"
|
|
23
23
|
else
|
|
24
|
-
# 找最后启动的 session
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
else
|
|
31
|
-
# Linux: stat -c "%W" 返回创建时间(秒)
|
|
32
|
-
LATEST_PID=$(ls "$LOG_DIR"/_*.pid 2>/dev/null | while read f; do
|
|
33
|
-
stat -c "%W:%N" "$f" 2>/dev/null
|
|
34
|
-
done | sort -rn | head -1 | cut -d: -f2)
|
|
35
|
-
fi
|
|
36
|
-
if [ -n "$LATEST_PID" ]; then
|
|
37
|
-
SESSION_SAFE=$(basename "$LATEST_PID" .pid)
|
|
38
|
-
LOG_FILE="$LOG_DIR/${SESSION_SAFE}_messages.log"
|
|
24
|
+
# 找最后启动的 session(按 .name 文件修改时间排序)
|
|
25
|
+
LATEST_NAME=$(ls -t "$LOG_DIR"/*.name 2>/dev/null | head -1)
|
|
26
|
+
if [ -n "$LATEST_NAME" ]; then
|
|
27
|
+
SESSION_NAME=$(cat "$LATEST_NAME" 2>/dev/null)
|
|
28
|
+
SESSION_LOG=$(echo "$SESSION_NAME" | tr '/. ' '___')
|
|
29
|
+
LOG_FILE="$LOG_DIR/${SESSION_LOG}_messages.log"
|
|
39
30
|
fi
|
|
40
31
|
fi
|
|
41
32
|
|
package/client/client.py
CHANGED
package/init.sh
CHANGED
|
@@ -26,6 +26,16 @@ print_error() {
|
|
|
26
26
|
echo -e "${RED}✗${NC} $1"
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
# 非交互 sudo:有免密 sudo 则执行(带 5 分钟超时),否则跳过
|
|
30
|
+
_sudo_or_skip() {
|
|
31
|
+
if sudo -n true 2>/dev/null; then
|
|
32
|
+
timeout 300 sudo "$@"
|
|
33
|
+
else
|
|
34
|
+
print_warning "sudo 需要密码,跳过: sudo $*"
|
|
35
|
+
return 1
|
|
36
|
+
fi
|
|
37
|
+
}
|
|
38
|
+
|
|
29
39
|
print_header() {
|
|
30
40
|
echo ""
|
|
31
41
|
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
@@ -200,96 +210,22 @@ check_tmux() {
|
|
|
200
210
|
brew install tmux 2>/dev/null || true
|
|
201
211
|
elif [[ "$OS" == "Linux" ]]; then
|
|
202
212
|
if command -v apt-get &> /dev/null; then
|
|
203
|
-
|
|
213
|
+
_sudo_or_skip apt-get update && _sudo_or_skip apt-get install -y tmux || true
|
|
204
214
|
elif command -v yum &> /dev/null; then
|
|
205
|
-
|
|
215
|
+
_sudo_or_skip yum install -y tmux || true
|
|
206
216
|
elif command -v pacman &> /dev/null; then
|
|
207
|
-
|
|
217
|
+
_sudo_or_skip pacman -Sy --noconfirm tmux || true
|
|
208
218
|
elif command -v apk &> /dev/null; then
|
|
209
|
-
|
|
219
|
+
_sudo_or_skip apk add --no-cache tmux || true
|
|
210
220
|
elif command -v zypper &> /dev/null; then
|
|
211
|
-
|
|
221
|
+
_sudo_or_skip zypper install -y tmux || true
|
|
212
222
|
else
|
|
213
|
-
print_warning "
|
|
214
|
-
install_tmux_from_source
|
|
215
|
-
return
|
|
223
|
+
print_warning "无法识别包管理器,请手动安装 tmux 或运行 remote-claude deps"
|
|
216
224
|
fi
|
|
217
225
|
fi
|
|
218
226
|
print_success "tmux 安装成功"
|
|
219
227
|
}
|
|
220
228
|
|
|
221
|
-
install_tmux_from_source() {
|
|
222
|
-
local TMUX_VERSION_TAG="3.6a"
|
|
223
|
-
local TMUX_URL="https://github.com/tmux/tmux/releases/download/${TMUX_VERSION_TAG}/tmux-${TMUX_VERSION_TAG}.tar.gz"
|
|
224
|
-
|
|
225
|
-
print_warning "包管理器版本不满足要求,尝试从源码编译 tmux ${TMUX_VERSION_TAG}..."
|
|
226
|
-
|
|
227
|
-
# 安装编译依赖
|
|
228
|
-
if [[ "$OS" == "Darwin" ]]; then
|
|
229
|
-
brew install libevent ncurses pkg-config bison 2>/dev/null || true
|
|
230
|
-
elif command -v apt-get &> /dev/null; then
|
|
231
|
-
sudo apt-get install -y build-essential libevent-dev libncurses5-dev libncursesw5-dev bison pkg-config || true
|
|
232
|
-
elif command -v yum &> /dev/null; then
|
|
233
|
-
sudo yum groupinstall -y "Development Tools" || true
|
|
234
|
-
sudo yum install -y libevent-devel ncurses-devel bison || true
|
|
235
|
-
fi
|
|
236
|
-
|
|
237
|
-
# 确定安装前缀
|
|
238
|
-
local PREFIX="/usr/local"
|
|
239
|
-
if ! sudo -n true 2>/dev/null; then
|
|
240
|
-
print_warning "无 sudo 权限,将安装到 \$HOME/.local"
|
|
241
|
-
PREFIX="$HOME/.local"
|
|
242
|
-
fi
|
|
243
|
-
|
|
244
|
-
# 创建临时目录,编译完成后清理
|
|
245
|
-
local TMPDIR
|
|
246
|
-
TMPDIR=$(mktemp -d)
|
|
247
|
-
trap "rm -rf '$TMPDIR'" RETURN
|
|
248
|
-
|
|
249
|
-
print_warning "下载 tmux-${TMUX_VERSION_TAG}.tar.gz..."
|
|
250
|
-
if ! curl -fsSL "$TMUX_URL" -o "$TMPDIR/tmux.tar.gz"; then
|
|
251
|
-
print_warning "下载失败,请检查网络或手动安装 tmux ${REQUIRED_MAJOR}.${REQUIRED_MINOR}+"
|
|
252
|
-
WARNINGS+=("tmux 源码下载失败,请手动安装 tmux ${REQUIRED_MAJOR}.${REQUIRED_MINOR}+")
|
|
253
|
-
return
|
|
254
|
-
fi
|
|
255
|
-
|
|
256
|
-
tar -xzf "$TMPDIR/tmux.tar.gz" -C "$TMPDIR"
|
|
257
|
-
local SRC_DIR
|
|
258
|
-
SRC_DIR=$(find "$TMPDIR" -maxdepth 1 -type d -name "tmux-*" | head -1)
|
|
259
|
-
|
|
260
|
-
print_warning "编译 tmux(可能需要几分钟)..."
|
|
261
|
-
if ! (cd "$SRC_DIR" && ./configure --prefix="$PREFIX" && make -j"$(nproc 2>/dev/null || echo 2)"); then
|
|
262
|
-
print_warning "编译失败,请手动安装 tmux ${REQUIRED_MAJOR}.${REQUIRED_MINOR}+"
|
|
263
|
-
WARNINGS+=("tmux 源码编译失败,请手动安装 tmux ${REQUIRED_MAJOR}.${REQUIRED_MINOR}+")
|
|
264
|
-
return
|
|
265
|
-
fi
|
|
266
|
-
|
|
267
|
-
if [[ "$PREFIX" == "/usr/local" ]]; then
|
|
268
|
-
sudo make -C "$SRC_DIR" install || { WARNINGS+=("tmux make install 失败,请手动安装 tmux ${REQUIRED_MAJOR}.${REQUIRED_MINOR}+"); return; }
|
|
269
|
-
else
|
|
270
|
-
make -C "$SRC_DIR" install || { WARNINGS+=("tmux make install 失败,请手动安装 tmux ${REQUIRED_MAJOR}.${REQUIRED_MINOR}+"); return; }
|
|
271
|
-
# 若 $HOME/.local/bin 不在 PATH 中,自动写入 shell 配置
|
|
272
|
-
if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then
|
|
273
|
-
export PATH="$HOME/.local/bin:$PATH"
|
|
274
|
-
local _RC
|
|
275
|
-
if [[ "$(basename "$SHELL")" == "zsh" ]]; then
|
|
276
|
-
_RC="$HOME/.zshrc"
|
|
277
|
-
else
|
|
278
|
-
_RC="$HOME/.bashrc"
|
|
279
|
-
fi
|
|
280
|
-
local _PATH_LINE='export PATH="$HOME/.local/bin:$PATH"'
|
|
281
|
-
if ! grep -qF "$HOME/.local/bin" "$_RC" 2>/dev/null; then
|
|
282
|
-
echo "" >> "$_RC"
|
|
283
|
-
echo "# remote-claude: tmux 路径" >> "$_RC"
|
|
284
|
-
echo "$_PATH_LINE" >> "$_RC"
|
|
285
|
-
print_success "已自动将 \$HOME/.local/bin 加入 PATH(写入 $_RC)"
|
|
286
|
-
fi
|
|
287
|
-
fi
|
|
288
|
-
fi
|
|
289
|
-
|
|
290
|
-
print_success "tmux ${TMUX_VERSION_TAG} 源码编译安装完成(前缀:${PREFIX})"
|
|
291
|
-
}
|
|
292
|
-
|
|
293
229
|
check_version() {
|
|
294
230
|
# tmux -V 输出格式:tmux 3.6 或 tmux 3.4a
|
|
295
231
|
local ver_str
|
|
@@ -310,30 +246,27 @@ check_tmux() {
|
|
|
310
246
|
print_success "$TMUX_VERSION 已安装(满足 >= ${REQUIRED_MAJOR}.${REQUIRED_MINOR})"
|
|
311
247
|
return
|
|
312
248
|
else
|
|
313
|
-
print_warning "$TMUX_VERSION 版本过低,需要 ${REQUIRED_MAJOR}.${REQUIRED_MINOR}
|
|
249
|
+
print_warning "$TMUX_VERSION 版本过低,需要 ${REQUIRED_MAJOR}.${REQUIRED_MINOR} 或更高,尝试通过包管理器升级..."
|
|
314
250
|
install_tmux
|
|
315
|
-
|
|
316
|
-
if ! check_version; then
|
|
317
|
-
install_tmux_from_source
|
|
318
|
-
if check_version; then
|
|
319
|
-
print_success "tmux 已升级至 $(tmux -V)"
|
|
320
|
-
else
|
|
321
|
-
print_warning "源码编译后版本仍不满足要求($(tmux -V)),需要 ${REQUIRED_MAJOR}.${REQUIRED_MINOR}+"
|
|
322
|
-
WARNINGS+=("tmux 版本不满足要求($(tmux -V)),需要 ${REQUIRED_MAJOR}.${REQUIRED_MINOR}+,请手动升级")
|
|
323
|
-
fi
|
|
324
|
-
else
|
|
251
|
+
if check_version; then
|
|
325
252
|
print_success "tmux 已升级至 $(tmux -V)"
|
|
253
|
+
else
|
|
254
|
+
print_warning "包管理器安装后版本仍不满足要求($(tmux -V)),需要 ${REQUIRED_MAJOR}.${REQUIRED_MINOR}+"
|
|
255
|
+
print_info "请运行 'remote-claude deps' 从源码编译安装 tmux ${REQUIRED_MAJOR}.${REQUIRED_MINOR}+"
|
|
256
|
+
WARNINGS+=("tmux 版本不满足要求($(tmux -V)),需要 ${REQUIRED_MAJOR}.${REQUIRED_MINOR}+,请运行 remote-claude deps 升级")
|
|
326
257
|
fi
|
|
327
258
|
fi
|
|
328
259
|
else
|
|
329
260
|
print_warning "未找到 tmux,正在安装..."
|
|
330
261
|
install_tmux
|
|
331
|
-
if
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
262
|
+
if command -v tmux &> /dev/null && check_version; then
|
|
263
|
+
print_success "tmux 已安装: $(tmux -V)"
|
|
264
|
+
else
|
|
265
|
+
local _cur_ver=""
|
|
266
|
+
command -v tmux &> /dev/null && _cur_ver="(当前: $(tmux -V))"
|
|
267
|
+
print_warning "tmux 安装后版本仍不满足要求${_cur_ver},需要 ${REQUIRED_MAJOR}.${REQUIRED_MINOR}+"
|
|
268
|
+
print_info "请运行 'remote-claude deps' 从源码编译安装 tmux ${REQUIRED_MAJOR}.${REQUIRED_MINOR}+"
|
|
269
|
+
WARNINGS+=("tmux 版本不满足要求,需要 ${REQUIRED_MAJOR}.${REQUIRED_MINOR}+,请运行 remote-claude deps 升级")
|
|
337
270
|
fi
|
|
338
271
|
fi
|
|
339
272
|
}
|
|
@@ -771,6 +771,16 @@ def build_stream_card(
|
|
|
771
771
|
|
|
772
772
|
# === 辅助卡片(保留不变)===
|
|
773
773
|
|
|
774
|
+
def _get_display_name(name: str, cwd: str = None) -> str:
|
|
775
|
+
"""从会话名和 CWD 获取显示名。自定义会话名直接用,默认路径名取 CWD 末段。"""
|
|
776
|
+
is_default = bool(_re.search(r'_\d{4}_\d{6}$', name))
|
|
777
|
+
if not is_default:
|
|
778
|
+
return name
|
|
779
|
+
if cwd:
|
|
780
|
+
return cwd.rstrip("/").rsplit("/", 1)[-1] or name
|
|
781
|
+
return name
|
|
782
|
+
|
|
783
|
+
|
|
774
784
|
def _build_session_list_elements(sessions: List[Dict], current_session: Optional[str], session_groups: Optional[Dict[str, str]], page: int = 0) -> List[Dict]:
|
|
775
785
|
"""构建会话列表元素(供 build_menu_card 复用)"""
|
|
776
786
|
import os
|
|
@@ -794,10 +804,7 @@ def _build_session_list_elements(sessions: List[Dict], current_session: Optional
|
|
|
794
804
|
|
|
795
805
|
status_icon = "🟢" if is_current else "⚪"
|
|
796
806
|
current_label = "(当前)" if is_current else ""
|
|
797
|
-
|
|
798
|
-
short_name = cwd.rstrip("/").rsplit("/", 1)[-1] or name
|
|
799
|
-
else:
|
|
800
|
-
short_name = name
|
|
807
|
+
short_name = _get_display_name(name, cwd)
|
|
801
808
|
|
|
802
809
|
# 构建4行内容:名字、cli类型、启动时间、目录
|
|
803
810
|
lines = [f"{status_icon} **{short_name}**{current_label}"]
|
|
@@ -1029,22 +1036,44 @@ def build_dir_card(target, entries: List[Dict], sessions: List[Dict], tree: bool
|
|
|
1029
1036
|
for sn, cid in session_groups.items():
|
|
1030
1037
|
if sn == auto_session or sn.startswith(auto_session + "_"):
|
|
1031
1038
|
matched_group_cid = cid
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1039
|
+
if matched_group_cid:
|
|
1040
|
+
action_btns = [
|
|
1041
|
+
{
|
|
1042
|
+
"tag": "button",
|
|
1043
|
+
"text": {"tag": "plain_text", "content": "进入群聊"},
|
|
1044
|
+
"type": "default",
|
|
1045
|
+
"behaviors": [{"type": "open_url",
|
|
1046
|
+
"default_url": f"https://applink.feishu.cn/client/chat/open?openChatId={matched_group_cid}",
|
|
1047
|
+
"android_url": f"https://applink.feishu.cn/client/chat/open?openChatId={matched_group_cid}",
|
|
1048
|
+
"ios_url": f"https://applink.feishu.cn/client/chat/open?openChatId={matched_group_cid}",
|
|
1049
|
+
"pc_url": f"https://applink.feishu.cn/client/chat/open?openChatId={matched_group_cid}"}]
|
|
1050
|
+
}
|
|
1051
|
+
]
|
|
1052
|
+
else:
|
|
1053
|
+
action_btns = [
|
|
1054
|
+
{
|
|
1055
|
+
"tag": "button",
|
|
1056
|
+
"text": {"tag": "plain_text", "content": "Claude群聊"},
|
|
1057
|
+
"type": "primary",
|
|
1058
|
+
"behaviors": [{"type": "callback", "value": {
|
|
1059
|
+
"action": "dir_new_group",
|
|
1060
|
+
"path": full_path,
|
|
1061
|
+
"session_name": auto_session,
|
|
1062
|
+
"cli_type": "claude"
|
|
1063
|
+
}}]
|
|
1064
|
+
},
|
|
1065
|
+
{
|
|
1066
|
+
"tag": "button",
|
|
1067
|
+
"text": {"tag": "plain_text", "content": "Codex群聊"},
|
|
1068
|
+
"type": "default",
|
|
1069
|
+
"behaviors": [{"type": "callback", "value": {
|
|
1070
|
+
"action": "dir_new_group",
|
|
1071
|
+
"path": full_path,
|
|
1072
|
+
"session_name": auto_session,
|
|
1073
|
+
"cli_type": "codex"
|
|
1074
|
+
}}]
|
|
1075
|
+
}
|
|
1076
|
+
]
|
|
1048
1077
|
elements.append({
|
|
1049
1078
|
"tag": "column_set",
|
|
1050
1079
|
"flex_mode": "none",
|
|
@@ -1067,19 +1096,7 @@ def build_dir_card(target, entries: List[Dict], sessions: List[Dict], tree: bool
|
|
|
1067
1096
|
"tag": "column",
|
|
1068
1097
|
"width": "weighted",
|
|
1069
1098
|
"weight": 2,
|
|
1070
|
-
"elements":
|
|
1071
|
-
{
|
|
1072
|
-
"tag": "button",
|
|
1073
|
-
"text": {"tag": "plain_text", "content": "Claude"},
|
|
1074
|
-
"type": "primary",
|
|
1075
|
-
"behaviors": [{"type": "callback", "value": {
|
|
1076
|
-
"action": "dir_start",
|
|
1077
|
-
"path": full_path,
|
|
1078
|
-
"session_name": auto_session
|
|
1079
|
-
}}]
|
|
1080
|
-
},
|
|
1081
|
-
group_btn
|
|
1082
|
-
]
|
|
1099
|
+
"elements": action_btns
|
|
1083
1100
|
}
|
|
1084
1101
|
]
|
|
1085
1102
|
})
|
|
@@ -1152,6 +1169,9 @@ def build_help_card() -> Dict[str, Any]:
|
|
|
1152
1169
|
**群聊协作**
|
|
1153
1170
|
• `/new-group <会话名>` - 创建专属群聊,多人共用同一 Claude
|
|
1154
1171
|
|
|
1172
|
+
**按键控制**
|
|
1173
|
+
• `/press <按键>` - 发送按键到会话(如 `/press ctrl+c`、`/press esc`、`/press ctrl+f`)
|
|
1174
|
+
|
|
1155
1175
|
**其他**
|
|
1156
1176
|
• `/help` - 显示此帮助
|
|
1157
1177
|
• `/menu` - 快捷操作面板"""
|
|
@@ -249,6 +249,8 @@ class LarkHandler:
|
|
|
249
249
|
await self._cmd_help(user_id, chat_id)
|
|
250
250
|
elif command == "/menu":
|
|
251
251
|
await self._cmd_menu(user_id, chat_id)
|
|
252
|
+
elif command == "/press":
|
|
253
|
+
await self._cmd_press(user_id, chat_id, args)
|
|
252
254
|
else:
|
|
253
255
|
await card_service.send_text(chat_id, f"未知命令: {command}\n使用 /help 查看帮助")
|
|
254
256
|
|
|
@@ -306,7 +308,7 @@ class LarkHandler:
|
|
|
306
308
|
if card_id:
|
|
307
309
|
await card_service.send_card(chat_id, card_id)
|
|
308
310
|
|
|
309
|
-
async def _cmd_start(self, user_id: str, chat_id: str, args: str):
|
|
311
|
+
async def _cmd_start(self, user_id: str, chat_id: str, args: str, cli_type: str = "claude"):
|
|
310
312
|
"""启动新会话"""
|
|
311
313
|
parts = args.strip().split(maxsplit=1)
|
|
312
314
|
if not parts:
|
|
@@ -348,10 +350,15 @@ class LarkHandler:
|
|
|
348
350
|
script_dir = Path(__file__).parent.parent.absolute()
|
|
349
351
|
server_script = script_dir / "server" / "server.py"
|
|
350
352
|
cmd = ["uv", "run", "--project", str(script_dir), "python3", str(server_script), session_name]
|
|
353
|
+
if cli_type == "codex":
|
|
354
|
+
cmd += ["--cli-type", "codex"]
|
|
351
355
|
if self._poller.get_bypass_enabled():
|
|
352
|
-
|
|
356
|
+
if cli_type == "codex":
|
|
357
|
+
cmd += ["--", "--dangerously-bypass-approvals-and-sandbox"]
|
|
358
|
+
else:
|
|
359
|
+
cmd += ["--", "--dangerously-skip-permissions", "--permission-mode=dontAsk"]
|
|
353
360
|
|
|
354
|
-
logger.info(f"启动会话: {session_name}, 工作目录: {work_dir}, 命令: {' '.join(cmd)}")
|
|
361
|
+
logger.info(f"启动会话: {session_name}, 工作目录: {work_dir}, cli_type: {cli_type}, 命令: {' '.join(cmd)}")
|
|
355
362
|
_track_stats('lark', 'cmd_start', session_name=session_name, chat_id=chat_id)
|
|
356
363
|
|
|
357
364
|
try:
|
|
@@ -408,7 +415,7 @@ class LarkHandler:
|
|
|
408
415
|
self._starting_sessions.discard(session_name)
|
|
409
416
|
|
|
410
417
|
async def _cmd_start_and_new_group(self, user_id: str, chat_id: str,
|
|
411
|
-
session_name: str, path: str):
|
|
418
|
+
session_name: str, path: str, cli_type: str = "claude"):
|
|
412
419
|
"""在指定目录启动会话并创建专属群聊"""
|
|
413
420
|
work_path = Path(path).expanduser()
|
|
414
421
|
if not work_path.is_dir():
|
|
@@ -426,8 +433,13 @@ class LarkHandler:
|
|
|
426
433
|
script_dir = Path(__file__).parent.parent.absolute()
|
|
427
434
|
server_script = script_dir / "server" / "server.py"
|
|
428
435
|
cmd = ["uv", "run", "--project", str(script_dir), "python3", str(server_script), session_name]
|
|
436
|
+
if cli_type == "codex":
|
|
437
|
+
cmd += ["--cli-type", "codex"]
|
|
429
438
|
if self._poller.get_bypass_enabled():
|
|
430
|
-
|
|
439
|
+
if cli_type == "codex":
|
|
440
|
+
cmd += ["--", "--dangerously-bypass-approvals-and-sandbox"]
|
|
441
|
+
else:
|
|
442
|
+
cmd += ["--", "--dangerously-skip-permissions", "--permission-mode=dontAsk"]
|
|
431
443
|
|
|
432
444
|
try:
|
|
433
445
|
env = _os.environ.copy()
|
|
@@ -732,7 +744,8 @@ class LarkHandler:
|
|
|
732
744
|
session = next((s for s in sessions if s["name"] == session_name), None)
|
|
733
745
|
pid = session.get("pid") if session else None
|
|
734
746
|
cwd = self._get_pid_cwd(pid) if pid else None
|
|
735
|
-
|
|
747
|
+
from .card_builder import _get_display_name
|
|
748
|
+
dir_label = _get_display_name(session_name, cwd)
|
|
736
749
|
|
|
737
750
|
from . import config
|
|
738
751
|
try:
|
|
@@ -1033,6 +1046,105 @@ class LarkHandler:
|
|
|
1033
1046
|
|
|
1034
1047
|
# ── 快捷键发送 ─────────────────────────────────────────────────────────────
|
|
1035
1048
|
|
|
1049
|
+
@staticmethod
|
|
1050
|
+
def _parse_key_combo(combo: str) -> Optional[bytes]:
|
|
1051
|
+
"""将用户输入的按键字符串解析为终端转义序列字节,解析失败返回 None"""
|
|
1052
|
+
BASE_KEY_MAP = {
|
|
1053
|
+
"up": b"\x1b[A",
|
|
1054
|
+
"down": b"\x1b[B",
|
|
1055
|
+
"right": b"\x1b[C",
|
|
1056
|
+
"left": b"\x1b[D",
|
|
1057
|
+
"enter": b"\r",
|
|
1058
|
+
"esc": b"\x1b",
|
|
1059
|
+
"tab": b"\t",
|
|
1060
|
+
"backspace": b"\x7f",
|
|
1061
|
+
"delete": b"\x1b[3~",
|
|
1062
|
+
"space": b" ",
|
|
1063
|
+
"home": b"\x1b[H",
|
|
1064
|
+
"end": b"\x1b[F",
|
|
1065
|
+
"pageup": b"\x1b[5~",
|
|
1066
|
+
"pagedown": b"\x1b[6~",
|
|
1067
|
+
"f1": b"\x1bOP", "f2": b"\x1bOQ", "f3": b"\x1bOR", "f4": b"\x1bOS",
|
|
1068
|
+
"f5": b"\x1b[15~", "f6": b"\x1b[17~","f7": b"\x1b[18~","f8": b"\x1b[19~",
|
|
1069
|
+
"f9": b"\x1b[20~", "f10": b"\x1b[21~","f11": b"\x1b[23~","f12": b"\x1b[24~",
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
s = combo.strip().lower()
|
|
1073
|
+
parts = [p.strip() for p in s.split("+")]
|
|
1074
|
+
|
|
1075
|
+
mods = set()
|
|
1076
|
+
keys = []
|
|
1077
|
+
for p in parts:
|
|
1078
|
+
if p in ("ctrl", "alt", "shift"):
|
|
1079
|
+
mods.add(p)
|
|
1080
|
+
else:
|
|
1081
|
+
keys.append(p)
|
|
1082
|
+
|
|
1083
|
+
if len(keys) != 1:
|
|
1084
|
+
return None
|
|
1085
|
+
key = keys[0]
|
|
1086
|
+
|
|
1087
|
+
# ctrl+letter → \x01-\x1a
|
|
1088
|
+
if mods == {"ctrl"}:
|
|
1089
|
+
if len(key) == 1 and 'a' <= key <= 'z':
|
|
1090
|
+
return bytes([ord(key) - ord('a') + 1])
|
|
1091
|
+
# ctrl+[ = ESC, ctrl+\ = FS 等特殊控制字符
|
|
1092
|
+
ctrl_special = {'[': b'\x1b', '\\': b'\x1c', ']': b'\x1d', '^': b'\x1e', '_': b'\x1f'}
|
|
1093
|
+
if key in ctrl_special:
|
|
1094
|
+
return ctrl_special[key]
|
|
1095
|
+
return None
|
|
1096
|
+
|
|
1097
|
+
# alt+key → ESC prefix
|
|
1098
|
+
if mods == {"alt"}:
|
|
1099
|
+
base = BASE_KEY_MAP.get(key)
|
|
1100
|
+
if base:
|
|
1101
|
+
return b"\x1b" + base
|
|
1102
|
+
if len(key) == 1:
|
|
1103
|
+
return b"\x1b" + key.encode()
|
|
1104
|
+
return None
|
|
1105
|
+
|
|
1106
|
+
# shift+tab / shift+enter
|
|
1107
|
+
if mods == {"shift"}:
|
|
1108
|
+
if key == "tab":
|
|
1109
|
+
return b"\x1b[Z"
|
|
1110
|
+
if key == "enter":
|
|
1111
|
+
return b"\x1b[13;2u"
|
|
1112
|
+
return None
|
|
1113
|
+
|
|
1114
|
+
# 无修饰键
|
|
1115
|
+
if not mods:
|
|
1116
|
+
return BASE_KEY_MAP.get(key)
|
|
1117
|
+
|
|
1118
|
+
return None
|
|
1119
|
+
|
|
1120
|
+
async def _cmd_press(self, user_id: str, chat_id: str, args: str):
|
|
1121
|
+
"""发送任意按键组合到会话"""
|
|
1122
|
+
combo = args.strip()
|
|
1123
|
+
if not combo:
|
|
1124
|
+
await card_service.send_text(
|
|
1125
|
+
chat_id,
|
|
1126
|
+
"用法:`/press <按键>`\n"
|
|
1127
|
+
"例如:`/press ctrl+c`、`/press esc`、`/press ctrl+f`、`/press alt+x`\n"
|
|
1128
|
+
"支持:ctrl/alt/shift 修饰键,方向键 up/down/left/right,enter/esc/tab/backspace/delete/space/home/end/pageup/pagedown/f1-f12"
|
|
1129
|
+
)
|
|
1130
|
+
return
|
|
1131
|
+
|
|
1132
|
+
raw = LarkHandler._parse_key_combo(combo)
|
|
1133
|
+
if raw is None:
|
|
1134
|
+
await card_service.send_text(chat_id, f"❌ 无法解析按键:`{combo}`\n使用 `/press` 查看支持的按键格式")
|
|
1135
|
+
return
|
|
1136
|
+
|
|
1137
|
+
bridge = self._bridges.get(chat_id)
|
|
1138
|
+
if not bridge or not bridge.running:
|
|
1139
|
+
await card_service.send_text(chat_id, "❌ 当前未连接到会话,请先使用 /attach 连接")
|
|
1140
|
+
return
|
|
1141
|
+
|
|
1142
|
+
success = await bridge.send_raw(raw)
|
|
1143
|
+
if success:
|
|
1144
|
+
logger.info(f"[press] 发送按键 {combo!r} ({raw!r}) 到会话")
|
|
1145
|
+
else:
|
|
1146
|
+
await card_service.send_text(chat_id, f"❌ 发送按键 `{combo}` 失败")
|
|
1147
|
+
|
|
1036
1148
|
async def send_raw_key(self, user_id: str, chat_id: str, key_name: str):
|
|
1037
1149
|
"""发送原始控制键到 Claude CLI"""
|
|
1038
1150
|
_track_stats('lark', 'raw_key',
|
package/lark_client/main.py
CHANGED
|
@@ -250,16 +250,18 @@ def handle_card_action(event: P2CardActionTrigger) -> P2CardActionTriggerRespons
|
|
|
250
250
|
if action_type == "dir_start":
|
|
251
251
|
path = action_value.get("path", "")
|
|
252
252
|
session_name = action_value.get("session_name", "")
|
|
253
|
-
|
|
254
|
-
|
|
253
|
+
cli_type = action_value.get("cli_type", "claude")
|
|
254
|
+
print(f"[Lark] dir_start: path={path}, session={session_name}, cli_type={cli_type}")
|
|
255
|
+
asyncio.create_task(handler._cmd_start(user_id, chat_id, f"{session_name} {path}", cli_type=cli_type))
|
|
255
256
|
return None
|
|
256
257
|
|
|
257
258
|
# 目录卡片:在该目录启动会话并创建专属群聊
|
|
258
259
|
if action_type == "dir_new_group":
|
|
259
260
|
path = action_value.get("path", "")
|
|
260
261
|
session_name = action_value.get("session_name", "")
|
|
261
|
-
|
|
262
|
-
|
|
262
|
+
cli_type = action_value.get("cli_type", "claude")
|
|
263
|
+
print(f"[Lark] dir_new_group: path={path}, session={session_name}, cli_type={cli_type}")
|
|
264
|
+
asyncio.create_task(handler._cmd_start_and_new_group(user_id, chat_id, session_name, path, cli_type=cli_type))
|
|
263
265
|
return None
|
|
264
266
|
|
|
265
267
|
# /menu 卡片按钮
|