10 Commits
Author SHA1 Message Date
openclaw-bot 2bd8ca0fba release: v5.1.4 — 桌面GUI自动化 + AI生图 + Web UI后端
- 新增 gui_auto 插件 (20 操作): Windows UIA 桌面元素识别与操控
- 新增 ai_image 插件: 基于 ai.2x.nz 自然语言生图
- 新增 webui_server.py: WebSocket 实时通信后端
- 重写 webui.html: 大厂设计感 WebSocket 交互界面
- 插件数: 17 → 19, 操作数: 141+ → 161+
- 测试数: 113 → 150+
- 更新 README/CHANGELOG/HELP
2026-04-29 15:50:40 +08:00
openclaw-bot e6fddf14f8 feat: 新增桌面GUI自动化插件 (gui_auto) - Windows UIA 元素识别与操控
- 新增 gui_auto 插件 (20 操作): 元素树获取、点击/输入/按键、窗口管理、元素查询、高亮闪烁
- 基于 Windows UI Automation API,支持模糊匹配、中文输入、ValuePattern 自动识别
- 完整 MCP/FC 定义,AI 可通过 Function Calling 直接调用
- 非 Windows 环境优雅降级
- 新增 30 个单元测试
- 更新 README/CHANGELOG/HELP
- 插件数: 17 → 18, 操作数: 141+ → 160+
2026-04-29 15:37:10 +08:00
shuiyu1123 a00396b4fd !2 feat(webui): 重写 Web UI — WebSocket 后端 + 大厂设计感
Merge pull request !2 from shuiyu1123/feat/webui-v2
2026-04-29 07:16:16 +00:00
shuiyu1123 c2dfa284d1 !1 feat: 添加 AI 生图插件 (ai_image)
Merge pull request !1 from shuiyu1123/feature/ai-image-plugin
2026-04-29 07:15:47 +00:00
XiaoLi Contributor 614b081364 feat(webui): 重写 Web UI — WebSocket 后端 + 大厂设计感
- 新增 webui_server.py: WebSocket 后端,桥接小狸 AI 引擎
  - WebSocket 实时通信(对话/工具调用/状态同步)
  - HTTP 静态文件服务
  - 引擎热切换、安全模式切换
  - 延迟初始化,不阻塞启动

- 重写 webui.html: 纯静态前端,连接 WebSocket 后端
  - 设计风格: Linear/Vercel 级别的暗色主题
  - 真实 WebSocket 通信(非前端模拟)
  - 对话/工具双视图
  - 实时状态栏(引擎/安全模式)
  - 代码高亮、打字动画、快速操作
  - 响应式布局

用法:
  python webui_server.py              # 启动 (HTTP:8080, WS:8079)
  python webui_server.py --port 3000  # 自定义端口
  python webui_server.py --no-ai      # 纯 UI 模式
2026-04-28 19:06:12 +08:00
OpenClaw 245413b2cd release: v5.1.3 — 图像生成 + 记忆系统插件 + 横评页面
- 新增 image_generator 插件(ComfyUI 在线生图)
- 新增 memory_plugin 插件(包装记忆系统供 AI 调用)
- 新增 compare.html 7工具横评页面
- 更新 README.md 全面重写(MCP/Clawli/记忆/通知/定时任务)
- 更新 CHANGELOG.md 新增 v5.1.3 条目
- 更新 about.txt 版本号至 5.1.3
- 插件数: 15 → 17, 操作数: 141 → 141+
2026-04-28 03:37:37 +08:00
OpenClaw 3fa1d56632 feat: 新增图像生成插件 + 记忆系统插件 + 7工具横评页面
- plugins/image_generator.py: 基于 ai.2x.nz ComfyUI 的图像生成插件
  - 自然语言/Tag 双模式,支持中文描述
  - WebSocket 实时进度,28 个角色工作流
  - 分辨率可调(最大 1344x1344)

- plugins/memory_plugin.py: 包装 xcli_core/memory.py 的记忆插件
  - AI 可写日记、读日记、搜索日记
  - 全局搜索(MEMORY + 日记 + 聊天记录)
  - 自动保存对话 + 上下文压缩
  - 长期记忆 MEMORY.md 管理

- compare.html: 7 款 AI 编程助手横评页面
  - Claude Code / Codex CLI / Gemini CLI / Aider / Copilot CLI / 小狸 / TG HELPER
  - 25+ 维度功能对比 + 评分 + 场景推荐

- .gitignore: 排除运行时生成的记忆数据和图片
2026-04-28 03:13:53 +08:00
openclaw-bot 68d085df28 fix: 自动选择默认工作流,无 Lora 优先 2026-04-28 00:52:09 +08:00
openclaw-bot 62c761b7d8 feat: 添加 AI 生图插件 (ai_image)
基于 ai.2x.nz 接口的自然语言生图插件,支持:
- 自然语言描述生图 (generate)
- Danbooru Tag 生图 (tag)
- Tag + 自然语言混合模式 (both)
- 工作流列表与切换 (workflows/switch)
- 图片尺寸设置 (size/width/height)
- 改写模式与覆写模式开关 (rewrite/override)
- 生成状态查询 (status)
- 自动下载生成图片到本地

依赖: requests, websocket-client
2026-04-28 00:36:36 +08:00
shuiyu1123 62aa5ed128 docs: v5.1.2 竞品实力对比分析页面 2026-04-27 01:14:49 +08:00
14 changed files with 5458 additions and 419 deletions

No files matched your search

