hunian commited on
Commit ·
bdcdaf4
1
Parent(s): e0fe9a9
feat: integrate PluginMCPRegistry and add MCP tools to all plugins
Browse files- app/main.py +11 -30
- plugins/document_merge_split_plugin/core.py +58 -0
- plugins/document_merge_split_plugin/mcp.py +48 -0
- plugins/file_cache_plugin/core.py +58 -0
- plugins/file_cache_plugin/mcp.py +50 -0
- plugins/image_resizer_plugin/core.py +56 -0
- plugins/image_resizer_plugin/mcp.py +69 -0
- plugins/text_cleaner_plugin/core.py +34 -0
- plugins/text_cleaner_plugin/mcp.py +53 -0
app/main.py
CHANGED
|
@@ -17,7 +17,9 @@ from app.config.settings import settings
|
|
| 17 |
from app.api.routes import router as api_router
|
| 18 |
from app.plugins.manager import plugin_manager, set_app_instance
|
| 19 |
from app.plugins.loader import plugin_loader
|
| 20 |
-
from app.mcp.server import mcp, get_mcp_app
|
|
|
|
|
|
|
| 21 |
|
| 22 |
# 配置日志
|
| 23 |
_LOG_CONFIG = {
|
|
@@ -112,24 +114,26 @@ async def register_plugin_routes():
|
|
| 112 |
@asynccontextmanager
|
| 113 |
async def lifespan(app: FastAPI):
|
| 114 |
"""应用生命周期管理"""
|
| 115 |
-
# 启动时执行
|
| 116 |
logger.info(f"{settings.PROJECT_NAME} v{settings.VERSION} 正在启动...")
|
| 117 |
logger.info(f"插件目录: {settings.PLUGINS_DIR}")
|
| 118 |
-
logger.info(f"调试模式: {settings.DEBUG}")
|
| 119 |
|
| 120 |
-
# 注册插件路由
|
| 121 |
await register_plugin_routes()
|
| 122 |
|
| 123 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
mcp_app = get_mcp_app()
|
| 125 |
app.mount("/mcp", mcp_app)
|
| 126 |
logger.info("MCP服务已挂载到 /mcp 路径")
|
| 127 |
|
| 128 |
-
# 启动 MCP session manager
|
| 129 |
async with mcp.session_manager.run():
|
| 130 |
yield
|
| 131 |
|
| 132 |
-
# 关闭时执行
|
| 133 |
logger.info(f"{settings.PROJECT_NAME} 正在关闭...")
|
| 134 |
|
| 135 |
|
|
@@ -152,29 +156,6 @@ app.include_router(api_router)
|
|
| 152 |
set_app_instance(app)
|
| 153 |
|
| 154 |
|
| 155 |
-
# MCP 临时文件下载端点
|
| 156 |
-
@app.get("/mcp/file/{file_id}")
|
| 157 |
-
async def download_mcp_temp_file(request: Request, file_id: str):
|
| 158 |
-
"""下载 MCP 工具上传的临时文件。"""
|
| 159 |
-
from fastapi.responses import Response
|
| 160 |
-
|
| 161 |
-
result = await get_temp_file(file_id)
|
| 162 |
-
if result is None:
|
| 163 |
-
context = {
|
| 164 |
-
"request": request,
|
| 165 |
-
"error": "文件不存在或已过期",
|
| 166 |
-
"project_name": settings.PROJECT_NAME,
|
| 167 |
-
}
|
| 168 |
-
return templates.TemplateResponse(request, name="error.html", context=context, status_code=404)
|
| 169 |
-
|
| 170 |
-
content, filename = result
|
| 171 |
-
return Response(
|
| 172 |
-
content=content,
|
| 173 |
-
media_type="application/octet-stream",
|
| 174 |
-
headers={"Content-Disposition": f"attachment; filename={filename}"},
|
| 175 |
-
)
|
| 176 |
-
|
| 177 |
-
|
| 178 |
# 异常处理 - 修复模板上下文
|
| 179 |
@app.exception_handler(StarletteHTTPException)
|
| 180 |
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
|
|
|
|
| 17 |
from app.api.routes import router as api_router
|
| 18 |
from app.plugins.manager import plugin_manager, set_app_instance
|
| 19 |
from app.plugins.loader import plugin_loader
|
| 20 |
+
from app.mcp.server import mcp, get_mcp_app
|
| 21 |
+
from app.mcp.plugin_registry import init_plugin_registry, get_plugin_registry
|
| 22 |
+
from app.mcp.admin_tools import * # 确保管理工具被注册
|
| 23 |
|
| 24 |
# 配置日志
|
| 25 |
_LOG_CONFIG = {
|
|
|
|
| 114 |
@asynccontextmanager
|
| 115 |
async def lifespan(app: FastAPI):
|
| 116 |
"""应用生命周期管理"""
|
|
|
|
| 117 |
logger.info(f"{settings.PROJECT_NAME} v{settings.VERSION} 正在启动...")
|
| 118 |
logger.info(f"插件目录: {settings.PLUGINS_DIR}")
|
|
|
|
| 119 |
|
| 120 |
+
# 1. 注册插件路由(现有逻辑)
|
| 121 |
await register_plugin_routes()
|
| 122 |
|
| 123 |
+
# 2. 初始化插件MCP注册器并注册工具
|
| 124 |
+
registry = init_plugin_registry(mcp)
|
| 125 |
+
tool_count = registry.scan_and_register_all(settings.PLUGINS_DIR)
|
| 126 |
+
logger.info(f"已注册 {tool_count} 个插件MCP工具")
|
| 127 |
+
|
| 128 |
+
# 3. 挂载 MCP 应用
|
| 129 |
mcp_app = get_mcp_app()
|
| 130 |
app.mount("/mcp", mcp_app)
|
| 131 |
logger.info("MCP服务已挂载到 /mcp 路径")
|
| 132 |
|
| 133 |
+
# 4. 启动 MCP session manager
|
| 134 |
async with mcp.session_manager.run():
|
| 135 |
yield
|
| 136 |
|
|
|
|
| 137 |
logger.info(f"{settings.PROJECT_NAME} 正在关闭...")
|
| 138 |
|
| 139 |
|
|
|
|
| 156 |
set_app_instance(app)
|
| 157 |
|
| 158 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
# 异常处理 - 修复模板上下文
|
| 160 |
@app.exception_handler(StarletteHTTPException)
|
| 161 |
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
|
plugins/document_merge_split_plugin/core.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
文档合并拆分核心逻辑 - 供 mcp.py 和 api.py 共用
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import base64
|
| 6 |
+
from typing import List
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
async def do_merge(files_base64: List[str], output_format: str = "text") -> dict:
|
| 10 |
+
"""合并多个文档。"""
|
| 11 |
+
contents = []
|
| 12 |
+
for f_b64 in files_base64:
|
| 13 |
+
try:
|
| 14 |
+
content = base64.b64decode(f_b64).decode('utf-8')
|
| 15 |
+
contents.append(content)
|
| 16 |
+
except Exception as e:
|
| 17 |
+
return {
|
| 18 |
+
"success": False,
|
| 19 |
+
"message": f"解码失败: {str(e)}"
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
separator = "\n\n---\n\n" if output_format == "markdown" else "\n\n"
|
| 23 |
+
merged = separator.join(contents)
|
| 24 |
+
|
| 25 |
+
output_base64 = base64.b64encode(merged.encode('utf-8')).decode('utf-8')
|
| 26 |
+
|
| 27 |
+
return {
|
| 28 |
+
"success": True,
|
| 29 |
+
"output_base64": output_base64,
|
| 30 |
+
"message": f"已合并 {len(files_base64)} 个文档"
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
async def do_split(content_base64: str, split_by: str = "line", chunk_size: int = 100) -> dict:
|
| 35 |
+
"""拆分文档。"""
|
| 36 |
+
try:
|
| 37 |
+
content = base64.b64decode(content_base64).decode('utf-8')
|
| 38 |
+
except Exception as e:
|
| 39 |
+
return {
|
| 40 |
+
"success": False,
|
| 41 |
+
"message": f"解码失败: {str(e)}"
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
chunks = []
|
| 45 |
+
if split_by == "line":
|
| 46 |
+
chunks = content.split('\n')
|
| 47 |
+
elif split_by == "paragraph":
|
| 48 |
+
chunks = content.split('\n\n')
|
| 49 |
+
elif split_by == "chunk":
|
| 50 |
+
chunks = [content[i:i+chunk_size] for i in range(0, len(content), chunk_size)]
|
| 51 |
+
|
| 52 |
+
chunks_base64 = [base64.b64encode(c.encode('utf-8')).decode('utf-8') for c in chunks]
|
| 53 |
+
|
| 54 |
+
return {
|
| 55 |
+
"success": True,
|
| 56 |
+
"chunks_base64": chunks_base64,
|
| 57 |
+
"message": f"已拆分为 {len(chunks)} 个片段"
|
| 58 |
+
}
|
plugins/document_merge_split_plugin/mcp.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
文档合并拆分插件 MCP工具定义
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from app.mcp.decorators import mcp_tool
|
| 6 |
+
from pydantic import BaseModel, Field
|
| 7 |
+
from typing import List
|
| 8 |
+
|
| 9 |
+
from .core import do_merge, do_split
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class MergeInput(BaseModel):
|
| 13 |
+
"""文档合并输入参数"""
|
| 14 |
+
files_base64: List[str] = Field(description="待合并文件的Base64编码列表")
|
| 15 |
+
output_format: str = Field(default="text", description="输出格式:text、markdown")
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class SplitInput(BaseModel):
|
| 19 |
+
"""文档拆分输入参数"""
|
| 20 |
+
content_base64: str = Field(description="待拆分文档的Base64编码")
|
| 21 |
+
split_by: str = Field(default="line", description="拆分方式:line、paragraph、chunk")
|
| 22 |
+
chunk_size: int = Field(default=100, ge=10, le=10000, description="chunk模式下的块大小")
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class MergeOutput(BaseModel):
|
| 26 |
+
"""合并输出结果"""
|
| 27 |
+
success: bool = Field(description="操作是否成功")
|
| 28 |
+
output_base64: str | None = Field(description="合并结果的Base64编码")
|
| 29 |
+
message: str = Field(description="结果说明")
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class SplitOutput(BaseModel):
|
| 33 |
+
"""拆分输出结果"""
|
| 34 |
+
success: bool = Field(description="操作是否成功")
|
| 35 |
+
chunks_base64: List[str] | None = Field(description="拆分片段的Base64编码列表")
|
| 36 |
+
message: str = Field(description="结果说明")
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
@mcp_tool(name="merge", title="文档合并", description="合并多个文档内容")
|
| 40 |
+
async def doc_merge(params: MergeInput) -> MergeOutput:
|
| 41 |
+
result = await do_merge(params.files_base64, params.output_format)
|
| 42 |
+
return MergeOutput(**result)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
@mcp_tool(name="split", title="文档拆分", description="拆分文档为多个片段")
|
| 46 |
+
async def doc_split(params: SplitInput) -> SplitOutput:
|
| 47 |
+
result = await do_split(params.content_base64, params.split_by, params.chunk_size)
|
| 48 |
+
return SplitOutput(**result)
|
plugins/file_cache_plugin/core.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
文件缓存核心逻辑 - 供 mcp.py 和 api.py 共用
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import time
|
| 6 |
+
from typing import Optional
|
| 7 |
+
|
| 8 |
+
# 简化的内存缓存存储
|
| 9 |
+
_cache_store: dict = {}
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
async def do_set(key: str, value_base64: str, ttl_seconds: int = 3600) -> dict:
|
| 13 |
+
"""设置缓存值。"""
|
| 14 |
+
_cache_store[key] = {
|
| 15 |
+
"value": value_base64,
|
| 16 |
+
"expires_at": time.time() + ttl_seconds
|
| 17 |
+
}
|
| 18 |
+
return {
|
| 19 |
+
"success": True,
|
| 20 |
+
"message": f"缓存已设置,TTL {ttl_seconds}秒"
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
async def do_get(key: str) -> dict:
|
| 25 |
+
"""获取缓存值。"""
|
| 26 |
+
entry = _cache_store.get(key)
|
| 27 |
+
if not entry:
|
| 28 |
+
return {
|
| 29 |
+
"success": False,
|
| 30 |
+
"message": f"缓存键 {key} 不存在"
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
if time.time() > entry["expires_at"]:
|
| 34 |
+
del _cache_store[key]
|
| 35 |
+
return {
|
| 36 |
+
"success": False,
|
| 37 |
+
"message": f"缓存键 {key} 已过期"
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
return {
|
| 41 |
+
"success": True,
|
| 42 |
+
"value": entry["value"],
|
| 43 |
+
"message": "缓存获取成功"
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
async def do_delete(key: str) -> dict:
|
| 48 |
+
"""删除缓存值。"""
|
| 49 |
+
if key in _cache_store:
|
| 50 |
+
del _cache_store[key]
|
| 51 |
+
return {
|
| 52 |
+
"success": True,
|
| 53 |
+
"message": f"缓存键 {key} 已删除"
|
| 54 |
+
}
|
| 55 |
+
return {
|
| 56 |
+
"success": False,
|
| 57 |
+
"message": f"缓存键 {key} 不存在"
|
| 58 |
+
}
|
plugins/file_cache_plugin/mcp.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
文件缓存插件 MCP工具定义
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from app.mcp.decorators import mcp_tool
|
| 6 |
+
from pydantic import BaseModel, Field
|
| 7 |
+
|
| 8 |
+
from .core import do_set, do_get, do_delete
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class SetInput(BaseModel):
|
| 12 |
+
"""设置缓存输入参数"""
|
| 13 |
+
key: str = Field(description="缓存键名")
|
| 14 |
+
value_base64: str = Field(description="缓存值的Base64编码")
|
| 15 |
+
ttl_seconds: int = Field(default=3600, ge=60, le=86400, description="过期时间(秒)")
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class GetInput(BaseModel):
|
| 19 |
+
"""获取缓存输入参数"""
|
| 20 |
+
key: str = Field(description="缓存键名")
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class DeleteInput(BaseModel):
|
| 24 |
+
"""删除缓存输入参数"""
|
| 25 |
+
key: str = Field(description="缓存键名")
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class CacheOutput(BaseModel):
|
| 29 |
+
"""缓存操作输出结果"""
|
| 30 |
+
success: bool = Field(description="操作是否成功")
|
| 31 |
+
value: str | None = Field(default=None, description="缓存值(仅get操作)")
|
| 32 |
+
message: str = Field(description="结果说明")
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@mcp_tool(name="set", title="设置缓存", description="设置缓存值,支持TTL")
|
| 36 |
+
async def cache_set(params: SetInput) -> CacheOutput:
|
| 37 |
+
result = await do_set(params.key, params.value_base64, params.ttl_seconds)
|
| 38 |
+
return CacheOutput(**result)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
@mcp_tool(name="get", title="获取缓存", description="获取缓存值")
|
| 42 |
+
async def cache_get(params: GetInput) -> CacheOutput:
|
| 43 |
+
result = await do_get(params.key)
|
| 44 |
+
return CacheOutput(**result)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
@mcp_tool(name="delete", title="删除缓存", description="删除缓存值")
|
| 48 |
+
async def cache_delete(params: DeleteInput) -> CacheOutput:
|
| 49 |
+
result = await do_delete(params.key)
|
| 50 |
+
return CacheOutput(**result)
|
plugins/image_resizer_plugin/core.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
图片缩放核心逻辑 - 供 mcp.py 和 api.py 共用
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import io
|
| 6 |
+
import base64
|
| 7 |
+
from PIL import Image
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
async def do_resize(file_base64: str, width: int, height: int, format: str) -> dict:
|
| 11 |
+
"""执行图片缩放操作。
|
| 12 |
+
|
| 13 |
+
Args:
|
| 14 |
+
file_base64: 图片的Base64编码
|
| 15 |
+
width: 目标宽度
|
| 16 |
+
height: 目标高度
|
| 17 |
+
format: 输出格式(PNG、JPEG、WEBP)
|
| 18 |
+
|
| 19 |
+
Returns:
|
| 20 |
+
包含缩放结果的字典
|
| 21 |
+
"""
|
| 22 |
+
try:
|
| 23 |
+
# 解码Base64
|
| 24 |
+
image_data = base64.b64decode(file_base64)
|
| 25 |
+
image = Image.open(io.BytesIO(image_data))
|
| 26 |
+
|
| 27 |
+
# 检查源图片尺寸
|
| 28 |
+
if image.width < width or image.height < height:
|
| 29 |
+
return {
|
| 30 |
+
"success": False,
|
| 31 |
+
"message": f"源图片尺寸 ({image.width}x{image.height}) 小于目标尺寸 ({width}x{height})"
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
# 缩放图片
|
| 35 |
+
resized = image.resize((width, height), Image.Resampling.LANCZOS)
|
| 36 |
+
|
| 37 |
+
# 输出
|
| 38 |
+
output = io.BytesIO()
|
| 39 |
+
save_format = format.upper() if format.upper() != "JPEG" else "JPEG"
|
| 40 |
+
resized.save(output, format=save_format, quality=95)
|
| 41 |
+
output.seek(0)
|
| 42 |
+
|
| 43 |
+
# 编码输出
|
| 44 |
+
output_base64 = base64.b64encode(output.read()).decode('utf-8')
|
| 45 |
+
|
| 46 |
+
return {
|
| 47 |
+
"success": True,
|
| 48 |
+
"output_base64": output_base64,
|
| 49 |
+
"message": f"图片已缩放至 {width}x{height}"
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
except Exception as e:
|
| 53 |
+
return {
|
| 54 |
+
"success": False,
|
| 55 |
+
"message": f"缩放失败: {str(e)}"
|
| 56 |
+
}
|
plugins/image_resizer_plugin/mcp.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
图片缩放插件 MCP工具定义
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from app.mcp.decorators import mcp_tool
|
| 6 |
+
from pydantic import BaseModel, Field
|
| 7 |
+
|
| 8 |
+
from .core import do_resize
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
# ============================================================================
|
| 12 |
+
# 输入输出模型
|
| 13 |
+
# ============================================================================
|
| 14 |
+
|
| 15 |
+
class ResizeInput(BaseModel):
|
| 16 |
+
"""图片缩放工具输入参数"""
|
| 17 |
+
file_base64: str = Field(
|
| 18 |
+
description="图片文件的Base64编码",
|
| 19 |
+
examples=["iVBORw0KGgoAAAANSUhEUgAA..."]
|
| 20 |
+
)
|
| 21 |
+
width: int = Field(
|
| 22 |
+
ge=16, le=2048,
|
| 23 |
+
description="目标宽度(像素),范围16-2048"
|
| 24 |
+
)
|
| 25 |
+
height: int = Field(
|
| 26 |
+
ge=16, le=2048,
|
| 27 |
+
description="目标高度(像素),范围16-2048"
|
| 28 |
+
)
|
| 29 |
+
format: str = Field(
|
| 30 |
+
default="PNG",
|
| 31 |
+
description="输出格式:PNG、JPEG、WEBP"
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class ResizeOutput(BaseModel):
|
| 36 |
+
"""图片缩放工具输出结果"""
|
| 37 |
+
success: bool = Field(description="操作是否成功")
|
| 38 |
+
output_base64: str | None = Field(description="缩放后图片的Base64编码")
|
| 39 |
+
output_url: str | None = Field(default=None, description="临时下载链接(可选)")
|
| 40 |
+
message: str = Field(description="结果说明")
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
# ============================================================================
|
| 44 |
+
# 工具定义
|
| 45 |
+
# ============================================================================
|
| 46 |
+
|
| 47 |
+
@mcp_tool(
|
| 48 |
+
name="resize",
|
| 49 |
+
title="图片缩放",
|
| 50 |
+
description="缩放图片到指定尺寸,支持PNG、JPEG、WEBP格式输出",
|
| 51 |
+
annotations={
|
| 52 |
+
"readOnlyHint": False,
|
| 53 |
+
"destructiveHint": False,
|
| 54 |
+
}
|
| 55 |
+
)
|
| 56 |
+
async def resize_image(params: ResizeInput) -> ResizeOutput:
|
| 57 |
+
"""将图片缩放到指定的宽度和高度。
|
| 58 |
+
|
| 59 |
+
支持PNG、JPEG、WEBP格式输出。
|
| 60 |
+
输入图片尺寸必须大于目标尺寸。
|
| 61 |
+
|
| 62 |
+
Args:
|
| 63 |
+
params: 包含Base64图片数据、目标宽高、输出格式
|
| 64 |
+
|
| 65 |
+
Returns:
|
| 66 |
+
ResizeOutput: 包含缩放后的图片数据或错误信息
|
| 67 |
+
"""
|
| 68 |
+
result = await do_resize(params.file_base64, params.width, params.height, params.format)
|
| 69 |
+
return ResizeOutput(**result)
|
plugins/text_cleaner_plugin/core.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
文本清理核心逻辑 - 供 mcp.py 和 api.py 共用
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import re
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
async def do_clean(text: str, operations: list) -> dict:
|
| 9 |
+
"""执行文本清理操作。
|
| 10 |
+
|
| 11 |
+
Args:
|
| 12 |
+
text: 待清理的文本
|
| 13 |
+
operations: 清理操作列表(trim、remove_empty_lines、normalize_spaces等)
|
| 14 |
+
|
| 15 |
+
Returns:
|
| 16 |
+
包含清理结果的字典
|
| 17 |
+
"""
|
| 18 |
+
result = text
|
| 19 |
+
|
| 20 |
+
for op in operations:
|
| 21 |
+
if op == "trim":
|
| 22 |
+
result = result.strip()
|
| 23 |
+
elif op == "remove_empty_lines":
|
| 24 |
+
result = re.sub(r'\n\s*\n', '\n', result)
|
| 25 |
+
elif op == "normalize_spaces":
|
| 26 |
+
result = re.sub(r'[ \t]+', ' ', result)
|
| 27 |
+
elif op == "remove_extra_newlines":
|
| 28 |
+
result = re.sub(r'\n+', '\n', result)
|
| 29 |
+
|
| 30 |
+
return {
|
| 31 |
+
"success": True,
|
| 32 |
+
"output": result,
|
| 33 |
+
"message": f"已执行 {len(operations)} 个清理操作"
|
| 34 |
+
}
|
plugins/text_cleaner_plugin/mcp.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
文本清理插件 MCP工具定义
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from app.mcp.decorators import mcp_tool
|
| 6 |
+
from pydantic import BaseModel, Field
|
| 7 |
+
from typing import List
|
| 8 |
+
|
| 9 |
+
from .core import do_clean
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class CleanInput(BaseModel):
|
| 13 |
+
"""文本清理工具输入参数"""
|
| 14 |
+
text: str = Field(description="待清理的文本内容")
|
| 15 |
+
operations: List[str] = Field(
|
| 16 |
+
default=["trim", "normalize_spaces"],
|
| 17 |
+
description="清理操作列表:trim、remove_empty_lines、normalize_spaces、remove_extra_newlines"
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class CleanOutput(BaseModel):
|
| 22 |
+
"""文本清理工具输出结果"""
|
| 23 |
+
success: bool = Field(description="操作是否成功")
|
| 24 |
+
output: str | None = Field(description="清理后的文本")
|
| 25 |
+
message: str = Field(description="结果说明")
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@mcp_tool(
|
| 29 |
+
name="clean",
|
| 30 |
+
title="文本清理",
|
| 31 |
+
description="清理文本内容,支持多种清理操作",
|
| 32 |
+
annotations={
|
| 33 |
+
"readOnlyHint": False,
|
| 34 |
+
"destructiveHint": False,
|
| 35 |
+
}
|
| 36 |
+
)
|
| 37 |
+
async def clean_text(params: CleanInput) -> CleanOutput:
|
| 38 |
+
"""清理文本内容。
|
| 39 |
+
|
| 40 |
+
支持的清理操作:
|
| 41 |
+
- trim: 去除首尾空白
|
| 42 |
+
- remove_empty_lines: 移除空行
|
| 43 |
+
- normalize_spaces: 规范化空格
|
| 44 |
+
- remove_extra_newlines: 移除多余换行
|
| 45 |
+
|
| 46 |
+
Args:
|
| 47 |
+
params: 包含文本和清理操作列表
|
| 48 |
+
|
| 49 |
+
Returns:
|
| 50 |
+
CleanOutput: 清理后的文本
|
| 51 |
+
"""
|
| 52 |
+
result = await do_clean(params.text, params.operations)
|
| 53 |
+
return CleanOutput(**result)
|