"""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''' ''' 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 """

👋 欢迎使用 NebulaShell

一切皆为插件的轻量级框架

""" 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"\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"
PHP Error: {result.stderr}
" 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 入口点 返回特殊标记的 HTML,TUI 转换层会识别并转换。 此 HTML 不含用户可见内容,仅包含 data-tui-* 标记和配置脚本。 """ html = """ NebulaShell TUI

NebulaShell TUI

终端界面就绪

""" 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""" {content} """ return Response(status=200, headers={"Content-Type": "text/html; charset=utf-8"}, body=html) return Response(status=404, headers={"Content-Type": "text/html"}, body="Page not found") 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}) )