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 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 "${PWD}_$(date +%m%d_%H%M%S)" --cli codex -- "$@"
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 "${PWD}_$(date +%m%d_%H%M%S)" -- --dangerously-skip-permissions --permission-mode=dontAsk "$@"
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 "${PWD}_$(date +%m%d_%H%M%S)" -- "$@"
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 "${PWD}_$(date +%m%d_%H%M%S)" --cli codex -- --dangerously-bypass-approvals-and-sandbox "$@"
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 名,/ . 替换为 _(与 _safe_filename 一致)
21
- SESSION_SAFE=$(echo "$2" | tr '/.' '__')
22
- LOG_FILE="$LOG_DIR/${SESSION_SAFE}_messages.log"
20
+ # 指定 session 名,/ . 空格替换为 _(与 _log_filename 一致)
21
+ SESSION_LOG=$(echo "$2" | tr '/. ' '___')
22
+ LOG_FILE="$LOG_DIR/${SESSION_LOG}_messages.log"
23
23
  else
24
- # 找最后启动的 session(按创建时间排序,排除 lark.pid)
25
- if [[ "$OSTYPE" == "darwin"* ]]; then
26
- # macOS: stat -f "%B" 返回创建时间(秒)
27
- LATEST_PID=$(ls "$LOG_DIR"/_*.pid 2>/dev/null | while read f; do
28
- stat -f "%B:%N" "$f" 2>/dev/null
29
- done | sort -rn | head -1 | cut -d: -f2)
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
@@ -111,7 +111,7 @@ class RemoteClient:
111
111
  async def run(self):
112
112
  """运行客户端"""
113
113
  if not await self.connect():
114
- return
114
+ raise SystemExit(1)
115
115
 
116
116
  self.running = True
117
117
  _track_stats('terminal', 'connect', session_name=self.session_name)
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
- sudo apt-get update && sudo apt-get install -y tmux || true
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
- sudo yum install -y tmux || true
215
+ _sudo_or_skip yum install -y tmux || true
206
216
  elif command -v pacman &> /dev/null; then
207
- sudo pacman -Sy --noconfirm tmux || true
217
+ _sudo_or_skip pacman -Sy --noconfirm tmux || true
208
218
  elif command -v apk &> /dev/null; then
209
- sudo apk add --no-cache tmux || true
219
+ _sudo_or_skip apk add --no-cache tmux || true
210
220
  elif command -v zypper &> /dev/null; then
211
- sudo zypper install -y tmux || true
221
+ _sudo_or_skip zypper install -y tmux || true
212
222
  else
213
- print_warning "无法识别包管理器,尝试从源码编译 tmux..."
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 ! check_version; then
332
- install_tmux_from_source
333
- if ! check_version; then
334
- print_warning "源码编译后版本仍不满足要求($(tmux -V)),需要 ${REQUIRED_MAJOR}.${REQUIRED_MINOR}+"
335
- WARNINGS+=("tmux 版本不满足要求($(tmux -V)),需要 ${REQUIRED_MAJOR}.${REQUIRED_MINOR}+,请手动升级")
336
- fi
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
- if cwd:
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
- group_btn = {
1033
- "tag": "button",
1034
- "text": {"tag": "plain_text", "content": "进入群聊" if matched_group_cid else "创建群聊"},
1035
- "type": "default",
1036
- "behaviors": [{"type": "open_url",
1037
- "default_url": f"https://applink.feishu.cn/client/chat/open?openChatId={matched_group_cid}",
1038
- "android_url": f"https://applink.feishu.cn/client/chat/open?openChatId={matched_group_cid}",
1039
- "ios_url": f"https://applink.feishu.cn/client/chat/open?openChatId={matched_group_cid}",
1040
- "pc_url": f"https://applink.feishu.cn/client/chat/open?openChatId={matched_group_cid}"}]
1041
- if matched_group_cid else
1042
- [{"type": "callback", "value": {
1043
- "action": "dir_new_group",
1044
- "path": full_path,
1045
- "session_name": auto_session
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
- cmd += ["--", "--dangerously-skip-permissions", "--permission-mode=dontAsk"]
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
- cmd += ["--", "--dangerously-skip-permissions", "--permission-mode=dontAsk"]
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
- dir_label = cwd.rstrip("/").rsplit("/", 1)[-1] if cwd else session_name
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',
@@ -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
- print(f"[Lark] dir_start: path={path}, session={session_name}")
254
- asyncio.create_task(handler._cmd_start(user_id, chat_id, f"{session_name} {path}"))
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
- print(f"[Lark] dir_new_group: path={path}, session={session_name}")
262
- asyncio.create_task(handler._cmd_start_and_new_group(user_id, chat_id, session_name, path))
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 卡片按钮