remote-claude 0.1.0

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 ADDED
@@ -0,0 +1,15 @@
1
+ # Remote Claude 飞书客户端配置
2
+
3
+ # 飞书应用配置(必填)
4
+ # 在飞书开发者后台创建应用获取: https://open.feishu.cn/app
5
+ FEISHU_APP_ID=cli_xxxxx
6
+ FEISHU_APP_SECRET=xxxxx
7
+
8
+ # 用户白名单(可选)
9
+ # 启用后只有白名单中的用户可以使用
10
+ ENABLE_USER_WHITELIST=false
11
+ ALLOWED_USERS=ou_xxxxx,ou_yyyyy
12
+
13
+ # 机器人名称(用于群聊命名,默认 Claude)
14
+ BOT_NAME=Ys-Claude
15
+
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Remote Claude Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,231 @@
1
+ # Remote Claude
2
+
3
+ **在电脑终端上打开的 Claude Code 进程,也可以在飞书中共享操作。电脑端、手机端无缝来回切换**
4
+
5
+ 电脑上用终端跑 Claude Code 写代码,同时在手机飞书上看进度、发指令、点按钮 — 不用守在电脑前,随时随地掌控 AI 编程。
6
+
7
+ ## 为什么需要它?
8
+
9
+ Claude Code 只能在启动它的那个终端窗口里操作。一旦离开电脑,就只能干等。Remote Claude 让你:
10
+
11
+ - **飞书里直接操作** — 手机/平板打开飞书,就能看到 Claude 的实时输出,发消息、选选项、批准权限,和终端里一模一样。
12
+ - **用手机无缝延续电脑上做的工作** — 电脑上打开的Claude进程,也可以用飞书共享操作,开会、午休、通勤、上厕所时,都可以用手机延续之前在电脑上的工作。
13
+ - **在电脑上也可以无缝延续手机上的工作** - 在lark端也可以打开新的Claude进程启动新的工作,回到电脑前还可以`attach`共享操作同一个Claude进程,延续手机端的工作。
14
+ - **多端共享操作** — 多个终端 + 飞书可以共享操作同一个claude进程,回到家里ssh登录到服务器上也可以通过`attach`继续操作在公司ssh登录到服务器上打开的claude进程操作。
15
+ - **机制安全** - 完全不侵入 Claude 进程,remote 功能完全通过终端交互来实现,不必担心 Claude 进程意外崩溃导致工作进展丢失。
16
+
17
+ ## 飞书端体验
18
+
19
+ - 彩色代码输出,ANSI 着色完整还原
20
+ - 交互式按钮:选项选择、权限确认,一键点击
21
+ - 流式卡片更新:Claude 边想边输出,飞书端实时滚动显示
22
+ - 后台 agent 状态面板:查看并管理正在运行的子任务
23
+
24
+ ## 快速开始
25
+
26
+ ### 1. 克隆并初始化
27
+
28
+ ```bash
29
+ git clone https://github.com/yyzybb537/remote_claude.git
30
+ cd remote_claude
31
+ ./init.sh
32
+ ```
33
+
34
+ `init.sh` 会自动安装 uv、tmux 等依赖,配置飞书环境(可选),并写入 `cla` / `cl` 快捷命令。执行完成后重启终端生效。
35
+
36
+ ### 2. 启动
37
+
38
+ | 快捷命令 | 说明 |
39
+ |------|------|
40
+ | `cla` | 启动 Claude (以当前目录路径为会话名) |
41
+ | `cl` | 同 `cla`,但跳过权限确认 |
42
+
43
+ ### 3. 从其他终端连接(一般不需要)
44
+
45
+ ```bash
46
+ remote-claude list
47
+ remote-claude attach <会话名>
48
+ ```
49
+
50
+ ### 4. 从飞书端连接
51
+
52
+ #### 4.1 配置飞书机器人
53
+
54
+ 1. 登录[飞书开放平台](https://open.feishu.cn/),创建企业自建应用
55
+ 2. 执行命令`cp .env.example .env`, 获取 **App ID** 和 **App Secret**,填入 `.env` 文件
56
+ 3. 用`cla`或`cl`启动一次claude(会附带启动飞书客户端,如果之前已经启动过,需要重启飞书客户端: "remote-claude lark restart" )
57
+ 4. 企业自建应用页面`添加应用能力`(机器人能力)
58
+ 5. 企业自建应用页面配置事件回调(如果第3步没启动成功这里配置不了):
59
+ - `事件与回调` -> `事件配置` -> `订阅方式`右边的笔图标 -> `选择:使用长连接接收事件` -> `点击保存` -> `下面添加事件: 接收消息 v2.0 (im.message.receive_v1)`
60
+ - `事件与回调` -> `回调配置` -> `订阅方式`右边的笔图标 -> `选择:使用长连接接收回调` -> `点击保存` -> `下面添加回调: 卡片回传交互 (card.action.trigger)`
61
+ 6. 企业自建应用页面配置权限:
62
+ - `权限管理` -> `批量导入/导出权限` -> 导入以下内容
63
+ ```json
64
+ {
65
+ "scopes": {
66
+ "tenant": [
67
+ "base:app:read",
68
+ "base:field:read",
69
+ "base:form:read",
70
+ "base:record:read",
71
+ "base:record:retrieve",
72
+ "base:table:read",
73
+ "board:whiteboard:node:read",
74
+ "calendar:calendar.free_busy:read",
75
+ "cardkit:card:write",
76
+ "contact:contact.base:readonly",
77
+ "contact:user.employee_id:readonly",
78
+ "contact:user.id:readonly",
79
+ "docs:document.comment:read",
80
+ "docs:document.content:read",
81
+ "docs:document.media:download",
82
+ "docs:document.media:upload",
83
+ "docs:document:import",
84
+ "docs:permission.member:auth",
85
+ "docs:permission.member:create",
86
+ "docs:permission.member:transfer",
87
+ "docx:document.block:convert",
88
+ "docx:document:create",
89
+ "docx:document:readonly",
90
+ "docx:document:write_only",
91
+ "drive:drive.metadata:readonly",
92
+ "drive:drive.search:readonly",
93
+ "drive:drive:version:readonly",
94
+ "drive:file:download",
95
+ "drive:file:upload",
96
+ "im:chat.members:read",
97
+ "im:chat.members:write_only",
98
+ "im:chat.tabs:read",
99
+ "im:chat.tabs:write_only",
100
+ "im:chat.top_notice:write_only",
101
+ "im:chat:create",
102
+ "im:chat:delete",
103
+ "im:chat:operate_as_owner",
104
+ "im:chat:read",
105
+ "im:chat:update",
106
+ "im:message.group_at_msg:readonly",
107
+ "im:message.group_msg",
108
+ "im:message.p2p_msg:readonly",
109
+ "im:message.reactions:read",
110
+ "im:message.reactions:write_only",
111
+ "im:message:readonly",
112
+ "im:message:recall",
113
+ "im:message:send_as_bot",
114
+ "im:message:update",
115
+ "im:resource",
116
+ "sheets:spreadsheet.meta:read",
117
+ "sheets:spreadsheet.meta:write_only",
118
+ "sheets:spreadsheet:create",
119
+ "sheets:spreadsheet:read",
120
+ "sheets:spreadsheet:write_only",
121
+ "space:document:delete",
122
+ "space:document:retrieve",
123
+ "wiki:wiki:readonly"
124
+ ],
125
+ "user": [
126
+ "base:app:read",
127
+ "base:field:read",
128
+ "base:record:read",
129
+ "base:record:retrieve",
130
+ "base:table:read",
131
+ "calendar:calendar.event:create",
132
+ "calendar:calendar.event:delete",
133
+ "calendar:calendar.event:read",
134
+ "calendar:calendar.event:reply",
135
+ "calendar:calendar.event:update",
136
+ "calendar:calendar.free_busy:read",
137
+ "calendar:calendar:read",
138
+ "cardkit:card:write",
139
+ "contact:user.base:readonly",
140
+ "contact:user.employee_id:readonly",
141
+ "contact:user.id:readonly",
142
+ "docs:document.comment:read",
143
+ "docs:document.content:read",
144
+ "docs:document.media:download",
145
+ "docs:document.media:upload",
146
+ "docx:document.block:convert",
147
+ "docx:document:create",
148
+ "docx:document:readonly",
149
+ "docx:document:write_only",
150
+ "im:chat.managers:write_only",
151
+ "im:chat.members:read",
152
+ "im:chat.members:write_only",
153
+ "im:chat.tabs:read",
154
+ "im:chat.tabs:write_only",
155
+ "im:chat.top_notice:write_only",
156
+ "im:chat:delete",
157
+ "im:chat:read",
158
+ "im:chat:update",
159
+ "im:message.reactions:read",
160
+ "im:message.reactions:write_only",
161
+ "im:message:readonly",
162
+ "im:message:recall",
163
+ "im:message:update",
164
+ "search:docs:read",
165
+ "search:suite_dataset:readonly",
166
+ "sheets:spreadsheet.meta:read",
167
+ "sheets:spreadsheet.meta:write_only",
168
+ "sheets:spreadsheet:create",
169
+ "sheets:spreadsheet:read",
170
+ "sheets:spreadsheet:write_only",
171
+ "space:document:retrieve",
172
+ "task:task:read",
173
+ "task:task:readonly",
174
+ "task:task:write",
175
+ "task:task:writeonly",
176
+ "task:tasklist:read",
177
+ "task:tasklist:writeonly",
178
+ "wiki:wiki:readonly"
179
+ ]
180
+ }
181
+ }
182
+ ```
183
+ 7. 企业自建应用页面: `创建版本` -> `发布到线上`
184
+ 8. 至此,完成飞书机器人配置
185
+
186
+ #### 4.2 通过飞书机器人操作claude
187
+
188
+ 1. 从飞书搜素刚刚创建的飞书机器人(第一次搜比较慢,如果搜不到可能是忘记发布了)
189
+ 2. 飞书中与机器人对话,可用命令:
190
+ - `/help` 展示可用命令
191
+ - `/menu` 展示菜单卡片,后续操作都操作这个卡片上的按钮即可
192
+
193
+ ## 使用指南
194
+
195
+ ### 快捷命令
196
+
197
+ | 命令 | 说明 |
198
+ |------|------|
199
+ | `cla` | 启动飞书客户端 + 以当前目录路径为会话名启动 Claude |
200
+ | `cl` | 同 `cla`,但跳过权限确认 |
201
+
202
+ ### 终端命令
203
+
204
+ ```bash
205
+ remote-claude start <会话名> # 启动新会话
206
+ remote-claude attach <会话名> # 连接现有会话
207
+ remote-claude list # 查看所有会话
208
+ remote-claude kill <会话名> # 终止会话
209
+ ```
210
+
211
+ ### 飞书客户端
212
+
213
+ ```bash
214
+ remote-claude lark start # 启动(后台运行)
215
+ remote-claude lark stop # 停止
216
+ remote-claude lark restart # 重启
217
+ remote-claude lark status # 查看状态
218
+ ```
219
+
220
+ 飞书中与机器人对话,可用命令:`/menu`、`/attach`、`/detach`、`/list`、`/help` 等。
221
+
222
+ ## 系统要求
223
+
224
+ - **操作系统**: macOS 或 Linux
225
+ - **依赖工具**: [uv](https://docs.astral.sh/uv/)、[tmux](https://github.com/tmux/tmux)、[Claude CLI](https://claude.ai/code)
226
+ - **可选**: 飞书企业自建应用
227
+
228
+ ## 文档
229
+
230
+ - [CLAUDE.md](./CLAUDE.md) — 项目架构和开发说明
231
+ - [LARK_CLIENT_GUIDE.md](./LARK_CLIENT_GUIDE.md) — 飞书客户端完整指南
package/bin/cl 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)" -- --dangerously-skip-permissions --permission-mode=dontAsk "$@"
package/bin/cla 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)" -- "$@"
@@ -0,0 +1,21 @@
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
+ INSTALL_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
+ # lark 子命令:检查 .env 配置
17
+ if [ "$1" = "lark" ]; then
18
+ source "$INSTALL_DIR/scripts/check-env.sh" "$INSTALL_DIR"
19
+ fi
20
+
21
+ exec uv run --project "$INSTALL_DIR" python3 "$INSTALL_DIR/remote_claude.py" "$@"
@@ -0,0 +1,251 @@
1
+ """
2
+ 客户端连接器
3
+
4
+ - 终端 raw mode 处理
5
+ - Socket 连接
6
+ - 输入转发
7
+ - 输出显示
8
+ - Ctrl+D 退出
9
+ """
10
+
11
+ import asyncio
12
+ import os
13
+ import sys as _sys
14
+ _sys.path.insert(0, str(__import__('pathlib').Path(__file__).parent.parent)) # 根目录 → protocol, utils
15
+ import sys
16
+ import tty
17
+ import termios
18
+ import signal
19
+ import select
20
+ from typing import Optional
21
+
22
+ from utils.protocol import (
23
+ Message, MessageType, InputMessage, ResizeMessage,
24
+ encode_message, decode_message
25
+ )
26
+ from utils.session import get_socket_path, generate_client_id, get_terminal_size
27
+
28
+ try:
29
+ from stats import track as _track_stats
30
+ except Exception:
31
+ def _track_stats(*args, **kwargs): pass
32
+
33
+
34
+ # 特殊按键
35
+ CTRL_D = b'\x04' # Ctrl+D - 退出
36
+
37
+
38
+ class RemoteClient:
39
+ """远程客户端"""
40
+
41
+ def __init__(self, session_name: str):
42
+ self.session_name = session_name
43
+ self.socket_path = get_socket_path(session_name)
44
+ self.client_id = generate_client_id()
45
+
46
+ # 连接
47
+ self.reader: Optional[asyncio.StreamReader] = None
48
+ self.writer: Optional[asyncio.StreamWriter] = None
49
+ self.buffer = b""
50
+
51
+ # 状态
52
+ self.running = False
53
+
54
+ # 终端设置
55
+ self.old_settings = None
56
+
57
+ async def connect(self) -> bool:
58
+ """连接到服务器"""
59
+ if not self.socket_path.exists():
60
+ print(f"错误: 会话 '{self.session_name}' 不存在")
61
+ return False
62
+
63
+ try:
64
+ self.reader, self.writer = await asyncio.open_unix_connection(
65
+ path=str(self.socket_path)
66
+ )
67
+ return True
68
+ except Exception as e:
69
+ print(f"连接失败: {e}")
70
+ return False
71
+
72
+ async def run(self):
73
+ """运行客户端"""
74
+ if not await self.connect():
75
+ return
76
+
77
+ self.running = True
78
+ _track_stats('terminal', 'connect', session_name=self.session_name)
79
+
80
+ # 设置终端 raw mode
81
+ self._setup_terminal()
82
+
83
+ # 设置信号处理
84
+ self._setup_signals()
85
+
86
+ # 发送初始终端尺寸,让 server 将 PTY 调整为实际终端大小
87
+ rows, cols = get_terminal_size()
88
+ await self._send_resize(rows, cols)
89
+
90
+ try:
91
+ # 并行运行输入和输出处理
92
+ await asyncio.gather(
93
+ self._read_server(),
94
+ self._read_stdin(),
95
+ return_exceptions=True
96
+ )
97
+ finally:
98
+ self._cleanup()
99
+
100
+ def _setup_terminal(self):
101
+ """设置终端 raw mode"""
102
+ if sys.stdin.isatty():
103
+ self.old_settings = termios.tcgetattr(sys.stdin)
104
+ tty.setraw(sys.stdin.fileno())
105
+
106
+ def _restore_terminal(self):
107
+ """恢复终端设置"""
108
+ if self.old_settings:
109
+ termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.old_settings)
110
+
111
+ def _setup_signals(self):
112
+ """设置信号处理"""
113
+ signal.signal(signal.SIGWINCH, self._handle_resize)
114
+
115
+ def _handle_resize(self, signum, frame):
116
+ """处理终端大小变化"""
117
+ if self.running and self.writer:
118
+ rows, cols = get_terminal_size()
119
+ asyncio.create_task(self._send_resize(rows, cols))
120
+
121
+ async def _send_resize(self, rows: int, cols: int):
122
+ """发送终端大小"""
123
+ msg = ResizeMessage(rows, cols, self.client_id)
124
+ await self._send_message(msg)
125
+
126
+ async def _read_server(self):
127
+ """读取服务器消息"""
128
+ while self.running:
129
+ try:
130
+ msg = await asyncio.wait_for(self._read_message(), timeout=0.5)
131
+ if msg is None:
132
+ self.running = False
133
+ break
134
+ await self._handle_server_message(msg)
135
+ except asyncio.TimeoutError:
136
+ continue
137
+ except Exception:
138
+ break
139
+
140
+ async def _read_message(self) -> Optional[Message]:
141
+ """读取一条消息"""
142
+ while True:
143
+ if b"\n" in self.buffer:
144
+ line, self.buffer = self.buffer.split(b"\n", 1)
145
+ try:
146
+ return decode_message(line)
147
+ except Exception:
148
+ continue
149
+
150
+ try:
151
+ data = await self.reader.read(4096)
152
+ if not data:
153
+ return None
154
+ self.buffer += data
155
+ except Exception:
156
+ return None
157
+
158
+ async def _handle_server_message(self, msg: Message):
159
+ """处理服务器消息"""
160
+ if msg.type == MessageType.OUTPUT:
161
+ data = msg.get_data()
162
+ sys.stdout.buffer.write(data)
163
+ sys.stdout.buffer.flush()
164
+
165
+ elif msg.type == MessageType.HISTORY:
166
+ data = msg.get_data()
167
+ sys.stdout.buffer.write(data)
168
+ sys.stdout.buffer.flush()
169
+
170
+ async def _read_stdin(self):
171
+ """读取标准输入"""
172
+ loop = asyncio.get_event_loop()
173
+
174
+ while self.running:
175
+ try:
176
+ # 在线程池中读取标准输入(带超时)
177
+ data = await loop.run_in_executor(None, self._read_stdin_sync)
178
+ if data:
179
+ await self._handle_input(data)
180
+ if not self.running:
181
+ break
182
+ except Exception:
183
+ break
184
+
185
+ def _read_stdin_sync(self) -> bytes:
186
+ """同步读取标准输入(带超时,便于检查 running 状态)"""
187
+ # 使用 select 等待输入,超时 0.1 秒
188
+ rlist, _, _ = select.select([sys.stdin], [], [], 0.1)
189
+ if rlist:
190
+ return os.read(sys.stdin.fileno(), 1024)
191
+ return b""
192
+
193
+ async def _handle_input(self, data: bytes):
194
+ """处理输入"""
195
+ # Ctrl+D 退出
196
+ if data == CTRL_D:
197
+ self.running = False
198
+ return
199
+
200
+ # 其他按键都发送给 Claude
201
+ _track_stats('terminal', 'input', session_name=self.session_name,
202
+ value=len(data))
203
+ await self._send_input(data)
204
+
205
+ async def _send_input(self, data: bytes):
206
+ """发送输入"""
207
+ msg = InputMessage(data, self.client_id)
208
+ await self._send_message(msg)
209
+
210
+ async def _send_message(self, msg: Message):
211
+ """发送消息"""
212
+ if self.writer:
213
+ try:
214
+ data = encode_message(msg)
215
+ self.writer.write(data)
216
+ await self.writer.drain()
217
+ except Exception:
218
+ pass
219
+
220
+ def _cleanup(self):
221
+ """清理"""
222
+ self.running = False
223
+ _track_stats('terminal', 'disconnect', session_name=self.session_name)
224
+ self._restore_terminal()
225
+
226
+ if self.writer:
227
+ try:
228
+ self.writer.close()
229
+ except Exception:
230
+ pass
231
+
232
+ print("\n已断开连接")
233
+
234
+
235
+ def run_client(session_name: str):
236
+ """运行客户端"""
237
+ client = RemoteClient(session_name)
238
+
239
+ try:
240
+ asyncio.run(client.run())
241
+ except KeyboardInterrupt:
242
+ pass
243
+
244
+
245
+ if __name__ == "__main__":
246
+ import argparse
247
+ parser = argparse.ArgumentParser(description="Remote Claude Client")
248
+ parser.add_argument("session_name", help="会话名称")
249
+ args = parser.parse_args()
250
+
251
+ run_client(args.session_name)
@@ -0,0 +1,3 @@
1
+ """
2
+ Remote Claude 飞书客户端
3
+ """
@@ -0,0 +1,91 @@
1
+ """
2
+ 捕获 remote_claude 的实际输出用于分析
3
+ """
4
+
5
+ import asyncio
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ sys.path.insert(0, str(Path(__file__).parent.parent))
10
+
11
+ from utils.protocol import Message, MessageType, decode_message
12
+ from utils.session import get_socket_path
13
+
14
+
15
+ async def capture_output(session_name: str, duration: int = 30):
16
+ """捕获会话输出"""
17
+ socket_path = get_socket_path(session_name)
18
+
19
+ if not socket_path.exists():
20
+ print(f"会话 {session_name} 不存在")
21
+ return
22
+
23
+ print(f"连接到 {session_name}...")
24
+
25
+ reader, writer = await asyncio.open_unix_connection(path=str(socket_path))
26
+
27
+ print(f"已连接,捕获 {duration} 秒的输出...")
28
+ print("=" * 60)
29
+
30
+ buffer = b""
31
+ outputs = []
32
+
33
+ async def read_messages():
34
+ nonlocal buffer
35
+ while True:
36
+ try:
37
+ data = await asyncio.wait_for(reader.read(4096), timeout=1.0)
38
+ if not data:
39
+ break
40
+ buffer += data
41
+
42
+ while b"\n" in buffer:
43
+ line, buffer = buffer.split(b"\n", 1)
44
+ try:
45
+ msg = decode_message(line)
46
+ if msg.type == MessageType.OUTPUT:
47
+ raw_data = msg.get_data()
48
+ outputs.append(raw_data)
49
+ print(f"收到 {len(raw_data)} 字节:")
50
+ print(f" 原始: {raw_data[:100]}...")
51
+ print(f" 解码: {raw_data.decode('utf-8', errors='replace')[:100]}...")
52
+ print()
53
+ except Exception as e:
54
+ print(f"解析错误: {e}")
55
+ except asyncio.TimeoutError:
56
+ continue
57
+ except Exception as e:
58
+ print(f"读取错误: {e}")
59
+ break
60
+
61
+ try:
62
+ await asyncio.wait_for(read_messages(), timeout=duration)
63
+ except asyncio.TimeoutError:
64
+ pass
65
+
66
+ writer.close()
67
+ await writer.wait_closed()
68
+
69
+ print("=" * 60)
70
+ print(f"共收到 {len(outputs)} 条输出消息")
71
+
72
+ # 保存原始输出到文件
73
+ output_file = Path("/tmp/claude_raw_output.bin")
74
+ with open(output_file, "wb") as f:
75
+ for data in outputs:
76
+ f.write(data)
77
+ f.write(b"\n---\n")
78
+
79
+ print(f"原始输出已保存到 {output_file}")
80
+
81
+ # 合并并分析
82
+ all_data = b"".join(outputs)
83
+ print(f"\n合并后总长度: {len(all_data)} 字节")
84
+ print(f"合并后内容:\n{all_data.decode('utf-8', errors='replace')}")
85
+
86
+
87
+ if __name__ == "__main__":
88
+ session = sys.argv[1] if len(sys.argv) > 1 else "test"
89
+ duration = int(sys.argv[2]) if len(sys.argv) > 2 else 30
90
+
91
+ asyncio.run(capture_output(session, duration))