remote-claude 0.2.12 → 0.2.13

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
@@ -26,3 +26,7 @@ ALLOWED_USERS=ou_xxxxx,ou_yyyyy
26
26
  # 支持: DEBUG / INFO / WARNING / ERROR
27
27
  # LARK_LOG_LEVEL=INFO
28
28
 
29
+ # server 日志级别(可选,默认 INFO)
30
+ # 支持: DEBUG / INFO / WARNING / ERROR
31
+ # SERVER_LOG_LEVEL=INFO
32
+
package/client/client.py CHANGED
@@ -57,16 +57,55 @@ class RemoteClient:
57
57
  async def connect(self) -> bool:
58
58
  """连接到服务器"""
59
59
  if not self.socket_path.exists():
60
- print(f"错误: 会话 '{self.session_name}' 不存在")
60
+ print(
61
+ f"❌ 错误: Socket 文件不存在\n"
62
+ f" 会话名: {self.session_name}\n"
63
+ f" Socket 路径: {self.socket_path}\n"
64
+ f"\n"
65
+ f" 请使用 `python3 remote_claude.py list` 查看可用会话"
66
+ )
61
67
  return False
62
68
 
63
69
  try:
64
70
  self.reader, self.writer = await asyncio.open_unix_connection(
65
71
  path=str(self.socket_path)
66
72
  )
73
+ print(f"✅ 已连接到会话: {self.session_name}")
67
74
  return True
75
+ except ConnectionRefusedError as e:
76
+ # 检查进程状态
77
+ from utils.session import list_active_sessions
78
+ sessions = list_active_sessions()
79
+ session_exists = any(s["name"] == self.session_name for s in sessions)
80
+
81
+ print(
82
+ f"❌ 连接失败: Connection refused\n"
83
+ f" 会话名: {self.session_name}\n"
84
+ f" Socket 路径: {self.socket_path}\n"
85
+ f" 文件存在: {self.socket_path.exists()}\n"
86
+ f" 会话在列表中: {session_exists}\n"
87
+ f"\n"
88
+ f" 当前活跃会话:"
89
+ )
90
+ for s in sessions:
91
+ print(f" - {s['name']} (PID: {s.get('pid', 'N/A')})")
92
+ print(
93
+ f"\n"
94
+ f" 可能原因:\n"
95
+ f" 1. Server 进程已终止但 Socket 文件残留\n"
96
+ f" 2. Socket 文件权限错误\n"
97
+ f"\n"
98
+ f" 建议操作:\n"
99
+ f" python3 remote_claude.py kill {self.session_name}\n"
100
+ f" python3 remote_claude.py start {self.session_name}"
101
+ )
102
+ return False
68
103
  except Exception as e:
69
- print(f"连接失败: {e}")
104
+ print(
105
+ f"❌ 连接失败: {type(e).__name__}: {e}\n"
106
+ f" 会话名: {self.session_name}\n"
107
+ f" Socket 路径: {self.socket_path}"
108
+ )
70
109
  return False
71
110
 
72
111
  async def run(self):
