remote-claude 1.0.2 → 1.0.3
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 +4 -0
- package/lark_client/card_builder.py +13 -6
- package/lark_client/config.py +4 -0
- package/lark_client/lark_handler.py +199 -78
- package/lark_client/main.py +20 -2
- package/lark_client/session_bridge.py +4 -10
- package/lark_client/shared_memory_poller.py +65 -4
- package/package.json +1 -1
- package/pyproject.toml +1 -0
- package/server/parsers/claude_parser.py +43 -0
- package/server/parsers/codex_parser.py +8 -0
- package/utils/components.py +1 -0
package/.env.example
CHANGED
|
@@ -427,6 +427,7 @@ def _build_buttons_v2(options: List[Dict[str, str]]) -> List[Dict[str, Any]]:
|
|
|
427
427
|
"value": {
|
|
428
428
|
"action": "select_option",
|
|
429
429
|
"value": opt["value"],
|
|
430
|
+
"needs_input": opt.get("needs_input", False),
|
|
430
431
|
"total": str(total),
|
|
431
432
|
}
|
|
432
433
|
}
|
|
@@ -1022,16 +1023,22 @@ def build_dir_card(target, entries: List[Dict], sessions: List[Dict], tree: bool
|
|
|
1022
1023
|
|
|
1023
1024
|
if is_dir and depth == 0:
|
|
1024
1025
|
auto_session = _dir_session_name(full_path)
|
|
1026
|
+
# 前缀匹配:找 session_groups 中精确匹配或以 auto_session + "_" 开头的条目(取最后一个,即最新的)
|
|
1027
|
+
matched_group_cid = None
|
|
1028
|
+
if session_groups:
|
|
1029
|
+
for sn, cid in session_groups.items():
|
|
1030
|
+
if sn == auto_session or sn.startswith(auto_session + "_"):
|
|
1031
|
+
matched_group_cid = cid
|
|
1025
1032
|
group_btn = {
|
|
1026
1033
|
"tag": "button",
|
|
1027
|
-
"text": {"tag": "plain_text", "content": "进入群聊" if
|
|
1034
|
+
"text": {"tag": "plain_text", "content": "进入群聊" if matched_group_cid else "创建群聊"},
|
|
1028
1035
|
"type": "default",
|
|
1029
1036
|
"behaviors": [{"type": "open_url",
|
|
1030
|
-
"default_url": f"https://applink.feishu.cn/client/chat/open?openChatId={
|
|
1031
|
-
"android_url": f"https://applink.feishu.cn/client/chat/open?openChatId={
|
|
1032
|
-
"ios_url": f"https://applink.feishu.cn/client/chat/open?openChatId={
|
|
1033
|
-
"pc_url": f"https://applink.feishu.cn/client/chat/open?openChatId={
|
|
1034
|
-
if
|
|
1037
|
+
"default_url": f"https://applink.feishu.cn/client/chat/open?openChatId={matched_group_cid}",
|
|
1038
|
+
"android_url": f"https://applink.feishu.cn/client/chat/open?openChatId={matched_group_cid}",
|
|
1039
|
+
"ios_url": f"https://applink.feishu.cn/client/chat/open?openChatId={matched_group_cid}",
|
|
1040
|
+
"pc_url": f"https://applink.feishu.cn/client/chat/open?openChatId={matched_group_cid}"}]
|
|
1041
|
+
if matched_group_cid else
|
|
1035
1042
|
[{"type": "callback", "value": {
|
|
1036
1043
|
"action": "dir_new_group",
|
|
1037
1044
|
"path": full_path,
|
package/lark_client/config.py
CHANGED
|
@@ -11,8 +11,11 @@
|
|
|
11
11
|
import asyncio
|
|
12
12
|
import json
|
|
13
13
|
import logging
|
|
14
|
+
import os as _os
|
|
14
15
|
import subprocess
|
|
15
16
|
import sys
|
|
17
|
+
import time
|
|
18
|
+
from datetime import datetime as _datetime
|
|
16
19
|
from pathlib import Path
|
|
17
20
|
from typing import Optional, Dict, Any, List
|
|
18
21
|
|
|
@@ -30,7 +33,23 @@ from .card_builder import (
|
|
|
30
33
|
from .shared_memory_poller import SharedMemoryPoller, CardSlice
|
|
31
34
|
|
|
32
35
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
33
|
-
from utils.session import list_active_sessions, get_socket_path, get_chat_bindings_file, ensure_user_data_dir
|
|
36
|
+
from utils.session import list_active_sessions, get_socket_path, get_chat_bindings_file, ensure_user_data_dir, USER_DATA_DIR
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _read_log_since(since: '_datetime', log_path: 'Path') -> str:
|
|
40
|
+
"""读取 startup.log 中 since 时间点之后的日志行"""
|
|
41
|
+
if not log_path.exists():
|
|
42
|
+
return ""
|
|
43
|
+
lines = []
|
|
44
|
+
for line in log_path.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
45
|
+
try:
|
|
46
|
+
ts = _datetime.strptime(line[:23], "%Y-%m-%d %H:%M:%S.%f")
|
|
47
|
+
if ts >= since:
|
|
48
|
+
lines.append(line)
|
|
49
|
+
except ValueError:
|
|
50
|
+
if lines:
|
|
51
|
+
lines.append(line)
|
|
52
|
+
return "\n".join(lines)
|
|
34
53
|
|
|
35
54
|
try:
|
|
36
55
|
from stats import track as _track_stats
|
|
@@ -66,6 +85,8 @@ class LarkHandler:
|
|
|
66
85
|
self._group_chat_ids: set = self._load_group_chat_ids()
|
|
67
86
|
# chat_id → CardSlice(用户主动断开后保留,供重连时冻结旧卡片)
|
|
68
87
|
self._detached_slices: Dict[str, CardSlice] = {}
|
|
88
|
+
# 正在启动中的会话名集合(防止并发点击触发竞态)
|
|
89
|
+
self._starting_sessions: set = set()
|
|
69
90
|
|
|
70
91
|
# ── 持久化绑定 ──────────────────────────────────────────────────────────
|
|
71
92
|
|
|
@@ -168,6 +189,9 @@ class LarkHandler:
|
|
|
168
189
|
if active_slice:
|
|
169
190
|
await self._update_card_disconnected(chat_id, session_name, active_slice)
|
|
170
191
|
|
|
192
|
+
# 会话退出时自动解散绑定到该会话的所有专属群聊
|
|
193
|
+
await self._disband_groups_for_session(session_name, source="disconnect")
|
|
194
|
+
|
|
171
195
|
# ── 消息入口 ────────────────────────────────────────────────────────────
|
|
172
196
|
|
|
173
197
|
async def handle_message(self, user_id: str, chat_id: str, text: str,
|
|
@@ -316,9 +340,14 @@ class LarkHandler:
|
|
|
316
340
|
)
|
|
317
341
|
return
|
|
318
342
|
|
|
343
|
+
if session_name in self._starting_sessions:
|
|
344
|
+
await card_service.send_text(chat_id, f"会话 '{session_name}' 正在启动中,请稍候")
|
|
345
|
+
return
|
|
346
|
+
self._starting_sessions.add(session_name)
|
|
347
|
+
|
|
319
348
|
script_dir = Path(__file__).parent.parent.absolute()
|
|
320
349
|
server_script = script_dir / "server" / "server.py"
|
|
321
|
-
cmd = [
|
|
350
|
+
cmd = ["uv", "run", "--project", str(script_dir), "python3", str(server_script), session_name]
|
|
322
351
|
if self._poller.get_bypass_enabled():
|
|
323
352
|
cmd += ["--", "--dangerously-skip-permissions", "--permission-mode=dontAsk"]
|
|
324
353
|
|
|
@@ -326,37 +355,21 @@ class LarkHandler:
|
|
|
326
355
|
_track_stats('lark', 'cmd_start', session_name=session_name, chat_id=chat_id)
|
|
327
356
|
|
|
328
357
|
try:
|
|
329
|
-
import os as _os
|
|
330
|
-
from datetime import datetime as _datetime
|
|
331
358
|
env = _os.environ.copy()
|
|
332
359
|
env.pop("CLAUDECODE", None)
|
|
333
360
|
|
|
334
|
-
from utils.session import USER_DATA_DIR
|
|
335
361
|
log_path = USER_DATA_DIR / "startup.log"
|
|
336
362
|
start_time = _datetime.now()
|
|
337
363
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
def _read_log_since(since):
|
|
348
|
-
if not log_path.exists():
|
|
349
|
-
return ""
|
|
350
|
-
lines = []
|
|
351
|
-
for line in log_path.read_text(encoding="utf-8").splitlines():
|
|
352
|
-
try:
|
|
353
|
-
ts = _datetime.strptime(line[:23], "%Y-%m-%d %H:%M:%S.%f")
|
|
354
|
-
if ts >= since:
|
|
355
|
-
lines.append(line)
|
|
356
|
-
except ValueError:
|
|
357
|
-
if lines:
|
|
358
|
-
lines.append(line)
|
|
359
|
-
return "\n".join(lines)
|
|
364
|
+
with open(log_path, 'a') as stderr_fd:
|
|
365
|
+
proc = subprocess.Popen(
|
|
366
|
+
cmd,
|
|
367
|
+
stdout=subprocess.DEVNULL,
|
|
368
|
+
stderr=stderr_fd,
|
|
369
|
+
start_new_session=True,
|
|
370
|
+
cwd=work_dir,
|
|
371
|
+
env=env,
|
|
372
|
+
)
|
|
360
373
|
|
|
361
374
|
socket_path = get_socket_path(session_name)
|
|
362
375
|
for i in range(120):
|
|
@@ -367,13 +380,13 @@ class LarkHandler:
|
|
|
367
380
|
elapsed = (i + 1) // 10
|
|
368
381
|
rc = proc.poll()
|
|
369
382
|
if rc is not None:
|
|
370
|
-
log_content = _read_log_since(start_time)
|
|
383
|
+
log_content = _read_log_since(start_time, log_path)
|
|
371
384
|
logger.warning(f"会话启动失败: server 进程已退出 (exitcode={rc}, elapsed={elapsed}s)\n{log_content}")
|
|
372
385
|
await card_service.send_text(chat_id, f"错误: Server 进程意外退出 (code={rc})\n\n{log_content}")
|
|
373
386
|
return
|
|
374
387
|
logger.info(f"等待 server socket... ({elapsed}s)")
|
|
375
388
|
else:
|
|
376
|
-
log_content = _read_log_since(start_time)
|
|
389
|
+
log_content = _read_log_since(start_time, log_path)
|
|
377
390
|
logger.error(f"会话启动超时 (12s), session={session_name}\n{log_content}")
|
|
378
391
|
await card_service.send_text(chat_id, f"错误: 会话启动超时 (12s)\n\n{log_content}")
|
|
379
392
|
return
|
|
@@ -391,44 +404,61 @@ class LarkHandler:
|
|
|
391
404
|
except Exception as e:
|
|
392
405
|
logger.error(f"启动会话失败: {e}")
|
|
393
406
|
await card_service.send_text(chat_id, f"错误: 启动失败 - {e}")
|
|
407
|
+
finally:
|
|
408
|
+
self._starting_sessions.discard(session_name)
|
|
394
409
|
|
|
395
410
|
async def _cmd_start_and_new_group(self, user_id: str, chat_id: str,
|
|
396
411
|
session_name: str, path: str):
|
|
397
412
|
"""在指定目录启动会话并创建专属群聊"""
|
|
398
|
-
sessions = list_active_sessions()
|
|
399
|
-
if any(s["name"] == session_name for s in sessions):
|
|
400
|
-
# 会话已存在,直接创建群聊
|
|
401
|
-
await self._cmd_new_group(user_id, chat_id, session_name)
|
|
402
|
-
return
|
|
403
|
-
|
|
404
413
|
work_path = Path(path).expanduser()
|
|
405
414
|
if not work_path.is_dir():
|
|
406
415
|
await card_service.send_text(chat_id, f"错误: 路径无效: {path}")
|
|
407
416
|
return
|
|
408
417
|
|
|
418
|
+
sessions = list_active_sessions()
|
|
419
|
+
active_names = {s["name"] for s in sessions}
|
|
420
|
+
if session_name in active_names or session_name in self._starting_sessions:
|
|
421
|
+
session_name = f"{session_name}_{_datetime.now().strftime('%m%d_%H%M%S')}"
|
|
422
|
+
|
|
423
|
+
self._starting_sessions.add(session_name)
|
|
424
|
+
|
|
409
425
|
work_dir = str(work_path.absolute())
|
|
410
426
|
script_dir = Path(__file__).parent.parent.absolute()
|
|
411
427
|
server_script = script_dir / "server" / "server.py"
|
|
412
|
-
cmd = [
|
|
428
|
+
cmd = ["uv", "run", "--project", str(script_dir), "python3", str(server_script), session_name]
|
|
413
429
|
if self._poller.get_bypass_enabled():
|
|
414
430
|
cmd += ["--", "--dangerously-skip-permissions", "--permission-mode=dontAsk"]
|
|
415
431
|
|
|
416
432
|
try:
|
|
417
|
-
import os as _os
|
|
418
433
|
env = _os.environ.copy()
|
|
419
434
|
env.pop("CLAUDECODE", None)
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
435
|
+
|
|
436
|
+
log_path = USER_DATA_DIR / "startup.log"
|
|
437
|
+
start_time = _datetime.now()
|
|
438
|
+
|
|
439
|
+
with open(log_path, 'a') as stderr_fd:
|
|
440
|
+
proc = subprocess.Popen(
|
|
441
|
+
cmd, stdout=subprocess.DEVNULL, stderr=stderr_fd,
|
|
442
|
+
start_new_session=True, cwd=work_dir, env=env,
|
|
443
|
+
)
|
|
424
444
|
|
|
425
445
|
socket_path = get_socket_path(session_name)
|
|
426
|
-
for
|
|
446
|
+
for i in range(120):
|
|
427
447
|
await asyncio.sleep(0.1)
|
|
428
448
|
if socket_path.exists():
|
|
429
449
|
break
|
|
450
|
+
if (i + 1) % 10 == 0:
|
|
451
|
+
elapsed = (i + 1) // 10
|
|
452
|
+
rc = proc.poll()
|
|
453
|
+
if rc is not None:
|
|
454
|
+
log_content = _read_log_since(start_time, log_path)
|
|
455
|
+
logger.warning(f"启动并创建群聊失败: server 进程已退出 (exitcode={rc}, elapsed={elapsed}s)\n{log_content}")
|
|
456
|
+
await card_service.send_text(chat_id, f"错误: Server 进程意外退出 (code={rc})\n\n{log_content}")
|
|
457
|
+
return
|
|
430
458
|
else:
|
|
431
|
-
|
|
459
|
+
log_content = _read_log_since(start_time, log_path)
|
|
460
|
+
logger.error(f"启动并创建群聊超时 (12s), session={session_name}\n{log_content}")
|
|
461
|
+
await card_service.send_text(chat_id, f"错误: 会话启动超时 (12s)\n\n{log_content}")
|
|
432
462
|
return
|
|
433
463
|
|
|
434
464
|
await self._cmd_new_group(user_id, chat_id, session_name)
|
|
@@ -436,6 +466,8 @@ class LarkHandler:
|
|
|
436
466
|
except Exception as e:
|
|
437
467
|
logger.error(f"启动并创建群聊失败: {e}")
|
|
438
468
|
await card_service.send_text(chat_id, f"操作失败:{e}")
|
|
469
|
+
finally:
|
|
470
|
+
self._starting_sessions.discard(session_name)
|
|
439
471
|
|
|
440
472
|
async def _cmd_kill(self, user_id: str, chat_id: str, args: str,
|
|
441
473
|
message_id: Optional[str] = None):
|
|
@@ -791,6 +823,32 @@ class LarkHandler:
|
|
|
791
823
|
except Exception as e:
|
|
792
824
|
return False, str(e)
|
|
793
825
|
|
|
826
|
+
async def _disband_groups_for_session(self, session_name: str, source: str = ""):
|
|
827
|
+
"""解散绑定到指定会话的所有专属群聊"""
|
|
828
|
+
disbanded = []
|
|
829
|
+
for cid in list(self._group_chat_ids):
|
|
830
|
+
if self._chat_bindings.get(cid) == session_name:
|
|
831
|
+
log_prefix = f"[{source}] " if source else ""
|
|
832
|
+
logger.info(f"{log_prefix}自动解散群聊: chat_id={cid[:8]}..., session={session_name}")
|
|
833
|
+
# 先清理本地状态(防止并发协程重入时重复处理)
|
|
834
|
+
self._group_chat_ids.discard(cid)
|
|
835
|
+
self._chat_bindings.pop(cid, None)
|
|
836
|
+
disbanded.append(cid)
|
|
837
|
+
# 停止轮询 + 断开 bridge
|
|
838
|
+
self._poller.stop(cid)
|
|
839
|
+
bridge = self._bridges.pop(cid, None)
|
|
840
|
+
if bridge:
|
|
841
|
+
await bridge.disconnect()
|
|
842
|
+
self._chat_sessions.pop(cid, None)
|
|
843
|
+
self._detached_slices.pop(cid, None)
|
|
844
|
+
# 调用飞书 API 解散
|
|
845
|
+
ok, err = await self._disband_group_via_api(cid)
|
|
846
|
+
if not ok:
|
|
847
|
+
logger.warning(f"{log_prefix}解散群 {cid[:8]}... API 失败: {err}")
|
|
848
|
+
if disbanded:
|
|
849
|
+
self._save_chat_bindings()
|
|
850
|
+
self._save_group_chat_ids()
|
|
851
|
+
|
|
794
852
|
async def _cmd_disband_group(self, user_id: str, chat_id: str, session_name: str,
|
|
795
853
|
message_id: Optional[str] = None):
|
|
796
854
|
"""解散与指定会话绑定的专属群聊"""
|
|
@@ -842,6 +900,8 @@ class LarkHandler:
|
|
|
842
900
|
await card_service.send_text(
|
|
843
901
|
chat_id, f"会话 '{saved_session}' 已不存在,请重新 /attach"
|
|
844
902
|
)
|
|
903
|
+
# 会话已不存在,解散绑定到该会话的所有专属群聊
|
|
904
|
+
await self._disband_groups_for_session(saved_session, source="lazy")
|
|
845
905
|
return
|
|
846
906
|
bridge = self._bridges.get(chat_id)
|
|
847
907
|
else:
|
|
@@ -861,12 +921,11 @@ class LarkHandler:
|
|
|
861
921
|
|
|
862
922
|
# ── 选项处理 ─────────────────────────────────────────────────────────────
|
|
863
923
|
|
|
864
|
-
async def handle_option_select(self, user_id: str, chat_id: str, option_value: str, option_total: int = 0):
|
|
865
|
-
"""
|
|
924
|
+
async def handle_option_select(self, user_id: str, chat_id: str, option_value: str, option_total: int = 0, *, needs_input: bool = False):
|
|
925
|
+
"""闭环选项选择:箭头键导航 + 共享内存验证
|
|
866
926
|
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
最后发 Enter 确认。
|
|
927
|
+
发箭头键导航到目标选项,每步从共享内存读取 selected_value 确认是否到位,
|
|
928
|
+
到位后发 Enter 确认。避免数字键在溢出选项上无效的问题。
|
|
870
929
|
"""
|
|
871
930
|
logger.info(f"处理选项选择: user={user_id[:8]}..., option={option_value}, total={option_total}")
|
|
872
931
|
_track_stats('lark', 'option_select',
|
|
@@ -878,37 +937,99 @@ class LarkHandler:
|
|
|
878
937
|
await card_service.send_text(chat_id, "未连接到任何会话,请先使用 /attach <会话名> 连接")
|
|
879
938
|
return
|
|
880
939
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
"no": "n",
|
|
884
|
-
"allow_once": "y",
|
|
885
|
-
"allow_always": "a",
|
|
886
|
-
"deny": "n",
|
|
887
|
-
}
|
|
888
|
-
key_to_send = key_mapping.get(option_value.lower())
|
|
889
|
-
|
|
890
|
-
if key_to_send:
|
|
891
|
-
# 固定映射的选项(permission 类型)
|
|
892
|
-
logger.info(f"发送按键到 Claude: {key_to_send}")
|
|
893
|
-
success = await bridge.send_key(key_to_send)
|
|
894
|
-
elif option_total > 1 and option_value == str(option_total):
|
|
895
|
-
# 最后一个选项:发 (N-1) 次 ↓ → Enter
|
|
896
|
-
import asyncio
|
|
897
|
-
steps = option_total - 1
|
|
898
|
-
logger.info(f"最后一个选项,发送: {steps}次↓ → Enter")
|
|
899
|
-
for _ in range(steps):
|
|
900
|
-
await bridge.send_raw(b"\x1b[B") # ↓ 箭头
|
|
901
|
-
await asyncio.sleep(0.05)
|
|
902
|
-
success = await bridge.send_raw(b"\r")
|
|
903
|
-
else:
|
|
904
|
-
# 普通数字选项
|
|
905
|
-
logger.info(f"发送按键到 Claude: {option_value}")
|
|
906
|
-
success = await bridge.send_key(option_value)
|
|
940
|
+
target = option_value # 目标选项 value(如 "2")
|
|
941
|
+
max_steps = max(option_total, 10) if option_total > 0 else 10
|
|
907
942
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
943
|
+
# 记录初始 option_block 的 block_id,防止跨选项交互误操作
|
|
944
|
+
initial_snapshot = self._poller.read_snapshot(chat_id)
|
|
945
|
+
if not initial_snapshot:
|
|
946
|
+
return
|
|
947
|
+
initial_ob = initial_snapshot.get('option_block')
|
|
948
|
+
if not initial_ob:
|
|
949
|
+
return
|
|
950
|
+
initial_block_id = initial_ob.get('block_id', '')
|
|
951
|
+
|
|
952
|
+
for step in range(max_steps):
|
|
953
|
+
# 1. 读取当前选中项
|
|
954
|
+
snapshot = self._poller.read_snapshot(chat_id)
|
|
955
|
+
if not snapshot:
|
|
956
|
+
break
|
|
957
|
+
ob = snapshot.get('option_block')
|
|
958
|
+
if not ob:
|
|
959
|
+
break # option_block 已消失(CLI 已进入下一状态)
|
|
960
|
+
|
|
961
|
+
# 检查 block_id 一致性
|
|
962
|
+
if initial_block_id and ob.get('block_id', '') != initial_block_id:
|
|
963
|
+
logger.warning(f"option_block 已切换,中止选项选择")
|
|
964
|
+
break
|
|
965
|
+
|
|
966
|
+
current = ob.get('selected_value', '')
|
|
967
|
+
|
|
968
|
+
# 闪烁帧重试:❯ 光标字符会时隐时现,selected_value 为空时短暂重试
|
|
969
|
+
if not current:
|
|
970
|
+
for _retry in range(5): # 最多重试 5 次,共 500ms
|
|
971
|
+
await asyncio.sleep(0.1)
|
|
972
|
+
snap = self._poller.read_snapshot(chat_id)
|
|
973
|
+
if not snap:
|
|
974
|
+
break
|
|
975
|
+
retry_ob = snap.get('option_block')
|
|
976
|
+
if not retry_ob:
|
|
977
|
+
break
|
|
978
|
+
current = retry_ob.get('selected_value', '')
|
|
979
|
+
if current:
|
|
980
|
+
break
|
|
981
|
+
|
|
982
|
+
# 2. 已到位 → 发 Enter(自由输入选项只导航不发 Enter)
|
|
983
|
+
if current == target:
|
|
984
|
+
if needs_input:
|
|
985
|
+
logger.info(f"自由输入选项已到位: target={target},不发送 Enter")
|
|
986
|
+
self._poller.kick(chat_id)
|
|
987
|
+
return
|
|
988
|
+
logger.info(f"选项已到位: current={current} == target={target},发送 Enter")
|
|
989
|
+
success = await bridge.send_raw(b"\r")
|
|
990
|
+
if success:
|
|
991
|
+
self._poller.kick(chat_id)
|
|
992
|
+
else:
|
|
993
|
+
await card_service.send_text(chat_id, "发送选择失败")
|
|
994
|
+
return
|
|
995
|
+
|
|
996
|
+
# 3. 未到位 → 发箭头键
|
|
997
|
+
if current and target:
|
|
998
|
+
try:
|
|
999
|
+
if int(current) < int(target):
|
|
1000
|
+
logger.info(f"步骤{step}: current={current} < target={target},发送 ↓")
|
|
1001
|
+
await bridge.send_raw(b"\x1b[B") # ↓
|
|
1002
|
+
else:
|
|
1003
|
+
logger.info(f"步骤{step}: current={current} > target={target},发送 ↑")
|
|
1004
|
+
await bridge.send_raw(b"\x1b[A") # ↑
|
|
1005
|
+
except ValueError:
|
|
1006
|
+
logger.warning(f"步骤{step}: 无法比较 current={current!r} 和 target={target!r},发送 ↓")
|
|
1007
|
+
await bridge.send_raw(b"\x1b[B")
|
|
1008
|
+
else:
|
|
1009
|
+
# selected_value 重试后仍为空(真正的初始状态),默认向下
|
|
1010
|
+
logger.info(f"步骤{step}: selected_value 重试后仍为空,发送 ↓")
|
|
1011
|
+
await bridge.send_raw(b"\x1b[B")
|
|
1012
|
+
|
|
1013
|
+
# 4. 等待共享内存更新(轮询直到 selected_value 变为另一个非空值或超时)
|
|
1014
|
+
old_selected = current
|
|
1015
|
+
deadline = time.time() + 2.0 # 单步超时 2s
|
|
1016
|
+
while time.time() < deadline:
|
|
1017
|
+
await asyncio.sleep(0.1) # 100ms 轮询
|
|
1018
|
+
snap = self._poller.read_snapshot(chat_id)
|
|
1019
|
+
if not snap:
|
|
1020
|
+
break
|
|
1021
|
+
new_ob = snap.get('option_block')
|
|
1022
|
+
if not new_ob:
|
|
1023
|
+
break # option_block 消失,退出
|
|
1024
|
+
if initial_block_id and new_ob.get('block_id', '') != initial_block_id:
|
|
1025
|
+
break # block_id 变了,外层会处理
|
|
1026
|
+
new_selected = new_ob.get('selected_value', '')
|
|
1027
|
+
# 忽略闪烁帧:只有变为另一个非空值才视为真正变化
|
|
1028
|
+
if new_selected and new_selected != old_selected:
|
|
1029
|
+
break
|
|
1030
|
+
|
|
1031
|
+
# 超过 max_steps 仍未到位,记录警告
|
|
1032
|
+
logger.warning(f"选项选择超步数: target={target}, steps={max_steps}")
|
|
912
1033
|
|
|
913
1034
|
# ── 快捷键发送 ─────────────────────────────────────────────────────────────
|
|
914
1035
|
|
package/lark_client/main.py
CHANGED
|
@@ -8,8 +8,10 @@ Remote Claude 飞书客户端
|
|
|
8
8
|
import asyncio
|
|
9
9
|
import json
|
|
10
10
|
import logging
|
|
11
|
+
import os
|
|
11
12
|
import signal
|
|
12
13
|
import sys
|
|
14
|
+
import urllib.request
|
|
13
15
|
from pathlib import Path
|
|
14
16
|
|
|
15
17
|
|
|
@@ -183,8 +185,9 @@ def handle_card_action(event: P2CardActionTrigger) -> P2CardActionTriggerRespons
|
|
|
183
185
|
if action_type == "select_option":
|
|
184
186
|
option_value = action_value.get("value", "")
|
|
185
187
|
option_total = int(action_value.get("total", "0"))
|
|
186
|
-
|
|
187
|
-
|
|
188
|
+
needs_input = action_value.get("needs_input", False)
|
|
189
|
+
print(f"[Lark] 用户选择了选项: {option_value} (total={option_total}, needs_input={needs_input})")
|
|
190
|
+
asyncio.create_task(handler.handle_option_select(user_id, chat_id, option_value, option_total, needs_input=needs_input))
|
|
188
191
|
return None
|
|
189
192
|
|
|
190
193
|
# 列表卡片:进入会话
|
|
@@ -375,6 +378,21 @@ class LarkBot:
|
|
|
375
378
|
log_level=lark.LogLevel.INFO,
|
|
376
379
|
)
|
|
377
380
|
|
|
381
|
+
# 代理兼容:检测 SOCKS 代理,按配置决定是否绕过
|
|
382
|
+
proxy_info = urllib.request.getproxies()
|
|
383
|
+
socks_proxy = (proxy_info.get('socks') or proxy_info.get('all')
|
|
384
|
+
or proxy_info.get('https') or proxy_info.get('http'))
|
|
385
|
+
if socks_proxy and 'socks' in socks_proxy.lower():
|
|
386
|
+
if config.LARK_NO_PROXY:
|
|
387
|
+
# 用户选择绕过代理 → 清除代理环境变量
|
|
388
|
+
for var in ('ALL_PROXY', 'all_proxy', 'HTTPS_PROXY', 'https_proxy',
|
|
389
|
+
'HTTP_PROXY', 'http_proxy', 'SOCKS_PROXY', 'socks_proxy'):
|
|
390
|
+
os.environ.pop(var, None)
|
|
391
|
+
print(f"检测到 SOCKS 代理 ({socks_proxy}),已按 LARK_NO_PROXY=1 绕过")
|
|
392
|
+
else:
|
|
393
|
+
print(f"检测到 SOCKS 代理 ({socks_proxy}),将通过代理连接")
|
|
394
|
+
print(" 如连接失败,可在 .env 中设置 LARK_NO_PROXY=1 绕过代理")
|
|
395
|
+
|
|
378
396
|
self.running = True
|
|
379
397
|
print("\n机器人已启动,等待消息...")
|
|
380
398
|
print("在飞书中发送 /help 查看使用说明\n")
|
|
@@ -119,22 +119,16 @@ class SessionBridge:
|
|
|
119
119
|
if not self.writer or not self.running:
|
|
120
120
|
return False
|
|
121
121
|
try:
|
|
122
|
+
# 分两步发送:先发文本,等 Ink 框架处理完毕,再发 Enter
|
|
123
|
+
# 不能合并为 text+\r 一次写入(Ink 同一 tick 处理时 \r 会被提前消费,导致需要两次 Enter)
|
|
124
|
+
# 不能在文本前/后加 ESC(ESC+Enter 被终端解释为 Alt+Enter,导致换行而非提交)
|
|
122
125
|
msg = InputMessage(text.encode('utf-8'), self.client_id)
|
|
123
126
|
self.writer.write(encode_message(msg))
|
|
124
127
|
await self.writer.drain()
|
|
125
|
-
|
|
126
|
-
# 发送 Escape 退出多行模式
|
|
127
|
-
await asyncio.sleep(0.1)
|
|
128
|
-
msg = InputMessage(b"\x1b", self.client_id)
|
|
129
|
-
self.writer.write(encode_message(msg))
|
|
130
|
-
await self.writer.drain()
|
|
131
|
-
|
|
132
|
-
# 发送 Enter 提交
|
|
133
|
-
await asyncio.sleep(0.1)
|
|
128
|
+
await asyncio.sleep(0.05)
|
|
134
129
|
msg = InputMessage(b"\r", self.client_id)
|
|
135
130
|
self.writer.write(encode_message(msg))
|
|
136
131
|
await self.writer.drain()
|
|
137
|
-
|
|
138
132
|
return True
|
|
139
133
|
except Exception as e:
|
|
140
134
|
logger.error(f"发送失败: {e}")
|
|
@@ -40,6 +40,7 @@ from utils.session import ensure_user_data_dir, USER_DATA_DIR
|
|
|
40
40
|
# ── 常量 ──────────────────────────────────────────────────────────────────────
|
|
41
41
|
INITIAL_WINDOW = 30 # 首次 attach 最多显示最近 30 个 blocks
|
|
42
42
|
from .config import MAX_CARD_BLOCKS # 单张卡片最多 N 个 blocks → 超限冻结(可通过 .env 配置)
|
|
43
|
+
CARD_SIZE_LIMIT = 25 * 1024 # 25KB,飞书限制 30KB,留 5KB 余量
|
|
43
44
|
POLL_INTERVAL = 1.0 # 轮询间隔(秒)
|
|
44
45
|
RAPID_INTERVAL = 0.2 # 快速轮询间隔(秒)
|
|
45
46
|
RAPID_DURATION = 2.0 # 快速轮询持续时间(秒)
|
|
@@ -153,6 +154,16 @@ class SharedMemoryPoller:
|
|
|
153
154
|
if exc:
|
|
154
155
|
logger.error(f"轮询 Task 异常: chat_id={chat_id[:8]}..., {exc}", exc_info=exc)
|
|
155
156
|
|
|
157
|
+
def read_snapshot(self, chat_id: str) -> Optional[dict]:
|
|
158
|
+
"""直接读取指定 chat_id 的当前共享内存快照(供 handle_option_select 等即时查询使用)"""
|
|
159
|
+
tracker = self._trackers.get(chat_id)
|
|
160
|
+
if tracker and tracker.reader:
|
|
161
|
+
try:
|
|
162
|
+
return tracker.reader.read()
|
|
163
|
+
except Exception as e:
|
|
164
|
+
logger.warning(f"read_snapshot 失败: {e}")
|
|
165
|
+
return None
|
|
166
|
+
|
|
156
167
|
def kick(self, chat_id: str) -> None:
|
|
157
168
|
"""触发立即轮询并进入快速轮询模式"""
|
|
158
169
|
self._rapid_until[chat_id] = time.time() + RAPID_DURATION
|
|
@@ -262,6 +273,16 @@ class SharedMemoryPoller:
|
|
|
262
273
|
from .card_builder import build_stream_card
|
|
263
274
|
card_dict = build_stream_card(blocks_slice, status_line, bottom_bar, agent_panel=agent_panel, option_block=option_block, session_name=tracker.session_name, cli_type=cli_type)
|
|
264
275
|
|
|
276
|
+
# 大小超限检查(与 blocks 数量超限同一套逻辑)
|
|
277
|
+
card_size = len(json.dumps(card_dict, ensure_ascii=False).encode('utf-8'))
|
|
278
|
+
if card_size > CARD_SIZE_LIMIT:
|
|
279
|
+
freeze_count = self._find_freeze_count(blocks_slice, tracker.session_name)
|
|
280
|
+
await self._freeze_and_split(
|
|
281
|
+
tracker, blocks, status_line, bottom_bar, agent_panel, option_block,
|
|
282
|
+
cli_type=cli_type, freeze_count=freeze_count,
|
|
283
|
+
)
|
|
284
|
+
return
|
|
285
|
+
|
|
265
286
|
active.sequence += 1
|
|
266
287
|
success = await self._card_service.update_card(
|
|
267
288
|
card_id=active.card_id,
|
|
@@ -326,6 +347,15 @@ class SharedMemoryPoller:
|
|
|
326
347
|
|
|
327
348
|
from .card_builder import build_stream_card
|
|
328
349
|
card_dict = build_stream_card(blocks_slice, status_line, bottom_bar, agent_panel=agent_panel, option_block=option_block, session_name=tracker.session_name, cli_type=cli_type)
|
|
350
|
+
|
|
351
|
+
# 新卡大小检查:超限则从头部裁剪
|
|
352
|
+
card_size = len(json.dumps(card_dict, ensure_ascii=False).encode('utf-8'))
|
|
353
|
+
while card_size > CARD_SIZE_LIMIT and len(blocks_slice) > 1:
|
|
354
|
+
blocks_slice = blocks_slice[1:]
|
|
355
|
+
start_idx += 1
|
|
356
|
+
card_dict = build_stream_card(blocks_slice, status_line, bottom_bar, agent_panel=agent_panel, option_block=option_block, session_name=tracker.session_name, cli_type=cli_type)
|
|
357
|
+
card_size = len(json.dumps(card_dict, ensure_ascii=False).encode('utf-8'))
|
|
358
|
+
|
|
329
359
|
card_id = await self._card_service.create_card(card_dict)
|
|
330
360
|
|
|
331
361
|
if card_id:
|
|
@@ -384,6 +414,24 @@ class SharedMemoryPoller:
|
|
|
384
414
|
f"[ELEMENT_LIMIT_SPLIT] session={tracker.session_name} "
|
|
385
415
|
f"new_start={new_start} blocks={len(new_blocks)} card_id={new_card_id}"
|
|
386
416
|
)
|
|
417
|
+
tracker.last_notify_message_id = None
|
|
418
|
+
|
|
419
|
+
def _find_freeze_count(self, blocks_slice: List[dict], session_name: str) -> int:
|
|
420
|
+
"""二分查找冻结卡片能容纳的最大 blocks 数(保证卡片 JSON 大小 ≤ CARD_SIZE_LIMIT)"""
|
|
421
|
+
from .card_builder import build_stream_card
|
|
422
|
+
lo, hi = 1, len(blocks_slice)
|
|
423
|
+
result = 1
|
|
424
|
+
while lo <= hi:
|
|
425
|
+
mid = (lo + hi) // 2
|
|
426
|
+
card = build_stream_card(blocks_slice[:mid], None, None,
|
|
427
|
+
is_frozen=True, session_name=session_name)
|
|
428
|
+
size = len(json.dumps(card, ensure_ascii=False).encode('utf-8'))
|
|
429
|
+
if size <= CARD_SIZE_LIMIT:
|
|
430
|
+
result = mid
|
|
431
|
+
lo = mid + 1
|
|
432
|
+
else:
|
|
433
|
+
hi = mid - 1
|
|
434
|
+
return result
|
|
387
435
|
|
|
388
436
|
async def _freeze_and_split(
|
|
389
437
|
self, tracker: StreamTracker, blocks: List[dict],
|
|
@@ -391,12 +439,15 @@ class SharedMemoryPoller:
|
|
|
391
439
|
agent_panel: Optional[dict] = None,
|
|
392
440
|
option_block: Optional[dict] = None,
|
|
393
441
|
cli_type: str = "claude",
|
|
442
|
+
freeze_count: Optional[int] = None,
|
|
394
443
|
) -> None:
|
|
395
444
|
"""冻结当前卡片 + 开新卡"""
|
|
396
445
|
active = tracker.cards[-1]
|
|
446
|
+
count = freeze_count if freeze_count is not None else MAX_CARD_BLOCKS
|
|
447
|
+
reason = 'size' if freeze_count is not None else 'count'
|
|
397
448
|
|
|
398
|
-
# 冻结当前卡片(只保留前
|
|
399
|
-
frozen_blocks = blocks[active.start_idx:active.start_idx +
|
|
449
|
+
# 冻结当前卡片(只保留前 count 个 blocks,移除状态区和按钮)
|
|
450
|
+
frozen_blocks = blocks[active.start_idx:active.start_idx + count]
|
|
400
451
|
from .card_builder import build_stream_card
|
|
401
452
|
frozen_card = build_stream_card(frozen_blocks, None, None, is_frozen=True)
|
|
402
453
|
active.sequence += 1
|
|
@@ -406,16 +457,25 @@ class SharedMemoryPoller:
|
|
|
406
457
|
chat_id=tracker.chat_id)
|
|
407
458
|
logger.info(
|
|
408
459
|
f"[FREEZE] session={tracker.session_name} card_id={active.card_id} "
|
|
409
|
-
f"blocks=[{active.start_idx}:{active.start_idx +
|
|
460
|
+
f"blocks=[{active.start_idx}:{active.start_idx + count}] reason={reason}"
|
|
410
461
|
)
|
|
411
462
|
|
|
412
463
|
# 创建新卡片
|
|
413
|
-
new_start = active.start_idx +
|
|
464
|
+
new_start = active.start_idx + count
|
|
414
465
|
new_blocks = blocks[new_start:]
|
|
415
466
|
if not new_blocks:
|
|
416
467
|
return
|
|
417
468
|
|
|
418
469
|
new_card_dict = build_stream_card(new_blocks, status_line, bottom_bar, agent_panel=agent_panel, option_block=option_block, session_name=tracker.session_name, cli_type=cli_type)
|
|
470
|
+
|
|
471
|
+
# 新卡大小检查:超限则从头部裁剪
|
|
472
|
+
new_card_size = len(json.dumps(new_card_dict, ensure_ascii=False).encode('utf-8'))
|
|
473
|
+
while new_card_size > CARD_SIZE_LIMIT and len(new_blocks) > 1:
|
|
474
|
+
new_blocks = new_blocks[1:]
|
|
475
|
+
new_start += 1
|
|
476
|
+
new_card_dict = build_stream_card(new_blocks, status_line, bottom_bar, agent_panel=agent_panel, option_block=option_block, session_name=tracker.session_name, cli_type=cli_type)
|
|
477
|
+
new_card_size = len(json.dumps(new_card_dict, ensure_ascii=False).encode('utf-8'))
|
|
478
|
+
|
|
419
479
|
new_card_id = await self._card_service.create_card(new_card_dict)
|
|
420
480
|
if new_card_id:
|
|
421
481
|
await self._card_service.send_card(tracker.chat_id, new_card_id)
|
|
@@ -425,6 +485,7 @@ class SharedMemoryPoller:
|
|
|
425
485
|
f"[NEW after FREEZE] session={tracker.session_name} start_idx={new_start} "
|
|
426
486
|
f"blocks={len(new_blocks)} card_id={new_card_id}"
|
|
427
487
|
)
|
|
488
|
+
tracker.last_notify_message_id = None
|
|
428
489
|
|
|
429
490
|
async def _check_ready_notification(
|
|
430
491
|
self, tracker: StreamTracker,
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -258,6 +258,40 @@ def _get_row_text(screen: pyte.Screen, row: int) -> str:
|
|
|
258
258
|
return ''.join(buf).rstrip()
|
|
259
259
|
|
|
260
260
|
|
|
261
|
+
def _is_dim_fg(fg_color: str) -> bool:
|
|
262
|
+
"""判断前景色是否为灰色/暗色(placeholder 风格)。"""
|
|
263
|
+
if not fg_color or fg_color == 'default':
|
|
264
|
+
return False
|
|
265
|
+
name = fg_color.lower().replace(' ', '').replace('-', '')
|
|
266
|
+
if name in ('brightblack', 'black'): # bright_black = gray
|
|
267
|
+
return True
|
|
268
|
+
if len(fg_color) == 6:
|
|
269
|
+
try:
|
|
270
|
+
r, g, b = int(fg_color[0:2], 16), int(fg_color[2:4], 16), int(fg_color[4:6], 16)
|
|
271
|
+
L = 0.2126 * r + 0.7152 * g + 0.0722 * b
|
|
272
|
+
# 灰色调(R≈G≈B)且非纯白 → inactive/placeholder 颜色(如 999999 亮度 153 > 128)
|
|
273
|
+
if max(r, g, b) - min(r, g, b) <= 30 and L < 230:
|
|
274
|
+
return True
|
|
275
|
+
# 非灰色但较暗
|
|
276
|
+
return L < 128
|
|
277
|
+
except ValueError:
|
|
278
|
+
pass
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _option_label_is_dim(screen: pyte.Screen, row: int) -> bool:
|
|
283
|
+
"""检查选项行 label 部分前景色是否为暗色(自由输入 placeholder 风格)。"""
|
|
284
|
+
text = _get_row_text(screen, row)
|
|
285
|
+
m = re.match(r'^[\s❯]*\d+[.)]\s+', text)
|
|
286
|
+
if not m:
|
|
287
|
+
return False
|
|
288
|
+
col = m.end()
|
|
289
|
+
buf_row = screen.buffer[row]
|
|
290
|
+
if col not in buf_row or not buf_row[col].data.strip():
|
|
291
|
+
return False
|
|
292
|
+
return _is_dim_fg(buf_row[col].fg)
|
|
293
|
+
|
|
294
|
+
|
|
261
295
|
def _get_col0(screen: pyte.Screen, row: int) -> str:
|
|
262
296
|
"""获取指定行第一列字符(col=0)"""
|
|
263
297
|
try:
|
|
@@ -874,6 +908,7 @@ class ClaudeParser(BaseParser):
|
|
|
874
908
|
|
|
875
909
|
options: List[dict] = []
|
|
876
910
|
current_opt: Optional[dict] = None
|
|
911
|
+
selected_value = ""
|
|
877
912
|
ansi_raw_lines = [_get_row_ansi_text(screen, r) for r in input_rows + overflow]
|
|
878
913
|
|
|
879
914
|
for row in all_option_rows:
|
|
@@ -891,7 +926,10 @@ class ClaudeParser(BaseParser):
|
|
|
891
926
|
'label': m.group(2).strip(),
|
|
892
927
|
'value': m.group(1),
|
|
893
928
|
'description': '',
|
|
929
|
+
'needs_input': _option_label_is_dim(screen, row), # 自由输入选项检测
|
|
894
930
|
}
|
|
931
|
+
if line.startswith('❯'):
|
|
932
|
+
selected_value = m.group(1)
|
|
895
933
|
elif current_opt is not None and line:
|
|
896
934
|
# 描述行
|
|
897
935
|
current_opt['description'] = (
|
|
@@ -905,6 +943,7 @@ class ClaudeParser(BaseParser):
|
|
|
905
943
|
return OptionBlock(
|
|
906
944
|
sub_type='option', tag=tag, question=question, options=options,
|
|
907
945
|
ansi_raw='\n'.join(ansi_raw_lines).rstrip(),
|
|
946
|
+
selected_value=selected_value,
|
|
908
947
|
)
|
|
909
948
|
return None
|
|
910
949
|
|
|
@@ -1020,6 +1059,7 @@ class ClaudeParser(BaseParser):
|
|
|
1020
1059
|
content_lines = pre_option_contents[1:-1]
|
|
1021
1060
|
|
|
1022
1061
|
# 只收集范围内的 options
|
|
1062
|
+
selected_value = ""
|
|
1023
1063
|
for i in range(first_opt_idx, last_opt_idx + 1):
|
|
1024
1064
|
line, cat = classified[i]
|
|
1025
1065
|
if cat == 'option':
|
|
@@ -1029,6 +1069,8 @@ class ClaudeParser(BaseParser):
|
|
|
1029
1069
|
'label': m.group(2).strip(),
|
|
1030
1070
|
'value': m.group(1),
|
|
1031
1071
|
})
|
|
1072
|
+
if line.startswith('❯'):
|
|
1073
|
+
selected_value = m.group(1)
|
|
1032
1074
|
|
|
1033
1075
|
return OptionBlock(
|
|
1034
1076
|
sub_type='permission',
|
|
@@ -1037,6 +1079,7 @@ class ClaudeParser(BaseParser):
|
|
|
1037
1079
|
question=question,
|
|
1038
1080
|
options=options,
|
|
1039
1081
|
ansi_raw='\n'.join(ansi_lines).rstrip(),
|
|
1082
|
+
selected_value=selected_value,
|
|
1040
1083
|
)
|
|
1041
1084
|
|
|
1042
1085
|
def _parse_agent_list_panel(
|
|
@@ -1256,6 +1256,7 @@ class CodexParser(BaseParser):
|
|
|
1256
1256
|
|
|
1257
1257
|
options: List[dict] = []
|
|
1258
1258
|
current_opt: Optional[dict] = None
|
|
1259
|
+
selected_value = ""
|
|
1259
1260
|
ansi_raw_lines = [_get_row_ansi_text(screen, r) for r in input_rows + overflow]
|
|
1260
1261
|
|
|
1261
1262
|
for row in all_option_rows:
|
|
@@ -1274,6 +1275,8 @@ class CodexParser(BaseParser):
|
|
|
1274
1275
|
'value': m.group(1),
|
|
1275
1276
|
'description': '',
|
|
1276
1277
|
}
|
|
1278
|
+
if line[0:1] in CODEX_PROMPT_CHARS or line.startswith('❯'):
|
|
1279
|
+
selected_value = m.group(1)
|
|
1277
1280
|
elif current_opt is not None and line:
|
|
1278
1281
|
# 描述行
|
|
1279
1282
|
current_opt['description'] = (
|
|
@@ -1287,6 +1290,7 @@ class CodexParser(BaseParser):
|
|
|
1287
1290
|
return OptionBlock(
|
|
1288
1291
|
sub_type='option', tag=tag, question=question, options=options,
|
|
1289
1292
|
ansi_raw='\n'.join(ansi_raw_lines).rstrip(),
|
|
1293
|
+
selected_value=selected_value,
|
|
1290
1294
|
)
|
|
1291
1295
|
return None
|
|
1292
1296
|
|
|
@@ -1407,6 +1411,7 @@ class CodexParser(BaseParser):
|
|
|
1407
1411
|
content_lines = pre_option_contents[1:-1]
|
|
1408
1412
|
|
|
1409
1413
|
# 只收集范围内的 options
|
|
1414
|
+
selected_value = ""
|
|
1410
1415
|
for i in range(first_opt_idx, last_opt_idx + 1):
|
|
1411
1416
|
line, cat = classified[i]
|
|
1412
1417
|
if cat == 'option':
|
|
@@ -1416,6 +1421,8 @@ class CodexParser(BaseParser):
|
|
|
1416
1421
|
'label': m.group(2).strip(),
|
|
1417
1422
|
'value': m.group(1),
|
|
1418
1423
|
})
|
|
1424
|
+
if line[0:1] in CODEX_PROMPT_CHARS or line.startswith('❯'):
|
|
1425
|
+
selected_value = m.group(1)
|
|
1419
1426
|
|
|
1420
1427
|
return OptionBlock(
|
|
1421
1428
|
sub_type='permission',
|
|
@@ -1424,6 +1431,7 @@ class CodexParser(BaseParser):
|
|
|
1424
1431
|
question=question,
|
|
1425
1432
|
options=options,
|
|
1426
1433
|
ansi_raw='\n'.join(ansi_lines).rstrip(),
|
|
1434
|
+
selected_value=selected_value,
|
|
1427
1435
|
)
|
|
1428
1436
|
|
|
1429
1437
|
def _parse_agent_list_panel(
|