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 +4 -0
- package/client/client.py +41 -2
- package/init.sh +18 -0
- package/lark_client/session_bridge.py +40 -1
- package/package.json +1 -1
- package/server/server.py +50 -27
- package/utils/session.py +9 -1
package/.env.example
CHANGED
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(
|
|
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(
|
|
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(
|
|
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
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
|
-
|
|
130
|
-
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
f"
|
|
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
|
-
|
|
322
|
-
|
|
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)}
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
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)}")
|