fix(v8.0): 启用真·全屏 textual TUI 并修复 opacity 崩溃

- 修复崩溃: tui.py MessageBubble.on_mount 用 self.animate('opacity'),但
  textual>=8 的 Widget.opacity 为只读属性会抛 'has no setter',导致每条消息
  气泡崩溃 -> 改 styles.animate 并兜底直接设置。
- run_tui 改为默认真·全屏 textual XiaoliTUI(上方滚动聊天区+底部固定输入框+侧栏),
  rich 面板仅作 textual 不可用时的回退(此前默认的是顺序面板,被指出是'假TUI')。
- 版本统一 v8.0.0: 欢迎框 + /about 改用 VERSION 常量。
- 状态栏联动 token 感知: 显示 used/context_window 进度条(引擎支持时)。
- 新增 /snapshot(/screenshot) 命令: 导出当前屏幕 SVG 到 .tui_snapshots/live.svg,
  让实时 TUI 也能被 AI 自验。
- 新增 tools/tui_real_snapshot.py: textual run_test+Pilot 无头驱动真实 TUI,
  喂对话、断言 DOM(用户/工具成功失败/助手/状态栏/token条/欢迎版本),导出 SVG。
  7 项断言全过。
- 注: launcher 启动会重写 plugins_config.py/requirements.txt(含 tkinter 等错误项),
  提交前已还原,避免误提交漂移。
This commit is contained in:
shuiyu67 committed 2026-08-01 10:30:42 +08:00
1 parent 79957eba0f
commit db9203687d
3 files changed
+191 -15

No files matched your search

+133
View File
@@ -0,0 +1,133 @@
"""
v8.0 真·全屏 TUI(textual)无头自验 + 截图
==========================================
用 textual 的 App.run_test() + Pilot 无头驱动真实 XiaoliTUI:
- 断言 DOM 结构(滚动聊天区 / 底部输入框 / 侧栏 / 状态栏)
- 模拟输入并发送一轮对话,驱动 cli.tui_output_callback 渲染气泡
- 断言渲染结果,导出 SVG 截图到 .tui_snapshots/real_demo.svg
AI 可据此自验"真 TUI 长啥样",无需人工截图。
"""
import os
import sys
import asyncio
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from xcli_core.tui import XiaoliTUI, TEXTUAL_AVAILABLE
OUT_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".tui_snapshots")
# ── 模拟 cli(满足 _TUIBridge 依赖)──
class _MockEngine:
name = "openai"
model = "deepseek-chat"
last_prompt_tokens = 47000
def context_window(self):
return 64000
class _MockMgr:
tools = [
{"name": "file_manager", "description": "读写文件"},
{"name": "git_tools", "description": "git 操作"},
{"name": "memory", "description": "记忆系统"},
]
class _MockCLI:
def __init__(self):
self.engines = {"openai": _MockEngine()}
self.current_engine = self.engines["openai"] # 状态栏 token 进度条依赖此
self.liugin_manager = _MockMgr()
self.shared_conversation_history = []
self.liugin_commands = {}
self.tui_output_callback = None
def get_current_engine_name(self):
return "openai"
def switch_engine(self, name):
return name in self.engines
def process_conversation(self, user_input):
"""模拟真实流程:经 tui_output_callback 喂 assistant/tool 输出"""
self.shared_conversation_history.append({"role": "user", "content": user_input})
cb = self.tui_output_callback
if cb:
cb(" 好的,我先看一下文件结构。")
cb(" OK 工具: file_manager")
cb(" 已读取 core/app.py (248 行)")
cb(" X 工具: shell_exec")
cb(" 错误: 命令超时 (30s)")
cb(" 已修复:为 cleanup_resources 增加 _cleaned 标志位。")
self.shared_conversation_history.append({"role": "assistant", "content": "已修复。"})
def _txt(w):
"""提取 widget 文本(textual 8: Static.content)"""
return str(getattr(w, "content", "") or getattr(w, "renderable", "") or "")
async def run():
app = XiaoliTUI(_MockCLI())
async with app.run_test(size=(120, 64)) as pilot:
await pilot.pause()
# 1) DOM 结构断言
chat = app.query_one("#chat-scroll")
ta = app.query_one("#user-input")
sidebar = app.query_one("#sidebar")
statusbar = app.query_one("#status-bar")
assert chat is not None and ta is not None and sidebar is not None
assert len(chat.children) > 0, "欢迎界面应已渲染"
# 2) 模拟输入并发送一轮
ta.text = "帮我看下 core/app.py 有没有内存泄漏"
app.action_send_message()
# 等待后台 worker(executor 线程)完成
for _ in range(80):
await pilot.pause(0.05)
if not app.is_generating:
break
await pilot.pause(0.2)
# 3) 渲染结果断言(DOM 含全部气泡,不受滚动视口影响)
rendered = "\n".join(_txt(w) for w in chat.children)
checks = {
"欢迎框 v8.0.0": "8.0.0" in rendered,
"用户消息": "core/app.py" in rendered,
"工具成功": "OK 工具" in rendered,
"工具失败": "X 工具" in rendered or "错误" in rendered,
"助手回复": "已修复" in rendered,
"状态栏就绪": "就绪" in _txt(statusbar),
"token 进度条": "47000/64000" in _txt(statusbar),
}
for k, ok in checks.items():
print(f"[{'PASS' if ok else 'FAIL'}] {k}")
# 4) 导出 SVG 截图(AI/人都能看)
os.makedirs(OUT_DIR, exist_ok=True)
svg_path = os.path.join(OUT_DIR, "real_demo.svg")
svg = app.export_screenshot()
with open(svg_path, "w", encoding="utf-8") as f:
f.write(svg)
print(f"\nSVG 截图已保存: {svg_path} ({len(svg)} bytes)")
# 5) 同时导出文本快照(供 AI 快速读回核对)
text_path = os.path.join(OUT_DIR, "real_demo.txt")
with open(text_path, "w", encoding="utf-8") as f:
f.write(rendered)
print(f"文本快照已保存: {text_path}")
ok = all(checks.values())
print("\n自验结论:", "全部通过 ✅" if ok else "存在失败项 ❌")
return ok
if __name__ == "__main__":
if not TEXTUAL_AVAILABLE:
print("textual 未安装,无法运行。pip install textual")
sys.exit(1)
sys.exit(0 if asyncio.run(run()) else 1)