+8
View File
@@ -36,6 +36,14 @@ Thumbs.db
# 嵌入式 Python
embedded_python/
# 记忆系统数据(运行时生成)
MEMORY.md
memory/
# 生成的图片
generated_*.png
screenshots/gen_*.png
# 临时文件
*.tmp
*.bak
+65
View File
@@ -4,6 +4,71 @@
---
## v5.1.4 (2026-04-29)
### 新增功能
- **桌面GUI自动化插件** (`gui_auto`, 20 操作) — 基于 Windows UI Automation (UIA) API,AI 可识别和操控桌面应用
- 元素树获取 (`snapshot`) — 获取整个屏幕或指定窗口的 UI 元素树,支持自定义遍历深度
- 元素交互 — 点击/右键/双击(名称模糊匹配 + 坐标点击),文本输入(中文 Unicode)、清空、发送按键
- 窗口管理 — 列出所有窗口 (`windows`)、聚焦指定窗口 (`focus`)
- 元素查询 — 查找/查找所有/详情/值/子树/存在检查/状态,共 8 个查询操作
- 高级功能 — 等待元素出现(带超时)、高亮闪烁元素(GDI 红色边框)、滚动
- MCP/FC 完整定义,AI 通过 Function Calling 直接调用,非 Windows 环境优雅降级
- **AI 生图插件** (`ai_image`) — 基于 ai.2x.nz 自然语言生图接口
- 自然语言描述 + 直接 Tag 双模式
- 工作流切换、画风选择、分辨率调整
- 异步生成 + WebSocket 进度推送
- 生成历史记录
- **Web UI 后端** (`webui_server.py`) — WebSocket 实时通信 + 静态文件服务
- 纯 Python 实现,基于 `websockets` 库
- 支持命令行参数 (`--port` / `--host`)
- 与前端 `webui.html` 配合,提供完整的浏览器端 AI 交互体验
### 改进
- **Web UI 前端重写** (`webui.html`) — 从纯静态页面升级为 WebSocket 实时交互
- 大厂设计感 UI,响应式布局
- 实时消息流、打字机效果
- 支持工具调用结果展示
### 文档
- 更新 README.md: 插件列表、项目结构、badge 版本号
- 更新 CHANGELOG.md: v5.1.4 完整变更记录
- 更新 HELP.md: 新增 GUI 自动化使用说明
### 测试
- 新增 gui_auto 插件 30 个单元测试(工具信息、MCP 定义、参数转换、错误处理、操作路由)
---
## v5.1.3 (2026-04-28)
### 新增功能
- **图像生成插件** (`image_generator`) — 基于 ai.2x.nz 的 ComfyUI 在线服务,自然语言/Tag 双模式,支持中文描述,28 个角色工作流可选,分辨率可调(最大 1344×1344)
- **记忆系统插件** (`memory_plugin`) — 包装 `xcli_core/memory.py` 的完整记忆工具,AI 可写日记、读日记、搜索记忆、管理聊天记录、操作长期记忆 MEMORY.md
- **AI 编程助手横评页面** (`compare.html`) — 7 款工具(Claude Code / Codex CLI / Gemini CLI / Aider / Copilot CLI / 小狸 / TG HELPER)25+ 维度同口径对比
### 修复
- 修复记忆系统插件(`Liugin` 类在 `xcli_core/memory.py` 中定义但未被插件管理器加载)导致 AI 无法调用记忆工具的问题,新增 `plugins/memory_plugin.py` 包装层
### 文档
- 全面更新 README.md 至 v5.1.3
- 新增记忆系统、Clawli 远程模式、MCP 协议支持、定时任务、系统通知等功能说明
- 更新插件列表:15 个 → 17 个(+image_generator, +memory_plugin)
- 更新操作数:141 → 141+
- 补充 MCP 协议支持说明(17 个插件全部实现 MCP 定义)
- 补充插件开发文档中 `Liugin` / `Plugin` 双协议说明
### 贡献者
- 感谢 **艾轮 卧壳** 对本版本的贡献 🖤
---
## v5.1.2 (2026-04-26)
### 新增功能
+20
View File
@@ -154,6 +154,26 @@ run make build --timeout 60 # 60秒超时
| `pdf page.pdf` | 保存 PDF |
| `stop` | 关闭浏览器 |
### 桌面GUI自动化 (`gui_auto`) ⭐ NEW
> 仅支持 Windows,基于 UI Automation API
| 操作 | 说明 |
|------|------|
| `snapshot [深度]` | 获取屏幕 UI 元素树 |
| `snapshot --window 记事本` | 获取指定窗口元素树 |
| `screenshot [路径]` | 全屏截图 |
| `windows` | 列出所有窗口 |
| `focus 记事本` | 聚焦窗口 |
| `click 确定` | 点击元素(模糊匹配) |
| `click --xy 500 300` | 点击坐标 |
| `type 搜索框 Hello` | 在元素中输入文本 |
| `keys ctrl+s` | 发送按键 |
| `find 保存` | 查找元素 |
| `exists 确定` | 检查元素是否存在 |
| `wait 加载完成 10` | 等待元素出现 |
| `highlight 确定` | 高亮闪烁元素 |
### 子 Agent (`sub_agent`)
| 操作 | 说明 |
+128 -48
View File
@@ -1,14 +1,16 @@
# 小狸 Pro-CLI
<p align="center">
<strong>智能编程助手 — 代码编辑 · Git 集成 · 浏览器自动化 · 多 Agent 协作</strong>
<strong>智能编程助手 — 代码编辑 · Git 集成 · 浏览器自动化 · 多 Agent 协作 · 图像生成 · 记忆系统</strong>
</p>
<p align="center">
<img src="https://img.shields.io/badge/version-5.1.2-blue" alt="version">
<img src="https://img.shields.io/badge/version-5.1.4-blue" alt="version">
<img src="https://img.shields.io/badge/python-3.10+-green" alt="python">
<img src="https://img.shields.io/badge/license-MIT-orange" alt="license">
<img src="https://img.shields.io/badge/tests-113%20passed-brightgreen" alt="tests">
<img src="https://img.shields.io/badge/tests-150+%20passed-brightgreen" alt="tests">
<img src="https://img.shields.io/badge/plugins-19-blueviolet" alt="plugins">
<img src="https://img.shields.io/badge/MCP-supported-brightgreen" alt="mcp">
</p>
---
@@ -21,9 +23,15 @@
- **浏览器自动化** — Playwright 驱动,导航/交互/截图/JS 执行/PDF
- **子 Agent 系统** — 多 Agent 并行协作,独立任务分配与结果回收
- **统一安全层** — AI 风险识别 + 三模式切换(普通/人工/无限制)
- **15 个插件 141 个操作** — 代码编辑、Git、浏览器、工程化、文件管理、任务管理等
- **三模式界面** — TUI 图形化 + 传统命令行 + **Web UI 浏览器界面**
- **113 个自动化测试** — pytest 覆盖全部插件和核心模块
- **图像生成** — 基于 ComfyUI 在线服务,自然语言/Tag 双模式,支持中文描述
- **记忆系统** — 长期记忆 + 每日日记 + AI 写日记 + 全局搜索 + 自动保存对话
- **Clawli 远程模式** — PC-手机 WebSocket 双向通信,支持内网穿透代理
- **定时任务** — 支持相对时间、绝对时间、重复任务,到期自动激活 AI
- **系统通知** — 跨平台任务完成通知(Windows / macOS / Linux)
- **MCP 协议** — 19 个插件全部实现 MCP 定义,支持 Function Calling 原生调用
- **19 个插件 161+ 个操作** — 代码编辑、Git、浏览器、工程化、文件管理、图像生成、记忆、桌面GUI自动化等
- **三模式界面** — TUI 图形化 + 传统命令行 + **Web UI 浏览器界面**
- **150+ 个自动化测试** — pytest 覆盖全部插件和核心模块
## 快速开始
@@ -43,6 +51,9 @@ python ai_cli.py
# 指定引擎
python ai_cli.py --engine openai
# Windows 一键启动(无需 Python 环境)
launcher.exe
```
## AI 引擎
@@ -76,17 +87,17 @@ python ai_cli.py --engine openai
}
```
## 插件列表 (15 个)
## 插件列表 (19 个)
### 核心生产力
| 插件 | 操作数 | 说明 |
|------|--------|------|
| `code_editor` | 23 | 代码编辑+搜索。精准替换、批量编辑、find/regex/symbols/imports/callers/todo/structure + 自动语法检查 |
| `git_tools` | 20 | Git 版本控制。status/diff/log/commit/branch + smart-commit/changelog/contributors |
| `code_editor` | 24 | 代码编辑+搜索。精准替换、批量编辑、find/regex/symbols/imports/callers/todo/structure + 自动语法检查 + diff 弹窗 |
| `git_tools` | 17 | Git 版本控制。status/diff/log/commit/branch + smart-commit/changelog/contributors/stale |
| `cmd_executor` | 1 | Shell 命令执行。跨平台,超时控制,危险命令拦截 |
| `file_manager` | 11 | 文件系统。list/read/write/copy/move/delete/search/info/mkdir |
| `auto_engineer` | 16 | 工程化。lint/format/test/build/deps + 复杂度/安全扫描/质量指标 |
| `file_manager` | 10 | 文件系统。list/read/write/copy/move/delete/search/info/mkdir/append |
| `auto_engineer` | 12 | 工程化。lint/format/test/build/deps + 复杂度/安全扫描/质量指标/项目初始化 |
| `task_manager` | 7 | 任务管理。add/list/complete/delete/clear,JSON 持久化 |
### 高级功能
@@ -94,7 +105,12 @@ python ai_cli.py --engine openai
| 插件 | 操作数 | 说明 |
|------|--------|------|
| `sub_agent` | 8 | 子 Agent 系统。spawn/list/status/result/send/kill,多 Agent 并行协作 |
| `browser_auto` | 31 | 浏览器自动化。导航/点击/填写/截图/JS执行/PDF/cookies(基于 Playwright) |
| `browser_auto` | 28 | 浏览器自动化。导航/点击/填写/截图/JS执行/PDF/cookies(基于 Playwright) |
| `gui_auto` | 20 | **桌面GUI自动化**。基于 Windows UIA,屏幕元素识别/点击/输入/截图/元素树遍历(仅Windows) |
| `ai_image` | — | **AI生图**。基于 ai.2x.nz 自然语言生图,异步生成 + WebSocket 进度推送 |
| `image_generator` | 7 | **图像生成**。基于 ComfyUI 在线服务,自然语言/Tag 双模式,28 个角色工作流 |
| `memory_plugin` | 11 | **记忆系统**。AI 写日记/搜索记忆/管理聊天记录/长期记忆 MEMORY.md |
| `scheduler` | 4 | **定时任务**。支持相对时间/绝对时间/重复任务,到期自动激活 AI |
| `tool_search` | — | 工具搜索。按关键词发现可用插件(供 AI 按需调用) |
### 辅助工具
@@ -104,10 +120,43 @@ python ai_cli.py --engine openai
| `network_tools` | 网络工具。ping/get/status/headers/ip |
| `ai_search` | AI 搜索。jina.ai 智能搜索 + 网页内容提取 |
| `frontend_tester` | 前端测试。HTML/CSS/JS 语法检查、响应式验证 |
| `speech_recognition` | 语音识别。录音 + 语音转文字 |
| `speech_recognition` | 语音识别。录音 + 语音转文字(Whisper) |
| `audio_player` | 音频播放 |
| `send_image` | Clawli 模式下发图片到手机 |
## 记忆系统
内置持久化记忆系统,AI 可以记住之前的工作:
| 功能 | 说明 |
|------|------|
| 长期记忆 | `MEMORY.md` — AI 自己维护的精华记忆 |
| 每日日记 | `memory/YYYY-MM-DD.md` — 带时间戳和标签 |
| AI 写日记 | `memory_plugin diary_ai` — 自动标记 [AI观察] |
| 自动保存对话 | 每轮对话后自动存 JSON |
| 上下文压缩 | 超过 50 条自动 AI 总结,保留最近 10 条 |
| 全局搜索 | 搜索 MEMORY + 日记 + 聊天记录 |
```
memory_plugin diary 今天完成了API对接
memory_plugin search 定时任务
memory_plugin remember 用户偏好深色主题
memory_plugin chat_search 图像生成
```
## Clawli 远程模式
通过 WebSocket 实现 PC-手机双向通信:
```bash
/remote start 9079 mypassword # 启动本地服务
/remote proxy relay.example.com # 通过中继服务器穿透内网
/remote stop # 停止服务
/remote status # 查看状态
```
功能:手机发消息给 AI、AI 回复推到手机、文件传输、图片识别。
## 安全机制
### 三模式切换 (`/safe`)
@@ -137,91 +186,107 @@ python ai_cli.py --engine openai
- 子进程隔离执行
- 内存限制
## MCP 协议支持
所有 19 个插件均实现 MCP (Model Context Protocol) 定义,支持三种工具调用协议:
| 协议 | 说明 |
|------|------|
| **MCP** | Model Context Protocol 标准定义 |
| **FC** | OpenAI Function Calling 原生调用 |
| **JSON** | 传统 `{"action": "use_tool", ...}` 格式 |
`fc_tools.py` 自动将 MCP 定义转换为 OpenAI FC 格式,支持混合响应解析。
## 命令列表
| 命令 | 说明 |
|------|------|
| `/help` | 显示帮助 |
| `/safe` | 切换安全模式(普通→人工→无限制) |
| `/safe off` | 直接切换到无限制模式 |
| `/safe manual` | 直接切换到人工确认模式 |
| `/engine list` | 列出可用引擎 |
| `/engine switch <引擎>` | 切换引擎 |
| `/engine.openai models` | 列出 OpenAI 引擎模型 |
| `/chat save/list/open` | 聊天记录管理 |
| `/file.read <路径>` | 读取文件 |
| `/remote` | Clawli 远程连接帮助 |
| `/notify` | 切换任务完成通知 |
| `/scheduler` / `/remind` | 管理定时任务 |
| `/memory` | 管理记忆系统 |
| `/tui` | 切换 TUI 模式 |
| `/quit` | 退出 |
## Web UI
纯静态 HTML 界面,无需后端,浏览器直接打开。
WebSocket 实时交互界面,支持完整的 AI 对话体验。
```bash
# 直接用浏览器打开
# 方式 1: 启动 WebSocket 后端(推荐)
python webui_server.py --port 8080
# 浏览器访问 http://localhost:8080
# 方式 2: 直接打开静态页面(功能有限)
start webui.html # Windows
open webui.html # macOS
xdg-open webui.html # Linux
start webui.html # Windows
```
### 功能
| 功能 | 说明 |
|------|------|
| 对话界面 | 聊天气泡、快捷指令、打字动画 |
| 代码编辑器 | 行号显示、Tab 缩进、Ctrl+S 保存、语法高亮 |
| 文件浏览 | 侧栏项目文件树,点击查看 |
| ️ 系统状态 | 插件/操作/引擎/测试统计 |
| 引擎面板 | Ollama / OpenAI 引擎一览 |
| 工具面板 | 15 个插件及操作数一览 |
| 日志面板 | 系统操作实时记录 |
## 项目结构
```
xiaoli-cli/
├── ai_cli.py # 主入口
├── webui.html # Web UI 界面 (纯静态 HTML)
├── launcher.py # 智能启动器
├── webui.html # Web UI 界面 (WebSocket 实时交互)
├── webui_server.py # Web UI 后端 (WebSocket + 静态文件服务)
├── launcher.py # 智能启动器(自动检测环境/安装依赖)
├── config.json.example # 配置模板
├── easter_egg.py # 彩蛋
│
├── xcli_core/ # 核心模块
│ ├── cli_core.py # 主循环 + 提示词构建
│ ├── cli_base.py # 初始化 + 引擎/插件加载
│ ├── cli_tools.py # 工具调用执行
│ ├── cli_tools.py # 工具调用执行(支持 MCP/FC/JSON)
│ ├── cli_clawli.py # Clawli 远程模式
│ ├── cli_code_exec.py # 代码沙箱执行
│ ├── cli_display.py # 显示输出
│ ├── cli_history.py # 聊天记录
│ ├── cli_code_exec.py # 代码执行
│ ├── config.py # 配置管理
│ ├── constants.py # 常量定义
│ ├── safety.py # 统一安全层
│ ├── sandbox.py # 代码安全沙箱
│ ├── plugin_manager.py # 插件管理器
│ └── tui.py # TUI 界面
│ ├── fc_tools.py # MCP → FC 格式转换
│ ├── memory.py # 记忆系统(长期记忆+日记+搜索+压缩)
│ ├── notification.py # 跨平台系统通知
│ ├── plugin_manager.py # 插件管理器(Liugin/Plugin 双协议)
│ ├── safety.py # 统一安全层(AI 风险分析)
│ ├── sandbox.py # 代码安全沙箱(AST 级检测)
│ └── tui.py # TUI 界面(Textual + 动画)
│
├── ai_engines/ # AI 引擎
│ ├── ollama_engine.py # Ollama 本地模型
│ └── openai_engine.py # OpenAI 兼容格式
│
├── plugins/ # 插件 (15 个)
│ ├── code_editor.py # 代码编辑+搜索
│ ├── git_tools.py # Git 版本控制
├── plugins/ # 插件 (19 个)
│ ├── code_editor.py # 代码编辑+搜索 (24 操作)
│ ├── git_tools.py # Git 版本控制 (17 操作)
│ ├── cmd_executor.py # Shell 命令执行
│ ├── file_manager.py # 文件管理
│ ├── auto_engineer.py # 工程化自动化
│ ├── file_manager.py # 文件管理 (10 操作)
│ ├── auto_engineer.py # 工程化自动化 (12 操作)
│ ├── task_manager.py # 任务管理
│ ├── sub_agent.py # 子 Agent 系统
│ ├── browser_auto.py # 浏览器自动化
│ ├── sub_agent.py # 子 Agent 系统 (8 操作)
│ ├── browser_auto.py # 浏览器自动化 (28 操作)
│ ├── gui_auto.py # 桌面GUI自动化 (20 操作) ⭐ NEW
│ ├── ai_image.py # AI生图 (ai.2x.nz) ⭐ NEW
│ ├── image_generator.py # 图像生成 (7 操作) ⭐ NEW
│ ├── memory_plugin.py # 记忆系统 (11 操作) ⭐ NEW
│ ├── scheduler.py # 定时任务
│ ├── tool_search.py # 工具搜索
│ ├── network_tools.py # 网络工具
│ ├── ai_search.py # AI 搜索
│ ├── frontend_tester.py # 前端测试
│ ├── speech_recognition.py # 语音识别
│ ├── audio_player.py # 音频播放
│ └── send_image.py # 图片发送
│
├── skills/ # 技能 (Skill 协议)
├── image_engine/ # 图像识别引擎
├── image_engine/ # 图像识别引擎(Ollama 视觉模型)
├── tests/ # 测试套件 (113 用例)
├── love.txt # 彩蛋情话
└── about.txt # 关于信息
@@ -239,8 +304,10 @@ python -c "import py_compile; py_compile.compile('ai_cli.py', doraise=True)"
## 插件开发
小狸支持 `Liugin`(自研协议)和 `Plugin`(通用协议)两种类名,均可被插件管理器自动识别:
```python
class Plugin:
class Plugin: # 或 class Liugin
"""我的插件"""
def __init__(self):
@@ -258,6 +325,18 @@ class Plugin:
"usage": self.usage
}
def get_mcp_definition(self):
return {
"name": "my_tool",
"description": "工具描述",
"inputSchema": {
"type": "object",
"properties": {
"args": {"type": "string", "description": "参数"}
}
}
}
def handle(self, args: str) -> str:
return "处理结果"
```
@@ -271,3 +350,4 @@ class Plugin:
- Gitee: https://gitee.com/shuiyu1123/xiaoli-cli
- [更新日志](CHANGELOG.md)
- [使用指南](HELP.md)
- [AI 编程助手横评](compare.html)
+2 -2
View File
@@ -1,7 +1,7 @@
**产品标识**
- **产品名称**:小狸智能体 增强-终端(Xiaoli Pro-CLI)
- **内部版本号**:20260426
- **发布版本**:发行版5.1.1
- **内部版本号**:20260428
- **发布版本**:发行版5.1.3
**技术规格**
- **主要语言**:简体中文(UTF-8)
+1161
View File
File diff suppressed because it is too large. Load diff
+590
View File
@@ -0,0 +1,590 @@
"""
AI 生图插件 - 基于 ai.2x.nz 自然语言生图接口
支持自然语言描述、直接 Tag、工作流切换、画风选择等
"""
import os
import json
import time
import threading
import hashlib
import re
from datetime import datetime
from urllib.parse import quote
try:
import requests
REQUESTS_AVAILABLE = True
except ImportError:
REQUESTS_AVAILABLE = False
try:
import websocket
WEBSOCKET_AVAILABLE = True
except ImportError:
WEBSOCKET_AVAILABLE = False
class Plugin:
"""AI 生图插件 - 调用 ai.2x.nz 接口生成图片"""
BASE_URL = "https://ai.2x.nz"
WS_URL = "wss://ai.2x.nz/ws/run"
API_URL = "https://ai.2x.nz/api"
# 保存目录
DEFAULT_SAVE_DIR = "generated_images"
def __init__(self):
self.usage = """AI 生图工具 - 基于自然语言描述生成图片
基本用法:
generate <描述> - 用自然语言描述生成图片
tag <直接Tag> - 用 Danbooru Tag 生成图片
both <Tag> <<<>>> <描述> - 同时使用 Tag 和自然语言
高级用法:
workflows - 列出所有可用工作流
switch <工作流名> - 切换工作流
width <数值> - 设置图片宽度 (最大1344)
height <数值> - 设置图片高度 (最大1344)
size <宽> <高> - 同时设置宽高
rewrite on|off - 改写模式开关
override on|off - 覆写模式开关
status - 查看当前状态和配置
示例:
ai_image generate 一个可爱的猫耳少女,樱花背景
ai_image tag 1girl, cat_ears, cherry_blossoms, smile
ai_image both 1girl, cat_ears <<<>>> 一个可爱的猫耳少女在樱花树下微笑
ai_image workflows
ai_image switch WAI - 37.json
ai_image size 1024 1024
ai_image rewrite on
"""
self.cli = None
self.save_dir = self.DEFAULT_SAVE_DIR
# 配置状态
self.current_workflow = None
self.direct_prompt = ""
self.nl_prompt = ""
self.rewrite = False
self.override = False
self.width = None
self.height = None
self.workflows_cache = None
self.is_generating = False
def set_cli(self, cli):
"""设置 CLI 实例引用"""
self.cli = cli
# 注册插件命令
if hasattr(cli, 'register_liugin_command'):
cli.register_liugin_command('ai_image', self.command_handler)
def get_tool_info(self):
return {
"name": "ai_image",
"description": "AI 生图工具 — 基于 ai.2x.nz 接口,支持自然语言描述和 Danbooru Tag 生成图片。当用户想要生成图片、画图、AI绘画、生图时使用此工具。",
"keywords": [
"生图", "画图", "图片", "AI绘画", "生成图片", "image", "generate",
"draw", "绘画", "插画", "二次元", "动漫", "Danbooru", "tag",
"工作流", "workflow", "ComfyUI"
],
"usage": self.usage
}
def get_mcp_definition(self):
return {
"name": "ai_image",
"description": "AI 生图工具,基于 ai.2x.nz 接口生成图片",
"inputSchema": {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["generate", "tag", "both", "workflows", "switch", "size", "status"],
"description": "操作类型"
},
"prompt": {
"type": "string",
"description": "生成提示词"
}
},
"required": ["operation"]
}
}
def convert_mcp_args(self, arguments):
op = arguments.get("operation", "")
prompt = arguments.get("prompt", "")
return f"{op} {prompt}"
def command_handler(self, args):
"""处理 /ai_image 命令"""
return self.handle(args)
def handle(self, args):
"""处理 AI 生图请求"""
if not REQUESTS_AVAILABLE:
return "❌ 缺少 requests 库,请运行: pip install requests"
args = args.strip()
if not args:
return self.usage
# 解析操作
parts = args.split(maxsplit=1)
operation = parts[0].lower()
remaining = parts[1] if len(parts) > 1 else ""
try:
if operation in ["generate", "gen", "g"]:
return self._cmd_generate(remaining)
elif operation in ["tag", "t", "direct", "d"]:
return self._cmd_tag(remaining)
elif operation in ["both", "b"]:
return self._cmd_both(remaining)
elif operation in ["workflows", "wf", "list"]:
return self._cmd_workflows()
elif operation in ["switch", "sw"]:
return self._cmd_switch(remaining)
elif operation in ["width", "w"]:
return self._cmd_set_width(remaining)
elif operation in ["height", "h"]:
return self._cmd_set_height(remaining)
elif operation in ["size", "s"]:
return self._cmd_set_size(remaining)
elif operation in ["rewrite", "rw"]:
return self._cmd_set_rewrite(remaining)
elif operation in ["override", "ov"]:
return self._cmd_set_override(remaining)
elif operation in ["status", "st"]:
return self._cmd_status()
elif operation in ["help", "?"]:
return self.usage
else:
# 尝试当作自然语言描述处理
return self._cmd_generate(args)
except Exception as e:
return f"❌ 操作失败: {str(e)}"
# ── 命令实现 ──
def _cmd_generate(self, prompt):
"""自然语言生成图片"""
if not prompt.strip():
return "❌ 请提供描述内容。示例: ai_image generate 一个可爱的猫耳少女"
self.nl_prompt = prompt.strip()
self.direct_prompt = ""
self.override = False
return self._run_generation()
def _cmd_tag(self, tags):
"""使用直接 Tag 生成图片"""
if not tags.strip():
return "❌ 请提供 Tag 内容。示例: ai_image tag 1girl, cat_ears, smile"
self.direct_prompt = tags.strip()
self.nl_prompt = ""
self.override = True
return self._run_generation()
def _cmd_both(self, args):
"""同时使用 Tag 和自然语言"""
if "<<<>>>" in args:
parts = args.split("<<<>>>", 1)
self.direct_prompt = parts[0].strip()
self.nl_prompt = parts[1].strip()
else:
# 尝试用 || 分隔
if "||" in args:
parts = args.split("||", 1)
self.direct_prompt = parts[0].strip()
self.nl_prompt = parts[1].strip()
else:
return "❌ 格式错误。用法: ai_image both <Tag> <<<>>> <描述>"
self.override = False
return self._run_generation()
def _cmd_workflows(self):
"""列出所有工作流"""
workflows = self._fetch_workflows()
if not workflows:
return "❌ 获取工作流列表失败"
result = f"📂 共 {len(workflows)} 个工作流:\n\n"
for i, wf in enumerate(workflows, 1):
name = wf.get("path", "未知")
current = " ← [当前]" if name == self.current_workflow else ""
result += f" {i:2d}. {name}{current}\n"
result += f"\n💡 使用 'ai_image switch <工作流名>' 切换工作流"
return result
def _cmd_switch(self, name):
"""切换工作流"""
if not name.strip():
return "❌ 请提供工作流名称。先用 'ai_image workflows' 查看列表"
# 模糊匹配
workflows = self._fetch_workflows()
if not workflows:
return "❌ 获取工作流列表失败"
matched = None
name_lower = name.strip().lower()
for wf in workflows:
wf_path = wf.get("path", "")
if name_lower == wf_path.lower():
matched = wf_path
break
if name_lower in wf_path.lower():
matched = wf_path
if not matched:
# 尝试数字索引
try:
idx = int(name.strip()) - 1
if 0 <= idx < len(workflows):
matched = workflows[idx].get("path", "")
except ValueError:
pass
if not matched:
return f"❌ 未找到匹配的工作流: {name}\n💡 先用 'ai_image workflows' 查看列表"
self.current_workflow = matched
return f"✅ 已切换工作流: {matched}"
def _cmd_set_width(self, val):
"""设置宽度"""
try:
w = int(val.strip())
if w < 64 or w > 1344:
return "❌ 宽度范围: 64 ~ 1344"
self.width = w
return f"✅ 宽度已设置为: {w}"
except ValueError:
return "❌ 请输入有效数字"
def _cmd_set_height(self, val):
"""设置高度"""
try:
h = int(val.strip())
if h < 64 or h > 1344:
return "❌ 高度范围: 64 ~ 1344"
self.height = h
return f"✅ 高度已设置为: {h}"
except ValueError:
return "❌ 请输入有效数字"
def _cmd_set_size(self, val):
"""同时设置宽高"""
parts = val.strip().split()
if len(parts) < 2:
return "❌ 用法: ai_image size <宽> <高>"
try:
w, h = int(parts[0]), int(parts[1])
if w < 64 or w > 1344 or h < 64 or h > 1344:
return "❌ 宽高范围: 64 ~ 1344"
self.width = w
self.height = h
return f"✅ 尺寸已设置为: {w} x {h}"
except ValueError:
return "❌ 请输入有效数字"
def _cmd_set_rewrite(self, val):
"""设置改写模式"""
val = val.strip().lower()
if val in ["on", "开", "1", "true", "yes"]:
self.rewrite = True
return "✅ 改写模式已开启 — LLM 将智能重写 prompt"
elif val in ["off", "关", "0", "false", "no"]:
self.rewrite = False
return "✅ 改写模式已关闭 — 普通翻译/追加模式"
else:
self.rewrite = not self.rewrite
status = "开启" if self.rewrite else "关闭"
return f"✅ 改写模式已{status}"
def _cmd_set_override(self, val):
"""设置覆写模式"""
val = val.strip().lower()
if val in ["on", "开", "1", "true", "yes"]:
self.override = True
return "✅ 覆写模式已开启 — 忽略工作流内置 prompt"
elif val in ["off", "关", "0", "false", "no"]:
self.override = False
return "✅ 覆写模式已关闭 — 保留工作流内置 prompt"
else:
self.override = not self.override
status = "开启" if self.override else "关闭"
return f"✅ 覆写模式已{status}"
def _cmd_status(self):
"""查看当前状态"""
result = " AI 生图状态:\n\n"
result += f" 工作流: {self.current_workflow or '(默认)'}\n"
result += f" 直接 Tag: {self.direct_prompt or '(未设置)'}\n"
result += f" 自然语言: {self.nl_prompt or '(未设置)'}\n"
result += f" 改写模式: {'✅ 开启' if self.rewrite else '❌ 关闭'}\n"
result += f" 覆写模式: {'✅ 开启' if self.override else '❌ 关闭'}\n"
result += f" 宽度: {self.width or '(默认)'}\n"
result += f" 高度: {self.height or '(默认)'}\n"
result += f" 保存目录: {self.save_dir}\n"
result += f" 生成中: {'是' if self.is_generating else '否'}\n"
return result
# ── 核心生图逻辑 ──
def _fetch_workflows(self):
"""获取工作流列表"""
if self.workflows_cache:
return self.workflows_cache
try:
resp = requests.get(f"{self.API_URL}/workflows", timeout=15)
if resp.status_code == 200:
data = resp.json()
self.workflows_cache = data.get("workflows", [])
return self.workflows_cache
except Exception as e:
pass
return []
def _run_generation(self):
"""执行图片生成"""
if self.is_generating:
return "⚠️ 已有生成任务在进行中,请等待完成"
if not self.direct_prompt and not self.nl_prompt:
return "❌ 请提供至少一种 prompt(直接 Tag 或自然语言描述)"
self.is_generating = True
result_lines = []
image_paths = []
try:
# 获取默认工作流(如果未指定则使用列表中第一个)
workflow = self.current_workflow
if not workflow:
workflows = self._fetch_workflows()
if workflows:
# 优先选 "无Lora" 工作流,否则用第一个
for wf in workflows:
if "无lora" in wf.get("path", "").lower():
workflow = wf["path"]
break
if not workflow:
workflow = workflows[0].get("path", "")
# 构建 payload
payload = {
"workflow_path": workflow or "",
"inline_workflow": None,
"direct_prompt": self.direct_prompt,
"nl_prompt": self.nl_prompt,
"rewrite": self.rewrite,
"override": self.override,
"width": self.width,
"height": self.height,
}
result_lines.append(" 正在连接 ai.2x.nz ...")
result_lines.append(f" 直接 Tag: {self.direct_prompt[:80] or '(无)'}")
result_lines.append(f" 自然语言: {self.nl_prompt[:80] or '(无)'}")
result_lines.append(f" 工作流: {workflow or '(默认)'}")
result_lines.append("")
# 使用同步方式通过 REST + 轮询获取结果
# 由于 websocket-client 可能未安装,提供降级方案
if WEBSOCKET_AVAILABLE:
images = self._generate_via_ws(payload, result_lines)
else:
# 降级:尝试使用 requests + SSE 或提示安装
result_lines.append("⚠️ websocket-client 未安装,尝试安装...")
images = self._generate_via_ws_fallback(payload, result_lines)
if images:
image_paths = images
result_lines.append(f"\n✅ 生成完成!共 {len(images)} 张图片")
for i, path in enumerate(images, 1):
result_lines.append(f" {i}. {path}")
else:
result_lines.append("\n❌ 未生成任何图片")
except Exception as e:
result_lines.append(f"\n❌ 生成失败: {str(e)}")
finally:
self.is_generating = False
return "\n".join(result_lines)
def _generate_via_ws(self, payload, result_lines):
"""通过 WebSocket 生成图片"""
images = []
final_prompt = ""
error_msg = None
ws_result = {"images": [], "error": None, "final_prompt": "", "done": False}
def on_message(ws, message):
try:
m = json.loads(message)
msg_type = m.get("type", "")
if msg_type == "log":
result_lines.append(f" {m.get('message', '')}")
elif msg_type == "llm_start":
result_lines.append(" LLM 正在处理 prompt...")
elif msg_type == "llm_chunk":
pass # 流式输出,不逐块显示
elif msg_type == "llm_done":
result_lines.append(" LLM 处理完成")
elif msg_type == "progress":
node = m.get("node", "")
value = m.get("value", 0)
max_val = m.get("max", 0)
if max_val and max_val > 1:
pct = int(value * 100 / max_val)
result_lines.append(f"⏳ 进度: {node} {value}/{max_val} ({pct}%)")
else:
result_lines.append(f"⏳ 执行: {node}")
elif msg_type == "prompt_id":
fp = m.get("final_prompt", "")
if fp:
ws_result["final_prompt"] = fp
result_lines.append(f"\n 最终 Prompt:\n{fp[:300]}{'...' if len(fp) > 300 else ''}\n")
elif msg_type == "image":
url = m.get("url", "")
filename = m.get("filename", "")
if url:
# 下载图片
saved = self._download_image(url, filename)
if saved:
ws_result["images"].append(saved)
result_lines.append(f" 收到图片: {filename}")
elif msg_type == "done":
count = m.get("count", 0)
result_lines.append(f"✅ 服务端完成,共 {count} 张")
ws_result["done"] = True
ws.close()
elif msg_type == "error":
ws_result["error"] = m.get("message", "未知错误")
result_lines.append(f"❌ 错误: {ws_result['error']}")
ws.close()
except Exception as e:
result_lines.append(f"⚠️ 解析消息出错: {e}")
def on_error(ws, error):
ws_result["error"] = str(error)
result_lines.append(f"❌ WebSocket 错误: {error}")
def on_close(ws, close_status_code, close_msg):
ws_result["done"] = True
def on_open(ws):
ws.send(json.dumps(payload))
try:
ws = websocket.WebSocketApp(
self.WS_URL,
on_message=on_message,
on_error=on_error,
on_close=on_close,
on_open=on_open
)
# 在独立线程中运行 WebSocket
ws_thread = threading.Thread(target=ws.run_forever, kwargs={"ping_timeout": 10})
ws_thread.daemon = True
ws_thread.start()
# 等待完成(最多 5 分钟)
timeout = 300
start = time.time()
while not ws_result["done"] and (time.time() - start) < timeout:
time.sleep(1)
if ws_result["error"]:
raise Exception(ws_result["error"])
if not ws_result["done"]:
ws.close()
raise Exception("生成超时(超过 5 分钟)")
return ws_result["images"]
except Exception as e:
raise Exception(f"WebSocket 连接失败: {e}")
def _generate_via_ws_fallback(self, payload, result_lines):
"""降级方案:尝试自动安装 websocket-client"""
try:
import subprocess
result_lines.append(" 正在安装 websocket-client ...")
subprocess.run(
["pip", "install", "websocket-client"],
capture_output=True, timeout=60
)
import importlib
global websocket
websocket = importlib.import_module("websocket")
global WEBSOCKET_AVAILABLE
WEBSOCKET_AVAILABLE = True
result_lines.append("✅ websocket-client 安装成功")
return self._generate_via_ws(payload, result_lines)
except Exception as e:
result_lines.append(f"❌ 无法安装 websocket-client: {e}")
result_lines.append("💡 请手动运行: pip install websocket-client")
return []
def _download_image(self, url, filename):
"""下载图片到本地"""
try:
# 确保 URL 是完整的
if url.startswith("/"):
url = f"{self.BASE_URL}{url}"
elif not url.startswith("http"):
url = f"{self.BASE_URL}/api/output/file?path={quote(url)}"
# 创建保存目录
os.makedirs(self.save_dir, exist_ok=True)
# 生成文件名
if not filename:
filename = f"img_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
# 清理文件名
filename = re.sub(r'[<>:"/\\|?*]', '_', filename)
save_path = os.path.join(self.save_dir, filename)
# 如果文件已存在,添加序号
if os.path.exists(save_path):
name, ext = os.path.splitext(filename)
for i in range(1, 100):
new_path = os.path.join(self.save_dir, f"{name}_{i}{ext}")
if not os.path.exists(new_path):
save_path = new_path
break
# 下载
resp = requests.get(url, timeout=60, stream=True)
if resp.status_code == 200:
with open(save_path, 'wb') as f:
for chunk in resp.iter_content(chunk_size=8192):
f.write(chunk)
return save_path
else:
return None
except Exception as e:
return None
# 兼容 Liugin 类名
Liugin = Plugin
+1036
View File
File diff suppressed because it is too large. Load diff
+370
View File
@@ -0,0 +1,370 @@
"""
图像生成插件 - 基于 ai.2x.nz 的 ComfyUI 在线服务
支持自然语言描述和英文 Tag 两种模式
"""
import os
import json
import asyncio
import urllib.request
import time
from typing import Optional
class Plugin:
"""图像生成工具 — 自然语言/Tag 生图,基于 ComfyUI 在线服务"""
def __init__(self):
self.usage = """图像生成工具 — 基于 ai.2x.nz 的 ComfyUI 在线服务
操作:
generate <描述> - 自然语言描述生成图片(中文/英文均可)
tag <英文tags> - 直接 Tag 模式(逗号分隔的英文标签)
list - 列出可用的工作流/角色
status - 查看 GPU 状态
示例:
image_generator generate 一个白发蓝眼的猫耳少女在写代码
image_generator tag masterpiece, 1girl, white hair, blue eyes, cat ears, coding
image_generator generate 画一个赛博朋克风格的城市夜景 --width 1024 --height 768
说明:
- 自然语言模式会经过 LLM 翻译为 Tag,效果更好
- 直接 Tag 模式适合熟悉 Danbooru 标签的用户
- 默认分辨率 1024x1344(WAI 推荐)
- 最大分辨率 1344x1344
- 生成的图片保存在 screenshots/ 目录
"""
self.cli = None
self.api_base = "https://ai.2x.nz"
self.ws_url = "wss://ai.2x.nz/ws/run"
self.default_workflow = "WAI - 无Lora.json"
self.output_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "screenshots")
def set_cli(self, cli):
self.cli = cli
def get_tool_info(self):
return {
"name": "image_generator",
"description": "图像生成 — 自然语言/Tag 生图,基于 ComfyUI 在线服务(ai.2x.nz),支持中文描述",
"keywords": ["图像", "生成", "画图", "绘图", "AI画", "生图", "image", "generate", "draw", "paint",
"diffusion", "comfyui", "stable", "sd", "文生图", "text2img"],
"usage": self.usage
}
def handle(self, args: str) -> str:
try:
parts = args.strip().split(maxsplit=1)
if not parts:
return "错误:请提供操作类型\n\n" + self.usage
op = parts[0].lower()
rest = parts[1] if len(parts) > 1 else ""
handlers = {
"generate": self._generate,
"tag": self._generate_tag,
"gen": self._generate,
"g": self._generate,
"list": self._list_workflows,
"ls": self._list_workflows,
"status": self._gpu_status,
}
handler = handlers.get(op)
if not handler:
# 如果没有指定操作,把整个 args 当作自然语言描述
return self._generate(args)
return handler(rest)
except Exception as e:
return f"图像生成错误: {e}"
def _generate(self, args: str) -> str:
"""自然语言模式生成图片"""
# 解析 --width 和 --height 参数
width, height = 1024, 1344
prompt = args.strip()
if "--width" in prompt or "--height" in prompt or "--w" in prompt or "--h" in prompt:
import re
w_match = re.search(r'--w(?:idth)?\s+(\d+)', prompt)
h_match = re.search(r'--h(?:eight)?\s+(\d+)', prompt)
if w_match:
width = int(w_match.group(1))
prompt = prompt[:w_match.start()] + prompt[w_match.end():]
if h_match:
height = int(h_match.group(1))
prompt = prompt[:h_match.start()] + prompt[h_match.end():]
prompt = prompt.strip()
if not prompt:
return "错误:请提供图片描述\n示例: image_generator generate 一个白发猫耳少女"
# 限制分辨率
width = min(width, 1344)
height = min(height, 1344)
return self._run_generation(prompt, "", width, height, rewrite=True, override=False)
def _generate_tag(self, args: str) -> str:
"""直接 Tag 模式"""
width, height = 1024, 1344
prompt = args.strip()
if "--width" in prompt or "--height" in prompt or "--w" in prompt or "--h" in prompt:
import re
w_match = re.search(r'--w(?:idth)?\s+(\d+)', prompt)
h_match = re.search(r'--h(?:eight)?\s+(\d+)', prompt)
if w_match:
width = int(w_match.group(1))
prompt = prompt[:w_match.start()] + prompt[w_match.end():]
if h_match:
height = int(h_match.group(1))
prompt = prompt[:h_match.start()] + prompt[h_match.end():]
prompt = prompt.strip()
if not prompt:
return "错误:请提供 Tag\n示例: image_generator tag masterpiece, 1girl, white hair, blue eyes"
width = min(width, 1344)
height = min(height, 1344)
return self._run_generation("", prompt, width, height, rewrite=False, override=False)
def _run_generation(self, nl_prompt: str, direct_prompt: str, width: int, height: int,
rewrite: bool = False, override: bool = False) -> str:
"""执行图像生成(同步包装)"""
try:
import websocket as ws_lib
except ImportError:
# 回退:用 urllib 的方式不行,WebSocket 必须用专用库
# 尝试用 subprocess 调用 python 脚本
return self._run_generation_subprocess(nl_prompt, direct_prompt, width, height, rewrite, override)
try:
start_time = time.time()
ws = ws_lib.create_connection(self.ws_url, timeout=180)
payload = {
"workflow_path": self.default_workflow,
"inline_workflow": None,
"direct_prompt": direct_prompt,
"nl_prompt": nl_prompt,
"rewrite": rewrite,
"override": override,
"width": width,
"height": height,
}
ws.send(json.dumps(payload))
image_path = None
final_prompt = ""
error_msg = ""
while True:
try:
raw = ws.recv()
data = json.loads(raw)
msg_type = data.get("type", "")
if msg_type == "image":
url = data.get("url", "")
filename = data.get("filename", "")
if url:
image_path = self._download_image(url, filename)
elif msg_type == "final_prompt":
final_prompt = data.get("final_prompt", "") if isinstance(data.get("final_prompt"), str) else data.get("prompt", "")
elif msg_type == "done":
break
elif msg_type == "error":
error_msg = data.get("message", "未知错误")
break
except Exception as e:
error_msg = str(e)
break
ws.close()
elapsed = time.time() - start_time
if error_msg:
return f" 生成失败: {error_msg}"
if image_path:
size = os.path.getsize(image_path)
size_str = f"{size/1024:.0f}KB" if size < 1024*1024 else f"{size/1024/1024:.1f}MB"
result = f" 图片生成成功!\n"
result += f" 路径: {image_path}\n"
result += f" 大小: {size_str}\n"
result += f" 分辨率: {width}x{height}\n"
result += f"⏱ 耗时: {elapsed:.1f}s\n"
if final_prompt:
result += f" Prompt: {final_prompt[:120]}...\n"
result += f"\n💡 提示: 用 'send_image {image_path}' 可以推送到手机"
return result
else:
return " 生成完成但未收到图片数据"
except Exception as e:
return f" 连接失败: {e}\n💡 请检查网络连接或稍后重试"
def _run_generation_subprocess(self, nl_prompt: str, direct_prompt: str, width: int, height: int,
rewrite: bool, override: bool) -> str:
"""用子进程执行 WebSocket 生成(无需 websocket 库)"""
import subprocess
import tempfile
script = f'''
import json, asyncio, urllib.request, os, sys
async def main():
try:
import websockets
except ImportError:
print("ERROR:需要安装 websockets: pip install websockets")
sys.exit(1)
uri = "wss://ai.2x.nz/ws/run"
async with websockets.connect(uri) as ws:
payload = {{
"workflow_path": "{self.default_workflow}",
"inline_workflow": None,
"direct_prompt": {json.dumps(direct_prompt)},
"nl_prompt": {json.dumps(nl_prompt)},
"rewrite": {"true" if rewrite else "false"},
"override": {"true" if override else "false"},
"width": {width},
"height": {height},
}}
await ws.send(json.dumps(payload))
image_url = None
while True:
try:
msg = await asyncio.wait_for(ws.recv(), timeout=180)
data = json.loads(msg)
t = data.get("type", "")
if t == "image":
image_url = data.get("url", "")
elif t == "done":
break
elif t == "error":
print(f"ERROR:{{data.get('message','')}}")
break
except asyncio.TimeoutError:
print("ERROR:超时")
break
if image_url:
full_url = f"https://ai.2x.nz{{image_url}}"
out_path = os.path.join("{self.output_dir}", f"generated_{{os.path.basename(image_url)}}")
os.makedirs(os.path.dirname(out_path), exist_ok=True)
urllib.request.urlretrieve(full_url, out_path)
print(f"OK:{{out_path}}")
asyncio.run(main())
'''
script_path = os.path.join(self.output_dir, "_gen_temp.py")
os.makedirs(self.output_dir, exist_ok=True)
with open(script_path, 'w') as f:
f.write(script)
try:
result = subprocess.run(
["python3", script_path],
capture_output=True, text=True, timeout=200
)
output = result.stdout.strip()
if output.startswith("OK:"):
image_path = output[3:]
size = os.path.getsize(image_path)
size_str = f"{size/1024:.0f}KB" if size < 1024*1024 else f"{size/1024/1024:.1f}MB"
res = f" 图片生成成功!\n"
res += f" 路径: {image_path}\n"
res += f" 大小: {size_str}\n"
res += f" 分辨率: {width}x{height}\n"
res += f"\n💡 提示: 用 'send_image {image_path}' 可以推送到手机"
return res
elif output.startswith("ERROR:"):
return f" {output[6:]}"
else:
return f" 生成异常:\n{result.stdout}\n{result.stderr}"
except subprocess.TimeoutExpired:
return " 生成超时(>200s),请稍后重试"
finally:
try:
os.remove(script_path)
except:
pass
def _download_image(self, url: str, filename: str) -> str:
"""下载生成的图片"""
full_url = f"{self.api_base}{url}"
os.makedirs(self.output_dir, exist_ok=True)
out_path = os.path.join(self.output_dir, f"gen_{filename}")
urllib.request.urlretrieve(full_url, out_path)
return out_path
def _list_workflows(self, args: str) -> str:
"""列出可用的工作流"""
try:
import urllib.request
req = urllib.request.Request(f"{self.api_base}/api/workflows")
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.loads(resp.read())
workflows = data.get("workflows", [])
result = f" 可用工作流 ({len(workflows)} 个):\n\n"
lora_workflows = []
no_lora = []
for w in workflows:
name = w.get("path", "")
if "无Lora" in name or "no" in name.lower():
no_lora.append(name)
else:
lora_workflows.append(name)
if no_lora:
result += " 通用(无角色绑定):\n"
for w in no_lora:
result += f" • {w}\n"
if lora_workflows:
result += "\n 角色工作流:\n"
for w in lora_workflows:
result += f" • {w}\n"
result += "\n💡 使用 'image_generator generate <描述>' 自然语言生图"
result += "\n💡 使用 'image_generator tag <tags>' 直接 Tag 生图"
return result
except Exception as e:
return f" 获取工作流列表失败: {e}"
def _gpu_status(self, args: str) -> str:
"""查看 GPU 状态"""
try:
import urllib.request
req = urllib.request.Request(f"{self.api_base}/api/gpu")
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.loads(resp.read())
result = " 服务器 GPU 状态:\n"
for gpu in data.get("gpus", []):
name = gpu.get("name", "未知")
util = gpu.get("utilization", 0)
mem_used = gpu.get("memory_used", 0)
mem_total = gpu.get("memory_total", 0)
temp = gpu.get("temperature", 0)
result += f" {name}\n"
result += f" GPU 负载: {util}%\n"
result += f" 显存: {mem_used}/{mem_total} MiB\n"
result += f" 温度: {temp}°C\n"
return result
except Exception as e:
return f" 获取 GPU 状态失败: {e}"
+185
View File
@@ -0,0 +1,185 @@
"""
记忆系统插件 — 包装 xcli_core/memory.py 中的 MemoryManager
使 AI 可以通过工具接口读写日记、搜索记忆、管理聊天记录
"""
import os
import sys
# 添加项目根目录到 path
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(current_dir)
sys.path.insert(0, project_root)
from xcli_core.memory import MemoryManager
class Plugin:
"""持久化记忆系统 — AI 可写日记、搜索记忆、管理聊天记录"""
def __init__(self):
self.cli = None
self.memory = None
self.usage = """持久化记忆系统工具
操作:
diary <内容> - 写当日日记
diary_ai <内容> - AI 写观察日记(标记为 AI 观察)
remember <内容> - 记录到长期记忆 (MEMORY.md)
search <关键词> - 全局搜索(日记+记忆+聊天记录)
context - 查看当前注入的记忆上下文
diary_list - 列出所有日记文件
diary_read <日期> - 读取指定日期的日记(如 2026-04-27)
chat_list - 列出最近的聊天记录
chat_search <关键词> - 在聊天记录中搜索
memory_read - 读取 MEMORY.md 长期记忆
memory_write <内容> - 写入 MEMORY.md(追加)
JSON 格式示例:
{"action": "use_tool", "tool": "memory_plugin", "args": "diary 今天完成了API对接"}
{"action": "use_tool", "tool": "memory_plugin", "args": "search 定时任务"}
{"action": "use_tool", "tool": "memory_plugin", "args": "remember 用户偏好深色主题"}
"""
def set_cli(self, cli):
self.cli = cli
# 初始化 MemoryManager
project_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
self.memory = MemoryManager(project_dir)
# 挂载到 cli 实例上,让 cli_core.py 能访问
cli.memory_manager = self.memory
def get_tool_info(self):
return {
"name": "memory_plugin",
"description": "持久化记忆系统 — AI 可写日记、记录长期记忆、搜索历史对话和日记。支持自动压缩上下文。",
"keywords": ["记忆", "memory", "日记", "diary", "搜索", "search", "历史", "history",
"remember", "聊天记录", "chat", "上下文", "context", "压缩", "compress"],
"usage": self.usage
}
def handle(self, args: str) -> str:
if not self.memory:
return "错误:记忆系统未初始化"
if not args or not args.strip():
return self._help()
parts = args.strip().split(None, 1)
action = parts[0].lower()
rest = parts[1].strip() if len(parts) > 1 else ""
handlers = {
"diary": self._diary,
"diary_ai": self._diary_ai,
"remember": self._remember,
"search": self._search,
"context": self._context,
"diary_list": self._diary_list,
"diary_read": self._diary_read,
"chat_list": self._chat_list,
"chat_search": self._chat_search,
"memory_read": self._memory_read,
"memory_write": self._memory_write,
"help": lambda _: self._help(),
}
handler = handlers.get(action)
if not handler:
return f"未知操作: {action}\n" + self._help()
return handler(rest)
def _diary(self, content: str) -> str:
if not content:
return "错误:请提供日记内容\n用法: diary <内容>"
self.memory.write_diary(content)
return f" 日记已写入 {self.memory._today_file()}"
def _diary_ai(self, content: str) -> str:
if not content:
return "错误:请提供 AI 观察内容\n用法: diary_ai <内容>"
self.memory.write_ai_diary(content)
return f" AI 观察日记已写入"
def _remember(self, content: str) -> str:
if not content:
return "错误:请提供要记住的内容\n用法: remember <内容>"
self.memory.append_memory("备忘", f"- {content}")
return f" 已记录到 MEMORY.md 长期记忆"
def _search(self, keyword: str) -> str:
if not keyword:
return "错误:请提供搜索关键词\n用法: search <关键词>"
results = self.memory.search_memory(keyword)
if not results:
return f" 未找到与 '{keyword}' 相关的记忆"
lines = [f" 搜索 '{keyword}' 的结果 ({len(results)} 条):"]
for r in results:
source = r.get("source", "")
snippet = r.get("snippet", "")
role = r.get("role", "")
line_num = r.get("line", "")
prefix = f"[{role}] " if role else ""
line_info = f" (L{line_num})" if line_num else ""
lines.append(f" {source}{line_info}: {prefix}{snippet}")
return "\n".join(lines)
def _context(self, _: str) -> str:
ctx = self.memory.build_memory_context()
return ctx if ctx else " 当前没有记忆上下文"
def _diary_list(self, _: str) -> str:
files = self.memory.list_diary_files()
if not files:
return " 还没有日记文件"
return " 日记文件:\n" + "\n".join(f" {f}" for f in files[:20])
def _diary_read(self, date_str: str) -> str:
if not date_str:
return "错误:请提供日期\n用法: diary_read <日期,如 2026-04-27>"
content = self.memory.load_diary(date_str)
return content if content else f" 没有找到 {date_str} 的日记"
def _chat_list(self, _: str) -> str:
chats = self.memory.list_chats()
if not chats:
return " 没有保存的聊天记录"
lines = [" 最近的聊天记录:"]
for c in chats:
lines.append(f" {c['name']} ({c['time']}, {c['size_kb']}KB)")
return "\n".join(lines)
def _chat_search(self, keyword: str) -> str:
if not keyword:
return "错误:请提供关键词\n用法: chat_search <关键词>"
results = self.memory.search_chats(keyword)
if not results:
return f" 未在聊天记录中找到 '{keyword}'"
lines = [f" 聊天记录搜索 '{keyword}':"]
for r in results:
lines.append(f" {r['file']} [{r['role']}]: {r['snippet']}")
return "\n".join(lines)
def _memory_read(self, _: str) -> str:
content = self.memory.load_memory()
return content if content else " MEMORY.md 为空"
def _memory_write(self, content: str) -> str:
if not content:
return "错误:请提供内容\n用法: memory_write <内容>"
self.memory.append_memory("备忘", f"- {content}")
return f" 已写入 MEMORY.md"
def _help(self) -> str:
return """ 记忆系统用法:
diary <内容> - 写当日日记
diary_ai <内容> - AI 写观察日记
remember <内容> - 记到长期记忆 (MEMORY.md)
search <关键词> - 全局搜索记忆
context - 查看记忆上下文
diary_list - 列出日记
diary_read <日期> - 读取日记
chat_list - 列出聊天记录
chat_search <关键词> - 搜索聊天记录
memory_read - 读取 MEMORY.md
memory_write <内容> - 写入 MEMORY.md"""
+141
View File
@@ -357,6 +357,147 @@ class TestBrowserAuto:
# 引擎加载测试
# ══════════════════════════════════════
class TestGuiAuto:
"""gui_auto 功能测试"""
@pytest.fixture
def gui(self):
mod = load_module("gui_auto", os.path.join(PLUGINS_DIR, "gui_auto.py"))
return mod.Plugin()
def test_tool_info(self, gui):
info = gui.get_tool_info()
assert info["name"] == "gui_auto"
assert "GUI" in info["description"]
assert len(info["keywords"]) >= 5
def test_mcp_definition(self, gui):
mcp = gui.get_mcp_definition()
assert mcp["name"] == "gui_auto"
assert "inputSchema" in mcp
assert "operation" in mcp["inputSchema"]["properties"]
def test_convert_mcp_args(self, gui):
args = {"operation": "click", "target": "确定"}
result = gui.convert_mcp_args(args)
assert "click" in result
assert "确定" in result
def test_convert_mcp_args_xy(self, gui):
args = {"operation": "click", "x": 100, "y": 200}
result = gui.convert_mcp_args(args)
assert "--xy" in result
assert "100" in result
def test_convert_mcp_args_snapshot(self, gui):
args = {"operation": "snapshot", "depth": 3}
result = gui.convert_mcp_args(args)
assert "snapshot" in result
assert "3" in result
def test_empty_args(self, gui):
result = gui.handle("")
assert "错误" in result
def test_invalid_operation(self, gui):
result = gui.handle("nonexistent_op")
assert "不支持" in result
def test_click_no_args(self, gui):
result = gui.handle("click")
assert "错误" in result
def test_type_no_args(self, gui):
result = gui.handle("type")
assert "错误" in result
def test_type_missing_text(self, gui):
result = gui.handle("type 按钮")
assert "错误" in result
def test_find_no_args(self, gui):
result = gui.handle("find")
assert "错误" in result
def test_findall_no_args(self, gui):
result = gui.handle("findall")
assert "错误" in result
def test_info_no_args(self, gui):
result = gui.handle("info")
assert "错误" in result
def test_value_no_args(self, gui):
result = gui.handle("value")
assert "错误" in result
def test_tree_no_args(self, gui):
result = gui.handle("tree")
assert "错误" in result
def test_wait_no_args(self, gui):
result = gui.handle("wait")
assert "错误" in result
def test_exists_no_args(self, gui):
result = gui.handle("exists")
assert "错误" in result
def test_highlight_no_args(self, gui):
result = gui.handle("highlight")
assert "错误" in result
def test_state_no_args(self, gui):
result = gui.handle("state")
assert "错误" in result
def test_keys_no_args(self, gui):
result = gui.handle("keys")
assert "错误" in result
def test_focus_no_args(self, gui):
result = gui.handle("focus")
assert "错误" in result
def test_click_xy_format_error(self, gui):
result = gui.handle("click --xy abc")
assert "错误" in result
def test_format_element_info(self, gui):
el = {
"name": "测试按钮", "type": "Button", "class": "Button",
"automation_id": "btn_ok",
"bounds": {"left": 100, "top": 200, "width": 80, "height": 30,
"right": 180, "bottom": 230},
"enabled": True, "focused": False
}
result = gui._format_element_info(el)
assert "测试按钮" in result
assert "Button" in result
assert "btn_ok" in result
def test_format_element_info_detailed(self, gui):
el = {
"name": "输入框", "type": "Edit", "class": "Edit",
"automation_id": "", "enabled": True, "focused": True,
"value": "hello",
"bounds": {"left": 0, "top": 0, "width": 200, "height": 25,
"right": 200, "bottom": 25}
}
result = gui._format_element_info(el, detailed=True)
assert "输入框" in result
assert "hello" in result
def test_all_operations_route(self, gui):
"""所有操作都能正确路由"""
for op in ["snapshot", "screenshot", "windows", "focus", "click",
"rightclick", "doubleclick", "type", "clear", "keys",
"scroll", "find", "findall", "info", "value", "tree",
"wait", "exists", "highlight", "state"]:
result = gui.handle(f"{op} test")
assert isinstance(result, str)
class TestSubAgent:
"""sub_agent 功能测试"""
+381
View File
@@ -0,0 +1,381 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>小狸 Pro-CLI v5.1.2 · 竞品实力对比</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{--bg:#06080d;--bg2:#0d1117;--bg3:#161b22;--border:#21262d;--txt:#e6edf3;--dim:#8b949e;--mute:#484f58;--blue:#58a6ff;--purple:#bc8cff;--cyan:#39d2c0;--green:#3fb950;--red:#f85149;--yellow:#d29922;--orange:#f0883e}
body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;line-height:1.7}
/* ── Hero ── */
.hero{text-align:center;padding:80px 20px 50px;background:linear-gradient(180deg,#0d1117 0%,#06080d 100%);position:relative;overflow:hidden}
.hero::before{content:'';position:absolute;top:0;left:50%;width:600px;height:600px;transform:translateX(-50%);background:radial-gradient(circle,rgba(88,166,255,.08) 0%,transparent 70%);pointer-events:none}
.hero h1{font-size:2.6em;font-weight:800;background:linear-gradient(135deg,#58a6ff,#bc8cff,#39d2c0);-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:8px}
.hero .sub{color:var(--dim);font-size:1.1em;margin-bottom:24px}
.hero .tags{display:flex;justify-content:center;gap:10px;flex-wrap:wrap}
.tag{padding:5px 14px;border-radius:20px;font-size:.82em;font-weight:600;border:1px solid}
.tag-b{background:rgba(88,166,255,.1);color:var(--blue);border-color:rgba(88,166,255,.25)}
.tag-p{background:rgba(188,140,255,.1);color:var(--purple);border-color:rgba(188,140,255,.25)}
.tag-g{background:rgba(63,185,80,.1);color:var(--green);border-color:rgba(63,185,80,.25)}
.tag-c{background:rgba(57,210,192,.1);color:var(--cyan);border-color:rgba(57,210,192,.25)}
.tag-y{background:rgba(210,153,34,.1);color:var(--yellow);border-color:rgba(210,153,34,.25)}
.container{max-width:1100px;margin:0 auto;padding:0 24px}
section{padding:56px 0;border-bottom:1px solid rgba(33,38,45,.5)}
section:last-child{border:none}
h2{font-size:1.7em;margin-bottom:6px;background:linear-gradient(90deg,var(--blue),var(--cyan));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
h2 .em{-webkit-text-fill-color:initial}
.sec-desc{color:var(--dim);margin-bottom:32px;font-size:1.02em}
/* ── What's New ── */
.new-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:16px}
.new-card{background:var(--bg3);border:1px solid var(--border);border-radius:12px;padding:24px;transition:.3s;position:relative;overflow:hidden}
.new-card:hover{border-color:var(--blue);box-shadow:0 0 24px rgba(88,166,255,.1);transform:translateY(-2px)}
.new-card .icon{font-size:1.8em;margin-bottom:8px}
.new-card h3{font-size:1.08em;margin-bottom:8px;color:#f0f6fc}
.new-card p{color:var(--dim);font-size:.93em}
.new-card .badge-new{position:absolute;top:12px;right:12px;background:var(--green);color:#000;font-size:.72em;font-weight:700;padding:2px 8px;border-radius:10px}
/* ── Compare Table ── */
.tbl-wrap{overflow-x:auto;border-radius:12px;border:1px solid var(--border)}
table{width:100%;border-collapse:collapse;font-size:.9em}
thead th{background:var(--bg3);color:#f0f6fc;padding:13px 16px;text-align:left;font-weight:700;position:sticky;top:0;border-bottom:1px solid var(--border)}
tbody td{padding:12px 16px;border-bottom:1px solid rgba(33,38,45,.5)}
tbody tr:hover{background:rgba(88,166,255,.04)}
tbody td:first-child{font-weight:600;color:var(--dim);white-space:nowrap}
.y{color:var(--green);font-weight:700}
.n{color:var(--red);opacity:.5}
.p{color:var(--yellow)}
.hl{background:rgba(88,166,255,.06);font-weight:600}
/* ── Score Cards ── */
.score-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:16px;margin-top:20px}
.score{background:var(--bg3);border:1px solid var(--border);border-radius:12px;padding:22px;text-align:center;transition:.3s}
.score:hover{border-color:var(--blue);box-shadow:0 0 20px rgba(88,166,255,.08)}
.score .nm{font-size:1.05em;font-weight:700;margin-bottom:4px}
.score .tot{font-size:2.6em;font-weight:800;margin:6px 0}
.score .sub{font-size:.82em;color:var(--dim)}
.score .bars{text-align:left;margin-top:14px;font-size:.86em;color:var(--dim)}
.score .bars .row{display:flex;justify-content:space-between;margin-bottom:3px}
.score .bars .bw{background:rgba(255,255,255,.05);border-radius:4px;height:6px;margin-bottom:8px;overflow:hidden}
.score .bars .bd{height:100%;border-radius:4px}
/* ── Radar ── */
.radar-wrap{display:flex;justify-content:center;margin:30px 0}
.radar{position:relative;width:400px;height:400px}
.radar svg{width:100%;height:100%}
/* ── SWOT ── */
.swot{display:grid;grid-template-columns:1fr 1fr;gap:16px}
.sc{border-radius:12px;padding:24px;border:1px solid}
.sc h3{font-size:1.08em;margin-bottom:12px}
.sc ul{list-style:none;padding:0}
.sc li{padding:5px 0 5px 22px;position:relative;color:var(--dim);font-size:.93em}
.sc li::before{content:'→';position:absolute;left:0;font-weight:700}
.sc-s{background:rgba(63,185,80,.04);border-color:rgba(63,185,80,.2)}.sc-s h3{color:var(--green)}.sc-s li::before{color:var(--green)}
.sc-w{background:rgba(248,81,73,.04);border-color:rgba(248,81,73,.2)}.sc-w h3{color:var(--red)}.sc-w li::before{color:var(--red)}
.sc-o{background:rgba(88,166,255,.04);border-color:rgba(88,166,255,.2)}.sc-o h3{color:var(--blue)}.sc-o li::before{color:var(--blue)}
.sc-t{background:rgba(240,136,62,.04);border-color:rgba(240,136,62,.2)}.sc-t h3{color:var(--orange)}.sc-t li::before{color:var(--orange)}
/* ── Verdict ── */
.verdict{background:linear-gradient(135deg,rgba(88,166,255,.06),rgba(188,140,255,.06));border:1px solid rgba(88,166,255,.2);border-radius:14px;padding:36px;text-align:center;margin-top:28px}
.verdict h3{font-size:1.4em;margin-bottom:10px;color:#f0f6fc}
.verdict p{color:var(--dim);max-width:680px;margin:0 auto;font-size:1.02em}
footer{text-align:center;padding:36px 20px;color:var(--mute);font-size:.86em;border-top:1px solid rgba(33,38,45,.5)}
footer a{color:var(--blue);text-decoration:none}
@media(max-width:768px){.hero h1{font-size:1.7em}.swot{grid-template-columns:1fr}table{font-size:.8em}}
</style>
</head>
<body>
<div class="hero">
<h1>小狸 Pro-CLI v5.1.2</h1>
<p class="sub">新增自动语法检查 · Diff 弹窗 · Web UI 后,与同类产品的实力对比</p>
<div class="tags">
<span class="tag tag-g"> 新增语法检查 · 11 种语言</span>
<span class="tag tag-b"> 新增 Diff 弹窗 · 可开关</span>
<span class="tag tag-p"> 新增 Web UI · 纯静态</span>
<span class="tag tag-c">v5.1.2 · 2026-04-27</span>
<span class="tag tag-y">16 插件 · 142 操作 · 约 19K 行</span>
</div>
</div>
<!-- ════════════════ 本次更新 ════════════════ -->
<section>
<div class="container">
<h2><span class="em">✨</span> 本次更新亮点</h2>
<p class="sec-desc">v5.1.2 新增 3 大功能,补齐了编辑后自动验证和可视化界面的短板。</p>
<div class="new-grid">
<div class="new-card">
<span class="badge-new">NEW</span>
<div class="icon"> </div>
<h3>自动语法检查</h3>
<p>编辑操作完成后自动触发,覆盖 <strong>11 种语言</strong>:Python (ast.parse + py_compile 双重验证)、JavaScript、TypeScript、JSON、YAML、TOML、HTML/XML、CSS、Shell (bash -n)、SQL。通过显示 ✓,失败显示精确的行号/列号/错误描述。AI 也可主动调用 <code>syntax_check</code>。</p>
</div>
<div class="new-card">
<span class="badge-new">NEW</span>
<div class="icon"> </div>
<h3>Diff 弹窗</h3>
<p>每次编辑后自动弹出新终端窗口,显示修改前/后的完整内容(带行号)、unified diff 差异、+N/-M 行数统计。兼容 <strong>Windows (cmd) / macOS (Terminal) / Linux (gnome-terminal 等 6 种)</strong>。异步不阻塞,<code>diff_popup on/off</code> 一键开关。</p>
</div>
<div class="new-card">
<span class="badge-new">NEW</span>
<div class="icon"> </div>
<h3>Web UI</h3>
<p>纯静态 HTML 界面,<strong>零依赖、零后端</strong>,浏览器直接打开。三栏布局:左侧文件树、中间对话/编辑器(Tab 切换)、右侧状态面板。暗色主题、动画效果、快捷芯片、行号编辑器、Ctrl+S 保存。</p>
</div>
</div>
</div>
</section>
<!-- ════════════════ 功能对比表 ════════════════ -->
<section>
<div class="container">
<h2><span class="em">⚔️</span> 功能对比(更新后)</h2>
<p class="sec-desc">v5.1.2 新增功能后,小狸在语法验证和界面维度补齐了关键差距。绿色高亮 = 本次新增优势。</p>
<div class="tbl-wrap">
<table>
<thead>
<tr><th>维度</th><th>小狸 v5.1.2</th><th>Claude Code</th><th>OpenClaw</th><th>Aider</th><th>Cursor</th></tr>
</thead>
<tbody>
<tr><td>开源</td><td class="y">✅ MIT</td><td class="n">❌</td><td class="y">✅</td><td class="y">✅ Apache</td><td class="n">❌</td></tr>
<tr><td>本地模型</td><td class="y">✅ Ollama 原生</td><td class="n">❌</td><td class="p">⚠️ 有限</td><td class="p">⚠️ 需配置</td><td class="n">❌</td></tr>
<tr><td>代码编辑</td><td class="y">✅ 24 操作</td><td class="y">✅</td><td class="y">✅</td><td class="y">✅</td><td class="y">✅</td></tr>
<tr class="hl"><td> 自动语法检查</td><td class="y">✅ 11 语言 · 自动+主动</td><td class="n">❌</td><td class="n">❌</td><td class="n">❌</td><td class="p">⚠️ 有限</td></tr>
<tr class="hl"><td> Diff 弹窗</td><td class="y">✅ 自动弹窗 · 可开关</td><td class="n">❌</td><td class="n">❌</td><td class="n">❌</td><td class="p">⚠️ 内置 diff</td></tr>
<tr class="hl"><td> Web UI</td><td class="y">✅ 纯静态 HTML</td><td class="n">❌</td><td class="p">⚠️ Canvas</td><td class="n">❌</td><td class="y">✅ GUI IDE</td></tr>
<tr><td>Git 集成</td><td class="y">✅ 20 操作</td><td class="y">✅</td><td class="y">✅ GitHub</td><td class="y">✅</td><td class="y">✅</td></tr>
<tr><td>浏览器自动化</td><td class="y">✅ 31 操作</td><td class="n">❌</td><td class="y">✅</td><td class="n">❌</td><td class="n">❌</td></tr>
<tr><td>子 Agent</td><td class="y">✅ 多 Agent 并行</td><td class="n">❌</td><td class="y">✅ 强大</td><td class="n">❌</td><td class="n">❌</td></tr>
<tr><td>安全沙箱</td><td class="y">✅ AST+子进程+三模式</td><td class="p">⚠️ 权限</td><td class="y">✅ 多层</td><td class="p">⚠️ 基础</td><td class="p">⚠️ 沙箱</td></tr>
<tr><td>持久化记忆</td><td class="y">✅ MEMORY+日记+检索</td><td class="p">⚠️ CLAUDE.md</td><td class="y">✅ 完整</td><td class="p">⚠️ 有限</td><td class="p">⚠️ 有限</td></tr>
<tr><td>TUI 界面</td><td class="y">✅ Textual 动画</td><td class="n">❌ 纯 CLI</td><td class="p">⚠️</td><td class="n">❌</td><td class="y">✅ GUI</td></tr>
<tr><td>工程化工具</td><td class="y">✅ lint/format/test/build</td><td class="p">⚠️</td><td class="p">⚠️</td><td class="p">⚠️</td><td class="y">✅</td></tr>
<tr><td>语音识别</td><td class="y">✅ STT</td><td class="n">❌</td><td class="p">⚠️ TTS</td><td class="n">❌</td><td class="n">❌</td></tr>
<tr><td>远程控制</td><td class="y">✅ Clawli+WebSocket</td><td class="n">❌</td><td class="y">✅ 多渠道</td><td class="n">❌</td><td class="n">❌</td></tr>
<tr><td>测试覆盖</td><td class="y">✅ 113 用例</td><td class="n">❌</td><td class="p">⚠️</td><td class="y">✅</td><td class="n">❌</td></tr>
<tr><td>中文原生</td><td class="y">✅</td><td class="p">⚠️</td><td class="y">✅</td><td class="p">⚠️</td><td class="p">⚠️</td></tr>
<tr><td>价格</td><td class="y">免费</td><td>$20/月</td><td class="y">免费</td><td>$20/月</td><td>$20/月</td></tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- ════════════════ 评分对比 ════════════════ -->
<section>
<div class="container">
<h2><span class="em"> </span> 多维度评分(v5.1.2 更新后)</h2>
<p class="sec-desc">满分 10 分,基于代码审查和功能实测。</p>
<div class="score-row">
<div class="score" style="border-color:rgba(88,166,255,.35)">
<div class="nm" style="color:var(--blue)">小狸 v5.1.2</div>
<div class="tot" style="color:var(--blue)">7.3</div>
<div class="sub">↑ 0.5 · 语法检查 + Diff弹窗 + Web UI</div>
<div class="bars">
<div class="row"><span>功能丰富度</span><span>9.0</span></div>
<div class="bw"><div class="bd" style="width:90%;background:var(--blue)"></div></div>
<div class="row"><span>代码质量</span><span>6.5</span></div>
<div class="bw"><div class="bd" style="width:65%;background:var(--blue)"></div></div>
<div class="row"><span>架构设计</span><span>6.5</span></div>
<div class="bw"><div class="bd" style="width:65%;background:var(--blue)"></div></div>
<div class="row"><span>安全机制</span><span>8.0</span></div>
<div class="bw"><div class="bd" style="width:80%;background:var(--blue)"></div></div>
<div class="row"><span>用户体验</span><span>7.5</span></div>
<div class="bw"><div class="bd" style="width:75%;background:var(--blue)"></div></div>
<div class="row"><span>生态与社区</span><span>4.0</span></div>
<div class="bw"><div class="bd" style="width:40%;background:var(--blue)"></div></div>
<div class="row"><span>文档完整度</span><span>7.5</span></div>
<div class="bw"><div class="bd" style="width:75%;background:var(--blue)"></div></div>
<div class="row"><span>工程化水平</span><span>7.5</span></div>
<div class="bw"><div class="bd" style="width:75%;background:var(--blue)"></div></div>
</div>
</div>
<div class="score" style="border-color:rgba(240,136,62,.35)">
<div class="nm" style="color:var(--orange)">Claude Code</div>
<div class="tot" style="color:var(--orange)">8.5</div>
<div class="sub">模型能力碾压,但无语法检查/Diff弹窗</div>
<div class="bars">
<div class="row"><span>功能丰富度</span><span>7.5</span></div>
<div class="bw"><div class="bd" style="width:75%;background:var(--orange)"></div></div>
<div class="row"><span>代码质量</span><span>9.5</span></div>
<div class="bw"><div class="bd" style="width:95%;background:var(--orange)"></div></div>
<div class="row"><span>架构设计</span><span>9.0</span></div>
<div class="bw"><div class="bd" style="width:90%;background:var(--orange)"></div></div>
<div class="row"><span>安全机制</span><span>8.0</span></div>
<div class="bw"><div class="bd" style="width:80%;background:var(--orange)"></div></div>
<div class="row"><span>用户体验</span><span>9.0</span></div>
<div class="bw"><div class="bd" style="width:90%;background:var(--orange)"></div></div>
<div class="row"><span>生态与社区</span><span>8.0</span></div>
<div class="bw"><div class="bd" style="width:80%;background:var(--orange)"></div></div>
<div class="row"><span>文档完整度</span><span>9.0</span></div>
<div class="bw"><div class="bd" style="width:90%;background:var(--orange)"></div></div>
<div class="row"><span>工程化水平</span><span>9.0</span></div>
<div class="bw"><div class="bd" style="width:90%;background:var(--orange)"></div></div>
</div>
</div>
<div class="score" style="border-color:rgba(63,185,80,.35)">
<div class="nm" style="color:var(--green)">OpenClaw</div>
<div class="tot" style="color:var(--green)">7.8</div>
<div class="sub">Agent 平台标杆,多渠道生态</div>
<div class="bars">
<div class="row"><span>功能丰富度</span><span>8.0</span></div>
<div class="bw"><div class="bd" style="width:80%;background:var(--green)"></div></div>
<div class="row"><span>代码质量</span><span>8.0</span></div>
<div class="bw"><div class="bd" style="width:80%;background:var(--green)"></div></div>
<div class="row"><span>架构设计</span><span>8.5</span></div>
<div class="bw"><div class="bd" style="width:85%;background:var(--green)"></div></div>
<div class="row"><span>安全机制</span><span>8.5</span></div>
<div class="bw"><div class="bd" style="width:85%;background:var(--green)"></div></div>
<div class="row"><span>用户体验</span><span>7.5</span></div>
<div class="bw"><div class="bd" style="width:75%;background:var(--green)"></div></div>
<div class="row"><span>生态与社区</span><span>7.0</span></div>
<div class="bw"><div class="bd" style="width:70%;background:var(--green)"></div></div>
<div class="row"><span>文档完整度</span><span>8.0</span></div>
<div class="bw"><div class="bd" style="width:80%;background:var(--green)"></div></div>
<div class="row"><span>工程化水平</span><span>7.5</span></div>
<div class="bw"><div class="bd" style="width:75%;background:var(--green)"></div></div>
</div>
</div>
<div class="score" style="border-color:rgba(188,140,255,.35)">
<div class="nm" style="color:var(--purple)">Aider</div>
<div class="tot" style="color:var(--purple)">7.2</div>
<div class="sub">专注代码编辑,简洁高效</div>
<div class="bars">
<div class="row"><span>功能丰富度</span><span>6.5</span></div>
<div class="bw"><div class="bd" style="width:65%;background:var(--purple)"></div></div>
<div class="row"><span>代码质量</span><span>8.5</span></div>
<div class="bw"><div class="bd" style="width:85%;background:var(--purple)"></div></div>
<div class="row"><span>架构设计</span><span>8.0</span></div>
<div class="bw"><div class="bd" style="width:80%;background:var(--purple)"></div></div>
<div class="row"><span>安全机制</span><span>5.0</span></div>
<div class="bw"><div class="bd" style="width:50%;background:var(--purple)"></div></div>
<div class="row"><span>用户体验</span><span>8.0</span></div>
<div class="bw"><div class="bd" style="width:80%;background:var(--purple)"></div></div>
<div class="row"><span>生态与社区</span><span>7.5</span></div>
<div class="bw"><div class="bd" style="width:75%;background:var(--purple)"></div></div>
<div class="row"><span>文档完整度</span><span>7.5</span></div>
<div class="bw"><div class="bd" style="width:75%;background:var(--purple)"></div></div>
<div class="row"><span>工程化水平</span><span>7.5</span></div>
<div class="bw"><div class="bd" style="width:75%;background:var(--purple)"></div></div>
</div>
</div>
</div>
</div>
</section>
<!-- ════════════════ 差异化优势 ════════════════ -->
<section>
<div class="container">
<h2><span class="em"> </span> v5.1.2 新增的独有优势</h2>
<p class="sec-desc">这三项功能在同类产品中均无直接竞品。</p>
<div class="new-grid">
<div class="new-card" style="border-left:3px solid var(--green)">
<h3> 自动语法检查 — 行业空白</h3>
<p><strong>Claude Code</strong>:不做语法检查,依赖模型自身判断。<br>
<strong>Aider</strong>:不做语法检查。<br>
<strong>Cursor</strong>:有 lint 集成但非自动触发。<br><br>
<strong>小狸</strong>:编辑后 <strong>自动</strong> 触发,11 种语言,AST 级精确错误定位。AI 修改代码后立刻知道对不对,不用等运行时才发现。<strong>这是编程 CLI 赛道的首创。</strong></p>
</div>
<div class="new-card" style="border-left:3px solid var(--blue)">
<h3> Diff 弹窗 — 所见即所得</h3>
<p><strong>Claude Code</strong>:终端内显示 diff 文本。<br>
<strong>Aider</strong>:终端内显示 diff。<br>
<strong>Cursor</strong>:IDE 内置 diff 视图。<br><br>
<strong>小狸</strong>:每次编辑后 <strong>弹出独立窗口</strong>,完整显示修改前后内容 + 行号 + unified diff + 统计。不挤在终端里,一目了然。<code>diff_popup off</code> 一键关闭。</p>
</div>
<div class="new-card" style="border-left:3px solid var(--purple)">
<h3> Web UI — 零门槛</h3>
<p><strong>Claude Code</strong>:纯 CLI,无 UI。<br>
<strong>Aider</strong>:纯 CLI,无 UI。<br>
<strong>OpenClaw</strong>:Canvas 组件,非完整 UI。<br><br>
<strong>小狸</strong>:纯静态 HTML 文件,<strong>双击打开</strong>。暗色主题三栏布局,对话 + 编辑器 + 文件树 + 状态面板。不需要 npm、不需要 Flask、不需要任何后端。</p>
</div>
</div>
</div>
</section>
<!-- ════════════════ SWOT ════════════════ -->
<section>
<div class="container">
<h2><span class="em"> </span> SWOT 分析(更新后)</h2>
<div class="swot">
<div class="sc sc-s">
<h3> Strengths</h3>
<ul>
<li><strong>自动语法检查</strong> — 行业首创,11 语言,编辑后即时反馈</li>
<li><strong>Diff 弹窗</strong> — 独立窗口对比,所见即所得</li>
<li><strong>三模式界面</strong> — CLI + TUI + Web UI,覆盖所有场景</li>
<li><strong>功能密度极高</strong> — 16 插件 142 操作,2 个月一人完成</li>
<li><strong>安全纵深</strong> — AST 沙箱 + 子进程隔离 + 三模式</li>
<li><strong>本地模型原生</strong> — Ollama 开箱即用,完全离线</li>
<li><strong>中文原生 + MIT 开源</strong> — 国内开发者零门槛</li>
</ul>
</div>
<div class="sc sc-w">
<h3> Weaknesses</h3>
<ul>
<li><strong>模型能力差距</strong> — 本地模型 vs Claude/GPT-4 鸿沟仍在</li>
<li><strong>单人维护</strong> — 长期迭代速度和可持续性存疑</li>
<li><strong>社区生态空白</strong> — 无插件市场、无活跃贡献者</li>
<li><strong>架构复杂度</strong> — cli_core.py 对话循环待拆分</li>
<li><strong>Web UI 是静态</strong> — 无后端,不能真正执行操作</li>
</ul>
</div>
<div class="sc sc-o">
<h3> Opportunities</h3>
<ul>
<li><strong>国产替代</strong> — 中文原生 + 本地模型是巨大优势</li>
<li><strong>本地 AI 爆发</strong> — Ollama/llama.cpp 快速发展</li>
<li><strong>语法检查可产品化</strong> — 独立为 CI/CD 工具或 IDE 插件</li>
<li><strong>教育市场</strong> — 免费 + 中文 + 有趣(彩蛋情书)</li>
<li><strong>Web UI 可进化</strong> — 加后端即可变为完整 Web IDE</li>
</ul>
</div>
<div class="sc sc-t">
<h3>⚠️ Threats</h3>
<ul>
<li><strong>大厂碾压</strong> — Claude Code / Cursor 资金和模型优势</li>
<li><strong>开源竞品</strong> — Aider、Cline、Continue.dev 社区更活跃</li>
<li><strong>AI 工具快速迭代</strong> — 功能可能被快速复制</li>
<li><strong>维护可持续性</strong> — 单人项目最大风险</li>
</ul>
</div>
</div>
</div>
</section>
<!-- ════════════════ 结论 ════════════════ -->
<section>
<div class="container">
<h2><span class="em"> </span> 结论</h2>
<div class="verdict">
<h3>能不能抗衡?</h3>
<p><strong>功能维度:能。</strong>16 插件 142 操作 + 自动语法检查 + Diff 弹窗 + 三模式界面,功能广度已经超过 Claude Code 和 Aider,在语法验证维度甚至领先全行业。</p>
<p style="margin-top:14px"><strong>体验维度:接近。</strong>Web UI 补齐了可视化短板,TUI 动画体验独特。但底层模型能力差距决定了对话质量和代码生成精度仍有代差。</p>
<p style="margin-top:14px"><strong>生态维度:差距大。</strong>社区、插件市场、贡献者体系是最大的结构性劣势。功能做得再好,一个人扛不住长期迭代。</p>
<p style="margin-top:18px;color:var(--blue);font-weight:600">一句话:功能够打,体验能看,生态要追。v5.1.2 是一次关键的能力补齐。</p>
</div>
</div>
</section>
<footer>
<p>基于 <a href="https://gitee.com/shuiyu1123/xiaoli-cli">xiaoli-cli v5.1.2</a> 全部源代码逐行审查生成 · 2026-04-27</p>
</footer>
</body>
</html>
+995 -369
View File
File diff suppressed because it is too large. Load diff
+376
View File
@@ -0,0 +1,376 @@
#!/usr/bin/env python3
"""
小狸 Pro-CLI Web UI 后端
WebSocket 服务器 + 静态文件服务
启动:
python webui_server.py [--port 8080] [--host 0.0.0.0]
依赖:
pip install websockets
"""
import asyncio
import json
import os
import sys
import time
import argparse
import mimetypes
from pathlib import Path
from datetime import datetime
from http.server import HTTPServer, SimpleHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
import threading
# 添加项目根目录
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, PROJECT_DIR)
try:
import websockets
WEBSOCKETS_AVAILABLE = True
except ImportError:
WEBSOCKETS_AVAILABLE = False
print("⚠️ websockets 未安装,WebSocket 功能不可用")
print(" 安装: pip install websockets")
# ═══════════════════════════════════════════
# AI 引擎桥接
# ═══════════════════════════════════════════
class AIEngineBridge:
"""桥接到小狸的 AI 引擎和插件系统"""
def __init__(self):
self.cli = None
self._initialized = False
def initialize(self):
"""延迟初始化小狸核心"""
if self._initialized:
return True
try:
from xcli_core import AICLI
self.cli = AICLI()
self._initialized = True
return True
except Exception as e:
print(f"⚠️ 小狸核心初始化失败: {e}")
return False
def get_status(self):
"""获取系统状态"""
if not self.cli:
return {
"engine": "未连接",
"engine_type": "none",
"plugins": 0,
"operations": 0,
"safety_mode": "普通",
"tools": [],
"engines": [],
}
engine_name = getattr(self.cli.current_engine, 'name', '未知') if self.cli.current_engine else '无'
tools = []
if hasattr(self.cli, 'liugin_manager') and self.cli.liugin_manager:
for t in self.cli.liugin_manager.tools:
tools.append({
"name": t.get('name', ''),
"description": t.get('description', ''),
"ops": len(t.get('usage', '').split('\n')) if t.get('usage') else 0,
})
from xcli_core.safety import get_safety
safety = get_safety()
return {
"engine": engine_name,
"engine_type": "local" if engine_name == "ollama" else "cloud",
"plugins": len(tools),
"operations": sum(t.get('ops', 0) for t in tools),
"safety_mode": safety.get_mode_name(),
"tools": tools,
"engines": list(self.cli.engines.keys()) if self.cli.engines else [],
}
def chat(self, message: str, callback=None):
"""发送消息到 AI 并获取回复"""
if not self.cli:
return "❌ 小狸核心未初始化,请检查配置"
try:
# 使用小狸的核心对话处理
import io
import contextlib
# 捕获输出
captured = io.StringIO()
with contextlib.redirect_stdout(captured):
# 构建系统提示词
liugin_prompts = self.cli.get_liugin_usage_prompts()
system_prompt = self.cli._build_system_prompt(liugin_prompts)
# 生成响应
if self.cli.current_engine:
response = self.cli.current_engine.generate_response(
message,
system_prompt=system_prompt
)
else:
response = "❌ 没有可用的 AI 引擎"
output = captured.getvalue()
return response or "(无响应)"
except Exception as e:
return f"❌ 错误: {e}"
def execute_tool(self, tool_name: str, tool_args: str):
"""直接执行工具"""
if not self.cli:
return "❌ 小狸核心未初始化"
try:
result = self.cli._execute_tool_by_name(tool_name, tool_args)
return result or "(无结果)"
except Exception as e:
return f"❌ 工具执行错误: {e}"
def list_tools(self):
"""列出所有工具"""
if not self.cli or not hasattr(self.cli, 'liugin_manager'):
return []
return [
{
"name": t.get('name', ''),
"description": t.get('description', ''),
"keywords": t.get('keywords', []),
}
for t in self.cli.liugin_manager.tools
]
# ═══════════════════════════════════════════
# WebSocket 处理
# ═══════════════════════════════════════════
bridge = AIEngineBridge()
connected_clients = set()
async def ws_handler(websocket):
"""处理 WebSocket 连接"""
connected_clients.add(websocket)
client_addr = websocket.remote_address
print(f"✅ 客户端已连接: {client_addr}")
try:
# 发送初始状态
status = bridge.get_status()
await websocket.send(json.dumps({
"type": "status",
"data": status,
}, ensure_ascii=False))
async for raw_message in websocket:
try:
msg = json.loads(raw_message)
msg_type = msg.get("type", "")
msg_data = msg.get("data", {})
if msg_type == "chat":
# 用户聊天消息
user_msg = msg_data.get("message", "").strip()
if not user_msg:
continue
# 发送"正在思考"状态
await websocket.send(json.dumps({
"type": "thinking",
"data": {"message": user_msg},
}, ensure_ascii=False))
# 在线程中执行 AI 调用(避免阻塞事件循环)
loop = asyncio.get_event_loop()
response = await loop.run_in_executor(
None, bridge.chat, user_msg
)
# 发送回复
await websocket.send(json.dumps({
"type": "reply",
"data": {"message": response},
}, ensure_ascii=False))
elif msg_type == "tool":
# 直接工具调用
tool_name = msg_data.get("name", "")
tool_args = msg_data.get("args", "")
await websocket.send(json.dumps({
"type": "tool_executing",
"data": {"name": tool_name, "args": tool_args},
}, ensure_ascii=False))
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(
None, bridge.execute_tool, tool_name, tool_args
)
await websocket.send(json.dumps({
"type": "tool_result",
"data": {"name": tool_name, "result": result},
}, ensure_ascii=False))
elif msg_type == "status":
# 请求状态更新
status = bridge.get_status()
await websocket.send(json.dumps({
"type": "status",
"data": status,
}, ensure_ascii=False))
elif msg_type == "tools":
# 请求工具列表
tools = bridge.list_tools()
await websocket.send(json.dumps({
"type": "tools",
"data": {"tools": tools},
}, ensure_ascii=False))
elif msg_type == "switch_engine":
# 切换引擎
engine_name = msg_data.get("engine", "")
if bridge.cli and engine_name in bridge.cli.engines:
bridge.cli.switch_engine(engine_name)
await websocket.send(json.dumps({
"type": "engine_switched",
"data": {"engine": engine_name},
}, ensure_ascii=False))
elif msg_type == "switch_safety":
# 切换安全模式
mode = msg_data.get("mode", "")
if bridge.cli:
from xcli_core.safety import get_safety, MODE_NORMAL, MODE_MANUAL, MODE_UNRESTRICTED
safety = get_safety()
if mode == "normal":
safety.set_mode(MODE_NORMAL)
elif mode == "manual":
safety.set_mode(MODE_MANUAL)
elif mode == "unrestricted":
safety.set_mode(MODE_UNRESTRICTED)
await websocket.send(json.dumps({
"type": "safety_switched",
"data": {"mode": safety.get_mode_name()},
}, ensure_ascii=False))
except json.JSONDecodeError:
await websocket.send(json.dumps({
"type": "error",
"data": {"message": "无效的 JSON 格式"},
}, ensure_ascii=False))
except Exception as e:
await websocket.send(json.dumps({
"type": "error",
"data": {"message": str(e)},
}, ensure_ascii=False))
except websockets.exceptions.ConnectionClosed:
pass
finally:
connected_clients.discard(websocket)
print(f"❌ 客户端已断开: {client_addr}")
# ═══════════════════════════════════════════
# 静态文件服务
# ═══════════════════════════════════════════
class WebUIHandler(SimpleHTTPRequestHandler):
"""自定义 HTTP 处理器"""
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=PROJECT_DIR, **kwargs)
def do_GET(self):
# 根路径返回 webui.html
if self.path == '/' or self.path == '/index.html':
self.path = '/webui.html'
return super().do_GET()
def log_message(self, format, *args):
# 静默日志
pass
# ═══════════════════════════════════════════
# 启动
# ═══════════════════════════════════════════
def main():
parser = argparse.ArgumentParser(description="小狸 Pro-CLI Web UI 服务器")
parser.add_argument("--host", default="0.0.0.0", help="绑定地址")
parser.add_argument("--port", type=int, default=8080, help="HTTP 端口")
parser.add_argument("--ws-port", type=int, default=8079, help="WebSocket 端口")
parser.add_argument("--no-ai", action="store_true", help="不初始化 AI 引擎(纯 UI 模式)")
args = parser.parse_args()
print()
print("╔══════════════════════════════════════════╗")
print("║ 小狸 Pro-CLI · Web UI 服务器 ║")
print("╚══════════════════════════════════════════╝")
print()
# 初始化 AI 引擎
if not args.no_ai:
print("⏳ 正在初始化小狸核心...")
if bridge.initialize():
status = bridge.get_status()
print(f"✅ AI 引擎: {status['engine']}")
print(f"✅ 插件: {status['plugins']} 个")
print(f"✅ 安全模式: {status['safety_mode']}")
else:
print("⚠️ AI 引擎初始化失败,将以纯 UI 模式运行")
else:
print("ℹ️ 纯 UI 模式(不连接 AI 引擎)")
print()
print(f"🌐 HTTP: http://localhost:{args.port}")
print(f"🔌 WebSocket: ws://localhost:{args.ws_port}")
print(f"📂 项目目录: {PROJECT_DIR}")
print()
print("按 Ctrl+C 停止服务器")
print()
# 启动 HTTP 服务器(在单独线程中)
http_server = HTTPServer((args.host, args.port), WebUIHandler)
http_thread = threading.Thread(target=http_server.serve_forever, daemon=True)
http_thread.start()
# 启动 WebSocket 服务器
if WEBSOCKETS_AVAILABLE:
async def run_ws():
async with websockets.serve(ws_handler, args.host, args.ws_port):
await asyncio.Future() # 永远运行
try:
asyncio.run(run_ws())
except KeyboardInterrupt:
print("\n🛑 服务器已停止")
else:
print("⚠️ WebSocket 不可用,仅启动 HTTP 服务")
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("\n🛑 服务器已停止")
http_server.shutdown()
if __name__ == "__main__":
main()