remote-claude 0.2.13 → 0.2.15
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 +6 -4
- package/init.sh +5 -0
- package/lark_client/card_builder.py +51 -1
- package/lark_client/card_service.py +66 -4
- package/lark_client/lark_handler.py +25 -7
- package/lark_client/main.py +14 -5
- package/lark_client/session_bridge.py +0 -1
- package/lark_client/shared_memory_poller.py +159 -2
- package/package.json +1 -1
- package/server/parsers/base_parser.py +4 -1
- package/server/parsers/claude_parser.py +27 -6
- package/server/parsers/codex_parser.py +264 -115
- package/server/rich_text_renderer.py +69 -4
- package/server/server.py +195 -16
package/server/server.py
CHANGED
|
@@ -68,6 +68,63 @@ except Exception:
|
|
|
68
68
|
HISTORY_BUFFER_SIZE = 100 * 1024 # 100KB
|
|
69
69
|
|
|
70
70
|
|
|
71
|
+
# ── VirtualScreen:将 HistoryScreen.history.top + buffer 合并为统一视图 ──────
|
|
72
|
+
|
|
73
|
+
class _VirtualBuffer:
|
|
74
|
+
"""统一访问接口:history rows [0..offset) + screen.buffer [offset..offset+lines)"""
|
|
75
|
+
__slots__ = ('_history', '_buffer', '_offset')
|
|
76
|
+
|
|
77
|
+
def __init__(self, history, buffer, offset):
|
|
78
|
+
self._history = history # list[dict],来自 list(screen.history.top)
|
|
79
|
+
self._buffer = buffer # pyte screen.buffer
|
|
80
|
+
self._offset = offset # len(history)
|
|
81
|
+
|
|
82
|
+
def __getitem__(self, row):
|
|
83
|
+
if row < self._offset:
|
|
84
|
+
return self._history[row]
|
|
85
|
+
return self._buffer[row - self._offset]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class _VirtualCursor:
|
|
89
|
+
"""模拟 pyte cursor,y 偏移 history 行数"""
|
|
90
|
+
__slots__ = ('y',)
|
|
91
|
+
|
|
92
|
+
def __init__(self, y: int):
|
|
93
|
+
self.y = y
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class VirtualScreen:
|
|
97
|
+
"""HistoryScreen wrapper:将 history.top + buffer 合并为 parser 可直接读取的统一屏幕视图。
|
|
98
|
+
|
|
99
|
+
parser 无需任何修改:
|
|
100
|
+
- _split_regions 从 cursor.y+5 向上扫描找分割线,先找到当前屏幕中的分割线就停了
|
|
101
|
+
- output_rows 自然包含 history 中的行号(0 到 offset-1)
|
|
102
|
+
- _get_row_text/buffer[row] 通过 VirtualBuffer 透明返回对应行
|
|
103
|
+
"""
|
|
104
|
+
__slots__ = ('_screen', '_history', '_offset')
|
|
105
|
+
|
|
106
|
+
def __init__(self, screen):
|
|
107
|
+
self._screen = screen
|
|
108
|
+
self._history = list(screen.history.top) if hasattr(screen, 'history') else []
|
|
109
|
+
self._offset = len(self._history)
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def columns(self):
|
|
113
|
+
return self._screen.columns
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def lines(self):
|
|
117
|
+
return self._offset + self._screen.lines
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def cursor(self):
|
|
121
|
+
return _VirtualCursor(self._offset + self._screen.cursor.y)
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def buffer(self):
|
|
125
|
+
return _VirtualBuffer(self._history, self._screen.buffer, self._offset)
|
|
126
|
+
|
|
127
|
+
|
|
71
128
|
# ── 全量快照架构 ─────────────────────────────────────────────────────────────
|
|
72
129
|
|
|
73
130
|
@dataclass
|
|
@@ -129,9 +186,17 @@ class OutputWatcher:
|
|
|
129
186
|
self._debug_verbose = debug_verbose # --debug-verbose 开启后输出 indicator/repr 等诊断信息
|
|
130
187
|
safe_name = _safe_filename(session_name)
|
|
131
188
|
self._debug_file = f"/tmp/remote-claude/{safe_name}_messages.log"
|
|
189
|
+
# PTY 原始字节流日志(仅 --debug-screen 开启时使用)
|
|
190
|
+
self._raw_log_fd = None
|
|
191
|
+
if debug_screen:
|
|
192
|
+
raw_log_path = f"/tmp/remote-claude/{safe_name}_pty_raw.log"
|
|
193
|
+
try:
|
|
194
|
+
self._raw_log_fd = open(raw_log_path, "a", encoding="ascii", buffering=1)
|
|
195
|
+
except Exception:
|
|
196
|
+
pass
|
|
132
197
|
# 持久化 pyte 渲染器:PTY 数据直接实时喂入,flush 时直接读 screen
|
|
133
198
|
from rich_text_renderer import RichTextRenderer
|
|
134
|
-
self._renderer = RichTextRenderer(columns=cols, lines=rows)
|
|
199
|
+
self._renderer = RichTextRenderer(columns=cols, lines=rows, debug_stream=debug_screen)
|
|
135
200
|
# 持久化解析器(跨帧保留 dot_row_cache);由调用方注入(可插拔架构)
|
|
136
201
|
import logging as _logging
|
|
137
202
|
_logging.getLogger('ComponentParser').setLevel(_logging.DEBUG)
|
|
@@ -175,6 +240,15 @@ class OutputWatcher:
|
|
|
175
240
|
# 诊断日志:记录 PTY 数据到达
|
|
176
241
|
if data:
|
|
177
242
|
logger.debug(f"[diag-feed] len={len(data)} data={data[:50]!r}")
|
|
243
|
+
# 改动3:--debug-screen 开启时追加原始字节到 _pty_raw.log(base64 编码)
|
|
244
|
+
if self._raw_log_fd is not None and data:
|
|
245
|
+
try:
|
|
246
|
+
import base64 as _b64
|
|
247
|
+
ts = time.strftime('%H:%M:%S') + f'.{int(time.time() * 1000) % 1000:03d}'
|
|
248
|
+
encoded = _b64.b64encode(data).decode('ascii')
|
|
249
|
+
self._raw_log_fd.write(f"{ts} len={len(data)} {encoded}\n")
|
|
250
|
+
except Exception:
|
|
251
|
+
pass
|
|
178
252
|
try:
|
|
179
253
|
loop = asyncio.get_running_loop()
|
|
180
254
|
except RuntimeError:
|
|
@@ -217,8 +291,10 @@ class OutputWatcher:
|
|
|
217
291
|
self._write_screen_debug(self._renderer.screen)
|
|
218
292
|
t1 = time.time()
|
|
219
293
|
|
|
220
|
-
# 1.
|
|
221
|
-
|
|
294
|
+
# 1. 构建 VirtualScreen(history.top + buffer)并解析
|
|
295
|
+
# _write_screen_debug 仍用原始 screen(只写当前帧快照,不含 history)
|
|
296
|
+
vscreen = VirtualScreen(self._renderer.screen)
|
|
297
|
+
components = self._parser.parse(vscreen)
|
|
222
298
|
input_text = self._parser.last_input_text
|
|
223
299
|
input_ansi_text = self._parser.last_input_ansi_text
|
|
224
300
|
|
|
@@ -256,10 +332,10 @@ class OutputWatcher:
|
|
|
256
332
|
last_ob_blink = b.is_streaming
|
|
257
333
|
last_ob_content = b.content[:40]
|
|
258
334
|
last_ob_start_row = b.start_row
|
|
259
|
-
#
|
|
335
|
+
# 读 vscreen.buffer 获取原始字符属性(支持 history 行)
|
|
260
336
|
if b.start_row >= 0:
|
|
261
337
|
try:
|
|
262
|
-
char =
|
|
338
|
+
char = vscreen.buffer[b.start_row][0]
|
|
263
339
|
last_ob_indicator_char = str(getattr(char, 'data', ''))
|
|
264
340
|
last_ob_indicator_fg = str(getattr(char, 'fg', ''))
|
|
265
341
|
last_ob_indicator_bold = bool(getattr(char, 'bold', False))
|
|
@@ -329,7 +405,7 @@ class OutputWatcher:
|
|
|
329
405
|
b.is_streaming = True
|
|
330
406
|
break
|
|
331
407
|
|
|
332
|
-
# 5. 直接使用 visible_blocks(
|
|
408
|
+
# 5. 直接使用 visible_blocks(VirtualScreen 已包含 history.top + 当前屏幕)
|
|
333
409
|
all_blocks = visible_blocks
|
|
334
410
|
|
|
335
411
|
# 5b. 后台 agent 摘要:BottomBar 有 agent 信息但面板未展开时,
|
|
@@ -513,8 +589,80 @@ class OutputWatcher:
|
|
|
513
589
|
except Exception:
|
|
514
590
|
pass
|
|
515
591
|
|
|
592
|
+
@staticmethod
|
|
593
|
+
def _char_to_ansi(char) -> str:
|
|
594
|
+
"""将 pyte Char 的颜色/样式属性转为 ANSI SGR 前缀序列。
|
|
595
|
+
|
|
596
|
+
返回带 ANSI 转义的字符字符串,调用方需在行尾追加 \\033[0m 重置。
|
|
597
|
+
仅在属性非默认时才输出对应 SGR,降低输出体积。
|
|
598
|
+
"""
|
|
599
|
+
# 标准颜色名到 ANSI 码
|
|
600
|
+
_FG_NAMES = {
|
|
601
|
+
'black': 30, 'red': 31, 'green': 32, 'yellow': 33,
|
|
602
|
+
'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37,
|
|
603
|
+
'brown': 33, # pyte 把 ANSI 33 解析为 brown
|
|
604
|
+
}
|
|
605
|
+
_BG_NAMES = {
|
|
606
|
+
'black': 40, 'red': 41, 'green': 42, 'yellow': 43,
|
|
607
|
+
'blue': 44, 'magenta': 45, 'cyan': 46, 'white': 47,
|
|
608
|
+
'brown': 43,
|
|
609
|
+
}
|
|
610
|
+
_BRIGHT_FG = {
|
|
611
|
+
'brightblack': 90, 'brightred': 91, 'brightgreen': 92, 'brightyellow': 93,
|
|
612
|
+
'brightblue': 94, 'brightmagenta': 95, 'brightcyan': 96, 'brightwhite': 97,
|
|
613
|
+
}
|
|
614
|
+
_BRIGHT_BG = {
|
|
615
|
+
'brightblack': 100, 'brightred': 101, 'brightgreen': 102, 'brightyellow': 103,
|
|
616
|
+
'brightblue': 104, 'brightmagenta': 105, 'brightcyan': 106, 'brightwhite': 107,
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
sgr = []
|
|
620
|
+
|
|
621
|
+
# bold / blink
|
|
622
|
+
if getattr(char, 'bold', False):
|
|
623
|
+
sgr.append('1')
|
|
624
|
+
if getattr(char, 'blink', False):
|
|
625
|
+
sgr.append('5')
|
|
626
|
+
|
|
627
|
+
# 前景色
|
|
628
|
+
fg = getattr(char, 'fg', 'default')
|
|
629
|
+
if fg and fg != 'default':
|
|
630
|
+
fg_low = str(fg).lower()
|
|
631
|
+
if fg_low in _FG_NAMES:
|
|
632
|
+
sgr.append(str(_FG_NAMES[fg_low]))
|
|
633
|
+
elif fg_low in _BRIGHT_FG:
|
|
634
|
+
sgr.append(str(_BRIGHT_FG[fg_low]))
|
|
635
|
+
elif len(fg_low) == 6 and all(c in '0123456789abcdef' for c in fg_low):
|
|
636
|
+
# 24-bit true color
|
|
637
|
+
r = int(fg_low[0:2], 16)
|
|
638
|
+
g = int(fg_low[2:4], 16)
|
|
639
|
+
b = int(fg_low[4:6], 16)
|
|
640
|
+
sgr.append(f'38;2;{r};{g};{b}')
|
|
641
|
+
|
|
642
|
+
# 背景色
|
|
643
|
+
bg = getattr(char, 'bg', 'default')
|
|
644
|
+
if bg and bg != 'default':
|
|
645
|
+
bg_low = str(bg).lower()
|
|
646
|
+
if bg_low in _BG_NAMES:
|
|
647
|
+
sgr.append(str(_BG_NAMES[bg_low]))
|
|
648
|
+
elif bg_low in _BRIGHT_BG:
|
|
649
|
+
sgr.append(str(_BRIGHT_BG[bg_low]))
|
|
650
|
+
elif len(bg_low) == 6 and all(c in '0123456789abcdef' for c in bg_low):
|
|
651
|
+
r = int(bg_low[0:2], 16)
|
|
652
|
+
g = int(bg_low[2:4], 16)
|
|
653
|
+
b = int(bg_low[4:6], 16)
|
|
654
|
+
sgr.append(f'48;2;{r};{g};{b}')
|
|
655
|
+
|
|
656
|
+
if sgr:
|
|
657
|
+
return f'\033[{";".join(sgr)}m{char.data}\033[0m'
|
|
658
|
+
return char.data
|
|
659
|
+
|
|
516
660
|
def _write_screen_debug(self, screen):
|
|
517
|
-
"""将 pyte 屏幕内容写入调试文件(_screen.log)
|
|
661
|
+
"""将 pyte 屏幕内容写入调试文件(_screen.log)
|
|
662
|
+
|
|
663
|
+
每个字符的 fg/bg 颜色通过 ANSI SGR 序列直接嵌入,
|
|
664
|
+
cat _screen.log 即可在终端看到与 pyte 渲染一致的着色效果。
|
|
665
|
+
"""
|
|
518
666
|
base = f"/tmp/remote-claude/{_safe_filename(self._session_name)}"
|
|
519
667
|
try:
|
|
520
668
|
# pyte 屏幕快照(覆盖写,只保留最新一帧)
|
|
@@ -526,20 +674,51 @@ class OutputWatcher:
|
|
|
526
674
|
"",
|
|
527
675
|
]
|
|
528
676
|
for row in range(scan_limit + 1):
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
677
|
+
# 构建带颜色的行内容(用于终端直接 cat 显示)
|
|
678
|
+
colored_buf = []
|
|
679
|
+
plain_buf = [' '] * screen.columns
|
|
680
|
+
for col in range(screen.columns):
|
|
681
|
+
char = screen.buffer[row].get(col)
|
|
682
|
+
if char is None:
|
|
683
|
+
colored_buf.append(' ')
|
|
684
|
+
continue
|
|
685
|
+
plain_buf[col] = char.data
|
|
686
|
+
colored_buf.append(self._char_to_ansi(char))
|
|
687
|
+
|
|
688
|
+
rstripped_plain = ''.join(plain_buf).rstrip()
|
|
689
|
+
if not rstripped_plain:
|
|
690
|
+
# 检查是否有背景色(对 Codex 背景色分割线很重要)
|
|
691
|
+
row_bgs = set()
|
|
692
|
+
for col in range(screen.columns):
|
|
693
|
+
char = screen.buffer[row].get(col)
|
|
694
|
+
if char is not None:
|
|
695
|
+
bg = getattr(char, 'bg', 'default')
|
|
696
|
+
if bg and bg != 'default':
|
|
697
|
+
row_bgs.add(str(bg))
|
|
698
|
+
if row_bgs:
|
|
699
|
+
bg_str = ','.join(sorted(row_bgs))
|
|
700
|
+
lines.append(f"{row:3d} |[bg:{bg_str} ···]")
|
|
701
|
+
else:
|
|
702
|
+
lines.append(f"{row:3d} |")
|
|
535
703
|
continue
|
|
704
|
+
|
|
536
705
|
try:
|
|
537
706
|
c0 = screen.buffer[row][0]
|
|
538
707
|
col0_blink = getattr(c0, "blink", False)
|
|
539
708
|
except (KeyError, IndexError):
|
|
540
709
|
col0_blink = False
|
|
710
|
+
|
|
541
711
|
blink_mark = "B" if col0_blink else " "
|
|
542
|
-
|
|
712
|
+
# rstrip 着色内容:找最后一个有内容的列号(正确处理 CJK 占 2 列的情况)
|
|
713
|
+
# plain_buf 按列号索引,len(rstripped_plain) 会因 CJK 字符导致列数偏小
|
|
714
|
+
last_col = 0
|
|
715
|
+
for col in range(screen.columns - 1, -1, -1):
|
|
716
|
+
if plain_buf[col] != ' ':
|
|
717
|
+
last_col = col + 1
|
|
718
|
+
break
|
|
719
|
+
colored_line = ''.join(colored_buf[:last_col])
|
|
720
|
+
lines.append(f"{row:3d}{blink_mark}|{colored_line}")
|
|
721
|
+
|
|
543
722
|
with open(screen_path, "w", encoding="utf-8") as f:
|
|
544
723
|
f.write("\n".join(lines))
|
|
545
724
|
except Exception:
|
|
@@ -707,7 +886,7 @@ class ProxyServer:
|
|
|
707
886
|
|
|
708
887
|
# PTY 终端尺寸:与 lark_client 的 pyte 渲染器保持一致
|
|
709
888
|
PTY_COLS = 220
|
|
710
|
-
PTY_ROWS =
|
|
889
|
+
PTY_ROWS = 100 # 行数缩减至 100,历史内容通过 HistoryScreen.history.top 保存(5000 行容量)
|
|
711
890
|
|
|
712
891
|
def _get_parser(self):
|
|
713
892
|
"""根据 cli_type 返回对应的解析器实例"""
|
|
@@ -965,7 +1144,7 @@ class ProxyServer:
|
|
|
965
1144
|
"""处理终端大小变化:同步更新 PTY 和 pyte 渲染尺寸,清空 raw buffer。
|
|
966
1145
|
Claude 收到 SIGWINCH 后会全屏重绘,buffer 清空后自然恢复为新尺寸的完整屏幕数据。"""
|
|
967
1146
|
try:
|
|
968
|
-
# output_watcher 的 rows 固定为 PTY_ROWS(
|
|
1147
|
+
# output_watcher 的 rows 固定为 PTY_ROWS(100),不跟随客户端终端尺寸变化
|
|
969
1148
|
# terminal client 直接渲染 PTY 原始输出,不依赖 output_watcher,无需同步 rows
|
|
970
1149
|
self.output_watcher.resize(msg.cols, self.PTY_ROWS)
|
|
971
1150
|
winsize = struct.pack('HHHH', msg.rows, msg.cols, 0, 0)
|