remote-claude 0.1.4 → 0.1.5
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/README.md +19 -7
- package/bin/remote-claude +25 -0
- package/lark_client/card_builder.py +23 -0
- package/package.json +1 -1
- package/scripts/check-env.sh +3 -1
- package/server/component_parser.py +84 -4
- package/server/server.py +12 -1
package/README.md
CHANGED
|
@@ -23,7 +23,19 @@ Claude Code 只能在启动它的那个终端窗口里操作。一旦离开电
|
|
|
23
23
|
|
|
24
24
|
## 快速开始
|
|
25
25
|
|
|
26
|
-
### 1.
|
|
26
|
+
### 1. 安装
|
|
27
|
+
|
|
28
|
+
以下安装方式2选1, 安装后重启shell生效
|
|
29
|
+
|
|
30
|
+
#### 1.1 npm安装
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install remote-claude
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
需要安装uv、tmux等依赖,第一次可能有点慢.
|
|
37
|
+
|
|
38
|
+
#### 1.2 或 源码安装
|
|
27
39
|
|
|
28
40
|
```bash
|
|
29
41
|
git clone https://github.com/yyzybb537/remote_claude.git
|
|
@@ -39,8 +51,9 @@ cd remote_claude
|
|
|
39
51
|
|------|------|
|
|
40
52
|
| `cla` | 启动 Claude (以当前目录路径为会话名) |
|
|
41
53
|
| `cl` | 同 `cla`,但跳过权限确认 |
|
|
54
|
+
| `remote-claude` | 管理工具(一般不用)|
|
|
42
55
|
|
|
43
|
-
### 3. 从其他终端连接(
|
|
56
|
+
### 3. 从其他终端连接(比较少用)
|
|
44
57
|
|
|
45
58
|
```bash
|
|
46
59
|
remote-claude list
|
|
@@ -52,9 +65,9 @@ remote-claude attach <会话名>
|
|
|
52
65
|
#### 4.1 配置飞书机器人
|
|
53
66
|
|
|
54
67
|
1. 登录[飞书开放平台](https://open.feishu.cn/),创建企业自建应用
|
|
55
|
-
2.
|
|
56
|
-
3. 用`cla`或`cl`启动一次claude
|
|
57
|
-
4.
|
|
68
|
+
2. 获取 **App ID** 和 **App Secret**
|
|
69
|
+
3. 用`cla`或`cl`启动一次claude, 按照交互提示填入**App ID** 和 **App Secret**
|
|
70
|
+
4. [飞书开放平台]的企业自建应用页面`添加应用能力`(机器人能力)
|
|
58
71
|
5. 企业自建应用页面配置事件回调(如果第3步没启动成功这里配置不了):
|
|
59
72
|
- `事件与回调` -> `事件配置` -> `订阅方式`右边的笔图标 -> `选择:使用长连接接收事件` -> `点击保存` -> `下面添加事件: 接收消息 v2.0 (im.message.receive_v1)`
|
|
60
73
|
- `事件与回调` -> `回调配置` -> `订阅方式`右边的笔图标 -> `选择:使用长连接接收回调` -> `点击保存` -> `下面添加回调: 卡片回传交互 (card.action.trigger)`
|
|
@@ -187,7 +200,6 @@ remote-claude attach <会话名>
|
|
|
187
200
|
|
|
188
201
|
1. 从飞书搜素刚刚创建的飞书机器人(第一次搜比较慢,如果搜不到可能是忘记发布了)
|
|
189
202
|
2. 飞书中与机器人对话,可用命令:
|
|
190
|
-
- `/help` 展示可用命令
|
|
191
203
|
- `/menu` 展示菜单卡片,后续操作都操作这个卡片上的按钮即可
|
|
192
204
|
|
|
193
205
|
## 使用指南
|
|
@@ -199,7 +211,7 @@ remote-claude attach <会话名>
|
|
|
199
211
|
| `cla` | 启动飞书客户端 + 以当前目录路径为会话名启动 Claude |
|
|
200
212
|
| `cl` | 同 `cla`,但跳过权限确认 |
|
|
201
213
|
|
|
202
|
-
###
|
|
214
|
+
### 管理命令 (一般不需要)
|
|
203
215
|
|
|
204
216
|
```bash
|
|
205
217
|
remote-claude start <会话名> # 启动新会话
|
package/bin/remote-claude
CHANGED
|
@@ -13,6 +13,31 @@ if ! command -v uv &>/dev/null; then
|
|
|
13
13
|
[ -f "$HOME/.local/bin/uv" ] && export PATH="$HOME/.local/bin:$PATH"
|
|
14
14
|
fi
|
|
15
15
|
|
|
16
|
+
# log 子命令:查看最后启动的 session 日志
|
|
17
|
+
if [ "$1" = "log" ]; then
|
|
18
|
+
LOG_DIR="/tmp/remote-claude"
|
|
19
|
+
if [ -n "$2" ]; then
|
|
20
|
+
# 指定 session 名,/ 和 . 替换为 _(与 _safe_filename 一致)
|
|
21
|
+
SESSION_SAFE=$(echo "$2" | tr '/.' '__')
|
|
22
|
+
LOG_FILE="$LOG_DIR/${SESSION_SAFE}_messages.log"
|
|
23
|
+
else
|
|
24
|
+
# 找最后启动的 session(最新的 session .pid 文件,排除 lark.pid)
|
|
25
|
+
LATEST_PID=$(ls -t "$LOG_DIR"/_*.pid 2>/dev/null | head -1)
|
|
26
|
+
if [ -n "$LATEST_PID" ]; then
|
|
27
|
+
SESSION_SAFE=$(basename "$LATEST_PID" .pid)
|
|
28
|
+
LOG_FILE="$LOG_DIR/${SESSION_SAFE}_messages.log"
|
|
29
|
+
fi
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
if [ -z "$LOG_FILE" ] || [ ! -f "$LOG_FILE" ]; then
|
|
33
|
+
echo "没有找到日志文件(请先启动一个会话)" >&2
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
echo "📄 $LOG_FILE"
|
|
38
|
+
exec tail -50f "$LOG_FILE"
|
|
39
|
+
fi
|
|
40
|
+
|
|
16
41
|
# lark 子命令:检查 .env 配置
|
|
17
42
|
if [ "$1" = "lark" ]; then
|
|
18
43
|
source "$INSTALL_DIR/scripts/check-env.sh" "$INSTALL_DIR"
|
|
@@ -522,6 +522,22 @@ def _render_block_colored(block_dict: dict) -> Optional[str]:
|
|
|
522
522
|
return None
|
|
523
523
|
|
|
524
524
|
|
|
525
|
+
def _render_plan_block(block_dict: dict) -> Optional[Dict[str, Any]]:
|
|
526
|
+
"""将 PlanBlock 渲染为飞书 collapsible_panel element"""
|
|
527
|
+
content = block_dict.get("content", "")
|
|
528
|
+
if not content:
|
|
529
|
+
return None
|
|
530
|
+
title = block_dict.get("title", "计划")
|
|
531
|
+
ansi_content = block_dict.get("ansi_content", "")
|
|
532
|
+
content_md = _ansi_to_lark_md(ansi_content) if ansi_content else _escape_md(content)
|
|
533
|
+
return {
|
|
534
|
+
"tag": "collapsible_panel",
|
|
535
|
+
"expanded": True,
|
|
536
|
+
"header": {"title": {"tag": "plain_text", "content": f"📋 {title}"}},
|
|
537
|
+
"elements": [{"tag": "markdown", "content": content_md}],
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
|
|
525
541
|
def _determine_header(
|
|
526
542
|
blocks: List[dict],
|
|
527
543
|
status_line: Optional[dict],
|
|
@@ -609,6 +625,13 @@ def build_stream_card(
|
|
|
609
625
|
has_content = False
|
|
610
626
|
|
|
611
627
|
for block_dict in blocks:
|
|
628
|
+
typ = block_dict.get("_type", "")
|
|
629
|
+
if typ == "PlanBlock":
|
|
630
|
+
plan_el = _render_plan_block(block_dict)
|
|
631
|
+
if plan_el:
|
|
632
|
+
has_content = True
|
|
633
|
+
elements.append(plan_el)
|
|
634
|
+
continue
|
|
612
635
|
rendered = _render_block_colored(block_dict)
|
|
613
636
|
if rendered:
|
|
614
637
|
has_content = True
|
package/package.json
CHANGED
package/scripts/check-env.sh
CHANGED
|
@@ -21,6 +21,8 @@ if [ "$ENV_OK" = false ]; then
|
|
|
21
21
|
echo "飞书客户端尚未配置,需要填写应用凭证。"
|
|
22
22
|
echo "(在飞书开发者后台创建应用获取: https://open.feishu.cn/app)"
|
|
23
23
|
echo ""
|
|
24
|
+
echo -e "\033[33m飞书机器人配置文档参考:https://github.com/yyzybb537/remote_claude\033[0m"
|
|
25
|
+
echo ""
|
|
24
26
|
read -p "FEISHU_APP_ID: " INPUT_APP_ID
|
|
25
27
|
read -p "FEISHU_APP_SECRET: " INPUT_APP_SECRET
|
|
26
28
|
|
|
@@ -36,6 +38,6 @@ if [ "$ENV_OK" = false ]; then
|
|
|
36
38
|
|
|
37
39
|
echo ""
|
|
38
40
|
echo "配置已保存到 $ENV_FILE"
|
|
39
|
-
echo "
|
|
41
|
+
echo "(可选配置, 如"白名单"等可稍后编辑该文件)"
|
|
40
42
|
echo ""
|
|
41
43
|
fi
|
|
@@ -40,6 +40,65 @@ BOX_CORNER_TOP: Set[str] = {'╭', '┌'}
|
|
|
40
40
|
BOX_CORNER_BOTTOM: Set[str] = {'╰', '└'}
|
|
41
41
|
BOX_VERTICAL: Set[str] = {'│', '┃', '║'}
|
|
42
42
|
|
|
43
|
+
# OutputBlock 内嵌框线清理(纯文本版,用于 content 检测)
|
|
44
|
+
_INLINE_BOX_TOP_RE = re.compile(r'^\s*[╭┌][─━═╌]+[╮┐]\s*$')
|
|
45
|
+
_INLINE_BOX_BOTTOM_RE = re.compile(r'^\s*[╰└][─━═╌]+[╯┘]\s*$')
|
|
46
|
+
_INLINE_BOX_LEFT_RE = re.compile(r'^(\s*)[│┃║] ?')
|
|
47
|
+
_INLINE_BOX_RIGHT_RE = re.compile(r'\s*[│┃║]\s*$')
|
|
48
|
+
|
|
49
|
+
# ANSI 版(用于 ansi_content 清理)
|
|
50
|
+
_A = r'(?:\x1b\[[\d;]*m)*'
|
|
51
|
+
_ANSI_BOX_LEFT_RE = re.compile(rf'^({_A}\s*){_A}[│┃║]{_A} ?')
|
|
52
|
+
_ANSI_BOX_RIGHT_RE = re.compile(rf'\s*{_A}[│┃║]{_A}\s*$')
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _strip_inline_boxes_pair(content: str, ansi_content: str) -> tuple:
|
|
56
|
+
"""去除 OutputBlock 内嵌的 box-drawing 框线,同步清理 content 和 ansi_content。
|
|
57
|
+
|
|
58
|
+
仅在检测到完整 box(顶边框 + 底边框配对)时才去除,防止误伤普通 │ 内容。
|
|
59
|
+
返回 (cleaned_content, cleaned_ansi_content)。
|
|
60
|
+
"""
|
|
61
|
+
lines = content.split('\n')
|
|
62
|
+
top_stack: list = []
|
|
63
|
+
box_ranges: list = []
|
|
64
|
+
for i, line in enumerate(lines):
|
|
65
|
+
if _INLINE_BOX_TOP_RE.match(line):
|
|
66
|
+
top_stack.append(i)
|
|
67
|
+
elif _INLINE_BOX_BOTTOM_RE.match(line) and top_stack:
|
|
68
|
+
top_idx = top_stack.pop()
|
|
69
|
+
box_ranges.append((top_idx, i))
|
|
70
|
+
|
|
71
|
+
if not box_ranges:
|
|
72
|
+
return content, ansi_content
|
|
73
|
+
|
|
74
|
+
ansi_lines = ansi_content.split('\n')
|
|
75
|
+
if len(ansi_lines) != len(lines):
|
|
76
|
+
return content, ansi_content
|
|
77
|
+
|
|
78
|
+
remove_lines: set = set()
|
|
79
|
+
side_lines: set = set()
|
|
80
|
+
for top_idx, bottom_idx in box_ranges:
|
|
81
|
+
remove_lines.add(top_idx)
|
|
82
|
+
remove_lines.add(bottom_idx)
|
|
83
|
+
for j in range(top_idx + 1, bottom_idx):
|
|
84
|
+
side_lines.add(j)
|
|
85
|
+
|
|
86
|
+
result_content = []
|
|
87
|
+
result_ansi = []
|
|
88
|
+
for i, (line, aline) in enumerate(zip(lines, ansi_lines)):
|
|
89
|
+
if i in remove_lines:
|
|
90
|
+
continue
|
|
91
|
+
if i in side_lines:
|
|
92
|
+
line = _INLINE_BOX_LEFT_RE.sub(r'\1', line)
|
|
93
|
+
line = _INLINE_BOX_RIGHT_RE.sub('', line)
|
|
94
|
+
aline = _ANSI_BOX_LEFT_RE.sub(r'\1', aline)
|
|
95
|
+
aline = _ANSI_BOX_RIGHT_RE.sub('', aline)
|
|
96
|
+
result_content.append(line)
|
|
97
|
+
result_ansi.append(aline)
|
|
98
|
+
|
|
99
|
+
return '\n'.join(result_content), '\n'.join(result_ansi)
|
|
100
|
+
|
|
101
|
+
|
|
43
102
|
# 编号选项行正则(权限确认对话框特征:❯ 1. Yes / 2. No 等)
|
|
44
103
|
_NUMBERED_OPTION_RE = re.compile(r'^(?:❯\s*)?\d+[.)]\s+.+')
|
|
45
104
|
# 带 ❯ 光标的编号选项行正则(锚点)
|
|
@@ -454,10 +513,27 @@ class ScreenParser:
|
|
|
454
513
|
return output_rows, input_rows, bottom_rows
|
|
455
514
|
|
|
456
515
|
def _trim_welcome(self, screen: pyte.Screen, rows: List[int]) -> List[int]:
|
|
457
|
-
"""
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
516
|
+
"""去掉欢迎区域:跳过首列为空的前缀行和欢迎框(Claude Code box)"""
|
|
517
|
+
i = 0
|
|
518
|
+
while i < len(rows):
|
|
519
|
+
col0 = _get_col0(screen, rows[i])
|
|
520
|
+
if not col0.strip():
|
|
521
|
+
i += 1
|
|
522
|
+
continue
|
|
523
|
+
# 首个非空 col0 是 box 顶角 → 检查是否为欢迎框
|
|
524
|
+
if col0 in BOX_CORNER_TOP:
|
|
525
|
+
first_line = _get_row_text(screen, rows[i])
|
|
526
|
+
if 'Claude Code' in first_line:
|
|
527
|
+
# 跳过整个欢迎框(到 ╰/└ 行为止)
|
|
528
|
+
i += 1
|
|
529
|
+
while i < len(rows):
|
|
530
|
+
if _get_col0(screen, rows[i]) in BOX_CORNER_BOTTOM:
|
|
531
|
+
i += 1
|
|
532
|
+
break
|
|
533
|
+
i += 1
|
|
534
|
+
continue # 继续跳过后续空行
|
|
535
|
+
# 非欢迎框,返回剩余行
|
|
536
|
+
return rows[i:]
|
|
461
537
|
return []
|
|
462
538
|
|
|
463
539
|
# ─── Step 2+3+4:输出区解析 ────────────────────────────────────────────
|
|
@@ -552,6 +628,7 @@ class ScreenParser:
|
|
|
552
628
|
content = '\n'.join([first_content] + body_lines).rstrip()
|
|
553
629
|
ansi_body_lines = [_get_row_ansi_text(screen, r) for r in block_rows[1:]]
|
|
554
630
|
ansi_content = '\n'.join([cached_ansi_first] + ansi_body_lines).rstrip()
|
|
631
|
+
content, ansi_content = _strip_inline_boxes_pair(content, ansi_content)
|
|
555
632
|
return OutputBlock(
|
|
556
633
|
content=content, is_streaming=cached_blink, start_row=first_row,
|
|
557
634
|
ansi_content=ansi_content, indicator=cached_ind, ansi_indicator=cached_ansi_ind,
|
|
@@ -602,6 +679,7 @@ class ScreenParser:
|
|
|
602
679
|
body_lines = lines[1:]
|
|
603
680
|
content = '\n'.join([first_content] + body_lines).rstrip()
|
|
604
681
|
ansi_content = '\n'.join([ansi_first] + ansi_body).rstrip()
|
|
682
|
+
content, ansi_content = _strip_inline_boxes_pair(content, ansi_content)
|
|
605
683
|
return OutputBlock(
|
|
606
684
|
content=content, is_streaming=is_blink, start_row=first_row,
|
|
607
685
|
ansi_content=ansi_content, indicator=ind, ansi_indicator=ansi_ind,
|
|
@@ -629,6 +707,8 @@ class ScreenParser:
|
|
|
629
707
|
inner = stripped[:-1]
|
|
630
708
|
content_lines.append(inner.rstrip())
|
|
631
709
|
ansi_line = _get_row_ansi_text(screen, row, start_col=1)
|
|
710
|
+
# 去掉右侧 │(可能包裹 ANSI 码,如 │\x1b[0m 或 \x1b[...m│\x1b[0m)
|
|
711
|
+
ansi_line = re.sub(r'(\x1b\[[0-9;]*m)*[│┃║](\x1b\[0m)?\s*$', '', ansi_line)
|
|
632
712
|
ansi_lines.append(ansi_line.rstrip())
|
|
633
713
|
|
|
634
714
|
content = '\n'.join(content_lines).strip()
|
package/server/server.py
CHANGED
|
@@ -119,6 +119,17 @@ class OutputWatcher:
|
|
|
119
119
|
self.last_window: Optional[ClaudeWindow] = None
|
|
120
120
|
# PTY 静止后延迟重刷:消除窗口平滑的延迟效应
|
|
121
121
|
self._reflush_handle: Optional[asyncio.TimerHandle] = None
|
|
122
|
+
# 调试日志截断长度(可通过 ~/.remote-claude/.debug_config 配置)
|
|
123
|
+
self._debug_truncate_len = 80
|
|
124
|
+
try:
|
|
125
|
+
cfg_path = os.path.expanduser("~/.remote-claude/.debug_config")
|
|
126
|
+
if os.path.exists(cfg_path):
|
|
127
|
+
import json as _json
|
|
128
|
+
with open(cfg_path) as _f:
|
|
129
|
+
_cfg = _json.load(_f)
|
|
130
|
+
self._debug_truncate_len = int(_cfg.get("debug_truncate_len", 80))
|
|
131
|
+
except Exception:
|
|
132
|
+
pass
|
|
122
133
|
|
|
123
134
|
def resize(self, cols: int, rows: int):
|
|
124
135
|
"""重建 renderer 以适应新尺寸,历史随之丢失(可接受)。
|
|
@@ -403,7 +414,7 @@ class OutputWatcher:
|
|
|
403
414
|
else:
|
|
404
415
|
lines.append(f"[{i}] UserInput: {block.text[:80]}")
|
|
405
416
|
else:
|
|
406
|
-
lines.append(f"[{i}] {type(block).__name__}: {str(block)[:
|
|
417
|
+
lines.append(f"[{i}] {type(block).__name__}: {str(block)[:self._debug_truncate_len]}")
|
|
407
418
|
lines.append("")
|
|
408
419
|
lines.append("-----")
|
|
409
420
|
with open(self._debug_file, "w", encoding="utf-8") as f:
|