From 0783428f804bf87df11e1fc496baac4f6b4ded9c Mon Sep 17 00:00:00 2001 From: Falck Date: Sat, 2 May 2026 13:32:51 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E6=AD=A5=E8=A7=84=E5=88=92TuUi?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=EF=BC=8C=E5=B9=B6=E9=A2=84=E7=95=99=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 14 +- ai.md | 170 +++--- data/DCIM/html-render-config.json | 2 +- oss/__pycache__/__init__.cpython-313.pyc | Bin 183 -> 185 bytes oss/__pycache__/cli.cpython-313.pyc | Bin 2359 -> 9433 bytes oss/cli.py | 22 +- .../__pycache__/__init__.cpython-313.pyc | Bin 142 -> 289 bytes oss/config/__pycache__/config.cpython-313.pyc | Bin 5864 -> 7027 bytes oss/logger/__pycache__/logger.cpython-313.pyc | Bin 4200 -> 4201 bytes .../__pycache__/capabilities.cpython-313.pyc | Bin 4131 -> 4132 bytes oss/plugin/__pycache__/loader.cpython-313.pyc | Bin 7134 -> 4789 bytes .../__pycache__/manager.cpython-313.pyc | Bin 1806 -> 4258 bytes oss/plugin/__pycache__/types.cpython-313.pyc | Bin 4990 -> 4991 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 315 -> 316 bytes oss/shared/__pycache__/router.cpython-313.pyc | Bin 5621 -> 6840 bytes oss/tui/client.py | 491 ++++++++++++++++++ oss/tui/converter.py | 8 + oss/tui/plugin.py | 5 + pyproject.toml | 1 + .../__pycache__/main.cpython-313.pyc | Bin 7686 -> 7687 bytes .../__pycache__/main.cpython-313.pyc | Bin 10088 -> 10089 bytes .../__pycache__/router.cpython-313.pyc | Bin 1141 -> 1142 bytes .../__pycache__/static.cpython-313.pyc | Bin 3631 -> 3632 bytes .../__pycache__/template.cpython-313.pyc | Bin 13205 -> 13206 bytes 24 files changed, 597 insertions(+), 116 deletions(-) create mode 100644 oss/tui/client.py diff --git a/AGENTS.md b/AGENTS.md index c776234..dfcf446 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,9 +4,14 @@ ```bash pip install -r requirements.txt -python -m oss.cli serve # start server on :8080 +pip install -e . # register nebula CLI +nebula serve # start server on :8080 # or: python main.py -# or: oss serve (after pip install -e .) + +## CLI modes (前后端分离) + +- `nebula serve` — 启动后端服务(HTTP API + WebUI) +- `nebula cli` — 启动 TUI 前端,连接现有后端(默认 localhost:8080) ``` ## Architecture (minimal core philosophy) @@ -22,8 +27,9 @@ python -m oss.cli serve # start server on :8080 | Action | Command | |--------|---------| -| Start server | `python -m oss.cli serve` | -| Show info | `python -m oss.cli info` | +| Start server | `nebula serve` | +| CLI / TUI mode | `nebula cli` (TBD) | +| Show info | `nebula info` | | Hidden achievements | Prefix with `!!` (e.g., `!!help`, `!!list`, `!!stats`, `!!debug`) | | Docker | `docker-compose up` (ports 8080-8082) | diff --git a/ai.md b/ai.md index 3411fc9..897e72a 100644 --- a/ai.md +++ b/ai.md @@ -1,5 +1,8 @@ # NebulaShell AI 开发文档 +> **架构决策**:`nebula cli` 采用前后端分离设计,TUI 前端直连后端 JSON API, +> 不使用 HTML→ANSI 转换引擎。详见下文 [TUI 架构决策](#tui-架构决策)。 + ## 项目介绍 NebulaShell 是一个企业级插件化运行时框架 (v1.2.0),核心理念是「一切皆为插件」。它提供了一个最小化的核心系统,仅负责加载 `plugin-loader` 插件,其余 26+ 个官方插件均由该加载器管理。 @@ -23,130 +26,77 @@ NebulaShell 是一个企业级插件化运行时框架 (v1.2.0),核心理念 --- -## TUI + WebUI 双启动架构 +## TUI 架构决策 -### 架构概述 +### 废弃方案:HTML→ANSI 动态转换层(v1.3) -系统现在默认同时启动 WebUI 和 TUI: -- **WebUI**:在浏览器中运行,提供完整的图形界面 -- **TUI**:在终端中运行,通过强大的转换层 (v1.3) 自动解析 WebUI 的 `/tui` 接口 +**已废弃。** 早期方案通过 `oss/tui/converter.py`(1430 行)在运行时将 WebUI 的 HTML 页面解析为终端元素,存在以下问题: -### TUI 转换层核心能力 (v1.3) +| 问题 | 说明 | +|------|------| +| **布局失真** | CSS Flex/Grid 布局模型无法映射到终端字符网格 | +| **交互断层** | JavaScript 事件系统只能在终端模拟,与真实浏览器行为不一致 | +| **维护成本高** | 1430 行转换引擎 + 每个 WebUI 页面需维护 TUI 兼容标记 | +| **渲染性能差** | 每次导航需对整个 HTML 进行 DOM 解析和布局计算 | +| **调试困难** | 终端渲染错误难以定位是 HTML 问题还是转换器 Bug | -TUI 转换层是一个强大的渲染引擎,能够自动访问 WebUI 开放的 `/tui` 接口,解析特殊的 `.html` 文件(入口为 `index.html`),并将其转换为终端界面。 - -#### 支持的组件类型 (64+) +### 当前方案:前后端分离,原生 ANSI 渲染 ``` -基础组件: text, heading, paragraph, span, divider, spacer -容器组件: container, box, panel, card, grid, flex, stack -表单组件: input, button, checkbox, radio, select, textarea, slider -数据组件: table, list, tree, progress, gauge, chart, stat -导航组件: navbar, sidebar, menu, breadcrumb, tabs, pagination -反馈组件: alert, toast, modal, spinner, tooltip, badge -布局组件: row, col, section, article, aside, header, footer -特殊组件: code, pre, blockquote, mark, kbd, time, avatar +nebula serve ─── JSON API ───→ nebula cli (TUI 前端) + (后端) (原生 ANSI 终端渲染) ``` -#### CSS 样式支持 +**后端职责**(`nebula serve`): +- 提供 RESTful JSON API(如 `/api/dashboard/stats`) +- WebSocket 实时推送 +- 不感知 TUI 存在 -转换层支持终端兼容的 CSS 样式: +**前端职责**(`nebula cli`): +- 通过 HTTP/WebSocket 消费后端 JSON 数据 +- 使用 ANSI 转义码直接在终端绘制界面 +- 不依赖任何 HTML/CSS 解析 -```css -/* 颜色系统 */ -color: #RGB, #RRGGBB, rgb(), rgba(), hsl(), 颜色名称 -background-color: 同上 -border-color: 同上 - -/* 字体排版 */ -font-size: small, medium, large, x-large, numeric(pt) -font-weight: normal, bold, bolder, lighter, numeric(100-900) -font-style: normal, italic, oblique -text-decoration: none, underline, overline, line-through -text-align: left, center, right, justify - -/* 边框样式 */ -border: width style color -border-style: none, solid, double, dashed, rounded, heavy, ascii -border-width: thin, medium, thick, numeric -border-radius: numeric (仅支持 rounded 样式) - -/* 布局与间距 */ -margin: numeric -padding: numeric -width: numeric, percentage, auto -height: numeric, percentage, auto -display: block, inline, flex, grid, none - -/* 特殊效果 */ -opacity: 0.0-1.0 (通过字符密度模拟) -white-space: normal, nowrap, pre -overflow: visible, hidden, scroll -``` - -#### JavaScript 交互支持 - -转换层模拟基础 JS 交互功能: - -```javascript -// 键盘事件 -document.addEventListener('keydown', (e) => { ... }) -document.addEventListener('keyup', (e) => { ... }) - -// 鼠标事件 (如果终端支持) -element.addEventListener('click', (e) => { ... }) -element.addEventListener('mouseover', (e) => { ... }) -element.addEventListener('mouseout', (e) => { ... }) - -// 焦点管理 -element.focus() -element.blur() - -// 类名切换 -element.classList.add('active') -element.classList.remove('active') -element.classList.toggle('active') -``` - -### 使用方式 - -#### 启动服务 - -```bash -# 方式 1: 直接启动 -python main.py - -# 方式 2: 模块方式 -python -m oss.cli serve - -# 方式 3: Docker -docker run -p 8080:8080 nebulashell:latest -``` - -启动后: -- WebUI 自动在默认浏览器打开 (通常是 http://localhost:8080) -- TUI 在终端中自动渲染,显示相同内容 - -#### TUI 交互 - -- **方向键**:导航焦点元素 -- **Enter/Space**:激活按钮/复选框 -- **Tab/Shift+Tab**:切换焦点 -- **q / Ctrl+C**:退出 TUI (WebUI 继续运行) -- **鼠标点击**:如果终端支持鼠标,可直接点击交互 - -#### 访问 /tui 接口 - -WebUI 需要开放 `/tui` 路径提供特殊 HTML: +#### 技术要点 ``` -GET /tui/index.html - TUI 主页面 -GET /tui/*.html - 其他 TUI 页面 -GET /tui/*.css - TUI 专用样式 -GET /tui/*.js - TUI 交互逻辑 +终端控制: + raw mode ─── tty.setraw(),单字节读取 + ONLCR ─── 重新开启 \n→\r\n 映射,避免阶梯乱码 + SGR 鼠标 ─── \x1b[?1000h\x1b[?1006h,解析 \x1b[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 Pk5$}egZ3jm1M&X?KtX&l delta 1362 zcmZuw`%6<%9RHqk?{@FrIv43ycI{!UGz*oYr(lISwAfWBCXS`kjJDNTD=J7UN_#-j zMUd!+`mKH`A}Fcf>K_Oy$fZFvi+)?#FZEO3bKGs~ap0Wq=YHR>JGJ|X_u!n%Wd}Uo zj@;0+1n`l6n1>d?Tn@oF=)gcHIt>v<1f`53l|oXx%w&zrJR1JDa z*lTKqea?2+-KIoDZz{N!JLfGI1G{?%`8E0aKrBcTl282z0DxGfNx9{-K+9M~k5>m#S$!}VL9RMSc zw}QpSFiZF&8w)Z-Ht#X7UfL^-Lxg82j@g<7?V`pVhp$N|5uUX;ZqR9?)Lc;3Ai}c~ z=eL>EC53Igqc{hI4i0fV^UiAZ660>#Ej3YH_QN4H2@$91D(vL*vKJY~dev9i4b6p( zz_5SmZY*JxnQn@-)U>+PSS-d`Gj$xFwFV+S(@=p}6##CNMYo0=Nm32nBwS@YR4Uq( zVhcJd00v;Iv;+o7P|cL1_3ZTrvzPB`#o_*R6tR}!J=5lO9(6ffgxHJ&&RVj_xa8bW zBc{c8rJY}Rn}aUKyU*PG;X^ZmuD$zZwt#>06=E147*9IQ6*aA;h1n2fE$KKr9%nU( zc~8=J@%b?V->u*%zp`qi^{uRoww-MoJAG^OME&g(FI^3<<#onA_f^O9#s#f5cwt-C zT4%I)62xiT_f%GkO`K0p_*c+*dMK4-E6{Ph@xkL84%~`NbiDMeej~5>ck6gfV(Tgq z{BPs1{}3ddK_d3}?{IP=Hk4#5(Y6sUZa81?X<50jQ-sQ@(Wu5%qS0VkR*6RYQ+?4W zTZU@Pk55*@pT&4nvtigW$WoajB1Tr}OC=JqWWVT=C`3fy&XTc293AjDB}J=S5~=>o z;rKST4h4Kb=`;A`B|^v)l)VGzJ8;cHU 1 and sys.argv[1].startswith("!!"): if _ACHIEVEMENTS_ENABLED: diff --git a/oss/config/__pycache__/__init__.cpython-313.pyc b/oss/config/__pycache__/__init__.cpython-313.pyc index 90f6dc88d9ce84fcf07a802effff1f71240d327c..f80c3f40520902a356937751cc53eb9493130c3e 100644 GIT binary patch literal 289 zcmey&%ge<81e;C2X0`z7#~=<2FhLogRe+4C48aUV48e@SOx}!MOhrsy%tg#zEJZ8| z4EhYgOo5C=tm({}ETud^r7v51pYL7wY{kN-bEm%qnW4#ii_JMdFD*0u7FT*|NqjPd z&7GN-3FG)_vfW~jk59=@j*q`3m|tA12T=s2ikN}M6tRE^Rv-aVnwXOlAHR~}Gmy)0 zOGCdXKfgrZFEyz&Cowo9H77?OXofz-4E^|cuubuL1(mlrY;yBcN^?@}ia<_f1ma?0 dAn}2jk&*ExgV22j+lvggkGKpQ*^59S0|0b*QIG%t delta 117 zcmZ3;)W;b5nU|M~0SL@wE@d(T>Bk@r41fec;4=%5n97jOpvmaBlA(wRBm@$_rK_Kj tpPQ;*l9^Yj?~+=aU6Nm*?;jjI@s^S}D^MP!zZk^$$jr#dSi}ru0RZXZ78w8l diff --git a/oss/config/__pycache__/config.cpython-313.pyc b/oss/config/__pycache__/config.cpython-313.pyc index aa7175f55e867ca89aa9294e1137a99aabf13cf6..dc67f36eb18de3841d00243645eca92ad9c9a27c 100644 GIT binary patch literal 7027 zcmcIoYj7Lab-oKM9t(g3L=YtS25CwpED@AQk&#?c2@OIbMG~a3fFuv;ZXj?;K?VWp zE@)e>r(v5orW@CS(%6RDCZ?TqEKL&GnRc2?ZD$gzKl+m}7V;Pj{jv9A^(LRS~3)v?PXwIB0C9B zDB&4iH_A{($#ql*xqeiCjHRpxb?m6&n2{QfnW)J_+@y{0hI+yq`*dns)XeM7a?_@1 zGd4bL;Y}To^X6&uejU%%lQS%}@;2TAC2Uwi(XhM~HD?%FF{krZEbaxx&1e7i#=BQ; zTzTzA=9``_&-L%TdHsdIy7A(hTd#k8>wNa+bLY3d{eoKN`peHz2j_DnsselW575TBcJD*7(Vt4r#^-LD!PLcJAY0E~+WN^- z=>oA>uDz4pdUpB7zrFF_@BDq~D)Aill&%u|d^`Kml^<*^y<<5dR}6>4lfvNSh%h-3 z3db`ogcM|9|D-DWHpyQK@Srd>5_;O`>z;|GqjJ^cSmfwPP~gK8 zp~0g8Mdwm=-3y7uxp=ZmN~dUaPL!+ok)y%Ea3mBEMgx!XO3~&*Q7Jx`jHVZ<*!6@+ zVe{a`#V5hB2y5` zYKo9hX}N;5ub_{RL{WRvk-!#|6k$3GTyzH^&<0odEXg8_)`|y9SW8p|RtqN&nIo5( zpw}p~Gx1m&CNOYhFftbAWs{%|NVXLiWn{8lNTi}O!YobAtAffBftY*(0F9Nz#4K%s zo+KQB$0P6Rrm0lAJ1Cx7Ohoz9Vj`hTlG;JHn)wz01qC6VjHd-bw)+)t`ZNI|t#lB` z64~t7eQD^;@yp|D9S<$DFW5F3JAP%Z`-73#YBy|-b(<$=^Q<)d%GQ-P60Tz1+>kRj zd|+xTVY)4I+6N?k_A|cIW3Smr15!{n> zy#AnGN0Ry}qb3g_LfnzsNJOtqT9O4!ho81rdB_R&ev{}V!|eb$VE@ALlanFa9ZkdM zpTU0y^y4*zocwC2^6nB51MYw0y9luo51DdkC5mGMcxi?`lsArY$1F+MQzND}qIn=` zj+jHWTAj~wO%KbA#a}DgL$nrWV4=_IJfsEs^N=7N(6^9Trj<;&wfZI!1+Qa?Id+!Z zQ|}`r$xStAHF(n$t~(_~IL)7-W=-~4UI{yTgsUXItX2y9;n^fN$jyQ8F`s3$UcKBR z_Hpg)7d^-LiC`I2Z~e{6NAJEXGxTZ0VtTgg0Br4MF&RtE#FKMDiV*;kia9YYJP}RA z0fkd^5vGewsZ0DZkJ>;f`fvT;h!;#Q-vW(b(y@_=GqwCesq_ z#wyiR1kH>V>+@j#>^YeWJQWj_lZk9tpz&lHF!S7kh=H1n&WnQJwNO|xgzm;RGzi2P zlbKoBidd+@h#ocw*#ItBgv|m=b47Q^j40vZ&r?hk7b8V4$u$}hl;aMrXbI0!OXONj zeRlufomsYSSZmg;O*w1Ry0s-|ZOQ8#6~~!PS6z1ZiuBfr_gw?O*s
kaSg-b;hu z-;){rL{DlCf7sTw-qxFI>-}|G--Tlt|3+>7tMeDxA_3kv^H3c+Y!xB7RGAJ;y-SN}`)L12H`(**QAN0Xl+@9nibeAx8fLne@1 zWi0+m{Z%W2ddzyrN&Yp#Eix52yi{RcCL>M#_Aho!&Mf6ie*wbKB1DYtr1OT}G zv;H+sgC|~x%L)1_!k$(Fq>KWL^AND5y}=blO?UMMKmnV8Q`1Lu8o=DQ{qF)Wd+yW! zk5yovLx8N-+6I6GKO=CBzYAbmfJm=?(FG*{tV$1v(7((MkDVFo}WGXhD zP1*S$h1Z=uIcLvC=e{@Xm+fnv2UiCk$#p)G8G2>x!q|#)t$OFCt7WC)N7A~hH|Ofj z>-4Us%{|?3aX$=Z*&+($&BWVNtnvE>Qr-DKx2s9xE+xR_LV*8K!o~4kv)gagUA0>L zb%v{M1}Ig$Vk)>oHc}Bz(8(Cao0iZqO8gxF=Km9aC#$q|*bdvxBChq1Ui|6S`Ipsg z1#S^uoib%~6jWaQXVXN(Ads-r;)Gxk*;1GgSOllA1n%xI3wG!EXP$p%-QJS3x2)*b z>|LwoF8Uzo=vRQ04F=m(27{WH$Vtb&zyt#?3g*J%hmgNBslhkL%rRb_I;bl@hud~> zP(c_~aoZ~!)mNaYvQf!;372zWRKU+-2|O*psH~N*bY19Lv$kw_cAg)5ek|*J-`14Z z1B@$UE5pd*u=YURvIFCN&-2Q>;KESn<@)nW*Z=W(bs#VZ4=9xRxy&e2BOxlQ@geJ@ ziA2$G;2#H-1ZRIR{F>dVF5xwM>#Di+ZexoJst3w{zOlRltQuf}$+{rL`^9A)oq$YT zLDRsM+E;Npon8>43vpo~MbmP1!Nq0L6x7*3|%Rc8Myhb|mivFDt7*X;XN&HKI( z!|_N#zl!AE7N=ti_pn%#N?0+51Xp+23iuN^6}OVh@b)96RV2O3MD}z_O3PNwm+#76RG=8@&>#RG4^Z;8ds*6$tL|E}`&P}qFT}C9h11jbv^0^L6B2MZ zPsp03MakDoE!CpPnsa`UKE>M@0Mt0z3J0mXUF%p7>3m$0;Q3OBofcz{%e4g$->G;G z%~$u4B5<4~oI8Y>M5+W#%wsh5BYwqdp7?i ziPWiRqHu$z5m+cO_!ko%m~b6#3?_K-(w;&9cK&@D!Af##--i2ffK4!#}2Ri!KAjrVK5ZAYj#9WgzHEq_l z{DXVBa?@G|D_O0IN3AlzM*|h}+kq)Rn`kGHv&<9@Qb~%*c(K`9{_fTr-`aZN2!e8-~ia}_>onAE`QsM0|T?Q@M zF5!&B?Vn*}(*Ik+{gzmNN8JBOJf9c`bxi*)f;4}OCGDL{l^JukCg|3*cz)YEu{{`zv88H9= literal 5864 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 Z=vVkt+1buUX7maJKePJ3@P|&we*nNd37`M~ diff --git a/oss/logger/__pycache__/logger.cpython-313.pyc b/oss/logger/__pycache__/logger.cpython-313.pyc index f9982f606cf8417c0efd610b2c3deab992917c74..5587c20c49a5c512ac17243729516a30e468c864 100644 GIT binary patch delta 44 ycmaE%@KS;MGcPX}0}yOB{koA`m03tlzbHSyMBgtpsWc}sI3qPDXR{--Ixhe>aSh1; delta 43 xcmaE<@Irz6GcPX}0}u%LKH12v$}Fg=UzDF;qMue;Qd*R%n_pbK*@;=57XTTU4Ez89 diff --git a/oss/plugin/__pycache__/capabilities.cpython-313.pyc b/oss/plugin/__pycache__/capabilities.cpython-313.pyc index c3efc938cee7ee55eb2d4809549d2187297ffe3a..902c4179e59b9e6027b6433fff76ad725fed5f44 100644 GIT binary patch delta 44 ycmZ3iutb6TGcPX}0}yOB{koC+1GkWleo=mYiN0TIQfW?Na7JoQ&SoASaTWkIQ4P}o delta 43 xcmZ3YuvmfnGcPX}0}u%LKH13qfm={pzbHSyL_e*xq_ikiH@~=eGcS)g3jh}>4F&)J diff --git a/oss/plugin/__pycache__/loader.cpython-313.pyc b/oss/plugin/__pycache__/loader.cpython-313.pyc index 87be3c261873a395544571d73aef6b373686348b..933d7bc060dc99e49fbfd7bce992e3d4afe11b58 100644 GIT binary patch literal 4789 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 diff --git a/oss/plugin/__pycache__/manager.cpython-313.pyc b/oss/plugin/__pycache__/manager.cpython-313.pyc index 31b8ea72803907274841d01552912042f6078db3..ab9b204e10f52a5286b288dd9d28de8af633d681 100644 GIT binary patch literal 4258 zcma)9eQ*=U72ngHJ}tx9mi&b=XB*?F!7?8vtrH5!!Avm3A(oxMIGKwqpN)!qQuib# zq|>RH1Tcv+nD3830!?XLIv83KO2Cki@Mr(92vp|kHq2nj=AWWsGBD(yzP;1QvN3H} z_TKI8dvAB&{@!ojjb~b9!$EJK#irmIRe6Y}cvS`^U#FU*>}TaEDK<^ang` z0=_rU6Wexvv8C0+9ht0O|Jw)zJTcQ zJ1E(@PO-mMF%Rv`4k5&E_V|5mo=^Zct@5fHQEMvM7+(M{`_UT+Q{W|?o5HkPhZ#3r zh_K$xKy7gAp*Fe=P@CLFZ0;~Q%yOnW*~=c$(;?uyZ~!Qr1xDi^ZyoLLn&JhEFS9e?TVe<73(<=KYk(h)w}T%+v6Xfzti>MSpSu={u8mie~z90{k?%BmP#&N z0DL(1`6pwae>$BflRhFUiYt?DRSx3bK=(^cP3!J-ZGBy6bqBnggb=*1^hg_910LMQ zjh^0lZ(!%$cQ21V#q;r-YEF}zpmoqht9H2f#M-jW(yLPnu9BY&#TO`6ILb)n(h(!F9g(~~hYe?d2M(^NPP-DuJ#N$@F~{42cdhfOpRktwB- zNK7eiYDseQBGpVilN|Lxm^VZ4l8vW3q{X1ArzB@12;~;D)~`8L)HUlGGqm1`r<>O1 z<4_0F!IYpDt2PQ@4lCr)92iN3xITOt#z!t~n;wQLq7~d(EUKH6!JdTZ$iNX5#suLD zoq$&YuBRYs^rQaR<^7gLPEMUI3GUuAqxKf1V%tu}k6w&*pSQe|giLZ_vdJe(A&=-4WJ{YM1o<}KW&t-ijF|XK zW(lx(F94ob_k)RpWIOMA@uk&uuhqR=*U;qV>l$3OYwK2Gav-uHEczkNWOLoyUO^!z z0Bt52F$VpBv-W^&PLYyCP{45S+Jyh~cUty%Ax_jjfYw<$^vX)%W;1W6}(>`3}_~?~h^AKAQWw{8; z9WT1cIuk~ac=W)8Y_rD^m1)1l+7dK8A4^#<&zG}Yx#()H#Z^XMEv2BI9^my5op8I5 zn*|ZbHl|`1Y$Qt>2vK7ynzd1cQsAv7@SaYhksyCc(X4V$(=kp_fM%M`e_d&_t`b7d zO;^IaBHd!rETvX@SP8RKKxuYGrkQCZCq1P!r-fO|Ol@%by}B9gzD>>MLFj*JHL{x3 ziKkotgryh25@9i=Oh5m0mKtrE&cstA4p9$#CiFO^H0w19pcb9_1A58V#M0asWF`6-6(iB@k)0%$V)*%Xp@Z_J(4FB!nhdZWZEx? zcn*2XR)tmjz!hOJgvmY~1|{q{BOZ1}rvWuw;Oh_yIsEsI#o zZdl9Hdgex~b8lGZB@8HU)m#|&vvVW%=ejN5XK)ICm?!f`WrVHl`^62m z=79m(W*w@I&Z>;es_fem&8dmx)C_cd-4K+w7=qLBzJ;Y|%~IvV+R-(;Wo0RSt%QPl5=D{lC)_%I2q@`kcW^4iR+51J|L%z4Qr5FO@F~$XUb0<1LfI;!O@x zgLwB}MlPI=A2_X=9s6p>Xx|4aB}}W5oL$LU;s?*ic3ix>?=a*i@93WA2AI z1+mRxO>zb#z(F}{jj%;Yp;wC`0poBGXe5Yz$=H|8A?)!AtsY1On9!D>Kypmc(qR@myP%%LZmC%}+E0C4dL%(zhsxFk` zfrQSOId6!yMcL8_TN-66B5cJC*74AW*z)T$=O%Retb$>#^3$2Ut8bHNoFB2x?|bv6 z?S+IHBoYP4TKtFvd^t%KYN=t?KA5}WZ>(!Lr)V6}ne+b6a-=VR#Wma4o^SJ_u2&FxLD z=jrV~EVAhfR6Qcy;K;zw!&5c@N_=7mCZvVQNy}OAvEo#32B-Rcqkj9?Nk2(RogVK- zpRifz6vU9ETwDsE z?-3V6KS>xEs`MYs;-MmLkSTa*Sw~R?_YtWd0V+&6aAH*HP*KTsrhq7xK9)#oyvJ0I NPMQ|bXQ&1Ae*k8Xb|e4* delta 930 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 delta 2584 zcmaJ@Yit}>6~43c+=rjL>s`Osu@h(VGD#d7HW3jtv{A^eXojtf;?|jXC+m&u+0~ud z7DYm~0tFkjO3JNaRaL6RL$Xyw8}TEi1Q5UA2Lh~Bn(h3-5^+o5&lU$F{s8C9>}K7l zaHak3-1EHmoO93J^<)3oKf2x05+E=-Q}^<(w-NGZTr`GK|Jc6z>S+Iboa`bR-$OLv zn9pNSQ4=R6O@g01DUVB<(nGEYRM|oGPXc$`CxQa-{VqS?S_*)r;3G@HTu5yx#X)TA zrJMKOed{l`-v02ln_IVjwDr=ht#AF}gXMSF7?+rzuJ6d5r21m}5PU3?NkTb|P@l$8 zUh`2w<0nN;fFJA;d`qrxS(B-fQ&hj>|GaJ)=L;p;*^W6_` z{@jt~411=Kb%dgB&mXa3~Z|dV1ZP-`d$uJBk`C0)!4u* zp=}@%aD&F1M$@XF{g6KuqmZ_haVtYJES!z|4iRJRR>znU+q z3hjeqLJpo$4jNqE;ffBYTX|1TOkFI~Maw1jPn_>z7x4AIu4Rv?lhHNjBPeHadu3ixkql|Ba{m~zX zOb1vt7{QDSChyh5PU0R@xPaB^j*0O`mw+_xX{PbK%vY@S2FmOeLeZIwQRPIl}%B+82Ed3P%N9rJ<|&&Ij3VThthT5H2p-Ym|`b zpZHEjW=iX?<|*D8ngF;DrzB}Q1gJ`M7ff^*0o_xm2SRrPIKq`e-c$qitRY1z>QtYz zD3&{g9T0i|;UMw?E^o@zA?5p!;HSo%o;UMm&cbeTsgdMF!b@}5_^M6yY1>USfNS{} zNS=V7wFvQ( z?kOKGA1Ox{&sK+a-AGnK10TtxW2_b;k)f5&jqu3Ax&Kuy!|QXEq}=E#NA4)2)Z5pCNHqKj23cP;9;E2#X#|v=e&!S8fayqCW7dE+ z)3L*~M|uI+k2#N*$L}br7wCSl+RZ{;zvhwys};w_8nou=w3X zx?mcCv?@}}g7;!!SMuVA^$IM^c=IuHM7I>^5ujRlhatF_CAJdV!(C;+iG6wAeVdZN zN~>WSLG36)7eWjH|5LbyL%Z<{Q$#U)^a}`Q5z+|f5wH*((JmF?iN}|~5wiJdn?8%Y z9)ws$%f2u!O_y3;$S*Td`6>+zfE2S|N*n%mDs{tn_1_hkPC6!Thr diff --git a/oss/tui/client.py b/oss/tui/client.py new file mode 100644 index 0000000..994c9d6 --- /dev/null +++ b/oss/tui/client.py @@ -0,0 +1,491 @@ +"""TUI 客户端 - 前后端分离的 TUI 前端 + +通过 HTTP 连接后端 nebula serve,消费 JSON API, +直接使用 ANSI 转义码绘制专业终端界面。 +支持鼠标点击导航。 +""" +import sys +import json +import time +import tty +import termios +import signal +import socket +import urllib.request +import urllib.error +import shutil +import re +from typing import Optional + + +# ── ANSI 工具 ──────────────────────────────────────────── + +def fg(r, g, b): return f"\x1b[38;2;{r};{g};{b}m" +def bg(r, g, b): return f"\x1b[48;2;{r};{g};{b}m" +def bold(s): return f"\x1b[1m{s}\x1b[22m" +def dim(s): return f"\x1b[2m{s}\x1b[22m" +def rst(): return "\x1b[0m" + +C = { + "header_bg": (30, 30, 46), + "status_bg": (30, 30, 46), + "accent": (0, 255, 135), + "green": (0, 255, 135), + "yellow": (255, 220, 80), + "red": (255, 80, 80), + "cyan": (80, 200, 255), + "dim": (100, 100, 120), + "white": (220, 220, 240), + "bar_bg": (50, 50, 70), +} + +# ── 鼠标转义 ──────────────────────────────────────────── + +_MOUSE_ON = "\x1b[?1000h\x1b[?1002h\x1b[?1006h" +_MOUSE_OFF = "\x1b[?1006l\x1b[?1002l\x1b[?1000l" + +# ── HTTP 请求 ──────────────────────────────────────────── + +def http_get(url: str, timeout=5) -> Optional[str]: + try: + req = urllib.request.Request(url, headers={"Accept": "application/json"}) + with urllib.request.urlopen(req, timeout=timeout) as r: + return r.read().decode("utf-8") + except Exception: + return None + + +def backend_alive(host="127.0.0.1", port=8080) -> bool: + try: + s = socket.create_connection((host, port), timeout=2) + s.close() + return True + except OSError: + return False + + +# ── 布局工具 ──────────────────────────────────────────── + +def term_size(): + return shutil.get_terminal_size((80, 24)) + + +def hbar(width: int, percent: float, color_fg=(0, 255, 135), color_bg=(50, 50, 70), char="█"): + filled = max(0, min(width, int(width * percent / 100))) + empty = width - filled + bar = fg(*color_fg) + char * filled + rst() + fg(*color_bg) + "░" * empty + rst() + return bar + + +# ── TUI 客户端 ────────────────────────────────────────── + +Page = dict # {"id": str, "label": str, "desc": str} + + +class TUIClient: + _resize_flag = False + + @classmethod + def _sigwinch(cls, sig, frame): + cls._resize_flag = True + + PAGES: list[Page] = [ + {"id": "welcome", "label": "首页", "desc": "系统概览"}, + {"id": "dashboard", "label": "仪表盘", "desc": "CPU · 内存 · 磁盘 · 网络"}, + {"id": "logs", "label": "日志", "desc": "实时日志输出"}, + {"id": "terminal", "label": "终端", "desc": "Shell"}, + {"id": "plugins", "label": "插件", "desc": "插件管理"}, + ] + + def __init__(self, host="127.0.0.1", port=8080): + self.host = host + self.port = port + self.base_url = f"http://{host}:{port}" + self.running = False + self.current_page = "welcome" + self.width = 80 + self.height = 24 + self._stats_cache = {} + self._stats_time = 0 + + # 鼠标点击区域: list of (y, page_id) + self._click_zones: list[tuple[int, str]] = [] + + def _fetch_stats(self) -> dict: + now = time.time() + if now - self._stats_time < 1 and self._stats_cache: + return self._stats_cache + raw = http_get(f"{self.base_url}/api/dashboard/stats") + if raw: + try: + self._stats_cache = json.loads(raw) + self._stats_time = now + except json.JSONDecodeError: + pass + return self._stats_cache + + # ── 鼠标事件 ────────────────────────────────────────── + + @staticmethod + def _parse_sgr_mouse(data: str): + """解析 SGR 鼠标事件 \x1b[ 1024**3: + return f"{b/(1024**3):.1f}G" + if b > 1024**2: + return f"{b/(1024**2):.1f}M" + if b > 1024: + return f"{b/1024:.1f}K" + return f"{b:.0f}B" + + # ── 主循环 ──────────────────────────────────────────── + + def _navigate(self, page_id: str): + self._stats_cache = {} + self._stats_time = 0 + self.current_page = page_id + self.width, self.height = term_size() + self._render_all() + + def run(self): + self.width, self.height = term_size() + self.running = True + self._render_all() + + signal.signal(signal.SIGWINCH, TUIClient._sigwinch) + + fd = sys.stdin.fileno() + old = termios.tcgetattr(fd) + try: + tty.setraw(fd) + # setraw 会关闭 ONLCR(\n→\r\n),重新开启避免阶梯乱码 + attrs = termios.tcgetattr(fd) + attrs[1] = attrs[1] | termios.ONLCR + termios.tcsetattr(fd, termios.TCSANOW, attrs) + sys.stdout.write(_MOUSE_ON) + sys.stdout.flush() + + buf = "" + while self.running: + # 终端 resize 检测 + if TUIClient._resize_flag: + TUIClient._resize_flag = False + self.width, self.height = term_size() + self._render_all() + + ch = sys.stdin.read(1) + buf += ch + + # 检测 SGR 鼠标事件结束符 M/m + if buf.startswith("\x1b[<") and ch in ("M", "m"): + ev = self._parse_sgr_mouse(buf) + buf = "" + if ev: + button, mx, my = ev + if button == 0 and ch == "M": # 左键按下 + for zy, page_id in self._click_zones: + if my == zy + 1: # 鼠标坐标 1-based + self._navigate(page_id) + break + continue + + # 非鼠标序列 → 重置缓冲区 + if not buf.startswith("\x1b"): + pass + elif buf.startswith("\x1b[<"): + continue # 等待更多字符 + elif len(buf) > 1: + buf = "" # 其他转义序列,丢弃 + + # 处理单字符输入 + if len(buf) == 1: + c = buf + buf = "" + if c in ("q", "Q", "\x03", "\x04"): + break + elif c == "1": + self._navigate("welcome") + elif c == "2": + self._navigate("dashboard") + elif c == "3": + self._navigate("logs") + elif c == "4": + self._navigate("terminal") + elif c == "5": + self._navigate("plugins") + elif c in ("r", "R"): + self._stats_cache = {} + self._stats_time = 0 + self._render_all() + except Exception: + pass + finally: + signal.signal(signal.SIGWINCH, signal.SIG_DFL) + sys.stdout.write(_MOUSE_OFF) + sys.stdout.flush() + termios.tcsetattr(fd, termios.TCSADRAIN, old) + print("\x1b[2J\x1b[H\x1b[0mTUI 已退出\n") diff --git a/oss/tui/converter.py b/oss/tui/converter.py index bf808c6..bf62fff 100644 --- a/oss/tui/converter.py +++ b/oss/tui/converter.py @@ -1344,6 +1344,14 @@ class TUIManager: else: self.show_error(f"Page not found: {path}") + def render_page(self, path: str = None) -> str: + """渲染指定页面,返回终端文本(不写入画布)""" + path = path or self.current_page + if not path or path not in self.pages: + return "" + html = self.pages[path] + return self.renderer.render_with_frame(html, title=f"NebulaShell - {path}") + def render_current(self): """渲染当前页面""" if not self.current_page or self.current_page not in self.pages: diff --git a/oss/tui/plugin.py b/oss/tui/plugin.py index 281197b..9c04535 100644 --- a/oss/tui/plugin.py +++ b/oss/tui/plugin.py @@ -620,6 +620,11 @@ export default TUI; }) ) + def wait_for_exit(self): + """前台阻塞等待 TUI 退出(用于 CLI 模式)""" + if self.tui_thread and self.tui_thread.is_alive(): + self.tui_thread.join() + def stop(self): """停止 TUI""" Log.info("tui", "TUI 停止中...") diff --git a/pyproject.toml b/pyproject.toml index 14af1d9..ce5bd2b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ dependencies = [ ] [project.scripts] +nebula = "oss.cli:main" oss = "oss.cli:main" [tool.setuptools.packages.find] diff --git a/store/@{Falck}/html-render/__pycache__/main.cpython-313.pyc b/store/@{Falck}/html-render/__pycache__/main.cpython-313.pyc index 108a6d0e131ed55e0205180e32efcd10f9056694..45eea58f5537a90134c5a2a754e8732ff934d79a 100644 GIT binary patch delta 44 ycmZp(X}972%*)Hg00f&&zi#CI&Lm`_UzDF;qVJcQRGO0*oRON7vssw=vlIX~9}dR= delta 43 xcmZp-X|v(}%*)Hg00ct5Pd0LYXA(5lFUrp^(N8NaDJ@FX%`YzAEW-R*3IG`M4f+58 diff --git a/store/@{Falck}/web-toolkit/__pycache__/main.cpython-313.pyc b/store/@{Falck}/web-toolkit/__pycache__/main.cpython-313.pyc index 5beadfbc7b38c730beffff4c42e550153a66576c..bcf5ae15515035bb27523de695fed539b7403288 100644 GIT binary patch delta 44 ycmaFi_tKC1GcPX}0}yOB{koAmomt34zbHSyMBgtpsWc}sI3qPDXLB?2Q)K{A84rs9 delta 43 xcmaFq_rj0+GcPX}0}u%LKH12f&MauGUzDF;qMue;Qd*R%n_pbKxrOhN delta 43 xcmeyy@s)%7GcPX}0}u%LKH11!&Ln86UzDF;qMue;Qd*R%n_pbKc?#1bMgSru4o3h0 diff --git a/store/@{Falck}/web-toolkit/__pycache__/static.cpython-313.pyc b/store/@{Falck}/web-toolkit/__pycache__/static.cpython-313.pyc index 340e3f5a3cdab54a7cfa9a1cf474213bd9b916ce..46a6446ac8ba8f0c6afaf411c52640dcbd40b8a1 100644 GIT binary patch delta 44 ycmZ24vq6UYGcPX}0}yOB{koC+7Nd}veo=mYiN0TIQfW?Na7JoQ&gO57mOKDCj1JZS delta 43 xcmdlWvtEY#GcPX}0}u%LKH129i&4;2zbHSyL_e*xq_ikiH@~=e^LIu|9snC^4haAN diff --git a/store/@{Falck}/web-toolkit/__pycache__/template.cpython-313.pyc b/store/@{Falck}/web-toolkit/__pycache__/template.cpython-313.pyc index 23ad4ed308287541def98346e67b76e95441252a..b77293e7acaf4e3d6b169addc907b198f29d388b 100644 GIT binary patch delta 44 ycmbQ5J}sU5GcPX}0}yOB{koA`hgrx%zbHSyMBgtpsWc}sI3qPDXR`;huK@rxx(*ru delta 43 xcmbQ1J~f^DGcPX}0}u%LKH12v!z^g7UzDF;qMue;Qd*R%n_pbK*^}AV000-@4NU+5