remote-claude 1.0.4 → 1.0.6
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 +12 -134
- package/bin/remote-claude +0 -5
- package/lark_client/lark_handler.py +41 -36
- package/lark_client/session_bridge.py +2 -2
- package/lark_client/setup_wizard.py +1023 -0
- package/package.json +1 -1
- package/remote_claude.py +25 -0
- package/scripts/check-env.sh +0 -43
package/README.md
CHANGED
|
@@ -66,143 +66,21 @@ remote-claude attach <会话名>
|
|
|
66
66
|
|
|
67
67
|
#### 4.1 配置飞书机器人
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
5. 企业自建应用页面配置事件回调(如果第3步没启动成功这里配置不了):
|
|
74
|
-
- `事件与回调` -> `事件配置` -> `订阅方式`右边的笔图标 -> `选择:使用长连接接收事件` -> `点击保存` -> `下面添加事件: 接收消息 v2.0 (im.message.receive_v1)`
|
|
75
|
-
- `事件与回调` -> `回调配置` -> `订阅方式`右边的笔图标 -> `选择:使用长连接接收回调` -> `点击保存` -> `下面添加回调: 卡片回传交互 (card.action.trigger)`
|
|
76
|
-
6. 企业自建应用页面配置权限:
|
|
77
|
-
- `权限管理` -> `批量导入/导出权限` -> 导入以下内容
|
|
78
|
-
```json
|
|
79
|
-
{
|
|
80
|
-
"scopes": {
|
|
81
|
-
"tenant": [
|
|
82
|
-
"base:app:read",
|
|
83
|
-
"base:field:read",
|
|
84
|
-
"base:form:read",
|
|
85
|
-
"base:record:read",
|
|
86
|
-
"base:record:retrieve",
|
|
87
|
-
"base:table:read",
|
|
88
|
-
"board:whiteboard:node:read",
|
|
89
|
-
"calendar:calendar.free_busy:read",
|
|
90
|
-
"cardkit:card:write",
|
|
91
|
-
"contact:contact.base:readonly",
|
|
92
|
-
"contact:user.employee_id:readonly",
|
|
93
|
-
"contact:user.id:readonly",
|
|
94
|
-
"docs:document.comment:read",
|
|
95
|
-
"docs:document.content:read",
|
|
96
|
-
"docs:document.media:download",
|
|
97
|
-
"docs:document.media:upload",
|
|
98
|
-
"docs:document:import",
|
|
99
|
-
"docs:permission.member:auth",
|
|
100
|
-
"docs:permission.member:create",
|
|
101
|
-
"docs:permission.member:transfer",
|
|
102
|
-
"docx:document.block:convert",
|
|
103
|
-
"docx:document:create",
|
|
104
|
-
"docx:document:readonly",
|
|
105
|
-
"docx:document:write_only",
|
|
106
|
-
"drive:drive.metadata:readonly",
|
|
107
|
-
"drive:drive.search:readonly",
|
|
108
|
-
"drive:drive:version:readonly",
|
|
109
|
-
"drive:file:download",
|
|
110
|
-
"drive:file:upload",
|
|
111
|
-
"im:chat.members:read",
|
|
112
|
-
"im:chat.members:write_only",
|
|
113
|
-
"im:chat.tabs:read",
|
|
114
|
-
"im:chat.tabs:write_only",
|
|
115
|
-
"im:chat.top_notice:write_only",
|
|
116
|
-
"im:chat:create",
|
|
117
|
-
"im:chat:delete",
|
|
118
|
-
"im:chat:operate_as_owner",
|
|
119
|
-
"im:chat:read",
|
|
120
|
-
"im:chat:update",
|
|
121
|
-
"im:message.group_at_msg:readonly",
|
|
122
|
-
"im:message.group_msg",
|
|
123
|
-
"im:message.p2p_msg:readonly",
|
|
124
|
-
"im:message.reactions:read",
|
|
125
|
-
"im:message.reactions:write_only",
|
|
126
|
-
"im:message.urgent",
|
|
127
|
-
"im:message.urgent.status:write",
|
|
128
|
-
"im:message:readonly",
|
|
129
|
-
"im:message:recall",
|
|
130
|
-
"im:message:send_as_bot",
|
|
131
|
-
"im:message:update",
|
|
132
|
-
"im:resource",
|
|
133
|
-
"sheets:spreadsheet.meta:read",
|
|
134
|
-
"sheets:spreadsheet.meta:write_only",
|
|
135
|
-
"sheets:spreadsheet:create",
|
|
136
|
-
"sheets:spreadsheet:read",
|
|
137
|
-
"sheets:spreadsheet:write_only",
|
|
138
|
-
"space:document:delete",
|
|
139
|
-
"space:document:retrieve",
|
|
140
|
-
"wiki:wiki:readonly"
|
|
141
|
-
],
|
|
142
|
-
"user": [
|
|
143
|
-
"base:app:read",
|
|
144
|
-
"base:field:read",
|
|
145
|
-
"base:record:read",
|
|
146
|
-
"base:record:retrieve",
|
|
147
|
-
"base:table:read",
|
|
148
|
-
"calendar:calendar.event:create",
|
|
149
|
-
"calendar:calendar.event:delete",
|
|
150
|
-
"calendar:calendar.event:read",
|
|
151
|
-
"calendar:calendar.event:reply",
|
|
152
|
-
"calendar:calendar.event:update",
|
|
153
|
-
"calendar:calendar.free_busy:read",
|
|
154
|
-
"calendar:calendar:read",
|
|
155
|
-
"cardkit:card:write",
|
|
156
|
-
"contact:user.base:readonly",
|
|
157
|
-
"contact:user.employee_id:readonly",
|
|
158
|
-
"contact:user.id:readonly",
|
|
159
|
-
"docs:document.comment:read",
|
|
160
|
-
"docs:document.content:read",
|
|
161
|
-
"docs:document.media:download",
|
|
162
|
-
"docs:document.media:upload",
|
|
163
|
-
"docx:document.block:convert",
|
|
164
|
-
"docx:document:create",
|
|
165
|
-
"docx:document:readonly",
|
|
166
|
-
"docx:document:write_only",
|
|
167
|
-
"im:chat.managers:write_only",
|
|
168
|
-
"im:chat.members:read",
|
|
169
|
-
"im:chat.members:write_only",
|
|
170
|
-
"im:chat.tabs:read",
|
|
171
|
-
"im:chat.tabs:write_only",
|
|
172
|
-
"im:chat.top_notice:write_only",
|
|
173
|
-
"im:chat:delete",
|
|
174
|
-
"im:chat:read",
|
|
175
|
-
"im:chat:update",
|
|
176
|
-
"im:message.reactions:read",
|
|
177
|
-
"im:message.reactions:write_only",
|
|
178
|
-
"im:message:readonly",
|
|
179
|
-
"im:message:recall",
|
|
180
|
-
"im:message:update",
|
|
181
|
-
"search:docs:read",
|
|
182
|
-
"sheets:spreadsheet.meta:read",
|
|
183
|
-
"sheets:spreadsheet.meta:write_only",
|
|
184
|
-
"sheets:spreadsheet:create",
|
|
185
|
-
"sheets:spreadsheet:read",
|
|
186
|
-
"sheets:spreadsheet:write_only",
|
|
187
|
-
"space:document:retrieve",
|
|
188
|
-
"task:task:read",
|
|
189
|
-
"task:task:readonly",
|
|
190
|
-
"task:task:write",
|
|
191
|
-
"task:task:writeonly",
|
|
192
|
-
"task:tasklist:read",
|
|
193
|
-
"wiki:wiki:readonly"
|
|
194
|
-
]
|
|
195
|
-
}
|
|
196
|
-
}
|
|
69
|
+
运行向导,按提示操作即可(约 5 分钟):
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
remote-claude lark init
|
|
197
73
|
```
|
|
198
|
-
7. 企业自建应用页面: `创建版本` -> `发布到线上`
|
|
199
|
-
8. 至此,完成飞书机器人配置
|
|
200
74
|
|
|
201
|
-
|
|
75
|
+
向导会自动完成:扫码创建企业自建应用、开通所需权限、配置事件回调、写入本地配置。
|
|
76
|
+
|
|
77
|
+
> **⚠ 向导最后一步会自动弹出发布页面**,按提示创建版本并发布后才能生效。
|
|
78
|
+
> 未发布的应用在飞书中无法被搜索到。
|
|
79
|
+
|
|
80
|
+
#### 4.2 通过飞书机器人操作 claude/codex
|
|
202
81
|
|
|
203
|
-
1.
|
|
204
|
-
2.
|
|
205
|
-
- `/menu` 展示菜单卡片,后续操作都操作这个卡片上的按钮即可
|
|
82
|
+
1. 从飞书搜索刚创建的机器人(应用发布后才能搜到,发布约需 1 分钟生效)
|
|
83
|
+
2. 飞书中与机器人对话,发送 `/menu` 展示菜单卡片,后续操作点卡片上的按钮即可
|
|
206
84
|
|
|
207
85
|
## 使用指南
|
|
208
86
|
|
package/bin/remote-claude
CHANGED
|
@@ -39,9 +39,4 @@ if [ "$1" = "log" ]; then
|
|
|
39
39
|
exec tail -50f "$LOG_FILE"
|
|
40
40
|
fi
|
|
41
41
|
|
|
42
|
-
# lark 子命令:检查 .env 配置
|
|
43
|
-
if [ "$1" = "lark" ]; then
|
|
44
|
-
source "$INSTALL_DIR/scripts/check-env.sh" "$INSTALL_DIR"
|
|
45
|
-
fi
|
|
46
|
-
|
|
47
42
|
exec uv run --project "$INSTALL_DIR" python3 "$INSTALL_DIR/remote_claude.py" "$@"
|
|
@@ -153,8 +153,9 @@ class LarkHandler:
|
|
|
153
153
|
self._chat_sessions.pop(chat_id, None)
|
|
154
154
|
self._detached_slices.pop(chat_id, None)
|
|
155
155
|
|
|
156
|
-
def on_disconnect():
|
|
157
|
-
asyncio.create_task(self._on_disconnect(chat_id, session_name
|
|
156
|
+
def on_disconnect(disconnected_bridge=None):
|
|
157
|
+
asyncio.create_task(self._on_disconnect(chat_id, session_name,
|
|
158
|
+
disconnected_bridge=disconnected_bridge))
|
|
158
159
|
|
|
159
160
|
bridge = SessionBridge(session_name, on_disconnect=on_disconnect)
|
|
160
161
|
if await bridge.connect():
|
|
@@ -175,8 +176,15 @@ class LarkHandler:
|
|
|
175
176
|
self._chat_sessions.pop(chat_id, None)
|
|
176
177
|
self._poller.stop(chat_id)
|
|
177
178
|
|
|
178
|
-
async def _on_disconnect(self, chat_id: str, session_name: str
|
|
179
|
+
async def _on_disconnect(self, chat_id: str, session_name: str,
|
|
180
|
+
disconnected_bridge=None):
|
|
179
181
|
"""服务端关闭连接时的统一处理"""
|
|
182
|
+
# 防竞态:若当前 bridge 已被 _attach 替换为新实例,跳过清理
|
|
183
|
+
current_bridge = self._bridges.get(chat_id)
|
|
184
|
+
if disconnected_bridge is not None and current_bridge is not disconnected_bridge:
|
|
185
|
+
logger.info(f"会话 '{session_name}' 旧连接断线(已被替换),跳过清理")
|
|
186
|
+
return
|
|
187
|
+
|
|
180
188
|
logger.info(f"会话 '{session_name}' 断线, chat_id={chat_id[:8]}...")
|
|
181
189
|
_track_stats('lark', 'disconnect', session_name=session_name,
|
|
182
190
|
chat_id=chat_id)
|
|
@@ -184,14 +192,12 @@ class LarkHandler:
|
|
|
184
192
|
self._bridges.pop(chat_id, None)
|
|
185
193
|
self._chat_sessions.pop(chat_id, None)
|
|
186
194
|
self._detached_slices.pop(chat_id, None)
|
|
187
|
-
|
|
195
|
+
# 注意:不清除持久化绑定,socket 断连是临时状态,下次操作可自动恢复
|
|
196
|
+
# 只有用户主动 /detach 或 /kill 时才清除绑定
|
|
188
197
|
|
|
189
198
|
if active_slice:
|
|
190
199
|
await self._update_card_disconnected(chat_id, session_name, active_slice)
|
|
191
200
|
|
|
192
|
-
# 会话退出时自动解散绑定到该会话的所有专属群聊
|
|
193
|
-
await self._disband_groups_for_session(session_name, source="disconnect")
|
|
194
|
-
|
|
195
201
|
# ── 消息入口 ────────────────────────────────────────────────────────────
|
|
196
202
|
|
|
197
203
|
async def handle_message(self, user_id: str, chat_id: str, text: str,
|
|
@@ -896,34 +902,33 @@ class LarkHandler:
|
|
|
896
902
|
|
|
897
903
|
# ── 消息转发 ─────────────────────────────────────────────────────────────
|
|
898
904
|
|
|
899
|
-
async def
|
|
900
|
-
"""
|
|
905
|
+
async def _ensure_bridge(self, chat_id: str, user_id: str = None):
|
|
906
|
+
"""获取 bridge,断连时尝试从持久化绑定自动恢复"""
|
|
901
907
|
bridge = self._bridges.get(chat_id)
|
|
908
|
+
if bridge and bridge.running:
|
|
909
|
+
return bridge
|
|
910
|
+
# 尝试从持久化绑定恢复
|
|
911
|
+
saved_session = self._chat_bindings.get(chat_id)
|
|
912
|
+
if saved_session:
|
|
913
|
+
logger.info(f"自动恢复绑定: chat_id={chat_id[:8]}..., session={saved_session}")
|
|
914
|
+
ok = await self._attach(chat_id, saved_session, user_id=user_id)
|
|
915
|
+
if ok:
|
|
916
|
+
return self._bridges.get(chat_id)
|
|
917
|
+
# 恢复失败:会话已不存在,清除绑定
|
|
918
|
+
self._group_chat_ids.discard(chat_id)
|
|
919
|
+
self._save_group_chat_ids()
|
|
920
|
+
self._remove_binding_by_chat(chat_id, force=True)
|
|
921
|
+
await self._disband_groups_for_session(saved_session, source="lazy")
|
|
922
|
+
return None
|
|
902
923
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
if saved_session:
|
|
907
|
-
logger.info(f"自动恢复绑定: chat_id={chat_id[:8]}..., session={saved_session}")
|
|
908
|
-
ok = await self._attach(chat_id, saved_session, user_id=user_id)
|
|
909
|
-
if not ok:
|
|
910
|
-
self._group_chat_ids.discard(chat_id)
|
|
911
|
-
self._save_group_chat_ids()
|
|
912
|
-
self._remove_binding_by_chat(chat_id, force=True)
|
|
913
|
-
await card_service.send_text(
|
|
914
|
-
chat_id, f"会话 '{saved_session}' 已不存在,请重新 /attach"
|
|
915
|
-
)
|
|
916
|
-
# 会话已不存在,解散绑定到该会话的所有专属群聊
|
|
917
|
-
await self._disband_groups_for_session(saved_session, source="lazy")
|
|
918
|
-
return
|
|
919
|
-
bridge = self._bridges.get(chat_id)
|
|
920
|
-
else:
|
|
921
|
-
await card_service.send_text(
|
|
922
|
-
chat_id, "未连接到任何会话,请先使用 /attach <会话名> 连接"
|
|
923
|
-
)
|
|
924
|
-
return
|
|
924
|
+
async def _forward_to_claude(self, user_id: str, chat_id: str, text: str):
|
|
925
|
+
"""转发消息给 Claude(输出由 SharedMemoryPoller 自动推卡片)"""
|
|
926
|
+
bridge = await self._ensure_bridge(chat_id, user_id=user_id)
|
|
925
927
|
|
|
926
928
|
if not bridge:
|
|
929
|
+
await card_service.send_text(
|
|
930
|
+
chat_id, "未连接到任何会话,请先使用 /attach <会话名> 连接"
|
|
931
|
+
)
|
|
927
932
|
return
|
|
928
933
|
|
|
929
934
|
success = await bridge.send_input(text)
|
|
@@ -945,8 +950,8 @@ class LarkHandler:
|
|
|
945
950
|
session_name=self._chat_sessions.get(chat_id, ''),
|
|
946
951
|
chat_id=chat_id, detail=option_value)
|
|
947
952
|
|
|
948
|
-
bridge = self.
|
|
949
|
-
if not bridge
|
|
953
|
+
bridge = await self._ensure_bridge(chat_id, user_id=user_id)
|
|
954
|
+
if not bridge:
|
|
950
955
|
await card_service.send_text(chat_id, "未连接到任何会话,请先使用 /attach <会话名> 连接")
|
|
951
956
|
return
|
|
952
957
|
|
|
@@ -1163,9 +1168,9 @@ class LarkHandler:
|
|
|
1163
1168
|
logger.warning(f"未知快捷键: {key_name}")
|
|
1164
1169
|
return
|
|
1165
1170
|
|
|
1166
|
-
bridge = self.
|
|
1167
|
-
if not bridge
|
|
1168
|
-
logger.warning(f"send_raw_key: chat_id={chat_id[:8]}...
|
|
1171
|
+
bridge = await self._ensure_bridge(chat_id, user_id=user_id)
|
|
1172
|
+
if not bridge:
|
|
1173
|
+
logger.warning(f"send_raw_key: chat_id={chat_id[:8]}... 未连接会话(自动恢复失败)")
|
|
1169
1174
|
return
|
|
1170
1175
|
|
|
1171
1176
|
success = await bridge.send_raw(raw)
|
|
@@ -183,12 +183,12 @@ class SessionBridge:
|
|
|
183
183
|
except asyncio.CancelledError:
|
|
184
184
|
break
|
|
185
185
|
except Exception as e:
|
|
186
|
-
logger.
|
|
186
|
+
logger.warning(f"读取错误: {e}")
|
|
187
187
|
break
|
|
188
188
|
|
|
189
189
|
if self.on_disconnect and not self._manually_disconnected:
|
|
190
190
|
try:
|
|
191
|
-
self.on_disconnect()
|
|
191
|
+
self.on_disconnect(self) # 传递 bridge 实例,供上层验证身份
|
|
192
192
|
except Exception as e:
|
|
193
193
|
logger.error(f"on_disconnect 回调异常: {e}")
|
|
194
194
|
|