feat: 增强编码能力 - 新增代码编辑器、代码搜索、Git集成

- 新增 plugins/code_editor.py: 精准代码编辑(搜索替换、批量编辑、diff对比)
- 新增 plugins/code_search.py: 代码搜索与理解(跨文件搜索、符号提取、依赖分析)
- 新增 plugins/git_tools.py: Git 版本控制(status/diff/log/commit/branch)
- 新增 core/ 模块: 配置管理、引擎管理、对话处理、显示输出、代码沙箱
- 修复 requirements.txt: 使用正确的 pip 包名(Pillow、opencv-python 等)
- 更新 unified_tool_manager.py: 兼容 Plugin 和 Liugin 两种类名
- 更新系统提示词: 增强编程工作流指导

本次更新由 mimo 自动化执行,与开发者无关。
This commit is contained in:
Xiaoli Dev committed 2026-04-25 10:55:27 +08:00
commit e22652fbbb
53 files changed
+20067

No files matched your search

+74
View File
@@ -0,0 +1,74 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual Environment
venv/
ENV/
env/
.venv/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# Logs
logs/
*.log
xiaoli_cli.log
# Temporary files
nul
temp/
tmp/
*.tmp
# OS
.DS_Store
Thumbs.db
desktop.ini
# Chat history
chat_history/
# Config with secrets
config.json
# Executables
*.exe
*.dll
*.so
*.appx
# Development files
love.txt
bug.txt
# Screenshots
clawli_files/
# 大型第三方插件 (NapCat QQ / GLaDOS TTS)
plugins/napcat/
plugins/glados_tts/
plugins/glados_tts_plugin.py
+80
View File
@@ -0,0 +1,80 @@
# 小狸 Pro-CLI 使用指南
## 基本操作
### 启动和退出
- 启动程序: 运行 `python ai_cli.py`
- 退出程序: 输入 `/quit`
### 帮助命令
- `/help` - 显示帮助信息
## AI引擎管理
### 切换AI引擎
- `/model spark` - 切换到讯飞星火AI引擎
## 聊天记录管理
### 保存聊天记录
- `/chat save <名称>` - 将当前对话保存为指定名称的聊天记录
### 查看聊天记录
- `/chat list` - 显示所有已保存的聊天记录
### 加载聊天记录
- `/chat open <名称>` - 加载指定名称的聊天记录并继续对话
## 插件工具
AI可以自动调用以下工具来帮助您完成各种任务:
### 文件管理工具␍
- 功能:读取、写入、追加、删除、复制、移动文件和目录操作␍
- 用法示例:␍
- `file_manager list` - 列出当前目录内容␍
- `file_manager read test.txt` - 读取文件内容␍
- `file_manager write test.txt 内容` - 写入文件␍
- `file_manager append test.txt 内容` - 追加内容到文件␍
- `file_manager delete test.txt` - 删除文件␍
- `file_manager create test.txt` - 创建空文件␍
- `file_manager mkdir new_dir` - 创建目录␍
- `file_manager copy source.txt dest.txt` - 复制文件␍
- `file_manager move source.txt dest.txt` - 移动文件␍
- `file_manager info test.txt` - 查看文件信息␍
- `file_manager tree` - 显示目录树结构␍
␍
### 音频播放工具␍
- 功能:播放音频文件␍
- 用法示例:␍
- `audio_player play` - 播放Ciallo~(∠・ω- )⌒☆-.mp3音频文件␍
- `/ciallo` - 播放音频文件␍
␍
## 基本操作命令␍
␍
### 系统信息命令␍
- `/about` - 显示程序信息和版本详情␍
␍
## 使用技巧␍
␍
1. **自然语言交互**:直接用自然语言与AI对话,AI会理解您的意图并自动调用相应工具。␍
␍
2. **工具调用**:当AI需要执行特定任务时,会自动调用相应的插件工具,并在屏幕上显示执行结果。␍
␍
3. **聊天记录管理**:可以随时保存当前对话,在需要时加载之前的对话继续交流。␍
␍
4. **路径操作**:␍
- 支持用户主目录下的绝对路径操作␍
- 支持项目目录和当前目录下的相对路径操作␍
- 文件大小限制为50MB␍
␍
5. **等待动画**:AI思考时会显示随机的温馨句子,每5秒更换一次。␍
␍
6. **插件命令**:可以使用 `/命令名` 直接调用插件功能,如 `/file`、`/read`、`/write`、`/ciallo` 等。␍
␍
## 注意事项␍
␍
- 请勿尝试访问系统关键目录(如Windows系统目录、/etc等)␍
- 文件操作大小限制为50MB␍
- 音频播放需要系统支持相应的音频格式␍
- 可以随时使用 `/help` 命令查看帮助信息
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 shuiyu1123
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+36
View File
@@ -0,0 +1,36 @@
# xiaoli cli
#### Description
本仓库用于提交xiaoli系列源代码,本仓库源于github 2024年九月项目,因账户2fa码丢失,现已迁移至gitee
#### Software Architecture
Software architecture description
#### Installation
1. xxxx
2. xxxx
3. xxxx
#### Instructions
1. xxxx
2. xxxx
3. xxxx
#### Contribution
1. Fork the repository
2. Create Feat_xxx branch
3. Commit your code
4. Create Pull Request
#### Gitee Feature
1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md
2. Gitee blog [blog.gitee.com](https://blog.gitee.com)
3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore)
4. The most valuable open source project [GVP](https://gitee.com/gvp)
5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help)
6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
+19
View File
@@ -0,0 +1,19 @@
# xiaoli cli
#请前往下载压缩包获取开源代码
#### 介绍
本仓库用于提交xiaoli系列源代码,本仓库源于github 2024年九月项目,因账户2fa码丢失,现已迁移至gitee
#### 软件架构
软件架构说明
#### 安装教程
1. 安装至少python 3.11以上版本
1. 或点击发行版内启动器快速安装
#### 使用说明
1. 直接点击启动器启动
+47
View File
@@ -0,0 +1,47 @@
**产品标识**
- **产品名称**:小狸智能体 增强-终端(Xiaoli Pro-CLI)
- **内部版本号**:20260212
- **发布版本**:发行版3.5
**技术规格**
- **主要语言**:简体中文(UTF-8)
- **开发语言**:Python 3.12.5
- **开发文档**:6
- **兼容性**:Windows 10 x64、x86-64及以上版本操作系统
**产品概述**
小狸智能体 增强-终端(Xiaoli Pro-CLI)是一款基于人工智能的命令行交互工具,通过模块化插件架构实现系统操作与任务流程的自动化执行。
**使用条款与免责声明**
1. **插件开发要求**
开发自定义插件需具备专业的Python开发能力。缺乏必要的技术背景可能导致插件运行异常、系统稳定性问题或安全风险。
2. **技术支持渠道**
开发者如需插件技术支持(包括但不限于接口扩展、功能调试、开发指导等),请加入QQ技术交流群:**1043779619**,开发团队将在群内提供技术支持与服务。
3. **测试版本声明**
- 本软件目前处于测试阶段,可能存在未知缺陷。对于因软件漏洞导致的系统故障、数据丢失或任何间接损失,开发团队不承担任何法律责任。
- 即软件进入正式版本,因用户操作不当(如指令AI删除系统关键目录、修改核心配置文件等)造成的设备故障或数据损坏,责任由操作者自行承担。
4. **第三方插件责任豁免**
开发团队仅对官方发布的插件提供技术支持和质量保证。任何由第三方插件引发的系统兼容性问题、安全漏洞、功能异常或数据损坏,均不属于官方责任范围,使用者应直接联系相应插件的开发者寻求解决方案。
5. **知识产权保护与使用限制**
本软件及其全部组成要素(包括但不限于源代码、算法架构、技术文档)均受知识产权法保护。**严禁任何形式的逆向工程行为**,包括但不限于反编译、反汇编、反向分析或任何试图获取软件源代码、破解技术实现的手段。
**我们严正声明**:所有必要的开发接口和技术规范均已完整收录于四个版本的官方开发文档中。任何通过逆向工程试图探索的功能实现,均可通过官方文档获得标准、安全的技术方案。
任何违反本条款的行为均构成严重侵权,我们将采取包括法律诉讼在内的一切必要手段维护自身权益,并追究侵权者的全部法律责任。
**特别鸣谢:**
**所有开源贡献者,插件开发者,技术文档撰写者,早期测试用户**
- **Python** - 项目核心开发语言,提供了简洁优雅的编程体验
- **Microsoft** - Windows操作系统及开发工具链的稳定支持
- **Visual Studio Code** - 主力开发环境,卓越的代码编辑体验
- **阿里巴巴** - iFlow CLI等开发工具的技术支持
- **DeepSeek** - 优秀的AI模型能力,为项目提供智能支持
**最后,感谢所有为技术进步付出努力的人,包括正在阅读这份文档的您。**
+3844
View File
File diff suppressed because it is too large. Load diff
+210
View File
@@ -0,0 +1,210 @@
import os
import json
import requests
from colorama import Fore, Style
from openai import OpenAI
# 获取项目根目录
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(current_dir)
class Glm_http_engineAI:
"""基于HTTP协议的智谱AI GLM引擎"""
requires_api_key = True # GLM HTTP引擎需要API密钥
def get_help_info(self):
"""获取引擎帮助信息"""
help_text = """
GLM HTTP AI引擎插件帮助信息
==========================
引擎名称: glm_http
描述: 基于HTTP协议的智谱AI GLM(GLM-4.5-Flash)引擎,支持智谱AI服务
可用命令:
无特定命令,直接与AI对话即可
使用说明:
1. 此引擎使用HTTP协议调用智谱AI API
2. 使用/engine.switch glm_http切换到此引擎
3. 直接输入自然语言与AI对话
4. 支持普通对话、流式对话、JSON模式等多种调用方式
"""
return help_text
def __init__(self):
self.name = "glm_http"
self.cli = None
# 直接从 config.json 读取配置
config_file = os.path.join(project_root, "config.json")
try:
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
engine_config = config.get("api", {}).get("engines", {}).get("GLM_http", {})
self.api_key = engine_config.get("api_key", "")
self.base_url = engine_config.get("base_url", "")
self.model_id = engine_config.get("model", "")
except Exception as e:
# 如果读取失败,使用空值
self.api_key = ""
self.base_url = ""
self.model_id = ""
# 初始化对话历史
self.conversation_history = []
# 共享对话历史引用(由CLI设置)
self.shared_conversation_history = None
# 设置最大历史记录数
try:
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
self.max_history = config.get("system", {}).get("max_history", 10)
except:
self.max_history = 10
# 初始化 OpenAI 客户端(配置从 config.json 动态读取)
try:
self.client = OpenAI(
api_key=self.api_key,
base_url=self.base_url if self.base_url else None,
timeout=None
)
except Exception as e:
print(f"初始化 OpenAI 客户端失败: {e}")
self.client = None
def generate_response(self, user_input, tool_results=None, system_prompt=None):
"""生成AI響應 - 使用智谱AI HTTP API"""
try:
# 如果有工具結果,將其作為上下文返回給AI處理
if tool_results:
tool_result_text = f"工具執行結果: {tool_results.get('result', '無結果')}"
# 構建包含歷史記錄的消息列表
messages = self._build_messages_with_history(system_prompt, tool_result_text)
# 調用API
response = self._call_api(messages)
if response:
return response
else:
return "智谱AI HTTP API調用失敗"
# 構建包含歷史記錄的消息列表
messages = self._build_messages_with_history(system_prompt, user_input)
# 調用API
response = self._call_api(messages)
if response:
return response
else:
return "智谱AI HTTP API調用失敗"
except Exception as e:
# 如果API調用失敗,返回錯誤信息
return f"智谱AI HTTP API調用失敗: {str(e)}"
def _call_api(self, messages, use_stream=False, extra_body=None):
"""調用API的核心方法"""
if extra_body is None:
extra_body = {}
try:
response = self.client.chat.completions.create(
model=self.model_id,
messages=messages,
stream=use_stream,
temperature=0.7,
max_tokens=4096,
extra_headers={"lora_id": "0"}, # 調用微調大模型時,對應替換為模型服務卡片上的resourceId
stream_options={"include_usage": True},
extra_body=extra_body
)
if use_stream:
# 處理流式響應
full_response = ""
for chunk in response:
if hasattr(chunk.choices[0].delta, 'content') and chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
full_response += content
return full_response
else:
# 處理非流式響應
return response.choices[0].message.content
except Exception as e:
print(f"智谱AI HTTP API請求出錯: {e}")
return None
def _build_messages_with_history(self, system_prompt, user_input):
"""構建包含歷史記錄的消息列表"""
messages = []
# 添加系統提示詞
if system_prompt:
messages.append({
"role": "system",
"content": system_prompt
})
# 添加歷史對話記錄(使用共享歷史)
if self.shared_conversation_history is not None:
# 確保歷史記錄格式正確
for msg in self.shared_conversation_history[-self.max_history:]:
if isinstance(msg, dict) and "role" in msg and "content" in msg:
messages.append(msg)
else:
# 如果格式不正確,跳過該條記錄
continue
# 添加當前用戶輸入
messages.append({
"role": "user",
"content": user_input
})
return messages
def _call_api_with_history(self, content, system_prompt=None):
"""使用對話歷史調用API"""
# 構建消息列表
messages = []
# 添加系統提示詞(如果提供)
if system_prompt:
messages.append({
"role": "system",
"content": system_prompt
})
# 添加歷史對話記錄(使用共享歷史)
if self.shared_conversation_history is not None:
# 確保歷史記錄格式正確
for msg in self.shared_conversation_history[-self.max_history:]:
if isinstance(msg, dict) and "role" in msg and "content" in msg:
messages.append(msg)
else:
# 如果格式不正確,跳過該條記錄
continue
# 添加當前內容
messages.append({
"role": "tool",
"content": content
})
# 調用API
response = self._call_api(messages)
if response:
return response
else:
return "智谱AI HTTP API調用失敗"
def handle_command(self, command):
"""處理引擎特定命令"""
# GLM HTTP引擎目前不支持特定命令
print(f"{Fore.RED}GLM HTTP引擎不支持此命令{Style.RESET_ALL}")
return False
+266
View File
@@ -0,0 +1,266 @@
"""
小狸 Pro-CLI AI 引擎开发 SDK
提供快速开发 AI 引擎的工具包
"""
import os
import sys
import json
import requests
from colorama import Fore, Style
from openai import OpenAI
import logging
# 设置日志
logger = logging.getLogger(__name__)
# 添加项目根目录到sys.path
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(current_dir)
sys.path.insert(0, project_root)
class _AIEngineSDK:
"""AI 引擎 SDK 内部实现"""
def __init__(self):
self.name = ""
self.api_key = ""
self.base_url = ""
self.model = ""
self.is_online = True
self.max_history = 10
self.conversation_history = []
self.shared_conversation_history = None
self.cli = None
# 深度思考引擎配置
self.is_thinking_engine = False # 是否是深度思考引擎
self.thinking_start_marker = "" # 思考开始标记
self.thinking_end_marker = "" # 思考结束标记
self.show_thinking = True # 是否显示思考过程
# 配置文件路径
self.config_file = os.path.join(project_root, "config.json")
def _load_config(self):
"""从配置文件加载配置"""
try:
with open(self.config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
engine_config = config.get("api", {}).get("engines", {}).get(self.name, {})
if not self.api_key:
self.api_key = engine_config.get("api_key", "")
if not self.base_url:
self.base_url = engine_config.get("base_url", "")
if not self.model:
self.model = engine_config.get("model", "")
self.max_history = config.get("system", {}).get("max_history", 10)
except Exception as e:
logger.error(f"加载配置失败: {e}")
self.max_history = 10
def _call_openai_api(self, messages):
"""调用 OpenAI 格式的 API"""
try:
self._load_config()
# 构建请求体
body = {
"model": self.model,
"messages": messages,
"stream": False
}
# 构建请求头
headers = {
'Content-Type': 'application/json'
}
if self.api_key:
headers['Authorization'] = f'Bearer {self.api_key}'
# 确定 API 地址
api_url = self.base_url
if not api_url.endswith("/chat/completions"):
api_url = api_url.rstrip('/') + "/chat/completions"
# 发送请求
response = requests.post(
url=api_url,
json=body,
headers=headers,
timeout=None
)
# 处理响应
if response.status_code == 200:
response_data = response.json()
if "choices" in response_data and len(response_data["choices"]) > 0:
ai_response = response_data["choices"][0]["message"]["content"]
# 如果是深度思考引擎,处理思考内容
if self.is_thinking_engine:
ai_response = self._process_thinking_content(ai_response)
return ai_response
else:
return f"AI 返回格式错误: {response_data}"
else:
return f"AI 调用失败: {response.status_code} - {response.text}"
except Exception as e:
return f"AI 调用失败: {str(e)}"
def _build_messages(self, system_prompt, user_input):
"""构建消息列表"""
messages = []
# 添加系统提示词
if system_prompt:
messages.append({
"role": "system",
"content": system_prompt
})
# 添加历史对话记录
if self.shared_conversation_history is not None:
for msg in self.shared_conversation_history[-self.max_history:]:
if isinstance(msg, dict) and "role" in msg and "content" in msg:
messages.append(msg)
# 添加当前用户输入
messages.append({
"role": "user",
"content": user_input
})
return messages
def generate_response(self, user_input, tool_results=None, system_prompt=None):
"""生成 AI 响应"""
try:
# 如果有工具结果,将其作为上下文返回给AI处理
if tool_results:
tool_result_text = f"工具执行结果: {tool_results.get('result', '无结果')}"
messages = self._build_messages(system_prompt, tool_result_text)
else:
messages = self._build_messages(system_prompt, user_input)
# 调用 API
return self._call_openai_api(messages)
except Exception as e:
return f"AI 调用失败: {str(e)}"
def _process_thinking_content(self, response):
"""处理思考内容,将思考部分以灰色文本显示,只返回回复部分"""
import re
if not self.thinking_start_marker or not self.thinking_end_marker:
# 如果没有设置标记,直接返回
return response
# 使用正则表达式查找思考内容
pattern = f"{re.escape(self.thinking_start_marker)}(.*?){re.escape(self.thinking_end_marker)}"
matches = re.findall(pattern, response, re.DOTALL)
if matches and self.show_thinking:
for match in matches:
# 用灰色文本显示思考内容
thinking_text = f"{Fore.LIGHTBLACK_EX}[思考: {match}]{Style.RESET_ALL}"
# 替换思考标记之间的内容,保留思考内容但以灰色显示
response = re.sub(pattern, thinking_text, response, 1)
elif matches and not self.show_thinking:
# 如果不显示思考内容,直接删除
for match in matches:
response = re.sub(pattern, "", response, 1)
return response
# 全局 SDK 实例
_sdk_instance = _AIEngineSDK()
def engine_Online(mode):
"""
设置引擎模式
Args:
mode: "on" - 云模式(在线 API)
"off" - 本地模式(如 Ollama)
"""
_sdk_instance.is_online = (mode.lower() == "on")
def ai_name(name):
"""设置引擎名称"""
_sdk_instance.name = name
def ai_api_url(url):
"""设置 API 地址"""
_sdk_instance.base_url = url
def ai_api_key(key):
"""设置 API 密钥"""
_sdk_instance.api_key = key
def ai_model_id(model):
"""设置模型名称"""
_sdk_instance.model = model
def ai_thinking_engine(enable=True, start_marker="<thinking>", end_marker="</thinking>", show_thinking=True):
"""
设置深度思考引擎配置
Args:
enable: 是否启用深度思考引擎模式
start_marker: 思考开始标记,默认为 "<thinking>"
end_marker: 思考结束标记,默认为 "</thinking>"
show_thinking: 是否显示思考过程,默认为 True
"""
_sdk_instance.is_thinking_engine = enable
_sdk_instance.thinking_start_marker = start_marker
_sdk_instance.thinking_end_marker = end_marker
_sdk_instance.show_thinking = show_thinking
def set_cli(cli):
"""设置 CLI 引用"""
_sdk_instance.cli = cli
def set_shared_history(history):
"""设置共享对话历史"""
_sdk_instance.shared_conversation_history = history
def generate_response(user_input, tool_results=None, system_prompt=None):
"""生成 AI 响应"""
return _sdk_instance.generate_response(user_input, tool_results, system_prompt)
def get_sdk_instance():
"""获取 SDK 实例(用于高级用法)"""
return _sdk_instance
# 导出
__all__ = [
'engine_Online',
'ai_name',
'ai_api_url',
'ai_api_key',
'ai_model_id',
'ai_thinking_engine',
'set_cli',
'set_shared_history',
'generate_response',
'get_sdk_instance'
]
+405
View File
@@ -0,0 +1,405 @@
import os
import sys
import json
import requests
import threading
import time
from colorama import Fore, Style
import logging
# 设置日志
logger = logging.getLogger(__name__)
# 添加项目根目录到sys.path,以便正确导入
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(current_dir) # 获取项目根目录
sys.path.insert(0, project_root)
class IflowAPIAI:
"""基于Qwen3-Coder-Plus模型的AI引擎"""
def get_help_info(self):
"""获取引擎帮助信息"""
help_text = """
iflow API AI引擎插件帮助信息
==========================
引擎名称: iflow-api
描述: 基于Qwen3-Coder-Plus模型的AI引擎,支持复杂推理和代码生成
可用命令:
无特定命令,直接与AI对话即可
使用说明:
1. 此引擎使用Qwen3-Coder-Plus云API
2. 使用/engine.switch iflow-api切换到此引擎
3. 直接输入自然语言与AI对话
4. 特别适合处理代码生成、技术问题等复杂任务
"""
return help_text
def __init__(self):
self.name = "iflow-api"
self.cli = None # 引用CLI实例以访问插件
# 直接从 config.json 读取配置(一次性读取)
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
config_file = os.path.join(project_root, "config.json")
# 默认值
self.api_key = ""
self.url = ""
self.model = ""
self.max_history = 10
try:
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
engine_config = config.get("api", {}).get("engines", {}).get("iflow_api", {})
self.api_key = engine_config.get("api_key", "")
self.url = engine_config.get("base_url", "")
self.model = engine_config.get("model", "")
self.max_history = config.get("system", {}).get("max_history", 10)
except Exception as e:
# 如果读取失败,使用默认值
logger.warning(f"警告: 读取配置文件失败: {e}")
print(f"{Fore.YELLOW}警告: 读取配置文件失败: {e}{Style.RESET_ALL}")
# 验证API密钥
if not self.api_key:
logger.warning("警告: iflow-api引擎的API密钥未设置")
print(f"{Fore.YELLOW}警告: iflow-api引擎的API密钥未设置{Style.RESET_ALL}")
self.headers = {
'Authorization': f'Bearer {self.api_key}',
'Content-Type': 'application/json'
}
# 初始化对话历史
self.conversation_history = []
# 设置思考标记
self.thinking_start_marker = ""
self.thinking_end_marker = ""
# 共享对话历史引用(由CLI设置)
self.shared_conversation_history = None
def generate_response(self, user_input, tool_results=None, system_prompt=None):
"""生成AI响应 - 使用 qwen3-coder-plus"""
try:
# 如果有工具结果,将其作为上下文返回给AI处理
if tool_results:
tool_result_text = f"工具执行结果: {tool_results.get('result', '无结果')}"
# 构建消息列表
messages = self._build_messages_with_history(system_prompt, tool_result_text)
# 构建请求体
body = {
"model": self.model or "qwen3-coder-plus",
"messages": messages,
"stream": False # 使用非流式响应以便于处理
}
# 发送请求到qwen3-coder-plus API
response = requests.post(url=self.url, json=body, headers=self.headers, timeout=None)
if response.status_code == 200:
response_data = response.json()
# 检查是否是API错误响应(例如认证失败)
if isinstance(response_data, dict) and "status" in response_data:
# 检查是否是认证错误
if response_data.get("status") in ["434", "401", "403"]:
error_msg = response_data.get("msg", "API认证失败")
return f"qwen3-coder-plus API错误: {error_msg}"
elif "body" in response_data and response_data["body"] is None:
error_msg = response_data.get("msg", "API请求失败")
return f"qwen3-coder-plus API错误: {error_msg}"
if "choices" in response_data and len(response_data["choices"]) > 0:
# 改进错误处理以适应不同的API响应格式
choice = response_data["choices"][0]
if "message" in choice and "content" in choice["message"]:
ai_response = choice["message"]["content"]
# 处理思考标记
processed_response = self._process_thinking_content(ai_response)
# 返回处理后的AI回复
return processed_response
else:
# 尝试其他可能的字段名
if "text" in choice:
return choice["text"]
elif "delta" in choice and "content" in choice["delta"]:
return choice["delta"]["content"]
else:
return f" qwen3-coder-plus返回格式错误: 缺少预期的响应字段 - {response_data}"
else:
return f" qwen3-coder-plus返回格式错误: {response_data}"
else:
return f" qwen3-coder-plus调用失败: {response.status_code} - {response.text}"
# 构建消息列表
messages = self._build_messages_with_history(system_prompt, user_input)
# 构建请求体
body = {
"model": self.model or "qwen3-coder-plus",
"messages": messages,
"stream": False # 使用非流式响应以便于处理
}
# 发送请求到qwen3-coder-plus
response = requests.post(url=self.url, json=body, headers=self.headers, timeout=None)
if response.status_code == 200:
response_data = response.json()
# 检查是否是API错误响应(例如认证失败)
if isinstance(response_data, dict) and "status" in response_data:
# 检查是否是认证错误
if response_data.get("status") in ["434", "401", "403"]:
error_msg = response_data.get("msg", "API认证失败")
return f"qwen3-coder-plus API错误: {error_msg}"
elif "body" in response_data and response_data["body"] is None:
error_msg = response_data.get("msg", "API请求失败")
return f"qwen3-coder-plus API错误: {error_msg}"
if "choices" in response_data and len(response_data["choices"]) > 0:
# 改进错误处理以适应不同的API响应格式
choice = response_data["choices"][0]
if "message" in choice and "content" in choice["message"]:
ai_response = choice["message"]["content"]
# 处理思考标记
processed_response = self._process_thinking_content(ai_response)
# 返回处理后的AI回复
return processed_response
else:
# 尝试其他可能的字段名
if "text" in choice:
return choice["text"]
elif "delta" in choice and "content" in choice["delta"]:
return choice["delta"]["content"]
else:
return f"qwen3-coder-plus返回格式错误: 缺少预期的响应字段 - {response_data}"
else:
return f"qwen3-coder-plus返回格式错误: {response_data}"
else:
return f"qwen3-coder-plus调用失败: {response.status_code} - {response.text}"
except Exception as e:
# 如果API调用失败,返回错误信息
return f"qwen3-coder-plus调用失败: {str(e)}"
def _build_messages_with_history(self, system_prompt, user_input):
"""构建包含历史记录的消息列表"""
messages = []
# 添加系统提示词
if system_prompt:
messages.append({
"role": "system",
"content": system_prompt
})
# 添加历史对话记录(使用共享历史)
if self.shared_conversation_history is not None:
# 确保历史记录格式正确
for msg in self.shared_conversation_history[-self.max_history:]:
if isinstance(msg, dict) and "role" in msg and "content" in msg:
messages.append(msg)
else:
# 如果格式不正确,跳过该条记录
continue
# 添加当前用户输入
messages.append({
"role": "user",
"content": user_input
})
return messages
def _call_api_with_history(self, content, system_prompt=None):
"""使用对话历史调用API"""
# 构建消息列表
messages = []
# 添加系统提示词(如果提供)
if system_prompt:
messages.append({
"role": "system",
"content": system_prompt
})
# 添加历史对话记录(使用共享历史)
if self.shared_conversation_history is not None:
# 确保历史记录格式正确
for msg in self.shared_conversation_history[-self.max_history:]:
if isinstance(msg, dict) and "role" in msg and "content" in msg:
messages.append(msg)
else:
# 如果格式不正确,跳过该条记录
continue
# 添加当前内容
messages.append({
"role": "tool",
"content": content
})
# 构建请求体
body = {
"model": self.model or "qwen3-coder-plus",
"messages": messages,
"stream": False # 使用非流式响应以便于处理
}
# 发送请求到qwen3-coder-plus
response = requests.post(url=self.url, json=body, headers=self.headers, timeout=None)
if response.status_code == 200:
response_data = response.json()
# 检查是否是API错误响应(例如认证失败)
if isinstance(response_data, dict) and "status" in response_data:
# 检查是否是认证错误
if response_data.get("status") in ["434", "401", "403"]:
error_msg = response_data.get("msg", "API认证失败")
return f"qwen3-coder-plus API错误: {error_msg}"
elif "body" in response_data and response_data["body"] is None:
error_msg = response_data.get("msg", "API请求失败")
return f"qwen3-coder-plus API错误: {error_msg}"
if "choices" in response_data and len(response_data["choices"]) > 0:
# 改进错误处理以适应不同的API响应格式
choice = response_data["choices"][0]
if "message" in choice and "content" in choice["message"]:
ai_response = choice["message"]["content"]
# 处理思考标记
processed_response = self._process_thinking_content(ai_response)
return processed_response
else:
# 尝试其他可能的字段名
if "text" in choice:
return choice["text"]
elif "delta" in choice and "content" in choice["delta"]:
return choice["delta"]["content"]
else:
return f"qwen3-coder-plus返回格式错误: 缺少预期的响应字段 - {response_data}"
else:
return f"qwen3-coder-plus返回格式错误: {response_data}"
else:
return f"qwen3-coder-plus调用失败: {response.status_code} - {response.text}"
def handle_command(self, command):
"""处理引擎特定命令"""
# qwen3-coder-plus引擎目前不支持特定命令
print(f"{Fore.RED}qwen3-coder-plus引擎不支持此命令{Style.RESET_ALL}")
return False
def _process_thinking_content(self, response):
"""处理思考内容,将思考部分以灰色文本显示,只返回回复部分"""
from colorama import Fore, Style
import re
# 这里可以设置思考标记,由用户自己定义
thinking_start_marker = ""
thinking_end_marker = ""
if self.thinking_start_marker and self.thinking_end_marker:
# 使用正则表达式查找思考内容
pattern = f"{re.escape(self.thinking_start_marker)}(.*?){re.escape(self.thinking_end_marker)}"
matches = re.findall(pattern, response, re.DOTALL)
if matches:
for match in matches:
# 用灰色文本显示思考内容
thinking_text = f"{Fore.LIGHTBLACK_EX}[思考: {match}]{Style.RESET_ALL}"
# 替换思考标记之间的内容,保留思考内容但以灰色显示
response = re.sub(pattern, thinking_text, response, 1)
return response
+205
View File
@@ -0,0 +1,205 @@
import os
import json
import requests
from colorama import Fore, Style
from openai import OpenAI
# 获取项目根目录
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(current_dir)
class Maas_http_engineAI:
"""基于HTTP协议的MaaS(Model as a Service)AI引擎"""
requires_api_key = True # MaaS HTTP引擎需要API密钥
def get_help_info(self):
"""获取引擎帮助信息"""
help_text = """
MaaS HTTP AI引擎插件帮助信息
==========================
引擎名称: maas_http
描述: 基于HTTP协议的MaaS(Model as a Service)AI引擎,支持多种大模型推理服务
可用命令:
无特定命令,直接与AI对话即可
使用说明:
1. 此引擎使用HTTP协议调用MaaS API
2. 使用/engine.switch maas_http切换到此引擎
3. 直接输入自然语言与AI对话
4. 支持普通对话、流式对话、JSON模式等多种调用方式
"""
return help_text
def __init__(self):
self.name = "maas_http"
self.cli = None # 引用CLI实例以訪問插件
# 直接从 config.json 读取配置
config_file = os.path.join(project_root, "config.json")
try:
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
engine_config = config.get("api", {}).get("engines", {}).get("maas_http", {})
self.api_key = engine_config.get("api_key", "")
self.base_url = engine_config.get("base_url", "")
self.model_id = engine_config.get("model", "")
except Exception as e:
# 如果读取失败,使用空值
self.api_key = ""
self.base_url = ""
self.model_id = ""
# 初始化客户端
self.client = OpenAI(
api_key=self.api_key,
base_url=self.base_url
)
# 初始化对话历史
self.conversation_history = []
# 设置最大历史记录数
try:
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
self.max_history = config.get("system", {}).get("max_history", 10)
except:
self.max_history = 10
# 共享對話歷史引用(由CLI設置)
self.shared_conversation_history = None
def generate_response(self, user_input, tool_results=None, system_prompt=None):
"""生成AI響應 - 使用MaaS HTTP API"""
try:
# 如果有工具結果,將其作為上下文返回給AI處理
if tool_results:
tool_result_text = f"工具執行結果: {tool_results.get('result', '無結果')}"
# 構建包含歷史記錄的消息列表
messages = self._build_messages_with_history(system_prompt, tool_result_text)
# 調用API
response = self._call_api(messages)
if response:
return response
else:
return "MaaS HTTP API調用失敗"
# 構建包含歷史記錄的消息列表
messages = self._build_messages_with_history(system_prompt, user_input)
# 調用API
response = self._call_api(messages)
if response:
return response
else:
return "MaaS HTTP API調用失敗"
except Exception as e:
# 如果API調用失敗,返回錯誤信息
return f"MaaS HTTP API調用失敗: {str(e)}"
def _call_api(self, messages, use_stream=False, extra_body=None):
"""調用API的核心方法"""
if extra_body is None:
extra_body = {}
try:
response = self.client.chat.completions.create(
model=self.model_id or self.default_model,
messages=messages,
stream=use_stream,
temperature=0.7,
max_tokens=4096,
extra_headers={"lora_id": "0"},
stream_options={"include_usage": True},
extra_body=extra_body
)
if use_stream:
# 處理流式響應
full_response = ""
for chunk in response:
if hasattr(chunk.choices[0].delta, 'content') and chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
full_response += content
return full_response
else:
# 處理非流式響應
return response.choices[0].message.content
except Exception as e:
print(f"MaaS HTTP API請求出錯: {e}")
return None
def _build_messages_with_history(self, system_prompt, user_input):
"""構建包含歷史記錄的消息列表"""
messages = []
# 添加系統提示詞
if system_prompt:
messages.append({
"role": "system",
"content": system_prompt
})
# 添加歷史對話記錄(使用共享歷史)
if self.shared_conversation_history is not None:
# 確保歷史記錄格式正確
for msg in self.shared_conversation_history[-self.max_history:]:
if isinstance(msg, dict) and "role" in msg and "content" in msg:
messages.append(msg)
else:
# 如果格式不正確,跳過該條記錄
continue
# 添加當前用戶輸入
messages.append({
"role": "user",
"content": user_input
})
return messages
def _call_api_with_history(self, content, system_prompt=None):
"""使用對話歷史調用API"""
# 構建消息列表
messages = []
# 添加系統提示詞(如果提供)
if system_prompt:
messages.append({
"role": "system",
"content": system_prompt
})
# 添加歷史對話記錄(使用共享歷史)
if self.shared_conversation_history is not None:
# 確保歷史記錄格式正確
for msg in self.shared_conversation_history[-self.max_history:]:
if isinstance(msg, dict) and "role" in msg and "content" in msg:
messages.append(msg)
else:
# 如果格式不正確,跳過該條記錄
continue
# 添加當前內容
messages.append({
"role": "tool",
"content": content
})
# 調用API
response = self._call_api(messages)
if response:
return response
else:
return "MaaS HTTP API調用失敗"
def handle_command(self, command):
"""處理引擎特定命令"""
# MaaS HTTP引擎目前不支持特定命令
print(f"{Fore.RED}MaaS HTTP引擎不支持此命令{Style.RESET_ALL}")
return False
+355
View File
@@ -0,0 +1,355 @@
import os
import sys
import json
import requests
from colorama import Fore, Style
from openai import OpenAI
import logging
from datetime import datetime
# 设置日志
logger = logging.getLogger(__name__)
# 添加项目根目录到sys.path
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(current_dir)
sys.path.insert(0, project_root)
class MimoAI:
"""小米MiMoAI引擎 - 使用MiMo-v2-flash模型 - OpenAI格式"""
requires_api_key = True # MiMo引擎需要API密钥
def get_help_info(self):
"""获取引擎帮助信息"""
help_text = """
MiMo AI引擎插件帮助信息
==========================
引擎名称: MiMo
描述: 基于MiMo模型的AI引擎,由小米公司开发
可用命令:
无特定命令,直接与AI对话即可
使用说明:
1. 此引擎使用小米MiMo云API
2. 使用/engine.switch mimo切换到此引擎
3. 直接输入自然语言与AI对话
4. 支持思考模式和多轮对话
"""
return help_text
def __init__(self):
self.name = "mimo"
self.cli = None
# 直接从 config.json 读取配置
config_file = os.path.join(project_root, "config.json")
try:
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
engine_config = config.get("api", {}).get("engines", {}).get("mimo", {})
self.api_key = engine_config.get("api_key", "")
base_url_raw = engine_config.get("base_url", "")
self.base_url = base_url_raw.rstrip('/') + '/chat/completions' if base_url_raw else ""
self.model = engine_config.get("model", "")
except Exception as e:
# 如果读取失败,使用空值
self.api_key = ""
self.base_url = ""
self.model = ""
# 初始化对话历史
self.conversation_history = []
# 设置最大历史记录数
try:
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
self.max_history = config.get("system", {}).get("max_history", 10)
except:
self.max_history = 10
# 初始化 headers(用于 API 请求)
self.headers = {
'Authorization': f'Bearer {self.api_key}',
'Content-Type': 'application/json'
}
# 初始化 OpenAI 客户端
try:
if self.api_key and self.base_url:
self.client = OpenAI(
api_key=self.api_key,
base_url=self.base_url.replace('/chat/completions', ''),
timeout=None
)
else:
self.client = None
except Exception as e:
logger.warning(f"初始化 MiMo OpenAI 客户端失败: {e}")
self.client = None
def generate_response(self, user_input, tool_results=None, system_prompt=None):
"""生成AI响应 - 使用MiMo-v2-flash - OpenAI格式"""
if not self.client:
return "MiMo引擎未正确初始化,无法调用API"
try:
# 获取当前日期和星期
now = datetime.now()
date_str = now.strftime("%Y年%m月%d日")
week_str = ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"][now.weekday()]
current_datetime = f"{date_str} {week_str}"
# 设置默认系统提示词(如果未提供)
if not system_prompt:
system_prompt = f"You are MiMo, an AI assistant developed by Xiaomi. Today is date: {current_datetime}. Your knowledge cutoff date is December 2024."
# 如果有工具结果,将其作为上下文返回给AI处理
if tool_results:
tool_result_text = f"工具执行结果: {tool_results.get('result', '无结果')}"
# 构建包含历史记录的消息列表
messages = self._build_messages_with_history(system_prompt, tool_result_text)
# 构建请求体,使用MiMo特定参数
body = {
"model": self.model,
"messages": messages,
"temperature": 0.7,
"max_tokens": 2048,
"top_p": 0.95,
"frequency_penalty": 0,
"presence_penalty": 0,
# MiMo特定参数
"extra_body": {
"thinking": {"type": "disabled"} # 禁用思考模式
}
}
# 直接使用requests调用MiMo API,因为需要特定的header格式
import requests
response = requests.post(
url=self.base_url,
json=body,
headers=self.headers,
timeout=None
)
if response.status_code == 200:
response_data = response.json()
if "choices" in response_data and len(response_data["choices"]) > 0:
ai_response = response_data["choices"][0]["message"]["content"]
# 处理思考标记
processed_response = self._process_thinking_content(ai_response)
# 更新对话历史
self._update_conversation_history(user_input if not tool_results else tool_result_text, ai_response)
# 返回处理后的AI回复,前面加上✦
return processed_response
else:
return "MiMo-v2-flash API返回格式错误"
elif response.status_code == 401:
return "MiMo-v2-flash API认证失败,请检查API密钥是否正确配置"
else:
return f"MiMo-v2-flash API调用失败: {response.status_code} - {response.text}"
# 构建包含历史记录的消息列表
messages = self._build_messages_with_history(system_prompt, user_input)
# 构建请求体,使用MiMo特定参数
body = {
"model": self.model,
"messages": messages,
"temperature": 0.7,
"max_tokens": 2048,
"top_p": 0.95,
"frequency_penalty": 0,
"presence_penalty": 0,
# MiMo特定参数
"extra_body": {
"thinking": {"type": "disabled"} # 禁用思考模式
}
}
# 直接使用requests调用MiMo API,因为需要特定的header格式
import requests
response = requests.post(
url=self.base_url,
json=body,
headers=self.headers,
timeout=None
)
if response.status_code == 200:
response_data = response.json()
if "choices" in response_data and len(response_data["choices"]) > 0:
ai_response = response_data["choices"][0]["message"]["content"]
# 处理思考标记
processed_response = self._process_thinking_content(ai_response)
# 更新对话历史
self._update_conversation_history(user_input, ai_response)
# 返回处理后的AI回复
return processed_response
else:
return "MiMo-v2-flash API返回格式错误"
elif response.status_code == 401:
return "MiMo-v2-flash API认证失败,请检查API密钥是否正确配置"
else:
return f"MiMo-v2-flash API调用失败: {response.status_code} - {response.text}"
except Exception as e:
# 如果API调用失败,返回错误信息
return f"MiMo-v2-flash API调用失败: {str(e)}"
def _build_messages_with_history(self, system_prompt, user_input):
"""构建包含历史记录的消息列表"""
messages = []
# 添加系统提示词
if system_prompt:
messages.append({
"role": "system",
"content": system_prompt
})
# 添加历史对话记录(使用共享历史)
if self.shared_conversation_history is not None:
# 确保历史记录格式正确
for msg in self.shared_conversation_history[-self.max_history:]:
if isinstance(msg, dict) and "role" in msg and "content" in msg:
messages.append(msg)
else:
# 如果格式不正确,跳过该条记录
continue
# 添加当前用户输入
messages.append({
"role": "user",
"content": user_input
})
return messages
def _update_conversation_history(self, user_input, ai_response):
"""更新对话历史"""
# 添加用户输入到历史记录
self.conversation_history.append({
"role": "user",
"content": user_input
})
# 添加AI回复到历史记录
self.conversation_history.append({
"role": "assistant",
"content": ai_response
})
# 限制历史记录数量
if len(self.conversation_history) > self.max_history * 2:
self.conversation_history = self.conversation_history[-self.max_history * 2:]
def _call_api_with_history(self, content, system_prompt=None):
"""使用对话历史调用API"""
# 获取当前日期和星期
now = datetime.now()
date_str = now.strftime("%Y年%m月%d日")
week_str = ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"][now.weekday()]
current_datetime = f"{date_str} {week_str}"
# 设置默认系统提示词(如果未提供)
if not system_prompt:
system_prompt = f"You are MiMo, an AI assistant developed by Xiaomi. Today is date: {current_datetime}. Your knowledge cutoff date is December 2024."
# 构建消息列表
messages = []
# 添加系统提示词(如果提供)
if system_prompt:
messages.append({
"role": "system",
"content": system_prompt
})
# 添加历史对话记录(使用共享历史)
if self.shared_conversation_history is not None:
# 确保历史记录格式正确
for msg in self.shared_conversation_history[-self.max_history:]:
if isinstance(msg, dict) and "role" in msg and "content" in msg:
messages.append(msg)
else:
# 如果格式不正确,跳过该条记录
continue
# 添加当前内容
messages.append({
"role": "user",
"content": content
})
# 构建请求体
body = {
"model": self.model,
"messages": messages,
"max_completion_tokens": 1024,
"temperature": 0.3,
"top_p": 0.95,
"stream": False,
"frequency_penalty": 0,
"presence_penalty": 0,
"extra_body": {
"thinking": {"type": "disabled"}
}
}
# 发送请求到MiMo-v2-flash API
response = requests.post(url=self.base_url, json=body, headers=self.headers, timeout=None)
if response.status_code == 200:
response_data = response.json()
if "choices" in response_data and len(response_data["choices"]) > 0:
ai_response = response_data["choices"][0]["message"]["content"]
# 处理思考标记
processed_response = self._process_thinking_content(ai_response)
return processed_response
else:
return "MiMo-v2-flash API返回格式错误"
else:
return f"MiMo-v2-flash API调用失败: {response.status_code} - {response.text}"
def handle_command(self, command):
"""处理引擎特定命令"""
# MiMo引擎目前不支持特定命令
print(f"{Fore.RED}MiMo引擎不支持此命令{Style.RESET_ALL}")
return False
def _process_thinking_content(self, response):
"""处理思考内容,将思考部分以灰色文本显示,只返回回复部分"""
from colorama import Fore, Style
import re
# 这里可以设置思考标记,由用户自己定义
thinking_start_marker = "<thinking>"
thinking_end_marker = "</thinking>"
# 如果响应中包含思考标记,则提取思考内容和回复内容
if thinking_start_marker in response and thinking_end_marker in response:
# 提取思考内容
thinking_match = re.search(f'{thinking_start_marker}(.*?){thinking_end_marker}', response, re.DOTALL)
if thinking_match:
thinking_content = thinking_match.group(1).strip()
# 打印思考内容(灰色)
print(f"{Fore.LIGHTBLACK_EX}思考: {thinking_content}{Style.RESET_ALL}")
# 提取回复内容
response_content = re.split(f'{thinking_end_marker}', response, maxsplit=1)[-1].strip()
return response_content
# 如果没有思考标记,直接返回响应
return response
+401
View File
@@ -0,0 +1,401 @@
import os
import sys
import json
import requests
import threading
import time
from colorama import Fore, Style
# 延迟导入 ollama,避免程序启动时卡住
# 如果导入失败,ollama_client 将为 None
try:
import ollama
ollama_client = ollama
except Exception as e:
print(f"{Fore.YELLOW}警告: 无法导入 ollama 库: {e}{Style.RESET_ALL}")
print(f"{Fore.YELLOW}请确保已安装 ollama 包 (pip install ollama){Style.RESET_ALL}")
ollama_client = None
# 添加项目根目录到sys.path
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(current_dir)
sys.path.insert(0, project_root)
class OllamaAI:
"""基于Ollama的本地AI引擎"""
requires_api_key = False # Ollama是本地引擎,不需要API密钥
def get_help_info(self):
"""获取引擎帮助信息"""
help_text = """
Ollama AI引擎插件帮助信息
========================
引擎名称: Ollama
描述: 基于Ollama的本地AI引擎,支持多种大型语言模型
可用命令:
/engine.ollama models - 列出本地可用的模型
/engine.ollama remote-models - 显示可下载的模型列表
/engine.ollama search <关键词> - 搜索可下载的模型
/engine.ollama set <模型名> - 设置当前使用的模型
/engine.ollama pull <模型名> - 从Ollama库拉取模型
注意: 本引擎强制使用流式输出,AI回复会实时显示。
使用说明:
1. 确保Ollama服务已在本地运行
2. 使用/engine.switch ollama切换到此引擎
3. 使用上述命令管理模型和与AI交互
思考模式:
- 支持深度思考模型(如 QwQ、DeepSeek-R1)
- 思考过程会自动以灰色文本显示
"""
return help_text
def __init__(self):
self.name = "ollama"
self.cli = None # 引用CLI实例以访问插件
# 初始化对话历史
self.conversation_history = []
# 直接从 config.json 读取配置
config_file = os.path.join(project_root, "config.json")
try:
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
engine_config = config.get("api", {}).get("engines", {}).get("ollama", {})
self.api_key = engine_config.get("api_key", "")
self.base_url = engine_config.get("base_url", "")
self.model = engine_config.get("model", "")
self.max_history = config.get("system", {}).get("max_history", 10)
except Exception as e:
# 如果读取失败,使用空值
self.api_key = ""
self.base_url = ""
self.model = ""
self.max_history = 10
# 共享对话历史引用(由CLI设置)
self.shared_conversation_history = None
# 直接启用思考标记,由主程序自动处理深度思考模型的响应
self.thinking_start_marker = "<thinking>"
self.thinking_end_marker = "</thinking>"
# 检测Ollama服务是否运行
self.is_service_running = self.check_ollama_service()
if self.is_service_running:
print(f"{Fore.GREEN}Ollama服务检测成功,引擎已启用{Style.RESET_ALL}")
else:
print(f"{Fore.RED}Ollama服务未运行,引擎将不可用{Style.RESET_ALL}")
def check_ollama_service(self):
"""检测Ollama服务是否运行"""
# 检查 ollama 库是否可用
if ollama_client is None:
return False
try:
# 尝试调用Ollama API获取版本信息
response = ollama_client.list()
# 如果没有异常,说明服务运行正常
return True
except Exception as e:
# 如果出现异常,说明服务未运行
return False
def generate_response(self, user_input, tool_results=None, system_prompt=None):
"""生成AI响应 - 使用Ollama API"""
try:
# 检查Ollama服务是否运行
if not self.is_service_running:
return "错误:Ollama服务未运行,请启动Ollama服务后再使用此引擎."
# 如果有工具结果,将其作为上下文返回给AI处理
if tool_results:
tool_result_text = f"工具执行结果: {tool_results.get('result', '无结果')}"
# 构建包含历史记录的消息列表
messages = self._build_messages_with_history(system_prompt, tool_result_text)
# 使用Ollama Python库调用API
response = ollama_client.chat(
model=self.model,
messages=messages,
stream=False
)
# 返回AI的回复
return response["message"]["content"]
# 构建包含历史记录的消息列表
messages = self._build_messages_with_history(system_prompt, user_input)
# 使用Ollama Python库调用API
response = ollama_client.chat(
model=self.model,
messages=messages,
stream=False
)
# 返回AI的回复
content = response["message"]["content"]
# 清理无效的 UTF-8 代理字符
content = content.encode('utf-8', 'replace').decode('utf-8')
return content
except Exception as e:
# 如果API调用失败,返回错误信息
return f"Ollama API调用失败: {str(e)}"
def _build_messages_with_history(self, system_prompt, user_input):
"""构建包含历史记录的消息列表"""
messages = []
# 添加系统提示词
if system_prompt:
# 清理代理字符
clean_prompt = system_prompt.encode('utf-8', 'replace').decode('utf-8')
messages.append({
"role": "system",
"content": clean_prompt
})
# 添加历史对话记录(使用共享历史)
if self.shared_conversation_history is not None:
# 确保历史记录格式正确
for msg in self.shared_conversation_history[-self.max_history:]:
if isinstance(msg, dict) and "role" in msg and "content" in msg:
# 清理内容中的代理字符
clean_content = msg["content"].encode('utf-8', 'replace').decode('utf-8')
messages.append({"role": msg["role"], "content": clean_content})
else:
# 如果格式不正确,跳过该条记录
continue
# 清理用户输入
if user_input:
clean_input = user_input.encode('utf-8', 'replace').decode('utf-8')
messages.append({
"role": "user",
"content": clean_input
})
return messages
def _call_api_with_history(self, content, system_prompt=None):
"""使用对话历史调用API"""
if not self.is_service_running:
return "错误:Ollama服务未运行,请启动Ollama服务后再使用此引擎."
# 构建消息列表
messages = []
# 添加系统提示词(如果提供)
if system_prompt:
messages.append({
"role": "system",
"content": system_prompt
})
# 添加历史对话记录(使用共享历史)
if self.shared_conversation_history is not None:
# 确保历史记录格式正确
for msg in self.shared_conversation_history[-self.max_history:]:
if isinstance(msg, dict) and "role" in msg and "content" in msg:
messages.append(msg)
else:
# 如果格式不正确,跳过该条记录
continue
# 添加当前内容
messages.append({
"role": "tool",
"content": content
})
# 使用Ollama Python库调用API
response = ollama_client.chat(
model=self.model,
messages=messages,
stream=False
)
return response["message"]["content"]
def set_model(self, model_name):
"""设置要使用的模型"""
if not self.is_service_running:
print(f"{Fore.RED}错误:Ollama服务未运行,无法切换模型{Style.RESET_ALL}")
return
self.model = model_name
print(f"{Fore.GREEN}已切换Ollama模型为: {model_name}{Style.RESET_ALL}")
def list_models(self):
"""列出本地可用的模型"""
if not self.is_service_running:
return ["错误:Ollama服务未运行"]
try:
response = ollama_client.list()
models = [model["name"] for model in response["models"]]
return models
except Exception as e:
return [f"获取模型列表失败: {str(e)}"]
def list_remote_models(self, limit=10):
"""获取可下载的模型列表(仅展示前N个)"""
if not self.is_service_running:
return ["错误:Ollama服务未运行"]
try:
# 使用Ollama Python库获取可下载的模型列表
# 注意:Ollama库本身没有直接获取可下载模型列表的API
# 这里返回一些常见的模型作为示例
# 实际应用中可能需要从Ollama官网或其他来源获取此信息
common_models = [
"llama3.1:latest",
"llama3:latest",
"mistral:latest",
"gemma2:latest",
"phi3:latest",
"qwen2:latest",
"command-r:latest",
"dolphin-mistral:latest",
"nous-hermes2:latest",
"llava:latest",
"codellama:latest",
"mxbai-embed-large:latest",
"all-minilm:latest",
"stable-diffusion:latest",
"moondream:latest"
]
return common_models[:limit]
except Exception as e:
return [f"获取远程模型列表失败: {str(e)}"]
def search_models(self, query, limit=10):
"""模糊搜索可下载的模型"""
if not self.is_service_running:
return ["错误:Ollama服务未运行"]
try:
# 使用Ollama Python库获取可下载的模型列表
# 注意:Ollama库本身没有直接获取可下载模型列表的API
all_models = [
"llama3.1:latest", "llama3:latest", "llama2:latest",
"mistral:latest", "mistral-openorca:latest",
"gemma2:latest", "gemma:latest",
"phi3:latest", "phi2:latest",
"qwen2:latest", "qwen:latest",
"command-r:latest", "command-r-plus:latest",
"dolphin-mistral:latest", "dolphin-llama3:latest",
"nous-hermes2:latest", "nous-hermes:latest",
"llava:latest", "bakllava:latest",
"codellama:latest", "codeup:latest",
"mxbai-embed-large:latest", "nomic-embed-text:latest",
"all-minilm:latest", "bge-m3:latest",
"stable-diffusion:latest", "moondream:latest",
"llama3-groq:latest", "eva-qwen2:latest"
]
# 进行模糊搜索(简单匹配,包含查询词的模型)
matched_models = [model for model in all_models if query.lower() in model.lower()]
# 限制返回数量
return matched_models[:limit]
except Exception as e:
return [f"搜索模型失败: {str(e)}"]
def pull_model(self, model_name):
"""从Ollama库拉取模型"""
if not self.is_service_running:
print(f"{Fore.RED}错误:Ollama服务未运行,无法拉取模型{Style.RESET_ALL}")
return False
try:
print(f"{Fore.YELLOW}正在拉取模型 {model_name}...{Style.RESET_ALL}")
# 使用Ollama Python库拉取模型
response = ollama_client.pull(model_name, stream=True)
# 逐块处理响应
for part in response:
if 'status' in part:
status = part['status']
print(f"{Fore.YELLOW}{status}{Style.RESET_ALL}")
# 检查是否完成
if 'completed' in part and 'total' in part:
if part['completed'] == part['total']:
print(f"{Fore.GREEN}模型 {model_name} 拉取完成!{Style.RESET_ALL}")
print(f"{Fore.GREEN}模型 {model_name} 拉取成功!{Style.RESET_ALL}")
return True
except Exception as e:
print(f"{Fore.RED}拉取模型 {model_name} 失败: {str(e)}{Style.RESET_ALL}")
return False
def handle_command(self, command):
"""处理引擎特定命令"""
if not self.is_service_running:
print(f"{Fore.RED}Ollama服务未运行,无法执行命令{Style.RESET_ALL}")
return False
if command == "models":
# 显示本地可用模型
models = self.list_models()
print(f"{Fore.GREEN}本地可用的Ollama模型:{Style.RESET_ALL}")
for model in models:
status = " (当前使用)" if model == self.model else ""
print(f"{Fore.GREEN} - {model}{status}{Style.RESET_ALL}")
return True
elif command == "remote-models":
# 显示可下载的模型(前10个)
models = self.list_remote_models()
print(f"{Fore.GREEN}可下载的Ollama模型(前10个):{Style.RESET_ALL}")
for i, model in enumerate(models, 1):
print(f"{Fore.GREEN} {i}. {model}{Style.RESET_ALL}")
return True
elif command.startswith("search "):
# 搜索模型
query = command[7:].strip() # 移除 'search '
if query:
models = self.search_models(query)
if models:
print(f"{Fore.GREEN}搜索 '{query}' 的结果(前10个):{Style.RESET_ALL}")
for i, model in enumerate(models, 1):
print(f"{Fore.GREEN} {i}. {model}{Style.RESET_ALL}")
else:
print(f"{Fore.YELLOW}未找到与 '{query}' 匹配的模型{Style.RESET_ALL}")
return True
else:
print(f"{Fore.RED}请提供搜索关键词.用法: /engine.search <关键词>{Style.RESET_ALL}")
return False
elif command.startswith("set "):
# 设置模型
model_name = command[4:].strip() # 移除 'set '
if model_name:
self.set_model(model_name)
return True
else:
print(f"{Fore.RED}请提供模型名称.用法: /engine.set <模型名>{Style.RESET_ALL}")
return False
elif command.startswith("pull "):
# 拉取模型
model_name = command[5:].strip() # 移除 'pull '
if model_name:
success = self.pull_model(model_name)
return success
else:
print(f"{Fore.RED}请提供模型名称.用法: /engine.pull <模型名>{Style.RESET_ALL}")
return False
else:
print(f"{Fore.RED}未知的Ollama命令.可用命令: models, remote-models, search, set, pull{Style.RESET_ALL}")
return False
+291
View File
@@ -0,0 +1,291 @@
import os
import json
import requests
import threading
import time
from colorama import Fore, Style
class QwqAI:
"""基于QwQ-32B模型的AI引擎"""
def get_help_info(self):
"""获取引擎帮助信息"""
help_text = """
QwQ-32B AI引擎插件帮助信息
==========================
引擎名称: QwQ
描述: 基于QwQ-32B模型的AI引擎,支持复杂推理和数学计算
可用命令:
无特定命令,直接与AI对话即可
使用说明:
1. 此引擎使用QwQ-32B云API
2. 使用/engine.switch qwq切换到此引擎
3. 直接输入自然语言与AI对话
4. 特别适合处理数学、逻辑推理等复杂任务
"""
return help_text
def __init__(self):
self.name = "qwq"
self.cli = None # 引用CLI实例以访问插件
# 直接从 config.json 读取配置
import json
import os
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
config_file = os.path.join(project_root, "config.json")
try:
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
engine_config = config.get("api", {}).get("engines", {}).get("qwq", {})
self.api_key = engine_config.get("api_key", "")
self.url = engine_config.get("base_url", "")
self.model = engine_config.get("model", "")
except Exception as e:
# 如果读取失败,使用空值
print(f"{Fore.YELLOW}警告: 读取配置文件失败: {e}{Style.RESET_ALL}")
self.api_key = ""
self.url = ""
self.model = ""
self.headers = {
'Authorization': f'Bearer {self.api_key}',
'Content-Type': 'application/json'
}
# 初始化对话历史
self.conversation_history = []
# 设置最大历史记录数
try:
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
self.max_history = config.get("system", {}).get("max_history", 10)
except:
self.max_history = 10
# 设置思考标记
self.thinking_start_marker = ""
self.thinking_end_marker = ""
# 共享对话历史引用(由CLI设置)
self.shared_conversation_history = None
def generate_response(self, user_input, tool_results=None, system_prompt=None):
"""生成AI响应 - 使用QwQ API"""
try:
# 如果有工具结果,将其作为上下文返回给AI处理
if tool_results:
tool_result_text = f"工具执行结果: {tool_results.get('result', '无结果')}"
# 构建消息列表
messages = self._build_messages_with_history(system_prompt, tool_result_text)
# 构建请求体
body = {
"model": self.model,
"messages": messages,
"stream": False # 使用非流式响应以便于处理
}
# 发送请求到QwQ API
response = requests.post(url=self.url, json=body, headers=self.headers, timeout=None)
if response.status_code == 200:
response_data = response.json()
if "choices" in response_data and len(response_data["choices"]) > 0:
ai_response = response_data["choices"][0]["message"]["content"]
# 处理思考标记
processed_response = self._process_thinking_content(ai_response)
# 返回处理后的AI回复
return processed_response
else:
return "QwQ API返回格式错误"
else:
return f"QwQ API调用失败: {response.status_code} - {response.text}"
# 构建消息列表
messages = self._build_messages_with_history(system_prompt, user_input)
# 构建请求体
body = {
"model": self.model,
"messages": messages,
"stream": False # 使用非流式响应以便于处理
}
# 发送请求到QwQ API
response = requests.post(url=self.url, json=body, headers=self.headers, timeout=None)
if response.status_code == 200:
response_data = response.json()
if "choices" in response_data and len(response_data["choices"]) > 0:
ai_response = response_data["choices"][0]["message"]["content"]
# 处理思考标记
processed_response = self._process_thinking_content(ai_response)
# 返回处理后的AI回复
return processed_response
else:
return "QwQ API返回格式错误"
else:
return f"QwQ API调用失败: {response.status_code} - {response.text}"
except Exception as e:
# 如果API调用失败,返回错误信息
return f"QwQ API调用失败: {str(e)}"
def _build_messages_with_history(self, system_prompt, user_input):
"""构建包含历史记录的消息列表"""
messages = []
# 添加系统提示词
if system_prompt:
messages.append({
"role": "system",
"content": system_prompt
})
# 添加历史对话记录(使用共享历史)
if self.shared_conversation_history is not None:
# 确保历史记录格式正确
for msg in self.shared_conversation_history[-self.max_history:]:
if isinstance(msg, dict) and "role" in msg and "content" in msg:
messages.append(msg)
else:
# 如果格式不正确,跳过该条记录
continue
# 添加当前用户输入
messages.append({
"role": "user",
"content": user_input
})
return messages
def _call_api_with_history(self, content, system_prompt=None):
"""使用对话历史调用API"""
# 构建消息列表
messages = []
# 添加系统提示词(如果提供)
if system_prompt:
messages.append({
"role": "system",
"content": system_prompt
})
# 添加历史对话记录(使用共享历史)
if self.shared_conversation_history is not None:
# 确保历史记录格式正确
for msg in self.shared_conversation_history[-self.max_history:]:
if isinstance(msg, dict) and "role" in msg and "content" in msg:
messages.append(msg)
else:
# 如果格式不正确,跳过该条记录
continue
# 添加当前内容
messages.append({
"role": "tool",
"content": content
})
# 构建请求体
body = {
"model": self.model,
"messages": messages,
"stream": False # 使用非流式响应以便于处理
}
# 发送请求到QwQ API
response = requests.post(url=self.url, json=body, headers=self.headers, timeout=None)
if response.status_code == 200:
response_data = response.json()
if "choices" in response_data and len(response_data["choices"]) > 0:
ai_response = response_data["choices"][0]["message"]["content"]
# 处理思考标记
processed_response = self._process_thinking_content(ai_response)
return processed_response
else:
return "QwQ API返回格式错误"
else:
return f"QwQ API调用失败: {response.status_code} - {response.text}"
def handle_command(self, command):
"""处理引擎特定命令"""
# QwQ引擎目前不支持特定命令
print(f"{Fore.RED}QwQ引擎不支持此命令{Style.RESET_ALL}")
return False
def _process_thinking_content(self, response):
"""处理思考内容,将思考部分以灰色文本显示,只返回回复部分"""
from colorama import Fore, Style
import re
# 这里可以设置思考标记,由用户自己定义
thinking_start_marker = "<think>" # 用户可以设置开始标记
thinking_end_marker = "</think>" # 用户可以设置结束标记
# 如果用户设置了思考标记,则处理思考内容
if thinking_start_marker and thinking_end_marker and thinking_start_marker in response and thinking_end_marker in response:
# 分离思考内容和回复内容
parts = response.split(thinking_start_marker)
if len(parts) > 1:
# 获取包含结束标记的部分
remaining = parts[1]
thinking_content = ""
reply_content = ""
# 检查结束标记
if thinking_end_marker in remaining:
thinking_part, reply_part = remaining.split(thinking_end_marker, 1)
thinking_content = thinking_part.strip()
reply_content = reply_part.strip()
else:
# 如果没有找到结束标记,将剩余部分都视为思考内容
thinking_content = remaining.strip()
reply_content = ""
# 显示思考内容为灰色文本
if thinking_content:
print(f"{Fore.LIGHTBLACK_EX}[思考: {thinking_content}]{Style.RESET_ALL}")
# 返回回复内容
if reply_content:
return reply_content
else:
return "" # 如果没有回复内容,返回空字符串
else:
# 如果没有设置思考标记或没有找到标记,直接返回原始响应
return response
+275
View File
@@ -0,0 +1,275 @@
import os
import sys
import json
import hashlib
import hmac
import base64
import websocket
import time
import threading
from datetime import datetime
from time import mktime
from urllib.parse import urlparse, urlencode
from wsgiref.handlers import format_date_time
from colorama import Fore, Style
# 获取项目根目录
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(current_dir)
sys.path.insert(0, project_root)
class XfyunSparkAI:
"""基于讯飞星火深度推理X2模型的AI引擎(WebSocket接口)"""
requires_api_key = True # 讯飞引擎需要API密钥
# X2深度推理版本固定配置
URL = "wss://spark-api.xf-yun.com/x2"
DOMAIN = "spark-x"
MAX_TOKENS = 131072 # X2输出上限128K
def __init__(self):
self.name = "xfyun-spark"
self.cli = None
# 直接从 config.json 读取配置
config_file = os.path.join(project_root, "config.json")
try:
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
engine_config = config.get("api", {}).get("engines", {}).get("xfyun_spark", {})
self.app_id = engine_config.get("app_id", "")
self.api_key = engine_config.get("api_key", "")
self.api_secret = engine_config.get("api_secret", "")
self.max_history = config.get("system", {}).get("max_history", 10)
except Exception as e:
self.app_id = ""
self.api_key = ""
self.api_secret = ""
self.max_history = 10
# 初始化对话历史
self.conversation_history = []
self.shared_conversation_history = None
self.chat_id = f"chat_{self.app_id}_{int(time.time())}" # 固定会话ID,确保上下文连续性
# 验证API密钥
if not self.app_id or not self.api_key or not self.api_secret:
print(f"{Fore.YELLOW}警告: xfyun-spark引擎的API配置未完整设置(需要app_id、api_key、api_secret){Style.RESET_ALL}")
def get_help_info(self):
"""获取引擎帮助信息"""
help_text = f"""
讯飞星火深度推理X2引擎帮助信息
================================
引擎名称: xfyun-spark
描述: 基于讯飞星火深度推理X2模型的AI引擎,支持WebSocket流式输出
版本: X2深度推理 (输入64K, 输出128K)
配置要求:
在config.json中配置:
{{
"api": {{
"engines": {{
"xfyun_spark": {{
"app_id": "你的应用ID",
"api_key": "你的API Key",
"api_secret": "你的API Secret"
}}
}}
}}
}}
使用说明:
1. 使用/engine.switch xfyun-spark切换到此引擎
2. 直接输入自然语言与AI对话
3. 本引擎强制使用流式输出,AI回复会实时显示
4. 支持深度思考模式,思考过程会自动显示
注意: 本引擎使用WebSocket协议,需要app_id、api_key、api_secret三个参数
"""
return help_text
def _generate_url(self):
"""生成鉴权URL(使用官方示例的方式)"""
# 生成RFC1123格式的时间戳
now = datetime.now()
date = format_date_time(mktime(now.timetuple()))
# 解析URL获取host和path
parsed_url = urlparse(self.URL)
host = parsed_url.netloc
path = parsed_url.path
# 拼接签名字符串
signature_origin = "host: " + host + "\n"
signature_origin += "date: " + date + "\n"
signature_origin += "GET " + path + " HTTP/1.1"
# 进行hmac-sha256进行加密
signature_sha = hmac.new(
self.api_secret.encode('utf-8'),
signature_origin.encode('utf-8'),
digestmod=hashlib.sha256
).digest()
signature_sha_base64 = base64.b64encode(signature_sha).decode(encoding='utf-8')
# 拼接authorization_origin
authorization_origin = f'api_key="{self.api_key}", algorithm="hmac-sha256", headers="host date request-line", signature="{signature_sha_base64}"'
authorization = base64.b64encode(authorization_origin.encode('utf-8')).decode(encoding='utf-8')
# 将请求的鉴权参数组合为字典
v = {
"authorization": authorization,
"date": date,
"host": host
}
# 拼接鉴权参数,生成url
url = self.URL + '?' + urlencode(v)
return url
def _build_messages_with_history(self, system_prompt, user_input):
"""构建包含历史记录的消息列表(传递完整历史对话)"""
messages = []
# 添加系统提示词
if system_prompt:
messages.append({
"role": "system",
"content": system_prompt
})
# 添加完整的历史对话(不限制数量,让API自己处理)
if self.shared_conversation_history:
for msg in self.shared_conversation_history:
if isinstance(msg, dict) and "role" in msg and "content" in msg:
role = msg["role"]
content = msg["content"]
# 转换角色名称
if role == "user":
messages.append({"role": "user", "content": content})
elif role == "assistant":
messages.append({"role": "assistant", "content": content})
# 添加当前用户输入
messages.append({
"role": "user",
"content": user_input
})
return messages
def generate_response(self, user_input, tool_results=None, system_prompt=None):
"""生成AI响应(非流式)"""
try:
# 处理工具结果
if tool_results:
tool_result_text = f"工具执行结果: {tool_results.get('result', '无结果')}"
messages = self._build_messages_with_history(system_prompt, tool_result_text)
else:
messages = self._build_messages_with_history(system_prompt, user_input)
# 构建请求数据
request_data = {
"header": {
"app_id": self.app_id,
"uid": str(time.time()).replace('.', '')[:32]
},
"parameter": {
"chat": {
"domain": self.DOMAIN,
"temperature": 0,
"max_tokens": 4096,
"presence_penalty": 1,
"frequency_penalty": 0.02,
"top_k": 5,
"chat_id": self.chat_id
}
},
"payload": {
"message": {
"text": messages
}
}
}
# 生成鉴权URL
auth_url = self._generate_url()
# 收集完整响应
full_response = ""
thinking_content = ""
ws = None
result_queue = []
done_event = threading.Event()
def on_message(ws, message):
nonlocal full_response, thinking_content
try:
data = json.loads(message)
code = data.get("header", {}).get("code", -1)
if code != 0:
result_queue.append(("error", data.get('header', {}).get('message', '未知错误')))
done_event.set()
return
payload = data.get("payload", {})
choices = payload.get("choices", {})
# 处理文本响应
text_list = choices.get("text", [])
if text_list:
reasoning = text_list[0].get("reasoning_content", "")
if reasoning:
thinking_content += reasoning
content = text_list[0].get("content", "")
if content:
full_response += content
if choices.get("status") == 2:
done_event.set()
except Exception as e:
result_queue.append(("error", str(e)))
done_event.set()
def on_error(ws, error):
result_queue.append(("error", str(error)))
done_event.set()
def on_close(ws, close_status_code, close_msg):
pass
def on_open(ws):
ws.send(json.dumps(request_data))
# 创建WebSocket连接
ws = websocket.WebSocketApp(
auth_url,
on_open=on_open,
on_message=on_message,
on_error=on_error,
on_close=on_close
)
# 在后台线程运行WebSocket
ws_thread = threading.Thread(target=ws.run_forever)
ws_thread.daemon = True
ws_thread.start()
# 等待完成
done_event.wait(timeout=60)
# 返回完整响应
return full_response
except Exception as e:
print(f"\n{Fore.RED}讯飞星火API调用失败: {str(e)}{Style.RESET_ALL}")
return f"讯飞星火API调用失败: {str(e)}"
+259
View File
@@ -0,0 +1,259 @@
"""
Clawli WebSocket Server - 独立进程 WebSocket 服务器
用于 PC-手机端通信
"""
import multiprocessing
from multiprocessing import Process, Queue
from typing import Optional, List
import threading
import time
import json
import base64
import os
import asyncio
class ClawliWebSocketServer:
"""Clawli WebSocket 服务器管理类"""
def __init__(self):
self.process: Optional[Process] = None
self.input_queue: Optional[Queue] = None
self.output_queue: Optional[Queue] = None
self.running = False
self.host = "0.0.0.0"
self.port = 9079
self.password = ""
def start(self, host: str = "0.0.0.0", port: int = 9079, password: str = "") -> bool:
"""启动 WebSocket 服务器进程"""
if self.running:
print("[Clawli] 服务器已在运行")
return True
self.host = host
self.port = int(port)
self.password = password
self.input_queue = Queue()
self.output_queue = Queue()
self.process = Process(
target=_ws_server_main,
args=(self.host, self.port, self.password, self.input_queue, self.output_queue)
)
self.process.daemon = True
self.process.start()
self.running = True
print(f"[Clawli] WebSocket 服务器已启动: ws://{host}:{self.port}")
return True
def stop(self):
"""停止服务器"""
if not self.running:
return
self.running = False
if self.process:
self.process.terminate()
self.process.join(timeout=2)
if self.process.is_alive():
self.process.kill()
self.process = None
self.input_queue = None
self.output_queue = None
print("[Clawli] WebSocket 服务器已停止")
def send_message(self, content: str, msg_type: str = "text", **kwargs) -> tuple:
"""发送消息到手机"""
if not self.running or not self.input_queue:
print(f"[Clawli] 发送失败: 服务器未运行")
return False, "服务器未运行"
try:
msg = {
"type": msg_type,
"content": content,
"timestamp": time.time(),
**kwargs
}
self.input_queue.put(msg)
print(f"[Clawli] 消息已放入队列: type={msg_type}, len={len(content)}")
return True, "消息已发送"
except Exception as e:
print(f"[Clawli] 发送异常: {e}")
return False, f"发送失败: {e}"
def send_image(self, image_path: str = None, image_data: str = None,
filename: str = "", caption: str = "") -> tuple:
"""发送图片到手机"""
if not self.running or not self.input_queue:
return False, "服务器未运行"
try:
if image_path and os.path.exists(image_path):
with open(image_path, 'rb') as f:
image_bytes = f.read()
image_data = base64.b64encode(image_bytes).decode('utf-8')
filename = filename or os.path.basename(image_path)
if not image_data:
return False, "无图片数据"
msg = {
"type": "image",
"image": image_data,
"filename": filename,
"caption": caption,
"timestamp": time.time()
}
self.input_queue.put(msg)
print(f"[Clawli] 图片已放入队列: {filename}")
return True, "图片已发送"
except Exception as e:
return False, f"发送失败: {e}"
def send_tool_status(self, tool_name: str, args: str, status: str, result: str = ""):
"""发送工具状态"""
return self.send_message(
content=result,
msg_type="tool_status",
tool=tool_name,
args=args,
status=status,
result=result
)
def get_messages(self) -> List[dict]:
"""获取从手机收到的消息"""
messages = []
if self.output_queue:
while True:
try:
msg = self.output_queue.get(timeout=0.01)
messages.append(msg)
except:
break
return messages
def is_running(self) -> bool:
return self.running and self.process is not None and self.process.is_alive()
def _ws_server_main(host: str, port: int, password: str,
input_queue: Queue, output_queue: Queue):
"""WebSocket 服务器主进程"""
import websockets
from websockets.server import serve
port = int(port)
running = [True] # 用列表包装以便在闭包中修改
async def handler(websocket):
"""WebSocket 连接处理器"""
# 认证
try:
auth_msg = await asyncio.wait_for(websocket.recv(), timeout=5.0)
auth_data = json.loads(auth_msg)
if auth_data.get("type") != "auth" or auth_data.get("password") != password:
await websocket.send(json.dumps({"type": "auth_failed", "message": "密码错误"}))
return
await websocket.send(json.dumps({"type": "auth_success"}))
print(f"[Clawli WS] 客户端认证成功")
except Exception as e:
print(f"[Clawli WS] 认证失败: {e}")
return
print(f"[Clawli WS] 开始处理消息循环")
try:
# 接收任务
async def receiver():
async for message in websocket:
try:
data = json.loads(message)
output_queue.put(data)
print(f"[Clawli WS] 收到: {data.get('type', 'unknown')}")
except Exception as e:
print(f"[Clawli WS] 解析失败: {e}")
# 发送任务
async def sender():
while running[0]:
try:
# 从队列获取消息(非阻塞)
msg = await asyncio.get_event_loop().run_in_executor(
None, lambda: input_queue.get(timeout=0.2)
)
await websocket.send(json.dumps(msg, ensure_ascii=False))
print(f"[Clawli WS] 发送: {msg.get('type', 'unknown')}")
except:
pass # 超时,继续循环
# 并行运行两个任务
await asyncio.gather(receiver(), sender())
except websockets.exceptions.ConnectionClosed:
print("[Clawli WS] 客户端断开")
except Exception as e:
print(f"[Clawli WS] 连接错误: {e}")
async def main():
print(f"[Clawli WS] 服务器启动于 ws://{host}:{port}")
async with serve(handler, host, port):
await asyncio.Future()
try:
asyncio.run(main())
except KeyboardInterrupt:
running[0] = False
print("[Clawli WS] 服务器关闭")
# 全局实例
_server_instance = ClawliWebSocketServer()
def start_server(host: str = "0.0.0.0", port: int = 9079, password: str = "") -> bool:
return _server_instance.start(host, port, password)
def stop_server():
_server_instance.stop()
def send_message(content: str, msg_type: str = "text", **kwargs) -> tuple:
return _server_instance.send_message(content, msg_type, **kwargs)
def send_image(image_path: str = None, image_data: str = None,
filename: str = "", caption: str = "") -> tuple:
return _server_instance.send_image(image_path, image_data, filename, caption)
def send_tool_status(tool_name: str, args: str, status: str, result: str = ""):
return _server_instance.send_tool_status(tool_name, args, status, result)
def get_messages() -> List[dict]:
return _server_instance.get_messages()
def is_running() -> bool:
return _server_instance.is_running()
if __name__ == "__main__":
start_server("0.0.0.0", 9079, "test")
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
stop_server()
+42
View File
@@ -0,0 +1,42 @@
"""核心配置管理模块"""
import os
import json
import logging
logger = logging.getLogger(__name__)
# 常量
CONFIG_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config.json")
MAX_FILE_SIZE = 52428800 # 50MB
DEFAULT_MAX_HISTORY = 999999
def load_config():
"""加载配置文件"""
try:
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
except FileNotFoundError:
return {"api": {"engines": {}}, "system": {}, "plugins": {}}
except json.JSONDecodeError:
return {"api": {"engines": {}}, "system": {}, "plugins": {}}
def save_config(config):
"""保存配置文件"""
try:
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
json.dump(config, f, ensure_ascii=False, indent=2)
except Exception as e:
logger.error(f"保存配置失败: {e}")
def get_system_config(key, default=None):
"""获取系统配置"""
config = load_config()
return config.get("system", {}).get(key, default)
def set_system_config(key, value):
"""设置系统配置"""
config = load_config()
if "system" not in config:
config["system"] = {}
config["system"][key] = value
save_config(config)
+315
View File
@@ -0,0 +1,315 @@
"""
对话处理模块 - 核心对话循环和工具调用
"""
import json
import re
import time
import threading
import logging
from concurrent.futures import ThreadPoolExecutor, as_completed
from colorama import Fore, Style
logger = logging.getLogger(__name__)
class ConversationManager:
"""对话管理器 - 处理用户输入、AI 响应、工具调用循环"""
def __init__(self, cli):
self.cli = cli
self.history = []
self.max_history = 999999
def process(self, user_input: str):
"""处理完整的对话循环"""
self.history.append({"role": "user", "content": user_input})
tool_prompts = self._get_tool_prompts()
loop_count = 0
current_input = user_input
max_loops = 20
while loop_count < max_loops:
response = self._generate_with_animation(current_input, tool_prompts)
processed = self._process_thinking(response)
processed = self._process_code_blocks(processed)
try:
text_content, json_data = self._parse_response(processed)
if json_data:
if text_content:
self._add_and_display(text_content)
if isinstance(json_data, dict):
result = self._handle_dict(json_data, text_content, processed)
if result == "break":
break
elif result == "continue":
loop_count += 1
continue
elif isinstance(json_data, list):
result = self._handle_list(json_data, text_content, processed)
if result == "break":
break
elif result == "continue":
loop_count += 1
continue
else:
display = text_content if text_content else processed
self._add_and_display(display)
break
except json.JSONDecodeError:
text_content, json_data = self._parse_response(processed)
if json_data:
# Re-process with parsed data
pass
else:
self._add_and_display(processed)
break
if loop_count >= max_loops:
self.cli.display.output(f"AI 已达到最大循环次数 ({max_loops}),已停止")
def _handle_dict(self, data, text_content, processed):
"""处理 dict 类型的 JSON 数据"""
if data.get('action') == 'use_tool':
self.cli._execute_single_tool(data)
return "break"
if data.get('action') == 'continue':
content = data.get('content', '')
if content:
self._add_and_display(content)
return "continue"
if data.get('continue') or data.get('need_continue') or data.get('think_more'):
msg = data.get('message', '')
if msg:
self.history.append({"role": "assistant", "content": msg})
self.cli.display.response(msg, is_continue=True)
return "continue"
msg = data.get('message', '')
if msg:
self._add_and_display(msg)
return "break"
if text_content:
self._add_and_display(text_content)
else:
self._add_and_display(processed)
return "break"
def _handle_list(self, data_list, text_content, processed):
"""处理 list 类型的 JSON 数据"""
tool_calls = [item for item in data_list
if isinstance(item, dict) and item.get('action') == 'use_tool']
if tool_calls:
self.cli._execute_concurrent_tools(tool_calls)
return "break"
continue_items = [item for item in data_list
if isinstance(item, dict) and
(item.get('continue') or item.get('need_continue') or item.get('think_more'))]
if continue_items:
msg = continue_items[0].get('message', '')
if msg:
self.history.append({"role": "assistant", "content": msg})
self.cli.display.response(msg, is_continue=True)
return "continue"
if text_content:
self._add_and_display(text_content)
else:
self._add_and_display(processed)
return "break"
def _add_and_display(self, content):
"""添加到历史并显示"""
self.history.append({"role": "assistant", "content": content})
self.cli.display.response(content)
def _generate_with_animation(self, user_input, tool_prompts=None):
"""生成 AI 响应(带动画)"""
system_prompt = self._build_system_prompt(tool_prompts)
if not self.cli.engine_manager.current_engine:
return "错误: 当前没有可用的AI引擎"
# TUI 模式:直接生成
if self.cli.display.tui_callback:
response = self.cli.engine_manager.generate_response(user_input, system_prompt)
if response:
self.history.append({"role": "assistant", "content": response})
return response
# CLI 模式:带动画
stop_event = threading.Event()
esc_event = threading.Event()
# 加载等待句子
sentences = self._load_love_sentences()
anim_thread = threading.Thread(
target=self.cli.display.thinking_animation,
args=(sentences, stop_event, esc_event), daemon=True)
anim_thread.start()
try:
response = self.cli.engine_manager.generate_response(user_input, system_prompt)
if response:
self.cli.display.typeprint(response, Fore.CYAN)
self.history.append({"role": "assistant", "content": response})
return response
finally:
stop_event.set()
anim_thread.join()
def _build_system_prompt(self, tool_prompts=None):
"""构建系统提示词"""
base = ("你叫小狸,是一个强大的智能编程助手。你具备精准的代码编辑能力、"
"完整的 Git 工作流支持、以及跨文件代码搜索和理解能力。"
"你可以帮助用户编写、修改、调试代码,管理版本控制,理解复杂代码库。")
tool_section = ""
if tool_prompts:
tool_section = f"\n\n{tool_prompts}"
call_format = """
【工具调用格式】
当需要执行操作时,在回复中包含 JSON 指令:
1. 纯 JSON: {"action": "use_tool", "tool": "工具名", "args": "参数"}
2. 文本+JSON: 好的,我来修改这个文件。{"action": "use_tool", "tool": "code_editor", "args": "edit file.py old_code <<<>>> new_code"}
【核心工具】
- code_editor: 精准代码编辑(edit/search/diff/insert/create/write)
- code_search: 代码搜索与理解(find/regex/symbols/imports/structure)
- git_tools: Git 操作(status/diff/log/add/commit/branch)
- cmd_executor: 执行 shell 命令
- file_manager: 文件管理
【编码工作流】
1. 先用 code_search 了解代码结构
2. 用 code_editor.read_range 查看相关代码
3. 用 code_editor.edit 精准修改
4. 用 git_tools 提交变更
【注意事项】
- code_editor.edit 的格式: old_text <<<>>> new_text(用 <<<>>> 分隔)
- args 必须是字符串
- 可以一次返回多个 JSON 指令顺序执行
- 使用 continue 补充回答: {"action": "continue", "content": "补充内容"}"""
return base + tool_section + call_format
def _process_thinking(self, response):
"""处理深度思考标记"""
# <think/> 格式
if '<think/>' in response:
parts = response.split('<think/>', 1)
if len(parts) >= 2:
thinking = parts[0].strip()
reply = parts[1].strip()
if thinking:
print(f"{Fore.LIGHTBLACK_EX}[思考: {thinking[:200]}...]{Style.RESET_ALL}" if len(thinking) > 200 else f"{Fore.LIGHTBLACK_EX}[思考: {thinking}]{Style.RESET_ALL}")
return reply if reply else ""
# <thinking>...</thinking> 格式
engine = self.cli.engine_manager.current_engine
if engine:
start = getattr(engine, 'thinking_start_marker', None)
end = getattr(engine, 'thinking_end_marker', None)
if start and end and start in response and end in response:
parts = response.split(start, 1)
if len(parts) > 1 and end in parts[1]:
thinking, reply = parts[1].split(end, 1)
if thinking.strip():
print(f"{Fore.LIGHTBLACK_EX}[思考: {thinking.strip()[:200]}]{Style.RESET_ALL}")
return reply.strip()
return response
def _process_code_blocks(self, text):
"""处理代码块标记"""
pattern = r'(""".*?""")'
def replace(m):
inner = m.group(1)[3:-3]
return f'{Fore.RED}{inner}{Style.RESET_ALL}'
return re.sub(pattern, replace, text, flags=re.DOTALL)
def _parse_response(self, response):
"""解析混合响应(文本+JSON)"""
response = response.strip()
# 直接 JSON
try:
parsed = json.loads(response)
if isinstance(parsed, (dict, list)):
return "", parsed
except json.JSONDecodeError:
pass
# 代码块中的 JSON
pattern = r"```(?:json)?\s*({.*?})\s*```"
matches = re.findall(pattern, response, re.DOTALL)
if matches:
try:
data = json.loads(matches[-1])
if isinstance(data, (dict, list)):
text = re.sub(pattern, "", response, flags=re.DOTALL).strip()
return text, data
except json.JSONDecodeError:
pass
# 内联 JSON
markers = ['{"action": "use_tool"', '{"action": "continue"',
'{"action":"use_tool"', '{"action":"continue"']
for marker in markers:
idx = response.find(marker)
if idx != -1:
brace_count = 0
for i in range(idx, len(response)):
if response[i] == '{':
brace_count += 1
elif response[i] == '}':
brace_count -= 1
if brace_count == 0:
try:
data = json.loads(response[idx:i+1])
if isinstance(data, dict) and data.get('action') in ['use_tool', 'continue']:
return response[:idx].strip(), data
except json.JSONDecodeError:
pass
break
return response, None
def _get_tool_prompts(self):
"""获取工具提示词"""
if not self.cli.tool_manager or not self.cli.tool_manager.tools:
return "当前没有可用的工具"
lines = ["可用工具列表:\n"]
for tool in self.cli.tool_manager.tools:
name = tool.get('name', '')
desc = tool.get('description', '')
lines.append(f"- {name}: {desc}")
return '\n'.join(lines)
def _load_love_sentences(self):
"""加载等待动画句子"""
import os
path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "love.txt")
if os.path.exists(path):
try:
with open(path, 'r', encoding='utf-8') as f:
return [l.strip() for l in f.readlines() if l.strip()]
except:
pass
return ["AI正在思考中...", "请稍等片刻...", "正在处理您的请求..."]
+131
View File
@@ -0,0 +1,131 @@
"""
显示与输出模块 - 统一 CLI/TUI 输出
"""
import sys
import os
import time
import random
import shutil
import threading
from colorama import Fore, Style
import logging
logger = logging.getLogger(__name__)
class Display:
"""统一显示管理器"""
def __init__(self, user_id="", tui_callback=None):
self.user_id = user_id
self.tui_callback = tui_callback
def output(self, message):
"""统一输出方法"""
if self.tui_callback:
self.tui_callback(message)
else:
print(message)
def response(self, content, is_continue=False):
"""显示 AI 响应"""
user_id_display = f"[用户ID: {self.user_id}]"
lines = content.split('\n')
for i, line in enumerate(lines):
if i == 0:
prefix = f"{Fore.YELLOW}✦{Style.RESET_ALL} " if is_continue else "✦ "
line = prefix + line
if i == len(lines) - 1:
print(f"{line} {user_id_display}")
else:
print(line)
print()
def tool_result(self, tool_name, tool_args, full_result, is_error=False):
"""显示工具执行结果"""
if full_result is None:
full_result = "无结果"
self.output("")
user_id_display = f"[用户ID: {self.user_id}]"
if is_error:
self.output(f"{Fore.RED} X 工具: {tool_name}{Style.RESET_ALL}")
self.output(f"{Fore.RED} 参数: {tool_args} {user_id_display}{Style.RESET_ALL}")
else:
self.output(f"{Fore.GREEN} OK 工具: {tool_name}{Style.RESET_ALL}")
self.output(f"{Fore.GREEN} 参数: {tool_args} {user_id_display}{Style.RESET_ALL}")
display_result = self._limit_lines(full_result, max_lines=2)
color = Fore.RED if is_error else Fore.GREEN
for line in display_result.split('\n'):
self.output(f"{color} {line}{Style.RESET_ALL}")
def error(self, message):
"""显示错误"""
self.output(f"{Fore.RED}{message}{Style.RESET_ALL}")
def success(self, message):
"""显示成功"""
self.output(f"{Fore.GREEN}{message}{Style.RESET_ALL}")
def info(self, message):
"""显示信息"""
self.output(f"{Fore.CYAN}{message}{Style.RESET_ALL}")
def warning(self, message):
"""显示警告"""
self.output(f"{Fore.YELLOW}{message}{Style.RESET_ALL}")
def typeprint(self, text, color=None, delay=0.01):
"""打字机效果输出"""
for char in text:
if color:
print(f"{color}{char}{Style.RESET_ALL}", end="", flush=True)
else:
print(char, end="", flush=True)
time.sleep(delay)
print()
def thinking_animation(self, sentences, stop_event, esc_event):
"""等待动画"""
if not sentences:
sentences = ["AI正在思考中...", "请稍等片刻...", "正在处理您的请求..."]
last_change = time.time()
current_sentence = random.choice(sentences)
chars = "|/-\\"
idx = 0
while not stop_event.is_set():
now = time.time()
if now - last_change >= 5:
current_sentence = random.choice(sentences)
last_change = now
char = chars[idx % len(chars)]
display = current_sentence[:47] + "..." if len(current_sentence) > 50 else current_sentence
print(f"\r{Fore.MAGENTA}{display} {char}{Style.RESET_ALL}", end="", flush=True)
idx += 1
time.sleep(0.1)
print("\r" + " " * 60 + "\r", end="", flush=True)
def _limit_lines(self, text, max_lines=2):
"""限制输出行数"""
lines = text.split('\n')
if len(lines) <= max_lines:
return text
return '\n'.join(lines[:max_lines]) + f"\n... (已截断,共{len(lines)}行)"
def show_banner(self, engine_name, engines, user_id):
"""显示启动横幅"""
self.success(f"小狸 Pro-CLI 已启动!")
self.success(f"用户ID: {user_id}")
print(f"{Fore.GREEN}输入 '/help' 查看帮助信息{Style.RESET_ALL}")
print(f"{Fore.GREEN}输入 '/quit' 退出程序{Style.RESET_ALL}")
print(f"{Fore.GREEN}当前引擎: {engine_name}{Style.RESET_ALL}")
if len(engines) > 1:
print(f"{Fore.GREEN}已加载引擎: {', '.join(engines)}{Style.RESET_ALL}")
print(f"{Fore.GREEN}{'-' * 50}{Style.RESET_ALL}")
+115
View File
@@ -0,0 +1,115 @@
"""
AI 引擎管理模块 - 动态加载和切换 AI 引擎
"""
import os
import importlib.util
import logging
from colorama import Fore, Style
logger = logging.getLogger(__name__)
class EngineManager:
"""AI 引擎管理器"""
def __init__(self):
self.engines = {}
self.current_engine = None
def load_engines(self, engines_dir: str) -> dict:
"""动态加载所有 AI 引擎"""
print(f"{Fore.GREEN}正在加载AI引擎插件...{Style.RESET_ALL}")
if not os.path.exists(engines_dir):
print(f"{Fore.YELLOW}引擎目录不存在: {engines_dir}{Style.RESET_ALL}")
return self.engines
for filename in os.listdir(engines_dir):
if not filename.endswith('.py') or filename == '__init__.py':
continue
engine_name = filename[:-3]
engine_path = os.path.join(engines_dir, filename)
try:
spec = importlib.util.spec_from_file_location(engine_name, engine_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
class_name = self._find_engine_class(module, engine_name)
if class_name and hasattr(module, class_name):
engine_instance = getattr(module, class_name)()
engine_key = getattr(engine_instance, 'name', engine_name)
if engine_key not in self.engines:
self.engines[engine_key] = engine_instance
print(f"{Fore.GREEN}已加载AI引擎: {engine_key} ({class_name}){Style.RESET_ALL}")
else:
print(f"{Fore.YELLOW}AI引擎 {engine_key} 已存在,跳过{Style.RESET_ALL}")
else:
print(f"{Fore.YELLOW}引擎插件 {filename} 中未找到合适的引擎类{Style.RESET_ALL}")
except Exception as e:
logger.error(f"加载引擎失败 {filename}: {e}")
print(f"{Fore.RED}加载引擎失败 {filename}: {e}{Style.RESET_ALL}")
print(f"{Fore.GREEN}总共加载了 {len(self.engines)} 个AI引擎{Style.RESET_ALL}")
return self.engines
def _find_engine_class(self, module, engine_name):
"""查找引擎类"""
possible_names = [
engine_name.capitalize() + "AI",
engine_name.replace('_', '').replace('-', '').capitalize() + "AI",
''.join(w.capitalize() for w in engine_name.replace('-', ' ').replace('_', ' ').split()) + "AI",
''.join(w.capitalize() for w in engine_name.replace('_engine', '').replace('-', ' ').replace('_', ' ').split()) + "AI",
]
for name in possible_names:
if hasattr(module, name):
return name
# 通用查找
for attr_name in dir(module):
attr = getattr(module, attr_name)
if (isinstance(attr, type) and
attr_name.endswith('AI') and
hasattr(attr, 'generate_response')):
return attr_name
return None
def set_default(self, name: str) -> bool:
"""设置默认引擎"""
if name in self.engines:
self.current_engine = self.engines[name]
return True
return False
def switch(self, name: str) -> bool:
"""切换引擎"""
if name not in self.engines:
return False
old_engine = self.current_engine
self.current_engine = self.engines[name]
if hasattr(self.current_engine, 'cli'):
self.current_engine.cli = getattr(old_engine, 'cli', None)
return True
def get_current_name(self) -> str:
"""获取当前引擎名称"""
if self.current_engine:
return getattr(self.current_engine, 'name', '未设置')
return '未设置'
def list_engines(self) -> list:
"""列出所有引擎"""
return list(self.engines.keys())
def generate_response(self, user_input: str, system_prompt: str = None) -> str:
"""使用当前引擎生成响应"""
if not self.current_engine:
return "错误: 没有可用的AI引擎"
return self.current_engine.generate_response(user_input, system_prompt=system_prompt)
+210
View File
@@ -0,0 +1,210 @@
"""
安全代码沙箱 - 在隔离环境中执行用户/插件代码
"""
import io
import ast
import time
import re
import threading
import traceback
from contextlib import redirect_stdout, redirect_stderr
from datetime import datetime
from colorama import Fore, Style
import logging
logger = logging.getLogger(__name__)
class CodeSandbox:
"""安全代码执行沙箱"""
# 危险模块黑名单
DANGEROUS_MODULES = {
'os', 'sys', 'subprocess', 'shutil', 'socket', 'pickle',
'marshal', 'ctypes', 'multiprocessing', 'threading',
'signal', 'resource', 'posix', 'nt', 'builtins',
'importlib', 'code', 'codeop', 'commands', 'popen2',
}
# 危险函数/属性黑名单
DANGEROUS_FUNCTIONS = {
'exec', 'eval', 'compile', 'execfile', 'open',
'input', 'raw_input', 'globals', 'locals', 'vars',
'dir', 'getattr', 'setattr', 'delattr', 'hasattr',
'__import__', '__builtins__', '__class__', '__bases__',
'__subclasses__', '__mro__', '__dict__', '__getattribute__',
}
def __init__(self, timeout=30, whitelist=None):
self.enabled = True
self.timeout = timeout
self.whitelist = whitelist or [
'json', 'math', 'datetime', 'random', 'collections',
'itertools', 'functools', 'operator', 'typing', 're'
]
self.history = []
self.max_history = 100
def is_safe(self, code: str) -> bool:
"""检查代码是否安全(AST 分析 + 字符串快速检查)"""
# 快速字符串检查
dangerous_patterns = [
r'__', r'\beval\b', r'\bexec\b', r'\bcompile\b',
r'\bglobals\b', r'\blocals\b', r'\bgetattr\b',
r'\bsetattr\b', r'\b__import__\b',
]
for pattern in dangerous_patterns:
if re.search(pattern, code, re.IGNORECASE):
return False
# AST 分析
try:
tree = ast.parse(code)
except SyntaxError:
return False
class SafetyVisitor(ast.NodeVisitor):
def __init__(self):
self.is_safe = True
def _check_name(self, name):
if name in CodeSandbox.DANGEROUS_FUNCTIONS:
self.is_safe = False
def visit_Import(self, node):
for alias in node.names:
if alias.name.split('.')[0] in CodeSandbox.DANGEROUS_MODULES:
self.is_safe = False
self.generic_visit(node)
def visit_ImportFrom(self, node):
if node.module:
if node.module.split('.')[0] in CodeSandbox.DANGEROUS_MODULES:
self.is_safe = False
self.generic_visit(node)
def visit_Call(self, node):
if isinstance(node.func, ast.Name):
self._check_name(node.func.id)
elif isinstance(node.func, ast.Attribute):
self._check_name(node.func.attr)
self.generic_visit(node)
def visit_Attribute(self, node):
self._check_name(node.attr)
self.generic_visit(node)
def visit_Name(self, node):
self._check_name(node.id)
self.generic_visit(node)
visitor = SafetyVisitor()
visitor.visit(tree)
return visitor.is_safe
def execute(self, code: str, variables: dict = None) -> dict:
"""
在安全沙箱中执行代码
Returns:
{
'success': bool,
'result': str,
'stdout': str,
'stderr': str,
'execution_time': float,
}
"""
if not self.enabled:
return {'success': False, 'result': '代码执行功能已禁用',
'stdout': '', 'stderr': '', 'execution_time': 0}
if not self.is_safe(code):
return {'success': False, 'result': '代码包含潜在危险操作,已阻止执行',
'stdout': '', 'stderr': '', 'execution_time': 0}
# 准备变量
exec_vars = {}
if variables:
exec_vars.update(variables)
# 添加安全的内置函数
safe_builtins = {
'print': print, 'len': len, 'str': str, 'int': int,
'float': float, 'list': list, 'dict': dict, 'tuple': tuple,
'set': set, 'range': range, 'enumerate': enumerate,
'zip': zip, 'sorted': sorted, 'sum': sum, 'max': max,
'min': min, 'abs': abs, 'round': round, 'bool': bool,
'type': type, 'isinstance': isinstance,
}
exec_vars.update(safe_builtins)
# 添加白名单模块
for module_name in self.whitelist:
try:
exec_vars[module_name] = __import__(module_name)
except ImportError:
pass
# 捕获输出
stdout_capture = io.StringIO()
stderr_capture = io.StringIO()
start_time = time.time()
result = {'success': True, 'error': None, 'traceback': None}
def _exec():
try:
exec(code, exec_vars)
except Exception as e:
result['success'] = False
result['error'] = str(e)
result['traceback'] = traceback.format_exc()
exec_thread = threading.Thread(target=_exec, daemon=True)
exec_thread.start()
exec_thread.join(timeout=self.timeout)
exec_time = time.time() - start_time
if exec_thread.is_alive():
return {
'success': False,
'result': f'代码执行超时(超过 {self.timeout} 秒)',
'stdout': stdout_capture.getvalue(),
'stderr': stderr_capture.getvalue(),
'execution_time': self.timeout,
}
if result['success']:
exec_result = exec_vars.get('result', None)
result_str = str(exec_result) if exec_result is not None else '代码执行成功(无返回值)'
return {
'success': True,
'result': result_str,
'stdout': stdout_capture.getvalue(),
'stderr': stderr_capture.getvalue(),
'execution_time': exec_time,
}
else:
return {
'success': False,
'result': f"代码执行错误: {result['error']}",
'stdout': stdout_capture.getvalue(),
'stderr': stderr_capture.getvalue() + '\n' + (result['traceback'] or ''),
'execution_time': exec_time,
}
def add_to_history(self, record: dict):
"""添加执行记录"""
self.history.append(record)
if len(self.history) > self.max_history:
self.history.pop(0)
def get_history(self, limit=10):
"""获取执行历史"""
return self.history[-limit:]
def clear_history(self):
"""清空执行历史"""
self.history = []
+209
View File
@@ -0,0 +1,209 @@
"""
图像识别引擎模块
支持多种图像识别引擎的切换和管理
"""
import os
import sys
import importlib
import json
from colorama import Fore, Style
# 添加项目根目录到sys.path
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(current_dir)
sys.path.insert(0, project_root)
class BaseImageEngine:
"""图像识别引擎基类"""
name = "base"
description = "基础图像识别引擎"
requires_api_key = False
def __init__(self):
self.config = {}
def set_config(self, config):
"""设置引擎配置"""
self.config = config
def is_available(self):
"""检查引擎是否可用(子类实现)"""
return False
def analyze(self, image_path, prompt="请描述这张图片"):
"""分析图片(子类实现)"""
raise NotImplementedError("子类必须实现 analyze 方法")
def stream_analyze(self, image_path, prompt="请描述这张图片"):
"""流式分析图片(子类实现,可选)"""
# 默认实现:调用 analyze 并一次性返回
result = self.analyze(image_path, prompt)
yield result
def get_model_list(self):
"""获取可用模型列表(子类实现,可选)"""
return []
def set_model(self, model_name):
"""设置当前使用的模型(子类实现,可选)"""
pass
def get_help(self):
"""获取引擎帮助信息"""
return f"{self.name}: {self.description}"
class ImageEngineManager:
"""图像识别引擎管理器"""
def __init__(self):
self.engines = {}
self.current_engine = None
self.current_engine_name = None
self.config = self._load_config()
# 自动加载所有引擎
self._load_engines()
# 设置默认引擎
self._set_default_engine()
def _load_config(self):
"""加载配置文件"""
config_file = os.path.join(project_root, "config.json")
try:
with open(config_file, 'r', encoding='utf-8') as f:
return json.load(f)
except:
return {"image": {"engines": {}, "default_engine": "ollama"}}
def _save_config(self):
"""保存配置文件"""
config_file = os.path.join(project_root, "config.json")
try:
with open(config_file, 'r', encoding='utf-8') as f:
full_config = json.load(f)
except:
full_config = {"api": {"engines": {}}, "system": {}, "image": {}}
# 确保image配置存在
if "image" not in full_config:
full_config["image"] = {}
full_config["image"]["default_engine"] = self.current_engine_name
try:
with open(config_file, 'w', encoding='utf-8') as f:
json.dump(full_config, f, ensure_ascii=False, indent=2)
except Exception as e:
print(f"{Fore.RED}保存配置失败: {e}{Style.RESET_ALL}")
def _load_engines(self):
"""加载所有引擎"""
engine_dir = os.path.dirname(__file__)
for filename in os.listdir(engine_dir):
if filename.endswith('_engine.py') and filename != '__init__.py':
engine_name = filename[:-11] # 移除 _engine.py
try:
# 动态导入模块
module_path = os.path.join(engine_dir, filename)
spec = importlib.util.spec_from_file_location(f"image_engine.{engine_name}", module_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
# 获取引擎类
for attr_name in dir(module):
attr = getattr(module, attr_name)
if (isinstance(attr, type) and
issubclass(attr, BaseImageEngine) and
attr != BaseImageEngine):
engine_instance = attr()
# 加载引擎配置
engine_config = self.config.get("image", {}).get("engines", {}).get(engine_name, {})
engine_instance.set_config(engine_config)
self.engines[engine_name] = engine_instance
print(f"{Fore.GREEN}已加载图像引擎: {engine_name}{Style.RESET_ALL}")
break
except Exception as e:
print(f"{Fore.RED}加载图像引擎 {engine_name} 失败: {e}{Style.RESET_ALL}")
def _set_default_engine(self):
"""设置默认引擎"""
# 从配置读取默认引擎
default = self.config.get("image", {}).get("default_engine", "ollama")
if default in self.engines:
engine = self.engines[default]
if engine.is_available():
self.current_engine = engine
self.current_engine_name = default
print(f"{Fore.GREEN}当前图像引擎: {default}{Style.RESET_ALL}")
return
# 如果配置的默认引擎不可用,找第一个可用的
for name, engine in self.engines.items():
if engine.is_available():
self.current_engine = engine
self.current_engine_name = name
print(f"{Fore.GREEN}当前图像引擎: {name}{Style.RESET_ALL}")
return
print(f"{Fore.YELLOW}警告: 没有可用的图像引擎{Style.RESET_ALL}")
def get_engine(self, name=None):
"""获取指定引擎或当前引擎"""
if name:
return self.engines.get(name)
return self.current_engine
def switch_engine(self, name):
"""切换引擎"""
if name not in self.engines:
available = list(self.engines.keys())
return False, f"引擎 '{name}' 不存在。可用引擎: {', '.join(available)}"
engine = self.engines[name]
if not engine.is_available():
return False, f"引擎 '{name}' 不可用,请检查配置"
self.current_engine = engine
self.current_engine_name = name
self._save_config()
return True, f"已切换到图像引擎: {name}"
def list_engines(self):
"""列出所有引擎"""
result = f"{Fore.CYAN}图像识别引擎列表:{Style.RESET_ALL}\n"
for name, engine in self.engines.items():
status = f"{Fore.GREEN}✓ 可用{Style.RESET_ALL}" if engine.is_available() else f"{Fore.RED}✗ 不可用{Style.RESET_ALL}"
current = f" {Fore.YELLOW}[当前]{Style.RESET_ALL}" if name == self.current_engine_name else ""
result += f" - {name}: {engine.description} {status}{current}\n"
return result
def analyze(self, image_path, prompt="请描述这张图片", stream=True):
"""使用当前引擎分析图片"""
if not self.current_engine:
raise Exception("没有可用的图像引擎")
if stream and hasattr(self.current_engine, 'stream_analyze'):
return self.current_engine.stream_analyze(image_path, prompt)
else:
return self.current_engine.analyze(image_path, prompt)
def get_current_engine_name(self):
"""获取当前引擎名称"""
return self.current_engine_name
# 全局引擎管理器实例
_engine_manager = None
def get_engine_manager():
"""获取全局引擎管理器实例"""
global _engine_manager
if _engine_manager is None:
_engine_manager = ImageEngineManager()
return _engine_manager
+204
View File
@@ -0,0 +1,204 @@
"""
Ollama 视觉引擎
使用本地 Ollama 服务进行图像识别
"""
import os
from colorama import Fore, Style
from . import BaseImageEngine
try:
import ollama
OLLAMA_AVAILABLE = True
except ImportError:
OLLAMA_AVAILABLE = False
class OllamaImageEngine(BaseImageEngine):
"""Ollama 视觉引擎"""
name = "ollama"
description = "Ollama本地视觉模型(支持多种视觉模型)"
requires_api_key = False
# 预设提示词
PROMPTS = {
"analyze": """请详细分析这张图片:
1. 图像类型:照片/截图/插画/图表/文档等
2. 主要内容:描述核心元素和场景
3. 文字内容:如有文字,请完整转录
4. 视觉细节:颜色、构图、风格等
5. 总结:一句话概括
请用中文简洁回答。""",
"ocr": "请识别图片中的所有文字内容,按原文格式输出。如果没有文字,请描述图片内容。",
"quick": "请用中文简要描述这张图片的内容。",
"describe": "请详细描述这张图片的内容,包括所有可见的元素、颜色、文字等。"
}
# 推荐的视觉模型列表
VISION_MODELS = [
'qwen3-vl:4b', 'qwen3-vl:latest',
'llava:7b', 'llava:13b', 'llava:34b',
'llava-llama3:latest',
'minicpm-v:latest',
'deepseek-ocr:latest',
'moondream:latest',
'bakllava:latest',
]
def __init__(self):
super().__init__()
self.model = "qwen3-vl:4b"
self.installed_models = []
self._refresh_models()
def set_config(self, config):
"""设置引擎配置"""
super().set_config(config)
if 'model' in config:
self.model = config['model']
def _refresh_models(self):
"""刷新已安装模型列表"""
if not OLLAMA_AVAILABLE:
self.installed_models = []
return
try:
result = ollama.list()
self.installed_models = [m['model'] for m in result.get('models', [])]
except:
self.installed_models = []
def is_available(self):
"""检查引擎是否可用"""
if not OLLAMA_AVAILABLE:
return False
try:
ollama.list()
return True
except:
return False
def _find_model(self, model_name):
"""查找模型(处理版本标签)"""
for m in self.installed_models:
if m == model_name or m.startswith(model_name + ':') or model_name in m:
return m
return None
def get_model_list(self):
"""获取可用模型列表"""
self._refresh_models()
result = f"{Fore.CYAN}已安装的模型:{Style.RESET_ALL}\n"
for m in self.installed_models:
marker = f"{Fore.GREEN}*{Style.RESET_ALL}" if any(vm.split(':')[0] in m for vm in ['vl', 'llava', 'vision', 'ocr', 'minicpm', 'moondream', 'qwen3-vl']) else " "
result += f" {marker} {m}\n"
result += f"\n{Fore.CYAN}推荐的视觉模型:{Style.RESET_ALL}\n"
for vm in self.VISION_MODELS:
status = f"{Fore.GREEN}[已安装]{Style.RESET_ALL}" if self._find_model(vm) else f"{Fore.YELLOW}[未安装]{Style.RESET_ALL}"
result += f" - {vm} {status}\n"
return result
def set_model(self, model_name):
"""设置当前使用的模型"""
actual = self._find_model(model_name)
if actual:
self.model = actual
return True, f"已切换到模型: {actual}"
# 如果没找到,直接使用用户指定的名称(可能还没安装)
self.model = model_name
return True, f"已设置模型: {model_name} (未安装? 运行 ollama pull {model_name})"
def analyze(self, image_path, prompt="请描述这张图片"):
"""分析图片(非流式)"""
if not OLLAMA_AVAILABLE:
raise Exception("ollama 库未安装,请运行: pip install ollama")
if not os.path.exists(image_path):
raise Exception(f"文件不存在: {image_path}")
# 使用预设提示词或自定义提示词
actual_prompt = self.PROMPTS.get(prompt, prompt)
try:
response = ollama.chat(
model=self.model,
messages=[{
'role': 'user',
'content': actual_prompt,
'images': [image_path]
}]
)
return response.get('message', {}).get('content', '')
except Exception as e:
raise Exception(f"Ollama识别失败: {str(e)}")
def stream_analyze(self, image_path, prompt="请描述这张图片"):
"""流式分析图片"""
if not OLLAMA_AVAILABLE:
raise Exception("ollama 库未安装,请运行: pip install ollama")
if not os.path.exists(image_path):
raise Exception(f"文件不存在: {image_path}")
# 使用预设提示词或自定义提示词
actual_prompt = self.PROMPTS.get(prompt, prompt)
# 检查模型是否已安装
actual_model = self._find_model(self.model)
if not actual_model:
raise Exception(f"模型 {self.model} 未安装。请运行: ollama pull {self.model}")
try:
response = ollama.chat(
model=actual_model,
messages=[{
'role': 'user',
'content': actual_prompt,
'images': [image_path]
}],
stream=False
)
content = response.get('message', {}).get('content', '')
if content:
yield content
else:
raise Exception("模型返回空内容")
except Exception as e:
error_msg = str(e)
if "connection" in error_msg.lower():
raise Exception("无法连接到 Ollama 服务,请确保服务正在运行 (ollama serve)")
raise Exception(f"Ollama识别失败: {error_msg}")
def get_help(self):
"""获取引擎帮助信息"""
return f"""
Ollama 视觉引擎帮助
==================
当前模型: {self.model}
使用说明:
1. 确保 Ollama 服务正在运行
2. 安装视觉模型: ollama pull qwen3-vl:4b
3. 使用 /image model <模型名> 切换模型
支持的操作:
- analyze 详细分析
- ocr 文字识别
- quick 快速描述
- describe 完整描述
模型管理:
- /image models 列出模型
- /image model <名> 切换模型
"""
+1814
View File
File diff suppressed because it is too large. Load diff
+47
View File
@@ -0,0 +1,47 @@
"""
增强日志记录模块
提供结构化日志记录功能
"""
import logging
import os
from datetime import datetime
def setup_logger():
"""设置日志记录器"""
# 创建logs目录
log_dir = os.path.join(os.path.dirname(__file__), 'logs')
if not os.path.exists(log_dir):
os.makedirs(log_dir)
# 创建日志文件名
log_file = os.path.join(log_dir, f"app_{datetime.now().strftime('%Y%m%d')}.log")
# 配置日志记录器
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(log_file, encoding='utf-8'),
logging.StreamHandler() # 同时输出到控制台
]
)
return logging.getLogger(__name__)
# 全局日志记录器
logger = setup_logger()
def log_error(operation, error, user_id=None):
"""记录错误日志"""
user_info = f" [用户ID: {user_id}]" if user_id else ""
logger.error(f"{operation} 失生错误{user_info}: {str(error)}")
def log_info(operation, message, user_id=None):
"""记录信息日志"""
user_info = f" [用户ID: {user_id}]" if user_id else ""
logger.info(f"{operation}{user_info}: {message}")
def log_warning(operation, message, user_id=None):
"""记录警告日志"""
user_info = f" [用户ID: {user_id}]" if user_id else ""
logger.warning(f"{operation}{user_info}: {message}")
+124
View File
@@ -0,0 +1,124 @@
"""
跨平台工具模块
提供跨平台的兼容性支持
"""
import os
import sys
def get_platform():
"""获取当前平台类型"""
if os.name == 'nt':
return 'windows'
elif sys.platform == 'darwin':
return 'macos'
else:
return 'linux'
def get_hidden_input(prompt="请输入密码: "):
"""跨平台的隐藏输入"""
platform = get_platform()
if platform == 'windows':
try:
import msvcrt
print(f"{prompt} ", end='', flush=True)
password = ""
while True:
char = msvcrt.getch()
if char in [b'\r', b'\n']:
print()
break
elif char == b'\x08':
if len(password) > 0:
password = password[:-1]
print('\b \b', end='', flush=True)
else:
password += char.decode('utf-8')
print('*', end='', flush=True)
return password
except ImportError:
import getpass
return getpass.getpass(prompt)
else:
import getpass
return getpass.getpass(prompt)
def check_key_input():
"""检查是否有键盘输入(跨平台)"""
platform = get_platform()
if platform == 'windows':
try:
import msvcrt
return msvcrt.kbhit() > 0
except ImportError:
return False
else:
# Linux/Mac 的实现较为复杂,暂时返回 False
return False
def get_key():
"""获取按键(跨平台)"""
platform = get_platform()
if platform == 'windows':
try:
import msvcrt
if msvcrt.kbhit():
return msvcrt.getch()
return None
except ImportError:
return None
else:
# Linux/Mac 的实现
import select
import tty
import termios
if select.select([sys.stdin], [], [], 0)[0]:
return sys.stdin.read(1)
return None
def supports_esc_detection():
"""检查是否支持 ESC 键检测"""
platform = get_platform()
return platform == 'windows' # 目前只有 Windows 完全支持 ESC 检测
def get_user_directory():
"""获取用户目录(跨平台)"""
platform = get_platform()
if platform == 'windows':
return os.path.expanduser("~")
elif platform == 'macos':
return os.path.expanduser("~")
else: # Linux
return os.path.expanduser("~")
def get_desktop_directory():
"""获取桌面目录(跨平台)"""
platform = get_platform()
if platform == 'windows':
return os.path.join(os.path.expanduser("~"), "Desktop")
elif platform == 'macos':
return os.path.join(os.path.expanduser("~"), "Desktop")
else: # Linux
desktop = os.path.join(os.path.expanduser("~"), "Desktop")
if os.path.exists(desktop):
return desktop
else:
return os.path.expanduser("~")
def get_config_directory():
"""获取应用配置目录(跨平台)"""
platform = get_platform()
if platform == 'windows':
return os.path.join(os.path.getenv('APPDATA', ''), 'Xiaoli Pro-CLI 3')
elif platform == 'macos':
return os.path.join(os.path.expanduser("~"), 'Library', 'Application Support', 'Xiaoli Pro-CLI 3')
else: # Linux
return os.path.join(os.path.expanduser('~'), '.config', 'xiaoli-pro-cli-3')
+174
View File
@@ -0,0 +1,174 @@
import subprocess
import json
from urllib.parse import quote
import requests
class Liugin:
"""AI搜索插件 - 使用jina.ai进行智能搜索"""
def __init__(self):
self.usage = """AI搜索工具使用方法:/r
ai_search <操作> <参数>/r
例如:/r
- ai_search url https://www.example.com - 搜索网址内容/r
- ai_search search Python教程 - 普通搜索/r
- ai_search web 什么是人工智能 - 搜索网络内容/r
/r
详细说明:/r
- url操作: ai_search url <网址> - 提取并搜索指定网址的内容/r
- search操作: ai_search search <搜索词> - 使用必应搜索指定内容/r
- web操作: ai_search web <搜索词> - 使用必应搜索指定内容(与search相同)/r
/r
使用工具的JSON格式示例:/r
{"action": "use_tool", "tool": "ai_search", "args": "url https://www.example.com"} - 搜索网址内容/r
{"action": "use_tool", "tool": "ai_search", "args": "search Python教程"} - 普通搜索/r
{"action": "use_tool", "tool": "ai_search", "args": "web 什么是人工智能"} - 搜索网络内容/r
"""
self.cli = None # CLI实例引用
self.timeout = 30 # 请求超时时间(秒)
def set_cli(self, cli):
"""设置CLI实例引用"""
self.cli = cli
# 注册插件命令
self.cli.register_liugin_command('ai_search', self.command_handler)
self.cli.register_liugin_command('search', self.search_command_handler)
self.cli.register_liugin_command('web', self.web_command_handler)
def command_handler(self, args):
"""处理 /ai_search 命令"""
# 解析参数
parts = args.strip().split(maxsplit=1)
if len(parts) < 1:
return "请提供操作类型: url, search, web"
operation = parts[0].lower()
remaining_args = parts[1] if len(parts) > 1 else ""
# 调用实际的AI搜索功能
return self.handle(f"{operation} {remaining_args}")
def search_command_handler(self, args):
"""处理 /search 命令"""
return self.handle(f"search {args}")
def web_command_handler(self, args):
"""处理 /web 命令"""
return self.handle(f"search {args}")
def get_tool_info(self):
return {
"name": "ai_search",
"description": "AI搜索工具,使用jina.ai服务进行智能搜索,支持网址内容提取和网络搜索。当用户询问天气、新闻、汇率、股票、航班、实时信息、网络内容搜索等问题时,应该直接使用此工具。使用格式:ai_search search <搜索词> 或 ai_search url <网址>",
"keywords": ["搜索", "search", "网址", "url", "web", "AI搜索", "网络搜索", "内容提取", "天气", "新闻", "实时信息"],
"usage": """ai_search 工具使用说明:
JSON格式示例:
{\"action\": \"use_tool\", \"tool\": \"ai_search\", \"args\": \"url https://www.example.com\"} - 搜索网址内容
{\"action\": \"use_tool\", \"tool\": \"ai_search\", \"args\": \"search Python教程\"} - 普通搜索
{\"action\": \"use_tool\", \"tool\": \"ai_search\", \"args\": \"web 什么是人工智能\"} - 搜索网络内容"""
}
def handle(self, args):
"""处理AI搜索请求"""
try:
# 解析参数
parts = args.strip().split(maxsplit=1)
if len(parts) < 1:
return "错误:参数不足。请提供操作类型。\n可用操作: url, search, web"
operation = parts[0].lower()
if operation == "url":
# 搜索网址内容
if len(parts) < 2:
return "错误:请提供URL。格式: url <网址>"
url = parts[1].strip()
return self._search_url(url)
elif operation in ["search", "web"]:
# 普通搜索
if len(parts) < 2:
return "错误:请提供搜索词。格式: search <搜索词>"
query = parts[1].strip()
return self._search_web(query)
else:
return f"错误:不支持的操作 '{operation}'。支持的操作有: url, search, web。"
except Exception as e:
return f"AI搜索操作错误: {str(e)}"
def _search_url(self, url):
"""搜索网址内容(使用jina.ai提取)"""
try:
# 构建jina.ai的URL - 直接使用URL
jina_url = f"https://r.jina.ai/{url}"
result = self._execute_curl(jina_url)
if result.startswith("错误:"):
return result
return f"📄 网址内容 (来自 {url}):\n\n{result}"
except Exception as e:
return f"网址搜索错误: {str(e)}"
def _search_web(self, query):
"""普通搜索(使用jina.ai + 必应搜索)"""
try:
# 对搜索词进行URL编码,确保中文等特殊字符正确编码
# 使用safe=''确保所有非字母数字字符都被编码
encoded_query = quote(query, safe='')
# 构建必应搜索URL
bing_url = f"https://cn.bing.com/search?q={encoded_query}"
# 构建jina.ai的URL - 直接使用必应URL,不需要二次编码
jina_url = f"https://r.jina.ai/{bing_url}"
result = self._execute_curl(jina_url)
if result.startswith("错误:"):
return result
return f"🔍 搜索结果 (搜索词: {query}):\n\n{result}"
except Exception as e:
return f"网络搜索错误: {str(e)}"
def _execute_curl(self, url):
"""使用 requests 库获取内容(替代 curl 命令)"""
try:
# 使用 requests 库发送请求
response = requests.get(
url,
timeout=self.timeout,
headers={'User-Agent': 'Mozilla/5.0 (compatible; AI-Search-Bot)'}
)
# 检查状态码
if response.status_code != 200:
return f"错误:HTTP请求失败,状态码: {response.status_code}"
# 获取内容
content = response.text.strip()
if not content:
return "错误:未获取到任何内容"
# 限制输出长度以避免过长输出
if len(content) > 5000:
content = content[:5000] + f"\n\n... (内容已截断,完整内容共{len(content)}字符)"
return content
except requests.Timeout:
return f"错误:请求超时 (>{self.timeout}秒)"
except requests.ConnectionError:
return "错误:网络连接失败,请检查网络"
except Exception as e:
return f"错误:请求时发生错误: {str(e)}"
+150
View File
@@ -0,0 +1,150 @@
import os
import threading
from colorama import Fore, Style
class Liugin:
"""音频播放插件"""
def __init__(self):
self.usage = """音频播放工具使用方法:
audio_player play - 播放音频文件
例如:
- audio_player play - 播放Ciallo~(∠・ω- )⌒☆-.mp3音频文件
使用工具的JSON格式示例:
{"action": "use_tool", "tool": "audio_player", "args": "play"} - 播放音频文件
直接命令:
- /ciallo - 播放音频文件
"""
self.cli = None # CLI实例引用
self.audio_file = None # 音频文件路径将在set_cli中设置
def set_cli(self, cli):
"""设置CLI实例引用"""
self.cli = cli
# 获取主程序目录路径,通过CLI实例
import os
# 使用CLI实例中的项目目录信息
self.audio_file = os.path.join(os.path.dirname(cli.chat_history_dir), "Ciallo~(∠・ω- )⌒☆-.mp3")
# 注册插件命令
self.cli.register_liugin_command('ciallo', self.play_command_handler)
def get_tool_info(self):
return {
"name": "audio_player",
"description": "音频播放工具,播放指定的音频文件",
"keywords": ["音频", "播放", "audio", "play", "音乐", "声音"],
"usage": """audio_player 工具使用说明:
JSON格式示例:
{"action": "use_tool", "tool": "audio_player", "args": "play"} - 播放音频文件
直接命令:/ciallo - 播放音频文件"""
}
def handle(self, args):
"""处理音频播放请求"""
try:
# 解析参数
parts = args.strip().split()
if len(parts) < 1:
return "错误:请提供操作类型。支持的操作:play"
operation = parts[0].lower()
if operation == "play":
return self._play_audio()
else:
return f"错误:不支持的操作 '{operation}'。支持的操作:play"
except Exception as e:
return f"音频播放错误: {str(e)}"
def play_command_handler(self, args):
"""处理 /ciallo 命令"""
# 在新线程中播放音频,避免阻塞主程序
thread = threading.Thread(target=self._play_audio_internal)
thread.daemon = True
thread.start()
return f"正在播放音频文件: {self.audio_file}"
def _play_audio(self):
"""播放音频文件"""
try:
# 检查音频文件是否存在
if not os.path.exists(self.audio_file):
return f"错误:音频文件 '{self.audio_file}' 不存在。"
# 使用多种方式尝试播放音频
success = self._try_play_audio()
if success:
return f"音频文件 '{self.audio_file}' 播放成功!"
else:
return f"音频播放失败。请确保已安装相应的音频播放库。"
except Exception as e:
return f"播放音频时发生错误: {str(e)}"
def _play_audio_internal(self):
"""在新线程中播放音频"""
try:
# 检查音频文件是否存在
if not os.path.exists(self.audio_file):
print(f"{Fore.RED}错误:音频文件 '{self.audio_file}' 不存在。{Style.RESET_ALL}")
return
# 使用多种方式尝试播放音频
self._try_play_audio()
except Exception as e:
print(f"{Fore.RED}播放音频时发生错误: {str(e)}{Style.RESET_ALL}")
def _try_play_audio(self):
"""尝试多种方式播放音频"""
# 首先尝试使用pygame(如果已安装)
try:
import pygame
pygame.mixer.init()
pygame.mixer.music.load(self.audio_file)
pygame.mixer.music.play()
# 等待音频播放完成
while pygame.mixer.music.get_busy():
pygame.time.Clock().tick(10)
pygame.mixer.quit()
return True
except ImportError:
pass # pygame未安装,尝试其他方式
except Exception:
pass # 其他pygame相关错误,尝试其他方式
# 尝试使用playsound库(如果已安装)
try:
from playsound import playsound
playsound(self.audio_file)
return True
except ImportError:
pass # playsound未安装,尝试其他方式
except Exception:
pass # 其他playsound相关错误,尝试其他方式
# 尝试使用winsound(仅Windows)
try:
import winsound
# 对于MP3文件,先尝试使用高级API
import subprocess
subprocess.run(["start", "", self.audio_file], shell=True)
return True
except Exception:
pass # winsound相关错误,尝试其他方式
# 尝试使用os.startfile(仅Windows)
try:
os.startfile(self.audio_file) # 这会使用默认程序打开文件
return True
except Exception:
pass # os.startfile相关错误
# 如果所有方式都失败,返回False
return False
+188
View File
@@ -0,0 +1,188 @@
import subprocess
import os
import sys
import time
from colorama import Fore, Style
class Liugin:
"""CMD命令执行插件 - 仅限Windows环境"""
def __init__(self):
self.usage = """CMD命令执行工具使用方法:
cmd_executor <操作> <参数>
例如:
- cmd_executor run dir - 执行dir命令查看当前目录内容
- cmd_executor run "ipconfig /all" - 执行ipconfig命令查看网络配置
- cmd_executor run "tasklist | findstr python" - 查找Python相关进程
详细说明:
- run操作: cmd_executor run <命令> - 执行指定的CMD命令
使用工具的JSON格式示例:
{"action": "use_tool", "tool": "cmd_executor", "args": "run dir"} - 执行dir命令
{"action": "use_tool", "tool": "cmd_executor", "args": "run ipconfig /all"} - 执行ipconfig命令
重要说明:
- 此工具仅在Windows环境下可用
- 为安全起见,某些系统命令可能被限制执行
- 命令执行结果会返回给AI进行分析"""
self.cli = None # CLI实例引用
def set_cli(self, cli):
"""设置CLI实例引用"""
self.cli = cli
# 注册插件命令
self.cli.register_liugin_command('cmd', self.command_handler)
def command_handler(self, args):
"""处理 /cmd 命令"""
# 解析参数
parts = args.strip().split(maxsplit=1)
if len(parts) < 1:
return "请提供操作类型: run <命令>"
operation = parts[0].lower()
remaining_args = parts[1] if len(parts) > 1 else ""
# 调用实际的CMD执行功能
return self.handle(f"{operation} {remaining_args}")
def get_tool_info(self):
return {
"name": "cmd_executor",
"description": "CMD命令执行工具,用于执行Windows命令行指令",
"keywords": ["命令", "执行", "CMD", "cmd", "executor", "Windows", "指令", "操作"],
"usage": """cmd_executor 工具使用说明:
JSON格式示例:
{"action": "use_tool", "tool": "cmd_executor", "args": "run dir"} - 执行dir命令
{"action": "use_tool", "tool": "cmd_executor", "args": "run ipconfig /all"} - 执行ipconfig命令
{"action": "use_tool", "tool": "cmd_executor", "args": "run tasklist"} - 列出所有进程
注意:仅限Windows环境使用"""
}
def handle(self, args):
"""处理CMD命令执行请求"""
try:
# 解析参数
parts = args.strip().split(maxsplit=1)
if len(parts) < 1:
return "错误:参数不足。请提供操作类型。\n可用操作: run"
operation = parts[0].lower()
if operation == "run":
# 检查是否为Windows系统
if os.name != 'nt':
return "错误:CMD命令执行工具仅支持Windows系统。"
if len(parts) < 2:
return "错误:请提供要执行的命令。格式: run <命令>"
command = parts[1]
return self._execute_command(command)
else:
return f"错误:不支持的操作 '{operation}'。支持的操作有: run。"
except Exception as e:
return f"CMD命令执行错误: {str(e)}"
def _execute_command(self, command):
"""执行CMD命令"""
try:
# 解析命令和可能的超时参数
# 允许格式如 "command --timeout 10" 或 "command -t 10"
timeout = 30 # 默认超时时间
import re
# 检查是否在命令中指定超时
timeout_pattern = r'(?:--timeout|-t)\s+(\d+)'
timeout_match = re.search(timeout_pattern, command, re.IGNORECASE)
if timeout_match:
timeout = int(timeout_match.group(1))
# 移除超时参数,只保留实际命令
command = re.sub(timeout_pattern, '', command).strip()
# 检查是否有CLI实例可用,请求用户确认
if self.cli is not None:
# 检查是否是 Clawli 远程模式
if not getattr(self.cli, 'is_clawli_mode', False):
# 非远程模式,请求用户确认
try:
from ai_cli import ask_user_confirmation
# 生成唯一的请求ID
import uuid
request_id = str(uuid.uuid4())
# 存储请求信息
self.cli.user_input_queue[request_id] = {
'type': 'cmd_confirm',
'command': command,
'timeout': timeout,
'status': 'pending'
}
# 发送确认请求给用户
confirmed = ask_user_confirmation(
f"即将执行CMD命令: {command}\n超时时间: {timeout}秒\n是否确认执行?",
request_id
)
if not confirmed:
return f"命令执行已取消: {command}"
except Exception as e:
# 如果无法请求确认,直接执行
pass
# 执行命令
result = subprocess.run(
command,
shell=True,
capture_output=True,
text=True,
timeout=timeout, # 使用自定义超时时间
encoding='utf-8', # 指定编码
errors='replace' # 处理编码错误
)
# 获取输出
stdout = result.stdout.strip()
stderr = result.stderr.strip()
return_code = result.returncode
output = ""
if stdout:
output += f"命令输出:\n{stdout}\n"
if stderr:
output += f"错误信息:\n{stderr}\n"
if return_code != 0:
output += f"命令退出码: {return_code}\n"
if not output:
output = f"命令执行完成,无输出。(超时设置: {timeout}秒)"
# 限制输出长度为5000字符
if len(output) > 5000:
output = output[:5000] + f"\n... (输出已截断,超时设置: {timeout}秒)"
return output
except subprocess.TimeoutExpired:
return f"错误:命令执行超时(超过{timeout}秒),已取消执行。"
except ValueError:
return f"错误:超时值无效,已使用默认30秒超时。命令: {command}"
except Exception as e:
return f"命令执行失败: {str(e)}"
# 测试函数
def test_plugin():
"""测试插件功能"""
plugin = Plugin()
print("CMD执行器插件测试:")
print("工具信息:", plugin.get_tool_info())
print("使用说明:", plugin.usage)
if __name__ == "__main__":
test_plugin()
+542
View File
@@ -0,0 +1,542 @@
"""
代码编辑器插件 - 小狸的精准代码编辑能力
对标 Claude Code 的多文件编辑、上下文感知修改
"""
import os
import re
import difflib
from typing import Optional, Tuple, List, Dict
class Plugin:
"""精准代码编辑器 - 支持搜索替换、多文件编辑、diff 预览"""
def __init__(self):
self.usage = """代码编辑器 - 精准修改代码文件
操作类型:
edit <文件路径> <old_text> <new_text> - 精准替换代码片段
multi <文件路径> <JSON批量操作> - 批量修改(JSON数组)
diff <文件路径1> <文件路径2> - 比较两个文件差异
diff_text <old_text> <new_text> - 比较两段文本差异
insert <文件路径> <行号> <内容> - 在指定行插入内容
delete_lines <文件路径> <起始行> <结束行> - 删除指定行范围
read_range <文件路径> <起始行> <结束行> - 读取指定行范围
search <目录> <正则表达式> [文件模式] - 在代码中搜索
ast_info <文件路径> - 获取 Python 文件的 AST 摘要
create <文件路径> <内容> - 创建新文件
write <文件路径> <内容> - 写入/覆盖文件内容
append <文件路径> <内容> - 追加内容到文件
"""
self.cli = None
def set_cli(self, cli):
self.cli = cli
def get_tool_info(self):
return {
"name": "code_editor",
"description": "精准代码编辑器 - 支持搜索替换、多文件批量编辑、代码搜索、diff对比。用于修改代码文件的核心工具。",
"keywords": ["编辑", "修改", "代码", "文件", "edit", "replace", "search", "diff", "code"],
"usage": self.usage
}
def get_mcp_definition(self):
return {
"name": "code_editor",
"description": "精准代码编辑器",
"inputSchema": {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["edit", "multi", "diff", "diff_text", "insert",
"delete_lines", "read_range", "search", "ast_info",
"create", "write", "append"],
"description": "操作类型"
},
"path": {"type": "string", "description": "文件路径"},
"old_text": {"type": "string", "description": "要替换的原文"},
"new_text": {"type": "string", "description": "替换后的新文本"},
"content": {"type": "string", "description": "文件内容"},
"line_number": {"type": "integer", "description": "行号"},
"start_line": {"type": "integer", "description": "起始行"},
"end_line": {"type": "integer", "description": "结束行"},
"pattern": {"type": "string", "description": "搜索正则表达式"},
"file_pattern": {"type": "string", "description": "文件匹配模式"},
"directory": {"type": "string", "description": "搜索目录"},
"operations": {"type": "array", "description": "批量操作列表"}
},
"required": ["operation"]
}
}
def convert_mcp_args(self, arguments):
op = arguments.get("operation", "")
path = arguments.get("path", "")
return f"{op} {path}"
def handle(self, args: str) -> str:
try:
parts = args.strip().split(maxsplit=2)
if not parts:
return "错误:请提供操作类型"
operation = parts[0].lower()
handlers = {
"edit": self._op_edit,
"multi": self._op_multi_edit,
"diff": self._op_diff,
"diff_text": self._op_diff_text,
"insert": self._op_insert,
"delete_lines": self._op_delete_lines,
"read_range": self._op_read_range,
"search": self._op_search,
"ast_info": self._op_ast_info,
"create": self._op_create,
"write": self._op_write,
"append": self._op_append,
}
handler = handlers.get(operation)
if not handler:
return f"错误:不支持的操作 '{operation}'"
return handler(parts[1] if len(parts) > 1 else "",
parts[2] if len(parts) > 2 else "")
except Exception as e:
return f"代码编辑器错误: {str(e)}"
def _normalize_path(self, path: str) -> str:
"""规范化路径"""
path = path.strip().strip('"').strip("'")
path = os.path.expanduser(path)
path = os.path.normpath(path)
return path
def _op_edit(self, path: str, rest: str) -> str:
"""精准替换代码片段 - 核心编辑能力"""
if not path:
return "错误:请提供文件路径"
file_path = self._normalize_path(path)
if not os.path.exists(file_path):
return f"错误:文件不存在: {file_path}"
# 从 rest 中解析 old_text 和 new_text
# 格式: old_text <<<SEPARATOR>>> new_text
separator = " <<<>>> "
if separator not in rest:
return "错误:格式应为: edit <文件> <old_text> <<<>>> <new_text>"
old_text, new_text = rest.split(separator, 1)
if not old_text.strip():
return "错误:old_text 不能为空"
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# 精准匹配(去除首尾空白但保留内部格式)
old_text = old_text.strip()
new_text = new_text.strip()
if old_text not in content:
# 尝试模糊匹配
lines = content.split('\n')
old_lines = old_text.split('\n')
match_info = self._fuzzy_match(lines, old_lines)
if match_info:
return f"错误:未找到精确匹配。最接近的位置在第 {match_info[0]+1}-{match_info[1]} 行,但内容不完全一致。请检查缩进和空白字符。"
return f"错误:在文件中未找到要替换的内容。请确保文本完全匹配(包括缩进)。"
count = content.count(old_text)
if count > 1:
return f"警告:找到 {count} 处匹配,请提供更多上下文以确保唯一匹配。"
new_content = content.replace(old_text, new_text, 1)
with open(file_path, 'w', encoding='utf-8') as f:
f.write(new_content)
# 生成 diff 预览
diff = self._make_diff(old_text, new_text, file_path)
return f"✅ 已编辑: {file_path}\n\n{diff}"
def _op_multi_edit(self, path: str, rest: str) -> str:
"""批量编辑 - 一次修改多处"""
import json
if not path:
return "错误:请提供文件路径"
file_path = self._normalize_path(path)
if not os.path.exists(file_path):
return f"错误:文件不存在: {file_path}"
try:
operations = json.loads(rest)
except json.JSONDecodeError:
return "错误:operations 必须是有效的 JSON 数组。格式: [{\"old\": \"...\", \"new\": \"...\"}]"
if not isinstance(operations, list):
return "错误:operations 必须是数组"
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
results = []
for i, op in enumerate(operations):
old_text = op.get("old", "").strip()
new_text = op.get("new", "").strip()
if not old_text:
results.append(f" ❌ 操作 {i+1}: old 为空")
continue
if old_text not in content:
results.append(f" ❌ 操作 {i+1}: 未找到匹配")
continue
content = content.replace(old_text, new_text, 1)
results.append(f" ✅ 操作 {i+1}: 已替换")
with open(file_path, 'w', encoding='utf-8') as f:
f.write(content)
return f"✅ 批量编辑完成: {file_path}\n" + "\n".join(results)
def _op_diff(self, path: str, rest: str) -> str:
"""比较两个文件差异"""
if not path:
return "错误:请提供第一个文件路径"
file1 = self._normalize_path(path)
file2 = self._normalize_path(rest.split()[0] if rest.strip() else "")
if not os.path.exists(file1):
return f"错误:文件不存在: {file1}"
if not os.path.exists(file2):
return f"错误:文件不存在: {file2}"
with open(file1, 'r', encoding='utf-8') as f:
lines1 = f.readlines()
with open(file2, 'r', encoding='utf-8') as f:
lines2 = f.readlines()
diff = difflib.unified_diff(lines1, lines2,
fromfile=file1, tofile=file2, lineterm='')
diff_text = '\n'.join(diff)
if not diff_text:
return "两个文件完全相同"
return f"文件差异:\n{diff_text}"
def _op_diff_text(self, path: str, rest: str) -> str:
"""比较两段文本差异"""
separator = " <<<>>> "
if separator not in rest:
return "错误:格式: diff_text <old_text> <<<>>> <new_text>"
old_text, new_text = rest.split(separator, 1)
diff = self._make_diff(old_text.strip(), new_text.strip(), "修改")
return diff
def _op_insert(self, path: str, rest: str) -> str:
"""在指定行插入内容"""
if not path:
return "错误:请提供文件路径"
file_path = self._normalize_path(path)
if not os.path.exists(file_path):
return f"错误:文件不存在: {file_path}"
parts = rest.split(maxsplit=1)
if len(parts) < 2:
return "错误:格式: insert <文件> <行号> <内容>"
try:
line_num = int(parts[0])
except ValueError:
return "错误:行号必须是整数"
content_to_insert = parts[1].strip()
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
if line_num < 1 or line_num > len(lines) + 1:
return f"错误:行号超出范围 (1-{len(lines)+1})"
insert_lines = [l + '\n' for l in content_to_insert.split('\n')]
lines[line_num-1:line_num-1] = insert_lines
with open(file_path, 'w', encoding='utf-8') as f:
f.writelines(lines)
return f"✅ 已在第 {line_num} 行插入 {len(insert_lines)} 行内容"
def _op_delete_lines(self, path: str, rest: str) -> str:
"""删除指定行范围"""
if not path:
return "错误:请提供文件路径"
file_path = self._normalize_path(path)
if not os.path.exists(file_path):
return f"错误:文件不存在: {file_path}"
parts = rest.split()
if len(parts) < 2:
return "错误:格式: delete_lines <文件> <起始行> <结束行>"
try:
start = int(parts[0])
end = int(parts[1])
except ValueError:
return "错误:行号必须是整数"
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
if start < 1 or end > len(lines) or start > end:
return f"错误:行号范围无效 (1-{len(lines)})"
deleted = lines[start-1:end]
lines[start-1:end] = []
with open(file_path, 'w', encoding='utf-8') as f:
f.writelines(lines)
return f"✅ 已删除第 {start}-{end} 行 ({len(deleted)} 行)"
def _op_read_range(self, path: str, rest: str) -> str:
"""读取指定行范围"""
if not path:
return "错误:请提供文件路径"
file_path = self._normalize_path(path)
if not os.path.exists(file_path):
return f"错误:文件不存在: {file_path}"
parts = rest.split()
start = int(parts[0]) if len(parts) > 0 else 1
end = int(parts[1]) if len(parts) > 1 else None
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
total = len(lines)
if end is None:
end = total
start = max(1, min(start, total))
end = max(start, min(end, total))
selected = lines[start-1:end]
numbered = [f"{start+i:4d} | {line.rstrip()}" for i, line in enumerate(selected)]
return f"📄 {file_path} (第 {start}-{end} 行,共 {total} 行)\n{'─'*60}\n" + '\n'.join(numbered)
def _op_search(self, path: str, rest: str) -> str:
"""在代码中搜索(类似 grep)"""
if not path:
return "错误:请提供搜索目录"
search_dir = self._normalize_path(path)
if not os.path.isdir(search_dir):
return f"错误:目录不存在: {search_dir}"
parts = rest.split(maxsplit=1)
if not parts:
return "错误:请提供搜索正则表达式"
pattern = parts[0]
file_pattern = parts[1] if len(parts) > 1 else "*.py"
try:
regex = re.compile(pattern)
except re.error as e:
return f"错误:无效的正则表达式: {e}"
# 构建文件匹配模式
if file_pattern.startswith("*."):
ext = file_pattern[1:] # .py
file_filter = lambda f: f.endswith(ext)
elif file_pattern == "*":
file_filter = lambda f: True
else:
file_filter = lambda f: file_pattern in f
results = []
max_results = 50
for root, dirs, files in os.walk(search_dir):
# 跳过隐藏目录和常见不需要搜索的目录
dirs[:] = [d for d in dirs if not d.startswith('.') and d not in
{'node_modules', '__pycache__', '.git', 'venv', '.venv', 'env'}]
for filename in files:
if not file_filter(filename):
continue
filepath = os.path.join(root, filename)
try:
with open(filepath, 'r', encoding='utf-8') as f:
for line_num, line in enumerate(f, 1):
if regex.search(line):
rel_path = os.path.relpath(filepath, search_dir)
results.append(f" {rel_path}:{line_num}: {line.rstrip()}")
if len(results) >= max_results:
break
except (UnicodeDecodeError, PermissionError):
continue
if len(results) >= max_results:
break
if len(results) >= max_results:
break
if not results:
return f"未找到匹配 '{pattern}' 的结果"
header = f"🔍 搜索结果 ({len(results)}{'+' if len(results) >= max_results else ''} 匹配):\n"
return header + '\n'.join(results)
def _op_ast_info(self, path: str, rest: str) -> str:
"""获取 Python 文件的 AST 摘要"""
import ast
if not path:
return "错误:请提供文件路径"
file_path = self._normalize_path(path)
if not os.path.exists(file_path):
return f"错误:文件不存在: {file_path}"
with open(file_path, 'r', encoding='utf-8') as f:
source = f.read()
try:
tree = ast.parse(source)
except SyntaxError as e:
return f"错误:语法错误 - {e}"
info = [f"📄 {file_path} AST 摘要:"]
for node in ast.iter_child_nodes(tree):
if isinstance(node, ast.ClassDef):
methods = [n.name for n in ast.iter_child_nodes(node)
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))]
info.append(f" class {node.name} (行 {node.lineno})")
for m in methods:
info.append(f" └─ {m}()")
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
info.append(f" def {node.name}() (行 {node.lineno})")
elif isinstance(node, ast.Import):
names = [a.name for a in node.names]
info.append(f" import {', '.join(names)}")
elif isinstance(node, ast.ImportFrom):
names = [a.name for a in node.names]
info.append(f" from {node.module} import {', '.join(names)}")
if len(info) == 1:
return f"📄 {file_path}: 文件为空或无顶层定义"
return '\n'.join(info)
def _op_create(self, path: str, rest: str) -> str:
"""创建新文件"""
if not path:
return "错误:请提供文件路径"
file_path = self._normalize_path(path)
if os.path.exists(file_path):
return f"错误:文件已存在: {file_path}"
# 确保目录存在
dir_path = os.path.dirname(file_path)
if dir_path and not os.path.exists(dir_path):
os.makedirs(dir_path, exist_ok=True)
content = rest if rest else ""
with open(file_path, 'w', encoding='utf-8') as f:
f.write(content)
return f"✅ 已创建文件: {file_path} ({len(content)} 字符)"
def _op_write(self, path: str, rest: str) -> str:
"""写入/覆盖文件内容"""
if not path:
return "错误:请提供文件路径"
file_path = self._normalize_path(path)
# 确保目录存在
dir_path = os.path.dirname(file_path)
if dir_path and not os.path.exists(dir_path):
os.makedirs(dir_path, exist_ok=True)
with open(file_path, 'w', encoding='utf-8') as f:
f.write(rest)
return f"✅ 已写入文件: {file_path} ({len(rest)} 字符)"
def _op_append(self, path: str, rest: str) -> str:
"""追加内容到文件"""
if not path:
return "错误:请提供文件路径"
file_path = self._normalize_path(path)
if not os.path.exists(file_path):
return f"错误:文件不存在: {file_path}"
with open(file_path, 'a', encoding='utf-8') as f:
f.write(rest)
return f"✅ 已追加内容到: {file_path} ({len(rest)} 字符)"
def _make_diff(self, old_text: str, new_text: str, label: str) -> str:
"""生成 diff 预览"""
old_lines = old_text.splitlines(keepends=True)
new_lines = new_text.splitlines(keepends=True)
diff = difflib.unified_diff(old_lines, new_lines,
fromfile=f"原始", tofile=f"修改后", lineterm='')
diff_text = '\n'.join(diff)
if not diff_text:
return "内容无变化"
return f"📝 变更预览:\n{diff_text}"
def _fuzzy_match(self, lines: list, old_lines: list) -> Optional[Tuple[int, int]]:
"""模糊匹配,尝试找到最接近的位置"""
if not old_lines:
return None
best_ratio = 0
best_pos = None
for i in range(len(lines) - len(old_lines) + 1):
candidate = lines[i:i+len(old_lines)]
ratio = difflib.SequenceMatcher(None,
''.join(candidate), ''.join(old_lines)).ratio()
if ratio > best_ratio:
best_ratio = ratio
best_pos = (i, i + len(old_lines) - 1)
if best_ratio > 0.6:
return best_pos
return None
+472
View File
@@ -0,0 +1,472 @@
"""
代码搜索与理解插件 - 代码库级别的智能搜索
对标 Claude Code 的代码理解能力
"""
import os
import re
import ast
from typing import List, Dict, Optional, Tuple
class Plugin:
"""代码搜索与理解工具 - 跨文件搜索、依赖分析、代码结构理解"""
def __init__(self):
self.usage = """代码搜索与理解工具
操作类型:
find <目录> <关键词> [文件模式] - 搜索代码中的关键词
regex <目录> <正则表达式> [文件模式] - 正则表达式搜索
symbols <目录> [文件模式] - 提取所有符号(类、函数、变量)
imports <文件路径> - 分析文件的导入依赖
callers <目录> <函数名> - 查找调用指定函数的位置
todo <目录> [文件模式] - 搜索 TODO/FIXME/HACK 注释
structure <目录> - 显示项目目录结构
context <文件路径> [行号] [范围] - 获取某行的上下文
stats <目录> - 代码统计(行数、文件数等)
"""
self.cli = None
def set_cli(self, cli):
self.cli = cli
def get_tool_info(self):
return {
"name": "code_search",
"description": "代码搜索与理解工具 - 跨文件搜索、符号提取、依赖分析、项目结构查看",
"keywords": ["搜索", "查找", "grep", "search", "find", "代码", "符号", "结构"],
"usage": self.usage
}
def get_mcp_definition(self):
return {
"name": "code_search",
"description": "代码搜索与理解工具",
"inputSchema": {
"type": "object",
"properties": {
"operation": {"type": "string"},
"directory": {"type": "string"},
"pattern": {"type": "string"},
"file_pattern": {"type": "string"},
"file_path": {"type": "string"},
"line_number": {"type": "integer"},
"range": {"type": "integer"}
},
"required": ["operation"]
}
}
def convert_mcp_args(self, arguments):
op = arguments.get("operation", "")
directory = arguments.get("directory", "")
pattern = arguments.get("pattern", "")
return f"{op} {directory} {pattern}".strip()
def handle(self, args: str) -> str:
try:
parts = args.strip().split(maxsplit=2)
if not parts:
return "错误:请提供操作类型"
operation = parts[0].lower()
arg1 = parts[1] if len(parts) > 1 else ""
arg2 = parts[2] if len(parts) > 2 else ""
handlers = {
"find": self._op_find,
"regex": self._op_regex,
"symbols": self._op_symbols,
"imports": self._op_imports,
"callers": self._op_callers,
"todo": self._op_todo,
"structure": self._op_structure,
"context": self._op_context,
"stats": self._op_stats,
}
handler = handlers.get(operation)
if not handler:
return f"错误:不支持的操作 '{operation}'"
return handler(arg1, arg2)
except Exception as e:
return f"代码搜索错误: {str(e)}"
def _normalize_path(self, path: str) -> str:
path = path.strip().strip('"').strip("'")
path = os.path.expanduser(path)
return os.path.normpath(path)
def _walk_files(self, directory: str, file_pattern: str = "*.py",
max_files: int = 200) -> List[str]:
"""遍历目录中的文件"""
if file_pattern.startswith("*."):
ext = file_pattern[1:]
file_filter = lambda f: f.endswith(ext)
elif file_pattern == "*":
file_filter = lambda f: True
else:
file_filter = lambda f: file_pattern in f
files = []
skip_dirs = {'.git', 'node_modules', '__pycache__', '.venv', 'venv',
'env', '.env', '.tox', '.mypy_cache', '.pytest_cache'}
for root, dirs, filenames in os.walk(directory):
dirs[:] = [d for d in dirs if not d.startswith('.') and d not in skip_dirs]
for f in filenames:
if file_filter(f):
files.append(os.path.join(root, f))
if len(files) >= max_files:
return files
return files
def _op_find(self, directory: str, rest: str) -> str:
"""关键词搜索"""
if not directory:
return "错误:请提供搜索目录"
search_dir = self._normalize_path(directory)
parts = rest.split(maxsplit=1)
keyword = parts[0] if parts else ""
file_pattern = parts[1] if len(parts) > 1 else "*.py"
if not keyword:
return "错误:请提供搜索关键词"
if not os.path.isdir(search_dir):
return f"错误:目录不存在: {search_dir}"
files = self._walk_files(search_dir, file_pattern)
results = []
max_results = 80
for filepath in files:
try:
with open(filepath, 'r', encoding='utf-8') as f:
for line_num, line in enumerate(f, 1):
if keyword.lower() in line.lower():
rel_path = os.path.relpath(filepath, search_dir)
results.append(f" {rel_path}:{line_num}: {line.rstrip()}")
if len(results) >= max_results:
break
except (UnicodeDecodeError, PermissionError):
continue
if len(results) >= max_results:
break
if not results:
return f"未找到 '{keyword}' 的匹配"
return f"🔍 搜索 '{keyword}' ({len(results)}{'+' if len(results) >= max_results else ''} 匹配):\n" + '\n'.join(results)
def _op_regex(self, directory: str, rest: str) -> str:
"""正则表达式搜索"""
if not directory:
return "错误:请提供搜索目录"
search_dir = self._normalize_path(directory)
parts = rest.split(maxsplit=1)
pattern = parts[0] if parts else ""
file_pattern = parts[1] if len(parts) > 1 else "*.py"
if not pattern:
return "错误:请提供正则表达式"
try:
regex = re.compile(pattern)
except re.error as e:
return f"错误:无效的正则表达式: {e}"
files = self._walk_files(search_dir, file_pattern)
results = []
max_results = 50
for filepath in files:
try:
with open(filepath, 'r', encoding='utf-8') as f:
for line_num, line in enumerate(f, 1):
if regex.search(line):
rel_path = os.path.relpath(filepath, search_dir)
results.append(f" {rel_path}:{line_num}: {line.rstrip()}")
if len(results) >= max_results:
break
except (UnicodeDecodeError, PermissionError):
continue
if len(results) >= max_results:
break
if not results:
return f"未找到匹配 '{pattern}' 的结果"
return f"🔍 正则搜索 ({len(results)}{'+' if len(results) >= max_results else ''} 匹配):\n" + '\n'.join(results)
def _op_symbols(self, directory: str, rest: str) -> str:
"""提取目录中所有 Python 符号"""
if not directory:
return "错误:请提供目录路径"
search_dir = self._normalize_path(directory)
file_pattern = rest.strip() or "*.py"
files = self._walk_files(search_dir, file_pattern)
all_symbols = []
for filepath in files:
if not filepath.endswith('.py'):
continue
try:
with open(filepath, 'r', encoding='utf-8') as f:
source = f.read()
tree = ast.parse(source)
rel_path = os.path.relpath(filepath, search_dir)
symbols = []
for node in ast.iter_child_nodes(tree):
if isinstance(node, ast.ClassDef):
methods = [n.name for n in ast.iter_child_nodes(node)
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))]
symbols.append(f" class {node.name} (行 {node.lineno})")
for m in methods[:10]: # 限制每个类最多显示10个方法
symbols.append(f" └─ {m}()")
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
symbols.append(f" def {node.name}() (行 {node.lineno})")
if symbols:
all_symbols.append(f"\n📄 {rel_path}:")
all_symbols.extend(symbols)
except (SyntaxError, UnicodeDecodeError):
continue
if not all_symbols:
return "未找到 Python 符号"
return f"📋 代码符号:\n" + '\n'.join(all_symbols)
def _op_imports(self, file_path: str, rest: str) -> str:
"""分析文件的导入依赖"""
if not file_path:
return "错误:请提供文件路径"
path = self._normalize_path(file_path)
if not os.path.exists(path):
return f"错误:文件不存在: {path}"
with open(path, 'r', encoding='utf-8') as f:
source = f.read()
try:
tree = ast.parse(source)
except SyntaxError as e:
return f"错误:语法错误 - {e}"
imports = []
for node in ast.iter_child_nodes(tree):
if isinstance(node, ast.Import):
for alias in node.names:
imports.append(f" import {alias.name} (行 {node.lineno})")
elif isinstance(node, ast.ImportFrom):
names = [a.name for a in node.names]
imports.append(f" from {node.module} import {', '.join(names)} (行 {node.lineno})")
if not imports:
return f"📄 {file_path}: 没有导入语句"
return f"📦 {file_path} 导入依赖:\n" + '\n'.join(imports)
def _op_callers(self, directory: str, rest: str) -> str:
"""查找调用指定函数的位置"""
if not directory:
return "错误:请提供搜索目录"
search_dir = self._normalize_path(directory)
parts = rest.split(maxsplit=1)
func_name = parts[0] if parts else ""
file_pattern = parts[1] if len(parts) > 1 else "*.py"
if not func_name:
return "错误:请提供函数名"
# 搜索函数调用模式
patterns = [
re.compile(rf'\b{re.escape(func_name)}\s*\('),
re.compile(rf'\.{re.escape(func_name)}\s*\('),
]
files = self._walk_files(search_dir, file_pattern)
results = []
for filepath in files:
try:
with open(filepath, 'r', encoding='utf-8') as f:
for line_num, line in enumerate(f, 1):
for pat in patterns:
if pat.search(line):
rel_path = os.path.relpath(filepath, search_dir)
results.append(f" {rel_path}:{line_num}: {line.rstrip()}")
break
except (UnicodeDecodeError, PermissionError):
continue
if not results:
return f"未找到调用 '{func_name}' 的位置"
return f"🔎 '{func_name}' 的调用位置 ({len(results)}):\n" + '\n'.join(results)
def _op_todo(self, directory: str, rest: str) -> str:
"""搜索 TODO/FIXME/HACK 注释"""
if not directory:
return "错误:请提供目录路径"
search_dir = self._normalize_path(directory)
file_pattern = rest.strip() or "*.py"
pattern = re.compile(r'#\s*(TODO|FIXME|HACK|XXX|BUG|NOTE)\b[:\s]*(.*)', re.IGNORECASE)
files = self._walk_files(search_dir, file_pattern)
results = []
for filepath in files:
try:
with open(filepath, 'r', encoding='utf-8') as f:
for line_num, line in enumerate(f, 1):
match = pattern.search(line)
if match:
rel_path = os.path.relpath(filepath, search_dir)
tag = match.group(1).upper()
desc = match.group(2).strip()
results.append(f" {rel_path}:{line_num} [{tag}] {desc}")
except (UnicodeDecodeError, PermissionError):
continue
if not results:
return "没有找到 TODO/FIXME 注释"
return f"📝 TODO/FIXME ({len(results)}):\n" + '\n'.join(results)
def _op_structure(self, directory: str, rest: str) -> str:
"""显示项目目录结构"""
if not directory:
return "错误:请提供目录路径"
search_dir = self._normalize_path(directory)
if not os.path.isdir(search_dir):
return f"错误:目录不存在: {search_dir}"
max_depth = int(rest) if rest.strip().isdigit() else 3
lines = [f"📁 {os.path.basename(search_dir)}/"]
skip_dirs = {'.git', 'node_modules', '__pycache__', '.venv', 'venv',
'env', '.tox', '.mypy_cache'}
def _tree(path, prefix="", depth=0):
if depth >= max_depth:
return
try:
entries = sorted(os.listdir(path))
except PermissionError:
return
dirs = [e for e in entries if os.path.isdir(os.path.join(path, e))
and e not in skip_dirs and not e.startswith('.')]
files = [e for e in entries if os.path.isfile(os.path.join(path, e))]
# 限制每个目录显示的文件数
if len(files) > 20:
files = files[:20] + [f"... ({len(files)-20} more)"]
for i, d in enumerate(dirs):
is_last_dir = (i == len(dirs) - 1) and not files
connector = "└── " if is_last_dir else "├── "
lines.append(f"{prefix}{connector}{d}/")
extension = " " if is_last_dir else "│ "
_tree(os.path.join(path, d), prefix + extension, depth + 1)
for i, f in enumerate(files):
is_last = i == len(files) - 1
connector = "└── " if is_last else "├── "
# 文件大小
fpath = os.path.join(path, f)
if os.path.isfile(fpath):
size = os.path.getsize(fpath)
if size > 1024 * 1024:
size_str = f" ({size // (1024*1024)}MB)"
elif size > 1024:
size_str = f" ({size // 1024}KB)"
else:
size_str = f" ({size}B)"
else:
size_str = ""
lines.append(f"{prefix}{connector}{f}{size_str}")
_tree(search_dir)
return '\n'.join(lines)
def _op_context(self, file_path: str, rest: str) -> str:
"""获取某行的上下文"""
if not file_path:
return "错误:请提供文件路径"
path = self._normalize_path(file_path)
if not os.path.exists(path):
return f"错误:文件不存在: {path}"
parts = rest.split()
line_num = int(parts[0]) if parts else 1
context_range = int(parts[1]) if len(parts) > 1 else 5
with open(path, 'r', encoding='utf-8') as f:
lines = f.readlines()
total = len(lines)
start = max(1, line_num - context_range)
end = min(total, line_num + context_range)
result = [f"📄 {path} 第 {start}-{end} 行 (目标: 第 {line_num} 行):"]
result.append("─" * 60)
for i in range(start - 1, end):
marker = ">>>" if i + 1 == line_num else " "
result.append(f"{marker} {i+1:4d} | {lines[i].rstrip()}")
return '\n'.join(result)
def _op_stats(self, directory: str, rest: str) -> str:
"""代码统计"""
if not directory:
return "错误:请提供目录路径"
search_dir = self._normalize_path(directory)
files = self._walk_files(search_dir, "*.py")
total_lines = 0
total_blank = 0
total_comment = 0
total_code = 0
file_count = len(files)
for filepath in files:
try:
with open(filepath, 'r', encoding='utf-8') as f:
for line in f:
total_lines += 1
stripped = line.strip()
if not stripped:
total_blank += 1
elif stripped.startswith('#'):
total_comment += 1
else:
total_code += 1
except (UnicodeDecodeError, PermissionError):
continue
return f"""📊 代码统计 ({search_dir}):
文件数: {file_count}
总行数: {total_lines}
代码行: {total_code}
注释行: {total_comment}
空行: {total_blank}
注释率: {total_comment/max(total_code,1)*100:.1f}%"""
+793
View File
@@ -0,0 +1,793 @@
"""
文件管理器插件 - 小狸自有协议版本
支持文件和目录的创建、读取、写入、删除、移动、复制、搜索等操作
"""
import os
import json
import shutil
from datetime import datetime
from pathlib import Path
from typing import Optional, Dict, Any, List
class Liugin:
"""文件管理器插件"""
def __init__(self):
self.usage = """文件管理器使用方法:
file_manager <操作> <参数>
操作类型:
- list <路径> [-p "模式"] - 列出目录内容(只列当前目录)
- read <文件路径> [-n 行数] - 读取文件内容
- write <文件路径> -c "内容" - 写入文件
- append <文件路径> -c "内容" - 追加内容到文件
- copy <源路径> <目标路径> - 复制文件或目录
- move <源路径> <目标路径> - 移动或重命名
- delete <路径> [-r] - 删除文件或目录
- search <目录> [-p "模式"] [-c "内容"] - 搜索文件(递归)
- info <路径> - 获取文件信息
- mkdir <路径> - 创建目录
- modify <文件路径> <行号> "新内容" - 修改指定行
- insert <文件路径> <行号> "内容" - 在指定行插入内容
- delete-line <文件路径> <行号> - 删除指定行
- replace <文件路径> "旧文本" "新文本" [--start 行号] [--end 行号] - 替换文本
示例:
- file_manager list . - 列出当前目录
- file_manager list . -p "*.py" - 列出所有Python文件
- file_manager read config.json - 读取文件
- file_manager write test.txt -c "Hello World" - 写入文件
- file_manager copy file1.txt file2.txt - 复制文件
- file_manager move old.txt new.txt - 重命名文件
- file_manager delete temp -r - 递归删除目录
- file_manager search . -c "TODO" - 搜索包含TODO的文件
JSON调用格式:
{"action": "use_tool", "tool": "file_manager", "args": "list ."}
{"action": "use_tool", "tool": "file_manager", "args": "read config.json -n 50"}
{"action": "use_tool", "tool": "file_manager", "args": "write test.txt -c Hello World"}
"""
self.cli = None
def set_cli(self, cli):
"""设置CLI实例引用"""
self.cli = cli
def get_tool_info(self):
return {
"name": "file_manager",
"description": "文件和目录管理工具,支持创建、读取、写入、删除、移动、复制、搜索文件等操作",
"keywords": ["文件", "目录", "管理", "file", "manager", "读写", "复制", "删除", "搜索", "mkdir"],
"usage": self.usage
}
def get_mcp_definition(self) -> Dict[str, Any]:
"""返回符合 MCP 标准的工具定义"""
return {
"name": "file_manager",
"description": "文件和目录管理工具,支持创建、读取、写入、删除、移动、复制、搜索文件等操作。操作类型: list(列出目录), read(读取文件), write(写入文件), append(追加内容), copy(复制), move(移动/重命名), delete(删除), search(搜索), info(文件信息), mkdir(创建目录), modify(修改行), insert(插入行), delete-line(删除行), replace(替换文本)",
"inputSchema": {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["list", "read", "write", "append", "copy", "move", "delete", "search", "info", "mkdir", "modify", "insert", "delete-line", "replace"],
"description": "操作类型"
},
"path": {
"type": "string",
"description": "文件或目录路径"
},
"content": {
"type": "string",
"description": "文件内容(用于 write/append 操作)"
},
"destination": {
"type": "string",
"description": "目标路径(用于 copy/move 操作)"
},
"pattern": {
"type": "string",
"description": "文件匹配模式(用于 list/search 操作)"
},
"line_number": {
"type": "integer",
"description": "行号(用于 modify/insert/delete-line 操作)"
},
"old_text": {
"type": "string",
"description": "旧文本(用于 replace 操作)"
},
"new_text": {
"type": "string",
"description": "新文本(用于 replace/modify 操作)"
},
"recursive": {
"type": "boolean",
"description": "是否递归(用于 delete/search 操作)",
"default": False
}
},
"required": ["operation"]
}
}
def convert_mcp_args(self, arguments: Dict[str, Any]) -> str:
"""将 MCP 参数转换为命令行格式"""
op = arguments.get("operation", "")
parts = [op]
if "path" in arguments:
parts.append(arguments["path"])
if "destination" in arguments:
parts.append(arguments["destination"])
if "content" in arguments:
parts.extend(["-c", f'"{arguments["content"]}"'])
if "pattern" in arguments:
parts.extend(["-p", f'"{arguments["pattern"]}"'])
if "line_number" in arguments:
parts.append(str(arguments["line_number"]))
if "old_text" in arguments:
parts.append(f'"{arguments["old_text"]}"')
if "new_text" in arguments:
parts.append(f'"{arguments["new_text"]}"')
if arguments.get("recursive"):
parts.append("-r")
return " ".join(parts)
def handle(self, args: str) -> str:
"""处理文件管理请求"""
try:
# 解析参数
parts = self._parse_args(args)
if not parts:
return "错误:请提供操作类型。\n可用操作: list, read, write, append, copy, move, delete, search, info, mkdir, modify, insert, delete-line, replace"
operation = parts[0].lower()
handlers = {
"list": self._op_list,
"read": self._op_read,
"write": self._op_write,
"append": self._op_append,
"copy": self._op_copy,
"move": self._op_move,
"delete": self._op_delete,
"search": self._op_search,
"info": self._op_info,
"mkdir": self._op_mkdir,
"modify": self._op_modify_line,
"insert": self._op_insert_line,
"delete-line": self._op_delete_line,
"replace": self._op_replace_text,
}
handler = handlers.get(operation)
if not handler:
return f"错误:不支持的操作 '{operation}'。\n支持的操作: {', '.join(handlers.keys())}"
return handler(parts[1:])
except Exception as e:
return f"文件操作错误: {str(e)}"
def _parse_args(self, args: str) -> List[str]:
"""解析命令行参数,支持引号"""
parts = []
current = ""
in_quotes = False
quote_char = None
for char in args:
if char in ('"', "'") and not in_quotes:
in_quotes = True
quote_char = char
elif char == quote_char and in_quotes:
in_quotes = False
quote_char = None
elif char == ' ' and not in_quotes:
if current:
parts.append(current)
current = ""
else:
current += char
if current:
parts.append(current)
return parts
def _op_list(self, args: List[str]) -> str:
"""列出目录内容 - 默认只列出当前目录,不递归"""
path = "."
pattern = None
i = 0
while i < len(args):
if args[i] == "-p" and i + 1 < len(args):
pattern = args[i + 1]
i += 1
elif not args[i].startswith("-"):
path = args[i]
i += 1
path = os.path.abspath(path)
if not os.path.exists(path):
return f"错误:路径不存在: {path}"
if not os.path.isdir(path):
return f"错误:不是目录: {path}"
items = []
try:
for item in os.listdir(path):
full_path = os.path.join(path, item)
if pattern and not Path(item).match(pattern):
continue
if os.path.isdir(full_path):
items.append(('dir', item, ''))
else:
size = os.path.getsize(full_path)
if size < 1024:
size_str = f"{size}B"
elif size < 1024 * 1024:
size_str = f"{size/1024:.1f}KB"
else:
size_str = f"{size/1024/1024:.1f}MB"
items.append(('file', item, size_str))
# 排序:文件夹优先
items.sort(key=lambda x: (0 if x[0] == 'dir' else 1, x[1].lower()))
result = [f"📁 {path}"]
result.append("─" * 50)
dir_count = sum(1 for x in items if x[0] == 'dir')
file_count = len(items) - dir_count
for item_type, name, size in items[:100]:
if item_type == 'dir':
result.append(f" 📁 {name}/")
else:
result.append(f" 📄 {name} ({size})")
if len(items) > 100:
result.append(f" ... 还有 {len(items) - 100} 项")
result.append("─" * 50)
result.append(f"共 {dir_count} 个文件夹, {file_count} 个文件")
return '\n'.join(result)
except Exception as e:
return f"列出目录失败: {e}"
def _op_read(self, args: List[str]) -> str:
"""读取文件内容"""
if not args:
return "错误:请提供文件路径"
file_path = args[0]
lines = None
i = 1
while i < len(args):
if args[i] == "-n" and i + 1 < len(args):
try:
lines = int(args[i + 1])
except ValueError:
return "错误:行数必须是整数"
i += 1
i += 1
file_path = os.path.abspath(file_path)
if not os.path.exists(file_path):
return f"错误:文件不存在: {file_path}"
if not os.path.isfile(file_path):
return f"错误:不是文件: {file_path}"
file_size = os.path.getsize(file_path)
if file_size > 10 * 1024 * 1024: # 10MB
return f"警告:文件过大 ({file_size / 1024 / 1024:.1f}MB),请使用 -n 参数限制行数"
try:
with open(file_path, 'r', encoding='utf-8') as f:
if lines:
content_lines = []
line_count = 0
for i, line in enumerate(f):
line_count = i + 1
if i < lines:
content_lines.append(f"{i+1:4d} | {line.rstrip()}")
content = '\n'.join(content_lines)
total_lines = line_count
else:
all_lines = f.readlines()
content = '\n'.join(f"{i+1:4d} | {line.rstrip()}" for i, line in enumerate(all_lines))
total_lines = len(all_lines)
result = [f"文件: {file_path}"]
result.append(f"大小: {file_size} 字节")
result.append(f"行数: {total_lines}")
if lines:
result.append(f"显示: 前 {min(lines, total_lines)} 行")
result.append(f"\n{'─' * 50}")
result.append(content)
return '\n'.join(result)
except UnicodeDecodeError:
return f"错误:无法解码文件(可能是二进制文件或编码不正确)"
except Exception as e:
return f"读取文件失败: {e}"
def _op_write(self, args: List[str]) -> str:
"""写入文件"""
if len(args) < 2:
return "错误:请提供文件路径和内容"
file_path = args[0]
content = None
i = 1
while i < len(args):
if args[i] == "-c" and i + 1 < len(args):
content = args[i + 1]
i += 1
i += 1
if content is None:
return "错误:请使用 -c 参数提供内容"
file_path = os.path.abspath(file_path)
try:
# 创建父目录
parent = os.path.dirname(file_path)
if parent and not os.path.exists(parent):
os.makedirs(parent, exist_ok=True)
with open(file_path, 'w', encoding='utf-8') as f:
f.write(content)
return f"✅ 文件已写入: {file_path}\n大小: {len(content.encode('utf-8'))} 字节"
except Exception as e:
return f"写入文件失败: {e}"
def _op_append(self, args: List[str]) -> str:
"""追加内容到文件"""
if len(args) < 2:
return "错误:请提供文件路径和内容"
file_path = args[0]
content = None
i = 1
while i < len(args):
if args[i] == "-c" and i + 1 < len(args):
content = args[i + 1]
i += 1
i += 1
if content is None:
return "错误:请使用 -c 参数提供内容"
file_path = os.path.abspath(file_path)
try:
with open(file_path, 'a', encoding='utf-8') as f:
f.write(content)
return f"✅ 内容已追加: {file_path}"
except Exception as e:
return f"追加内容失败: {e}"
def _op_copy(self, args: List[str]) -> str:
"""复制文件或目录"""
if len(args) < 2:
return "错误:请提供源路径和目标路径"
source = os.path.abspath(args[0])
destination = os.path.abspath(args[1])
if not os.path.exists(source):
return f"错误:源路径不存在: {source}"
try:
parent = os.path.dirname(destination)
if parent and not os.path.exists(parent):
os.makedirs(parent, exist_ok=True)
if os.path.isfile(source):
shutil.copy2(source, destination)
item_type = "文件"
else:
shutil.copytree(source, destination, dirs_exist_ok=True)
item_type = "目录"
return f"✅ {item_type}已复制:\n 源: {source}\n 目标: {destination}"
except Exception as e:
return f"复制失败: {e}"
def _op_move(self, args: List[str]) -> str:
"""移动或重命名文件或目录"""
if len(args) < 2:
return "错误:请提供源路径和目标路径"
source = os.path.abspath(args[0])
destination = os.path.abspath(args[1])
if not os.path.exists(source):
return f"错误:源路径不存在: {source}"
try:
parent = os.path.dirname(destination)
if parent and not os.path.exists(parent):
os.makedirs(parent, exist_ok=True)
shutil.move(source, destination)
return f"✅ 已移动/重命名:\n 源: {source}\n 目标: {destination}"
except Exception as e:
return f"移动失败: {e}"
def _op_delete(self, args: List[str]) -> str:
"""删除文件或目录"""
if not args:
return "错误:请提供路径"
path = os.path.abspath(args[0])
recursive = "-r" in args
if not os.path.exists(path):
return f"错误:路径不存在: {path}"
try:
if os.path.isfile(path):
os.remove(path)
item_type = "文件"
elif os.path.isdir(path):
if recursive:
shutil.rmtree(path)
item_type = "目录"
else:
# 检查是否为空
if os.listdir(path):
return f"错误:目录不为空。使用 -r 参数递归删除。"
os.rmdir(path)
item_type = "空目录"
return f"✅ 已删除 {item_type}: {path}"
except Exception as e:
return f"删除失败: {e}"
def _op_search(self, args: List[str]) -> str:
"""搜索文件"""
if not args:
return "错误:请提供搜索目录"
directory = args[0]
pattern = None
content_search = None
i = 1
while i < len(args):
if args[i] == "-p" and i + 1 < len(args):
pattern = args[i + 1]
i += 1
elif args[i] == "-c" and i + 1 < len(args):
content_search = args[i + 1]
i += 1
i += 1
directory = os.path.abspath(directory)
if not os.path.exists(directory):
return f"错误:目录不存在: {directory}"
results = []
try:
for root, dirs, files in os.walk(directory):
for filename in files:
file_path = os.path.join(root, filename)
rel_path = os.path.relpath(file_path, directory)
# 模式匹配
if pattern and not Path(filename).match(pattern):
continue
# 内容搜索
if content_search:
try:
with open(file_path, 'r', encoding='utf-8') as f:
file_content = f.read()
if content_search not in file_content:
continue
# 找到匹配行
for i, line in enumerate(file_content.split('\n'), 1):
if content_search in line:
results.append(f"{rel_path}:{i}: {line.strip()[:80]}")
if len(results) >= 50:
break
except (UnicodeDecodeError, IOError):
continue
else:
results.append(rel_path)
if len(results) >= 100:
break
if len(results) >= 100:
break
if not results:
return "未找到匹配的文件"
result_text = [f"搜索结果 (共 {len(results)} 个):"]
result_text.append(f"目录: {directory}")
if pattern:
result_text.append(f"模式: {pattern}")
if content_search:
result_text.append(f"内容: {content_search}")
result_text.append("")
result_text.extend(results[:100])
if len(results) > 100:
result_text.append(f"... 还有 {len(results) - 100} 个结果")
return '\n'.join(result_text)
except Exception as e:
return f"搜索失败: {e}"
def _op_info(self, args: List[str]) -> str:
"""获取文件信息"""
if not args:
return "错误:请提供路径"
path = os.path.abspath(args[0])
if not os.path.exists(path):
return f"错误:路径不存在: {path}"
try:
stat_info = os.stat(path)
result = [f"路径: {path}"]
result.append(f"名称: {os.path.basename(path)}")
result.append(f"类型: {'目录' if os.path.isdir(path) else '文件'}")
result.append(f"大小: {stat_info.st_size:,} 字节 ({stat_info.st_size / 1024:.1f} KB)")
result.append(f"创建时间: {datetime.fromtimestamp(stat_info.st_ctime).strftime('%Y-%m-%d %H:%M:%S')}")
result.append(f"修改时间: {datetime.fromtimestamp(stat_info.st_mtime).strftime('%Y-%m-%d %H:%M:%S')}")
result.append(f"访问时间: {datetime.fromtimestamp(stat_info.st_atime).strftime('%Y-%m-%d %H:%M:%S')}")
if os.path.isfile(path):
ext = os.path.splitext(path)[1]
result.append(f"扩展名: {ext if ext else '无'}")
# 尝试检测编码
try:
with open(path, 'r', encoding='utf-8') as f:
f.read(1024)
result.append("编码: UTF-8 (文本文件)")
except UnicodeDecodeError:
result.append("编码: 二进制文件")
return '\n'.join(result)
except Exception as e:
return f"获取信息失败: {e}"
def _op_mkdir(self, args: List[str]) -> str:
"""创建目录"""
if not args:
return "错误:请提供目录路径"
path = os.path.abspath(args[0])
if os.path.exists(path):
return f"错误:路径已存在: {path}"
try:
os.makedirs(path, exist_ok=True)
return f"✅ 目录已创建: {path}"
except Exception as e:
return f"创建目录失败: {e}"
def _op_modify_line(self, args: List[str]) -> str:
"""修改指定行"""
if len(args) < 3:
return "错误:请提供文件路径、行号和新内容"
file_path = os.path.abspath(args[0])
try:
line_number = int(args[1])
except ValueError:
return "错误:行号必须是整数"
new_content = args[2]
if not os.path.exists(file_path):
return f"错误:文件不存在: {file_path}"
if line_number < 1:
return "错误:行号必须 >= 1"
try:
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
if line_number > len(lines):
return f"错误:文件只有 {len(lines)} 行,无法修改第 {line_number} 行"
old_content = lines[line_number - 1].rstrip('\n\r')
lines[line_number - 1] = new_content + '\n'
with open(file_path, 'w', encoding='utf-8') as f:
f.writelines(lines)
return f"✅ 第 {line_number} 行已修改:\n 旧: {old_content}\n 新: {new_content}"
except Exception as e:
return f"修改行失败: {e}"
def _op_insert_line(self, args: List[str]) -> str:
"""插入行"""
if len(args) < 3:
return "错误:请提供文件路径、行号和内容"
file_path = os.path.abspath(args[0])
try:
line_number = int(args[1])
except ValueError:
return "错误:行号必须是整数"
content = args[2]
if not os.path.exists(file_path):
return f"错误:文件不存在: {file_path}"
if line_number < 1:
return "错误:行号必须 >= 1"
try:
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
insert_pos = min(line_number - 1, len(lines))
lines.insert(insert_pos, content + '\n')
with open(file_path, 'w', encoding='utf-8') as f:
f.writelines(lines)
return f"✅ 已在第 {line_number} 行插入内容: {content}"
except Exception as e:
return f"插入行失败: {e}"
def _op_delete_line(self, args: List[str]) -> str:
"""删除指定行"""
if len(args) < 2:
return "错误:请提供文件路径和行号"
file_path = os.path.abspath(args[0])
try:
line_number = int(args[1])
except ValueError:
return "错误:行号必须是整数"
if not os.path.exists(file_path):
return f"错误:文件不存在: {file_path}"
if line_number < 1:
return "错误:行号必须 >= 1"
try:
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
if line_number > len(lines):
return f"错误:文件只有 {len(lines)} 行,无法删除第 {line_number} 行"
deleted = lines[line_number - 1].rstrip('\n\r')
del lines[line_number - 1]
with open(file_path, 'w', encoding='utf-8') as f:
f.writelines(lines)
return f"✅ 已删除第 {line_number} 行: {deleted}"
except Exception as e:
return f"删除行失败: {e}"
def _op_replace_text(self, args: List[str]) -> str:
"""替换文本"""
if len(args) < 3:
return "错误:请提供文件路径、旧文本和新文本"
file_path = os.path.abspath(args[0])
old_text = args[1]
new_text = args[2]
start_line = None
end_line = None
i = 3
while i < len(args):
if args[i] == "--start" and i + 1 < len(args):
start_line = int(args[i + 1])
i += 1
elif args[i] == "--end" and i + 1 < len(args):
end_line = int(args[i + 1])
i += 1
i += 1
if not os.path.exists(file_path):
return f"错误:文件不存在: {file_path}"
try:
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
total_lines = len(lines)
replacements = []
start = max(0, (start_line or 1) - 1)
end = min(total_lines, end_line or total_lines)
for i in range(start, end):
if old_text in lines[i]:
count = lines[i].count(old_text)
lines[i] = lines[i].replace(old_text, new_text)
replacements.append(f"第 {i+1} 行: 替换了 {count} 处")
if not replacements:
return f"未找到匹配文本: {old_text}"
with open(file_path, 'w', encoding='utf-8') as f:
f.writelines(lines)
result = [f"✅ 文本替换完成"]
result.append(f"文件: {file_path}")
result.append(f"'{old_text}' -> '{new_text}'")
if start_line or end_line:
result.append(f"范围: 第 {start+1} 行到第 {end} 行")
result.append(f"修改了 {len(replacements)} 行:")
result.extend(replacements[:20])
if len(replacements) > 20:
result.append(f"... 还有 {len(replacements) - 20} 行")
return '\n'.join(result)
except Exception as e:
return f"文本替换失败: {e}"
# 测试
if __name__ == "__main__":
plugin = Plugin()
print("文件管理器插件测试:")
print(plugin.get_tool_info())
print("\n测试 list 操作:")
print(plugin.handle("list . -p *.py"))
+452
View File
@@ -0,0 +1,452 @@
"""
前端测试验证插件 - 类似 iFlow CLI 的 frontend-tester
支持HTML/CSS/JS自动化测试和UI验证
"""
import os
import subprocess
from colorama import Fore, Style
class Liugin:
"""前端测试验证插件 - 自动化测试Web界面"""
def __init__(self):
self.usage = """前端测试工具使用方法:
JSON格式示例:
{"action": "use_tool", "tool": "frontend_tester", "args": "validate index.html"} - 验证HTML文件
{"action": "use_tool", "tool": "frontend_tester", "args": "test style.css"} - 测试CSS文件
{"action": "use_tool", "tool": "frontend_tester", "args": "check app.js"} - 检查JS文件
{"action": "use_tool", "tool": "frontend_tester", "args": "preview index.html"} - 在浏览器中预览
{"action": "use_tool", "tool": "frontend_tester", "args": "analyze project"} - 分析整个项目
功能说明:
- validate <HTML文件路径> - 验证HTML文件的语法和结构
- test <CSS文件路径> - 测试CSS文件的样式和兼容性
- check <JS文件路径> - 检查JS文件的语法和潜在问题
- preview <HTML文件路径> - 在默认浏览器中打开预览
- analyze <项目路径> - 分析整个前端项目的结构和问题
支持的检查项:
✓ HTML语法验证
✓ CSS语法和兼容性检查
✓ JavaScript语法检查
✓ 响应式设计检查
✓ 可访问性检查
✓ 性能优化建议"""
self.cli = None
def set_cli(self, cli):
"""设置CLI实例引用"""
self.cli = cli
# 注册插件命令
self.cli.register_liugin_command('ftest', self.command_handler)
self.cli.register_liugin_command('frontend', self.command_handler)
def command_handler(self, args):
"""处理 /ftest 或 /frontend 命令"""
return self.handle(args)
def get_tool_info(self):
return {
"name": "frontend_tester",
"description": "前端测试验证工具,支持HTML/CSS/JS自动化测试和UI验证,包括语法检查、响应式设计、可访问性检查等",
"keywords": ["前端", "测试", "HTML", "CSS", "JavaScript", "UI", "验证", "preview", "validate", "frontend", "test"],
"usage": self.usage
}
def handle(self, args):
"""处理前端测试请求"""
try:
parts = args.strip().split(maxsplit=1)
if not parts:
return "错误:请提供操作类型和参数。使用 'frontend_tester help' 查看帮助"
operation = parts[0].lower()
if operation == "help":
return self.usage
elif operation == "validate":
# 验证HTML文件
if len(parts) < 2:
return "错误:请提供HTML文件路径。格式: validate <HTML文件路径>"
file_path = parts[1]
return self._validate_html(file_path)
elif operation == "test":
# 测试CSS文件
if len(parts) < 2:
return "错误:请提供CSS文件路径。格式: test <CSS文件路径>"
file_path = parts[1]
return self._test_css(file_path)
elif operation == "check":
# 检查JS文件
if len(parts) < 2:
return "错误:请提供JS文件路径。格式: check <JS文件路径>"
file_path = parts[1]
return self._check_js(file_path)
elif operation == "preview":
# 在浏览器中预览
if len(parts) < 2:
return "错误:请提供HTML文件路径。格式: preview <HTML文件路径>"
file_path = parts[1]
return self._preview_in_browser(file_path)
elif operation == "analyze":
# 分析整个项目
if len(parts) < 2:
return "错误:请提供项目路径。格式: analyze <项目路径>"
project_path = parts[1]
return self._analyze_project(project_path)
else:
return f"错误:不支持的操作 '{operation}'。支持的操作有: validate, test, check, preview, analyze, help"
except Exception as e:
return f"前端测试操作错误: {str(e)}"
def _validate_html(self, file_path):
"""验证HTML文件"""
if not os.path.exists(file_path):
return f"❌ 文件不存在: {file_path}"
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
result = f"\n🧪 HTML验证结果: {file_path}\n"
result += "=" * 60 + "\n\n"
# 基本检查
issues = []
# 检查DOCTYPE
if not content.strip().startswith('<!DOCTYPE'):
issues.append({
"level": "warning",
"message": "缺少DOCTYPE声明,建议添加 <!DOCTYPE html>"
})
# 检查lang属性
if '<html' in content and 'lang=' not in content:
issues.append({
"level": "warning",
"message": "建议在<html>标签中添加lang属性,如 lang=\"zh-CN\""
})
# 检查meta标签
if '<meta charset=' not in content:
issues.append({
"level": "error",
"message": "缺少字符集meta标签,建议添加 <meta charset=\"UTF-8\">"
})
if '<meta name="viewport"' not in content:
issues.append({
"level": "warning",
"message": "缺少viewport meta标签,建议添加以支持响应式设计"
})
# 检查title标签
if '<title>' not in content:
issues.append({
"level": "error",
"message": "缺少<title>标签"
})
# 统计元素
div_count = content.count('<div')
img_count = content.count('<img')
img_alt_count = content.count('alt=')
link_count = content.count('<link')
script_count = content.count('<script')
result += f"📊 文件统计:\n"
result += f" - div元素: {div_count}\n"
result += f" - img元素: {img_count}\n"
result += f" - img alt属性: {img_alt_count}\n"
result += f" - link标签: {link_count}\n"
result += f" - script标签: {script_count}\n\n"
# 检查img alt属性
if img_count > 0 and img_alt_count < img_count:
issues.append({
"level": "warning",
"message": f"有 {img_count - img_alt_count} 个img元素缺少alt属性"
})
# 显示问题
if issues:
result += f"⚠️ 发现 {len(issues)} 个问题:\n\n"
for i, issue in enumerate(issues, 1):
level = issue['level']
if level == 'error':
icon = f"{Fore.RED}❌{Style.RESET_ALL}"
elif level == 'warning':
icon = f"{Fore.YELLOW}⚠️{Style.RESET_ALL}"
else:
icon = f"{Fore.GREEN}ℹ️{Style.RESET_ALL}"
result += f"{icon} {i}. {issue['message']}\n"
else:
result += f"{Fore.GREEN}✅ 未发现问题{Style.RESET_ALL}\n"
result += "\n" + "=" * 60 + "\n"
return result
except UnicodeDecodeError:
return "❌ 文件编码问题,无法读取"
except Exception as e:
return f"❌ 验证失败: {str(e)}"
def _test_css(self, file_path):
"""测试CSS文件"""
if not os.path.exists(file_path):
return f"❌ 文件不存在: {file_path}"
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
result = f"\n🧪 CSS测试结果: {file_path}\n"
result += "=" * 60 + "\n\n"
issues = []
# 检查常见的CSS问题
if content.count('{') != content.count('}'):
issues.append({
"level": "error",
"message": "大括号不匹配"
})
# 检查颜色使用
if '#000000' in content or '#ffffff' in content:
issues.append({
"level": "info",
"message": "使用了纯黑或纯白,建议使用更柔和的颜色"
})
# 检查单位使用
if content.count('px') > 10 and 'rem' not in content and 'em' not in content:
issues.append({
"level": "warning",
"message": "大量使用px单位,建议考虑使用rem或em以支持更好的缩放"
})
# 统计
rule_count = content.count('{')
selector_count = len([line for line in content.split('\n') if '{' in line])
result += f"📊 文件统计:\n"
result += f" - CSS规则: {rule_count}\n"
result += f" - 选择器: {selector_count}\n\n"
# 显示问题
if issues:
result += f"⚠️ 发现 {len(issues)} 个问题:\n\n"
for i, issue in enumerate(issues, 1):
level = issue['level']
if level == 'error':
icon = f"{Fore.RED}❌{Style.RESET_ALL}"
elif level == 'warning':
icon = f"{Fore.YELLOW}⚠️{Style.RESET_ALL}"
else:
icon = f"{Fore.GREEN}ℹ️{Style.RESET_ALL}"
result += f"{icon} {i}. {issue['message']}\n"
else:
result += f"{Fore.GREEN}✅ 未发现问题{Style.RESET_ALL}\n"
result += "\n" + "=" * 60 + "\n"
return result
except UnicodeDecodeError:
return "❌ 文件编码问题,无法读取"
except Exception as e:
return f"❌ 测试失败: {str(e)}"
def _check_js(self, file_path):
"""检查JS文件"""
if not os.path.exists(file_path):
return f"❌ 文件不存在: {file_path}"
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
result = f"\n🧪 JavaScript检查结果: {file_path}\n"
result += "=" * 60 + "\n\n"
issues = []
# 检查常见问题
if 'var ' in content:
issues.append({
"level": "warning",
"message": "使用了var声明,建议使用let或const"
})
if '==' in content and '!==' not in content:
issues.append({
"level": "warning",
"message": "使用了==比较,建议使用===严格相等"
})
if content.count('console.log') > 5:
issues.append({
"level": "info",
"message": f"有{content.count('console.log')}个console.log,生产环境建议移除"
})
if 'eval(' in content:
issues.append({
"level": "error",
"message": "使用了eval(),存在安全风险"
})
# 统计
line_count = len(content.split('\n'))
function_count = content.count('function ')
result += f"📊 文件统计:\n"
result += f" - 代码行数: {line_count}\n"
result += f" - 函数数量: {function_count}\n\n"
# 显示问题
if issues:
result += f"⚠️ 发现 {len(issues)} 个问题:\n\n"
for i, issue in enumerate(issues, 1):
level = issue['level']
if level == 'error':
icon = f"{Fore.RED}❌{Style.RESET_ALL}"
elif level == 'warning':
icon = f"{Fore.YELLOW}⚠️{Style.RESET_ALL}"
else:
icon = f"{Fore.GREEN}ℹ️{Style.RESET_ALL}"
result += f"{icon} {i}. {issue['message']}\n"
else:
result += f"{Fore.GREEN}✅ 未发现问题{Style.RESET_ALL}\n"
result += "\n" + "=" * 60 + "\n"
return result
except UnicodeDecodeError:
return "❌ 文件编码问题,无法读取"
except Exception as e:
return f"❌ 检查失败: {str(e)}"
def _preview_in_browser(self, file_path):
"""在浏览器中预览"""
if not os.path.exists(file_path):
return f"❌ 文件不存在: {file_path}"
try:
# Windows系统
os.startfile(file_path)
return f"✅ 已在默认浏览器中打开: {file_path}"
except Exception as e:
return f"❌ 无法打开浏览器: {str(e)}"
def _analyze_project(self, project_path):
"""分析整个前端项目"""
if not os.path.exists(project_path):
return f"❌ 项目路径不存在: {project_path}"
result = f"\n🧪 前端项目分析: {project_path}\n"
result += "=" * 60 + "\n\n"
try:
# 统计文件
html_files = []
css_files = []
js_files = []
for root, dirs, files in os.walk(project_path):
for file in files:
if file.endswith('.html'):
html_files.append(os.path.join(root, file))
elif file.endswith('.css'):
css_files.append(os.path.join(root, file))
elif file.endswith('.js'):
js_files.append(os.path.join(root, file))
result += f"📊 项目文件统计:\n"
result += f" - HTML文件: {len(html_files)}\n"
result += f" - CSS文件: {len(css_files)}\n"
result += f" - JavaScript文件: {len(js_files)}\n\n"
# 建议检查
result += f"💡 建议检查:\n"
if html_files:
result += f" - HTML文件:\n"
for html_file in html_files[:3]: # 只显示前3个
result += f" • {os.path.basename(html_file)}\n"
if len(html_files) > 3:
result += f" • ... 还有 {len(html_files) - 3} 个文件\n"
if css_files:
result += f" - CSS文件:\n"
for css_file in css_files[:3]:
result += f" • {os.path.basename(css_file)}\n"
if len(css_files) > 3:
result += f" • ... 还有 {len(css_files) - 3} 个文件\n"
if js_files:
result += f" - JavaScript文件:\n"
for js_file in js_files[:3]:
result += f" • {os.path.basename(js_file)}\n"
if len(js_files) > 3:
result += f" • ... 还有 {len(js_files) - 3} 个文件\n"
result += f"\n{Fore.GREEN}✅ 分析完成{Style.RESET_ALL}\n"
result += "\n" + "=" * 60 + "\n"
return result
except Exception as e:
return f"❌ 分析失败: {str(e)}"
# 测试函数
def test_plugin():
"""测试插件功能"""
plugin = Plugin()
print("前端测试验证插件测试:")
print("\n1. 创建测试HTML文件:")
test_html = """<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>测试页面</title>
</head>
<body>
<h1>测试</h1>
</body>
</html>"""
with open('test.html', 'w', encoding='utf-8') as f:
f.write(test_html)
print("\n2. 验证HTML:")
print(plugin.handle("validate test.html"))
print("\n3. 在浏览器中预览:")
print(plugin.handle("preview test.html"))
print("\n4. 分析项目:")
print(plugin.handle("analyze ."))
# 清理测试文件
if os.path.exists('test.html'):
os.remove('test.html')
if __name__ == "__main__":
test_plugin()
+346
View File
@@ -0,0 +1,346 @@
"""
Git 集成插件 - 版本控制能力
对标 Claude Code 的 Git 工作流支持
"""
import os
import subprocess
from typing import Optional, Tuple
class Plugin:
"""Git 集成工具 - 提供完整的 Git 操作支持"""
def __init__(self):
self.usage = """Git 集成工具
操作类型:
status - 查看仓库状态
diff [文件路径] - 查看未暂存的变更
diff --cached - 查看已暂存的变更
log [数量] - 查看提交历史
log --oneline [数量] - 查看简洁提交历史
add <文件路径> - 暂存文件
add . - 暂存所有变更
commit <提交信息> - 提交变更
branch - 查看分支列表
branch <名称> - 创建新分支
checkout <分支名> - 切换分支
checkout -b <分支名> - 创建并切换分支
stash - 暂存当前变更
stash pop - 恢复暂存的变更
show <commit> - 查看指定提交
blame <文件路径> - 查看文件逐行修改记录
remote - 查看远程仓库
init - 初始化仓库
clone <URL> [目录] - 克隆仓库
"""
self.cli = None
def set_cli(self, cli):
self.cli = cli
def get_tool_info(self):
return {
"name": "git_tools",
"description": "Git 版本控制工具 - 查看状态、提交、分支管理、查看历史等",
"keywords": ["git", "版本控制", "提交", "分支", "commit", "branch", "status"],
"usage": self.usage
}
def get_mcp_definition(self):
return {
"name": "git_tools",
"description": "Git 版本控制工具",
"inputSchema": {
"type": "object",
"properties": {
"operation": {"type": "string", "description": "Git 操作"},
"args": {"type": "string", "description": "操作参数"},
"cwd": {"type": "string", "description": "工作目录"}
},
"required": ["operation"]
}
}
def convert_mcp_args(self, arguments):
op = arguments.get("operation", "")
args = arguments.get("args", "")
return f"{op} {args}".strip()
def _run_git(self, args: list, cwd: Optional[str] = None) -> Tuple[bool, str]:
"""执行 git 命令"""
try:
result = subprocess.run(
["git"] + args,
capture_output=True,
text=True,
cwd=cwd,
timeout=30
)
output = result.stdout
if result.stderr:
output += result.stderr
return result.returncode == 0, output.strip()
except FileNotFoundError:
return False, "错误:git 未安装。请先安装 Git。"
except subprocess.TimeoutExpired:
return False, "错误:Git 命令执行超时"
except Exception as e:
return False, f"错误:{str(e)}"
def handle(self, args: str) -> str:
try:
parts = args.strip().split(maxsplit=1)
if not parts:
return self._git_status("")
operation = parts[0].lower()
rest = parts[1] if len(parts) > 1 else ""
# 获取工作目录
cwd = None
if self.cli and hasattr(self.cli, 'current_dir'):
cwd = self.cli.current_dir
handlers = {
"status": self._git_status,
"diff": self._git_diff,
"log": self._git_log,
"add": self._git_add,
"commit": self._git_commit,
"branch": self._git_branch,
"checkout": self._git_checkout,
"stash": self._git_stash,
"show": self._git_show,
"blame": self._git_blame,
"remote": self._git_remote,
"init": self._git_init,
"clone": self._git_clone,
}
handler = handlers.get(operation)
if not handler:
# 尝试直接执行 git 命令
return self._git_raw(args, cwd)
return handler(rest, cwd)
except Exception as e:
return f"Git 工具错误: {str(e)}"
def _git_status(self, args: str, cwd=None) -> str:
ok, output = self._run_git(["status", "--porcelain"], cwd)
if not ok:
return f"Git 错误: {output}"
if not output:
return "✅ 工作区干净,没有未提交的变更"
# 解析状态
lines = output.split('\n')
staged = []
unstaged = []
untracked = []
for line in lines:
if len(line) < 4:
continue
status = line[:2]
filename = line[3:]
if status[0] in ('M', 'A', 'D', 'R', 'C'):
staged.append(f" ✅ {status[0]} {filename}")
if status[1] in ('M', 'D'):
unstaged.append(f" ⚠️ {status[1]} {filename}")
if status == '??':
untracked.append(f" 🆕 {filename}")
result = ["📊 Git 状态:"]
if staged:
result.append(f"\n已暂存 ({len(staged)}):")
result.extend(staged)
if unstaged:
result.append(f"\n未暂存 ({len(unstaged)}):")
result.extend(unstaged)
if untracked:
result.append(f"\n未跟踪 ({len(untracked)}):")
result.extend(untracked)
# 获取分支信息
ok2, branch = self._run_git(["branch", "--show-current"], cwd)
if ok2 and branch:
result.insert(1, f"🌿 分支: {branch}")
return '\n'.join(result)
def _git_diff(self, args: str, cwd=None) -> str:
if args.strip() == "--cached":
ok, output = self._run_git(["diff", "--cached"], cwd)
elif args.strip():
ok, output = self._run_git(["diff", args.strip()], cwd)
else:
ok, output = self._run_git(["diff"], cwd)
if not ok:
return f"Git 错误: {output}"
if not output:
return "没有变更"
# 限制输出长度
if len(output) > 5000:
output = output[:5000] + f"\n... (输出截断,共 {len(output)} 字符)"
return f"📝 变更:\n{output}"
def _git_log(self, args: str, cwd=None) -> str:
parts = args.split()
oneline = "--oneline" in parts
count = 10
for p in parts:
if p.isdigit():
count = int(p)
if oneline:
ok, output = self._run_git(
["log", "--oneline", f"-{count}", "--graph", "--decorate"], cwd)
else:
ok, output = self._run_git(
["log", f"-{count}", "--format=%h %an %ad %s", "--date=short"], cwd)
if not ok:
return f"Git 错误: {output}"
if not output:
return "暂无提交历史"
return f"📜 提交历史:\n{output}"
def _git_add(self, args: str, cwd=None) -> str:
target = args.strip()
if not target:
return "错误:请提供文件路径(使用 . 暂存所有)"
ok, output = self._run_git(["add", target], cwd)
if not ok:
return f"Git add 失败: {output}"
return f"✅ 已暂存: {target}"
def _git_commit(self, args: str, cwd=None) -> str:
message = args.strip()
if not message:
return "错误:请提供提交信息"
ok, output = self._run_git(["commit", "-m", message], cwd)
if not ok:
return f"Git commit 失败: {output}"
return f"✅ 提交成功:\n{output}"
def _git_branch(self, args: str, cwd=None) -> str:
if not args.strip():
ok, output = self._run_git(["branch", "-a"], cwd)
if not ok:
return f"Git 错误: {output}"
return f"🌿 分支列表:\n{output}"
else:
ok, output = self._run_git(["branch", args.strip()], cwd)
if not ok:
return f"创建分支失败: {output}"
return f"✅ 已创建分支: {args.strip()}"
def _git_checkout(self, args: str, cwd=None) -> str:
if args.startswith("-b "):
branch = args[3:].strip()
ok, output = self._run_git(["checkout", "-b", branch], cwd)
else:
branch = args.strip()
ok, output = self._run_git(["checkout", branch], cwd)
if not ok:
return f"切换分支失败: {output}"
return f"✅ 已切换到分支: {branch}"
def _git_stash(self, args: str, cwd=None) -> str:
if args.strip() == "pop":
ok, output = self._run_git(["stash", "pop"], cwd)
else:
ok, output = self._run_git(["stash"], cwd)
if not ok:
return f"Git stash 失败: {output}"
return f"✅ {output}"
def _git_show(self, args: str, cwd=None) -> str:
commit = args.strip() or "HEAD"
ok, output = self._run_git(["show", "--stat", commit], cwd)
if not ok:
return f"Git show 失败: {output}"
if len(output) > 3000:
output = output[:3000] + "\n... (输出截断)"
return output
def _git_blame(self, args: str, cwd=None) -> str:
path = args.strip()
if not path:
return "错误:请提供文件路径"
ok, output = self._run_git(["blame", "--line-porcelain", path], cwd)
if not ok:
return f"Git blame 失败: {output}"
# 简化输出
lines = output.split('\n')
simplified = []
for line in lines:
if line.startswith('author '):
author = line[7:]
elif line.startswith('\t'):
simplified.append(f" {author}: {line[1:]}")
if len(simplified) > 100:
simplified = simplified[:100]
simplified.append(f" ... (共 {len(simplified)} 行)")
return f"📝 {path} 修改记录:\n" + '\n'.join(simplified)
def _git_remote(self, args: str, cwd=None) -> str:
ok, output = self._run_git(["remote", "-v"], cwd)
if not ok:
return f"Git 错误: {output}"
if not output:
return "没有配置远程仓库"
return f"🌐 远程仓库:\n{output}"
def _git_init(self, args: str, cwd=None) -> str:
path = args.strip() or "."
ok, output = self._run_git(["init", path], cwd)
if not ok:
return f"Git init 失败: {output}"
return f"✅ {output}"
def _git_clone(self, args: str, cwd=None) -> str:
parts = args.split()
if not parts:
return "错误:请提供仓库 URL"
url = parts[0]
target = parts[1] if len(parts) > 1 else ""
cmd = ["clone", url]
if target:
cmd.append(target)
ok, output = self._run_git(cmd, cwd)
if not ok:
return f"Git clone 失败: {output}"
return f"✅ 克隆成功:\n{output}"
def _git_raw(self, args: str, cwd=None) -> str:
"""直接执行 git 命令"""
parts = args.split()
ok, output = self._run_git(parts, cwd)
if not ok:
return f"Git 错误: {output}"
return output
+1216
View File
File diff suppressed because it is too large. Load diff
+280
View File
@@ -0,0 +1,280 @@
import requests
import time
import socket
from urllib.parse import urlparse
import json
from colorama import Fore, Style
class Liugin:
"""网络工具插件"""
def __init__(self):
self.usage = """网络工具使用方法:/r
network_tools <操作> <参数>/r
例如:/r
- network_tools ping www.baidu.com - 测试网站连通性和延迟/r
- network_tools get https://www.example.com - 获取网页内容/r
- network_tools status https://www.example.com - 检查网站状态/r
- network_tools headers https://www.example.com - 获取网站响应头/r
- network_tools ip www.example.com - 获取网站IP地址/r
/r
详细说明:/r
- ping操作: network_tools ping <域名或IP> - 测试连通性和延迟/r
- get操作: network_tools get <URL> - 获取网页内容/r
- status操作: network_tools status <URL> - 检查网站HTTP状态码/r
- headers操作: network_tools headers <URL> - 获取网站响应头信息/r
- ip操作: network_tools ip <域名> - 获取域名对应的IP地址/r
/r
使用工具的JSON格式示例:/r
{"action": "use_tool", "tool": "network_tools", "args": "ping www.baidu.com"} - 测试百度连通性/r
{"action": "use_tool", "tool": "network_tools", "args": "get https://www.example.com"} - 获取网页内容/r
{"action": "use_tool", "tool": "network_tools", "args": "status https://www.example.com"} - 检查网站状态/r
{"action": "use_tool", "tool": "network_tools", "args": "headers https://www.example.com"} - 获取响应头/r
{"action": "use_tool", "tool": "network_tools", "args": "ip www.example.com"} - 获取IP地址/r
"""
self.cli = None # CLI实例引用
self.timeout = 10 # 请求超时时间(秒)
def set_cli(self, cli):
"""设置CLI实例引用"""
self.cli = cli
# 注册插件命令
self.cli.register_liugin_command('net', self.command_handler)
self.cli.register_liugin_command('ping', self.ping_command_handler)
self.cli.register_liugin_command('website', self.website_command_handler)
def command_handler(self, args):
"""处理 /net 命令"""
# 解析参数
parts = args.strip().split(maxsplit=1)
if len(parts) < 1:
return "请提供操作类型: ping, get, status, headers, ip 等"
operation = parts[0].lower()
remaining_args = parts[1] if len(parts) > 1 else ""
# 调用实际的网络工具功能
return self.handle(f"{operation} {remaining_args}")
def ping_command_handler(self, args):
"""处理 /ping 命令"""
return self.handle(f"ping {args}")
def website_command_handler(self, args):
"""处理 /website 命令"""
return self.handle(f"status {args}")
def get_tool_info(self):
return {
"name": "network_tools",
"description": "网络工具,用于测试网站连通性、获取网页内容、检查网站状态、获取响应头、解析IP地址等",
"keywords": ["网络", "ping", "网站", "状态", "网页", "IP", "headers", "get", "网络工具", "连通性", "延迟"],
"usage": """network_tools 工具使用说明:
JSON格式示例:
{"action": "use_tool", "tool": "network_tools", "args": "ping www.baidu.com"} - 测试百度连通性
{"action": "use_tool", "tool": "network_tools", "args": "get https://www.example.com"} - 获取网页内容
{"action": "use_tool", "tool": "network_tools", "args": "status https://www.example.com"} - 检查网站状态
{"action": "use_tool", "tool": "network_tools", "args": "headers https://www.example.com"} - 获取网站响应头
{"action": "use_tool", "tool": "network_tools", "args": "ip www.example.com"} - 获取网站IP地址"""
}
def handle(self, args):
"""处理网络工具请求"""
try:
# 解析参数
parts = args.strip().split(maxsplit=1)
if len(parts) < 1:
return "错误:参数不足。请提供操作类型。\n可用操作: ping, get, status, headers, ip"
operation = parts[0].lower()
if operation == "ping":
# 测试网站连通性和延迟
if len(parts) < 2:
return "错误:请提供要测试的域名或IP地址。格式: ping <域名或IP>"
target = parts[1].strip()
return self._ping(target)
elif operation == "get":
# 获取网页内容
if len(parts) < 2:
return "错误:请提供URL。格式: get <URL>"
url = parts[1].strip()
return self._get_content(url)
elif operation == "status":
# 检查网站状态码
if len(parts) < 2:
return "错误:请提供URL。格式: status <URL>"
url = parts[1].strip()
return self._check_status(url)
elif operation == "headers":
# 获取网站响应头
if len(parts) < 2:
return "错误:请提供URL。格式: headers <URL>"
url = parts[1].strip()
return self._get_headers(url)
elif operation == "ip":
# 获取域名IP地址
if len(parts) < 2:
return "错误:请提供域名。格式: ip <域名>"
domain = parts[1].strip()
return self._get_ip(domain)
else:
return f"错误:不支持的操作 '{operation}'。支持的操作有: ping, get, status, headers, ip。"
except Exception as e:
return f"网络工具操作错误: {str(e)}"
def _ping(self, target):
"""测试网站连通性和延迟"""
try:
# 确保URL格式正确
if not target.startswith(('http://', 'https://')):
test_url = 'http://' + target
else:
test_url = target
# 添加协议如果缺失
if not test_url.startswith(('http://', 'https://')):
test_url = 'http://' + test_url
start_time = time.time()
response = requests.get(test_url, timeout=self.timeout)
end_time = time.time()
response_time = (end_time - start_time) * 1000 # 转换为毫秒
result = f".ping 测试结果:\n"
result += f"目标: {target}\n"
result += f"HTTP状态码: {response.status_code}\n"
result += f"响应时间: {response_time:.2f} ms\n"
result += f"连接状态: {'成功' if response.status_code < 400 else '失败'}\n"
if response.status_code >= 400:
result += f"错误信息: {response.reason if hasattr(response, 'reason') else '请求失败'}\n"
return result
except requests.exceptions.Timeout:
return f".ping 测试结果:\n目标: {target}\n错误: 请求超时 (>{self.timeout}秒)\n连接状态: 失败"
except requests.exceptions.ConnectionError:
return f".ping 测试结果:\n目标: {target}\n错误: 连接失败 (无法连接到服务器)\n连接状态: 失败"
except requests.exceptions.RequestException as e:
return f".ping 测试结果:\n目标: {target}\n错误: {str(e)}\n连接状态: 失败"
except Exception as e:
return f".ping 测试结果:\n目标: {target}\n错误: {str(e)}\n连接状态: 失败"
def _get_content(self, url):
"""获取网页内容"""
try:
# 确保URL格式正确
if not url.startswith(('http://', 'https://')):
url = 'https://' + url
response = requests.get(url, timeout=self.timeout)
if response.status_code == 200:
content = response.text
# 限制内容长度以避免过长输出
if len(content) > 2000:
content = content[:2000] + f"\n... (内容已截断,完整内容共{len(content)}字符)"
result = f"网页内容 (来自 {url}):\n"
result += content
return result
else:
return f"获取网页内容失败,状态码: {response.status_code}, 原因: {response.reason if hasattr(response, 'reason') else ''}"
except requests.exceptions.Timeout:
return f"获取网页内容超时 (>{self.timeout}秒)"
except requests.exceptions.ConnectionError:
return f"无法连接到 {url},请检查URL是否正确"
except requests.exceptions.RequestException as e:
return f"请求失败: {str(e)}"
except Exception as e:
return f"获取网页内容时发生错误: {str(e)}"
def _check_status(self, url):
"""检查网站状态码"""
try:
# 确保URL格式正确
if not url.startswith(('http://', 'https://')):
url = 'https://' + url
response = requests.get(url, timeout=self.timeout)
result = f"网站状态 (来自 {url}):\n"
result += f"HTTP状态码: {response.status_code}\n"
result += f"状态描述: {response.reason if hasattr(response, 'reason') else 'Unknown'}\n"
result += f"响应时间: {response.elapsed.total_seconds():.2f} 秒\n"
result += f"内容长度: {len(response.content)} 字节\n"
result += f"服务器: {response.headers.get('Server', '未知')}\n"
result += f"内容类型: {response.headers.get('Content-Type', '未知')}\n"
return result
except requests.exceptions.Timeout:
return f"检查网站状态超时 (>{self.timeout}秒)"
except requests.exceptions.ConnectionError:
return f"无法连接到 {url},请检查URL是否正确"
except requests.exceptions.RequestException as e:
return f"请求失败: {str(e)}"
except Exception as e:
return f"检查网站状态时发生错误: {str(e)}"
def _get_headers(self, url):
"""获取网站响应头"""
try:
# 确保URL格式正确
if not url.startswith(('http://', 'https://')):
url = 'https://' + url
response = requests.get(url, timeout=self.timeout)
headers = dict(response.headers)
result = f"响应头 (来自 {url}):\n"
for key, value in headers.items():
result += f"{key}: {value}\n"
return result
except requests.exceptions.Timeout:
return f"获取响应头超时 (>{self.timeout}秒)"
except requests.exceptions.ConnectionError:
return f"无法连接到 {url},请检查URL是否正确"
except requests.exceptions.RequestException as e:
return f"请求失败: {str(e)}"
except Exception as e:
return f"获取响应头时发生错误: {str(e)}"
def _get_ip(self, domain):
"""获取域名对应的IP地址"""
try:
# 如果域名包含协议,去掉协议部分
if domain.startswith(('http://', 'https://')):
parsed = urlparse(domain)
domain = parsed.netloc
ip_address = socket.gethostbyname(domain)
result = f"域名解析结果:\n"
result += f"域名: {domain}\n"
result += f"IP地址: {ip_address}\n"
return result
except socket.gaierror:
return f"无法解析域名: {domain},请检查域名是否正确"
except Exception as e:
return f"域名解析失败: {str(e)}"
+198
View File
@@ -0,0 +1,198 @@
"""
Ollama图像识别插件
支持多种图像识别引擎的切换
"""
import os
import sys
from colorama import Fore, Style
# 添加项目根目录到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 image_engine import get_engine_manager
class Liugin:
"""图像识别插件 - 支持多引擎切换"""
def __init__(self):
self.usage = """图像识别插件使用方法:
/image <操作> <参数>
操作命令:
- analyze <图片路径> - 详细分析图片
- ocr <图片路径> - 快速OCR文字识别
- quick <图片路径> - 快速图片描述
- describe <图片路径> - 完整描述图片
- model [模型名] - 查看/切换模型
- models - 列出可用的视觉模型
- engine [引擎名] - 查看/切换识别引擎
- engines - 列出所有识别引擎
示例:
- image analyze C:\\photo.png
- image ocr C:\\screenshot.png
- image model llava:13b
- image engine openai
JSON调用示例:
{"action": "use_tool", "tool": "image", "args": "analyze c:\\\\test.png"}
"""
self.cli = None
self.engine_manager = None
# 支持的图片格式
self.supported_formats = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp']
def set_cli(self, cli):
"""设置CLI实例引用"""
self.cli = cli
# 注册插件命令
self.cli.register_liugin_command('image', self.command_handler)
self.cli.register_liugin_command('img', self.command_handler)
def command_handler(self, args):
"""处理命令"""
parts = args.strip().split(maxsplit=1)
if len(parts) < 1:
return self._get_help()
operation = parts[0].lower()
remaining_args = parts[1] if len(parts) > 1 else ""
return self.handle(f"{operation} {remaining_args}")
def get_tool_info(self):
return {
"name": "image",
"description": "图像识别工具,支持多引擎切换,使用本地或云端模型进行图像分析",
"keywords": ["图像", "图片", "image", "analyze", "分析", "视觉", "识别", "OCR"],
"usage": """image 工具使用说明:
- analyze <图片路径> - 详细分析
- ocr <图片路径> - OCR文字识别
- quick <图片路径> - 快速描述
- engine <引擎名> - 切换引擎
- engines - 列出所有引擎
支持格式:PNG, JPG, JPEG, GIF, WEBP, BMP"""
}
def handle(self, args):
"""处理请求"""
# 延迟初始化引擎管理器
if self.engine_manager is None:
self.engine_manager = get_engine_manager()
try:
parts = args.strip().split(maxsplit=1)
if len(parts) < 1:
return self._get_help()
operation = parts[0].lower()
image_path = parts[1].strip() if len(parts) > 1 else ""
# 引擎管理命令
if operation == "engines":
return self.engine_manager.list_engines()
elif operation == "engine":
if image_path:
success, msg = self.engine_manager.switch_engine(image_path)
return msg
return f"当前引擎: {self.engine_manager.get_current_engine_name()}"
elif operation == "models":
engine = self.engine_manager.get_engine()
if engine and hasattr(engine, 'get_model_list'):
return engine.get_model_list()
return "当前引擎不支持模型列表"
elif operation == "model":
engine = self.engine_manager.get_engine()
if not image_path:
if engine and hasattr(engine, 'model'):
return f"当前模型: {engine.model}"
return "当前引擎不支持模型切换"
if engine and hasattr(engine, 'set_model'):
success, msg = engine.set_model(image_path)
return msg
return "当前引擎不支持模型切换"
# 图像分析命令
elif operation in ["analyze", "ocr", "quick", "describe"]:
if not image_path:
return f"错误:请提供图片路径。用法: {operation} <图片路径>"
return self._analyze_image(image_path, operation)
else:
# 兼容旧用法,直接当作图片路径处理
return self._analyze_image(operation, "analyze")
except Exception as e:
return f"错误: {str(e)}"
def _get_help(self):
"""获取帮助信息"""
current = self.engine_manager.get_current_engine_name() if self.engine_manager else "未初始化"
return f"""图像识别插件命令:
- analyze <图片路径> 详细分析图片
- ocr <图片路径> OCR文字识别
- quick <图片路径> 快速描述
- describe <图片路径> 完整描述
- model [模型名] 查看/切换模型
- models 列出视觉模型
- engine [引擎名] 查看/切换引擎
- engines 列出所有引擎
当前引擎: {current}"""
def _analyze_image(self, image_path, mode="analyze"):
"""使用引擎分析图片"""
# 检查文件
if not os.path.exists(image_path):
return f"文件不存在: {image_path}"
ext = os.path.splitext(image_path)[1].lower().lstrip('.')
if ext not in self.supported_formats:
return f"不支持的图片格式: {ext}。支持: {', '.join(self.supported_formats)}"
# 检查引擎
engine = self.engine_manager.get_engine()
if not engine:
return "没有可用的图像引擎"
file_size = os.path.getsize(image_path)
engine_name = self.engine_manager.get_current_engine_name()
# 获取模型名(如果支持)
model_info = ""
if hasattr(engine, 'model'):
model_info = f" | 模型: {engine.model}"
# 文件大小警告
size_warning = ""
if file_size > 1024 * 1024: # 大于1MB
size_warning = f" {Fore.YELLOW}(较大图片,处理可能需要较长时间){Style.RESET_ALL}"
print(f"{Fore.CYAN}📷 图像识别: {os.path.basename(image_path)}{Style.RESET_ALL}{size_warning}")
print(f"{Fore.YELLOW}📦 大小: {file_size/1024:.1f}KB | 引擎: {engine_name}{model_info}{Style.RESET_ALL}")
print(f"{Fore.YELLOW}⏳ 正在识别,请耐心等待...{Style.RESET_ALL}")
try:
# 使用引擎分析
full_response = ""
for chunk in self.engine_manager.analyze(image_path, mode, stream=True):
if chunk:
print(chunk, end='', flush=True)
full_response += chunk
print() # 换行
print(f"{Fore.GREEN}✓ 识别完成{Style.RESET_ALL}")
return full_response if full_response else "识别结果为空"
except Exception as e:
error_msg = str(e)
print(f"{Fore.RED}✗ {error_msg}{Style.RESET_ALL}")
return f"识别失败: {error_msg}"
+1
View File
@@ -0,0 +1 @@
"hello"
+144
View File
@@ -0,0 +1,144 @@
"""
发送图片插件 - Clawli 模式下向手机端发送图片
支持独立进程服务器模式
"""
import os
import json
from typing import Dict, Any
def get_clawli_server():
"""获取 Clawli 服务器实例(优先使用独立进程服务器)"""
try:
import clawli_server
if clawli_server.is_running():
return clawli_server
except ImportError:
pass
# 回退到旧的 WebSocket 服务器
try:
from websocket_server import websocket_server
return websocket_server
except ImportError:
return None
class Liugin:
"""发送图片插件 - 在 Clawli 模式下发送图片到手机端"""
def __init__(self):
self.name = "send_image"
self.cli = None
def set_cli(self, cli_instance):
"""设置 CLI 引用"""
self.cli = cli_instance
def get_tool_info(self):
"""返回工具信息"""
return {
"name": "send_image",
"description": "在 Clawli 模式下发送图片到手机端,支持 JPG、PNG、GIF、BMP、WEBP 格式",
"keywords": ["图片", "发送", "手机", "clawli", "image", "send"],
"usage": "send_image {\"image_path\": \"图片路径\", \"caption\": \"说明文字\"}"
}
def handle(self, args):
"""处理工具调用
Args:
args: 可以是 JSON 字符串或字典
"""
# 解析参数
if isinstance(args, str):
try:
params = json.loads(args)
except json.JSONDecodeError:
params = {"image_path": args.strip()}
elif isinstance(args, dict):
params = args
else:
return "错误: 参数格式无效"
image_path = params.get("image_path", "")
caption = params.get("caption", "")
if not image_path:
return "错误: 请提供图片路径"
# 检查文件是否存在
if not os.path.isfile(image_path):
return f"错误: 图片文件不存在: {image_path}"
# 检查是否是图片文件
ext = image_path.lower().split('.')[-1] if '.' in image_path else ''
supported_formats = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']
if ext not in supported_formats:
return f"错误: 不支持的图片格式: {ext},支持: {', '.join(supported_formats)}"
# 获取服务器
server = get_clawli_server()
if not server:
return "错误: Clawli 服务器未初始化"
# 尝试使用独立进程服务器
try:
import clawli_server
if clawli_server.is_running():
success, msg = clawli_server.send_image(image_path, caption)
if success:
file_size = os.path.getsize(image_path)
size_str = f"{file_size/1024:.1f}KB" if file_size < 1024*1024 else f"{file_size/1024/1024:.2f}MB"
return f"图片已发送到手机端: {os.path.basename(image_path)} ({size_str})"
else:
return f"错误: {msg}"
except ImportError:
pass
# 回退到旧的 WebSocket 服务器
if hasattr(server, 'running') and not server.running:
return "错误: Clawli 模式未启动,请先使用 /remote start 启动远程服务"
if hasattr(server, 'clients') and not server.clients:
return "错误: 没有连接的手机端,请确保手机已连接"
if hasattr(server, 'send_image_to_clients'):
success = server.send_image_to_clients(image_path, caption)
if success:
file_size = os.path.getsize(image_path)
size_str = f"{file_size/1024:.1f}KB" if file_size < 1024*1024 else f"{file_size/1024/1024:.2f}MB"
return f"图片已发送到手机端: {os.path.basename(image_path)} ({size_str})"
return "错误: 发送图片失败"
# 工具定义
TOOLS = [
{
"type": "function",
"function": {
"name": "send_image",
"description": "在 Clawli 模式下发送图片到手机端。支持 JPG、PNG、GIF、BMP、WEBP 格式。",
"parameters": {
"type": "object",
"properties": {
"image_path": {
"type": "string",
"description": "图片文件的绝对路径"
},
"caption": {
"type": "string",
"description": "图片说明文字(可选)"
}
},
"required": ["image_path"]
}
}
}
]
def get_tools():
"""返回工具定义列表"""
return TOOLS
+549
View File
@@ -0,0 +1,549 @@
"""
语音识别插件 - 弹窗录音界面
AI 触发后弹出独立录音窗口
"""
import os
import json
import subprocess
import sys
import time
from typing import Optional, Dict, Any, List
# 结果保存路径
RESULT_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "speech_temp", "last_result.txt")
TEMP_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "speech_temp")
class Liugin:
"""语音识别插件"""
def __init__(self):
self.plugin_dir = os.path.dirname(os.path.abspath(__file__))
self.temp_dir = TEMP_DIR
os.makedirs(self.temp_dir, exist_ok=True)
self.usage = """语音识别插件使用方法:
speech_recognition <操作> [参数]
录音识别:
- record - 弹出录音窗口,录音并识别
文件识别:
- file <音频文件路径> - 识别音频文件内容
其他:
- status - 查看状态
- result - 获取上次识别结果
支持的音频格式: wav, mp3, m4a, flac, ogg
"""
self.cli = None
def set_cli(self, cli):
self.cli = cli
def get_tool_info(self):
return {
"name": "speech_recognition",
"description": "语音识别插件,弹出录音窗口进行语音识别,将语音转换为文字",
"keywords": ["语音", "识别", "录音", "转文字", "speech", "voice", "recognition"],
"usage": self.usage
}
def get_mcp_definition(self):
return {
"name": "speech_recognition",
"description": "语音识别插件,弹出录音窗口进行语音识别",
"inputSchema": {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["record", "file", "status", "result"],
"description": "操作类型"
},
"file_path": {"type": "string", "description": "音频文件路径"}
},
"required": ["operation"]
}
}
def convert_mcp_args(self, arguments: Dict[str, Any]) -> str:
op = arguments.get("operation", "")
parts = [op]
if op == "file":
parts.append(arguments.get("file_path", ""))
return " ".join(filter(None, parts))
def handle(self, args: str) -> str:
try:
parts = self._parse_args(args)
if not parts:
return "错误:请提供操作类型。\n" + self.usage
operation = parts[0].lower()
handlers = {
"record": self._op_record,
"file": self._op_file,
"status": self._op_status,
"result": self._op_result,
}
if operation in handlers:
return handlers[operation](parts[1:])
else:
return f"错误:未知操作 '{operation}'\n可用操作: " + ", ".join(handlers.keys())
except Exception as e:
return f"处理请求失败: {e}"
def _parse_args(self, args: str) -> List[str]:
parts = []
current = ""
in_quotes = False
quote_char = None
for char in args:
if char in ('"', "'") and not in_quotes:
in_quotes = True
quote_char = char
elif char == quote_char and in_quotes:
in_quotes = False
quote_char = None
elif char == ' ' and not in_quotes:
if current:
parts.append(current)
current = ""
else:
current += char
if current:
parts.append(current)
return parts
# ==================== 弹窗录音 ====================
def _op_record(self, args: List[str]) -> str:
"""弹出录音窗口"""
# 创建录音窗口脚本
recorder_script = os.path.join(self.temp_dir, "recorder_gui.py")
gui_code = f'''import tkinter as tk
from tkinter import messagebox
import os
import sys
import threading
import time
RESULT_FILE = r"{RESULT_FILE}"
TEMP_DIR = r"{TEMP_DIR}"
class VoiceRecorder:
def __init__(self):
self.root = tk.Tk()
self.root.title("语音识别录音")
self.root.geometry("450x350")
self.root.resizable(False, False)
# 居中显示
self.root.update_idletasks()
x = (self.root.winfo_screenwidth() - 450) // 2
y = (self.root.winfo_screenheight() - 350) // 2
self.root.geometry(f"450x350+{{x}}+{{y}}")
# 录音状态
self.is_recording = False
self.record_file = None
self.frames = []
self.start_time = None
self.p = None
self.stream = None
# 创建界面
self._create_ui()
# 检查 pyaudio
self.has_pyaudio = self._check_pyaudio()
if not self.has_pyaudio:
self.status_label.config(text="请安装 pyaudio: pip install pyaudio", fg="red")
def _check_pyaudio(self):
try:
import pyaudio
return True
except:
return False
def _create_ui(self):
# 标题
title_label = tk.Label(self.root, text="🎙️ 语音识别录音", font=("Arial", 18, "bold"))
title_label.pack(pady=15)
# 状态标签
self.status_label = tk.Label(self.root, text="点击开始录音", font=("Arial", 11))
self.status_label.pack(pady=5)
# 时间标签
self.time_label = tk.Label(self.root, text="00:00", font=("Arial", 28, "bold"), fg="#2196F3")
self.time_label.pack(pady=10)
# 按钮框架
btn_frame = tk.Frame(self.root)
btn_frame.pack(pady=15)
# 开始/停止按钮
self.record_btn = tk.Button(
btn_frame, text="🎤 开始录音", font=("Arial", 12),
width=12, height=2, command=self.toggle_record,
bg="#4CAF50", fg="white", cursor="hand2"
)
self.record_btn.pack(side=tk.LEFT, padx=10)
# 识别按钮
self.recognize_btn = tk.Button(
btn_frame, text="📝 识别", font=("Arial", 12),
width=12, height=2, command=self.recognize,
bg="#2196F3", fg="white", cursor="hand2", state=tk.DISABLED
)
self.recognize_btn.pack(side=tk.LEFT, padx=10)
# 结果显示
result_frame = tk.LabelFrame(self.root, text="识别结果", font=("Arial", 10))
result_frame.pack(pady=10, padx=15, fill=tk.BOTH, expand=True)
self.result_text = tk.Text(result_frame, height=5, width=50, font=("Arial", 10))
self.result_text.pack(pady=5, padx=5, fill=tk.BOTH, expand=True)
def toggle_record(self):
if not self.has_pyaudio:
messagebox.showerror("错误", "请先安装 pyaudio:\npip install pyaudio")
return
if self.is_recording:
self.stop_record()
else:
self.start_record()
def start_record(self):
import pyaudio
self.is_recording = True
self.frames = []
self.start_time = time.time()
self.record_btn.config(text="⏹️ 停止录音", bg="#f44336")
self.status_label.config(text="🔴 正在录音...", fg="red")
self.recognize_btn.config(state=tk.DISABLED)
CHUNK = 1024
FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 16000
self.p = pyaudio.PyAudio()
self.stream = self.p.open(
format=FORMAT, channels=CHANNELS, rate=RATE,
input=True, frames_per_buffer=CHUNK
)
# 更新时间
self._update_time()
# 录音线程
def record_loop():
while self.is_recording:
try:
data = self.stream.read(CHUNK, exception_on_overflow=False)
self.frames.append(data)
except:
break
self.record_thread = threading.Thread(target=record_loop, daemon=True)
self.record_thread.start()
def stop_record(self):
self.is_recording = False
self.record_btn.config(text="🎤 开始录音", bg="#4CAF50")
self.status_label.config(text="录音完成,可以识别", fg="green")
self.recognize_btn.config(state=tk.NORMAL)
# 停止录音
if self.stream:
self.stream.stop_stream()
self.stream.close()
if self.p:
self.p.terminate()
# 保存文件
import wave
import pyaudio
self.record_file = os.path.join(TEMP_DIR, f"record_{{int(time.time())}}.wav")
wf = wave.open(self.record_file, "wb")
wf.setnchannels(1)
wf.setsampwidth(pyaudio.get_sample_size(pyaudio.paInt16))
wf.setframerate(16000)
wf.writeframes(b"".join(self.frames))
wf.close()
self.result_text.delete(1.0, tk.END)
self.result_text.insert(tk.END, "✅ 录音已保存\n点击 [识别] 按钮进行语音识别")
def _update_time(self):
if self.is_recording:
elapsed = int(time.time() - self.start_time)
mins = elapsed // 60
secs = elapsed % 60
self.time_label.config(text=f"{{mins:02d}}:{{secs:02d}}")
self.root.after(100, self._update_time)
def recognize(self):
if not self.record_file or not os.path.exists(self.record_file):
messagebox.showerror("错误", "没有录音文件,请先录音")
return
self.status_label.config(text="正在识别...", fg="blue")
self.result_text.delete(1.0, tk.END)
self.result_text.insert(tk.END, "🔍 识别中,请稍候...")
self.root.update()
# 尝试识别
result = self._do_recognize()
self.result_text.delete(1.0, tk.END)
if result:
self.result_text.insert(tk.END, result)
self.status_label.config(text="识别完成", fg="green")
# 保存结果
with open(RESULT_FILE, "w", encoding="utf-8") as f:
f.write(result)
else:
self.result_text.insert(tk.END, "❌ 识别失败\n\n请安装识别引擎:\n• pip install openai-whisper\n• pip install SpeechRecognition")
self.status_label.config(text="识别失败", fg="red")
def _do_recognize(self):
# 尝试 Whisper
try:
import whisper
self.status_label.config(text="使用 Whisper 识别...", fg="blue")
self.root.update()
model = whisper.load_model("base")
result = model.transcribe(self.record_file, language="zh")
text = result.get("text", "").strip()
if text:
return f"✅ {{text}}"
except Exception as e:
print(f"Whisper error: {{e}}")
# 尝试 SpeechRecognition
try:
import speech_recognition as sr
self.status_label.config(text="使用 Google 识别...", fg="blue")
self.root.update()
r = sr.Recognizer()
with sr.AudioFile(self.record_file) as source:
audio = r.record(source)
text = r.recognize_google(audio, language="zh-CN")
if text:
return f"✅ {{text}}"
except Exception as e:
print(f"SpeechRecognition error: {{e}}")
return None
def run(self):
self.root.mainloop()
# 清理
if self.is_recording:
self.is_recording = False
if self.stream:
self.stream.close()
if self.p:
self.p.terminate()
if __name__ == "__main__":
app = VoiceRecorder()
app.run()
'''
# 写入脚本
with open(recorder_script, "w", encoding="utf-8") as f:
f.write(gui_code)
# 启动录音窗口
try:
subprocess.Popen([sys.executable, recorder_script],
creationflags=subprocess.CREATE_NEW_CONSOLE)
return "🎙️ 已弹出录音窗口,请在窗口中进行录音和识别\n\n使用方法:\n1. 点击「开始录音」\n2. 对着麦克风说话\n3. 点击「停止录音」\n4. 点击「识别」获取文字结果"
except Exception as e:
return f"❌ 启动录音窗口失败: {e}"
# ==================== 文件识别 ====================
def _op_file(self, args: List[str]) -> str:
"""识别音频文件"""
if not args:
return "错误:请提供音频文件路径"
file_path = args[0]
if not os.path.exists(file_path):
return f"❌ 文件不存在: {file_path}"
return self._recognize_file(file_path)
def _recognize_file(self, file_path: str) -> str:
"""识别音频文件内容"""
# 尝试使用 whisper(本地)
result = self._try_whisper(file_path)
if result:
self._save_result(result)
return result
# 尝试使用 speech_recognition 库
result = self._try_speech_recognition(file_path)
if result:
self._save_result(result)
return result
return "❌ 语音识别失败:未安装识别引擎\n\n安装方法:\n1. Whisper (推荐): pip install openai-whisper\n2. SpeechRecognition: pip install SpeechRecognition\n3. PyAudio (录音): pip install pyaudio"
def _save_result(self, result: str):
"""保存识别结果"""
try:
with open(RESULT_FILE, "w", encoding="utf-8") as f:
f.write(result)
except:
pass
def _try_whisper(self, file_path: str) -> Optional[str]:
"""使用 Whisper 识别"""
try:
import whisper
print(f"[语音识别] 使用 Whisper 识别中...")
model = whisper.load_model("base")
result = model.transcribe(file_path, language="zh")
text = result.get("text", "").strip()
if text:
return f"✅ 识别结果:\n{text}"
return None
except ImportError:
return None
except Exception as e:
print(f"[语音识别] Whisper 错误: {e}")
return None
def _try_speech_recognition(self, file_path: str) -> Optional[str]:
"""使用 SpeechRecognition 库识别"""
wav_file = file_path
try:
import speech_recognition as sr
print(f"[语音识别] 使用 SpeechRecognition 识别中...")
r = sr.Recognizer()
# 转换为 wav 格式
wav_file = self._convert_to_wav(file_path)
with sr.AudioFile(wav_file) as source:
audio = r.record(source)
# 尝试 Google 语音识别
try:
text = r.recognize_google(audio, language="zh-CN")
if text:
return f"✅ 识别结果:\n{text}"
except:
pass
return None
except ImportError:
return None
except Exception as e:
print(f"[语音识别] SpeechRecognition 错误: {e}")
return None
finally:
# 清理转换的临时文件
if wav_file != file_path and os.path.exists(wav_file):
try:
os.remove(wav_file)
except:
pass
def _convert_to_wav(self, file_path: str) -> str:
"""转换音频为 wav 格式"""
if file_path.lower().endswith('.wav'):
return file_path
try:
wav_file = os.path.join(self.temp_dir, f"convert_{int(time.time())}.wav")
# 使用 ffmpeg 转换
subprocess.run([
'ffmpeg', '-y', '-i', file_path,
'-acodec', 'pcm_s16le',
'-ar', '16000',
'-ac', '1',
wav_file
], capture_output=True, check=True)
return wav_file
except Exception as e:
print(f"[语音识别] 转换错误: {e}")
return file_path
# ==================== 其他 ====================
def _op_status(self, args: List[str]) -> str:
"""查看状态"""
lines = ["📊 语音识别插件状态"]
# 检查可用引擎
engines = []
try:
import whisper
engines.append("whisper ✅")
except:
engines.append("whisper ❌ (pip install openai-whisper)")
try:
import speech_recognition
engines.append("speech_recognition ✅")
except:
engines.append("speech_recognition ❌ (pip install SpeechRecognition)")
try:
import pyaudio
engines.append("pyaudio ✅ (录音支持)")
except:
engines.append("pyaudio ❌ (pip install pyaudio)")
lines.append("\n可用引擎:")
for e in engines:
lines.append(f" - {e}")
return "\n".join(lines)
def _op_result(self, args: List[str]) -> str:
"""获取上次识别结果"""
if os.path.exists(RESULT_FILE):
try:
with open(RESULT_FILE, "r", encoding="utf-8") as f:
return f.read()
except:
pass
return "暂无识别结果"
if __name__ == "__main__":
plugin = Plugin()
print("语音识别插件")
print(f"临时目录: {plugin.temp_dir}")
print("\n" + plugin.usage)
+263
View File
@@ -0,0 +1,263 @@
"""
任务管理插件 - 提供任务追踪和进度管理功能
"""
import json
import os
from datetime import datetime
from colorama import Fore, Style
class Liugin:
"""任务管理插件 - 类似 iFlow CLI 的 todo 功能"""
def __init__(self):
self.usage = """任务管理工具使用方法:
JSON格式示例:
{"action": "use_tool", "tool": "task_manager", "args": "add 创建登录页面"} - 添加新任务
{"action": "use_tool", "tool": "task_manager", "args": "list"} - 列出所有任务
{"action": "use_tool", "tool": "task_manager", "args": "complete 1"} - 完成任务ID为1的任务
{"action": "use_tool", "tool": "task_manager", "args": "in_progress 2"} - 将任务ID为2标记为进行中
{"action": "use_tool", "tool": "task_manager", "args": "delete 3"} - 删除任务ID为3的任务
{"action": "use_tool", "tool": "task_manager", "args": "clear"} - 清除所有已完成的任务
功能说明:
- add <任务描述> - 添加新任务(自动分配ID)
- list - 列出所有任务及其状态
- complete <任务ID> - 标记任务为已完成
- in_progress <任务ID> - 标记任务为进行中
- delete <任务ID> - 删除指定任务
- clear - 清除所有已完成的任务
状态说明:
- pending: 待处理
- in_progress: 进行中
- completed: 已完成
- failed: 失败"""
self.cli = None
self.tasks_file = "tasks.json"
self.tasks = []
self._load_tasks()
def set_cli(self, cli):
"""设置CLI实例引用"""
self.cli = cli
# 注册插件命令
self.cli.register_liugin_command('task', self.command_handler)
self.cli.register_liugin_command('todo', self.command_handler)
def command_handler(self, args):
"""处理 /task 或 /todo 命令"""
return self.handle(args)
def get_tool_info(self):
return {
"name": "task_manager",
"description": "任务管理工具,提供任务追踪和进度管理功能,支持添加、完成、删除任务",
"keywords": ["任务", "task", "todo", "管理", "进度", "追踪", "list", "complete"],
"usage": self.usage
}
def _load_tasks(self):
"""从文件加载任务"""
try:
if os.path.exists(self.tasks_file):
with open(self.tasks_file, 'r', encoding='utf-8') as f:
self.tasks = json.load(f)
except Exception as e:
self.tasks = []
def _save_tasks(self):
"""保存任务到文件"""
try:
with open(self.tasks_file, 'w', encoding='utf-8') as f:
json.dump(self.tasks, f, ensure_ascii=False, indent=2)
except Exception as e:
return f"保存任务失败: {e}"
return None
def handle(self, args):
"""处理任务管理请求"""
try:
parts = args.strip().split()
if not parts:
return self._list_tasks()
operation = parts[0].lower()
if operation == "add":
# 添加任务
if len(parts) < 2:
return "错误:请提供任务描述。格式: add <任务描述>"
task_desc = " ".join(parts[1:])
return self._add_task(task_desc)
elif operation == "list":
# 列出任务
return self._list_tasks()
elif operation == "complete":
# 完成任务
if len(parts) < 2:
return "错误:请提供任务ID。格式: complete <任务ID>"
task_id = parts[1]
return self._update_task_status(task_id, "completed")
elif operation == "in_progress":
# 标记为进行中
if len(parts) < 2:
return "错误:请提供任务ID。格式: in_progress <任务ID>"
task_id = parts[1]
return self._update_task_status(task_id, "in_progress")
elif operation == "delete":
# 删除任务
if len(parts) < 2:
return "错误:请提供任务ID。格式: delete <任务ID>"
task_id = parts[1]
return self._delete_task(task_id)
elif operation == "clear":
# 清除已完成任务
return self._clear_completed()
else:
return f"错误:不支持的操作 '{operation}'。支持的操作有: add, list, complete, in_progress, delete, clear"
except Exception as e:
return f"任务管理操作错误: {str(e)}"
def _add_task(self, description):
"""添加新任务"""
# 使用最大ID+1的方式分配ID,避免删除后ID重复
existing_ids = [int(t.get("id", 0)) for t in self.tasks if t.get("id", "").isdigit()]
task_id = str(max(existing_ids, default=0) + 1)
new_task = {
"id": task_id,
"task": description,
"status": "pending",
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"priority": "medium"
}
self.tasks.append(new_task)
error = self._save_tasks()
if error:
return error
return f"✅ 已添加任务 #{task_id}: {description}"
def _list_tasks(self):
"""列出所有任务"""
if not self.tasks:
return "📝 当前没有任务"
result = f"\n📋 任务列表 (共 {len(self.tasks)} 个任务)\n"
result += "=" * 50 + "\n"
pending_count = 0
in_progress_count = 0
completed_count = 0
for task in self.tasks:
status = task.get('status', 'pending')
task_id = task.get('id', '?')
description = task.get('task', '')
created_at = task.get('created_at', '')
# 统计
if status == 'pending':
pending_count += 1
status_icon = "⏳"
status_color = Fore.YELLOW
elif status == 'in_progress':
in_progress_count += 1
status_icon = "🔄"
status_color = Fore.CYAN
elif status == 'completed':
completed_count += 1
status_icon = "✅"
status_color = Fore.GREEN
else:
status_icon = "❌"
status_color = Fore.RED
result += f"{status_icon} [{task_id}] {status_color}{status}{Style.RESET_ALL} - {description}\n"
result += f" 创建时间: {created_at}\n\n"
result += "=" * 50 + "\n"
result += f"📊 统计: {Fore.YELLOW}待处理 {pending_count}{Style.RESET_ALL} | {Fore.CYAN}进行中 {in_progress_count}{Style.RESET_ALL} | {Fore.GREEN}已完成 {completed_count}{Style.RESET_ALL}\n"
return result
def _update_task_status(self, task_id, new_status):
"""更新任务状态"""
for task in self.tasks:
if task.get('id') == task_id:
old_status = task.get('status')
task['status'] = new_status
if new_status == 'completed':
task['completed_at'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
error = self._save_tasks()
if error:
return error
return f"✅ 任务 #{task_id} 状态已更新: {old_status} → {new_status}"
return f"❌ 未找到任务ID: {task_id}"
def _delete_task(self, task_id):
"""删除任务"""
for i, task in enumerate(self.tasks):
if task.get('id') == task_id:
deleted_task = self.tasks.pop(i)
error = self._save_tasks()
if error:
return error
return f"🗑️ 已删除任务 #{task_id}: {deleted_task.get('task', '')}"
return f"❌ 未找到任务ID: {task_id}"
def _clear_completed(self):
"""清除所有已完成的任务"""
completed_count = len([t for t in self.tasks if t.get('status') == 'completed'])
if completed_count == 0:
return "📝 没有已完成的任务需要清除"
self.tasks = [t for t in self.tasks if t.get('status') != 'completed']
error = self._save_tasks()
if error:
return error
return f"✅ 已清除 {completed_count} 个已完成的任务"
# 测试函数
def test_plugin():
"""测试插件功能"""
plugin = Plugin()
print("任务管理插件测试:")
print("\n1. 添加任务:")
print(plugin.handle("add 创建登录页面"))
print(plugin.handle("add 实现用户认证"))
print(plugin.handle("add 添加数据库连接"))
print("\n2. 列出任务:")
print(plugin.handle("list"))
print("\n3. 标记任务为进行中:")
print(plugin.handle("in_progress 1"))
print("\n4. 完成任务:")
print(plugin.handle("complete 1"))
print("\n5. 再次列出任务:")
print(plugin.handle("list"))
print("\n6. 清除已完成任务:")
print(plugin.handle("clear"))
print("\n7. 最终任务列表:")
print(plugin.handle("list"))
if __name__ == "__main__":
test_plugin()
+107
View File
@@ -0,0 +1,107 @@
"""
工具搜索插件 - 支持按需发现工具
"""
import json
class Liugin:
"""工具搜索插件 - 允许 AI 按需搜索和发现可用工具"""
def __init__(self):
self.usage = """工具搜索使用方法:
tool_search <关键词> - 根据关键词搜索可用工具
例如:
- tool_search 文件 - 搜索与文件相关的工具
- tool_search 网络 - 搜索与网络相关的工具
- tool_search 图片 - 搜索与图片相关的工具
此插件帮助 AI 在需要特定功能时快速找到合适的工具,
而不是预加载所有工具到上下文中。"""
self.cli = None
def set_cli(self, cli):
"""设置 CLI 实例引用"""
self.cli = cli
def get_tool_info(self):
return {
"name": "tool_search",
"description": "工具搜索插件,支持按需发现和搜索可用工具。当你不确定有哪些工具可用时,可以使用此工具搜索相关功能的工具。",
"keywords": ["搜索", "工具", "发现", "search", "tool", "find", "可用工具", "工具列表"],
"usage": """tool_search 工具使用说明:
JSON格式示例:
{"action": "use_tool", "tool": "tool_search", "args": "文件"} - 搜索与文件相关的工具
{"action": "use_tool", "tool": "tool_search", "args": "网络"} - 搜索与网络相关的工具
{"action": "use_tool", "tool": "tool_search", "args": "图片"} - 搜索与图片相关的工具
功能说明:
- 支持关键词匹配(工具名称、描述、关键词)
- 返回匹配度最高的前 5 个工具
- 包含工具的详细使用说明
- 帮助 AI 快速找到合适的工具而不需要预加载所有工具"""
}
def handle(self, args):
"""处理工具搜索请求"""
try:
# 解析参数
query = args.strip()
if not query:
return "错误:请提供搜索关键词。格式: tool_search <关键词>"
# 搜索工具
if self.cli and hasattr(self.cli, 'liugin_manager'):
matches = self.cli.liugin_manager.search_tools(query, limit=5)
if not matches:
return f"未找到与 '{query}' 相关的工具。"
# 格式化搜索结果
result_lines = [f"找到 {len(matches)} 个与 '{query}' 相关的工具:\n"]
for i, match in enumerate(matches, 1):
result_lines.append(f"{i}. {match['name']}")
result_lines.append(f" 描述: {match['description']}")
# 显示协议类型
protocol = match.get('protocol', 'liugin')
result_lines.append(f" 协议: {protocol}")
# 关键词
keywords = match.get('keywords', [])
if keywords:
result_lines.append(f" 关键词: {', '.join(keywords)}")
# 根据协议类型显示不同的使用说明
if protocol == 'skill':
# Skill 协议:显示完整指令
instance = match.get('instance')
if instance:
instructions = getattr(instance, 'instructions', '')
if instructions:
# 限制长度
if len(instructions) > 1500:
instructions = instructions[:1500] + "\n...(内容已截断)"
result_lines.append(f" 指令:\n{instructions}")
else:
usage = match.get('usage', '')
if usage:
result_lines.append(f" 用法: {usage}")
else:
# Plugin 协议:显示 usage
if match.get('usage'):
usage = match['usage']
if len(usage) > 500:
usage = usage[:500] + "..."
result_lines.append(f" 用法: {usage}")
result_lines.append("")
return "\n".join(result_lines)
else:
return "错误:无法访问插件管理器。"
except Exception as e:
return f"工具搜索失败: {str(e)}"
+16
View File
@@ -0,0 +1,16 @@
# 小狸插件配置
# 自动生成于: 2026-04-25
ENABLE_AI_SEARCH = True
ENABLE_AUDIO_PLAYER = True
ENABLE_CMD_EXECUTOR = True
ENABLE_FILE_MANAGER = True
ENABLE_CODE_EDITOR = True
ENABLE_CODE_SEARCH = True
ENABLE_GIT_TOOLS = True
ENABLE_FRONTEND_TESTER = True
ENABLE_NETWORK_TOOLS = True
ENABLE_SCHEDULER = True
ENABLE_SPEECH_RECOGNITION = True
ENABLE_TASK_MANAGER = True
ENABLE_TOOL_SEARCH = True
+30
View File
@@ -0,0 +1,30 @@
# 小狸 Pro-CLI 依赖库
# 修复后的包名(使用 pip 安装名)
# 核心依赖
colorama>=0.4.6
openai>=1.0.0
requests>=2.28.0
ollama>=0.1.0
# 图像处理
Pillow>=9.0.0
opencv-python>=4.5.0
numpy>=1.21.0
ascii-magic>=2.0.0
# 数据处理
pandas>=1.3.0
scipy>=1.7.0
# 音频
pygame>=2.0.0
# TUI 模式
textual>=0.40.0
# WebSocket 远程
websockets>=11.0
# 类型提示
typing-extensions>=4.0.0
+28
View File
@@ -0,0 +1,28 @@
"""
Agent Skills 协议实现
支持两种格式:
1. 标准开源格式: SKILL.md (YAML frontmatter + Markdown)
2. Python 类格式: Skill 子类 (用于需要代码执行的技能)
"""
from .base import (
# 基础类
Skill,
SkillResult,
SkillParameter,
ParameterType,
SkillLoader,
# Markdown 技能
MarkdownSkill,
)
__all__ = [
'Skill',
'SkillResult',
'SkillParameter',
'ParameterType',
'SkillLoader',
'MarkdownSkill',
]
__version__ = '1.0.0'
+396
View File
@@ -0,0 +1,396 @@
"""
Agent Skills 协议基类
支持两种格式:
1. 标准开源格式: SKILL.md (YAML frontmatter + Markdown)
2. Python 类格式: Skill 子类 (用于需要代码执行的技能)
"""
import os
import re
import json
import threading
import importlib
import inspect
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional, List, Dict, Any, Union
class ParameterType(Enum):
"""参数类型枚举 - 映射到 JSON Schema"""
STRING = "string"
INTEGER = "integer"
NUMBER = "number"
BOOLEAN = "boolean"
ARRAY = "array"
OBJECT = "object"
@dataclass
class SkillParameter:
"""技能参数定义"""
name: str
param_type: ParameterType
description: str
required: bool = True
default: Any = None
enum: Optional[List[Any]] = None
def to_json_schema(self) -> Dict[str, Any]:
"""转换为 JSON Schema 格式"""
schema = {
"type": self.param_type.value,
"description": self.description
}
if self.enum:
schema["enum"] = self.enum
return schema
@dataclass
class SkillResult:
"""技能执行结果"""
success: bool
data: Any = None
message: str = ""
error: Optional[str] = None
metadata: Dict[str, Any] = field(default_factory=dict)
def __str__(self) -> str:
if self.success:
return self.message or str(self.data)
return f"Error: {self.error}"
def to_dict(self) -> Dict[str, Any]:
return {
"success": self.success,
"data": self.data,
"message": self.message,
"error": self.error,
"metadata": self.metadata
}
class Skill(ABC):
"""技能基类 - 用于 Python 实现的技能"""
def __init__(self):
self.name: str = ""
self.description: str = ""
self.version: str = "1.0.0"
self.author: str = ""
self.tags: List[str] = []
self._cli = None
@property
@abstractmethod
def parameters(self) -> List[SkillParameter]:
pass
@abstractmethod
def execute(self, **kwargs) -> SkillResult:
pass
def set_cli(self, cli) -> None:
self._cli = cli
def get_tool_definition(self) -> Dict[str, Any]:
"""获取 OpenAI 格式的工具定义"""
properties = {}
required = []
for param in self.parameters:
properties[param.name] = param.to_json_schema()
if param.required:
required.append(param.name)
return {
"type": "function",
"function": {
"name": self.name,
"description": self.description,
"parameters": {
"type": "object",
"properties": properties,
"required": required
}
}
}
def get_anthropic_tool_definition(self) -> Dict[str, Any]:
"""获取 Anthropic 格式的工具定义"""
properties = {}
required = []
for param in self.parameters:
properties[param.name] = param.to_json_schema()
if param.required:
required.append(param.name)
return {
"name": self.name,
"description": self.description,
"input_schema": {
"type": "object",
"properties": properties,
"required": required
}
}
def run(self, **kwargs) -> SkillResult:
"""执行技能(带参数验证)"""
error = self._validate_parameters(**kwargs)
if error:
return SkillResult(success=False, error=error)
return self.execute(**kwargs)
def _validate_parameters(self, **kwargs) -> Optional[str]:
"""验证参数"""
for param in self.parameters:
if param.required and param.name not in kwargs:
return f"缺少必需参数: {param.name}"
return None
@dataclass
class MarkdownSkill:
"""Markdown 格式的技能 (标准 Agent Skills 格式)"""
name: str
description: str
version: str = "1.0.0"
author: str = ""
license: str = "MIT"
tools: List[str] = field(default_factory=list)
instructions: str = ""
file_path: str = ""
scripts_dir: str = ""
def get_tool_definition(self) -> Dict[str, Any]:
"""获取 OpenAI 格式的工具定义"""
return {
"type": "function",
"function": {
"name": self.name,
"description": self.description,
"parameters": {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["list", "read", "write", "delete", "copy", "move", "info", "search"],
"description": "要执行的文件操作类型"
},
"path": {
"type": "string",
"description": "目标文件或目录路径"
},
"content": {
"type": "string",
"description": "写入内容(write/append 操作时使用)"
},
"destination": {
"type": "string",
"description": "目标路径(copy/move 操作时使用)"
}
},
"required": ["operation"]
}
}
}
def get_anthropic_tool_definition(self) -> Dict[str, Any]:
"""获取 Anthropic 格式的工具定义"""
return {
"name": self.name,
"description": self.description,
"input_schema": {
"type": "object",
"properties": {
"operation": {"type": "string"},
"path": {"type": "string"},
"content": {"type": "string"},
"destination": {"type": "string"}
},
"required": ["operation"]
}
}
class SkillLoader:
"""技能加载器 - 支持标准 SKILL.md 和 Python 类两种格式"""
def __init__(self, skills_dir: str = None):
self.skills_dir = skills_dir
self._skills: Dict[str, Union[Skill, MarkdownSkill]] = {}
self._lock = threading.RLock()
@property
def skills(self) -> Dict[str, Union[Skill, MarkdownSkill]]:
return self._skills.copy()
def discover_skills(self, skills_dir: str = None) -> List[Dict[str, str]]:
"""发现所有技能"""
search_dir = skills_dir or self.skills_dir
if not search_dir or not os.path.isdir(search_dir):
return []
discovered = []
for item in os.listdir(search_dir):
item_path = os.path.join(search_dir, item)
# 跳过特殊文件
if item in ('__init__.py', 'base.py') or item.startswith('_') or item.startswith('.'):
continue
# SKILL.md 格式 (目录)
skill_md = os.path.join(item_path, "SKILL.md")
if os.path.isdir(item_path) and os.path.isfile(skill_md):
discovered.append({"name": item, "type": "markdown", "path": skill_md})
# Python 单文件格式
elif item.endswith('.py') and os.path.isfile(item_path):
discovered.append({"name": item[:-3], "type": "python", "path": item_path})
return discovered
def parse_skill_md(self, file_path: str) -> Optional[MarkdownSkill]:
"""解析 SKILL.md 文件"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# 解析 YAML frontmatter
if not content.startswith('---'):
return None
end_idx = content.find('---', 3)
if end_idx == -1:
return None
frontmatter = content[3:end_idx].strip()
instructions = content[end_idx + 3:].strip()
# 解析 YAML (简化版)
metadata = {}
current_list = None
current_key = None
for line in frontmatter.split('\n'):
stripped = line.strip()
if not stripped:
continue
# 列表项
if stripped.startswith('- '):
if current_key and current_key not in metadata:
metadata[current_key] = []
if current_key and isinstance(metadata.get(current_key), list):
metadata[current_key].append(stripped[2:])
continue
# 键值对
if ':' in line:
current_key = None
colon_idx = line.index(':')
key = line[:colon_idx].strip()
value = line[colon_idx + 1:].strip()
if value:
# 处理列表格式 [a, b, c]
if value.startswith('[') and value.endswith(']'):
value = [v.strip() for v in value[1:-1].split(',') if v.strip()]
metadata[key] = value
else:
current_key = key
current_list = []
skill_dir = os.path.dirname(file_path)
scripts_dir = os.path.join(skill_dir, "scripts")
# 解析参数定义 (从 frontmatter 中的 parameters 字段)
parameters = {}
if "parameters" in metadata and isinstance(metadata["parameters"], dict):
parameters = metadata["parameters"]
elif "params" in metadata and isinstance(metadata["params"], dict):
parameters = metadata["params"]
return MarkdownSkill(
name=metadata.get('name', os.path.basename(skill_dir)),
description=metadata.get('description', ''),
version=metadata.get('version', '1.0.0'),
author=metadata.get('author', ''),
license=metadata.get('license', 'MIT'),
tools=metadata.get('tools', []) if isinstance(metadata.get('tools'), list) else [],
instructions=instructions,
file_path=file_path,
scripts_dir=scripts_dir if os.path.isdir(scripts_dir) else ""
)
except Exception as e:
print(f"解析 SKILL.md 失败 ({file_path}): {e}")
return None
def load_skill(self, skill_info: Dict[str, str]) -> Optional[Union[Skill, MarkdownSkill]]:
"""加载单个技能"""
skill_type = skill_info.get('type')
skill_path = skill_info.get('path')
skill_name = skill_info.get('name')
if skill_type == "markdown":
skill = self.parse_skill_md(skill_path)
if skill:
self._skills[skill.name] = skill
return skill
elif skill_type == "python":
try:
# 动态导入 Python 模块
spec = importlib.util.spec_from_file_location(skill_name, skill_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
# 查找 Skill 子类
for name, obj in inspect.getmembers(module, inspect.isclass):
if issubclass(obj, Skill) and obj is not Skill:
skill = obj()
self._skills[skill.name] = skill
return skill
except Exception as e:
print(f"加载 Python 技能 '{skill_name}' 失败: {e}")
return None
def load_all_skills(self, skills_dir: str = None) -> Dict[str, Union[Skill, MarkdownSkill]]:
"""加载所有技能"""
discovered = self.discover_skills(skills_dir)
for skill_info in discovered:
self.load_skill(skill_info)
return self.skills
def get_skill(self, name: str) -> Optional[Union[Skill, MarkdownSkill]]:
"""获取已加载的技能"""
return self._skills.get(name)
def get_all_tool_definitions(self, format: str = "openai") -> List[Dict]:
"""获取所有技能的工具定义"""
definitions = []
for skill in self._skills.values():
if format == "anthropic":
definitions.append(skill.get_anthropic_tool_definition())
else:
definitions.append(skill.get_tool_definition())
return definitions
def get_skill(self, name: str) -> Optional[Union[Skill, MarkdownSkill]]:
"""获取已加载的技能"""
return self._skills.get(name)
def get_all_tool_definitions(self, format: str = "openai") -> List[Dict]:
"""获取所有技能的工具定义"""
definitions = []
for skill in self._skills.values():
if format == "anthropic":
definitions.append(skill.get_anthropic_tool_definition())
else:
definitions.append(skill.get_tool_definition())
return definitions
+305
View File
@@ -0,0 +1,305 @@
---
name: file-manager
description: File and directory management tool for creating, reading, writing, deleting, moving, copying, and searching files. Use this skill when the user needs to perform file operations, list directories, search by pattern or content, or get file metadata like size and modification time.
license: MIT
version: 1.0.0
author: Xiaoli Team (based on openakita/openakita)
tools:
- read_file
- write_file
- bash
---
# File Manager Skill
A comprehensive file and directory management toolset for AI agents.
## Capabilities
- **Create**: Files and directories
- **Read**: File contents with encoding support
- **Write**: Create or overwrite files
- **Append**: Add content to existing files
- **Delete**: Remove files or directories
- **Move**: Move or rename files and directories
- **Copy**: Duplicate files or directories
- **Search**: Find files by pattern or content
- **List**: Browse directory contents
- **Info**: Get file metadata (size, dates, permissions)
## Script Usage
This skill includes a Python script for structured JSON output:
```bash
python scripts/file_ops.py list <path> [--recursive] [--pattern "*.py"]
python scripts/file_ops.py read <file_path> [--encoding utf-8] [--lines N]
python scripts/file_ops.py write <file_path> --content "内容" [--append]
python scripts/file_ops.py copy <source> <destination>
python scripts/file_ops.py move <source> <destination>
python scripts/file_ops.py delete <path> [--recursive]
python scripts/file_ops.py search <directory> --pattern "*.py" [--content "搜索词"]
python scripts/file_ops.py info <path>
```
## Direct Shell Commands
### List Directory Contents
When asked to show files in a directory:
```bash
# List current directory
ls -la
# List specific directory
ls -la /path/to/directory
# Recursive listing (Unix)
find /path -type f -name "*.py"
# Windows
dir /s /b C:\path\*.py
```
### Read File
When asked to read a file:
```bash
# Read entire file
cat filename.txt
# Read with line numbers
cat -n filename.txt
# Read first N lines
head -n 50 filename.txt
# Read last N lines
tail -n 50 filename.txt
```
### Write File
When asked to create or write a file:
```bash
# Write content to file (overwrites)
echo "content" > filename.txt
# Write multiple lines
cat > filename.txt << 'EOF'
Line 1
Line 2
Line 3
EOF
# Windows PowerShell
Set-Content -Path filename.txt -Value "content"
```
### Append to File
When asked to add content to an existing file:
```bash
# Append to file
echo "new line" >> filename.txt
# Append multiple lines
cat >> filename.txt << 'EOF'
Additional line 1
Additional line 2
EOF
```
### Create Directory
When asked to create a folder:
```bash
# Create single directory
mkdir directory_name
# Create nested directories
mkdir -p path/to/nested/directories
# Windows
mkdir path\to\nested\directories
```
### Delete File or Directory
When asked to delete:
```bash
# Delete a file
rm filename.txt
# Delete empty directory
rmdir directory_name
# Delete directory and contents
rm -rf directory_name
# Windows
del filename.txt
rmdir /s /q directory_name
```
⚠️ **Warning**: Deletion is permanent. Always confirm before deleting.
### Copy File or Directory
When asked to copy:
```bash
# Copy file
cp source.txt destination.txt
# Copy directory recursively
cp -r source_dir/ destination_dir/
# Windows
copy source.txt destination.txt
xcopy /E /I source_dir destination_dir
```
### Move or Rename
When asked to move or rename:
```bash
# Move file
mv source.txt /new/location/source.txt
# Rename file
mv oldname.txt newname.txt
# Windows
move source.txt destination.txt
ren oldname.txt newname.txt
```
### Search Files
When asked to find files:
```bash
# Find by name pattern
find /path -name "*.py"
find /path -iname "*.PY" # case-insensitive
# Find by content (grep)
grep -r "search_term" /path
grep -rl "search_term" /path # filenames only
# Windows PowerShell
Get-ChildItem -Path C:\path -Filter *.py -Recurse
Select-String -Path C:\path\*.py -Pattern "search_term"
```
### Get File Information
When asked about file details:
```bash
# File metadata (Unix)
ls -la filename.txt
stat filename.txt
# File size only
du -h filename.txt
# Windows
dir filename.txt
```
## JSON Output Format
All operations should return structured results:
```json
{
"success": true,
"operation": "list",
"path": "/home/user/project",
"data": {
"files": ["main.py", "config.json", "README.md"],
"directories": ["src", "tests", "docs"],
"total_count": 6
},
"timestamp": "2026-03-21T10:30:00Z"
}
```
## Safety Rules
1. **Always validate paths**: Reject paths containing `..` that escape the working directory
2. **Confirm destructive operations**: Ask before delete, move, or overwrite
3. **Handle encoding**: Default to UTF-8, detect and handle other encodings
4. **Large file warning**: Alert for files > 10MB
5. **Binary file detection**: Cannot read binary files as text
## Protected Directories
Never allow operations on:
- System directories: `/etc`, `/root`, `C:\Windows`, `C:\Program Files`
- Hidden files starting with `.` (unless explicitly requested)
- Files with sensitive names: `.env`, `credentials`, `secrets`
## Error Handling
| Error | Response |
|-------|----------|
| File not found | Suggest similar filenames or check path |
| Permission denied | Explain required permissions |
| Disk full | Warn user and suggest cleanup |
| Invalid path | Show correct path format |
## Examples
### Example 1: List Project Files
User: "Show me the project structure"
```bash
find . -type f -name "*.py" | head -20
tree -L 2 -I '__pycache__|*.pyc'
```
### Example 2: Search and Replace
User: "Replace 'old_func' with 'new_func' in all Python files"
```bash
# Find and preview
grep -r "old_func" --include="*.py" .
# Replace (with backup)
find . -name "*.py" -exec sed -i.bak 's/old_func/new_func/g' {} \;
```
### Example 3: Safe Delete
User: "Delete the temp folder"
Before executing:
1. Check if path exists
2. List contents to show what will be deleted
3. Ask for confirmation
4. Execute if confirmed
## Cross-Platform Notes
| Operation | Unix | Windows |
|-----------|------|---------|
| List | `ls -la` | `dir` |
| Copy | `cp` | `copy` |
| Move | `mv` | `move` |
| Delete | `rm -rf` | `rmdir /s /q` |
| Find | `find` | `Get-ChildItem` |
| Grep | `grep` | `Select-String` |
Always detect the OS and use appropriate commands.
+670
View File
@@ -0,0 +1,670 @@
#!/usr/bin/env python3
"""
File Operations Script for file-manager skill
Provides file system operations as JSON output
"""
import os
import sys
import json
import shutil
import argparse
from datetime import datetime
from pathlib import Path
from typing import Optional, Dict, Any, List
def json_response(success: bool, operation: str, data: Any = None, error: str = None) -> str:
"""Generate JSON response"""
return json.dumps({
"success": success,
"operation": operation,
"data": data,
"error": error,
"timestamp": datetime.now().isoformat()
}, ensure_ascii=False, indent=2)
def list_directory(path: str, recursive: bool = False, pattern: str = None) -> str:
"""List directory contents"""
try:
path = os.path.abspath(path)
if not os.path.exists(path):
return json_response(False, "list", error=f"Path not found: {path}")
if not os.path.isdir(path):
return json_response(False, "list", error=f"Not a directory: {path}")
files = []
directories = []
if recursive:
for root, dirs, filenames in os.walk(path):
for d in dirs:
rel_path = os.path.relpath(os.path.join(root, d), path)
if pattern is None or Path(d).match(pattern):
directories.append(rel_path)
for f in filenames:
rel_path = os.path.relpath(os.path.join(root, f), path)
if pattern is None or Path(f).match(pattern):
files.append(rel_path)
else:
for item in os.listdir(path):
full_path = os.path.join(path, item)
if os.path.isdir(full_path):
if pattern is None or Path(item).match(pattern):
directories.append(item)
else:
if pattern is None or Path(item).match(pattern):
files.append(item)
return json_response(True, "list", {
"path": path,
"files": sorted(files),
"directories": sorted(directories),
"file_count": len(files),
"dir_count": len(directories),
"total_count": len(files) + len(directories)
})
except Exception as e:
return json_response(False, "list", error=str(e))
def read_file(file_path: str, encoding: str = "utf-8", lines: int = None) -> str:
"""Read file content"""
try:
file_path = os.path.abspath(file_path)
if not os.path.exists(file_path):
return json_response(False, "read", error=f"File not found: {file_path}")
if not os.path.isfile(file_path):
return json_response(False, "read", error=f"Not a file: {file_path}")
file_size = os.path.getsize(file_path)
with open(file_path, 'r', encoding=encoding) as f:
if lines:
content_lines = []
for i, line in enumerate(f):
if i >= lines:
break
content_lines.append(line.rstrip('\n\r'))
content = '\n'.join(content_lines)
total_lines = sum(1 for _ in open(file_path, 'r', encoding=encoding))
else:
content = f.read()
total_lines = content.count('\n') + 1
return json_response(True, "read", {
"path": file_path,
"content": content,
"size": file_size,
"lines": total_lines,
"displayed_lines": lines if lines else total_lines
})
except UnicodeDecodeError:
return json_response(False, "read", error="Cannot decode file (binary or wrong encoding)")
except Exception as e:
return json_response(False, "read", error=str(e))
def write_file(file_path: str, content: str, append: bool = False) -> str:
"""Write or append to file"""
try:
file_path = os.path.abspath(file_path)
# Create parent directory if needed
parent = os.path.dirname(file_path)
if parent and not os.path.exists(parent):
os.makedirs(parent, exist_ok=True)
mode = 'a' if append else 'w'
with open(file_path, mode, encoding='utf-8') as f:
f.write(content)
return json_response(True, "write" if not append else "append", {
"path": file_path,
"size": len(content.encode('utf-8')),
"appended": append
})
except Exception as e:
return json_response(False, "write", error=str(e))
def copy_path(source: str, destination: str) -> str:
"""Copy file or directory"""
try:
source = os.path.abspath(source)
destination = os.path.abspath(destination)
if not os.path.exists(source):
return json_response(False, "copy", error=f"Source not found: {source}")
# Create parent directory for destination
parent = os.path.dirname(destination)
if parent and not os.path.exists(parent):
os.makedirs(parent, exist_ok=True)
if os.path.isfile(source):
shutil.copy2(source, destination)
else:
shutil.copytree(source, destination, dirs_exist_ok=True)
return json_response(True, "copy", {
"source": source,
"destination": destination,
"type": "file" if os.path.isfile(source) else "directory"
})
except Exception as e:
return json_response(False, "copy", error=str(e))
def move_path(source: str, destination: str) -> str:
"""Move or rename file or directory"""
try:
source = os.path.abspath(source)
destination = os.path.abspath(destination)
if not os.path.exists(source):
return json_response(False, "move", error=f"Source not found: {source}")
# Create parent directory for destination
parent = os.path.dirname(destination)
if parent and not os.path.exists(parent):
os.makedirs(parent, exist_ok=True)
shutil.move(source, destination)
return json_response(True, "move", {
"source": source,
"destination": destination
})
except Exception as e:
return json_response(False, "move", error=str(e))
def delete_path(path: str, recursive: bool = False) -> str:
"""Delete file or directory"""
try:
path = os.path.abspath(path)
if not os.path.exists(path):
return json_response(False, "delete", error=f"Path not found: {path}")
if os.path.isfile(path):
os.remove(path)
item_type = "file"
elif os.path.isdir(path):
if recursive:
shutil.rmtree(path)
else:
os.rmdir(path) # Only works for empty directories
item_type = "directory"
return json_response(True, "delete", {
"path": path,
"type": item_type
})
except OSError as e:
if "not empty" in str(e).lower():
return json_response(False, "delete", error="Directory not empty. Use --recursive to delete.")
return json_response(False, "delete", error=str(e))
except Exception as e:
return json_response(False, "delete", error=str(e))
def search_files(directory: str, pattern: str = None, content: str = None) -> str:
"""Search files by name pattern or content"""
try:
directory = os.path.abspath(directory)
if not os.path.exists(directory):
return json_response(False, "search", error=f"Directory not found: {directory}")
results = []
for root, dirs, files in os.walk(directory):
for filename in files:
file_path = os.path.join(root, filename)
rel_path = os.path.relpath(file_path, directory)
# Pattern match
if pattern and not Path(filename).match(pattern):
continue
# Content search
if content:
try:
with open(file_path, 'r', encoding='utf-8') as f:
file_content = f.read()
if content not in file_content:
continue
# Find matching lines
matches = []
for i, line in enumerate(file_content.split('\n'), 1):
if content in line:
matches.append({"line": i, "text": line.strip()[:100]})
if matches:
results.append({
"path": rel_path,
"matches": matches[:5] # Limit matches per file
})
except (UnicodeDecodeError, IOError):
continue
else:
results.append({"path": rel_path})
return json_response(True, "search", {
"directory": directory,
"pattern": pattern,
"content_search": content,
"results": results[:100], # Limit results
"total": len(results)
})
except Exception as e:
return json_response(False, "search", error=str(e))
def get_info(path: str) -> str:
"""Get file/directory information"""
try:
path = os.path.abspath(path)
if not os.path.exists(path):
return json_response(False, "info", error=f"Path not found: {path}")
stat_info = os.stat(path)
info = {
"path": path,
"name": os.path.basename(path),
"type": "directory" if os.path.isdir(path) else "file",
"size": stat_info.st_size,
"created": datetime.fromtimestamp(stat_info.st_ctime).isoformat(),
"modified": datetime.fromtimestamp(stat_info.st_mtime).isoformat(),
"accessed": datetime.fromtimestamp(stat_info.st_atime).isoformat(),
"permissions": oct(stat_info.st_mode)[-3:]
}
if os.path.isfile(path):
info["extension"] = os.path.splitext(path)[1]
# Try to detect if text file
try:
with open(path, 'r', encoding='utf-8') as f:
f.read(1024)
info["encoding"] = "utf-8"
except UnicodeDecodeError:
info["encoding"] = "binary"
return json_response(True, "info", info)
except Exception as e:
return json_response(False, "info", error=str(e))
def modify_line(file_path: str, line_number: int, new_content: str) -> str:
"""Modify a specific line in a file"""
try:
file_path = os.path.abspath(file_path)
if not os.path.exists(file_path):
return json_response(False, "modify_line", error=f"File not found: {file_path}")
if not os.path.isfile(file_path):
return json_response(False, "modify_line", error=f"Not a file: {file_path}")
if line_number < 1:
return json_response(False, "modify_line", error="Line number must be >= 1")
# Read all lines
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
total_lines = len(lines)
if line_number > total_lines:
return json_response(False, "modify_line",
error=f"File has only {total_lines} lines, cannot modify line {line_number}")
# Get old content for reporting
old_content = lines[line_number - 1].rstrip('\n\r')
# Modify the line (preserve newline if existed)
has_newline = lines[line_number - 1].endswith('\n')
lines[line_number - 1] = new_content + ('\n' if has_newline else '')
# Write back
with open(file_path, 'w', encoding='utf-8') as f:
f.writelines(lines)
return json_response(True, "modify_line", {
"path": file_path,
"line_number": line_number,
"old_content": old_content,
"new_content": new_content,
"total_lines": total_lines
})
except UnicodeDecodeError:
return json_response(False, "modify_line", error="Cannot modify binary file")
except Exception as e:
return json_response(False, "modify_line", error=str(e))
def insert_line(file_path: str, line_number: int, content: str) -> str:
"""Insert a new line at specified position"""
try:
file_path = os.path.abspath(file_path)
if not os.path.exists(file_path):
return json_response(False, "insert_line", error=f"File not found: {file_path}")
if not os.path.isfile(file_path):
return json_response(False, "insert_line", error=f"Not a file: {file_path}")
if line_number < 1:
return json_response(False, "insert_line", error="Line number must be >= 1")
# Read all lines
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
total_lines = len(lines)
# Insert at position (can be after last line)
insert_pos = min(line_number - 1, total_lines)
lines.insert(insert_pos, content + '\n')
# Write back
with open(file_path, 'w', encoding='utf-8') as f:
f.writelines(lines)
return json_response(True, "insert_line", {
"path": file_path,
"line_number": line_number,
"inserted_content": content,
"total_lines": len(lines)
})
except Exception as e:
return json_response(False, "insert_line", error=str(e))
def delete_line(file_path: str, line_number: int) -> str:
"""Delete a specific line from a file"""
try:
file_path = os.path.abspath(file_path)
if not os.path.exists(file_path):
return json_response(False, "delete_line", error=f"File not found: {file_path}")
if not os.path.isfile(file_path):
return json_response(False, "delete_line", error=f"Not a file: {file_path}")
if line_number < 1:
return json_response(False, "delete_line", error="Line number must be >= 1")
# Read all lines
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
total_lines = len(lines)
if line_number > total_lines:
return json_response(False, "delete_line",
error=f"File has only {total_lines} lines, cannot delete line {line_number}")
# Get deleted content
deleted_content = lines[line_number - 1].rstrip('\n\r')
# Remove the line
del lines[line_number - 1]
# Write back
with open(file_path, 'w', encoding='utf-8') as f:
f.writelines(lines)
return json_response(True, "delete_line", {
"path": file_path,
"line_number": line_number,
"deleted_content": deleted_content,
"total_lines": len(lines)
})
except Exception as e:
return json_response(False, "delete_line", error=str(e))
def modify_lines(file_path: str, modifications: Dict[int, str]) -> str:
"""
Modify multiple lines at once
Args:
file_path: Path to the file
modifications: Dict mapping line numbers (1-based) to new content
Example: {2: "new line 2", 5: "new line 5"}
"""
try:
file_path = os.path.abspath(file_path)
if not os.path.exists(file_path):
return json_response(False, "modify_lines", error=f"File not found: {file_path}")
if not os.path.isfile(file_path):
return json_response(False, "modify_lines", error=f"Not a file: {file_path}")
# Read all lines
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
total_lines = len(lines)
results = []
# Sort modifications by line number (descending to avoid index shift issues)
for line_number in sorted(modifications.keys()):
if line_number < 1:
results.append({"line": line_number, "success": False, "error": "Line number must be >= 1"})
continue
if line_number > total_lines:
results.append({"line": line_number, "success": False,
"error": f"File has only {total_lines} lines"})
continue
# Get old content
old_content = lines[line_number - 1].rstrip('\n\r')
new_content = modifications[line_number]
# Modify the line
has_newline = lines[line_number - 1].endswith('\n')
lines[line_number - 1] = new_content + ('\n' if has_newline else '')
results.append({
"line": line_number,
"success": True,
"old_content": old_content,
"new_content": new_content
})
# Write back
with open(file_path, 'w', encoding='utf-8') as f:
f.writelines(lines)
return json_response(True, "modify_lines", {
"path": file_path,
"total_lines": total_lines,
"modifications_count": len(modifications),
"results": results
})
except Exception as e:
return json_response(False, "modify_lines", error=str(e))
def replace_text(file_path: str, old_text: str, new_text: str, start_line: int = None, end_line: int = None) -> str:
"""
Replace all occurrences of text in a file (optionally within a line range)
Args:
file_path: Path to the file
old_text: Text to find and replace
new_text: Replacement text
start_line: Optional start line (1-based, inclusive)
end_line: Optional end line (1-based, inclusive)
"""
try:
file_path = os.path.abspath(file_path)
if not os.path.exists(file_path):
return json_response(False, "replace_text", error=f"File not found: {file_path}")
if not os.path.isfile(file_path):
return json_response(False, "replace_text", error=f"Not a file: {file_path}")
# Read all lines
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
total_lines = len(lines)
replacements = []
# Determine range
start = max(0, (start_line or 1) - 1)
end = min(total_lines, end_line or total_lines)
for i in range(start, end):
line = lines[i]
if old_text in line:
count = line.count(old_text)
lines[i] = line.replace(old_text, new_text)
replacements.append({
"line": i + 1,
"count": count,
"old_line": line.rstrip('\n\r'),
"new_line": lines[i].rstrip('\n\r')
})
# Write back
with open(file_path, 'w', encoding='utf-8') as f:
f.writelines(lines)
return json_response(True, "replace_text", {
"path": file_path,
"old_text": old_text,
"new_text": new_text,
"line_range": [start + 1, end] if start_line or end_line else None,
"total_replacements": sum(r["count"] for r in replacements),
"lines_modified": len(replacements),
"details": replacements
})
except Exception as e:
return json_response(False, "replace_text", error=str(e))
def main():
parser = argparse.ArgumentParser(description="File operations for file-manager skill")
subparsers = parser.add_subparsers(dest="operation", help="Operation to perform")
# List
list_parser = subparsers.add_parser("list", help="List directory contents")
list_parser.add_argument("path", help="Directory path")
list_parser.add_argument("-r", "--recursive", action="store_true", help="Recursive listing")
list_parser.add_argument("-p", "--pattern", help="File pattern (e.g., *.py)")
# Read
read_parser = subparsers.add_parser("read", help="Read file content")
read_parser.add_argument("path", help="File path")
read_parser.add_argument("-e", "--encoding", default="utf-8", help="File encoding")
read_parser.add_argument("-n", "--lines", type=int, help="Number of lines to read")
# Write
write_parser = subparsers.add_parser("write", help="Write to file")
write_parser.add_argument("path", help="File path")
write_parser.add_argument("-c", "--content", required=True, help="Content to write")
write_parser.add_argument("-a", "--append", action="store_true", help="Append to file")
# Copy
copy_parser = subparsers.add_parser("copy", help="Copy file or directory")
copy_parser.add_argument("source", help="Source path")
copy_parser.add_argument("destination", help="Destination path")
# Move
move_parser = subparsers.add_parser("move", help="Move file or directory")
move_parser.add_argument("source", help="Source path")
move_parser.add_argument("destination", help="Destination path")
# Delete
delete_parser = subparsers.add_parser("delete", help="Delete file or directory")
delete_parser.add_argument("path", help="Path to delete")
delete_parser.add_argument("-r", "--recursive", action="store_true", help="Recursive delete")
# Search
search_parser = subparsers.add_parser("search", help="Search files")
search_parser.add_argument("directory", help="Directory to search")
search_parser.add_argument("-p", "--pattern", help="File pattern")
search_parser.add_argument("-c", "--content", help="Content to search for")
# Info
info_parser = subparsers.add_parser("info", help="Get file info")
info_parser.add_argument("path", help="File or directory path")
# Modify line
modify_parser = subparsers.add_parser("modify-line", help="Modify a specific line")
modify_parser.add_argument("path", help="File path")
modify_parser.add_argument("line", type=int, help="Line number (1-based)")
modify_parser.add_argument("content", help="New line content")
# Insert line
insert_parser = subparsers.add_parser("insert-line", help="Insert a new line")
insert_parser.add_argument("path", help="File path")
insert_parser.add_argument("line", type=int, help="Line number to insert at (1-based)")
insert_parser.add_argument("content", help="Line content to insert")
# Delete line
del_line_parser = subparsers.add_parser("delete-line", help="Delete a specific line")
del_line_parser.add_argument("path", help="File path")
del_line_parser.add_argument("line", type=int, help="Line number to delete (1-based)")
# Modify multiple lines (JSON format)
multi_parser = subparsers.add_parser("modify-lines", help="Modify multiple lines at once")
multi_parser.add_argument("path", help="File path")
multi_parser.add_argument("-m", "--modifications", required=True,
help='JSON dict of line numbers to content, e.g. \'{"2": "new line 2", "5": "new line 5"}\'')
# Replace text
replace_parser = subparsers.add_parser("replace", help="Replace text in file")
replace_parser.add_argument("path", help="File path")
replace_parser.add_argument("old", help="Text to find and replace")
replace_parser.add_argument("new", help="Replacement text")
replace_parser.add_argument("--start", type=int, help="Start line (optional)")
replace_parser.add_argument("--end", type=int, help="End line (optional)")
args = parser.parse_args()
if args.operation == "list":
print(list_directory(args.path, args.recursive, args.pattern))
elif args.operation == "read":
print(read_file(args.path, args.encoding, args.lines))
elif args.operation == "write":
print(write_file(args.path, args.content, args.append))
elif args.operation == "copy":
print(copy_path(args.source, args.destination))
elif args.operation == "move":
print(move_path(args.source, args.destination))
elif args.operation == "delete":
print(delete_path(args.path, args.recursive))
elif args.operation == "search":
print(search_files(args.directory, args.pattern, args.content))
elif args.operation == "info":
print(get_info(args.path))
elif args.operation == "modify-line":
print(modify_line(args.path, args.line, args.content))
elif args.operation == "insert-line":
print(insert_line(args.path, args.line, args.content))
elif args.operation == "delete-line":
print(delete_line(args.path, args.line))
elif args.operation == "modify-lines":
mods = json.loads(args.modifications)
mods_int = {int(k): v for k, v in mods.items()}
print(modify_lines(args.path, mods_int))
elif args.operation == "replace":
print(replace_text(args.path, args.old, args.new, args.start, args.end))
else:
parser.print_help()
if __name__ == "__main__":
main()
+3
View File
@@ -0,0 +1,3 @@
Line 1
MODIFIED LINE
Line 3
@@ -0,0 +1,5 @@
Line 1
REPLACED LINE 2
Line 3
REPLACED LINE 4
Line 5
+832
View File
@@ -0,0 +1,832 @@
"""
统一工具管理器 - 协调 Liugin、Skill 和 MCP 三套协议
自动判断并路由到正确的处理器
"""
import os
import json
import threading
from typing import Dict, List, Any, Optional, Union, Callable
from colorama import Fore, Style
class UnifiedToolManager:
"""
统一工具管理器
协调三种协议:
1. Liugin 协议 (plugins/) - 小狸自有协议
2. Skill 协议 (skills/) - Agent Skills 开源协议
3. MCP 协议 - Model Context Protocol (Anthropic/OpenAI 标准)
路由策略:
- 优先使用 Skill 协议(如果存在同名技能)
- 回退到 Liugin 协议
- 支持强制指定协议
- MCP 格式调用统一入口
"""
def __init__(self, cli_instance=None):
self.cli = cli_instance
self._lock = threading.RLock()
# 两个独立的加载器
self._liugin_manager = None
self._skill_loader = None
# 统一的工具注册表
self._tools: Dict[str, Dict[str, Any]] = {}
# 协议映射: tool_name -> protocol_type
self._protocol_map: Dict[str, str] = {}
# MCP 工具定义缓存
self._mcp_tools_cache: Optional[List[Dict]] = None
def initialize(self, plugins_dir: str = "plugins", skills_dir: str = "skills") -> None:
"""
初始化并加载所有工具
Args:
plugins_dir: Liugin 目录
skills_dir: Skill 目录
"""
# 加载 Liugins
self._load_plugins(plugins_dir)
# 加载 Skills
self._load_skills(skills_dir)
# 构建缓存
self._build_mcp_cache()
print(f"{Fore.GREEN}[统一工具管理器] 加载完成: {len(self._tools)} 个工具{Style.RESET_ALL}")
print(f" - Liugin 协议: {sum(1 for p in self._protocol_map.values() if p == 'liugin')}")
print(f" - Skill 协议: {sum(1 for p in self._protocol_map.values() if p == 'skill')}")
def _load_plugins(self, plugins_dir: str) -> None:
"""加载 Liugin 协议工具"""
import importlib.util
if not os.path.exists(plugins_dir):
return
# 获取启用的插件列表
enabled_plugins = self._get_enabled_plugins(plugins_dir)
for filename in os.listdir(plugins_dir):
if not filename.endswith('.py') or filename == '__init__.py':
continue
plugin_name = filename[:-3]
# 检查是否启用
if enabled_plugins is not None and plugin_name.lower() not in enabled_plugins:
continue
plugin_path = os.path.join(plugins_dir, filename)
try:
spec = importlib.util.spec_from_file_location(plugin_name, plugin_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
# 兼容 Plugin 和 Liugin 两种类名
plugin_class = None
if hasattr(module, 'Liugin'):
plugin_class = module.Liugin
elif hasattr(module, 'Plugin'):
plugin_class = module.Plugin
if plugin_class:
plugin_instance = plugin_class()
# 设置 CLI 引用
if hasattr(plugin_instance, 'set_cli') and self.cli:
plugin_instance.set_cli(self.cli)
tool_info = plugin_instance.get_tool_info()
tool_name = tool_info.get('name', plugin_name)
# 获取 MCP 工具定义(如果插件提供)
mcp_definition = None
if hasattr(plugin_instance, 'get_mcp_definition'):
mcp_definition = plugin_instance.get_mcp_definition()
# 注册到统一工具表
self._tools[tool_name] = {
'name': tool_name,
'description': tool_info.get('description', ''),
'keywords': tool_info.get('keywords', []),
'usage': tool_info.get('usage', ''),
'handler': plugin_instance.handle,
'protocol': 'liugin',
'instance': plugin_instance,
'mcp_definition': mcp_definition
}
self._protocol_map[tool_name] = 'liugin'
except Exception as e:
print(f"{Fore.RED}加载 Liugin '{plugin_name}' 失败: {e}{Style.RESET_ALL}")
def _load_skills(self, skills_dir: str) -> None:
"""加载 Skill 协议工具 (支持 Markdown 和 Python 两种格式)"""
if not os.path.exists(skills_dir):
print(f"{Fore.YELLOW}Skills 目录不存在: {skills_dir}{Style.RESET_ALL}")
return
try:
from skills.base import SkillLoader, MarkdownSkill, Skill
self._skill_loader = SkillLoader(skills_dir)
# 先发现所有技能
discovered = self._skill_loader.discover_skills(skills_dir)
print(f"{Fore.CYAN}发现 {len(discovered)} 个技能: {[s['name'] for s in discovered]}{Style.RESET_ALL}")
# 加载所有技能
self._skill_loader.load_all_skills(skills_dir)
loaded_count = len(self._skill_loader.skills)
if loaded_count == 0:
print(f"{Fore.YELLOW}未加载任何 Skill 技能{Style.RESET_ALL}")
return
for skill_name, skill in self._skill_loader.skills.items():
# 设置 CLI 引用 (仅 Python 类技能)
if hasattr(skill, 'set_cli') and self.cli:
skill.set_cli(self.cli)
# 获取工具定义
tool_def = skill.get_tool_definition()
func_def = tool_def.get('function', {})
# 检查是否已有同名 Plugin,Skill 优先覆盖
if skill_name in self._tools:
old_protocol = self._protocol_map[skill_name]
print(f"{Fore.YELLOW}工具 '{skill_name}' 已存在({old_protocol}),使用 Skill 协议覆盖{Style.RESET_ALL}")
# 判断技能格式
is_markdown = isinstance(skill, MarkdownSkill)
skill_format = "markdown" if is_markdown else "python"
# 注册到统一工具表
self._tools[skill_name] = {
'name': skill_name,
'description': func_def.get('description', ''),
'keywords': getattr(skill, 'tags', []),
'usage': getattr(skill, 'instructions', '')[:500] if is_markdown else getattr(skill, 'usage', ''),
'handler': self._create_skill_handler(skill),
'protocol': 'skill',
'format': skill_format,
'instance': skill,
'tool_definition': tool_def,
'anthropic_definition': skill.get_anthropic_tool_definition(),
'mcp_definition': tool_def # Skill 的 OpenAI 格式就是 MCP 格式
}
self._protocol_map[skill_name] = 'skill'
print(f"{Fore.GREEN}已加载 Skill ({skill_format}): {skill_name}{Style.RESET_ALL}")
except ImportError as e:
print(f"{Fore.YELLOW}Skill 协议模块未找到: {e}{Style.RESET_ALL}")
except Exception as e:
print(f"{Fore.RED}加载 Skills 失败: {e}{Style.RESET_ALL}")
def _create_skill_handler(self, skill) -> callable:
"""
为 Skill 创建兼容 Plugin 的 handler
支持 Markdown 和 Python 两种技能格式
"""
from skills.base import MarkdownSkill, Skill
def handler(args: str) -> str:
import json
kwargs = {}
# 解析参数
if args.strip().startswith('{'):
try:
kwargs = json.loads(args)
except json.JSONDecodeError:
kwargs = {'operation': args}
else:
kwargs = {'operation': args}
# Markdown 技能: 返回指令供 AI 执行
if isinstance(skill, MarkdownSkill):
return f"[Skill: {skill.name}]\n{skill.instructions}"
# Python 技能: 直接执行
if hasattr(skill, 'run'):
result = skill.run(**kwargs)
if result.success:
return result.message or str(result.data)
else:
return f"错误: {result.error}"
return f"未知的技能类型: {type(skill)}"
return handler
def _get_enabled_plugins(self, plugins_dir: str) -> Optional[List[str]]:
"""获取启用的插件列表"""
# 检查环境变量
enabled_env = os.environ.get("XIAOLI_ENABLED_PLUGINS")
if enabled_env is not None:
if enabled_env == "":
return []
return [p.lower() for p in enabled_env.split(",")]
# 检查配置文件
config_file = os.path.join(os.path.dirname(plugins_dir), "plugins_config.py")
if os.path.exists(config_file):
try:
with open(config_file, 'r', encoding='utf-8') as f:
content = f.read()
if "# 不启用任何插件" in content:
return []
enabled = []
for line in content.split('\n'):
if line.startswith('ENABLE_') and '= True' in line:
name = line.split('=')[0].replace('ENABLE_', '').strip().lower()
enabled.append(name)
return enabled
except Exception:
pass
return None # 加载全部
def _build_mcp_cache(self) -> None:
"""构建 MCP 工具定义缓存(线程安全)"""
with self._lock:
self._mcp_tools_cache = []
for tool in self._tools.values():
mcp_def = tool.get('mcp_definition')
if mcp_def:
# 确保符合 MCP 标准
normalized = self._normalize_mcp_definition(mcp_def)
self._mcp_tools_cache.append(normalized)
else:
# 为没有 MCP 定义的 Plugin 生成默认定义
self._mcp_tools_cache.append(self._generate_mcp_definition(tool))
def _normalize_mcp_definition(self, mcp_def: Dict[str, Any]) -> Dict[str, Any]:
"""
标准化 MCP 工具定义
支持多种输入格式并转换为标准 MCP 格式
"""
# 如果已经是 MCP 格式(有 name 或 inputSchema)
if 'name' in mcp_def and ('inputSchema' in mcp_def or 'description' in mcp_def):
return mcp_def
# 如果是 OpenAI Function Calling 格式,转换
if mcp_def.get('type') == 'function' and 'function' in mcp_def:
func = mcp_def['function']
return {
"name": func.get('name', ''),
"description": func.get('description', ''),
"inputSchema": func.get('parameters', {
"type": "object",
"properties": {}
})
}
# 其他情况,直接返回
return mcp_def
def _generate_mcp_definition(self, tool: Dict[str, Any]) -> Dict[str, Any]:
"""
为 Plugin 生成符合 MCP 标准的工具定义
MCP 官方格式 (2025-03-26):
{
"name": "tool_name",
"title": "Human Readable Name", // optional
"description": "Human-readable description",
"inputSchema": {
"type": "object",
"properties": {...},
"required": [...]
},
"outputSchema": {...}, // optional
"annotations": {...} // optional
}
"""
return {
"name": tool['name'],
"description": tool.get('description', ''),
"inputSchema": {
"type": "object",
"properties": {
"args": {
"type": "string",
"description": f"命令参数。用法:\\n{tool.get('usage', '')[:1000]}"
}
},
"required": ["args"]
}
}
# ==================== 公共 API ====================
@property
def tools(self) -> List[Dict[str, Any]]:
"""获取所有工具列表(兼容 LiuginManager 接口)"""
return list(self._tools.values())
def get_tool(self, name: str) -> Optional[Dict[str, Any]]:
"""获取指定工具"""
return self._tools.get(name)
def get_protocol(self, name: str) -> Optional[str]:
"""获取工具使用的协议"""
return self._protocol_map.get(name)
def execute(self, tool_name: str, args: str = "", **kwargs) -> Dict[str, Any]:
"""
执行工具调用
Args:
tool_name: 工具名称
args: 参数字符串(Plugin 格式)
**kwargs: 关键字参数(Skill 格式或 MCP arguments)
Returns:
{"result": "..."} 格式的结果
"""
tool = self._tools.get(tool_name)
if not tool:
return {"result": f"未找到工具: {tool_name}"}
try:
handler = tool['handler']
protocol = tool['protocol']
instance = tool.get('instance')
if protocol == 'skill' and kwargs:
# Skill 协议,使用 kwargs
skill = tool['instance']
result = skill.run(**kwargs)
return {"result": result.message or str(result.data) if result.success else f"错误: {result.error}"}
elif protocol == 'liugin' and kwargs and not args:
# Liugin 协议但有 MCP 结构化参数,需要转换
# 检查插件是否有 convert_mcp_args 方法
if instance and hasattr(instance, 'convert_mcp_args'):
args = instance.convert_mcp_args(kwargs)
else:
args = self._convert_mcp_args_to_plugin_args(tool_name, kwargs, instance)
# 执行 handler
result = handler(args)
return {"result": result if result is not None else "无结果"}
except Exception as e:
return {"result": f"工具执行错误: {e}"}
# ==================== MCP 协议支持 ====================
def get_mcp_tools(self) -> List[Dict[str, Any]]:
"""
获取 MCP 格式的工具定义列表
MCP (Model Context Protocol) 使用 OpenAI Function Calling 格式
作为标准工具定义格式
Returns:
List of MCP tool definitions
"""
with self._lock:
if self._mcp_tools_cache is None:
self._build_mcp_cache()
return self._mcp_tools_cache.copy() # 返回副本,避免外部修改
def mcp_call(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
"""
MCP 格式的工具调用入口
这是 MCP 协议的主要调用接口,接收结构化参数并返回结果
Args:
tool_name: 工具名称
arguments: 结构化参数 (JSON对象)
Returns:
MCP 标准响应格式:
{
"content": [
{
"type": "text",
"text": "结果内容"
}
],
"isError": false
}
"""
tool = self._tools.get(tool_name)
if not tool:
return self._mcp_error(f"未找到工具: {tool_name}")
try:
protocol = tool['protocol']
instance = tool.get('instance')
if protocol == 'liugin':
# Liugin 协议: 将 arguments 转换为 args 字符串
args = self._convert_mcp_args_to_plugin_args(tool_name, arguments, instance)
result = tool['handler'](args)
return self._mcp_success(result if result else "执行完成")
elif protocol == 'skill':
# Skill 协议: 直接传递 arguments 作为 kwargs
from skills.base import MarkdownSkill
if isinstance(instance, MarkdownSkill):
return self._mcp_success(f"[Skill: {instance.name}]\n{instance.instructions}")
if hasattr(instance, 'run'):
result = instance.run(**arguments)
if result.success:
return self._mcp_success(result.message or str(result.data))
else:
return self._mcp_error(f"执行失败: {result.error}")
return self._mcp_error(f"未知协议类型: {protocol}")
except Exception as e:
return self._mcp_error(f"工具执行错误: {str(e)}")
def _convert_mcp_args_to_plugin_args(self, tool_name: str, arguments: Dict[str, Any],
instance: Any) -> str:
"""
将 MCP 结构化参数转换为 Plugin 的 args 字符串
Args:
tool_name: 工具名称
arguments: MCP 参数对象
instance: 插件实例(可能有自定义转换方法)
Returns:
args 字符串
"""
# 检查插件是否有自定义的 MCP 参数转换方法
if instance and hasattr(instance, 'convert_mcp_args'):
return instance.convert_mcp_args(arguments)
# 默认转换策略
if 'args' in arguments:
# 如果有 args 字段,直接使用
return arguments['args']
# 将 arguments 转换为命令行格式
parts = []
# 第一个参数通常是操作/命令
if 'operation' in arguments:
parts.append(arguments['operation'])
elif 'action' in arguments:
parts.append(arguments['action'])
# 添加其他参数
for key, value in arguments.items():
if key in ('operation', 'action'):
continue
if isinstance(value, bool):
if value:
parts.append(f"-{key}")
elif isinstance(value, (list, tuple)):
for v in value:
parts.append(f"--{key}")
parts.append(self._quote_if_needed(str(v)))
else:
parts.append(f"--{key}")
parts.append(self._quote_if_needed(str(value)))
return ' '.join(parts)
def _quote_if_needed(self, s: str) -> str:
"""如果字符串包含空格,添加引号"""
if ' ' in s or '"' in s:
return f'"{s}"'
return s
def _mcp_success(self, text: str) -> Dict[str, Any]:
"""生成 MCP 成功响应"""
return {
"content": [
{
"type": "text",
"text": text
}
],
"isError": False
}
def _mcp_error(self, error_msg: str) -> Dict[str, Any]:
"""生成 MCP 错误响应"""
return {
"content": [
{
"type": "text",
"text": error_msg
}
],
"isError": True
}
# ==================== 兼容性 API ====================
def get_openai_tools(self) -> List[Dict[str, Any]]:
"""
获取 OpenAI Function Calling 格式的工具定义
OpenAI 格式:
{
"type": "function",
"function": {
"name": "...",
"description": "...",
"parameters": {...}
}
}
"""
tools = []
for mcp_def in self.get_mcp_tools():
tools.append({
"type": "function",
"function": {
"name": mcp_def.get("name", ""),
"description": mcp_def.get("description", ""),
"parameters": mcp_def.get("inputSchema", {
"type": "object",
"properties": {}
})
}
})
return tools
def get_anthropic_tools(self) -> List[Dict[str, Any]]:
"""
获取 Anthropic Tool Use 格式的工具定义
Anthropic 格式:
{
"name": "tool_name",
"description": "...",
"input_schema": {...}
}
"""
tools = []
for mcp_def in self.get_mcp_tools():
tools.append({
"name": mcp_def.get("name", ""),
"description": mcp_def.get("description", ""),
"input_schema": mcp_def.get("inputSchema", {
"type": "object",
"properties": {}
})
})
return tools
def search_tools(self, query: str, limit: int = 5) -> List[Dict[str, Any]]:
"""
搜索工具
Args:
query: 搜索关键词
limit: 返回数量限制
Returns:
匹配的工具列表
"""
query_lower = query.lower()
# 标准化:移除所有分隔符(连字符、下划线、空格)
query_normalized = query_lower.replace('-', '').replace('_', '').replace(' ', '')
matches = []
for tool in self._tools.values():
score = 0
name = tool['name'].lower()
name_normalized = name.replace('-', '').replace('_', '')
desc = tool['description'].lower()
keywords = [kw.lower() for kw in tool.get('keywords', [])]
# 名称完全匹配(忽略分隔符差异)
if query_lower == name or query_normalized == name_normalized:
score += 100
# 名称包含(标准化后匹配)
elif query_normalized in name_normalized or query_lower in name:
score += 50
# 描述包含
if query_lower in desc:
score += 30
# 关键词匹配
for kw in keywords:
kw_normalized = kw.replace('-', '').replace('_', '')
if query_lower in kw or query_normalized in kw_normalized:
score += 20
break
if score > 0:
matches.append({
'name': tool['name'],
'description': tool['description'],
'usage': tool.get('usage', ''),
'keywords': keywords,
'protocol': tool['protocol'],
'instance': tool.get('instance'),
'score': score
})
# 按分数排序
matches.sort(key=lambda x: x['score'], reverse=True)
return matches[:limit]
def register_tool(self, name: str, handler: callable, description: str = "",
protocol: str = "custom", mcp_definition: Dict = None, **kwargs) -> None:
"""
动态注册工具
Args:
name: 工具名称
handler: 处理函数
description: 描述
protocol: 协议类型
mcp_definition: MCP 工具定义(可选)
"""
with self._lock:
self._tools[name] = {
'name': name,
'description': description,
'handler': handler,
'protocol': protocol,
'mcp_definition': mcp_definition,
**kwargs
}
self._protocol_map[name] = protocol
# 更新缓存
if self._mcp_tools_cache is not None:
if mcp_definition:
self._mcp_tools_cache.append(mcp_definition)
else:
self._mcp_tools_cache.append(self._generate_mcp_definition(self._tools[name]))
def unregister_tool(self, name: str) -> bool:
"""注销工具"""
with self._lock:
if name in self._tools:
del self._tools[name]
del self._protocol_map[name]
# 重建缓存
self._build_mcp_cache()
return True
return False
def get_skills_system_prompt(self) -> str:
"""
获取所有技能的系统提示词
用于注入到 AI 的 system prompt 中,
让 AI 了解如何使用这些技能
"""
skill_prompts = []
for name, tool in self._tools.items():
if tool['protocol'] == 'skill':
instance = tool.get('instance')
if instance:
# 获取技能指令
instructions = getattr(instance, 'instructions', '')
scripts_dir = getattr(instance, 'scripts_dir', '')
prompt_parts = [f"## 技能: {name}"]
prompt_parts.append(f"描述: {tool['description']}")
if instructions:
# 截取关键部分,避免过长
prompt_parts.append(f"\n{instructions}")
if scripts_dir:
prompt_parts.append(f"\n脚本目录: {scripts_dir}")
skill_prompts.append('\n'.join(prompt_parts))
if skill_prompts:
header = "# 已加载的 Agent Skills\n以下技能可用于执行特定任务,调用方式与工具相同:\n"
return header + '\n\n---\n\n'.join(skill_prompts)
return ""
def get_tools_prompt(self) -> str:
"""
获取工具使用提示词(兼容旧接口)
包含 Plugin 和 Skill 两种协议的工具说明
"""
prompts = []
# Liugin 协议工具
plugin_tools = [t for t in self._tools.values() if t['protocol'] == 'liugin']
if plugin_tools:
prompts.append("## Liugin 工具")
for tool in plugin_tools:
usage = tool.get('usage', '')
if usage:
prompts.append(f"### {tool['name']}\n{usage}\n")
# Skill 协议工具(使用系统提示词)
skills_prompt = self.get_skills_system_prompt()
if skills_prompt:
prompts.append(skills_prompt)
return '\n\n'.join(prompts)
# ==================== MCP Server 模式支持 ====================
def get_mcp_server_info(self) -> Dict[str, Any]:
"""
获取 MCP Server 信息
用于 MCP 客户端连接时的初始化响应
"""
return {
"name": "xiaoli-tool-server",
"version": "1.0.0",
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {
"listChanged": True
}
}
}
def handle_mcp_request(self, request: Dict[str, Any]) -> Dict[str, Any]:
"""
处理 MCP 协议请求
统一处理 MCP 客户端的各种请求
Args:
request: MCP 请求对象
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list" | "tools/call" | ...,
"params": {...}
}
Returns:
MCP 响应对象
{
"jsonrpc": "2.0",
"id": 1,
"result": {...}
}
"""
method = request.get('method', '')
params = request.get('params', {})
req_id = request.get('id')
result = None
error = None
try:
if method == 'tools/list':
result = {"tools": self.get_mcp_tools()}
elif method == 'tools/call':
tool_name = params.get('name')
arguments = params.get('arguments', {})
if not tool_name:
error = {"code": -32602, "message": "Missing tool name"}
else:
result = self.mcp_call(tool_name, arguments)
elif method == 'initialize':
result = self.get_mcp_server_info()
elif method == 'ping':
result = {}
else:
error = {"code": -32601, "message": f"Method not found: {method}"}
except Exception as e:
error = {"code": -32603, "message": str(e)}
response = {"jsonrpc": "2.0", "id": req_id}
if error:
response["error"] = error
else:
response["result"] = result
return response
+1170
View File
File diff suppressed because it is too large. Load diff
+738
View File
@@ -0,0 +1,738 @@
# 小狸 Pro-CLI 开发文档
> 版本:2.0 | 更新日期:2026年3月
---
## 目录
1. [系统架构概述](#1-系统架构概述)
2. [AI引擎开发规范](#2-ai引擎开发规范)
3. [Plugin协议开发](#3-plugin协议开发)
4. [Skill协议开发](#4-skill协议开发)
5. [MCP协议支持](#5-mcp协议支持)
6. [WebSocket API](#6-websocket-api)
7. [已加载模块列表](#7-已加载模块列表)
8. [最佳实践](#8-最佳实践)
9. [调试与测试](#9-调试与测试)
10. [常见问题](#10-常见问题)
---
## 1. 系统架构概述
### 1.1 三大协议支持
小狸 Pro-CLI 的工具系统支持三种协议:
| 协议 | 目录 | 说明 |
|------|------|------|
| **Plugin 协议** | `plugins/` | 小狸自有协议,简单易用 |
| **Skill 协议** | `skills/` | Agent Skills 开源协议,支持 Markdown 和 Python 格式 |
| **MCP 协议** | - | Model Context Protocol,Anthropic/OpenAI 标准 |
### 1.2 统一工具管理器
`unified_tool_manager.py` 协调三种协议,提供统一的工具调用接口:
```python
from unified_tool_manager import UnifiedToolManager
manager = UnifiedToolManager(cli_instance)
manager.initialize("plugins", "skills")
# Plugin 格式调用
result = manager.execute("file_manager", "read test.txt")
# MCP 格式调用
result = manager.mcp_call("file_manager", {"operation": "read", "path": "test.txt"})
```
### 1.3 路由策略
- 优先使用 Skill 协议(如果存在同名技能)
- 回退到 Plugin 协议
- 支持强制指定协议
- MCP 格式调用统一入口
---
## 2. AI引擎开发规范
### 2.1 文件与类命名规范
| 规范 | 说明 |
|------|------|
| 文件命名 | 必须以 `_engine.py` 结尾 |
| 类命名 | 使用驼峰命名,如 `Qwen3CoderPlusAI` |
**示例:**
- 文件:`qwen3-coder-plus_engine.py`
- 类名:`Qwen3CoderPlusAI`
### 2.2 引擎配置声明
```python
class MyEngineAI:
# 配置属性
requires_api_key = True # 是否需要API密钥
default_base_url = "https://api.example.com/v1"
default_model = "my-model"
def __init__(self):
self.name = "my_engine"
# 初始化代码...
```
### 2.3 核心接口
```python
def generate_response(self, user_input, tool_results=None, system_prompt=None):
"""
生成AI响应
参数:
user_input: 用户输入文本
tool_results: 工具执行结果(可选)
system_prompt: 系统提示词(可选,由主程序生成)
返回:
AI响应字符串
"""
pass
def get_help_info(self):
"""返回引擎帮助信息"""
return "引擎帮助信息..."
```
### 2.4 禁止设置超时
```python
# ✅ 正确
response = requests.post(url, json=body, headers=headers, timeout=None)
# ❌ 错误
response = requests.post(url, json=body, headers=headers, timeout=30)
```
### 2.5 SDK快速开发
使用 SDK,只需几行代码就能完成引擎开发:
```python
from SDK_openAI import *
class MyAIAI:
def __init__(self):
engine_Online("on") # 云模式
ai_name("my_ai")
ai_api_url("https://api.example.com/v1")
ai_api_key("your_api_key")
ai_model_id("gpt-3.5-turbo")
self.name = "my_ai"
self.cli = None
self.shared_conversation_history = None
def set_cli(self, cli):
set_cli(cli)
self.cli = cli
def generate_response(self, user_input, tool_results=None, system_prompt=None):
set_shared_history(self.shared_conversation_history)
return generate_response(user_input, tool_results, system_prompt)
```
### 2.6 SDK方法说明
| 方法 | 说明 |
|------|------|
| `engine_Online("on"/"off")` | 云模式/本地模式 |
| `ai_name(name)` | 设置引擎名称 |
| `ai_api_url(url)` | 设置API地址 |
| `ai_api_key(key)` | 设置API密钥 |
| `ai_model_id(model)` | 设置模型名称 |
| `ai_thinking_engine(...)` | 深度思考引擎配置 |
### 2.7 深度思考引擎配置
适用于 QwQ、DeepSeek-R1 等会输出思考过程的模型:
```python
ai_thinking_engine(
enable=True, # 启用深度思考
start_marker="<thinking>", # 思考开始标记
end_marker="</thinking>", # 思考结束标记
show_thinking=True # 显示思考过程
)
```
---
## 3. Plugin协议开发
### 3.1 基本结构
```python
class Plugin:
"""插件描述"""
def __init__(self):
self.usage = "使用方法说明"
self.cli = None
def set_cli(self, cli):
"""设置CLI引用(可选)"""
self.cli = cli
def get_tool_info(self):
"""返回工具信息(必需)"""
return {
"name": "tool_name",
"description": "工具描述",
"keywords": ["关键词1", "关键词2"],
"usage": self.usage
}
def get_mcp_definition(self):
"""返回MCP格式定义(可选)"""
return {
"name": "tool_name",
"description": "工具描述",
"inputSchema": {
"type": "object",
"properties": {
"operation": {"type": "string"},
"path": {"type": "string"}
},
"required": ["operation"]
}
}
def convert_mcp_args(self, arguments):
"""转换MCP参数(可选)"""
return f"{arguments.get('operation', '')} {arguments.get('path', '')}"
def handle(self, args: str) -> str:
"""处理工具调用(必需)"""
try:
# 实现逻辑
return "处理结果"
except Exception as e:
return f"错误: {str(e)}"
```
### 3.2 完整示例:文件管理器
```python
import os
from typing import Dict, Any, List
class Plugin:
"""文件管理器插件"""
def __init__(self):
self.usage = """文件管理器使用方法:
file_manager <操作> <参数>
操作类型:
- list <路径> - 列出目录内容
- read <文件路径> - 读取文件内容
- write <文件路径> -c "内容" - 写入文件
- delete <路径> - 删除文件或目录
"""
self.cli = None
def set_cli(self, cli):
self.cli = cli
def get_tool_info(self):
return {
"name": "file_manager",
"description": "文件和目录管理工具",
"keywords": ["文件", "目录", "管理", "file"],
"usage": self.usage
}
def get_mcp_definition(self) -> Dict[str, Any]:
return {
"name": "file_manager",
"description": "文件和目录管理工具",
"inputSchema": {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["list", "read", "write", "delete"],
"description": "操作类型"
},
"path": {"type": "string", "description": "路径"},
"content": {"type": "string", "description": "内容"}
},
"required": ["operation"]
}
}
def convert_mcp_args(self, arguments: Dict[str, Any]) -> str:
op = arguments.get("operation", "")
path = arguments.get("path", "")
content = arguments.get("content", "")
if content:
return f'{op} {path} -c "{content}"'
return f"{op} {path}"
def handle(self, args: str) -> str:
parts = args.strip().split(maxsplit=2)
if not parts:
return "错误:请提供操作类型"
operation = parts[0].lower()
if operation == "list":
return self._op_list(parts[1:] if len(parts) > 1 else ["."])
elif operation == "read":
return self._op_read(parts[1:] if len(parts) > 1 else [])
elif operation == "write":
return self._op_write(parts[1:])
elif operation == "delete":
return self._op_delete(parts[1:])
return f"错误:不支持的操作 '{operation}'"
def _op_list(self, args: List[str]) -> str:
path = args[0] if args else "."
if not os.path.exists(path):
return f"错误:路径不存在: {path}"
# 实现列出目录逻辑...
return f"目录内容: {path}"
def _op_read(self, args: List[str]) -> str:
if not args:
return "错误:请提供文件路径"
# 实现读取文件逻辑...
return "文件内容..."
def _op_write(self, args: List[str]) -> str:
if len(args) < 2:
return "错误:请提供文件路径和内容"
# 实现写入文件逻辑...
return "写入成功"
def _op_delete(self, args: List[str]) -> str:
if not args:
return "错误:请提供路径"
# 实现删除逻辑...
return "删除成功"
```
### 3.3 参数格式规范
**多操作插件格式:**
```
<操作类型> <目标对象> [可选内容]
```
示例:`read file.txt`、`write file.txt 内容`
**单参数插件格式:**
```
<待处理内容>
```
示例:`2+3`、`hello world`
---
## 4. Skill协议开发
### 4.1 Python格式技能
```python
# skills/my_skill/__init__.py
from skills.base import Skill, SkillResult
class MySkill(Skill):
"""我的技能"""
name = "my_skill"
description = "技能描述"
tags = ["标签1", "标签2"]
def get_tool_definition(self):
return {
"type": "function",
"function": {
"name": self.name,
"description": self.description,
"parameters": {
"type": "object",
"properties": {
"operation": {"type": "string"}
}
}
}
}
def run(self, **kwargs) -> SkillResult:
try:
operation = kwargs.get("operation", "")
# 实现逻辑
return SkillResult(
success=True,
message="执行成功",
data={"result": "..."}
)
except Exception as e:
return SkillResult(
success=False,
error=str(e)
)
```
### 4.2 Markdown格式技能
```markdown
# skills/my_skill/SKILL.md
---
name: my_skill
description: 技能描述
tags: [标签1, 标签2]
---
# 技能说明
这是技能的详细使用说明...
## 使用方法
1. 步骤一
2. 步骤二
```
---
## 5. MCP协议支持
### 5.1 MCP工具定义格式
```python
{
"name": "tool_name",
"description": "工具描述",
"inputSchema": {
"type": "object",
"properties": {
"param1": {"type": "string", "description": "参数说明"},
"param2": {"type": "integer", "description": "参数说明"}
},
"required": ["param1"]
}
}
```
### 5.2 MCP调用格式
**请求:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "file_manager",
"arguments": {
"operation": "read",
"path": "test.txt"
}
}
}
```
**响应:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{"type": "text", "text": "文件内容..."}
],
"isError": false
}
}
```
### 5.3 统一工具管理器MCP接口
```python
# 获取MCP工具列表
tools = manager.get_mcp_tools()
# MCP格式调用
result = manager.mcp_call("file_manager", {"operation": "read", "path": "test.txt"})
# 处理MCP请求
response = manager.handle_mcp_request({
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {...}
})
```
---
## 6. WebSocket API
### 6.1 消息类型
| 类型 | 方向 | 说明 |
|------|------|------|
| `heartbeat` | 双向 | 心跳检测 |
| `auth` | 客户端→服务端 | 认证请求 |
| `auth_success` / `auth_failed` | 服务端→客户端 | 认证结果 |
| `message` | 双向 | 文本消息 |
| `file` | 客户端→服务端 | 文件上传 |
| `image` | 服务端→客户端 | 图片发送 |
| `tool_status` | 服务端→客户端 | 工具状态 |
### 6.2 消息格式示例
**认证请求:**
```json
{"type": "auth", "password": "your_password"}
```
**发送消息:**
```json
{"type": "message", "content": "你好"}
```
**上传文件:**
```json
{
"type": "file",
"file": "base64_encoded_data",
"filename": "example.txt",
"text": "附加说明"
}
```
### 6.3 启动远程服务
```bash
# 本地模式
/remote start 9079 your_password
# 代理模式
/remote proxy relay.example.com:9079 server_pwd 12345 pc_pwd
```
---
## 7. 已加载模块列表
### 7.1 AI引擎
| 引擎名 | 类型 | 说明 |
|--------|------|------|
| ollama | 本地 | Ollama本地模型,无需API密钥 |
| glm | 云端 | 智谱GLM |
| spark | 云端 | 讯飞星火 |
| qwq | 云端 | QwQ深度思考引擎 |
| qwen | 云端 | 通义千问 |
### 7.2 插件
| 插件名 | 功能 |
|--------|------|
| file_manager | 文件管理(14种操作) |
| ai_search | AI搜索 |
| audio_player | 音频播放 |
| cmd_executor | 命令执行 |
| frontend_tester | 前端测试 |
| glados_tts_plugin | GLaDOS语音合成 |
| napcat_qq | QQ机器人 |
| network_tools | 网络工具 |
| ollama_image | Ollama图像 |
| send_image | 发送图片 |
| speech_recognition | 语音识别 |
| task_manager | 任务管理 |
| tool_search | 工具搜索 |
---
## 8. 最佳实践
### 8.1 安全性
```python
# ❌ 避免使用危险函数
result = eval(user_input) # 危险!
# ✅ 安全的替代方案
import ast
try:
tree = ast.parse(user_input, mode='eval')
result = eval(compile(tree, '<string>', 'eval'))
except:
return "无效表达式"
```
### 8.2 输入验证
```python
def handle(self, args: str) -> str:
# 参数验证
if not args:
return "错误:参数不能为空"
# 长度限制
if len(args) > 10000:
return "错误:输入过长"
# 格式验证
if not re.match(r'^[\w\s\-\.]+$', args):
return "错误:包含非法字符"
# 继续处理...
```
### 8.3 错误处理
```python
def handle(self, args: str) -> str:
try:
# 业务逻辑
return "结果"
except FileNotFoundError:
return "错误:文件不存在"
except PermissionError:
return "错误:权限不足"
except Exception as e:
return f"错误: {str(e)}"
```
### 8.4 资源管理
```python
# 使用上下文管理器
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# 或确保资源释放
try:
f = open(file_path, 'r')
content = f.read()
finally:
f.close()
```
---
## 9. 调试与测试
### 9.1 日志记录
```python
def handle(self, args: str) -> str:
if self.cli:
print(f"[DEBUG] 插件被调用: {args}")
# 处理逻辑
return "结果"
```
### 9.2 单元测试
```python
import unittest
from plugins.file_manager import Plugin
class TestFileManager(unittest.TestCase):
def setUp(self):
self.plugin = Plugin()
def test_get_tool_info(self):
info = self.plugin.get_tool_info()
self.assertIn("name", info)
self.assertEqual(info["name"], "file_manager")
def test_handle_list(self):
result = self.plugin.handle("list .")
self.assertIsInstance(result, str)
if __name__ == "__main__":
unittest.main()
```
### 9.3 集成测试
```python
# integration_test.py
import sys
import os
sys.path.insert(0, os.path.dirname(__file__))
from ai_cli import AICLI
def test_integration():
cli = AICLI()
# 测试插件调用...
```
---
## 10. 常见问题
### Q1: 插件不加载?
检查以下几点:
- 文件是否放在 `plugins/` 目录
- 类名是否为 `Plugin`
- 是否实现了 `get_tool_info()` 和 `handle()` 方法
- 文件名是否为 `.py` 结尾
### Q2: 插件不被调用?
检查:
- 关键词是否匹配用户输入
- `keywords` 列表是否正确设置
- 插件是否成功加载(查看启动日志)
### Q3: MCP格式调用失败?
确保:
- 实现了 `get_mcp_definition()` 方法
- 实现了 `convert_mcp_args()` 方法(如果需要)
- `inputSchema` 格式正确
### Q4: 如何调试?
- 在 `handle()` 方法中添加 `print()` 语句
- 检查返回值是否为字符串
- 查看主程序日志
### Q5: 如何处理大文件?
```python
# 限制输出长度
MAX_LENGTH = 10000
if len(content) > MAX_LENGTH:
content = content[:MAX_LENGTH] + "\n... (内容已截断)"
```
---
## 附录:插件开发检查清单
- [ ] 插件类名是 `Plugin`
- [ ] 实现了 `get_tool_info()` 方法
- [ ] 实现了 `handle(args)` 方法
- [ ] 返回字符串结果
- [ ] 包含 `usage` 属性
- [ ] 可选:实现 `get_mcp_definition()` 支持 MCP
- [ ] 可选:实现 `convert_mcp_args()` 转换参数
- [ ] 所有异常都被捕获
- [ ] 遵循参数格式规范
- [ ] 遵循 PEP 8 代码规范
- [ ] 进行了基本测试