14 KiB
14 KiB
NebulaShell AI 开发文档
架构决策:
nebula cli采用前后端分离设计,TUI 前端直连后端 JSON API, 不使用 HTML→ANSI 转换引擎。详见下文 TUI 架构决策。
项目介绍
NebulaShell 是一个企业级插件化运行时框架 (v1.2.0),核心理念是「一切皆为插件」。它提供了一个最小化的核心系统,仅负责加载 plugin-loader 插件,其余 26+ 个官方插件均由该加载器管理。
核心特性
- 插件化架构:所有功能均通过插件实现,支持热插拔
- 隐藏成就系统:通过
!!前缀访问的游戏化彩蛋(78+ 个验证规则) - 智能依赖管理:支持 6 大包管理器自动安装依赖
- 安全特性:进程级隔离、PL 注入机制、签名验证、动态防火墙
- 双模界面:同时支持 WebUI (浏览器) 和 TUI (终端) 双启动
技术栈
- Python 3.10+
- Click (命令行框架)
- PyYAML (配置解析)
- websockets (实时通信)
- Rich (TUI 渲染引擎)
- 纯静态 WebUI (HTML/CSS/JS)
TUI 架构决策
废弃方案:HTML→ANSI 动态转换层(v1.3)
已废弃。 早期方案通过 oss/tui/converter.py(1430 行)在运行时将 WebUI 的 HTML 页面解析为终端元素,存在以下问题:
| 问题 | 说明 |
|---|---|
| 布局失真 | CSS Flex/Grid 布局模型无法映射到终端字符网格 |
| 交互断层 | JavaScript 事件系统只能在终端模拟,与真实浏览器行为不一致 |
| 维护成本高 | 1430 行转换引擎 + 每个 WebUI 页面需维护 TUI 兼容标记 |
| 渲染性能差 | 每次导航需对整个 HTML 进行 DOM 解析和布局计算 |
| 调试困难 | 终端渲染错误难以定位是 HTML 问题还是转换器 Bug |
当前方案:前后端分离,原生 ANSI 渲染
nebula serve ─── JSON API ───→ nebula cli (TUI 前端)
(后端) (原生 ANSI 终端渲染)
后端职责(nebula serve):
- 提供 RESTful JSON API(如
/api/dashboard/stats) - WebSocket 实时推送
- 不感知 TUI 存在
前端职责(nebula cli):
- 通过 HTTP/WebSocket 消费后端 JSON 数据
- 使用 ANSI 转义码直接在终端绘制界面
- 不依赖任何 HTML/CSS 解析
技术要点
终端控制:
raw mode ─── tty.setraw(),单字节读取
ONLCR ─── 重新开启 \n→\r\n 映射,避免阶梯乱码
SGR 鼠标 ─── \x1b[?1000h\x1b[?1006h,解析 \x1b[<button;x;y;M
SIGWINCH ─── 捕获终端 resize,全屏重绘
ANSI 绘制:
24-bit 真彩色 ─── \x1b[38;2;R;G;Bm / \x1b[48;2;R;G;Bm
字符图形 ─── █ ░ ▓ 等 Unicode 块字符作进度条
光标定位 ─── \x1b[{row};{col}H
页面导航:
热点区域映射 ─── 预计算每行点击区域 (y → page_id)
键盘备选 ─── 数字键 1-5 作为鼠标候补
开放代码(opencode)TUI 架构分析
opencode 是目前最成熟的终端 AI 编程助手,其 TUI 架构可参考:
| 特性 | opencode 实现 |
|---|---|
| 渲染引擎 | 原生终端渲染,无 HTML 中间层。直接操作终端缓冲区 |
| 组件系统 | 类似 React 的组件化方案,但所有组件直接输出 ANSI 字符串 |
| 输入处理 | raw mode + SGR 鼠标 + 组合键解析,支持 Tab 切换模式 |
| 布局 | 弹性布局引擎,组件根据终端宽度自动折行/折叠 |
| 状态管理 | 全局状态树,每次状态变更触发受控重绘(非全屏重绘) |
| 会话管理 | 多会话并行,每个会话独立维护上下文和渲染状态 |
| 主题系统 | 完整的配色方案,支持暗色/亮色主题切换 |
关键差异:opencode 不使用任何 Web 技术栈做 TUI,其所有界面元素(输入框、按钮、列表、状态栏、侧边栏)都是直接通过 ANSI 转义码在终端绘制的。每个组件是一个纯 Python/TypeScript 类,render() 方法返回 ANSI 字符串。
对我们的启发:
- 放弃 HTML 转换层是正确的方向
- 直接 ANSI 渲染的架构更可控、性能更好
- 需要设计自己的组件化终端渲染库(参考 opencode 的组件系统)
nebula cli命令已预留,后续在此框架上构建原生 TUI
插件开发指南
插件基础结构
所有插件必须继承自 oss.plugin.types.Plugin 基类,并实现三个核心方法:
from oss.plugin.types import Plugin
from oss.plugin.decorators import plugin
@plugin(name="my-plugin", version="1.0.0", description="我的插件")
class MyPlugin(Plugin):
"""插件类"""
def init(self) -> None:
"""初始化阶段:加载配置、注册路由等"""
self.logger.info("插件初始化")
def start(self) -> None:
"""启动阶段:启动服务、连接数据库等"""
self.logger.info("插件启动")
def stop(self) -> None:
"""停止阶段:清理资源、断开连接等"""
self.logger.info("插件停止")
插件目录结构
plugins/
└── my-plugin/
├── __init__.py # 插件入口
├── plugin.py # 主插件类
├── config.yaml # 配置文件
├── routes/ # HTTP 路由
│ ├── __init__.py
│ └── api.py
├── tui/ # TUI 专用页面
│ ├── index.html
│ ├── styles.css
│ └── interaction.js
├── webui/ # WebUI 页面
│ ├── index.html
│ ├── styles.css
│ └── app.js
└── utils/ # 工具函数
└── helpers.py
插件装饰器参数
@plugin(
name="unique-name", # 唯一插件名 (必填)
version="1.0.0", # 版本号 (必填)
description="插件描述", # 描述 (必填)
author="作者名", # 作者 (可选)
dependencies=["plugin-a"], # 依赖插件列表 (可选)
optional_dependencies=["plugin-b"], # 可选依赖 (可选)
min_core_version="1.0.0", # 最低核心版本要求 (可选)
tags=["category", "type"], # 标签分类 (可选)
enabled=True # 默认是否启用 (可选)
)
注册 HTTP 路由
from flask import Blueprint, jsonify
# 创建路由蓝图
bp = Blueprint('my_plugin', __name__, url_prefix='/api/my-plugin')
@bp.route('/health', methods=['GET'])
def health_check():
"""健康检查接口"""
return jsonify({"status": "ok", "plugin": "my-plugin"})
@bp.route('/data', methods=['POST'])
def receive_data():
"""接收数据接口"""
data = request.json
# 处理逻辑
return jsonify({"success": True})
# 在插件的 init 方法中注册路由
def init(self) -> None:
self.app.register_blueprint(bp)
创建 TUI/WebUI 页面
WebUI 页面 (webui/index.html)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>我的插件</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<h1>欢迎使用我的插件</h1>
<button id="action-btn">执行操作</button>
<div id="result"></div>
</div>
<script src="app.js"></script>
</body>
</html>
TUI 页面 (tui/index.html)
<!-- TUI 专用页面,包含 data-tui 标记 -->
<div data-tui-type="panel" data-tui-title="我的插件" data-tui-border="rounded">
<h1 data-tui-type="heading" data-tui-level="1" data-tui-align="center">
欢迎使用我的插件
</h1>
<button
data-tui-type="button"
data-tui-label="执行操作"
data-tui-id="action-btn"
data-tui-style="primary">
</button>
<div
data-tui-type="text"
data-tui-id="result"
data-tui-color="gray">
等待操作...
</div>
</div>
TUI 专用样式 (tui/styles.css)
/* 仅包含终端支持的样式 */
.container {
padding: 2;
margin: 1;
}
h1 {
font-size: large;
font-weight: bold;
text-align: center;
color: #00ff00;
}
button {
background-color: #0066cc;
color: #ffffff;
border: 2 solid #004499;
border-style: rounded;
padding: 1 2;
}
#result {
color: #888888;
font-style: italic;
}
TUI 交互逻辑 (tui/interaction.js)
// 仅支持基础交互
document.getElementById('action-btn').addEventListener('click', function() {
// 发送请求到后端
fetch('/api/my-plugin/action', {method: 'POST'})
.then(res => res.json())
.then(data => {
document.getElementById('result').textContent = data.message;
});
});
// 键盘快捷键
document.addEventListener('keydown', function(e) {
if (e.key === 'r') {
// 刷新操作
location.reload();
}
});
插件配置
在 config.yaml 中定义插件配置:
# plugins/my-plugin/config.yaml
plugin_name: my-plugin
enabled: true
settings:
api_key: ""
timeout: 30
max_retries: 3
debug: false
routes:
prefix: /api/my-plugin
auth_required: true
tui:
enabled: true
theme: default
refresh_rate: 1000 # 毫秒
webui:
enabled: true
port: 8080
在插件中读取配置:
def init(self) -> None:
config = self.get_config()
self.api_key = config.get('settings', {}).get('api_key', '')
self.timeout = config.get('settings', {}).get('timeout', 30)
self.logger.info(f"插件配置加载完成: timeout={self.timeout}")
插件间通信
# 调用其他插件的方法
other_plugin = self.get_plugin('other-plugin')
if other_plugin:
result = other_plugin.some_method(arg1, arg2)
# 发布事件
self.emit_event('my-event', {'data': 'value'})
# 订阅事件
@self.on_event('other-event')
def handle_event(event_data):
self.logger.info(f"收到事件: {event_data}")
插件生命周期
1. 发现阶段:扫描 plugins 目录,识别插件
2. 排序阶段:根据依赖关系确定加载顺序
3. 初始化阶段:调用每个插件的 init() 方法
4. 启动阶段:调用每个插件的 start() 方法
5. 运行阶段:插件正常提供服务
6. 停止阶段:调用每个插件的 stop() 方法 (按依赖逆序)
调试插件
# 启用调试模式
export NEBULA_DEBUG=1
python main.py
# 查看特定插件日志
tail -f logs/nebula.log | grep my-plugin
# 热重载插件 (开发模式)
python main.py --dev --reload-plugins
打包插件
# 创建插件包
cd plugins/my-plugin
zip -r my-plugin-1.0.0.zip .
# 安装插件
nebula plugin install my-plugin-1.0.0.zip
# 发布到插件市场
nebula plugin publish my-plugin-1.0.0.zip
最佳实践
1. 代码规范
- 遵循 PEP 8 编码规范
- 使用类型注解
- 编写单元测试 (覆盖率 > 80%)
- 添加详细的文档字符串
2. 错误处理
try:
result = risky_operation()
except SpecificError as e:
self.logger.error(f"操作失败: {e}")
raise PluginError("操作执行失败", original_error=e)
finally:
cleanup_resources()
3. 日志记录
# 不同级别的日志
self.logger.debug("调试信息")
self.logger.info("一般信息")
self.logger.warning("警告信息")
self.logger.error("错误信息")
self.logger.critical("严重错误")
# 带上下文的日志
self.logger.info(
"用户操作",
extra={"user_id": user_id, "action": "create"}
)
4. 性能优化
- 使用异步操作处理 I/O 密集型任务
- 实现缓存机制减少重复计算
- 批量处理数据库操作
- 监控资源使用情况
5. 安全考虑
- 验证所有用户输入
- 使用参数化查询防止 SQL 注入
- 实施速率限制
- 定期更新依赖
- 敏感信息使用环境变量
常见问题
Q: 如何禁用 TUI 只使用 WebUI?
# 设置环境变量
export NEBULA_TUI_ENABLED=false
python main.py
# 或在配置文件中
# config.yaml
tui:
enabled: false
Q: TUI 显示乱码怎么办?
确保终端支持 UTF-8 和真彩色:
# 检查终端支持
echo $TERM # 应该类似 xterm-256color
# 启用真彩色
export COLORTERM=truecolor
Q: 如何自定义 TUI 主题?
在插件的 tui/styles.css 中定义主题变量:
:root {
--primary-color: #0066cc;
--secondary-color: #6c757d;
--success-color: #28a745;
--danger-color: #dc3545;
--bg-color: #1a1a2e;
--text-color: #eeeeee;
}
Q: 插件加载失败如何排查?
# 查看详细日志
python main.py --verbose
# 检查插件依赖
nebula plugin check my-plugin
# 验证插件结构
nebula plugin validate my-plugin/
贡献指南
- Fork 项目仓库
- 创建功能分支 (
git checkout -b feature/amazing-feature) - 提交更改 (
git commit -m 'Add amazing feature') - 推送到分支 (
git push origin feature/amazing-feature) - 创建 Pull Request
开发环境设置
# 克隆仓库
git clone https://github.com/nebulashell/nebulashell.git
cd nebulashell
# 创建虚拟环境
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# 安装开发依赖
pip install -e ".[dev]"
# 运行测试
pytest tests/
# 代码格式化
black .
isort .
flake8 .
许可证
本项目采用 MIT 许可证,详见 LICENSE 文件。