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:
commit
e22652fbbb
53 files changed
+20067
No files matched your search
+74
@@ -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
|
||||||
@@ -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` 命令查看帮助信息
|
||||||
@@ -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.
|
||||||
@@ -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/)
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# xiaoli cli
|
||||||
|
|
||||||
|
#请前往下载压缩包获取开源代码
|
||||||
|
|
||||||
|
#### 介绍
|
||||||
|
本仓库用于提交xiaoli系列源代码,本仓库源于github 2024年九月项目,因账户2fa码丢失,现已迁移至gitee
|
||||||
|
|
||||||
|
#### 软件架构
|
||||||
|
软件架构说明
|
||||||
|
|
||||||
|
|
||||||
|
#### 安装教程
|
||||||
|
|
||||||
|
1. 安装至少python 3.11以上版本
|
||||||
|
1. 或点击发行版内启动器快速安装
|
||||||
|
|
||||||
|
#### 使用说明
|
||||||
|
|
||||||
|
1. 直接点击启动器启动
|
||||||
@@ -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模型能力,为项目提供智能支持
|
||||||
|
|
||||||
|
**最后,感谢所有为技术进步付出努力的人,包括正在阅读这份文档的您。**
|
||||||
@@ -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
|
||||||
@@ -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'
|
||||||
|
]
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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)}"
|
||||||
@@ -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()
|
||||||
@@ -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)
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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 = []
|
||||||
@@ -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
|
||||||
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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}")
|
||||||
@@ -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')
|
||||||
@@ -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)}"
|
||||||
@@ -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
|
||||||
@@ -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()
|
||||||
@@ -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
|
||||||
@@ -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}%"""
|
||||||
@@ -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"))
|
||||||
@@ -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()
|
||||||
@@ -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
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -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)}"
|
||||||
@@ -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}"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"hello"
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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()
|
||||||
@@ -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)}"
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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()
|
||||||
@@ -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
|
||||||
@@ -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
File diff suppressed because it is too large.
Load diff
+738
@@ -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 代码规范
|
||||||
|
- [ ] 进行了基本测试
|
||||||
Reference in new issue
Block a user