remote-claude 1.0.2 → 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 +8 -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 +60 -33
- package/lark_client/config.py +4 -0
- package/lark_client/lark_handler.py +316 -83
- package/lark_client/main.py +26 -6
- package/lark_client/session_bridge.py +4 -10
- package/lark_client/shared_memory_poller.py +193 -107
- package/package.json +1 -1
- package/pyproject.toml +1 -0
- package/remote_claude.py +226 -0
- package/server/parsers/claude_parser.py +43 -0
- package/server/parsers/codex_parser.py +8 -0
- package/server/server.py +31 -19
- package/utils/components.py +1 -0
- package/utils/session.py +81 -30
package/utils/session.py
CHANGED
|
@@ -20,11 +20,6 @@ SOCKET_DIR = Path("/tmp/remote-claude")
|
|
|
20
20
|
USER_DATA_DIR = Path.home() / ".remote-claude"
|
|
21
21
|
TMUX_SESSION_PREFIX = "rc-"
|
|
22
22
|
|
|
23
|
-
# macOS AF_UNIX sun_path 限制 104 字节
|
|
24
|
-
# /tmp/remote-claude/ = 19 字节, .sock = 5 字节
|
|
25
|
-
_MAX_SOCKET_PATH = 104
|
|
26
|
-
_MAX_FILENAME = _MAX_SOCKET_PATH - len(str(SOCKET_DIR)) - 1 - len(".sock")
|
|
27
|
-
|
|
28
23
|
|
|
29
24
|
def get_env_file() -> Path:
|
|
30
25
|
"""获取 .env 配置文件路径"""
|
|
@@ -47,14 +42,15 @@ def ensure_user_data_dir():
|
|
|
47
42
|
|
|
48
43
|
|
|
49
44
|
def _safe_filename(session_name: str) -> str:
|
|
50
|
-
"""
|
|
51
|
-
name = session_name.replace('/', '_').replace('.', '_')
|
|
52
|
-
if len(name) <= _MAX_FILENAME:
|
|
53
|
-
return name
|
|
54
|
-
# 超长:直接用完整 32 字符 MD5,彻底避免路径超限
|
|
45
|
+
"""将会话名转为安全文件名(MD5 hash)"""
|
|
55
46
|
return hashlib.md5(session_name.encode()).hexdigest()
|
|
56
47
|
|
|
57
48
|
|
|
49
|
+
def _log_filename(session_name: str) -> str:
|
|
50
|
+
"""将会话名转为可读的安全文件名(用于日志文件,不受路径长度限制)"""
|
|
51
|
+
return session_name.replace("/", "_").replace(".", "_").replace(" ", "_")
|
|
52
|
+
|
|
53
|
+
|
|
58
54
|
def get_socket_path(session_name: str) -> Path:
|
|
59
55
|
"""获取会话的 socket 路径"""
|
|
60
56
|
return SOCKET_DIR / f"{_safe_filename(session_name)}.sock"
|
|
@@ -75,6 +71,11 @@ def get_env_snapshot_path(session_name: str) -> Path:
|
|
|
75
71
|
return SOCKET_DIR / f"{_safe_filename(session_name)}_env.json"
|
|
76
72
|
|
|
77
73
|
|
|
74
|
+
def get_name_file(session_name: str) -> Path:
|
|
75
|
+
"""获取会话名映射文件路径(存储原始会话名,供 list 恢复显示名)"""
|
|
76
|
+
return SOCKET_DIR / f"{_safe_filename(session_name)}.name"
|
|
77
|
+
|
|
78
|
+
|
|
78
79
|
def ensure_socket_dir():
|
|
79
80
|
"""确保 socket 目录存在"""
|
|
80
81
|
SOCKET_DIR.mkdir(parents=True, exist_ok=True)
|
|
@@ -221,13 +222,19 @@ def get_process_cwd(pid: int) -> Optional[str]:
|
|
|
221
222
|
|
|
222
223
|
|
|
223
224
|
def list_active_sessions() -> List[dict]:
|
|
224
|
-
"""列出所有活跃会话
|
|
225
|
+
"""列出所有活跃会话
|
|
226
|
+
|
|
227
|
+
注意:sock 文件的 stem 已经是 MD5 哈希(_safe_filename 的输出),
|
|
228
|
+
因此直接用 stem 构造 PID/MQ 文件路径,不再经过 get_pid_file 等函数
|
|
229
|
+
(否则会被 _safe_filename 二次哈希)。
|
|
230
|
+
"""
|
|
225
231
|
ensure_socket_dir()
|
|
226
232
|
sessions = []
|
|
227
233
|
|
|
228
234
|
for sock_file in SOCKET_DIR.glob("*.sock"):
|
|
229
|
-
|
|
230
|
-
|
|
235
|
+
# stem 已经是 _safe_filename() 的输出(MD5 哈希)
|
|
236
|
+
safe_name = sock_file.stem
|
|
237
|
+
pid_file = SOCKET_DIR / f"{safe_name}.pid"
|
|
231
238
|
|
|
232
239
|
# 检查 socket 文件是否有效(进程是否存在)
|
|
233
240
|
if pid_file.exists():
|
|
@@ -237,6 +244,17 @@ def list_active_sessions() -> List[dict]:
|
|
|
237
244
|
os.kill(pid, 0)
|
|
238
245
|
# 获取进程 CWD
|
|
239
246
|
cwd = get_process_cwd(pid)
|
|
247
|
+
|
|
248
|
+
# 恢复原始会话名(从 .name 文件读取,不存在则回退到哈希值)
|
|
249
|
+
name_file = SOCKET_DIR / f"{safe_name}.name"
|
|
250
|
+
if name_file.exists():
|
|
251
|
+
try:
|
|
252
|
+
display_name = name_file.read_text().strip()
|
|
253
|
+
except OSError:
|
|
254
|
+
display_name = safe_name
|
|
255
|
+
else:
|
|
256
|
+
display_name = safe_name
|
|
257
|
+
|
|
240
258
|
# 获取启动时间(PID 文件的修改时间,文件可能已被并发清理)
|
|
241
259
|
import datetime
|
|
242
260
|
try:
|
|
@@ -246,7 +264,7 @@ def list_active_sessions() -> List[dict]:
|
|
|
246
264
|
mtime = 0
|
|
247
265
|
start_time = "?"
|
|
248
266
|
|
|
249
|
-
# 读取 .mq 文件获取 cli_type
|
|
267
|
+
# 读取 .mq 文件获取 cli_type
|
|
250
268
|
try:
|
|
251
269
|
import sys
|
|
252
270
|
from pathlib import Path
|
|
@@ -255,29 +273,38 @@ def list_active_sessions() -> List[dict]:
|
|
|
255
273
|
if project_root not in sys.path:
|
|
256
274
|
sys.path.insert(0, project_root)
|
|
257
275
|
from server.shared_state import SharedStateReader
|
|
258
|
-
|
|
276
|
+
# 用 _BypassHashReader 直接传入 safe_name 避免二次哈希
|
|
277
|
+
mq_path = SOCKET_DIR / f"{safe_name}.mq"
|
|
278
|
+
reader = SharedStateReader.__new__(SharedStateReader)
|
|
279
|
+
reader._path = mq_path
|
|
259
280
|
snapshot = reader.read()
|
|
260
281
|
cli_type = snapshot.get("cli_type", "claude")
|
|
261
282
|
except Exception as e:
|
|
262
|
-
# 添加详细日志记录,便于诊断问题
|
|
263
283
|
import logging
|
|
264
284
|
logger = logging.getLogger('Session')
|
|
265
|
-
logger.warning(f"读取共享内存 cli_type 失败: session={
|
|
266
|
-
cli_type = "claude"
|
|
285
|
+
logger.warning(f"读取共享内存 cli_type 失败: session={display_name}, error={e}")
|
|
286
|
+
cli_type = "claude"
|
|
287
|
+
|
|
288
|
+
# tmux 会话名也用 safe_name 直接构造
|
|
289
|
+
tmux_name = f"{TMUX_SESSION_PREFIX}{safe_name}"
|
|
290
|
+
tmux_exists = subprocess.run(
|
|
291
|
+
["tmux", "has-session", "-t", tmux_name],
|
|
292
|
+
capture_output=True
|
|
293
|
+
).returncode == 0
|
|
267
294
|
|
|
268
295
|
sessions.append({
|
|
269
|
-
"name":
|
|
296
|
+
"name": display_name,
|
|
270
297
|
"socket": str(sock_file),
|
|
271
298
|
"pid": pid,
|
|
272
299
|
"cwd": cwd or "",
|
|
273
300
|
"start_time": start_time,
|
|
274
301
|
"mtime": mtime,
|
|
275
|
-
"tmux":
|
|
302
|
+
"tmux": tmux_exists,
|
|
276
303
|
"cli_type": cli_type
|
|
277
304
|
})
|
|
278
305
|
except (ProcessLookupError, ValueError, OSError):
|
|
279
306
|
# 进程不存在或文件被并发清理,清理残留文件
|
|
280
|
-
|
|
307
|
+
_cleanup_by_safe_name(safe_name)
|
|
281
308
|
else:
|
|
282
309
|
# 没有 PID 文件,清理 socket
|
|
283
310
|
sock_file.unlink(missing_ok=True)
|
|
@@ -287,14 +314,32 @@ def list_active_sessions() -> List[dict]:
|
|
|
287
314
|
|
|
288
315
|
|
|
289
316
|
def cleanup_session(session_name: str):
|
|
290
|
-
"""
|
|
291
|
-
|
|
292
|
-
|
|
317
|
+
"""清理会话残留文件(传入原始会话名,经过 _safe_filename 转换)"""
|
|
318
|
+
safe = _safe_filename(session_name)
|
|
319
|
+
_cleanup_by_safe_name(safe, session_name)
|
|
293
320
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
321
|
+
|
|
322
|
+
def _cleanup_by_safe_name(safe_name: str, session_name: Optional[str] = None):
|
|
323
|
+
"""清理会话残留文件(传入已经是 MD5 哈希的 safe_name,不再二次哈希)"""
|
|
324
|
+
# 尝试从 .name 文件读取原始会话名(用于清理可读日志文件)
|
|
325
|
+
if session_name is None:
|
|
326
|
+
name_file = SOCKET_DIR / f"{safe_name}.name"
|
|
327
|
+
if name_file.exists():
|
|
328
|
+
try:
|
|
329
|
+
session_name = name_file.read_text().strip()
|
|
330
|
+
except OSError:
|
|
331
|
+
pass
|
|
332
|
+
for suffix in [".sock", ".pid", ".mq", ".name", "_env.json"]:
|
|
333
|
+
(SOCKET_DIR / f"{safe_name}{suffix}").unlink(missing_ok=True)
|
|
334
|
+
# 清理带后缀的日志文件(使用可读文件名)
|
|
335
|
+
log_suffixes = ["_messages.log", "_screen.log", "_server.log", "_debug.log", "_pty_raw.log"]
|
|
336
|
+
if session_name:
|
|
337
|
+
log_name = _log_filename(session_name)
|
|
338
|
+
for suffix in log_suffixes:
|
|
339
|
+
(SOCKET_DIR / f"{log_name}{suffix}").unlink(missing_ok=True)
|
|
340
|
+
# 兼容旧的 MD5 日志文件
|
|
341
|
+
for suffix in log_suffixes:
|
|
342
|
+
(SOCKET_DIR / f"{safe_name}{suffix}").unlink(missing_ok=True)
|
|
298
343
|
|
|
299
344
|
|
|
300
345
|
def is_session_active(session_name: str) -> bool:
|
|
@@ -346,8 +391,14 @@ def is_lark_running() -> bool:
|
|
|
346
391
|
try:
|
|
347
392
|
pid = int(pid_file.read_text().strip())
|
|
348
393
|
os.kill(pid, 0)
|
|
349
|
-
|
|
350
|
-
|
|
394
|
+
# 额外验证:确认进程确实是我们的 lark_client(防止 PID 复用误判)
|
|
395
|
+
import subprocess
|
|
396
|
+
result = subprocess.run(
|
|
397
|
+
["ps", "-p", str(pid), "-o", "command="],
|
|
398
|
+
capture_output=True, text=True, timeout=2
|
|
399
|
+
)
|
|
400
|
+
return "lark_client" in result.stdout
|
|
401
|
+
except (ProcessLookupError, ValueError, OSError, Exception):
|
|
351
402
|
return False
|
|
352
403
|
|
|
353
404
|
|