remote-claude 0.2.10 → 0.2.11

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
@@ -22,3 +22,7 @@ ALLOWED_USERS=ou_xxxxx,ou_yyyyy
22
22
  # 单张卡片最多显示的 block 数量,超限自动冻结并创建新卡片(默认 50)
23
23
  # MAX_CARD_BLOCKS=50
24
24
 
25
+ # lark_client 日志级别(可选,默认 INFO)
26
+ # 支持: DEBUG / INFO / WARNING / ERROR
27
+ # LARK_LOG_LEVEL=INFO
28
+
package/README.md CHANGED
@@ -43,7 +43,7 @@ cd remote_claude
43
43
  ./init.sh
44
44
  ```
45
45
 
46
- `init.sh` 会自动安装 uv、tmux 等依赖,配置飞书环境(可选),并写入 `cla` / `cl` 快捷命令。执行完成后重启终端生效。
46
+ `init.sh` 会自动安装 uv、tmux 等依赖,配置飞书环境(可选),并写入 `cla` / `cl` / `cx` / `cdx` 快捷命令。执行完成后重启终端生效。
47
47
 
48
48
  ### 2. 启动
49
49
 
@@ -51,6 +51,8 @@ cd remote_claude
51
51
  |------|------|
52
52
  | `cla` | 启动 Claude (以当前目录路径为会话名) |
53
53
  | `cl` | 同 `cla`,但跳过权限确认 |
54
+ | `cx` | 启动 Codex (以当前目录路径为会话名,跳过权限确认) |
55
+ | `cdx` | 同 `cx`,但需要确认权限 |
54
56
  | `remote-claude` | 管理工具(一般不用)|
55
57
 
56
58
  ### 3. 从其他终端连接(比较少用)
@@ -210,6 +212,8 @@ remote-claude attach <会话名>
210
212
  |------|------|
211
213
  | `cla` | 启动飞书客户端 + 以当前目录路径为会话名启动 Claude |
212
214
  | `cl` | 同 `cla`,但跳过权限确认 |
215
+ | `cx` | 启动飞书客户端 + 以当前目录路径为会话名启动 Codex(跳过权限确认)|
216
+ | `cdx` | 同 `cx`,但需要确认权限 |
213
217
 
214
218
  ### 管理命令 (一般不需要)
215
219
 
@@ -260,7 +264,8 @@ CLAUDE_COMMAND=/usr/local/bin/claude
260
264
  ## 系统要求
261
265
 
262
266
  - **操作系统**: macOS 或 Linux
263
- - **依赖工具**: [uv](https://docs.astral.sh/uv/)、[tmux](https://github.com/tmux/tmux)、[Claude CLI](https://claude.ai/code)
267
+ - **依赖工具**: [uv](https://docs.astral.sh/uv/)、[tmux](https://github.com/tmux/tmux)
268
+ - **CLI 工具**: [Claude CLI](https://claude.ai/code) 或 [Codex CLI](https://github.com/openai/codex)
264
269
  - **可选**: 飞书企业自建应用
265
270
 
266
271
  ## 文档
package/bin/cdx ADDED
@@ -0,0 +1,20 @@
1
+ #!/bin/bash
2
+ # 解析符号链接,兼容 macOS(不支持 readlink -f)
3
+ SOURCE="$0"
4
+ while [ -L "$SOURCE" ]; do
5
+ DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
6
+ SOURCE="$(readlink "$SOURCE")"
7
+ [[ "$SOURCE" != /* ]] && SOURCE="$DIR/$SOURCE"
8
+ done
9
+ SCRIPT_DIR="$(cd -P "$(dirname "$SOURCE")" && cd .. && pwd)"
10
+
11
+ # uv 路径兜底
12
+ if ! command -v uv &>/dev/null; then
13
+ [ -f "$HOME/.local/bin/uv" ] && export PATH="$HOME/.local/bin:$PATH"
14
+ fi
15
+
16
+ # 检查飞书配置
17
+ source "$SCRIPT_DIR/scripts/check-env.sh" "$SCRIPT_DIR"
18
+
19
+ 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 -- "$@"
package/bin/cx ADDED
@@ -0,0 +1,20 @@
1
+ #!/bin/bash
2
+ # 解析符号链接,兼容 macOS(不支持 readlink -f)
3
+ SOURCE="$0"
4
+ while [ -L "$SOURCE" ]; do
5
+ DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
6
+ SOURCE="$(readlink "$SOURCE")"
7
+ [[ "$SOURCE" != /* ]] && SOURCE="$DIR/$SOURCE"
8
+ done
9
+ SCRIPT_DIR="$(cd -P "$(dirname "$SOURCE")" && cd .. && pwd)"
10
+
11
+ # uv 路径兜底
12
+ if ! command -v uv &>/dev/null; then
13
+ [ -f "$HOME/.local/bin/uv" ] && export PATH="$HOME/.local/bin:$PATH"
14
+ fi
15
+
16
+ # 检查飞书配置
17
+ source "$SCRIPT_DIR/scripts/check-env.sh" "$SCRIPT_DIR"
18
+
19
+ 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 "$@"
package/bin/remote-claude CHANGED
@@ -21,8 +21,18 @@ if [ "$1" = "log" ]; then
21
21
  SESSION_SAFE=$(echo "$2" | tr '/.' '__')
22
22
  LOG_FILE="$LOG_DIR/${SESSION_SAFE}_messages.log"
23
23
  else
24
- # 找最后启动的 session(最新的 session .pid 文件,排除 lark.pid)
25
- LATEST_PID=$(ls -t "$LOG_DIR"/_*.pid 2>/dev/null | head -1)
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
26
36
  if [ -n "$LATEST_PID" ]; then
27
37
  SESSION_SAFE=$(basename "$LATEST_PID" .pid)
28
38
  LOG_FILE="$LOG_DIR/${SESSION_SAFE}_messages.log"
@@ -0,0 +1,3 @@
1
+ """
2
+ Remote Claude 终端客户端模块
3
+ """
package/init.sh CHANGED
@@ -350,6 +350,26 @@ check_claude() {
350
350
  fi
351
351
  }
352
352
 
353
+ # 检查 Codex CLI
354
+ check_codex() {
355
+ print_header "检查 Codex CLI"
356
+
357
+ if command -v codex &> /dev/null; then
358
+ print_success "Codex CLI 已安装"
359
+ return
360
+ fi
361
+
362
+ print_warning "未找到 Codex CLI"
363
+ print_info "请运行以下命令安装 Codex CLI:"
364
+ print_info " npm install -g @openai/codex"
365
+ print_info "或访问 https://github.com/openai/codex 了解更多"
366
+
367
+ if $NPM_MODE; then
368
+ print_info "(npm 模式:跳过交互,请安装后重新运行)"
369
+ return
370
+ fi
371
+ }
372
+
353
373
  # 安装 Python 依赖
354
374
  install_dependencies() {
355
375
  print_header "安装 Python 依赖"
@@ -449,7 +469,7 @@ configure_shell() {
449
469
  print_header "安装快捷命令"
450
470
 
451
471
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
452
- chmod +x "$SCRIPT_DIR/bin/cla" "$SCRIPT_DIR/bin/cl" "$SCRIPT_DIR/bin/remote-claude" 2>/dev/null || true
472
+ chmod +x "$SCRIPT_DIR/bin/cla" "$SCRIPT_DIR/bin/cl" "$SCRIPT_DIR/bin/cx" "$SCRIPT_DIR/bin/cdx" "$SCRIPT_DIR/bin/remote-claude" 2>/dev/null || true
453
473
 
454
474
  # 优先 /usr/local/bin,权限不够则选 ~/bin 或 ~/.local/bin 中已在 PATH 里的
455
475
  BIN_DIR="/usr/local/bin"
@@ -479,15 +499,21 @@ configure_shell() {
479
499
  mkdir -p "$BIN_DIR"
480
500
  ln -sf "$SCRIPT_DIR/bin/cla" "$BIN_DIR/cla" 2>/dev/null || true
481
501
  ln -sf "$SCRIPT_DIR/bin/cl" "$BIN_DIR/cl" 2>/dev/null || true
502
+ ln -sf "$SCRIPT_DIR/bin/cx" "$BIN_DIR/cx" 2>/dev/null || true
503
+ ln -sf "$SCRIPT_DIR/bin/cdx" "$BIN_DIR/cdx" 2>/dev/null || true
482
504
  ln -sf "$SCRIPT_DIR/bin/remote-claude" "$BIN_DIR/remote-claude" 2>/dev/null || true
483
505
  else
484
506
  ln -sf "$SCRIPT_DIR/bin/cl" "$BIN_DIR/cl" 2>/dev/null || true
507
+ ln -sf "$SCRIPT_DIR/bin/cx" "$BIN_DIR/cx" 2>/dev/null || true
508
+ ln -sf "$SCRIPT_DIR/bin/cdx" "$BIN_DIR/cdx" 2>/dev/null || true
485
509
  ln -sf "$SCRIPT_DIR/bin/remote-claude" "$BIN_DIR/remote-claude" 2>/dev/null || true
486
510
  fi
487
511
 
488
- print_success "已安装 cla、cl 和 remote-claude 到 $BIN_DIR"
512
+ print_success "已安装 cla、cl、cx、cdx 和 remote-claude 到 $BIN_DIR"
489
513
  print_info " cla - 启动飞书客户端 + 以当前目录路径+时间戳为会话名启动 Claude"
490
514
  print_info " cl - 同 cla,但跳过权限确认"
515
+ print_info " cx - 启动飞书客户端 + 以当前目录路径+时间戳为会话名启动 Codex(跳过权限)"
516
+ print_info " cdx - 同 cx,但需确认权限"
491
517
  print_info " remote-claude - Remote Claude 主命令(start/attach/list/kill/lark)"
492
518
 
493
519
  # 安装 shell 自动补全
@@ -536,6 +562,8 @@ ${YELLOW}快捷命令:${NC}
536
562
 
537
563
  ${GREEN}cla${NC} - 启动飞书客户端 + 以当前目录+时间戳为会话名启动 Claude
538
564
  ${GREEN}cl${NC} - 同 cla,但跳过权限确认
565
+ ${GREEN}cx${NC} - 启动飞书客户端 + 以当前目录+时间戳为会话名启动 Codex(跳过权限)
566
+ ${GREEN}cdx${NC} - 同 cx,但需确认权限
539
567
 
540
568
  详细使用说明请阅读 README.md
541
569
 
@@ -565,6 +593,7 @@ main() {
565
593
  check_uv
566
594
  check_tmux
567
595
  check_claude
596
+ check_codex
568
597
  install_dependencies
569
598
  if ! $NPM_MODE; then
570
599
  configure_lark
@@ -784,29 +784,36 @@ def _build_session_list_elements(sessions: List[Dict], current_session: Optional
784
784
  name = s["name"]
785
785
  cwd = s.get("cwd", "")
786
786
  start_time = s.get("start_time", "")
787
+ cli_type = s.get("cli_type", "claude")
787
788
  is_current = (name == current_session)
788
789
 
790
+ # CLI 类型颜色和标签:Claude=黄色,Codex=绿色
791
+ cli_color = "yellow" if cli_type == "claude" else "green"
792
+ cli_label = CLI_NAMES.get(cli_type, "Claude")
793
+
789
794
  status_icon = "🟢" if is_current else "⚪"
790
795
  current_label = "(当前)" if is_current else ""
791
796
  if cwd:
792
797
  short_name = cwd.rstrip("/").rsplit("/", 1)[-1] or name
793
798
  else:
794
799
  short_name = name
795
- meta_parts = []
800
+
801
+ # 构建4行内容:名字、cli类型、启动时间、目录
802
+ lines = [f"{status_icon} **{short_name}**{current_label}"]
803
+ lines.append(f"<font color=\"{cli_color}\">{cli_label}</font>")
804
+
796
805
  if start_time:
797
- meta_parts.append(f"启动:{start_time}")
806
+ lines.append(f"启动:{start_time}")
807
+
798
808
  if cwd:
799
809
  home = os.path.expanduser("~")
800
810
  display_cwd = cwd.replace(home, "~")
801
811
  if len(display_cwd) > 40:
802
812
  parts = display_cwd.rstrip("/").rsplit("/", 2)
803
813
  display_cwd = "…/" + "/".join(parts[-2:]) if len(parts) > 2 else display_cwd[-40:]
804
- meta_parts.append(f"`{display_cwd}`")
805
- meta_str = " ".join(meta_parts) if meta_parts else ""
814
+ lines.append(f"`{display_cwd}`")
806
815
 
807
- header_text = f"{status_icon} **{short_name}**{current_label}"
808
- if meta_str:
809
- header_text += f"\n{meta_str}"
816
+ header_text = "\n".join(lines)
810
817
 
811
818
  if is_current:
812
819
  btn_label = "断开连接"
@@ -38,3 +38,13 @@ GROUP_NAME_PREFIX = os.getenv("GROUP_NAME_PREFIX", "【Remote-Claude】")
38
38
 
39
39
  # 流式卡片配置
40
40
  MAX_CARD_BLOCKS = int(os.getenv("MAX_CARD_BLOCKS", "50"))
41
+
42
+ # lark_client 日志级别(可选,默认 INFO)
43
+ # 支持: DEBUG / INFO / WARNING / ERROR
44
+ _LARK_LOG_LEVEL = os.getenv("LARK_LOG_LEVEL", "INFO").upper()
45
+ LARK_LOG_LEVEL = {
46
+ "DEBUG": 10,
47
+ "INFO": 20,
48
+ "WARNING": 30,
49
+ "ERROR": 40,
50
+ }.get(_LARK_LOG_LEVEL, 20) # 默认 INFO
@@ -12,16 +12,60 @@ import signal
12
12
  import sys
13
13
  from pathlib import Path
14
14
 
15
+
16
+ # 设置 sys.path 以导入 utils 模块
17
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
18
+ from utils.session import USER_DATA_DIR
19
+
20
+
21
+ def _setup_logging():
22
+ """配置 lark_client 日志:INFO → lark_client.log, DEBUG → lark_client.debug.log"""
23
+ from .config import LARK_LOG_LEVEL
24
+
25
+ log_dir = USER_DATA_DIR
26
+ log_dir.mkdir(parents=True, exist_ok=True)
27
+
28
+ # 日志格式(含毫秒级时间戳)
29
+ log_format = "%(asctime)s.%(msecs)03d [%(name)s] %(levelname)s %(message)s"
30
+ date_format = "%Y-%m-%d %H:%M:%S"
31
+ formatter = logging.Formatter(log_format, datefmt=date_format)
32
+
33
+ # 根 logger 配置
34
+ root_logger = logging.getLogger()
35
+ root_logger.setLevel(LARK_LOG_LEVEL)
36
+
37
+ # 清除默认 handler
38
+ root_logger.handlers.clear()
39
+
40
+ # 正常日志文件(INFO 及以上)
41
+ info_handler = logging.FileHandler(log_dir / "lark_client.log", encoding="utf-8")
42
+ info_handler.setLevel(logging.INFO)
43
+ info_handler.setFormatter(formatter)
44
+ root_logger.addHandler(info_handler)
45
+
46
+ # 调试日志文件(DEBUG 及以上,仅当 LARK_LOG_LEVEL=DEBUG 时写入)
47
+ if LARK_LOG_LEVEL == logging.DEBUG:
48
+ debug_handler = logging.FileHandler(log_dir / "lark_client.debug.log", encoding="utf-8")
49
+ debug_handler.setLevel(logging.DEBUG)
50
+ debug_handler.setFormatter(formatter)
51
+ root_logger.addHandler(debug_handler)
52
+
53
+ # 第三方库保持 INFO 级别
54
+ for _noisy in ('urllib3', 'websockets', 'asyncio'):
55
+ logging.getLogger(_noisy).setLevel(logging.INFO)
56
+
57
+ # 控制台输出(仅重要消息,无调试信息)
58
+ console_handler = logging.StreamHandler()
59
+ console_handler.setLevel(logging.INFO)
60
+ console_handler.setFormatter(formatter)
61
+ root_logger.addHandler(console_handler)
62
+
63
+
64
+ # 在导入 lark SDK 之前配置日志
65
+ _setup_logging()
66
+
15
67
  import lark_oapi as lark
16
68
 
17
- # 在 SDK 配置 logging 之前,先设置根 logger 和我们自己模块的 DEBUG 级别
18
- logging.basicConfig(
19
- level=logging.DEBUG,
20
- format='[%(name)s] %(message)s',
21
- )
22
- # 将噪音较大的第三方库保持 INFO 级别
23
- for _noisy in ('urllib3', 'websockets', 'asyncio'):
24
- logging.getLogger(_noisy).setLevel(logging.INFO)
25
69
  from lark_oapi.api.im.v1 import P2ImMessageReceiveV1
26
70
  from lark_oapi.event.callback.model.p2_card_action_trigger import (
27
71
  P2CardActionTrigger, P2CardActionTriggerResponse, CallBackToast
@@ -11,7 +11,7 @@ import sys
11
11
  from pathlib import Path
12
12
  from typing import Optional, Callable, Dict
13
13
 
14
- logging.basicConfig(level=logging.DEBUG, format='[%(name)s] %(message)s')
14
+ logging.basicConfig(level=logging.INFO, format='[%(name)s] %(message)s')
15
15
  logger = logging.getLogger('SessionBridge')
16
16
 
17
17
  sys.path.insert(0, str(Path(__file__).parent.parent))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remote-claude",
3
- "version": "0.2.10",
3
+ "version": "0.2.11",
4
4
  "description": "双端共享 Claude CLI 工具",
5
5
  "bin": {
6
6
  "remote-claude": "bin/remote-claude",
package/remote_claude.py CHANGED
@@ -3,11 +3,14 @@
3
3
  Remote Claude - 双端共享 Claude CLI 工具
4
4
 
5
5
  命令:
6
- start <name> 启动新会话(在 tmux 中)
7
- attach <name> 连接到已有会话
8
- list 列出所有会话
9
- kill <name> 终止会话
10
- lark 飞书客户端管理(start/stop/restart/status
6
+ start <name> 启动新会话(在 tmux 中)
7
+ attach <name> 连接到已有会话
8
+ list 列出所有会话
9
+ kill <name> 终止会话
10
+ status <name> 显示会话状态
11
+ lark 飞书客户端管理(start/stop/restart/status)
12
+ stats 查看使用统计
13
+ update 更新 remote-claude 到最新版本
11
14
  """
12
15
 
13
16
  import argparse
@@ -183,16 +186,29 @@ def cmd_list(args):
183
186
  print("没有活跃的会话")
184
187
  return 0
185
188
 
189
+ # ANSI 颜色码
190
+ YELLOW = "\033[33m"
191
+ GREEN = "\033[32m"
192
+ RESET = "\033[0m"
193
+
186
194
  print("活跃会话:")
187
- print("-" * 60)
188
- print(f"{'名称':<20} {'PID':<10} {'tmux':<10} {'Socket'}")
189
- print("-" * 60)
195
+ print("-" * 50)
196
+ print(f"{'类型':<8} {'PID':<10} {'tmux':<10} {'名称'}")
197
+ print("-" * 50)
190
198
 
191
199
  for s in sessions:
192
200
  tmux_status = "是" if s["tmux"] else "否"
193
- print(f"{s['name']:<20} {s['pid']:<10} {tmux_status:<10} {s['socket']}")
201
+ cli_type = s.get('cli_type', 'claude')
202
+ # 根据类型选择颜色
203
+ if cli_type == 'codex':
204
+ cli_colored = f"{GREEN}{cli_type}{RESET}"
205
+ else:
206
+ cli_colored = f"{YELLOW}{cli_type}{RESET}"
207
+ # 带颜色的字段需要单独计算宽度
208
+ padding = " " * (8 - len(cli_type))
209
+ print(f"{cli_colored}{padding} {s['pid']:<10} {tmux_status:<10} {s['name']}")
194
210
 
195
- print("-" * 60)
211
+ print("-" * 50)
196
212
  print(f"共 {len(sessions)} 个会话")
197
213
 
198
214
  return 0
@@ -483,9 +499,11 @@ def main():
483
499
  epilog="""
484
500
  示例:
485
501
  %(prog)s start mywork 启动名为 mywork 的会话
502
+ %(prog)s start mywork --cli codex 启动 codex 会话
486
503
  %(prog)s attach mywork 连接到 mywork 会话
487
504
  %(prog)s list 列出所有会话
488
505
  %(prog)s kill mywork 终止 mywork 会话
506
+ %(prog)s status mywork 显示 mywork 会话状态
489
507
 
490
508
  飞书客户端:
491
509
  %(prog)s lark start 启动飞书客户端
@@ -508,6 +526,9 @@ def main():
508
526
  %(prog)s stats --detail 详细分类
509
527
  %(prog)s stats --session mywork 按会话筛选
510
528
  %(prog)s stats --reset 清空数据
529
+
530
+ 更新:
531
+ %(prog)s update 更新到最新版本
511
532
  """
512
533
  )
513
534
 
@@ -615,12 +636,16 @@ def main():
615
636
  update_parser = subparsers.add_parser("update", help="更新 remote-claude 到最新版本")
616
637
  update_parser.set_defaults(func=cmd_update)
617
638
 
618
- args = parser.parse_args()
639
+ args, remaining = parser.parse_known_args()
619
640
 
620
641
  if args.command is None:
621
642
  parser.print_help()
622
643
  return 0
623
644
 
645
+ # 将剩余参数合并到 claude_args(支持 cx/cdx 脚本中使用 -- 分隔符)
646
+ if args.command == "start" and hasattr(args, 'claude_args'):
647
+ args.claude_args = args.claude_args + remaining
648
+
624
649
  return args.func(args)
625
650
 
626
651
 
@@ -0,0 +1 @@
1
+ # Remote Claude Server Package
package/server/server.py CHANGED
@@ -698,6 +698,9 @@ class ProxyServer:
698
698
  # 启动 PTY 读取任务
699
699
  asyncio.create_task(self._read_pty())
700
700
 
701
+ # 切换到运行阶段日志
702
+ self._switch_to_runtime_logging()
703
+
701
704
  # 等待服务器关闭
702
705
  async with self.server:
703
706
  await self.server.serve_forever()
@@ -713,6 +716,32 @@ class ProxyServer:
713
716
  return CodexParser()
714
717
  return ClaudeParser()
715
718
 
719
+ def _switch_to_runtime_logging(self):
720
+ """从启动日志切换到运行阶段日志"""
721
+ root_logger = logging.getLogger()
722
+
723
+ # 移除启动日志 handler(保留 stdout handler)
724
+ for handler in root_logger.handlers[:]:
725
+ if isinstance(handler, logging.FileHandler) and \
726
+ not hasattr(handler, '_runtime_handler') and \
727
+ not hasattr(handler, '_debug_handler'):
728
+ root_logger.removeHandler(handler)
729
+
730
+ # 添加运行阶段日志文件
731
+ safe_name = _safe_filename(self.session_name)
732
+ runtime_handler = logging.FileHandler(
733
+ f"{SOCKET_DIR}/{safe_name}_server.log",
734
+ encoding="utf-8"
735
+ )
736
+ runtime_handler.setFormatter(logging.Formatter(
737
+ "%(asctime)s.%(msecs)03d [%(name)s] %(levelname)s %(message)s",
738
+ datefmt="%Y-%m-%d %H:%M:%S"
739
+ ))
740
+ runtime_handler._runtime_handler = True # 标记,方便后续清理
741
+ root_logger.addHandler(runtime_handler)
742
+
743
+ logger.info(f"日志已切换到运行阶段: {safe_name}_server.log")
744
+
716
745
  def _get_effective_cmd(self) -> str:
717
746
  """根据 cli_type 返回实际执行的命令(codex 时使用 'codex',否则用 claude_cmd)"""
718
747
  if self.cli_type == "codex":
@@ -998,20 +1027,27 @@ if __name__ == "__main__":
998
1027
  help="debug 日志输出完整诊断信息(indicator、repr 等)")
999
1028
  args = parser.parse_args()
1000
1029
 
1001
- # 配置日志:写文件(供故障诊断)+ stdout(供 tmux attach 时查看)
1002
- from utils.session import USER_DATA_DIR
1030
+ # 配置日志:启动阶段输出到 stdout + startup.log
1031
+ from utils.session import USER_DATA_DIR, _safe_filename
1003
1032
  USER_DATA_DIR.mkdir(parents=True, exist_ok=True)
1004
- _log_path = USER_DATA_DIR / "startup.log"
1033
+
1034
+ # 先配置基本输出(stdout)
1005
1035
  logging.basicConfig(
1006
- level=logging.DEBUG,
1036
+ level=logging.INFO,
1007
1037
  format="%(asctime)s.%(msecs)03d [%(name)s] %(levelname)s %(message)s",
1008
1038
  datefmt="%Y-%m-%d %H:%M:%S",
1009
- handlers=[
1010
- logging.FileHandler(_log_path, encoding="utf-8"),
1011
- logging.StreamHandler(sys.stdout),
1012
- ],
1039
+ handlers=[logging.StreamHandler(sys.stdout)],
1013
1040
  )
1014
1041
 
1042
+ # 添加启动日志 handler
1043
+ startup_handler = logging.FileHandler(USER_DATA_DIR / "startup.log", encoding="utf-8")
1044
+ startup_handler.setFormatter(logging.Formatter(
1045
+ "%(asctime)s.%(msecs)03d [%(name)s] %(levelname)s %(message)s",
1046
+ datefmt="%Y-%m-%d %H:%M:%S"
1047
+ ))
1048
+ startup_handler._startup_handler = True # 标记为启动日志 handler
1049
+ logging.getLogger().addHandler(startup_handler)
1050
+
1015
1051
  claude_cmd = os.environ.get("CLAUDE_COMMAND", "claude")
1016
1052
  logger.info(f"CLAUDE_COMMAND={claude_cmd!r}")
1017
1053
  run_server(args.session_name, args.claude_args, claude_cmd=claude_cmd,
@@ -93,6 +93,7 @@ class SharedStateWriter:
93
93
  """写端:Server 进程持有,生命周期与 ProxyServer 相同"""
94
94
 
95
95
  def __init__(self, session_name: str):
96
+ self._session_name = session_name
96
97
  self._path = get_mq_path(session_name)
97
98
  self._path.parent.mkdir(parents=True, exist_ok=True)
98
99
 
@@ -139,7 +140,7 @@ class SharedStateWriter:
139
140
  "input_area_text": window.input_area_text,
140
141
  "timestamp": window.timestamp,
141
142
  "layout_mode": window.layout_mode,
142
- "cli_type": getattr(window, "cli_type", "claude"),
143
+ "cli_type": getattr(window, "cli_type", "unknown"),
143
144
  }
144
145
  data = json.dumps(snapshot, ensure_ascii=False).encode('utf-8')
145
146
 
@@ -177,7 +178,7 @@ class SharedStateReader:
177
178
  反映跨进程写入更新的问题。每次 read() 打开文件读取后关闭,保证读到最新数据。
178
179
  """
179
180
 
180
- _EMPTY = {"blocks": [], "status_line": None, "bottom_bar": None, "option_block": None}
181
+ _EMPTY = {"blocks": [], "status_line": None, "bottom_bar": None, "option_block": None, "cli_type": "claude"}
181
182
 
182
183
  def __init__(self, session_name: str):
183
184
  self._path = get_mq_path(session_name)
@@ -0,0 +1,3 @@
1
+ """
2
+ Remote Claude 工具模块
3
+ """
package/utils/session.py CHANGED
@@ -223,10 +223,30 @@ def list_active_sessions() -> List[dict]:
223
223
  import datetime
224
224
  try:
225
225
  mtime = pid_file.stat().st_mtime
226
- start_time = datetime.datetime.fromtimestamp(mtime).strftime("%H:%M")
226
+ start_time = datetime.datetime.fromtimestamp(mtime).strftime("%m-%d %H:%M")
227
227
  except OSError:
228
228
  mtime = 0
229
229
  start_time = "?"
230
+
231
+ # 读取 .mq 文件获取 cli_type(避免循环导入,在函数内导入)
232
+ try:
233
+ import sys
234
+ from pathlib import Path
235
+ import logging
236
+ project_root = str(Path(__file__).parent.parent)
237
+ if project_root not in sys.path:
238
+ sys.path.insert(0, project_root)
239
+ from server.shared_state import SharedStateReader
240
+ reader = SharedStateReader(session_name)
241
+ snapshot = reader.read()
242
+ cli_type = snapshot.get("cli_type", "claude")
243
+ except Exception as e:
244
+ # 添加详细日志记录,便于诊断问题
245
+ import logging
246
+ logger = logging.getLogger('Session')
247
+ logger.warning(f"读取共享内存 cli_type 失败: session={session_name}, error={e}")
248
+ cli_type = "claude" # 读取失败时使用默认值
249
+
230
250
  sessions.append({
231
251
  "name": session_name,
232
252
  "socket": str(sock_file),
@@ -234,7 +254,8 @@ def list_active_sessions() -> List[dict]:
234
254
  "cwd": cwd or "",
235
255
  "start_time": start_time,
236
256
  "mtime": mtime,
237
- "tmux": tmux_session_exists(session_name)
257
+ "tmux": tmux_session_exists(session_name),
258
+ "cli_type": cli_type
238
259
  })
239
260
  except (ProcessLookupError, ValueError, OSError):
240
261
  # 进程不存在或文件被并发清理,清理残留文件