--- name: bricks-terminal-and-popup description: Use when Wterm/.xterm terminal or PopupWindow resize breaks. tags: [bricks, wterm, xterm, terminal, popup, resize, sage, pipeline] --- # Bricks 终端后端 + 弹窗 resize 踩坑 ## .xterm 终端后端(Wterm 控件 → SSH 远程命令/vi 编辑) ### 机制 - `.xterm` 文件是 Python 脚本(同 `.dspy` 语法),经 XtermProcessor 执行,返回 DictObject:`{host, username, password/client_keys, cmdargs, noinput}`。 - Wterm 前端 `new WebSocket(ws_url)`;`ws_url` 用 `entire_url('/wss//xxx.xterm')`(浏览器 WebSocket 构造函数自动把 https→wss,**无需** `websocket_url`)。 - nginx `location /wss/` 做 WebSocket 升级 + proxy 到 app 端口(`proxy_set_header X-Forwarded-Path 'wss'`)。 - 后端 XtermProcessor → SSHServer → asyncssh `create_process(cmdargs)` 跑命令(如 `vi <文件>`)。 ### asyncssh create_process 单 command bug(CRITICAL) 新版 asyncssh 的 `create_process(command, *, ...)` 只接受单个 command 位置参数,内部 `create_session(session_factory, command, *, ...)` 只有 2 个位置参数。 xtermProcessor.py 用 `create_process(*login_info.cmdargs, ...)` 展开列表,`cmdargs=['vi', path]` 两个元素 → `TypeError: SSHClientConnection.create_session() takes from 2 to 3 positional arguments but 4 ...`。 **修复**:`.xterm` 里 cmdargs 改成单个字符串:`r.cmdargs = ['vi ' + shlex.quote(full_path)]`(不要 `['vi', path]`)。单命令无参(如 `['~/bin/sagelog']`)不受影响。 ### config.json processors `website.processors` 必须含 `[".xterm","xterm"]` 和 `[".ws","ws"]`(及 `.wss`),否则 .xterm 端点在新环境 404 或当静态文件处理。提交进仓库 config.json 模板,不要手改生产机。 ### 本地 SSH 编辑 `.xterm` 返回 `host='localhost', username=, cmdargs=['vi ']`,依赖服务器 ~/.ssh 免密(authorized_keys)。验证:`ssh localhost 'which vi'`。 ## PopupWindow 右下角 resize 两个 bug PopupWindow 构造器已强制 `opts.resizable = true`(popup.js),**无需在 .dspy 显式设置**。拖拽失效是另外两个 bug: ### bug 1: resizing() 的 e.target 检查 ```js resizing(e){ ele = this.resizable_w.dom_element; if (ele != e.target && !ele.contains(e.target)){ // ❌ mousemove 的 e.target 随鼠标移动变化,拖出 30x30 resizebox 就 stop this.stop_resizing(); return; } ... } ``` **修复**:删掉这段检查。resize_start_pos(mousedown)已用 `contains(e.target)` 保证起点在 resizebox 上,mousemove 期间不应再检查 e.target。 ### bug 2: resizebox z-index 被内容覆盖 `.resizebox` 默认 `z-index: auto`,而 Wterm/xterm 的 canvas/cursor-layer 在 DOM 靠后覆盖它 → mousedown 点不到 resizebox(`document.elementFromPoint` 命中 `xterm-cursor-layer`)。 **修复**:bricks.css `.resizebox` 加 `z-index: 9999`。 ## Wterm 光标 xterm.js `cursorBlink` 默认 false(光标静态 block 不闪烁,视觉上不明显)。Wterm 的 `term_options` 加 `"cursorBlink": true` 让光标闪烁可见。xterm 用 canvas renderer(`rendererType:"canvas"`),光标画在 canvas 上而非 `.xterm-cursor` DOM 元素——`.xterm-cursor` 为空是正常的。 ## DSPY 返回文件(下载/媒体流) `web.FileResponse` 是 `StreamResponse` 子类,DSPY 的 `handle()` 里 `isinstance(content, StreamResponse)` 会直接返回它。所以 `.dspy` 可 `return FileResponse(path, headers={'Content-Disposition': 'attachment; filename="..."'})` 直接提供文件下载/媒体流,无需 register function(如 `idfile`)。 ## 验证方法 - resize:CDP `Input.dispatchMouseEvent` 模拟 mousedown→mousemove→mouseup,对比 popup `getBoundingClientRect` 尺寸变化。 - 命中元素:`document.elementFromPoint(x,y)` 确认点击点命中的是 resizebox 还是被内容覆盖。 - 终端连接:浏览器 console 看 `ws msg=` 数据流 + vi 的 VIM 控制序列(`\u001b[2;2R` 光标定位、`\u001b[>0;276;0c` 设备属性、`\u001b]10;...` 颜色查询)。