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 CHANGED
@@ -66,143 +66,21 @@ remote-claude attach <会话名>
66
66
 
67
67
  #### 4.1 配置飞书机器人
68
68
 
69
- 1. 登录[飞书开放平台](https://open.feishu.cn/),创建企业自建应用
70
- 2. 获取 **App ID** 和 **App Secret**
71
- 3. 用`cla`或`cl`启动一次claude(或用cx或cdx启动一次codex), 按照交互提示填入**App ID** 和 **App Secret**
72
- 4. [飞书开放平台]的企业自建应用页面`添加应用能力`(机器人能力)
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
- #### 4.2 通过飞书机器人操作claude/codex
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
- self._remove_binding_by_chat(chat_id)
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 _forward_to_claude(self, user_id: str, chat_id: str, text: str):
900
- """转发消息给 Claude(输出由 SharedMemoryPoller 自动推卡片)"""
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
- if not bridge or not bridge.running:
904
- # 尝试从持久化绑定自动恢复
905
- saved_session = self._chat_bindings.get(chat_id)
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._bridges.get(chat_id)
949
- if not bridge or not bridge.running:
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._bridges.get(chat_id)
1167
- if not bridge or not bridge.running:
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.debug(f"读取错误: {e}")
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