Files
NebulaShell/store/@{NebulaShell}/webui/core/server.py
qwen.ai[bot] 9f7ca46f96 Update TUI to v1.3 with enhanced conversion layer and dual UI architecture
- ai.md: Added comprehensive documentation for TUI v1.3 conversion layer with 64+ supported components, CSS styling, and JavaScript interaction capabilities
- oss/tui/: Created complete TUI module with converter.py implementing HTML/CSS/JS to terminal conversion engine, supporting 40+ component types and advanced styling
- oss/tui/plugin.py: Implemented TUI plugin with dual startup architecture accessing WebUI's /tui interface for HTML conversion and terminal rendering
- store/@{NebulaShell}/webui/tui/: Added TUI package with converter, configuration files, and index.html for terminal interface
- store/@{NebulaShell}/webui/core/server.py: Enhanced WebUI server with TUI interface endpoints (/tui/*) for providing special-marked HTML to conversion layer
- store/@{NebulaShell}/webui/main.py: Updated WebUI plugin to support TUI dual launch with automatic homepage redirection and navigation integration
- .gitignore: Updated ignore patterns for better project cleanliness

The update provides a sophisticated terminal interface that automatically converts WebUI content through a powerful transformation layer, enabling seamless dual-mode operation.
2026-05-02 12:04:27 +08:00

270 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""WebUI 服务器 - 容器模式"""
import subprocess
import os
import tempfile
from oss.plugin.types import Response
from pathlib import Path
class WebUIServer:
"""WebUI 服务器"""
def __init__(self, router, config: dict):
self.router = router
self.config = config
self.frontend_dir = Path(__file__).parent.parent / "frontend"
# 页面注册表
self.pages = {} # path -> content_provider
self.nav_items = [] # 导航项列表
def start(self):
"""注册默认路由"""
# 静态资源
self.router.get("/static/css/main.css", self._handle_css)
self.router.get("/static/js/main.js", self._handle_js)
self.router.get("/health", self._handle_health)
# TUI 接口 - 供 TUI 转换层访问
self.router.get("/tui/index.html", self._handle_tui_index)
self.router.get("/tui/page", self._handle_tui_page)
self.router.get("/tui/css", self._handle_tui_css)
self.router.get("/tui/pages", self._handle_tui_pages)
def register_page(self, path: str, content_provider, nav_item: dict = None):
"""供其他插件注册页面"""
self.pages[path] = content_provider
if nav_item:
nav_item['url'] = path
self.nav_items.append(nav_item)
# 注册路由
self.router.get(path, lambda req: self._render_page(path, req))
def _render_page(self, path: str, request):
"""渲染页面布局 + 内容"""
provider = self.pages.get(path)
content = provider() if provider else ""
# 排序导航项(首页在前)
sorted_nav = sorted(self.nav_items, key=lambda x: 0 if x.get('url') == '/' else 1)
# 构建导航项 HTML
nav_html = ""
icon_map = {
'🏠': 'ri-home-4-line',
'📊': 'ri-dashboard-line',
'📋': 'ri-file-list-3-line',
'🧩': 'ri-puzzle-line',
'⚙️': 'ri-settings-3-line',
'🔌': 'ri-plug-line',
'📦': 'ri-box-3-line',
'🌐': 'ri-global-line',
}
for item in sorted_nav:
url = item.get('url', '#')
is_active = 'active' if url == path else ''
icon = item.get('icon', 'ri-dashboard-line')
text = item.get('text', '')
ri_icon = icon_map.get(icon, icon)
title = text
nav_html += f'''
<a href="{url}" class="nav-item {is_active}" title="{title}">
<i class="{ri_icon}"></i>
</a>
'''
page_title = self.config.get("title", "NebulaShell")
# 读取 HTML 模板
template_file = self.frontend_dir / "views" / "layout.html"
with open(template_file, 'r', encoding='utf-8') as f:
html_template = f.read()
html = html_template.replace('{{ pageTitle }}', page_title)
html = html.replace('{{ navItems }}', nav_html)
html = html.replace('{{ content }}', content)
return Response(
status=200,
headers={"Content-Type": "text/html; charset=utf-8"},
body=html
)
def _default_home_content(self) -> str:
"""默认首页内容"""
return """
<div class="home-content">
<div class="welcome-banner">
<h2>👋 欢迎使用 NebulaShell</h2>
<p>一切皆为插件的轻量级框架</p>
</div>
</div>
"""
def _execute_php(self, php_file: str, variables: dict = None) -> str:
"""执行 PHP 文件"""
variables = variables or {}
# 构建 PHP 变量注入
php_vars = ""
for key, value in variables.items():
if isinstance(value, dict):
php_vars += f"${key} = {self._php_array(value)};\n"
elif isinstance(value, list):
php_vars += f"${key} = {self._php_array_list(value)};\n"
elif isinstance(value, str):
php_vars += f"${key} = '{value.replace(chr(39), chr(92) + chr(39))}';\n"
else:
php_vars += f"${key} = {str(value).lower() if isinstance(value, bool) else value};\n"
with open(php_file, 'r', encoding='utf-8') as f:
php_content = f.read()
# 临时文件必须和 views 在同一目录,这样 __DIR__ 才能正确解析
views_dir = str(Path(php_file).parent)
tmp_file = os.path.join(views_dir, '.temp_render.php')
try:
with open(tmp_file, 'w', encoding='utf-8') as f:
f.write(f"<?php\n{php_vars}\n?>\n{php_content}")
result = subprocess.run(
["php", "-f", tmp_file],
capture_output=True, text=True, timeout=10, cwd=views_dir,
encoding='utf-8', errors='replace'
)
if result.returncode != 0:
print(f"[webui] PHP 执行错误: {result.stderr}")
return f"<div class='error'>PHP Error: {result.stderr}</div>"
return result.stdout
finally:
try:
if os.path.exists(tmp_file):
os.unlink(tmp_file)
except:
pass
def _php_array(self, py_dict: dict) -> str:
"""Python Dict -> PHP Array"""
items = []
for key, value in py_dict.items():
if isinstance(value, str):
items.append(f"'{key}' => '{value.replace(chr(39), chr(92) + chr(39))}'")
elif isinstance(value, dict):
items.append(f"'{key}' => {self._php_array(value)}")
else:
items.append(f"'{key}' => {value}")
return "[" + ", ".join(items) + "]"
def _php_array_list(self, py_list: list) -> str:
"""Python List -> PHP Array"""
items = []
for item in py_list:
if isinstance(item, dict):
items.append(self._php_array(item))
elif isinstance(item, str):
items.append(f"'{item.replace(chr(39), chr(92) + chr(39))}'")
else:
items.append(str(item))
return "[" + ", ".join(items) + "]"
def _handle_css(self, request):
css_file = self.frontend_dir / "assets" / "css" / "main.css"
with open(css_file, 'r', encoding='utf-8') as f:
css = f.read()
return Response(status=200, headers={"Content-Type": "text/css; charset=utf-8"}, body=css)
def _handle_js(self, request):
js_file = self.frontend_dir / "assets" / "js" / "main.js"
with open(js_file, 'r', encoding='utf-8') as f:
js = f.read()
return Response(status=200, headers={"Content-Type": "application/javascript; charset=utf-8"}, body=js)
def _handle_health(self, request):
import json
return Response(status=200, headers={"Content-Type": "application/json"}, body=json.dumps({"status": "ok"}))
# ========== TUI 接口实现 ==========
def _handle_tui_index(self, request):
"""处理 /tui/index.html 请求 - TUI 入口点
返回特殊标记的 HTMLTUI 转换层会识别并转换。
此 HTML 不含用户可见内容,仅包含 data-tui-* 标记和配置脚本。
"""
html = """<!DOCTYPE html>
<html class="tui-page" data-tui-version="2.0">
<head>
<meta charset="UTF-8">
<title>NebulaShell TUI</title>
<!-- TUI 标记:此页面专为终端渲染 -->
<style type="text/x-tui-css">
.tui-page { background-color: #000000; color: #ffffff; }
.tui-body { font-family: monospace; }
.bold { font-weight: bold; }
.underline { text-decoration: underline; }
</style>
</head>
<body class="tui-body">
<div class="tui-container" data-tui-layout="vertical">
<header data-tui-type="header">
<h1>NebulaShell TUI</h1>
<p>终端界面就绪</p>
</header>
<separator data-tui-char=""/>
<nav data-tui-type="nav" data-tui-layout="horizontal">
<a href="/" data-tui-action="navigate" data-tui-key="1">首页</a>
<a href="/dashboard" data-tui-action="navigate" data-tui-key="2">仪表盘</a>
<a href="/logs" data-tui-action="navigate" data-tui-key="3">日志</a>
</nav>
</div>
<script type="application/x-tui-keys">
{"1": {"action": "navigate", "target": "/"}, "2": {"action": "navigate", "target": "/dashboard"}, "3": {"action": "navigate", "target": "/logs"}}
</script>
</body>
</html>"""
return Response(status=200, headers={"Content-Type": "text/html; charset=utf-8"}, body=html)
def _handle_tui_page(self, request):
"""处理 /tui/page 请求 - 获取任意页面的 TUI 版本"""
from urllib.parse import parse_qs, urlparse
parsed = urlparse(request.path)
params = parse_qs(parsed.query)
page_path = params.get('path', ['/'])[0]
# 查找已注册的页面
provider = self.pages.get(page_path)
if provider:
content = provider()
html = f"""<!DOCTYPE html>
<html class="tui-page" data-tui-source="webui">
<body class="tui-body">{content}</body>
</html>"""
return Response(status=200, headers={"Content-Type": "text/html; charset=utf-8"}, body=html)
return Response(status=404, headers={"Content-Type": "text/html"}, body="<html><body>Page not found</body></html>")
def _handle_tui_css(self, request):
"""处理 /tui/css 请求 - 返回终端兼容的 CSS"""
css = """/* TUI 兼容 CSS */
.tui-page { background-color: #000000; color: #ffffff; }
.tui-body { font-family: monospace; }
.bold { font-weight: bold; }
.underline { text-decoration: underline; }
[data-tui-action] { cursor: pointer; }
"""
return Response(status=200, headers={"Content-Type": "text/css"}, body=css)
def _handle_tui_pages(self, request):
"""处理 /tui/pages 请求 - 列出所有可用页面"""
import json
pages = list(self.pages.keys())
return Response(
status=200,
headers={"Content-Type": "application/json"},
body=json.dumps({'success': True, 'pages': pages})
)