remote-claude 1.0.6 → 1.0.8
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/bin/cdx +0 -3
- package/bin/cl +0 -3
- package/bin/cla +0 -3
- package/bin/cx +0 -3
- package/lark_client/lark_handler.py +96 -29
- package/lark_client/session_bridge.py +3 -1
- package/lark_client/shared_memory_poller.py +4 -2
- package/package.json +1 -1
- package/remote_claude.py +43 -5
- package/utils/session.py +41 -0
package/bin/cdx
CHANGED
|
@@ -13,9 +13,6 @@ 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
|
-
# 检查飞书配置
|
|
17
|
-
source "$SCRIPT_DIR/scripts/check-env.sh" "$SCRIPT_DIR"
|
|
18
|
-
|
|
19
16
|
# 会话名:第一个参数非 - 开头时作为自定义名,否则用 PWD_时间戳
|
|
20
17
|
if [ -n "$1" ] && [[ "$1" != -* ]]; then
|
|
21
18
|
SESSION_NAME="$1"
|
package/bin/cl
CHANGED
|
@@ -13,9 +13,6 @@ 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
|
-
# 检查飞书配置
|
|
17
|
-
source "$SCRIPT_DIR/scripts/check-env.sh" "$SCRIPT_DIR"
|
|
18
|
-
|
|
19
16
|
# 会话名:第一个参数非 - 开头时作为自定义名,否则用 PWD_时间戳
|
|
20
17
|
if [ -n "$1" ] && [[ "$1" != -* ]]; then
|
|
21
18
|
SESSION_NAME="$1"
|
package/bin/cla
CHANGED
|
@@ -13,9 +13,6 @@ 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
|
-
# 检查飞书配置
|
|
17
|
-
source "$SCRIPT_DIR/scripts/check-env.sh" "$SCRIPT_DIR"
|
|
18
|
-
|
|
19
16
|
# 会话名:第一个参数非 - 开头时作为自定义名,否则用 PWD_时间戳
|
|
20
17
|
if [ -n "$1" ] && [[ "$1" != -* ]]; then
|
|
21
18
|
SESSION_NAME="$1"
|
package/bin/cx
CHANGED
|
@@ -13,9 +13,6 @@ 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
|
-
# 检查飞书配置
|
|
17
|
-
source "$SCRIPT_DIR/scripts/check-env.sh" "$SCRIPT_DIR"
|
|
18
|
-
|
|
19
16
|
# 会话名:第一个参数非 - 开头时作为自定义名,否则用 PWD_时间戳
|
|
20
17
|
if [ -n "$1" ] && [[ "$1" != -* ]]; then
|
|
21
18
|
SESSION_NAME="$1"
|
|
@@ -87,42 +87,61 @@ class LarkHandler:
|
|
|
87
87
|
self._detached_slices: Dict[str, CardSlice] = {}
|
|
88
88
|
# 正在启动中的会话名集合(防止并发点击触发竞态)
|
|
89
89
|
self._starting_sessions: set = set()
|
|
90
|
+
logger.info(
|
|
91
|
+
f"LarkHandler 初始化: bindings={len(self._chat_bindings)}, "
|
|
92
|
+
f"groups={len(self._group_chat_ids)}, "
|
|
93
|
+
f"binding_details={{{', '.join(f'{k[:8]}...→{v}' for k, v in self._chat_bindings.items())}}}"
|
|
94
|
+
)
|
|
90
95
|
|
|
91
96
|
# ── 持久化绑定 ──────────────────────────────────────────────────────────
|
|
92
97
|
|
|
93
|
-
def
|
|
98
|
+
def _load_json_file(self, path: Path):
|
|
99
|
+
"""原子读取 JSON 文件,损坏时尝试从 .tmp 备份恢复"""
|
|
94
100
|
try:
|
|
95
|
-
if
|
|
96
|
-
|
|
101
|
+
if path.exists():
|
|
102
|
+
content = path.read_text()
|
|
103
|
+
if content.strip():
|
|
104
|
+
return json.loads(content)
|
|
105
|
+
except json.JSONDecodeError as e:
|
|
106
|
+
logger.warning(f"文件 {path.name} JSON 损坏: {e},尝试恢复备份")
|
|
107
|
+
tmp_path = path.with_suffix('.json.tmp')
|
|
108
|
+
if tmp_path.exists():
|
|
109
|
+
try:
|
|
110
|
+
content = tmp_path.read_text()
|
|
111
|
+
if content.strip():
|
|
112
|
+
result = json.loads(content)
|
|
113
|
+
logger.warning(f"从 {tmp_path.name} 恢复成功")
|
|
114
|
+
return result
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
97
117
|
except Exception:
|
|
98
118
|
pass
|
|
99
|
-
return
|
|
119
|
+
return None
|
|
100
120
|
|
|
101
|
-
def
|
|
121
|
+
def _atomic_write_json(self, path: Path, data):
|
|
122
|
+
"""原子写入 JSON 文件:先写 .tmp 再 rename"""
|
|
102
123
|
try:
|
|
103
124
|
ensure_user_data_dir()
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
)
|
|
125
|
+
text = json.dumps(data, ensure_ascii=False)
|
|
126
|
+
tmp_path = path.with_suffix('.json.tmp')
|
|
127
|
+
tmp_path.write_text(text)
|
|
128
|
+
tmp_path.rename(path)
|
|
107
129
|
except Exception as e:
|
|
108
|
-
logger.warning(f"
|
|
130
|
+
logger.warning(f"保存 {path.name} 失败: {e}")
|
|
131
|
+
|
|
132
|
+
def _load_chat_bindings(self) -> Dict[str, str]:
|
|
133
|
+
result = self._load_json_file(self._CHAT_BINDINGS_FILE)
|
|
134
|
+
return result if result is not None else {}
|
|
135
|
+
|
|
136
|
+
def _save_chat_bindings(self):
|
|
137
|
+
self._atomic_write_json(self._CHAT_BINDINGS_FILE, self._chat_bindings)
|
|
109
138
|
|
|
110
139
|
def _load_group_chat_ids(self) -> set:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return set(json.loads(self._LARK_GROUP_IDS_FILE.read_text()))
|
|
114
|
-
except Exception:
|
|
115
|
-
pass
|
|
116
|
-
return set()
|
|
140
|
+
result = self._load_json_file(self._LARK_GROUP_IDS_FILE)
|
|
141
|
+
return set(result) if result is not None else set()
|
|
117
142
|
|
|
118
143
|
def _save_group_chat_ids(self):
|
|
119
|
-
|
|
120
|
-
ensure_user_data_dir()
|
|
121
|
-
self._LARK_GROUP_IDS_FILE.write_text(
|
|
122
|
-
json.dumps(list(self._group_chat_ids), ensure_ascii=False)
|
|
123
|
-
)
|
|
124
|
-
except Exception as e:
|
|
125
|
-
logger.warning(f"保存群聊 ID 失败: {e}")
|
|
144
|
+
self._atomic_write_json(self._LARK_GROUP_IDS_FILE, list(self._group_chat_ids))
|
|
126
145
|
|
|
127
146
|
def _remove_binding_by_chat(self, chat_id: str, force: bool = False):
|
|
128
147
|
"""移除 chat_id 的绑定。
|
|
@@ -131,6 +150,9 @@ class LarkHandler:
|
|
|
131
150
|
"""
|
|
132
151
|
if not force and chat_id in self._group_chat_ids:
|
|
133
152
|
return
|
|
153
|
+
session = self._chat_bindings.get(chat_id)
|
|
154
|
+
if session is not None:
|
|
155
|
+
logger.info(f"移除绑定: chat_id={chat_id[:8]}..., session={session}, force={force}")
|
|
134
156
|
self._chat_bindings.pop(chat_id, None)
|
|
135
157
|
self._save_chat_bindings()
|
|
136
158
|
|
|
@@ -139,6 +161,11 @@ class LarkHandler:
|
|
|
139
161
|
async def _attach(self, chat_id: str, session_name: str,
|
|
140
162
|
user_id: Optional[str] = None) -> bool:
|
|
141
163
|
"""统一 attach 逻辑(私聊/群聊共用)"""
|
|
164
|
+
logger.info(
|
|
165
|
+
f"[ATTACH] 开始: chat_id={chat_id[:8]}..., session={session_name}, "
|
|
166
|
+
f"old_session={self._chat_sessions.get(chat_id)}, "
|
|
167
|
+
f"old_bridge={'有' if chat_id in self._bridges else '无'}"
|
|
168
|
+
)
|
|
142
169
|
# 在断开旧连接之前,先更新旧流式卡片为已断开状态
|
|
143
170
|
old_session = self._chat_sessions.get(chat_id)
|
|
144
171
|
old_slice = self._poller.stop_and_get_active_slice(chat_id)
|
|
@@ -165,11 +192,15 @@ class LarkHandler:
|
|
|
165
192
|
notify_user_id=user_id)
|
|
166
193
|
_track_stats('lark', 'attach', session_name=session_name,
|
|
167
194
|
chat_id=chat_id)
|
|
195
|
+
logger.info(f"[ATTACH] 成功: chat_id={chat_id[:8]}..., session={session_name}")
|
|
168
196
|
return True
|
|
197
|
+
logger.warning(f"[ATTACH] 失败: chat_id={chat_id[:8]}..., session={session_name}")
|
|
169
198
|
return False
|
|
170
199
|
|
|
171
200
|
async def _detach(self, chat_id: str):
|
|
172
201
|
"""统一 detach 逻辑(私聊/群聊共用)"""
|
|
202
|
+
session_name = self._chat_sessions.get(chat_id, "未知")
|
|
203
|
+
logger.info(f"[DETACH] chat_id={chat_id[:8]}..., session={session_name}")
|
|
173
204
|
bridge = self._bridges.pop(chat_id, None)
|
|
174
205
|
if bridge:
|
|
175
206
|
await bridge.disconnect()
|
|
@@ -185,7 +216,12 @@ class LarkHandler:
|
|
|
185
216
|
logger.info(f"会话 '{session_name}' 旧连接断线(已被替换),跳过清理")
|
|
186
217
|
return
|
|
187
218
|
|
|
188
|
-
|
|
219
|
+
bridge_running = disconnected_bridge.running if disconnected_bridge else "N/A"
|
|
220
|
+
logger.info(f"会话 '{session_name}' 断线, chat_id={chat_id[:8]}..., bridge_running={bridge_running}")
|
|
221
|
+
logger.info(
|
|
222
|
+
f"[DISCONNECT] 状态快照: bridges={[k[:8] for k in self._bridges]}, "
|
|
223
|
+
f"sessions={{{', '.join(f'{k[:8]}→{v}' for k, v in self._chat_sessions.items())}}}"
|
|
224
|
+
)
|
|
189
225
|
_track_stats('lark', 'disconnect', session_name=session_name,
|
|
190
226
|
chat_id=chat_id)
|
|
191
227
|
active_slice = self._poller.stop_and_get_active_slice(chat_id)
|
|
@@ -196,7 +232,12 @@ class LarkHandler:
|
|
|
196
232
|
# 只有用户主动 /detach 或 /kill 时才清除绑定
|
|
197
233
|
|
|
198
234
|
if active_slice:
|
|
199
|
-
await self._update_card_disconnected(chat_id, session_name, active_slice)
|
|
235
|
+
updated = await self._update_card_disconnected(chat_id, session_name, active_slice)
|
|
236
|
+
if not updated:
|
|
237
|
+
# 就地更新失败 → 降级发新卡片,确保用户看到断开状态
|
|
238
|
+
logger.warning(f"断线卡片就地更新失败,降级发送新卡: {chat_id[:8]}...")
|
|
239
|
+
card = build_stream_card([], disconnected=True, session_name=session_name)
|
|
240
|
+
await card_service.create_and_send_card(chat_id, card)
|
|
200
241
|
|
|
201
242
|
# ── 消息入口 ────────────────────────────────────────────────────────────
|
|
202
243
|
|
|
@@ -280,6 +321,7 @@ class LarkHandler:
|
|
|
280
321
|
|
|
281
322
|
ok = await self._attach(chat_id, session_name, user_id=user_id)
|
|
282
323
|
if ok:
|
|
324
|
+
logger.info(f"[BINDING] 写入: chat_id={chat_id[:8]}..., session={session_name}, source=cmd_attach")
|
|
283
325
|
self._chat_bindings[chat_id] = session_name
|
|
284
326
|
self._save_chat_bindings()
|
|
285
327
|
if message_id:
|
|
@@ -406,6 +448,7 @@ class LarkHandler:
|
|
|
406
448
|
|
|
407
449
|
ok = await self._attach(chat_id, session_name, user_id=user_id)
|
|
408
450
|
if ok:
|
|
451
|
+
logger.info(f"[BINDING] 写入: chat_id={chat_id[:8]}..., session={session_name}, source=cmd_start")
|
|
409
452
|
self._chat_bindings[chat_id] = session_name
|
|
410
453
|
self._save_chat_bindings()
|
|
411
454
|
else:
|
|
@@ -520,6 +563,7 @@ class LarkHandler:
|
|
|
520
563
|
|
|
521
564
|
# 清理所有残留绑定(包括已断开的群聊,其绑定在断开时被保留)
|
|
522
565
|
for cid in [c for c, s in list(self._chat_bindings.items()) if s == session_name]:
|
|
566
|
+
logger.info(f"[KILL] 清理残留绑定: chat_id={cid[:8]}..., session={session_name}, is_group={'是' if cid in self._group_chat_ids else '否'}")
|
|
523
567
|
self._group_chat_ids.discard(cid)
|
|
524
568
|
self._chat_bindings.pop(cid, None)
|
|
525
569
|
self._save_chat_bindings()
|
|
@@ -547,7 +591,7 @@ class LarkHandler:
|
|
|
547
591
|
|
|
548
592
|
async def _update_card_disconnected(self, chat_id: str, session_name: str,
|
|
549
593
|
active_slice: 'CardSlice') -> bool:
|
|
550
|
-
"""读取最新 blocks 并就地更新卡片为断开状态(disconnected=True
|
|
594
|
+
"""读取最新 blocks 并就地更新卡片为断开状态(disconnected=True)。返回是否成功。"""
|
|
551
595
|
blocks = []
|
|
552
596
|
try:
|
|
553
597
|
import sys as _sys
|
|
@@ -795,6 +839,7 @@ class LarkHandler:
|
|
|
795
839
|
self._save_chat_bindings()
|
|
796
840
|
self._group_chat_ids.add(group_chat_id)
|
|
797
841
|
self._save_group_chat_ids()
|
|
842
|
+
logger.info(f"创建群聊成功: group_chat_id={group_chat_id[:8]}..., session={session_name}")
|
|
798
843
|
# 立即 attach,让新群即刻开始接收 Claude 输出
|
|
799
844
|
await self._attach(group_chat_id, session_name, user_id=user_id)
|
|
800
845
|
|
|
@@ -885,6 +930,7 @@ class LarkHandler:
|
|
|
885
930
|
logger.error(f"解散群 API 失败: {feishu_msg}")
|
|
886
931
|
|
|
887
932
|
# 无论 Feishu delete 是否成功,都清理本地绑定
|
|
933
|
+
logger.info(f"[DISBAND] 单群解散: group_chat_id={group_chat_id[:8]}..., session={session_name}")
|
|
888
934
|
self._group_chat_ids.discard(group_chat_id)
|
|
889
935
|
self._save_group_chat_ids()
|
|
890
936
|
self._remove_binding_by_chat(group_chat_id, force=True)
|
|
@@ -907,6 +953,8 @@ class LarkHandler:
|
|
|
907
953
|
bridge = self._bridges.get(chat_id)
|
|
908
954
|
if bridge and bridge.running:
|
|
909
955
|
return bridge
|
|
956
|
+
if bridge and not bridge.running:
|
|
957
|
+
logger.info(f"bridge 存在但已停止: chat_id={chat_id[:8]}..., session={self._chat_sessions.get(chat_id)}")
|
|
910
958
|
# 尝试从持久化绑定恢复
|
|
911
959
|
saved_session = self._chat_bindings.get(chat_id)
|
|
912
960
|
if saved_session:
|
|
@@ -914,11 +962,28 @@ class LarkHandler:
|
|
|
914
962
|
ok = await self._attach(chat_id, saved_session, user_id=user_id)
|
|
915
963
|
if ok:
|
|
916
964
|
return self._bridges.get(chat_id)
|
|
917
|
-
#
|
|
965
|
+
# 区分临时失败和永久失败
|
|
966
|
+
sessions = list_active_sessions()
|
|
967
|
+
if any(s["name"] == saved_session for s in sessions):
|
|
968
|
+
# 会话存在但连接失败 → 临时性故障,保留绑定供重试
|
|
969
|
+
logger.warning(f"自动恢复失败(临时性): session={saved_session}, 保留绑定")
|
|
970
|
+
return None
|
|
971
|
+
# 会话已不存在 → 永久失败,级联清除
|
|
972
|
+
logger.warning(f"会话 '{saved_session}' 已不存在,清除绑定: chat_id={chat_id[:8]}...")
|
|
973
|
+
# 1. 先停止当前 chat_id 的轮询器和 bridge
|
|
974
|
+
self._poller.stop(chat_id)
|
|
975
|
+
stale_bridge = self._bridges.pop(chat_id, None)
|
|
976
|
+
if stale_bridge:
|
|
977
|
+
await stale_bridge.disconnect()
|
|
978
|
+
self._chat_sessions.pop(chat_id, None)
|
|
979
|
+
self._detached_slices.pop(chat_id, None)
|
|
980
|
+
# 2. 清除绑定
|
|
918
981
|
self._group_chat_ids.discard(chat_id)
|
|
919
|
-
self._save_group_chat_ids()
|
|
920
982
|
self._remove_binding_by_chat(chat_id, force=True)
|
|
983
|
+
# 3. 解散同会话的其他群聊(内部会清理各自的轮询器/bridge/绑定)
|
|
921
984
|
await self._disband_groups_for_session(saved_session, source="lazy")
|
|
985
|
+
# 4. 确保持久化保存(disband 按绑定匹配遍历,已在上面移除绑定,确保保存)
|
|
986
|
+
self._save_group_chat_ids()
|
|
922
987
|
return None
|
|
923
988
|
|
|
924
989
|
async def _forward_to_claude(self, user_id: str, chat_id: str, text: str):
|
|
@@ -1139,14 +1204,15 @@ class LarkHandler:
|
|
|
1139
1204
|
await card_service.send_text(chat_id, f"❌ 无法解析按键:`{combo}`\n使用 `/press` 查看支持的按键格式")
|
|
1140
1205
|
return
|
|
1141
1206
|
|
|
1142
|
-
bridge = self.
|
|
1143
|
-
if not bridge
|
|
1207
|
+
bridge = await self._ensure_bridge(chat_id, user_id=user_id)
|
|
1208
|
+
if not bridge:
|
|
1144
1209
|
await card_service.send_text(chat_id, "❌ 当前未连接到会话,请先使用 /attach 连接")
|
|
1145
1210
|
return
|
|
1146
1211
|
|
|
1147
1212
|
success = await bridge.send_raw(raw)
|
|
1148
1213
|
if success:
|
|
1149
1214
|
logger.info(f"[press] 发送按键 {combo!r} ({raw!r}) 到会话")
|
|
1215
|
+
self._poller.kick(chat_id)
|
|
1150
1216
|
else:
|
|
1151
1217
|
await card_service.send_text(chat_id, f"❌ 发送按键 `{combo}` 失败")
|
|
1152
1218
|
|
|
@@ -1241,6 +1307,7 @@ class LarkHandler:
|
|
|
1241
1307
|
|
|
1242
1308
|
async def disconnect_all_for_shutdown(self) -> None:
|
|
1243
1309
|
"""lark stop 时清理所有活跃流式卡片(更新为已断开状态)"""
|
|
1310
|
+
logger.info(f"[SHUTDOWN] 开始清理: bridges={len(self._bridges)}, sessions={len(self._chat_sessions)}")
|
|
1244
1311
|
chat_ids = list(self._bridges.keys())
|
|
1245
1312
|
for chat_id in chat_ids:
|
|
1246
1313
|
session_name = self._chat_sessions.get(chat_id, "")
|
|
@@ -99,6 +99,7 @@ class SessionBridge:
|
|
|
99
99
|
|
|
100
100
|
async def disconnect(self):
|
|
101
101
|
"""断开连接"""
|
|
102
|
+
logger.info(f"主动断开: session={self.session_name}, running={self.running}")
|
|
102
103
|
self._manually_disconnected = True
|
|
103
104
|
self.running = False
|
|
104
105
|
if self._read_task:
|
|
@@ -173,6 +174,7 @@ class SessionBridge:
|
|
|
173
174
|
try:
|
|
174
175
|
msg = await asyncio.wait_for(self._read_message(), timeout=1.0)
|
|
175
176
|
if msg is None:
|
|
177
|
+
logger.info(f"read_loop 收到 EOF: session={self.session_name}")
|
|
176
178
|
self.running = False
|
|
177
179
|
break
|
|
178
180
|
if msg.type == MessageType.INPUT and self.on_input:
|
|
@@ -183,7 +185,7 @@ class SessionBridge:
|
|
|
183
185
|
except asyncio.CancelledError:
|
|
184
186
|
break
|
|
185
187
|
except Exception as e:
|
|
186
|
-
logger.warning(f"读取错误: {e}")
|
|
188
|
+
logger.warning(f"读取错误: session={self.session_name}, error={e}")
|
|
187
189
|
break
|
|
188
190
|
|
|
189
191
|
if self.on_disconnect and not self._manually_disconnected:
|
|
@@ -113,12 +113,13 @@ class SharedMemoryPoller:
|
|
|
113
113
|
self._rapid_until.pop(chat_id, None)
|
|
114
114
|
|
|
115
115
|
tracker = self._trackers.pop(chat_id, None)
|
|
116
|
+
session_name = tracker.session_name if tracker else "N/A"
|
|
116
117
|
if tracker and tracker.reader:
|
|
117
118
|
try:
|
|
118
119
|
tracker.reader.close()
|
|
119
120
|
except Exception:
|
|
120
121
|
pass
|
|
121
|
-
logger.info(f"轮询器停止: chat_id={chat_id[:8]}
|
|
122
|
+
logger.info(f"轮询器停止: chat_id={chat_id[:8]}..., session={session_name}")
|
|
122
123
|
|
|
123
124
|
def stop_and_get_active_slice(self, chat_id: str) -> Optional['CardSlice']:
|
|
124
125
|
"""停止轮询并返回活跃(未冻结)CardSlice,原子操作。供 detach/disconnect 就地更新卡片使用。"""
|
|
@@ -143,7 +144,8 @@ class SharedMemoryPoller:
|
|
|
143
144
|
except Exception:
|
|
144
145
|
pass
|
|
145
146
|
|
|
146
|
-
|
|
147
|
+
session_name = tracker.session_name if tracker else "N/A"
|
|
148
|
+
logger.info(f"轮询器停止(含活跃切片): chat_id={chat_id[:8]}..., session={session_name}, active={'有' if active else '无'}")
|
|
147
149
|
return active
|
|
148
150
|
|
|
149
151
|
def _on_task_done(self, task: asyncio.Task, chat_id: str) -> None:
|
package/package.json
CHANGED
package/remote_claude.py
CHANGED
|
@@ -34,7 +34,7 @@ from utils.session import (
|
|
|
34
34
|
tmux_kill_session,
|
|
35
35
|
list_active_sessions, is_session_active, cleanup_session,
|
|
36
36
|
is_lark_running, get_lark_pid, get_lark_status, get_lark_pid_file,
|
|
37
|
-
save_lark_status, cleanup_lark,
|
|
37
|
+
save_lark_status, cleanup_lark, kill_all_lark_processes,
|
|
38
38
|
USER_DATA_DIR, ensure_user_data_dir, get_lark_log_file,
|
|
39
39
|
get_env_snapshot_path,
|
|
40
40
|
)
|
|
@@ -214,10 +214,28 @@ def cmd_list(args):
|
|
|
214
214
|
return 0
|
|
215
215
|
|
|
216
216
|
|
|
217
|
+
def _find_session_by_pid(pid: int):
|
|
218
|
+
"""通过 PID 查找对应会话名"""
|
|
219
|
+
for session in list_active_sessions():
|
|
220
|
+
if session.get("pid") == pid:
|
|
221
|
+
return session["name"]
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
|
|
217
225
|
def cmd_kill(args):
|
|
218
226
|
"""终止会话"""
|
|
219
227
|
session_name = args.name
|
|
220
228
|
|
|
229
|
+
# 若参数是纯数字,按 PID 反查会话名
|
|
230
|
+
if session_name.isdigit():
|
|
231
|
+
pid = int(session_name)
|
|
232
|
+
matched = _find_session_by_pid(pid)
|
|
233
|
+
if not matched:
|
|
234
|
+
print(f"错误: 未找到 PID={pid} 对应的会话")
|
|
235
|
+
return 1
|
|
236
|
+
session_name = matched
|
|
237
|
+
print(f"PID {pid} 对应会话: {session_name}")
|
|
238
|
+
|
|
221
239
|
# 检查会话是否存在
|
|
222
240
|
if not is_session_active(session_name) and not tmux_session_exists(session_name):
|
|
223
241
|
print(f"错误: 会话 '{session_name}' 不存在")
|
|
@@ -307,6 +325,11 @@ def cmd_lark_start(args):
|
|
|
307
325
|
ensure_socket_dir()
|
|
308
326
|
ensure_user_data_dir()
|
|
309
327
|
|
|
328
|
+
# 启动前清理残留进程
|
|
329
|
+
stale = kill_all_lark_processes()
|
|
330
|
+
if stale:
|
|
331
|
+
print(f" 清理了 {len(stale)} 个残留进程: {stale}")
|
|
332
|
+
|
|
310
333
|
# 启动守护进程(使用 -m 模块方式运行,确保相对导入正常工作)
|
|
311
334
|
log_file = get_lark_log_file()
|
|
312
335
|
|
|
@@ -364,8 +387,11 @@ def cmd_lark_stop(args):
|
|
|
364
387
|
print(f"正在停止飞书客户端 (PID: {pid})...")
|
|
365
388
|
|
|
366
389
|
try:
|
|
367
|
-
#
|
|
368
|
-
|
|
390
|
+
# 杀整个进程组(uv 父进程 + Python 子进程一起杀)
|
|
391
|
+
try:
|
|
392
|
+
os.killpg(os.getpgid(pid), signal.SIGTERM)
|
|
393
|
+
except (ProcessLookupError, PermissionError):
|
|
394
|
+
os.kill(pid, signal.SIGTERM) # 降级
|
|
369
395
|
|
|
370
396
|
# 等待进程退出
|
|
371
397
|
for i in range(50): # 最多等待 5 秒
|
|
@@ -375,9 +401,17 @@ def cmd_lark_stop(args):
|
|
|
375
401
|
else:
|
|
376
402
|
# 如果还没退出,强制终止
|
|
377
403
|
print("进程未响应,强制终止...")
|
|
378
|
-
|
|
404
|
+
try:
|
|
405
|
+
os.killpg(os.getpgid(pid), signal.SIGKILL)
|
|
406
|
+
except (ProcessLookupError, PermissionError):
|
|
407
|
+
os.kill(pid, signal.SIGKILL)
|
|
379
408
|
time.sleep(0.5)
|
|
380
409
|
|
|
410
|
+
# 清理所有残留 lark_client 进程
|
|
411
|
+
stale = kill_all_lark_processes()
|
|
412
|
+
if stale:
|
|
413
|
+
print(f" 清理了 {len(stale)} 个残留进程: {stale}")
|
|
414
|
+
|
|
381
415
|
if not is_lark_running():
|
|
382
416
|
print("✓ 飞书客户端已停止")
|
|
383
417
|
cleanup_lark()
|
|
@@ -389,6 +423,10 @@ def cmd_lark_stop(args):
|
|
|
389
423
|
return 1
|
|
390
424
|
|
|
391
425
|
except ProcessLookupError:
|
|
426
|
+
# 清理所有残留 lark_client 进程
|
|
427
|
+
stale = kill_all_lark_processes()
|
|
428
|
+
if stale:
|
|
429
|
+
print(f" 清理了 {len(stale)} 个残留进程: {stale}")
|
|
392
430
|
print("进程已不存在,清理残留文件")
|
|
393
431
|
cleanup_lark()
|
|
394
432
|
_stop_watchdog()
|
|
@@ -817,7 +855,7 @@ def main():
|
|
|
817
855
|
|
|
818
856
|
# kill 命令
|
|
819
857
|
kill_parser = subparsers.add_parser("kill", help="终止会话")
|
|
820
|
-
kill_parser.add_argument("name", help="
|
|
858
|
+
kill_parser.add_argument("name", help="会话名称或 PID")
|
|
821
859
|
kill_parser.set_defaults(func=cmd_kill)
|
|
822
860
|
|
|
823
861
|
# status 命令
|
package/utils/session.py
CHANGED
|
@@ -519,6 +519,47 @@ def save_lark_status(pid: int):
|
|
|
519
519
|
status_file.write_text(json.dumps(status_data))
|
|
520
520
|
|
|
521
521
|
|
|
522
|
+
def kill_all_lark_processes(exclude_pid: int = None) -> list:
|
|
523
|
+
"""查找并杀死所有 lark_client 相关进程(排除指定 PID)
|
|
524
|
+
|
|
525
|
+
返回被杀死的 PID 列表
|
|
526
|
+
"""
|
|
527
|
+
import signal
|
|
528
|
+
import time
|
|
529
|
+
killed = []
|
|
530
|
+
|
|
531
|
+
# 查找所有 lark_client.main 相关进程
|
|
532
|
+
try:
|
|
533
|
+
result = subprocess.run(
|
|
534
|
+
["pgrep", "-f", "lark_client.main"],
|
|
535
|
+
capture_output=True, text=True, timeout=5
|
|
536
|
+
)
|
|
537
|
+
pids = [int(p) for p in result.stdout.strip().split('\n') if p.strip()]
|
|
538
|
+
except Exception:
|
|
539
|
+
return killed
|
|
540
|
+
|
|
541
|
+
for pid in pids:
|
|
542
|
+
if pid == exclude_pid or pid == os.getpid():
|
|
543
|
+
continue
|
|
544
|
+
try:
|
|
545
|
+
os.kill(pid, signal.SIGTERM)
|
|
546
|
+
killed.append(pid)
|
|
547
|
+
except (ProcessLookupError, PermissionError):
|
|
548
|
+
pass
|
|
549
|
+
|
|
550
|
+
# 等待退出,必要时 SIGKILL
|
|
551
|
+
if killed:
|
|
552
|
+
time.sleep(1)
|
|
553
|
+
for pid in killed:
|
|
554
|
+
try:
|
|
555
|
+
os.kill(pid, 0) # 检查是否还活着
|
|
556
|
+
os.kill(pid, signal.SIGKILL)
|
|
557
|
+
except (ProcessLookupError, PermissionError):
|
|
558
|
+
pass
|
|
559
|
+
|
|
560
|
+
return killed
|
|
561
|
+
|
|
562
|
+
|
|
522
563
|
def cleanup_lark():
|
|
523
564
|
"""清理飞书客户端残留文件"""
|
|
524
565
|
pid_file = get_lark_pid_file()
|