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 +4 -0
- package/README.md +7 -2
- package/bin/cdx +20 -0
- package/bin/cx +20 -0
- package/bin/remote-claude +12 -2
- package/client/__init__.py +3 -0
- package/init.sh +31 -2
- package/lark_client/card_builder.py +14 -7
- package/lark_client/config.py +10 -0
- package/lark_client/main.py +52 -8
- package/lark_client/session_bridge.py +1 -1
- package/package.json +1 -1
- package/remote_claude.py +36 -11
- package/server/__init__.py +1 -0
- package/server/server.py +44 -8
- package/server/shared_state.py +3 -2
- package/utils/__init__.py +3 -0
- package/utils/session.py +23 -2
package/.env.example
CHANGED
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)
|
|
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
|
|
25
|
-
|
|
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"
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
805
|
-
meta_str = " ".join(meta_parts) if meta_parts else ""
|
|
814
|
+
lines.append(f"`{display_cwd}`")
|
|
806
815
|
|
|
807
|
-
header_text =
|
|
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 = "断开连接"
|
package/lark_client/config.py
CHANGED
|
@@ -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
|
package/lark_client/main.py
CHANGED
|
@@ -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.
|
|
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
package/remote_claude.py
CHANGED
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
Remote Claude - 双端共享 Claude CLI 工具
|
|
4
4
|
|
|
5
5
|
命令:
|
|
6
|
-
start <name>
|
|
7
|
-
attach <name>
|
|
8
|
-
list
|
|
9
|
-
kill <name>
|
|
10
|
-
|
|
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("-" *
|
|
188
|
-
print(f"{'
|
|
189
|
-
print("-" *
|
|
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
|
-
|
|
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("-" *
|
|
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.
|
|
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
|
-
#
|
|
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
|
-
|
|
1033
|
+
|
|
1034
|
+
# 先配置基本输出(stdout)
|
|
1005
1035
|
logging.basicConfig(
|
|
1006
|
-
level=logging.
|
|
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,
|
package/server/shared_state.py
CHANGED
|
@@ -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", "
|
|
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)
|
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
|
# 进程不存在或文件被并发清理,清理残留文件
|