package/init.sh CHANGED
@@ -159,6 +159,19 @@ check_uv() {
159
159
  check_tmux() {
160
160
  print_header "检查 tmux"
161
161
 
162
+ # CI 模式:跳过 tmux 版本检查(Docker 环境可能没有 sudo)
163
+ if [ "$CI_MODE" = "true" ]; then
164
+ if command -v tmux &> /dev/null; then
165
+ TMUX_VERSION=$(tmux -V)
166
+ print_success "$TMUX_VERSION 已安装(CI 模式跳过版本检查)"
167
+ return
168
+ else
169
+ print_error "未找到 tmux"
170
+ WARNINGS+=("tmux 未安装,CI 模式跳过版本检查")
171
+ return
172
+ fi
173
+ fi
174
+
162
175
  REQUIRED_MAJOR=3
163
176
  REQUIRED_MINOR=6
164
177
 
@@ -581,6 +594,11 @@ main() {
581
594
  [[ "$arg" == "--npm" ]] && NPM_MODE=true
582
595
  done
583
596
 
597
+ # CI 模式:跳过 tmux 版本检查(CI 环境可能没有 sudo)
598
+ if [ -n "$CI" ]; then
599
+ export CI_MODE=true
600
+ fi
601
+
584
602
  echo ""
585
603
  echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
586
604
  echo -e "${GREEN} Remote Claude 初始化脚本${NC}"
@@ -47,6 +47,14 @@ class SessionBridge:
47
47
  async def connect(self) -> bool:
48
48
  """连接到会话"""
49
49
  if not self.socket_path.exists():
50
+ logger.error(
51
+ f"连接失败: Socket 文件不存在\n"
52
+ f" 会话名: {self.session_name}\n"
53
+ f" Socket 路径: {self.socket_path}\n"
54
+ f" 请确认:\n"
55
+ f" 1. 会话已启动 (使用 /list 查看)\n"
56
+ f" 2. 会话名拼写正确"
57
+ )
50
58
  return False
51
59
  try:
52
60
  self.reader, self.writer = await asyncio.open_unix_connection(
@@ -54,9 +62,40 @@ class SessionBridge:
54
62
  )
55
63
  self.running = True
56
64
  self._read_task = asyncio.create_task(self._read_loop())
65
+ logger.info(f"连接成功: {self.session_name}")
57
66
  return True
67
+ except FileNotFoundError:
68
+ logger.error(
69
+ f"连接失败: Socket 文件不存在\n"
70
+ f" 会话名: {self.session_name}\n"
71
+ f" Socket 路径: {self.socket_path}\n"
72
+ f" 可能原因: Socket 文件在连接前被删除"
73
+ )
74
+ return False
75
+ except ConnectionRefusedError as e:
76
+ # 检查进程状态
77
+ sessions = list_active_sessions()
78
+ session_exists = any(s["name"] == self.session_name for s in sessions)
79
+
80
+ logger.error(
81
+ f"连接失败: Connection refused\n"
82
+ f" 会话名: {self.session_name}\n"
83
+ f" Socket 路径: {self.socket_path}\n"
84
+ f" 文件存在: {self.socket_path.exists()}\n"
85
+ f" 会话在列表中: {session_exists}\n"
86
+ f" 当前活跃会话: {[s['name'] for s in sessions]}\n"
87
+ f" 可能原因:\n"
88
+ f" 1. Server 进程已终止但 Socket 文件残留\n"
89
+ f" 2. Socket 文件权限错误\n"
90
+ f" 建议: 使用 /kill {self.session_name} 清理后重新启动"
91
+ )
92
+ return False
58
93
  except Exception as e:
59
- logger.error(f"连接失败: {e}")
94
+ logger.error(
95
+ f"连接失败: {type(e).__name__}: {e}\n"
96
+ f" 会话名: {self.session_name}\n"
97
+ f" Socket 路径: {self.socket_path}"
98
+ )
60
99
  return False
61
100
 
62
101
  async def disconnect(self):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remote-claude",
3
- "version": "0.2.12",
3
+ "version": "0.2.13",
4
4
  "description": "双端共享 Claude CLI 工具",
5
5
  "bin": {
6
6
  "remote-claude": "bin/remote-claude",
package/server/server.py CHANGED
@@ -42,6 +42,15 @@ from utils.session import (
42
42
 
43
43
  logger = logging.getLogger('Server')
44
44
 
45
+ # Server 日志级别配置
46
+ _SERVER_LOG_LEVEL = os.getenv("SERVER_LOG_LEVEL", "INFO").upper()
47
+ SERVER_LOG_LEVEL_MAP = {
48
+ "DEBUG": logging.DEBUG,
49
+ "INFO": logging.INFO,
50
+ "WARNING": logging.WARNING,
51
+ "ERROR": logging.ERROR,
52
+ }.get(_SERVER_LOG_LEVEL, logging.INFO) # 默认 INFO
53
+
45
54
  # 加载用户 .env 配置(支持 CLAUDE_COMMAND 等)
46
55
  try:
47
56
  from dotenv import load_dotenv
@@ -126,11 +135,11 @@ class OutputWatcher:
126
135
  # 持久化解析器(跨帧保留 dot_row_cache);由调用方注入(可插拔架构)
127
136
  import logging as _logging
128
137
  _logging.getLogger('ComponentParser').setLevel(_logging.DEBUG)
129
- _blink_handler = _logging.FileHandler(
130
- f"/tmp/remote-claude/{safe_name}_blink.log"
131
- )
132
- _blink_handler.setFormatter(_logging.Formatter('%(asctime)s %(message)s', '%H:%M:%S'))
133
- _logging.getLogger('ComponentParser').addHandler(_blink_handler)
138
+ # 创建专用 logger 替换直接文件写入
139
+ self._blink_logger = _logging.getLogger(f'OutputWatcher.{self._session_name}.blink')
140
+ self._blink_logger.setLevel(_logging.DEBUG)
141
+ self._flush_logger = _logging.getLogger(f'OutputWatcher.{self._session_name}.flush')
142
+ self._flush_logger.setLevel(_logging.DEBUG)
134
143
  if parser is None:
135
144
  from parsers import ClaudeParser
136
145
  parser = ClaudeParser()
@@ -258,11 +267,7 @@ class OutputWatcher:
258
267
  pass
259
268
  break
260
269
  if last_ob_blink:
261
- _blink_log = open(f"/tmp/remote-claude/{self._session_name}_blink.log", "a")
262
- _blink_log.write(
263
- f"[{time.strftime('%H:%M:%S')}] raw-blink last_ob={last_ob_content!r}\n"
264
- )
265
- _blink_log.close()
270
+ self._blink_logger.debug(f"raw-blink last_ob={last_ob_content!r}")
266
271
 
267
272
  self._frame_window.append(_FrameObs(
268
273
  ts=now,
@@ -308,23 +313,19 @@ class OutputWatcher:
308
313
  if len(chars) > 1 or len(fgs) > 1 or len(bolds) > 1:
309
314
  window_block_active = True
310
315
  # 记录字符变化触发原因
311
- _blink_log = open(f"/tmp/remote-claude/{self._session_name}_blink.log", "a")
312
- _blink_log.write(
313
- f"[{time.strftime('%H:%M:%S')}] char-change row={last_ob_start_row}"
314
- f" chars={chars} fgs={fgs} bolds={bolds}\n"
316
+ self._blink_logger.debug(
317
+ f"char-change row={last_ob_start_row}"
318
+ f" chars={chars} fgs={fgs} bolds={bolds}"
315
319
  )
316
- _blink_log.close()
317
320
 
318
321
  if window_block_active:
319
322
  for b in reversed(visible_blocks):
320
323
  if isinstance(b, OutputBlock):
321
- _blink_log = open(f"/tmp/remote-claude/{self._session_name}_blink.log", "a")
322
- _blink_log.write(
323
- f"[{time.strftime('%H:%M:%S')}] win-smooth last_ob={b.content[:40]!r}"
324
+ self._blink_logger.debug(
325
+ f"win-smooth last_ob={b.content[:40]!r}"
324
326
  f" window_frames={len(window_list)}"
325
- f" blink_frames={sum(1 for o in window_list if o.block_blink)}\n"
327
+ f" blink_frames={sum(1 for o in window_list if o.block_blink)}"
326
328
  )
327
- _blink_log.close()
328
329
  b.is_streaming = True
329
330
  break
330
331
 
@@ -373,13 +374,12 @@ class OutputWatcher:
373
374
  self._on_snapshot(window)
374
375
  t4 = time.time()
375
376
 
376
- with open(f"/tmp/remote-claude/{_safe_filename(self._session_name)}_flush.log", "a") as _f:
377
- _f.write(
378
- f"[flush] screen_log={1000*(t1-t0):.1f}ms parse={1000*(t2-t1):.1f}ms "
379
- f"msg_log={1000*(t3-t2):.1f}ms snapshot={1000*(t4-t3):.1f}ms "
380
- f"total={1000*(t4-t0):.1f}ms rows={self._rows}\n"
381
- f" └─ {self._parser.last_parse_timing}\n"
382
- )
377
+ self._flush_logger.debug(
378
+ f"[flush] screen_log={1000*(t1-t0):.1f}ms parse={1000*(t2-t1):.1f}ms "
379
+ f"msg_log={1000*(t3-t2):.1f}ms snapshot={1000*(t4-t3):.1f}ms "
380
+ f"total={1000*(t4-t0):.1f}ms rows={self._rows}\n"
381
+ f" └─ {self._parser.last_parse_timing}"
382
+ )
383
383
 
384
384
  except Exception as e:
385
385
  print(f"[OutputWatcher] flush 失败: {e}")
@@ -727,6 +727,14 @@ class ProxyServer:
727
727
  not hasattr(handler, '_debug_handler'):
728
728
  root_logger.removeHandler(handler)
729
729
 
730
+ # 重定向 sys.stderr 到 ~/.remote-claude/server.error.log
731
+ # 注意:这不会影响外层的 2>> startup.log,但 Python 的 stderr 输出会走这里
732
+ # 适用于:print(..., file=sys.stderr)、logging 的 StreamHandler 等
733
+ # 不适用于:C 扩展模块直接写文件描述符 2、解释器崩溃等底层错误
734
+ error_log_path = os.path.expanduser('~/.remote-claude/server.error.log')
735
+ sys.stderr = open(error_log_path, 'w', encoding='utf-8')
736
+ logger.info(f"已重定向 stderr 到 {error_log_path}")
737
+
730
738
  # 添加运行阶段日志文件
731
739
  safe_name = _safe_filename(self.session_name)
732
740
  runtime_handler = logging.FileHandler(
@@ -740,6 +748,21 @@ class ProxyServer:
740
748
  runtime_handler._runtime_handler = True # 标记,方便后续清理
741
749
  root_logger.addHandler(runtime_handler)
742
750
 
751
+ # DEBUG 级别时额外记录调试日志到独立文件
752
+ if SERVER_LOG_LEVEL_MAP == logging.DEBUG:
753
+ debug_handler = logging.FileHandler(
754
+ f"{SOCKET_DIR}/{safe_name}_debug.log",
755
+ encoding="utf-8"
756
+ )
757
+ debug_handler.setLevel(logging.DEBUG)
758
+ debug_handler.setFormatter(logging.Formatter(
759
+ "%(asctime)s.%(msecs)03d [%(name)s] %(levelname)s %(message)s",
760
+ datefmt="%Y-%m-%d %H:%M:%S"
761
+ ))
762
+ debug_handler._debug_handler = True # 标记,方便后续清理
763
+ root_logger.addHandler(debug_handler)
764
+ logger.info(f"已启用 DEBUG 日志: {safe_name}_debug.log")
765
+
743
766
  logger.info(f"日志已切换到运行阶段: {safe_name}_server.log")
744
767
 
745
768
  def _get_effective_cmd(self) -> str:
package/utils/session.py CHANGED
@@ -99,7 +99,15 @@ def tmux_create_session(session_name: str, command: str, detached: bool = True)
99
99
  if detached:
100
100
  args.append("-d")
101
101
  args.extend(["-x", "200", "-y", "50"]) # 默认大小
102
- args.append(command)
102
+
103
+ # 将 stderr 重定向到 startup.log,捕获 Python 启动错误(如 ModuleNotFoundError)
104
+ # startup.log 位于 ~/.remote-claude/startup.log
105
+ startup_log = USER_DATA_DIR / "startup.log"
106
+ startup_log.parent.mkdir(parents=True, exist_ok=True)
107
+ # 直接在命令末尾添加重定向,使用 str(startup_log) 确保路径正确展开
108
+ command_with_stderr = f"{command} 2>> {startup_log}"
109
+
110
+ args.append(command_with_stderr)
103
111
 
104
112
  import logging as _logging
105
113
  _logging.getLogger('Start').info(f"tmux_cmd: {' '.join(args)}")