Files
NebulaShell/ai.md
2026-05-02 13:32:51 +08:00

14 KiB
Raw Blame History

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.py1430 行)在运行时将 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 作为鼠标候补

开放代码opencodeTUI 架构分析

opencode 是目前最成熟的终端 AI 编程助手,其 TUI 架构可参考:

特性 opencode 实现
渲染引擎 原生终端渲染,无 HTML 中间层。直接操作终端缓冲区
组件系统 类似 React 的组件化方案,但所有组件直接输出 ANSI 字符串
输入处理 raw mode + SGR 鼠标 + 组合键解析,支持 Tab 切换模式
布局 弹性布局引擎,组件根据终端宽度自动折行/折叠
状态管理 全局状态树,每次状态变更触发受控重绘(非全屏重绘)
会话管理 多会话并行,每个会话独立维护上下文和渲染状态
主题系统 完整的配色方案,支持暗色/亮色主题切换

关键差异opencode 不使用任何 Web 技术栈做 TUI其所有界面元素输入框、按钮、列表、状态栏、侧边栏都是直接通过 ANSI 转义码在终端绘制的。每个组件是一个纯 Python/TypeScript 类,render() 方法返回 ANSI 字符串。

对我们的启发

  1. 放弃 HTML 转换层是正确的方向
  2. 直接 ANSI 渲染的架构更可控、性能更好
  3. 需要设计自己的组件化终端渲染库(参考 opencode 的组件系统)
  4. 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/

贡献指南

  1. Fork 项目仓库
  2. 创建功能分支 (git checkout -b feature/amazing-feature)
  3. 提交更改 (git commit -m 'Add amazing feature')
  4. 推送到分支 (git push origin feature/amazing-feature)
  5. 创建 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 文件。

联系方式