Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bd8ca0fba | ||
|
|
e6fddf14f8 | ||
|
|
a00396b4fd | ||
|
|
c2dfa284d1 | ||
|
|
614b081364 | ||
|
|
245413b2cd | ||
|
|
3fa1d56632 | ||
|
|
68d085df28 | ||
|
|
62c761b7d8 | ||
|
|
62aa5ed128 |
No files matched your search
@@ -36,6 +36,14 @@ Thumbs.db
|
||||
# 嵌入式 Python
|
||||
embedded_python/
|
||||
|
||||
# 记忆系统数据(运行时生成)
|
||||
MEMORY.md
|
||||
memory/
|
||||
|
||||
# 生成的图片
|
||||
generated_*.png
|
||||
screenshots/gen_*.png
|
||||
|
||||
# 临时文件
|
||||
*.tmp
|
||||
*.bak
|
||||
@@ -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)
|
||||
|
||||
### 新增功能
|
||||
|
||||
@@ -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`)
|
||||
|
||||
| 操作 | 说明 |
|
||||
|
||||
@@ -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)
|
||||
@@ -1,7 +1,7 @@
|
||||
**产品标识**
|
||||
- **产品名称**:小狸智能体 增强-终端(Xiaoli Pro-CLI)
|
||||
- **内部版本号**:20260426
|
||||
- **发布版本**:发行版5.1.1
|
||||
- **内部版本号**:20260428
|
||||
- **发布版本**:发行版5.1.3
|
||||
|
||||
**技术规格**
|
||||
- **主要语言**:简体中文(UTF-8)
|
||||
|
||||
+1161
File diff suppressed because it is too large.
Load diff
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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}"
|
||||
@@ -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"""
|
||||
@@ -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 功能测试"""
|
||||
|
||||
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
+376
@@ -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()
|
||||
Reference in new issue
Block a user