chore: 整理插件分类 - 删除图像类,移动娱乐类
删除(图像类): - ai_image.py (AI生图) - image_generator.py (ComfyUI生图) - send_image.py (远程发图) 移至插件仓库(娱乐/可选): - color_picker.py - bookmark_manager.py - audio_player.py 保留24个必备生产力插件: 核心: code_editor/git_tools/file_manager/cmd_executor/terminal 工程: auto_engineer/frontend_tester/workflow/sub_agent 搜索: ai_search/tool_search/network_tools 自动化: browser_auto/gui_auto 数据: json_toolkit/regex_tester/markdown_tool/id_generator/snippet_manager 系统: scheduler/task_manager/memory_plugin/operit_bridge/speech_recognition 更新 README.md/plugin_registry.json/plugins_config.py
This commit is contained in:
1 parent
5ea19b75ae
commit
125e00a485
10 files changed
+194
-2425
No files matched your search
@@ -94,7 +94,7 @@ launcher.exe
|
||||
}
|
||||
```
|
||||
|
||||
## 插件列表 (19 个)
|
||||
## 插件列表 (24 个)
|
||||
|
||||
> 更多社区插件请访问 [xiaoli-cli-plugins](https://gitee.com/shuiyu1123/xiaoli-cli-plugins),使用 `/plugin install <插件码>` 一键安装。
|
||||
|
||||
@@ -114,24 +114,28 @@ launcher.exe
|
||||
| 插件 | 操作数 | 说明 |
|
||||
|------|--------|------|
|
||||
| `sub_agent` | 8 | 子 Agent 系统。spawn/list/status/result/send/kill,多 Agent 并行协作 |
|
||||
| `browser_auto` | 28 | 浏览器自动化。导航/点击/填写/截图/JS执行/PDF/cookies(基于 Playwright) |
|
||||
| `gui_auto` | 20 | 桌面 GUI 自动化。基于 Windows UIA,屏幕元素识别/点击/输入/截图(仅 Windows) |
|
||||
| `ai_image` | — | AI 生图。基于 ai.2x.nz 自然语言生图,异步生成 + WebSocket 进度推送 |
|
||||
| `image_generator` | 7 | 图像生成。基于 ComfyUI 在线服务,自然语言/Tag 双模式,28 个角色工作流 |
|
||||
| `memory_plugin` | 11 | 记忆系统。AI 写日记/搜索记忆/管理聊天记录/长期记忆 |
|
||||
| `scheduler` | 4 | 定时任务。支持相对时间/绝对时间/重复任务,到期自动激活 AI |
|
||||
| `tool_search` | — | 工具搜索。按关键词发现可用插件 |
|
||||
| `terminal` | 8 | 终端模拟器。AI 的虚拟键盘和屏幕,打字/按键/看屏幕,支持 Python REPL |
|
||||
| `operit_bridge` | — | Operit 双向工具桥接。解析 47 个 Operit 脚本,288 个工具,格式转换 |
|
||||
|
||||
### 辅助工具
|
||||
|
||||
| 插件 | 说明 |
|
||||
|------|------|
|
||||
| `browser_auto` | 浏览器自动化。导航/点击/填写/截图/JS执行(基于 Playwright) |
|
||||
| `gui_auto` | 桌面 GUI 自动化。Windows UIA 元素识别/点击/输入(仅 Windows) |
|
||||
| `memory_plugin` | 记忆系统。AI 写日记/搜索记忆/管理聊天记录 |
|
||||
| `scheduler` | 定时任务。支持相对/绝对/重复任务,到期自动激活 AI |
|
||||
| `tool_search` | 工具搜索。按关键词发现可用插件 |
|
||||
| `workflow` | 工作流引擎。YAML 多步骤自动化,变量传递,条件分支 |
|
||||
| `network_tools` | 网络工具。ping/get/status/headers/ip |
|
||||
| `ai_search` | AI 搜索。jina.ai 智能搜索 + 网页内容提取 |
|
||||
| `frontend_tester` | 前端测试。HTML/CSS/JS 语法检查、响应式验证 |
|
||||
| `speech_recognition` | 语音识别。录音 + 语音转文字(Whisper) |
|
||||
| `audio_player` | 音频播放 |
|
||||
| `send_image` | 远程模式下发图片到手机 |
|
||||
| `json_toolkit` | JSON 工具箱。格式化/压缩/校验/路径查询/差异比较 |
|
||||
| `regex_tester` | 正则表达式测试器。匹配/提取/替换/解释 |
|
||||
| `snippet_manager` | 代码片段管理器。保存/搜索/复用常用代码 |
|
||||
| `markdown_tool` | Markdown 工具。TOC/统计/表格生成/待办提取 |
|
||||
| `id_generator` | ID 生成器。UUID/NanoID/短ID/雪花ID/ULID |
|
||||
|
||||
## 记忆系统
|
||||
|
||||
@@ -285,7 +289,8 @@ xiaoli-cli/
|
||||
│ ├── ai_search.py
|
||||
│ ├── frontend_tester.py
|
||||
│ ├── speech_recognition.py
|
||||
│ └── send_image.py
|
||||
│ ├── terminal.py
|
||||
│ └── operit_bridge.py
|
||||
├── skills/ # 技能
|
||||
├── image_engine/ # 图像识别引擎
|
||||
├── tests/ # 测试套件 (150+ 用例)
|
||||
|
||||
+37
-7
@@ -10,9 +10,20 @@
|
||||
"file": "dglab_ws.py",
|
||||
"url": "https://gitee.com/shuiyu1123/xiaoli-cli-plugins/raw/master/dglab_ws/dglab_ws.py",
|
||||
"docs": "https://gitee.com/shuiyu1123/xiaoli-cli-plugins/raw/master/dglab_ws/README.md",
|
||||
"deps": ["websocket-client"],
|
||||
"optional_deps": ["qrcode"],
|
||||
"keywords": ["dg-lab", "dglab", "郊狼", "coyote", "websocket", "脉冲"]
|
||||
"deps": [
|
||||
"websocket-client"
|
||||
],
|
||||
"optional_deps": [
|
||||
"qrcode"
|
||||
],
|
||||
"keywords": [
|
||||
"dg-lab",
|
||||
"dglab",
|
||||
"郊狼",
|
||||
"coyote",
|
||||
"websocket",
|
||||
"脉冲"
|
||||
]
|
||||
},
|
||||
"operit_bridge": {
|
||||
"name": "Operit Bridge 双向工具桥接",
|
||||
@@ -24,7 +35,15 @@
|
||||
"docs": "",
|
||||
"deps": [],
|
||||
"optional_deps": [],
|
||||
"keywords": ["operit", "bridge", "桥接", "兼容", "格式转换", "METADATA", "插件互通"]
|
||||
"keywords": [
|
||||
"operit",
|
||||
"bridge",
|
||||
"桥接",
|
||||
"兼容",
|
||||
"格式转换",
|
||||
"METADATA",
|
||||
"插件互通"
|
||||
]
|
||||
},
|
||||
"onebot": {
|
||||
"name": "OneBot v11 QQ 机器人",
|
||||
@@ -34,9 +53,20 @@
|
||||
"file": "onebot.py",
|
||||
"url": "https://gitee.com/shuiyu1123/xiaoli-cli-plugins/raw/master/onebot/onebot.py",
|
||||
"docs": "https://gitee.com/shuiyu1123/xiaoli-cli-plugins/raw/master/onebot/README.md",
|
||||
"deps": ["websocket-client"],
|
||||
"deps": [
|
||||
"websocket-client"
|
||||
],
|
||||
"optional_deps": [],
|
||||
"keywords": ["onebot", "qq", "机器人", "bot", "go-cqhttp", "lagrange", "群聊", "私聊"]
|
||||
"keywords": [
|
||||
"onebot",
|
||||
"qq",
|
||||
"机器人",
|
||||
"bot",
|
||||
"go-cqhttp",
|
||||
"lagrange",
|
||||
"群聊",
|
||||
"私聊"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,589 +0,0 @@
|
||||
"""
|
||||
AI 生图插件 - 基于 ai.2x.nz 自然语言生图接口
|
||||
支持自然语言描述、直接 Tag、工作流切换、画风选择等
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
import threading
|
||||
import hashlib
|
||||
import re
|
||||
from datetime import datetime
|
||||
from urllib.parse import quote
|
||||
|
||||
try:
|
||||
import requests
|
||||
REQUESTS_AVAILABLE = True
|
||||
except ImportError:
|
||||
REQUESTS_AVAILABLE = False
|
||||
|
||||
try:
|
||||
import websocket
|
||||
WEBSOCKET_AVAILABLE = True
|
||||
except ImportError:
|
||||
WEBSOCKET_AVAILABLE = False
|
||||
|
||||
|
||||
class Liugin:
|
||||
"""AI 生图插件 - 调用 ai.2x.nz 接口生成图片"""
|
||||
|
||||
BASE_URL = "https://ai.2x.nz"
|
||||
WS_URL = "wss://ai.2x.nz/ws/run"
|
||||
API_URL = "https://ai.2x.nz/api"
|
||||
|
||||
# 保存目录
|
||||
DEFAULT_SAVE_DIR = "generated_images"
|
||||
|
||||
def __init__(self):
|
||||
self.usage = """AI 生图工具 - 基于自然语言描述生成图片
|
||||
|
||||
基本用法:
|
||||
generate <描述> - 用自然语言描述生成图片
|
||||
tag <直接Tag> - 用 Danbooru Tag 生成图片
|
||||
both <Tag> <<<>>> <描述> - 同时使用 Tag 和自然语言
|
||||
|
||||
高级用法:
|
||||
workflows - 列出所有可用工作流
|
||||
switch <工作流名> - 切换工作流
|
||||
width <数值> - 设置图片宽度 (最大1344)
|
||||
height <数值> - 设置图片高度 (最大1344)
|
||||
size <宽> <高> - 同时设置宽高
|
||||
rewrite on|off - 改写模式开关
|
||||
override on|off - 覆写模式开关
|
||||
status - 查看当前状态和配置
|
||||
|
||||
示例:
|
||||
ai_image generate 一个可爱的猫耳少女,樱花背景
|
||||
ai_image tag 1girl, cat_ears, cherry_blossoms, smile
|
||||
ai_image both 1girl, cat_ears <<<>>> 一个可爱的猫耳少女在樱花树下微笑
|
||||
ai_image workflows
|
||||
ai_image switch WAI - 37.json
|
||||
ai_image size 1024 1024
|
||||
ai_image rewrite on
|
||||
"""
|
||||
self.cli = None
|
||||
self.save_dir = self.DEFAULT_SAVE_DIR
|
||||
|
||||
# 配置状态
|
||||
self.current_workflow = None
|
||||
self.direct_prompt = ""
|
||||
self.nl_prompt = ""
|
||||
self.rewrite = False
|
||||
self.override = False
|
||||
self.width = None
|
||||
self.height = None
|
||||
self.workflows_cache = None
|
||||
self.is_generating = False
|
||||
|
||||
def set_cli(self, cli):
|
||||
"""设置 CLI 实例引用"""
|
||||
self.cli = cli
|
||||
# 注册插件命令
|
||||
if hasattr(cli, 'register_liugin_command'):
|
||||
cli.register_liugin_command('ai_image', self.command_handler)
|
||||
|
||||
def get_tool_info(self):
|
||||
return {
|
||||
"name": "ai_image",
|
||||
"description": "AI 生图工具 — 基于 ai.2x.nz 接口,支持自然语言描述和 Danbooru Tag 生成图片。当用户想要生成图片、画图、AI绘画、生图时使用此工具。",
|
||||
"keywords": [
|
||||
"生图", "画图", "图片", "AI绘画", "生成图片", "image", "generate",
|
||||
"draw", "绘画", "插画", "二次元", "动漫", "Danbooru", "tag",
|
||||
"工作流", "workflow", "ComfyUI"
|
||||
],
|
||||
"usage": self.usage
|
||||
}
|
||||
|
||||
def get_mcp_definition(self):
|
||||
return {
|
||||
"name": "ai_image",
|
||||
"description": "AI 生图工具,基于 ai.2x.nz 接口生成图片",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"operation": {
|
||||
"type": "string",
|
||||
"enum": ["generate", "tag", "both", "workflows", "switch", "size", "status"],
|
||||
"description": "操作类型"
|
||||
},
|
||||
"prompt": {
|
||||
"type": "string",
|
||||
"description": "生成提示词"
|
||||
}
|
||||
},
|
||||
"required": ["operation"]
|
||||
}
|
||||
}
|
||||
|
||||
def convert_mcp_args(self, arguments):
|
||||
op = arguments.get("operation", "")
|
||||
prompt = arguments.get("prompt", "")
|
||||
return f"{op} {prompt}"
|
||||
|
||||
def command_handler(self, args):
|
||||
"""处理 /ai_image 命令"""
|
||||
return self.handle(args)
|
||||
|
||||
def handle(self, args):
|
||||
"""处理 AI 生图请求"""
|
||||
if not REQUESTS_AVAILABLE:
|
||||
return "❌ 缺少 requests 库,请运行: pip install requests"
|
||||
|
||||
args = args.strip()
|
||||
if not args:
|
||||
return self.usage
|
||||
|
||||
# 解析操作
|
||||
parts = args.split(maxsplit=1)
|
||||
operation = parts[0].lower()
|
||||
remaining = parts[1] if len(parts) > 1 else ""
|
||||
|
||||
try:
|
||||
if operation in ["generate", "gen", "g"]:
|
||||
return self._cmd_generate(remaining)
|
||||
elif operation in ["tag", "t", "direct", "d"]:
|
||||
return self._cmd_tag(remaining)
|
||||
elif operation in ["both", "b"]:
|
||||
return self._cmd_both(remaining)
|
||||
elif operation in ["workflows", "wf", "list"]:
|
||||
return self._cmd_workflows()
|
||||
elif operation in ["switch", "sw"]:
|
||||
return self._cmd_switch(remaining)
|
||||
elif operation in ["width", "w"]:
|
||||
return self._cmd_set_width(remaining)
|
||||
elif operation in ["height", "h"]:
|
||||
return self._cmd_set_height(remaining)
|
||||
elif operation in ["size", "s"]:
|
||||
return self._cmd_set_size(remaining)
|
||||
elif operation in ["rewrite", "rw"]:
|
||||
return self._cmd_set_rewrite(remaining)
|
||||
elif operation in ["override", "ov"]:
|
||||
return self._cmd_set_override(remaining)
|
||||
elif operation in ["status", "st"]:
|
||||
return self._cmd_status()
|
||||
elif operation in ["help", "?"]:
|
||||
return self.usage
|
||||
else:
|
||||
# 尝试当作自然语言描述处理
|
||||
return self._cmd_generate(args)
|
||||
except Exception as e:
|
||||
return f"❌ 操作失败: {str(e)}"
|
||||
|
||||
# ── 命令实现 ──
|
||||
|
||||
def _cmd_generate(self, prompt):
|
||||
"""自然语言生成图片"""
|
||||
if not prompt.strip():
|
||||
return "❌ 请提供描述内容。示例: ai_image generate 一个可爱的猫耳少女"
|
||||
|
||||
self.nl_prompt = prompt.strip()
|
||||
self.direct_prompt = ""
|
||||
self.override = False
|
||||
return self._run_generation()
|
||||
|
||||
def _cmd_tag(self, tags):
|
||||
"""使用直接 Tag 生成图片"""
|
||||
if not tags.strip():
|
||||
return "❌ 请提供 Tag 内容。示例: ai_image tag 1girl, cat_ears, smile"
|
||||
|
||||
self.direct_prompt = tags.strip()
|
||||
self.nl_prompt = ""
|
||||
self.override = True
|
||||
return self._run_generation()
|
||||
|
||||
def _cmd_both(self, args):
|
||||
"""同时使用 Tag 和自然语言"""
|
||||
if "<<<>>>" in args:
|
||||
parts = args.split("<<<>>>", 1)
|
||||
self.direct_prompt = parts[0].strip()
|
||||
self.nl_prompt = parts[1].strip()
|
||||
else:
|
||||
# 尝试用 || 分隔
|
||||
if "||" in args:
|
||||
parts = args.split("||", 1)
|
||||
self.direct_prompt = parts[0].strip()
|
||||
self.nl_prompt = parts[1].strip()
|
||||
else:
|
||||
return "❌ 格式错误。用法: ai_image both <Tag> <<<>>> <描述>"
|
||||
|
||||
self.override = False
|
||||
return self._run_generation()
|
||||
|
||||
def _cmd_workflows(self):
|
||||
"""列出所有工作流"""
|
||||
workflows = self._fetch_workflows()
|
||||
if not workflows:
|
||||
return "❌ 获取工作流列表失败"
|
||||
|
||||
result = f"📂 共 {len(workflows)} 个工作流:\n\n"
|
||||
for i, wf in enumerate(workflows, 1):
|
||||
name = wf.get("path", "未知")
|
||||
current = " ← [当前]" if name == self.current_workflow else ""
|
||||
result += f" {i:2d}. {name}{current}\n"
|
||||
|
||||
result += f"\n💡 使用 'ai_image switch <工作流名>' 切换工作流"
|
||||
return result
|
||||
|
||||
def _cmd_switch(self, name):
|
||||
"""切换工作流"""
|
||||
if not name.strip():
|
||||
return "❌ 请提供工作流名称。先用 'ai_image workflows' 查看列表"
|
||||
|
||||
# 模糊匹配
|
||||
workflows = self._fetch_workflows()
|
||||
if not workflows:
|
||||
return "❌ 获取工作流列表失败"
|
||||
|
||||
matched = None
|
||||
name_lower = name.strip().lower()
|
||||
for wf in workflows:
|
||||
wf_path = wf.get("path", "")
|
||||
if name_lower == wf_path.lower():
|
||||
matched = wf_path
|
||||
break
|
||||
if name_lower in wf_path.lower():
|
||||
matched = wf_path
|
||||
|
||||
if not matched:
|
||||
# 尝试数字索引
|
||||
try:
|
||||
idx = int(name.strip()) - 1
|
||||
if 0 <= idx < len(workflows):
|
||||
matched = workflows[idx].get("path", "")
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if not matched:
|
||||
return f"❌ 未找到匹配的工作流: {name}\n💡 先用 'ai_image workflows' 查看列表"
|
||||
|
||||
self.current_workflow = matched
|
||||
return f"✅ 已切换工作流: {matched}"
|
||||
|
||||
def _cmd_set_width(self, val):
|
||||
"""设置宽度"""
|
||||
try:
|
||||
w = int(val.strip())
|
||||
if w < 64 or w > 1344:
|
||||
return "❌ 宽度范围: 64 ~ 1344"
|
||||
self.width = w
|
||||
return f"✅ 宽度已设置为: {w}"
|
||||
except ValueError:
|
||||
return "❌ 请输入有效数字"
|
||||
|
||||
def _cmd_set_height(self, val):
|
||||
"""设置高度"""
|
||||
try:
|
||||
h = int(val.strip())
|
||||
if h < 64 or h > 1344:
|
||||
return "❌ 高度范围: 64 ~ 1344"
|
||||
self.height = h
|
||||
return f"✅ 高度已设置为: {h}"
|
||||
except ValueError:
|
||||
return "❌ 请输入有效数字"
|
||||
|
||||
def _cmd_set_size(self, val):
|
||||
"""同时设置宽高"""
|
||||
parts = val.strip().split()
|
||||
if len(parts) < 2:
|
||||
return "❌ 用法: ai_image size <宽> <高>"
|
||||
try:
|
||||
w, h = int(parts[0]), int(parts[1])
|
||||
if w < 64 or w > 1344 or h < 64 or h > 1344:
|
||||
return "❌ 宽高范围: 64 ~ 1344"
|
||||
self.width = w
|
||||
self.height = h
|
||||
return f"✅ 尺寸已设置为: {w} x {h}"
|
||||
except ValueError:
|
||||
return "❌ 请输入有效数字"
|
||||
|
||||
def _cmd_set_rewrite(self, val):
|
||||
"""设置改写模式"""
|
||||
val = val.strip().lower()
|
||||
if val in ["on", "开", "1", "true", "yes"]:
|
||||
self.rewrite = True
|
||||
return "✅ 改写模式已开启 — LLM 将智能重写 prompt"
|
||||
elif val in ["off", "关", "0", "false", "no"]:
|
||||
self.rewrite = False
|
||||
return "✅ 改写模式已关闭 — 普通翻译/追加模式"
|
||||
else:
|
||||
self.rewrite = not self.rewrite
|
||||
status = "开启" if self.rewrite else "关闭"
|
||||
return f"✅ 改写模式已{status}"
|
||||
|
||||
def _cmd_set_override(self, val):
|
||||
"""设置覆写模式"""
|
||||
val = val.strip().lower()
|
||||
if val in ["on", "开", "1", "true", "yes"]:
|
||||
self.override = True
|
||||
return "✅ 覆写模式已开启 — 忽略工作流内置 prompt"
|
||||
elif val in ["off", "关", "0", "false", "no"]:
|
||||
self.override = False
|
||||
return "✅ 覆写模式已关闭 — 保留工作流内置 prompt"
|
||||
else:
|
||||
self.override = not self.override
|
||||
status = "开启" if self.override else "关闭"
|
||||
return f"✅ 覆写模式已{status}"
|
||||
|
||||
def _cmd_status(self):
|
||||
"""查看当前状态"""
|
||||
result = " AI 生图状态:\n\n"
|
||||
result += f" 工作流: {self.current_workflow or '(默认)'}\n"
|
||||
result += f" 直接 Tag: {self.direct_prompt or '(未设置)'}\n"
|
||||
result += f" 自然语言: {self.nl_prompt or '(未设置)'}\n"
|
||||
result += f" 改写模式: {'✅ 开启' if self.rewrite else '❌ 关闭'}\n"
|
||||
result += f" 覆写模式: {'✅ 开启' if self.override else '❌ 关闭'}\n"
|
||||
result += f" 宽度: {self.width or '(默认)'}\n"
|
||||
result += f" 高度: {self.height or '(默认)'}\n"
|
||||
result += f" 保存目录: {self.save_dir}\n"
|
||||
result += f" 生成中: {'是' if self.is_generating else '否'}\n"
|
||||
return result
|
||||
|
||||
# ── 核心生图逻辑 ──
|
||||
|
||||
def _fetch_workflows(self):
|
||||
"""获取工作流列表"""
|
||||
if self.workflows_cache:
|
||||
return self.workflows_cache
|
||||
|
||||
try:
|
||||
resp = requests.get(f"{self.API_URL}/workflows", timeout=15)
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
self.workflows_cache = data.get("workflows", [])
|
||||
return self.workflows_cache
|
||||
except Exception as e:
|
||||
pass
|
||||
return []
|
||||
|
||||
def _run_generation(self):
|
||||
"""执行图片生成"""
|
||||
if self.is_generating:
|
||||
return "⚠️ 已有生成任务在进行中,请等待完成"
|
||||
|
||||
if not self.direct_prompt and not self.nl_prompt:
|
||||
return "❌ 请提供至少一种 prompt(直接 Tag 或自然语言描述)"
|
||||
|
||||
self.is_generating = True
|
||||
result_lines = []
|
||||
image_paths = []
|
||||
|
||||
try:
|
||||
# 获取默认工作流(如果未指定则使用列表中第一个)
|
||||
workflow = self.current_workflow
|
||||
if not workflow:
|
||||
workflows = self._fetch_workflows()
|
||||
if workflows:
|
||||
# 优先选 "无Lora" 工作流,否则用第一个
|
||||
for wf in workflows:
|
||||
if "无lora" in wf.get("path", "").lower():
|
||||
workflow = wf["path"]
|
||||
break
|
||||
if not workflow:
|
||||
workflow = workflows[0].get("path", "")
|
||||
|
||||
# 构建 payload
|
||||
payload = {
|
||||
"workflow_path": workflow or "",
|
||||
"inline_workflow": None,
|
||||
"direct_prompt": self.direct_prompt,
|
||||
"nl_prompt": self.nl_prompt,
|
||||
"rewrite": self.rewrite,
|
||||
"override": self.override,
|
||||
"width": self.width,
|
||||
"height": self.height,
|
||||
}
|
||||
|
||||
result_lines.append(" 正在连接 ai.2x.nz ...")
|
||||
result_lines.append(f" 直接 Tag: {self.direct_prompt[:80] or '(无)'}")
|
||||
result_lines.append(f" 自然语言: {self.nl_prompt[:80] or '(无)'}")
|
||||
result_lines.append(f" 工作流: {workflow or '(默认)'}")
|
||||
result_lines.append("")
|
||||
|
||||
# 使用同步方式通过 REST + 轮询获取结果
|
||||
# 由于 websocket-client 可能未安装,提供降级方案
|
||||
if WEBSOCKET_AVAILABLE:
|
||||
images = self._generate_via_ws(payload, result_lines)
|
||||
else:
|
||||
# 降级:尝试使用 requests + SSE 或提示安装
|
||||
result_lines.append("⚠️ websocket-client 未安装,尝试安装...")
|
||||
images = self._generate_via_ws_fallback(payload, result_lines)
|
||||
|
||||
if images:
|
||||
image_paths = images
|
||||
result_lines.append(f"\n✅ 生成完成!共 {len(images)} 张图片")
|
||||
for i, path in enumerate(images, 1):
|
||||
result_lines.append(f" {i}. {path}")
|
||||
else:
|
||||
result_lines.append("\n❌ 未生成任何图片")
|
||||
|
||||
except Exception as e:
|
||||
result_lines.append(f"\n❌ 生成失败: {str(e)}")
|
||||
finally:
|
||||
self.is_generating = False
|
||||
|
||||
return "\n".join(result_lines)
|
||||
|
||||
def _generate_via_ws(self, payload, result_lines):
|
||||
"""通过 WebSocket 生成图片"""
|
||||
images = []
|
||||
final_prompt = ""
|
||||
error_msg = None
|
||||
|
||||
ws_result = {"images": [], "error": None, "final_prompt": "", "done": False}
|
||||
|
||||
def on_message(ws, message):
|
||||
try:
|
||||
m = json.loads(message)
|
||||
msg_type = m.get("type", "")
|
||||
|
||||
if msg_type == "log":
|
||||
result_lines.append(f" {m.get('message', '')}")
|
||||
elif msg_type == "llm_start":
|
||||
result_lines.append(" LLM 正在处理 prompt...")
|
||||
elif msg_type == "llm_chunk":
|
||||
pass # 流式输出,不逐块显示
|
||||
elif msg_type == "llm_done":
|
||||
result_lines.append(" LLM 处理完成")
|
||||
elif msg_type == "progress":
|
||||
node = m.get("node", "")
|
||||
value = m.get("value", 0)
|
||||
max_val = m.get("max", 0)
|
||||
if max_val and max_val > 1:
|
||||
pct = int(value * 100 / max_val)
|
||||
result_lines.append(f"⏳ 进度: {node} {value}/{max_val} ({pct}%)")
|
||||
else:
|
||||
result_lines.append(f"⏳ 执行: {node}")
|
||||
elif msg_type == "prompt_id":
|
||||
fp = m.get("final_prompt", "")
|
||||
if fp:
|
||||
ws_result["final_prompt"] = fp
|
||||
result_lines.append(f"\n 最终 Prompt:\n{fp[:300]}{'...' if len(fp) > 300 else ''}\n")
|
||||
elif msg_type == "image":
|
||||
url = m.get("url", "")
|
||||
filename = m.get("filename", "")
|
||||
if url:
|
||||
# 下载图片
|
||||
saved = self._download_image(url, filename)
|
||||
if saved:
|
||||
ws_result["images"].append(saved)
|
||||
result_lines.append(f" 收到图片: {filename}")
|
||||
elif msg_type == "done":
|
||||
count = m.get("count", 0)
|
||||
result_lines.append(f"✅ 服务端完成,共 {count} 张")
|
||||
ws_result["done"] = True
|
||||
ws.close()
|
||||
elif msg_type == "error":
|
||||
ws_result["error"] = m.get("message", "未知错误")
|
||||
result_lines.append(f"❌ 错误: {ws_result['error']}")
|
||||
ws.close()
|
||||
except Exception as e:
|
||||
result_lines.append(f"⚠️ 解析消息出错: {e}")
|
||||
|
||||
def on_error(ws, error):
|
||||
ws_result["error"] = str(error)
|
||||
result_lines.append(f"❌ WebSocket 错误: {error}")
|
||||
|
||||
def on_close(ws, close_status_code, close_msg):
|
||||
ws_result["done"] = True
|
||||
|
||||
def on_open(ws):
|
||||
ws.send(json.dumps(payload))
|
||||
|
||||
try:
|
||||
ws = websocket.WebSocketApp(
|
||||
self.WS_URL,
|
||||
on_message=on_message,
|
||||
on_error=on_error,
|
||||
on_close=on_close,
|
||||
on_open=on_open
|
||||
)
|
||||
|
||||
# 在独立线程中运行 WebSocket
|
||||
ws_thread = threading.Thread(target=ws.run_forever, kwargs={"ping_timeout": 10})
|
||||
ws_thread.daemon = True
|
||||
ws_thread.start()
|
||||
|
||||
# 等待完成(最多 5 分钟)
|
||||
timeout = 300
|
||||
start = time.time()
|
||||
while not ws_result["done"] and (time.time() - start) < timeout:
|
||||
time.sleep(1)
|
||||
|
||||
if ws_result["error"]:
|
||||
raise Exception(ws_result["error"])
|
||||
|
||||
if not ws_result["done"]:
|
||||
ws.close()
|
||||
raise Exception("生成超时(超过 5 分钟)")
|
||||
|
||||
return ws_result["images"]
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"WebSocket 连接失败: {e}")
|
||||
|
||||
def _generate_via_ws_fallback(self, payload, result_lines):
|
||||
"""降级方案:尝试自动安装 websocket-client"""
|
||||
try:
|
||||
import subprocess
|
||||
result_lines.append(" 正在安装 websocket-client ...")
|
||||
subprocess.run(
|
||||
["pip", "install", "websocket-client"],
|
||||
capture_output=True, timeout=60
|
||||
)
|
||||
import importlib
|
||||
global websocket
|
||||
websocket = importlib.import_module("websocket")
|
||||
global WEBSOCKET_AVAILABLE
|
||||
WEBSOCKET_AVAILABLE = True
|
||||
result_lines.append("✅ websocket-client 安装成功")
|
||||
return self._generate_via_ws(payload, result_lines)
|
||||
except Exception as e:
|
||||
result_lines.append(f"❌ 无法安装 websocket-client: {e}")
|
||||
result_lines.append("💡 请手动运行: pip install websocket-client")
|
||||
return []
|
||||
|
||||
def _download_image(self, url, filename):
|
||||
"""下载图片到本地"""
|
||||
try:
|
||||
# 确保 URL 是完整的
|
||||
if url.startswith("/"):
|
||||
url = f"{self.BASE_URL}{url}"
|
||||
elif not url.startswith("http"):
|
||||
url = f"{self.BASE_URL}/api/output/file?path={quote(url)}"
|
||||
|
||||
# 创建保存目录
|
||||
os.makedirs(self.save_dir, exist_ok=True)
|
||||
|
||||
# 生成文件名
|
||||
if not filename:
|
||||
filename = f"img_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
|
||||
|
||||
# 清理文件名
|
||||
filename = re.sub(r'[<>:"/\\|?*]', '_', filename)
|
||||
save_path = os.path.join(self.save_dir, filename)
|
||||
|
||||
# 如果文件已存在,添加序号
|
||||
if os.path.exists(save_path):
|
||||
name, ext = os.path.splitext(filename)
|
||||
for i in range(1, 100):
|
||||
new_path = os.path.join(self.save_dir, f"{name}_{i}{ext}")
|
||||
if not os.path.exists(new_path):
|
||||
save_path = new_path
|
||||
break
|
||||
|
||||
# 下载
|
||||
resp = requests.get(url, timeout=60, stream=True)
|
||||
if resp.status_code == 200:
|
||||
with open(save_path, 'wb') as f:
|
||||
for chunk in resp.iter_content(chunk_size=8192):
|
||||
f.write(chunk)
|
||||
return save_path
|
||||
else:
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
|
||||
# 兼容 Liugin 类名
|
||||
@@ -1,169 +0,0 @@
|
||||
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 get_mcp_definition(self):
|
||||
return {
|
||||
"name": "audio_player",
|
||||
"description": "音频播放工具",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file": {
|
||||
"type": "string",
|
||||
"description": "音频文件路径(可选)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def convert_mcp_args(self, arguments):
|
||||
return arguments.get("file", "")
|
||||
|
||||
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
|
||||
@@ -1,489 +0,0 @@
|
||||
"""
|
||||
书签管理器插件 - 管理开发相关链接和资源
|
||||
支持分类、标签、搜索、导入导出,JSON 持久化
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import hashlib
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class Liugin:
|
||||
"""书签管理器 - 开发资源和链接管理"""
|
||||
|
||||
def __init__(self):
|
||||
self.usage = """书签管理器
|
||||
|
||||
操作:
|
||||
add <URL> -n <名称> [-c 分类] [-t 标签1,标签2] [-d 描述]
|
||||
- 添加书签
|
||||
list [-c 分类] [-t 标签] - 列出书签
|
||||
open <名称或ID> - 获取书签 URL
|
||||
search <关键词> - 搜索书签
|
||||
delete <名称或ID> - 删除书签
|
||||
edit <名称或ID> [-n 名称] [-d 描述] [-c 分类] [-t 标签]
|
||||
- 编辑书签
|
||||
categories - 列出所有分类
|
||||
tags - 列出所有标签
|
||||
export [文件] - 导出书签
|
||||
import <文件> - 导入书签
|
||||
stats - 统计信息
|
||||
recent [-n 数量] - 最近添加的书签
|
||||
check - 检查书签是否可访问
|
||||
favicon <URL> - 获取网站 favicon URL
|
||||
|
||||
示例:
|
||||
bookmark_manager add https://github.com -n GitHub -c 开发 -t 代码,托管 -d 代码托管平台
|
||||
bookmark_manager list -c 开发
|
||||
bookmark_manager search github
|
||||
bookmark_manager open GitHub
|
||||
bookmark_manager categories
|
||||
"""
|
||||
self.cli = None
|
||||
self.bookmarks_file = "bookmarks.json"
|
||||
self.bookmarks = []
|
||||
self._load_bookmarks()
|
||||
|
||||
def set_cli(self, cli):
|
||||
self.cli = cli
|
||||
|
||||
def get_tool_info(self):
|
||||
return {
|
||||
"name": "bookmark_manager",
|
||||
"description": "书签管理器 - 管理开发相关链接和资源,支持分类、标签、搜索",
|
||||
"keywords": ["书签", "链接", "收藏", "bookmark", "网址", "资源", "网站", "URL"],
|
||||
"usage": self.usage
|
||||
}
|
||||
|
||||
def get_mcp_definition(self):
|
||||
return {
|
||||
"name": "bookmark_manager",
|
||||
"description": "书签管理器,支持添加、搜索、获取、删除书签",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"operation": {
|
||||
"type": "string",
|
||||
"enum": ["add", "list", "open", "search", "delete",
|
||||
"edit", "categories", "tags", "export",
|
||||
"import", "stats", "recent", "check"],
|
||||
"description": "操作类型"
|
||||
},
|
||||
"url": {"type": "string", "description": "URL"},
|
||||
"name": {"type": "string", "description": "书签名称"},
|
||||
"category": {"type": "string", "description": "分类"},
|
||||
"tags": {"type": "string", "description": "标签(逗号分隔)"},
|
||||
"description": {"type": "string", "description": "描述"},
|
||||
"keyword": {"type": "string", "description": "搜索关键词"},
|
||||
"file": {"type": "string", "description": "文件路径"},
|
||||
"count": {"type": "integer", "description": "数量"}
|
||||
},
|
||||
"required": ["operation"]
|
||||
}
|
||||
}
|
||||
|
||||
def convert_mcp_args(self, arguments):
|
||||
op = arguments.get("operation", "")
|
||||
name = arguments.get("name", "")
|
||||
return f"{op} {name}".strip()
|
||||
|
||||
def _load_bookmarks(self):
|
||||
try:
|
||||
if os.path.exists(self.bookmarks_file):
|
||||
with open(self.bookmarks_file, 'r', encoding='utf-8') as f:
|
||||
self.bookmarks = json.load(f)
|
||||
except Exception:
|
||||
self.bookmarks = []
|
||||
|
||||
def _save_bookmarks(self):
|
||||
try:
|
||||
with open(self.bookmarks_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.bookmarks, f, ensure_ascii=False, indent=2)
|
||||
return None
|
||||
except Exception as e:
|
||||
return f"保存失败: {e}"
|
||||
|
||||
def _gen_id(self, url: str) -> str:
|
||||
return hashlib.md5(f"{url}{time.time()}".encode()).hexdigest()[:8]
|
||||
|
||||
def handle(self, args: str) -> str:
|
||||
try:
|
||||
parts = self._parse_args(args)
|
||||
if not parts:
|
||||
return self._op_list([])
|
||||
|
||||
operation = parts[0].lower()
|
||||
handlers = {
|
||||
"add": self._op_add,
|
||||
"list": self._op_list,
|
||||
"open": self._op_open,
|
||||
"search": self._op_search,
|
||||
"delete": self._op_delete,
|
||||
"edit": self._op_edit,
|
||||
"categories": self._op_categories,
|
||||
"tags": self._op_tags,
|
||||
"export": self._op_export,
|
||||
"import": self._op_import,
|
||||
"stats": self._op_stats,
|
||||
"recent": self._op_recent,
|
||||
"check": self._op_check,
|
||||
"favicon": self._op_favicon,
|
||||
}
|
||||
|
||||
handler = handlers.get(operation)
|
||||
if not handler:
|
||||
return f"错误:不支持的操作 '{operation}'"
|
||||
return handler(parts[1:])
|
||||
|
||||
except Exception as e:
|
||||
return f"书签管理错误: {str(e)}"
|
||||
|
||||
def _parse_args(self, args: str) -> List[str]:
|
||||
parts, current, in_quotes, qc = [], "", False, None
|
||||
for ch in args:
|
||||
if ch in ('"', "'") and not in_quotes:
|
||||
in_quotes, qc = True, ch
|
||||
elif ch == qc and in_quotes:
|
||||
in_quotes, qc = False, None
|
||||
elif ch == ' ' and not in_quotes:
|
||||
if current:
|
||||
parts.append(current)
|
||||
current = ""
|
||||
else:
|
||||
current += ch
|
||||
if current:
|
||||
parts.append(current)
|
||||
return parts
|
||||
|
||||
def _op_add(self, args: List[str]) -> str:
|
||||
if not args:
|
||||
return "错误:请提供 URL"
|
||||
url = args[0]
|
||||
name, category, tags, desc = "", "", [], ""
|
||||
|
||||
i = 1
|
||||
while i < len(args):
|
||||
if args[i] == "-n" and i + 1 < len(args):
|
||||
name = args[i + 1]; i += 1
|
||||
elif args[i] == "-c" and i + 1 < len(args):
|
||||
category = args[i + 1]; i += 1
|
||||
elif args[i] == "-t" and i + 1 < len(args):
|
||||
tags = [t.strip() for t in args[i + 1].split(",") if t.strip()]; i += 1
|
||||
elif args[i] == "-d" and i + 1 < len(args):
|
||||
desc = args[i + 1]; i += 1
|
||||
i += 1
|
||||
|
||||
if not name:
|
||||
# 从 URL 推断名称
|
||||
from urllib.parse import urlparse
|
||||
parsed = urlparse(url if '://' in url else 'https://' + url)
|
||||
name = parsed.netloc.replace('www.', '').split('.')[0].capitalize()
|
||||
url = parsed.geturl()
|
||||
|
||||
# 检查重复
|
||||
for b in self.bookmarks:
|
||||
if b["url"] == url:
|
||||
return f"错误:URL 已存在 (书签: {b['name']})"
|
||||
|
||||
bookmark = {
|
||||
"id": self._gen_id(url),
|
||||
"name": name,
|
||||
"url": url,
|
||||
"category": category,
|
||||
"tags": tags,
|
||||
"description": desc,
|
||||
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"access_count": 0
|
||||
}
|
||||
self.bookmarks.append(bookmark)
|
||||
err = self._save_bookmarks()
|
||||
if err:
|
||||
return err
|
||||
return f"✅ 已添加书签 '{name}'\n URL: {url}\n 分类: {category or '未分类'}"
|
||||
|
||||
def _op_list(self, args: List[str]) -> str:
|
||||
cat_filter, tag_filter = None, None
|
||||
i = 0
|
||||
while i < len(args):
|
||||
if args[i] == "-c" and i + 1 < len(args):
|
||||
cat_filter = args[i + 1].lower(); i += 1
|
||||
elif args[i] == "-t" and i + 1 < len(args):
|
||||
tag_filter = args[i + 1].lower(); i += 1
|
||||
i += 1
|
||||
|
||||
filtered = self.bookmarks
|
||||
if cat_filter:
|
||||
filtered = [b for b in filtered if b.get("category", "").lower() == cat_filter]
|
||||
if tag_filter:
|
||||
filtered = [b for b in filtered if any(tag_filter in t.lower() for t in b.get("tags", []))]
|
||||
|
||||
if not filtered:
|
||||
return "📭 没有找到书签"
|
||||
|
||||
# 按分类分组
|
||||
grouped = {}
|
||||
for b in filtered:
|
||||
cat = b.get("category", "未分类")
|
||||
grouped.setdefault(cat, []).append(b)
|
||||
|
||||
result = f"\n 书签列表 (共 {len(filtered)} 个)\n"
|
||||
result += "═" * 55 + "\n"
|
||||
|
||||
for cat, items in sorted(grouped.items()):
|
||||
result += f"\n {cat} ({len(items)} 个)\n"
|
||||
result += "─" * 45 + "\n"
|
||||
for b in items:
|
||||
tags = ", ".join(b.get("tags", [])) if b.get("tags") else ""
|
||||
result += f" [{b['id']}] {b['name']}\n"
|
||||
result += f" {b['url']}\n"
|
||||
if tags:
|
||||
result += f" ️ {tags}\n"
|
||||
if b.get("description"):
|
||||
result += f" {b['description'][:50]}\n"
|
||||
result += "\n"
|
||||
|
||||
return result.rstrip()
|
||||
|
||||
def _op_open(self, args: List[str]) -> str:
|
||||
if not args:
|
||||
return "错误:请提供书签名称或 ID"
|
||||
key = args[0]
|
||||
bookmark = self._find_bookmark(key)
|
||||
if not bookmark:
|
||||
return f"❌ 未找到书签: {key}"
|
||||
|
||||
bookmark["access_count"] = bookmark.get("access_count", 0) + 1
|
||||
self._save_bookmarks()
|
||||
|
||||
return (f" {bookmark['name']}\n"
|
||||
f" URL: {bookmark['url']}\n"
|
||||
f" 访问次数: {bookmark.get('access_count', 0)}")
|
||||
|
||||
def _op_search(self, args: List[str]) -> str:
|
||||
if not args:
|
||||
return "错误:请提供搜索关键词"
|
||||
keyword = " ".join(args).lower()
|
||||
|
||||
results = []
|
||||
for b in self.bookmarks:
|
||||
searchable = f"{b['name']} {b['url']} {b.get('description', '')} {' '.join(b.get('tags', []))} {b.get('category', '')}".lower()
|
||||
if keyword in searchable:
|
||||
results.append(b)
|
||||
|
||||
if not results:
|
||||
return f" 未找到匹配 '{keyword}' 的书签"
|
||||
|
||||
output = f" 搜索结果 ({len(results)} 个):\n"
|
||||
output += "─" * 50 + "\n"
|
||||
for b in results:
|
||||
output += f" [{b['id']}] {b['name']} - {b['url']}\n"
|
||||
if b.get("description"):
|
||||
output += f" {b['description'][:40]}\n"
|
||||
return output.rstrip()
|
||||
|
||||
def _op_delete(self, args: List[str]) -> str:
|
||||
if not args:
|
||||
return "错误:请提供书签名称或 ID"
|
||||
key = args[0]
|
||||
for i, b in enumerate(self.bookmarks):
|
||||
if b["name"].lower() == key.lower() or b["id"] == key:
|
||||
removed = self.bookmarks.pop(i)
|
||||
self._save_bookmarks()
|
||||
return f" ️ 已删除书签: {removed['name']} ({removed['url']})"
|
||||
return f"❌ 未找到书签: {key}"
|
||||
|
||||
def _op_edit(self, args: List[str]) -> str:
|
||||
if not args:
|
||||
return "错误:请提供书签名称或 ID"
|
||||
key = args[0]
|
||||
bookmark = self._find_bookmark(key)
|
||||
if not bookmark:
|
||||
return f"❌ 未找到书签: {key}"
|
||||
|
||||
i = 1
|
||||
while i < len(args):
|
||||
if args[i] == "-n" and i + 1 < len(args):
|
||||
bookmark["name"] = args[i + 1]; i += 1
|
||||
elif args[i] == "-d" and i + 1 < len(args):
|
||||
bookmark["description"] = args[i + 1]; i += 1
|
||||
elif args[i] == "-c" and i + 1 < len(args):
|
||||
bookmark["category"] = args[i + 1]; i += 1
|
||||
elif args[i] == "-t" and i + 1 < len(args):
|
||||
bookmark["tags"] = [t.strip() for t in args[i + 1].split(",") if t.strip()]; i += 1
|
||||
i += 1
|
||||
|
||||
bookmark["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
self._save_bookmarks()
|
||||
return f"✅ 已更新书签: {bookmark['name']}"
|
||||
|
||||
def _op_categories(self, args: List[str]) -> str:
|
||||
cat_count = {}
|
||||
for b in self.bookmarks:
|
||||
cat = b.get("category", "未分类")
|
||||
cat_count[cat] = cat_count.get(cat, 0) + 1
|
||||
|
||||
if not cat_count:
|
||||
return " 暂无分类"
|
||||
|
||||
result = " 分类列表:\n"
|
||||
for cat, count in sorted(cat_count.items(), key=lambda x: -x[1]):
|
||||
bar = "█" * min(count, 20)
|
||||
result += f" {cat:15s} {bar} ({count})\n"
|
||||
return result.rstrip()
|
||||
|
||||
def _op_tags(self, args: List[str]) -> str:
|
||||
tag_count = {}
|
||||
for b in self.bookmarks:
|
||||
for t in b.get("tags", []):
|
||||
tag_count[t] = tag_count.get(t, 0) + 1
|
||||
|
||||
if not tag_count:
|
||||
return " ️ 暂无标签"
|
||||
|
||||
result = " ️ 标签列表:\n"
|
||||
for tag, count in sorted(tag_count.items(), key=lambda x: -x[1]):
|
||||
result += f" {tag} ({count})\n"
|
||||
return result.rstrip()
|
||||
|
||||
def _op_export(self, args: List[str]) -> str:
|
||||
output_file = args[0] if args else "bookmarks_export.json"
|
||||
try:
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.bookmarks, f, ensure_ascii=False, indent=2)
|
||||
return f"✅ 已导出 {len(self.bookmarks)} 个书签到: {output_file}"
|
||||
except Exception as e:
|
||||
return f"❌ 导出失败: {e}"
|
||||
|
||||
def _op_import(self, args: List[str]) -> str:
|
||||
if not args:
|
||||
return "错误:请提供导入文件路径"
|
||||
file_path = args[0]
|
||||
if not os.path.isfile(file_path):
|
||||
return f"❌ 文件不存在: {file_path}"
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
imported = json.load(f)
|
||||
if not isinstance(imported, list):
|
||||
return "❌ 文件格式错误:需要 JSON 数组"
|
||||
|
||||
existing_urls = {b["url"] for b in self.bookmarks}
|
||||
added = 0
|
||||
for b in imported:
|
||||
if isinstance(b, dict) and b.get("url") and b["url"] not in existing_urls:
|
||||
b["id"] = self._gen_id(b["url"])
|
||||
self.bookmarks.append(b)
|
||||
existing_urls.add(b["url"])
|
||||
added += 1
|
||||
|
||||
self._save_bookmarks()
|
||||
return f"✅ 已导入 {added} 个书签 (跳过 {len(imported) - added} 个重复)"
|
||||
except Exception as e:
|
||||
return f"❌ 导入失败: {e}"
|
||||
|
||||
def _op_stats(self, args: List[str]) -> str:
|
||||
total = len(self.bookmarks)
|
||||
if not total:
|
||||
return "📊 暂无书签"
|
||||
|
||||
cat_count = {}
|
||||
total_access = 0
|
||||
for b in self.bookmarks:
|
||||
cat = b.get("category", "未分类")
|
||||
cat_count[cat] = cat_count.get(cat, 0) + 1
|
||||
total_access += b.get("access_count", 0)
|
||||
|
||||
most_accessed = max(self.bookmarks, key=lambda b: b.get("access_count", 0))
|
||||
|
||||
result = f"📊 书签统计\n"
|
||||
result += "─" * 35 + "\n"
|
||||
result += f" 总数: {total}\n"
|
||||
result += f" 总访问: {total_access} 次\n"
|
||||
result += f" 分类数: {len(cat_count)}\n"
|
||||
result += f" 最常访问: {most_accessed['name']} ({most_accessed.get('access_count', 0)} 次)\n"
|
||||
result += f"\n 分类分布:\n"
|
||||
for cat, count in sorted(cat_count.items(), key=lambda x: -x[1]):
|
||||
result += f" {cat}: {count}\n"
|
||||
return result.rstrip()
|
||||
|
||||
def _op_recent(self, args: List[str]) -> str:
|
||||
count = 5
|
||||
for i, a in enumerate(args):
|
||||
if a == "-n" and i + 1 < len(args):
|
||||
count = int(args[i + 1])
|
||||
|
||||
recent = sorted(self.bookmarks, key=lambda b: b.get("created_at", ""), reverse=True)[:count]
|
||||
if not recent:
|
||||
return "📭 暂无书签"
|
||||
|
||||
result = f"⏰ 最近 {len(recent)} 个书签:\n"
|
||||
for b in recent:
|
||||
result += f" [{b['id']}] {b['name']} - {b.get('created_at', '')}\n"
|
||||
return result.rstrip()
|
||||
|
||||
def _op_check(self, args: List[str]) -> str:
|
||||
import socket
|
||||
from urllib.parse import urlparse
|
||||
|
||||
bookmarks = self.bookmarks
|
||||
if args:
|
||||
# 检查指定书签
|
||||
bookmark = self._find_bookmark(args[0])
|
||||
if not bookmark:
|
||||
return f"❌ 未找到书签: {args[0]}"
|
||||
bookmarks = [bookmark]
|
||||
|
||||
result = f" ️ 书签可访问性检查 ({len(bookmarks)} 个):\n"
|
||||
result += "─" * 50 + "\n"
|
||||
|
||||
for b in bookmarks[:20]:
|
||||
url = b["url"]
|
||||
try:
|
||||
parsed = urlparse(url)
|
||||
host = parsed.netloc or parsed.path.split('/')[0]
|
||||
socket.create_connection((host, 80), timeout=3)
|
||||
result += f" {b['name']} ({host})\n"
|
||||
except (socket.timeout, socket.error, OSError):
|
||||
result += f" {b['name']} ({host}) - 不可达\n"
|
||||
|
||||
if len(bookmarks) > 20:
|
||||
result += f" ... 仅检查前 20 个\n"
|
||||
return result.rstrip()
|
||||
|
||||
def _op_favicon(self, args: List[str]) -> str:
|
||||
if not args:
|
||||
return "错误:请提供 URL"
|
||||
url = args[0]
|
||||
if '://' not in url:
|
||||
url = 'https://' + url
|
||||
|
||||
from urllib.parse import urlparse
|
||||
parsed = urlparse(url)
|
||||
base = f"{parsed.scheme}://{parsed.netloc}"
|
||||
favicon_url = f"{base}/favicon.ico"
|
||||
|
||||
return (f" Favicon:\n"
|
||||
f" 网站: {base}\n"
|
||||
f" Favicon: {favicon_url}\n"
|
||||
f" Google: https://www.google.com/s2/favicons?domain={parsed.netloc}&sz=64")
|
||||
|
||||
def _find_bookmark(self, key: str) -> Optional[dict]:
|
||||
for b in self.bookmarks:
|
||||
if b["name"].lower() == key.lower() or b["id"] == key:
|
||||
return b
|
||||
return None
|
||||
|
||||
|
||||
def test_plugin():
|
||||
plugin = Liugin()
|
||||
print("书签管理器测试:")
|
||||
print(plugin.handle('add https://github.com -n GitHub -c 开发 -t 代码,托管 -d 代码托管平台'))
|
||||
print(plugin.handle('add https://stackoverflow.com -n StackOverflow -c 学习 -t 问答,编程'))
|
||||
print(plugin.handle('list'))
|
||||
print(plugin.handle('search github'))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_plugin()
|
||||
@@ -1,459 +0,0 @@
|
||||
"""
|
||||
颜色工具插件 - 颜色格式转换、调色板、对比度检查
|
||||
支持 HEX/RGB/HSL/HSV/CMYK 格式互转,WCAG 对比度检查
|
||||
"""
|
||||
|
||||
import re
|
||||
import colorsys
|
||||
from typing import List, Tuple
|
||||
|
||||
|
||||
class Liugin:
|
||||
"""颜色工具 - 格式转换、调色板、对比度检查"""
|
||||
|
||||
def __init__(self):
|
||||
self.usage = """颜色工具
|
||||
|
||||
操作:
|
||||
convert <颜色> - 自动检测并转换为所有格式
|
||||
hex <RGB或HSL> - 转换为 HEX
|
||||
rgb <HEX或HSL> - 转换为 RGB
|
||||
hsl <HEX或RGB> - 转换为 HSL
|
||||
hsv <HEX或RGB> - 转换为 HSV
|
||||
cmyk <HEX或RGB> - 转换为 CMYK
|
||||
contrast <颜色1> <颜色2> - WCAG 对比度检查
|
||||
palette <颜色> [-n 数量] - 生成调色板(明暗变体)
|
||||
complementary <颜色> - 互补色
|
||||
analogous <颜色> - 类似色
|
||||
triadic <颜色> - 三色组
|
||||
random [-n 数量] - 随机颜色
|
||||
mix <颜色1> <颜色2> [-w 权重] - 混合两种颜色
|
||||
blind <颜色> <类型> - 色盲模拟
|
||||
named - 列出 CSS 命名颜色
|
||||
|
||||
颜色格式:
|
||||
HEX: #FF6600, #F60, FF6600
|
||||
RGB: rgb(255,102,0) 或 255,102,0
|
||||
HSL: hsl(24,100%,50%)
|
||||
HSV: hsv(24,100%,100%)
|
||||
CMYK: cmyk(0,60,100,0)
|
||||
|
||||
示例:
|
||||
color_picker convert "#3498db"
|
||||
color_picker rgb "hsl(207,62%,54%)"
|
||||
color_picker contrast "#FFFFFF" "#000000"
|
||||
color_picker palette "#e74c3c" -n 9
|
||||
color_picker complementary "#3498db"
|
||||
"""
|
||||
self.cli = None
|
||||
|
||||
def set_cli(self, cli):
|
||||
self.cli = cli
|
||||
|
||||
def get_tool_info(self):
|
||||
return {
|
||||
"name": "color_picker",
|
||||
"description": "颜色工具 - HEX/RGB/HSL/HSV/CMYK 转换、对比度检查、调色板生成",
|
||||
"keywords": ["颜色", "color", "HEX", "RGB", "HSL", "调色板", "对比度", "色盲", "转换"],
|
||||
"usage": self.usage
|
||||
}
|
||||
|
||||
def get_mcp_definition(self):
|
||||
return {
|
||||
"name": "color_picker",
|
||||
"description": "颜色工具,支持格式转换、对比度检查、调色板生成",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"operation": {
|
||||
"type": "string",
|
||||
"enum": ["convert", "hex", "rgb", "hsl", "hsv", "cmyk",
|
||||
"contrast", "palette", "complementary", "analogous",
|
||||
"triadic", "random", "mix", "blind"],
|
||||
"description": "操作类型"
|
||||
},
|
||||
"color": {"type": "string", "description": "颜色值"},
|
||||
"color2": {"type": "string", "description": "第二个颜色值"},
|
||||
"count": {"type": "integer", "description": "数量"},
|
||||
"weight": {"type": "number", "description": "混合权重 (0-1)"},
|
||||
"blind_type": {"type": "string", "description": "色盲类型"}
|
||||
},
|
||||
"required": ["operation"]
|
||||
}
|
||||
}
|
||||
|
||||
def convert_mcp_args(self, arguments):
|
||||
op = arguments.get("operation", "")
|
||||
color = arguments.get("color", "")
|
||||
return f"{op} {color}".strip()
|
||||
|
||||
def handle(self, args: str) -> str:
|
||||
try:
|
||||
parts = self._parse_args(args)
|
||||
if not parts:
|
||||
return self._op_random(["1"])
|
||||
|
||||
operation = parts[0].lower()
|
||||
handlers = {
|
||||
"convert": self._op_convert,
|
||||
"hex": self._op_hex,
|
||||
"rgb": self._op_rgb,
|
||||
"hsl": self._op_hsl,
|
||||
"hsv": self._op_hsv,
|
||||
"cmyk": self._op_cmyk,
|
||||
"contrast": self._op_contrast,
|
||||
"palette": self._op_palette,
|
||||
"complementary": self._op_complementary,
|
||||
"analogous": self._op_analogous,
|
||||
"triadic": self._op_triadic,
|
||||
"random": self._op_random,
|
||||
"mix": self._op_mix,
|
||||
"blind": self._op_blind,
|
||||
}
|
||||
|
||||
handler = handlers.get(operation)
|
||||
if not handler:
|
||||
return f"错误:不支持的操作 '{operation}'"
|
||||
return handler(parts[1:])
|
||||
|
||||
except Exception as e:
|
||||
return f"颜色工具错误: {str(e)}"
|
||||
|
||||
def _parse_args(self, args: str) -> List[str]:
|
||||
parts, current, in_quotes, qc = [], "", False, None
|
||||
for ch in args:
|
||||
if ch in ('"', "'") and not in_quotes:
|
||||
in_quotes, qc = True, ch
|
||||
elif ch == qc and in_quotes:
|
||||
in_quotes, qc = False, None
|
||||
elif ch == ' ' and not in_quotes:
|
||||
if current:
|
||||
parts.append(current)
|
||||
current = ""
|
||||
else:
|
||||
current += ch
|
||||
if current:
|
||||
parts.append(current)
|
||||
return parts
|
||||
|
||||
def _parse_opts(self, args: List[str]) -> dict:
|
||||
opts = {}
|
||||
i = 0
|
||||
while i < len(args):
|
||||
if args[i].startswith("-") and i + 1 < len(args):
|
||||
key = args[i].lstrip("-")
|
||||
opts[key] = args[i + 1]
|
||||
i += 1
|
||||
i += 1
|
||||
return opts
|
||||
|
||||
def _parse_color(self, color_str: str) -> Tuple[int, int, int]:
|
||||
"""解析颜色字符串为 RGB (0-255)"""
|
||||
s = color_str.strip().lower()
|
||||
|
||||
# HEX
|
||||
hex_match = re.match(r'^#?([0-9a-f]{3,8})$', s)
|
||||
if hex_match:
|
||||
h = hex_match.group(1)
|
||||
if len(h) == 3:
|
||||
h = h[0]*2 + h[1]*2 + h[2]*2
|
||||
elif len(h) == 8:
|
||||
h = h[:6] # ignore alpha
|
||||
if len(h) == 6:
|
||||
return (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))
|
||||
|
||||
# RGB
|
||||
rgb_match = re.match(r'rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)', s)
|
||||
if rgb_match:
|
||||
return (int(rgb_match.group(1)), int(rgb_match.group(2)), int(rgb_match.group(3)))
|
||||
|
||||
# plain r,g,b
|
||||
plain_match = re.match(r'^(\d+)\s*,\s*(\d+)\s*,\s*(\d+)$', s)
|
||||
if plain_match:
|
||||
return (int(plain_match.group(1)), int(plain_match.group(2)), int(plain_match.group(3)))
|
||||
|
||||
# HSL
|
||||
hsl_match = re.match(r'hsl\s*\(\s*(\d+)\s*,\s*(\d+)%,\s*(\d+)%\s*\)', s)
|
||||
if hsl_match:
|
||||
h = int(hsl_match.group(1)) / 360
|
||||
sl = int(hsl_match.group(2)) / 100
|
||||
l = int(hsl_match.group(3)) / 100
|
||||
r, g, b = colorsys.hls_to_rgb(h, l, sl)
|
||||
return (int(r * 255), int(g * 255), int(b * 255))
|
||||
|
||||
# HSV/HSB
|
||||
hsv_match = re.match(r'hsv\s*\(\s*(\d+)\s*,\s*(\d+)%,\s*(\d+)%\s*\)', s)
|
||||
if hsv_match:
|
||||
h = int(hsv_match.group(1)) / 360
|
||||
sv = int(hsv_match.group(2)) / 100
|
||||
v = int(hsv_match.group(3)) / 100
|
||||
r, g, b = colorsys.hsv_to_rgb(h, sv, v)
|
||||
return (int(r * 255), int(g * 255), int(b * 255))
|
||||
|
||||
# CMYK
|
||||
cmyk_match = re.match(r'cmyk\s*\(\s*(\d+)%?\s*,\s*(\d+)%?\s*,\s*(\d+)%?\s*,\s*(\d+)%?\s*\)', s)
|
||||
if cmyk_match:
|
||||
c = int(cmyk_match.group(1)) / 100
|
||||
m = int(cmyk_match.group(2)) / 100
|
||||
y = int(cmyk_match.group(3)) / 100
|
||||
k = int(cmyk_match.group(4)) / 100
|
||||
r = int(255 * (1 - c) * (1 - k))
|
||||
g = int(255 * (1 - m) * (1 - k))
|
||||
b = int(255 * (1 - y) * (1 - k))
|
||||
return (r, g, b)
|
||||
|
||||
raise ValueError(f"无法解析颜色: {color_str}")
|
||||
|
||||
def _rgb_to_hex(self, r: int, g: int, b: int) -> str:
|
||||
return f"#{r:02X}{g:02X}{b:02X}"
|
||||
|
||||
def _rgb_to_hsl(self, r: int, g: int, b: int) -> Tuple[int, int, int]:
|
||||
h, l, s = colorsys.rgb_to_hls(r / 255, g / 255, b / 255)
|
||||
return (int(h * 360), int(s * 100), int(l * 100))
|
||||
|
||||
def _rgb_to_hsv(self, r: int, g: int, b: int) -> Tuple[int, int, int]:
|
||||
h, s, v = colorsys.rgb_to_hsv(r / 255, g / 255, b / 255)
|
||||
return (int(h * 360), int(s * 100), int(v * 100))
|
||||
|
||||
def _rgb_to_cmyk(self, r: int, g: int, b: int) -> Tuple[int, int, int, int]:
|
||||
if r == g == b == 0:
|
||||
return (0, 0, 0, 100)
|
||||
c = 1 - r / 255
|
||||
m = 1 - g / 255
|
||||
y = 1 - b / 255
|
||||
k = min(c, m, y)
|
||||
c = int((c - k) / (1 - k) * 100)
|
||||
m = int((m - k) / (1 - k) * 100)
|
||||
y = int((y - k) / (1 - k) * 100)
|
||||
k = int(k * 100)
|
||||
return (c, m, y, k)
|
||||
|
||||
def _luminance(self, r: int, g: int, b: int) -> float:
|
||||
"""计算相对亮度 (WCAG 2.0)"""
|
||||
def linearize(c):
|
||||
c = c / 255
|
||||
return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4
|
||||
return 0.2126 * linearize(r) + 0.7152 * linearize(g) + 0.0722 * linearize(b)
|
||||
|
||||
def _format_color_block(self, r: int, g: int, b: int) -> str:
|
||||
"""生成颜色预览块"""
|
||||
return f" ■ RGB({r}, {g}, {b})"
|
||||
|
||||
def _op_convert(self, args: List[str]) -> str:
|
||||
if not args:
|
||||
return "错误:请提供颜色值"
|
||||
r, g, b = self._parse_color(args[0])
|
||||
h, s, l = self._rgb_to_hsl(r, g, b)
|
||||
hh, sv, vv = self._rgb_to_hsv(r, g, b)
|
||||
c, m, y, k = self._rgb_to_cmyk(r, g, b)
|
||||
|
||||
result = f" 颜色转换: {args[0]}\n"
|
||||
result += self._format_color_block(r, g, b) + "\n"
|
||||
result += "─" * 35 + "\n"
|
||||
result += f" HEX: {self._rgb_to_hex(r, g, b)}\n"
|
||||
result += f" RGB: rgb({r}, {g}, {b})\n"
|
||||
result += f" HSL: hsl({h}, {s}%, {l}%)\n"
|
||||
result += f" HSV: hsv({hh}, {sv}%, {vv}%)\n"
|
||||
result += f" CMYK: cmyk({c}%, {m}%, {y}%, {k}%)\n"
|
||||
return result.rstrip()
|
||||
|
||||
def _op_hex(self, args: List[str]) -> str:
|
||||
if not args:
|
||||
return "错误:请提供颜色值"
|
||||
r, g, b = self._parse_color(args[0])
|
||||
return f" HEX: {self._rgb_to_hex(r, g, b)}"
|
||||
|
||||
def _op_rgb(self, args: List[str]) -> str:
|
||||
if not args:
|
||||
return "错误:请提供颜色值"
|
||||
r, g, b = self._parse_color(args[0])
|
||||
return f" RGB: rgb({r}, {g}, {b})"
|
||||
|
||||
def _op_hsl(self, args: List[str]) -> str:
|
||||
if not args:
|
||||
return "错误:请提供颜色值"
|
||||
r, g, b = self._parse_color(args[0])
|
||||
h, s, l = self._rgb_to_hsl(r, g, b)
|
||||
return f" HSL: hsl({h}, {s}%, {l}%)"
|
||||
|
||||
def _op_hsv(self, args: List[str]) -> str:
|
||||
if not args:
|
||||
return "错误:请提供颜色值"
|
||||
r, g, b = self._parse_color(args[0])
|
||||
h, s, v = self._rgb_to_hsv(r, g, b)
|
||||
return f" HSV: hsv({h}, {s}%, {v}%)"
|
||||
|
||||
def _op_cmyk(self, args: List[str]) -> str:
|
||||
if not args:
|
||||
return "错误:请提供颜色值"
|
||||
r, g, b = self._parse_color(args[0])
|
||||
c, m, y, k = self._rgb_to_cmyk(r, g, b)
|
||||
return f" CMYK: cmyk({c}%, {m}%, {y}%, {k}%)"
|
||||
|
||||
def _op_contrast(self, args: List[str]) -> str:
|
||||
if len(args) < 2:
|
||||
return "错误:请提供两种颜色"
|
||||
r1, g1, b1 = self._parse_color(args[0])
|
||||
r2, g2, b2 = self._parse_color(args[1])
|
||||
|
||||
l1 = self._luminance(r1, g1, b1)
|
||||
l2 = self._luminance(r2, g2, b2)
|
||||
lighter = max(l1, l2)
|
||||
darker = min(l1, l2)
|
||||
ratio = (lighter + 0.05) / (darker + 0.05)
|
||||
|
||||
aa_large = ratio >= 3
|
||||
aa_normal = ratio >= 4.5
|
||||
aaa_large = ratio >= 4.5
|
||||
aaa_normal = ratio >= 7
|
||||
|
||||
result = f" WCAG 对比度检查\n"
|
||||
result += "─" * 40 + "\n"
|
||||
result += f" 颜色1: {self._rgb_to_hex(r1, g1, b1)} {self._format_color_block(r1, g1, b1)}\n"
|
||||
result += f" 颜色2: {self._rgb_to_hex(r2, g2, b2)} {self._format_color_block(r2, g2, b1)}\n"
|
||||
result += f" 对比度: {ratio:.2f}:1\n"
|
||||
result += "─" * 40 + "\n"
|
||||
result += f" {' ' if aa_normal else '❌'} AA 正文 (≥4.5:1)\n"
|
||||
result += f" {' ' if aa_large else '❌'} AA 大字 (≥3:1)\n"
|
||||
result += f" {' ' if aaa_normal else '❌'} AAA 正文 (≥7:1)\n"
|
||||
result += f" {' ' if aaa_large else '❌'} AAA 大字 (≥4.5:1)\n"
|
||||
return result.rstrip()
|
||||
|
||||
def _op_palette(self, args: List[str]) -> str:
|
||||
if not args:
|
||||
return "错误:请提供基础颜色"
|
||||
r, g, b = self._parse_color(args[0])
|
||||
opts = self._parse_opts(args[1:])
|
||||
count = int(opts.get("n", 9))
|
||||
count = max(3, min(count, 15))
|
||||
|
||||
h, l, s = colorsys.rgb_to_hls(r / 255, g / 255, b / 255)
|
||||
|
||||
result = f" 调色板 (基于 {args[0]}):\n"
|
||||
result += "─" * 50 + "\n"
|
||||
|
||||
steps = count
|
||||
for i in range(steps):
|
||||
# 从暗到亮
|
||||
new_l = 0.1 + (0.8 * i / (steps - 1))
|
||||
nr, ng, nb = colorsys.hls_to_rgb(h, new_l, s)
|
||||
nr, ng, nb = int(nr * 255), int(ng * 255), int(nb * 255)
|
||||
hex_val = self._rgb_to_hex(nr, ng, nb)
|
||||
bar = "█" * int(new_l * 20)
|
||||
result += f" {hex_val} {bar} L:{int(new_l*100)}%\n"
|
||||
|
||||
return result.rstrip()
|
||||
|
||||
def _op_complementary(self, args: List[str]) -> str:
|
||||
if not args:
|
||||
return "错误:请提供颜色"
|
||||
r, g, b = self._parse_color(args[0])
|
||||
h, l, s = colorsys.rgb_to_hls(r / 255, g / 255, b / 255)
|
||||
# 互补色: 色相旋转180°
|
||||
comp_h = (h + 0.5) % 1.0
|
||||
cr, cg, cb = colorsys.hls_to_rgb(comp_h, l, s)
|
||||
cr, cg, cb = int(cr * 255), int(cg * 255), int(cb * 255)
|
||||
|
||||
result = f" 互补色:\n"
|
||||
result += f" 原色: {self._rgb_to_hex(r, g, b)}\n"
|
||||
result += f" 互补: {self._rgb_to_hex(cr, cg, cb)}\n"
|
||||
return result
|
||||
|
||||
def _op_analogous(self, args: List[str]) -> str:
|
||||
if not args:
|
||||
return "错误:请提供颜色"
|
||||
r, g, b = self._parse_color(args[0])
|
||||
h, l, s = colorsys.rgb_to_hls(r / 255, g / 255, b / 255)
|
||||
|
||||
result = f" 类似色:\n"
|
||||
for offset in [-30, -15, 0, 15, 30]:
|
||||
ah = (h + offset / 360) % 1.0
|
||||
ar, ag, ab = colorsys.hls_to_rgb(ah, l, s)
|
||||
ar, ag, ab = int(ar * 255), int(ag * 255), int(ab * 255)
|
||||
marker = " ◀" if offset == 0 else ""
|
||||
result += f" {self._rgb_to_hex(ar, ag, ab)} ({offset:+d}°){marker}\n"
|
||||
return result.rstrip()
|
||||
|
||||
def _op_triadic(self, args: List[str]) -> str:
|
||||
if not args:
|
||||
return "错误:请提供颜色"
|
||||
r, g, b = self._parse_color(args[0])
|
||||
h, l, s = colorsys.rgb_to_hls(r / 255, g / 255, b / 255)
|
||||
|
||||
result = f" 三色组:\n"
|
||||
for i, offset in enumerate([0, 120, 240]):
|
||||
th = (h + offset / 360) % 1.0
|
||||
tr, tg, tb = colorsys.hls_to_rgb(th, l, s)
|
||||
tr, tg, tb = int(tr * 255), int(tg * 255), int(tb * 255)
|
||||
result += f" {i+1}. {self._rgb_to_hex(tr, tg, tb)} ({offset}°)\n"
|
||||
return result.rstrip()
|
||||
|
||||
def _op_random(self, args: List[str]) -> str:
|
||||
opts = self._parse_opts(args)
|
||||
count = int(opts.get("n", 5))
|
||||
count = min(count, 20)
|
||||
|
||||
result = f" 随机颜色 ({count} 个):\n"
|
||||
for _ in range(count):
|
||||
r, g, b = __import__('random').randint(0, 255), __import__('random').randint(0, 255), __import__('random').randint(0, 255)
|
||||
result += f" {self._rgb_to_hex(r, g, b)} rgb({r}, {g}, {b})\n"
|
||||
return result.rstrip()
|
||||
|
||||
def _op_mix(self, args: List[str]) -> str:
|
||||
if len(args) < 2:
|
||||
return "错误:请提供两种颜色"
|
||||
r1, g1, b1 = self._parse_color(args[0])
|
||||
r2, g2, b2 = self._parse_color(args[1])
|
||||
opts = self._parse_opts(args[2:])
|
||||
w = float(opts.get("w", 0.5))
|
||||
w = max(0, min(1, w))
|
||||
|
||||
mr = int(r1 * (1 - w) + r2 * w)
|
||||
mg = int(g1 * (1 - w) + g2 * w)
|
||||
mb = int(b1 * (1 - w) + b2 * w)
|
||||
|
||||
result = f" 颜色混合 (权重 {w}):\n"
|
||||
result += f" 颜色1: {self._rgb_to_hex(r1, g1, b1)}\n"
|
||||
result += f" 颜色2: {self._rgb_to_hex(r2, g2, b2)}\n"
|
||||
result += f" 混合: {self._rgb_to_hex(mr, mg, mb)}\n"
|
||||
return result
|
||||
|
||||
def _op_blind(self, args: List[str]) -> str:
|
||||
if len(args) < 2:
|
||||
return "错误:请提供颜色和色盲类型\n类型: protanopia, deuteranopia, tritanopia"
|
||||
r, g, b = self._parse_color(args[0])
|
||||
blind_type = args[1].lower()
|
||||
|
||||
# 简化的色盲模拟矩阵
|
||||
matrices = {
|
||||
"protanopia": [[0.567, 0.433, 0.0], [0.558, 0.442, 0.0], [0.0, 0.242, 0.758]],
|
||||
"deuteranopia": [[0.625, 0.375, 0.0], [0.7, 0.3, 0.0], [0.0, 0.3, 0.7]],
|
||||
"tritanopia": [[0.95, 0.05, 0.0], [0.0, 0.433, 0.567], [0.0, 0.475, 0.525]],
|
||||
}
|
||||
|
||||
matrix = matrices.get(blind_type)
|
||||
if not matrix:
|
||||
return f"错误:未知类型 '{blind_type}'\n支持: protanopia, deuteranopia, tritanopia"
|
||||
|
||||
nr = int(matrix[0][0] * r + matrix[0][1] * g + matrix[0][2] * b)
|
||||
ng = int(matrix[1][0] * r + matrix[1][1] * g + matrix[1][2] * b)
|
||||
nb = int(matrix[2][0] * r + matrix[2][1] * g + matrix[2][2] * b)
|
||||
nr, ng, nb = max(0, min(255, nr)), max(0, min(255, ng)), max(0, min(255, nb))
|
||||
|
||||
result = f" 色盲模拟 ({blind_type}):\n"
|
||||
result += f" 原色: {self._rgb_to_hex(r, g, b)}\n"
|
||||
result += f" 模拟: {self._rgb_to_hex(nr, ng, nb)}\n"
|
||||
return result
|
||||
|
||||
|
||||
def test_plugin():
|
||||
plugin = Liugin()
|
||||
print("颜色工具测试:")
|
||||
print(plugin.handle('convert "#3498db"'))
|
||||
print(plugin.handle('contrast "#FFFFFF" "#000000"'))
|
||||
print(plugin.handle('palette "#e74c3c" -n 7'))
|
||||
print(plugin.handle('complementary "#3498db"'))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_plugin()
|
||||
@@ -1,370 +0,0 @@
|
||||
"""
|
||||
图像生成插件 - 基于 ai.2x.nz 的 ComfyUI 在线服务
|
||||
支持自然语言描述和英文 Tag 两种模式
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import asyncio
|
||||
import urllib.request
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Liugin:
|
||||
"""图像生成工具 — 自然语言/Tag 生图,基于 ComfyUI 在线服务"""
|
||||
|
||||
def __init__(self):
|
||||
self.usage = """图像生成工具 — 基于 ai.2x.nz 的 ComfyUI 在线服务
|
||||
|
||||
操作:
|
||||
generate <描述> - 自然语言描述生成图片(中文/英文均可)
|
||||
tag <英文tags> - 直接 Tag 模式(逗号分隔的英文标签)
|
||||
list - 列出可用的工作流/角色
|
||||
status - 查看 GPU 状态
|
||||
|
||||
示例:
|
||||
image_generator generate 一个白发蓝眼的猫耳少女在写代码
|
||||
image_generator tag masterpiece, 1girl, white hair, blue eyes, cat ears, coding
|
||||
image_generator generate 画一个赛博朋克风格的城市夜景 --width 1024 --height 768
|
||||
|
||||
说明:
|
||||
- 自然语言模式会经过 LLM 翻译为 Tag,效果更好
|
||||
- 直接 Tag 模式适合熟悉 Danbooru 标签的用户
|
||||
- 默认分辨率 1024x1344(WAI 推荐)
|
||||
- 最大分辨率 1344x1344
|
||||
- 生成的图片保存在 screenshots/ 目录
|
||||
"""
|
||||
self.cli = None
|
||||
self.api_base = "https://ai.2x.nz"
|
||||
self.ws_url = "wss://ai.2x.nz/ws/run"
|
||||
self.default_workflow = "WAI - 无Lora.json"
|
||||
self.output_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "screenshots")
|
||||
|
||||
def set_cli(self, cli):
|
||||
self.cli = cli
|
||||
|
||||
def get_tool_info(self):
|
||||
return {
|
||||
"name": "image_generator",
|
||||
"description": "图像生成 — 自然语言/Tag 生图,基于 ComfyUI 在线服务(ai.2x.nz),支持中文描述",
|
||||
"keywords": ["图像", "生成", "画图", "绘图", "AI画", "生图", "image", "generate", "draw", "paint",
|
||||
"diffusion", "comfyui", "stable", "sd", "文生图", "text2img"],
|
||||
"usage": self.usage
|
||||
}
|
||||
|
||||
def handle(self, args: str) -> str:
|
||||
try:
|
||||
parts = args.strip().split(maxsplit=1)
|
||||
if not parts:
|
||||
return "错误:请提供操作类型\n\n" + self.usage
|
||||
|
||||
op = parts[0].lower()
|
||||
rest = parts[1] if len(parts) > 1 else ""
|
||||
|
||||
handlers = {
|
||||
"generate": self._generate,
|
||||
"tag": self._generate_tag,
|
||||
"gen": self._generate,
|
||||
"g": self._generate,
|
||||
"list": self._list_workflows,
|
||||
"ls": self._list_workflows,
|
||||
"status": self._gpu_status,
|
||||
}
|
||||
|
||||
handler = handlers.get(op)
|
||||
if not handler:
|
||||
# 如果没有指定操作,把整个 args 当作自然语言描述
|
||||
return self._generate(args)
|
||||
|
||||
return handler(rest)
|
||||
|
||||
except Exception as e:
|
||||
return f"图像生成错误: {e}"
|
||||
|
||||
def _generate(self, args: str) -> str:
|
||||
"""自然语言模式生成图片"""
|
||||
# 解析 --width 和 --height 参数
|
||||
width, height = 1024, 1344
|
||||
prompt = args.strip()
|
||||
|
||||
if "--width" in prompt or "--height" in prompt or "--w" in prompt or "--h" in prompt:
|
||||
import re
|
||||
w_match = re.search(r'--w(?:idth)?\s+(\d+)', prompt)
|
||||
h_match = re.search(r'--h(?:eight)?\s+(\d+)', prompt)
|
||||
if w_match:
|
||||
width = int(w_match.group(1))
|
||||
prompt = prompt[:w_match.start()] + prompt[w_match.end():]
|
||||
if h_match:
|
||||
height = int(h_match.group(1))
|
||||
prompt = prompt[:h_match.start()] + prompt[h_match.end():]
|
||||
prompt = prompt.strip()
|
||||
|
||||
if not prompt:
|
||||
return "错误:请提供图片描述\n示例: image_generator generate 一个白发猫耳少女"
|
||||
|
||||
# 限制分辨率
|
||||
width = min(width, 1344)
|
||||
height = min(height, 1344)
|
||||
|
||||
return self._run_generation(prompt, "", width, height, rewrite=True, override=False)
|
||||
|
||||
def _generate_tag(self, args: str) -> str:
|
||||
"""直接 Tag 模式"""
|
||||
width, height = 1024, 1344
|
||||
prompt = args.strip()
|
||||
|
||||
if "--width" in prompt or "--height" in prompt or "--w" in prompt or "--h" in prompt:
|
||||
import re
|
||||
w_match = re.search(r'--w(?:idth)?\s+(\d+)', prompt)
|
||||
h_match = re.search(r'--h(?:eight)?\s+(\d+)', prompt)
|
||||
if w_match:
|
||||
width = int(w_match.group(1))
|
||||
prompt = prompt[:w_match.start()] + prompt[w_match.end():]
|
||||
if h_match:
|
||||
height = int(h_match.group(1))
|
||||
prompt = prompt[:h_match.start()] + prompt[h_match.end():]
|
||||
prompt = prompt.strip()
|
||||
|
||||
if not prompt:
|
||||
return "错误:请提供 Tag\n示例: image_generator tag masterpiece, 1girl, white hair, blue eyes"
|
||||
|
||||
width = min(width, 1344)
|
||||
height = min(height, 1344)
|
||||
|
||||
return self._run_generation("", prompt, width, height, rewrite=False, override=False)
|
||||
|
||||
def _run_generation(self, nl_prompt: str, direct_prompt: str, width: int, height: int,
|
||||
rewrite: bool = False, override: bool = False) -> str:
|
||||
"""执行图像生成(同步包装)"""
|
||||
try:
|
||||
import websocket as ws_lib
|
||||
except ImportError:
|
||||
# 回退:用 urllib 的方式不行,WebSocket 必须用专用库
|
||||
# 尝试用 subprocess 调用 python 脚本
|
||||
return self._run_generation_subprocess(nl_prompt, direct_prompt, width, height, rewrite, override)
|
||||
|
||||
try:
|
||||
start_time = time.time()
|
||||
ws = ws_lib.create_connection(self.ws_url, timeout=180)
|
||||
|
||||
payload = {
|
||||
"workflow_path": self.default_workflow,
|
||||
"inline_workflow": None,
|
||||
"direct_prompt": direct_prompt,
|
||||
"nl_prompt": nl_prompt,
|
||||
"rewrite": rewrite,
|
||||
"override": override,
|
||||
"width": width,
|
||||
"height": height,
|
||||
}
|
||||
ws.send(json.dumps(payload))
|
||||
|
||||
image_path = None
|
||||
final_prompt = ""
|
||||
error_msg = ""
|
||||
|
||||
while True:
|
||||
try:
|
||||
raw = ws.recv()
|
||||
data = json.loads(raw)
|
||||
msg_type = data.get("type", "")
|
||||
|
||||
if msg_type == "image":
|
||||
url = data.get("url", "")
|
||||
filename = data.get("filename", "")
|
||||
if url:
|
||||
image_path = self._download_image(url, filename)
|
||||
elif msg_type == "final_prompt":
|
||||
final_prompt = data.get("final_prompt", "") if isinstance(data.get("final_prompt"), str) else data.get("prompt", "")
|
||||
elif msg_type == "done":
|
||||
break
|
||||
elif msg_type == "error":
|
||||
error_msg = data.get("message", "未知错误")
|
||||
break
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
break
|
||||
|
||||
ws.close()
|
||||
elapsed = time.time() - start_time
|
||||
|
||||
if error_msg:
|
||||
return f" 生成失败: {error_msg}"
|
||||
|
||||
if image_path:
|
||||
size = os.path.getsize(image_path)
|
||||
size_str = f"{size/1024:.0f}KB" if size < 1024*1024 else f"{size/1024/1024:.1f}MB"
|
||||
result = f" 图片生成成功!\n"
|
||||
result += f" 路径: {image_path}\n"
|
||||
result += f" 大小: {size_str}\n"
|
||||
result += f" 分辨率: {width}x{height}\n"
|
||||
result += f"⏱ 耗时: {elapsed:.1f}s\n"
|
||||
if final_prompt:
|
||||
result += f" Prompt: {final_prompt[:120]}...\n"
|
||||
result += f"\n💡 提示: 用 'send_image {image_path}' 可以推送到手机"
|
||||
return result
|
||||
else:
|
||||
return " 生成完成但未收到图片数据"
|
||||
|
||||
except Exception as e:
|
||||
return f" 连接失败: {e}\n💡 请检查网络连接或稍后重试"
|
||||
|
||||
def _run_generation_subprocess(self, nl_prompt: str, direct_prompt: str, width: int, height: int,
|
||||
rewrite: bool, override: bool) -> str:
|
||||
"""用子进程执行 WebSocket 生成(无需 websocket 库)"""
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
script = f'''
|
||||
import json, asyncio, urllib.request, os, sys
|
||||
|
||||
async def main():
|
||||
try:
|
||||
import websockets
|
||||
except ImportError:
|
||||
print("ERROR:需要安装 websockets: pip install websockets")
|
||||
sys.exit(1)
|
||||
|
||||
uri = "wss://ai.2x.nz/ws/run"
|
||||
async with websockets.connect(uri) as ws:
|
||||
payload = {{
|
||||
"workflow_path": "{self.default_workflow}",
|
||||
"inline_workflow": None,
|
||||
"direct_prompt": {json.dumps(direct_prompt)},
|
||||
"nl_prompt": {json.dumps(nl_prompt)},
|
||||
"rewrite": {"true" if rewrite else "false"},
|
||||
"override": {"true" if override else "false"},
|
||||
"width": {width},
|
||||
"height": {height},
|
||||
}}
|
||||
await ws.send(json.dumps(payload))
|
||||
|
||||
image_url = None
|
||||
while True:
|
||||
try:
|
||||
msg = await asyncio.wait_for(ws.recv(), timeout=180)
|
||||
data = json.loads(msg)
|
||||
t = data.get("type", "")
|
||||
if t == "image":
|
||||
image_url = data.get("url", "")
|
||||
elif t == "done":
|
||||
break
|
||||
elif t == "error":
|
||||
print(f"ERROR:{{data.get('message','')}}")
|
||||
break
|
||||
except asyncio.TimeoutError:
|
||||
print("ERROR:超时")
|
||||
break
|
||||
|
||||
if image_url:
|
||||
full_url = f"https://ai.2x.nz{{image_url}}"
|
||||
out_path = os.path.join("{self.output_dir}", f"generated_{{os.path.basename(image_url)}}")
|
||||
os.makedirs(os.path.dirname(out_path), exist_ok=True)
|
||||
urllib.request.urlretrieve(full_url, out_path)
|
||||
print(f"OK:{{out_path}}")
|
||||
|
||||
asyncio.run(main())
|
||||
'''
|
||||
|
||||
script_path = os.path.join(self.output_dir, "_gen_temp.py")
|
||||
os.makedirs(self.output_dir, exist_ok=True)
|
||||
with open(script_path, 'w') as f:
|
||||
f.write(script)
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["python3", script_path],
|
||||
capture_output=True, text=True, timeout=200
|
||||
)
|
||||
output = result.stdout.strip()
|
||||
|
||||
if output.startswith("OK:"):
|
||||
image_path = output[3:]
|
||||
size = os.path.getsize(image_path)
|
||||
size_str = f"{size/1024:.0f}KB" if size < 1024*1024 else f"{size/1024/1024:.1f}MB"
|
||||
res = f" 图片生成成功!\n"
|
||||
res += f" 路径: {image_path}\n"
|
||||
res += f" 大小: {size_str}\n"
|
||||
res += f" 分辨率: {width}x{height}\n"
|
||||
res += f"\n💡 提示: 用 'send_image {image_path}' 可以推送到手机"
|
||||
return res
|
||||
elif output.startswith("ERROR:"):
|
||||
return f" {output[6:]}"
|
||||
else:
|
||||
return f" 生成异常:\n{result.stdout}\n{result.stderr}"
|
||||
except subprocess.TimeoutExpired:
|
||||
return " 生成超时(>200s),请稍后重试"
|
||||
finally:
|
||||
try:
|
||||
os.remove(script_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
def _download_image(self, url: str, filename: str) -> str:
|
||||
"""下载生成的图片"""
|
||||
full_url = f"{self.api_base}{url}"
|
||||
os.makedirs(self.output_dir, exist_ok=True)
|
||||
out_path = os.path.join(self.output_dir, f"gen_{filename}")
|
||||
urllib.request.urlretrieve(full_url, out_path)
|
||||
return out_path
|
||||
|
||||
def _list_workflows(self, args: str) -> str:
|
||||
"""列出可用的工作流"""
|
||||
try:
|
||||
import urllib.request
|
||||
req = urllib.request.Request(f"{self.api_base}/api/workflows")
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
data = json.loads(resp.read())
|
||||
|
||||
workflows = data.get("workflows", [])
|
||||
result = f" 可用工作流 ({len(workflows)} 个):\n\n"
|
||||
|
||||
lora_workflows = []
|
||||
no_lora = []
|
||||
for w in workflows:
|
||||
name = w.get("path", "")
|
||||
if "无Lora" in name or "no" in name.lower():
|
||||
no_lora.append(name)
|
||||
else:
|
||||
lora_workflows.append(name)
|
||||
|
||||
if no_lora:
|
||||
result += " 通用(无角色绑定):\n"
|
||||
for w in no_lora:
|
||||
result += f" • {w}\n"
|
||||
|
||||
if lora_workflows:
|
||||
result += "\n 角色工作流:\n"
|
||||
for w in lora_workflows:
|
||||
result += f" • {w}\n"
|
||||
|
||||
result += "\n💡 使用 'image_generator generate <描述>' 自然语言生图"
|
||||
result += "\n💡 使用 'image_generator tag <tags>' 直接 Tag 生图"
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
return f" 获取工作流列表失败: {e}"
|
||||
|
||||
def _gpu_status(self, args: str) -> str:
|
||||
"""查看 GPU 状态"""
|
||||
try:
|
||||
import urllib.request
|
||||
req = urllib.request.Request(f"{self.api_base}/api/gpu")
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
data = json.loads(resp.read())
|
||||
|
||||
result = " 服务器 GPU 状态:\n"
|
||||
for gpu in data.get("gpus", []):
|
||||
name = gpu.get("name", "未知")
|
||||
util = gpu.get("utilization", 0)
|
||||
mem_used = gpu.get("memory_used", 0)
|
||||
mem_total = gpu.get("memory_total", 0)
|
||||
temp = gpu.get("temperature", 0)
|
||||
result += f" {name}\n"
|
||||
result += f" GPU 负载: {util}%\n"
|
||||
result += f" 显存: {mem_used}/{mem_total} MiB\n"
|
||||
result += f" 温度: {temp}°C\n"
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
return f" 获取 GPU 状态失败: {e}"
|
||||
@@ -1,144 +0,0 @@
|
||||
"""
|
||||
发送图片插件 - 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
|
||||
+11
-7
@@ -1,11 +1,10 @@
|
||||
# 小狸插件配置
|
||||
|
||||
# ── 必备生产力 ──
|
||||
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
|
||||
@@ -14,10 +13,15 @@ ENABLE_SPEECH_RECOGNITION = True
|
||||
ENABLE_TASK_MANAGER = True
|
||||
ENABLE_TOOL_SEARCH = True
|
||||
ENABLE_AUTO_ENGINEER = True
|
||||
ENABLE_GIT_WORKFLOW = True
|
||||
ENABLE_PROJECT_ANALYZER = True
|
||||
|
||||
ENABLE_DGLAB_WS = False
|
||||
|
||||
ENABLE_TERMINAL = True
|
||||
ENABLE_OPERIT_BRIDGE = True
|
||||
ENABLE_BROWSER_AUTO = True
|
||||
ENABLE_GUI_AUTO = True
|
||||
ENABLE_SUB_AGENT = True
|
||||
ENABLE_WORKFLOW = True
|
||||
ENABLE_MEMORY_PLUGIN = True
|
||||
ENABLE_JSON_TOOLKIT = True
|
||||
ENABLE_REGEX_TESTER = True
|
||||
ENABLE_SNIPPET_MANAGER = True
|
||||
ENABLE_MARKDOWN_TOOL = True
|
||||
ENABLE_ID_GENERATOR = True
|
||||
+130
-180
@@ -1,229 +1,179 @@
|
||||
"""终端模拟器插件测试"""
|
||||
"""终端模拟器 v2 测试"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from plugins.terminal import Liugin, TerminalSession
|
||||
from plugins.terminal import Liugin, Terminal
|
||||
|
||||
|
||||
def test_basic_open_close():
|
||||
"""打开和关闭终端"""
|
||||
p = Liugin()
|
||||
r = p.handle("open")
|
||||
assert "✓" in r
|
||||
assert "s1" in r or "main" in r
|
||||
|
||||
assert "已打开" in r
|
||||
r = p.handle("list")
|
||||
assert "终端" in r
|
||||
|
||||
assert "main" in r
|
||||
r = p.handle("close")
|
||||
assert "✓" in r
|
||||
|
||||
assert "已关闭" in r
|
||||
r = p.handle("list")
|
||||
assert "暂无" in r
|
||||
assert "无终端" in r
|
||||
print("✓ test_basic_open_close")
|
||||
|
||||
|
||||
def test_send_command():
|
||||
"""发送命令并获取输出"""
|
||||
def test_type_and_enter():
|
||||
p = Liugin()
|
||||
p.handle("open")
|
||||
|
||||
r = p.handle("send echo hello_world")
|
||||
assert "hello_world" in r, f"期望 hello_world,实际: {r}"
|
||||
|
||||
r = p.handle("send echo 42")
|
||||
assert "42" in r
|
||||
|
||||
p.handle("close")
|
||||
print("✓ test_send_command")
|
||||
|
||||
|
||||
def test_cwd():
|
||||
"""切换目录"""
|
||||
p = Liugin()
|
||||
p.handle("open")
|
||||
|
||||
r = p.handle("cwd /tmp")
|
||||
assert "✓" in r
|
||||
|
||||
r = p.handle("send pwd")
|
||||
assert "/tmp" in r
|
||||
|
||||
p.handle("close")
|
||||
print("✓ test_cwd")
|
||||
|
||||
|
||||
def test_env():
|
||||
"""环境变量"""
|
||||
p = Liugin()
|
||||
p.handle("open")
|
||||
|
||||
r = p.handle("env MY_TEST_VAR hello123")
|
||||
assert "✓" in r
|
||||
|
||||
r = p.handle("send echo $MY_TEST_VAR")
|
||||
assert "hello123" in r
|
||||
|
||||
p.handle("close")
|
||||
print("✓ test_env")
|
||||
|
||||
|
||||
def test_multiple_sessions():
|
||||
"""多会话管理"""
|
||||
p = Liugin()
|
||||
|
||||
p.handle("open bash session1")
|
||||
p.handle("open bash session2")
|
||||
|
||||
r = p.handle("list")
|
||||
assert "2 个" in r
|
||||
|
||||
# 第一个会话是 main
|
||||
p.handle("send echo AAA") # 当前是 s2,发给 s2
|
||||
p.handle("switch main")
|
||||
p.handle("send echo BBB") # 发给 main
|
||||
|
||||
r = p.handle("close main")
|
||||
assert "✓" in r
|
||||
|
||||
r = p.handle("list")
|
||||
assert "1 个" in r
|
||||
|
||||
p.handle("close s2")
|
||||
print("✓ test_multiple_sessions")
|
||||
|
||||
|
||||
def test_keys():
|
||||
"""特殊按键"""
|
||||
p = Liugin()
|
||||
p.handle("open")
|
||||
|
||||
r = p.handle("keys")
|
||||
assert "ctrl+c" in r
|
||||
|
||||
# 发送 Ctrl+C(不应该报错)
|
||||
r = p.handle("keys ctrl+c")
|
||||
assert "←" in r
|
||||
|
||||
p.handle("close")
|
||||
print("✓ test_keys")
|
||||
|
||||
|
||||
def test_input():
|
||||
"""纯文本输入"""
|
||||
p = Liugin()
|
||||
p.handle("open")
|
||||
|
||||
p.handle("send cat << 'EOF'")
|
||||
p.handle("input line1")
|
||||
p.handle("input line2")
|
||||
p.handle("input EOF")
|
||||
|
||||
p.handle("close")
|
||||
print("✓ test_input")
|
||||
|
||||
|
||||
def test_status():
|
||||
"""状态查询"""
|
||||
p = Liugin()
|
||||
p.handle("open")
|
||||
|
||||
r = p.handle("status")
|
||||
assert "终端状态" in r
|
||||
assert "运行中" in r
|
||||
|
||||
p.handle("close")
|
||||
print("✓ test_status")
|
||||
|
||||
|
||||
def test_read_output():
|
||||
"""读取输出"""
|
||||
p = Liugin()
|
||||
p.handle("open")
|
||||
|
||||
p.handle("send echo AAA")
|
||||
p.handle("send echo BBB")
|
||||
p.handle("send echo CCC")
|
||||
|
||||
p.handle("type echo hello_v2")
|
||||
p.handle("key enter")
|
||||
time.sleep(0.5)
|
||||
r = p.handle("read")
|
||||
# read 返回最近一次 send_cmd 的结果
|
||||
assert "CCC" in r, f"read 结果: {r[:100]}"
|
||||
|
||||
assert "hello_v2" in r
|
||||
p.handle("close")
|
||||
print("✓ test_read_output")
|
||||
print("✓ test_type_and_enter")
|
||||
|
||||
|
||||
def test_last_n():
|
||||
p = Liugin()
|
||||
p.handle("open")
|
||||
for i in range(10):
|
||||
p.handle(f"type echo line_{i}")
|
||||
p.handle("key enter")
|
||||
time.sleep(0.15)
|
||||
time.sleep(0.5)
|
||||
r = p.handle("last 3")
|
||||
assert "line_9" in r
|
||||
# last 3 不应该包含 line_0
|
||||
assert "line_0" not in r
|
||||
p.handle("close")
|
||||
print("✓ test_last_n")
|
||||
|
||||
|
||||
def test_read_shortcut():
|
||||
"""read 5 等同 last 5"""
|
||||
p = Liugin()
|
||||
p.handle("open")
|
||||
p.handle("type echo aaa")
|
||||
p.handle("key enter")
|
||||
time.sleep(0.3)
|
||||
p.handle("type echo bbb")
|
||||
p.handle("key enter")
|
||||
time.sleep(0.3)
|
||||
r = p.handle("read 5")
|
||||
assert "bbb" in r
|
||||
p.handle("close")
|
||||
print("✓ test_read_shortcut")
|
||||
|
||||
|
||||
def test_env_persist():
|
||||
p = Liugin()
|
||||
p.handle("open")
|
||||
p.handle("type export MYVAR=hello")
|
||||
p.handle("key enter")
|
||||
time.sleep(0.3)
|
||||
p.handle("type echo val=$MYVAR")
|
||||
p.handle("key enter")
|
||||
time.sleep(0.5)
|
||||
r = p.handle("read")
|
||||
assert "val=hello" in r
|
||||
p.handle("close")
|
||||
print("✓ test_env_persist")
|
||||
|
||||
|
||||
def test_python_repl():
|
||||
"""Python REPL 交互"""
|
||||
p = Liugin()
|
||||
p.handle("open python3 Python-REPL")
|
||||
import time; time.sleep(1)
|
||||
|
||||
# REPL 用 input 发送(不触发哨兵),用 read 拿输出
|
||||
p.handle('input print(2 + 3)')
|
||||
time.sleep(0.8)
|
||||
r = p.handle('read')
|
||||
assert "5" in r, f"Python REPL 输出: {r[:200]}"
|
||||
|
||||
p.handle("open python3")
|
||||
time.sleep(1)
|
||||
p.handle("type print(2+3)")
|
||||
p.handle("key enter")
|
||||
time.sleep(0.5)
|
||||
r = p.handle("read")
|
||||
assert "5" in r
|
||||
p.handle("close")
|
||||
print("✓ test_python_repl")
|
||||
|
||||
|
||||
def test_session_persistence():
|
||||
"""会话持久性 — 环境变量在命令间保持"""
|
||||
def test_ctrl_c():
|
||||
p = Liugin()
|
||||
p.handle("open")
|
||||
|
||||
p.handle("send export FOO=bar123")
|
||||
r = p.handle("send echo $FOO")
|
||||
assert "bar123" in r, f"环境变量未保持: {r}"
|
||||
|
||||
p.handle("send cd /var")
|
||||
r = p.handle("send pwd")
|
||||
assert "/var" in r, f"目录未保持: {r}"
|
||||
|
||||
p.handle("type python3 -c 'import time; time.sleep(99)'")
|
||||
p.handle("key enter")
|
||||
time.sleep(0.5)
|
||||
p.handle("key ctrl+c")
|
||||
time.sleep(0.3)
|
||||
# 终端应该还活着
|
||||
r = p.handle("list")
|
||||
assert "●" in r
|
||||
p.handle("close")
|
||||
print("✓ test_session_persistence")
|
||||
print("✓ test_ctrl_c")
|
||||
|
||||
|
||||
def test_auto_create_session():
|
||||
"""无会话时自动创建"""
|
||||
def test_multi_session():
|
||||
p = Liugin()
|
||||
# 不 open,直接 send
|
||||
r = p.handle("send echo auto_created")
|
||||
assert "auto_created" in r
|
||||
p.handle("open")
|
||||
p.handle("open python3")
|
||||
r = p.handle("list")
|
||||
assert "main" in r
|
||||
assert "s2" in r
|
||||
p.handle("switch main")
|
||||
p.handle("type echo from_bash")
|
||||
p.handle("key enter")
|
||||
time.sleep(0.3)
|
||||
r = p.handle("read")
|
||||
assert "from_bash" in r
|
||||
p.handle("close s2")
|
||||
p.handle("close main")
|
||||
print("✓ test_multi_session")
|
||||
|
||||
|
||||
def test_cwd():
|
||||
p = Liugin()
|
||||
p.handle("open")
|
||||
p.handle("type cd /tmp")
|
||||
p.handle("key enter")
|
||||
time.sleep(0.3)
|
||||
p.handle("type pwd")
|
||||
p.handle("key enter")
|
||||
time.sleep(0.5)
|
||||
r = p.handle("read")
|
||||
assert "/tmp" in r
|
||||
p.handle("close")
|
||||
print("✓ test_auto_create_session")
|
||||
print("✓ test_cwd")
|
||||
|
||||
|
||||
def test_long_output():
|
||||
"""长输出处理"""
|
||||
p = Liugin()
|
||||
p.handle("open")
|
||||
|
||||
# 生成长输出
|
||||
r = p.handle("send seq 1 200")
|
||||
assert "200" in r or "1" in r
|
||||
|
||||
p.handle("type seq 1 100")
|
||||
p.handle("key enter")
|
||||
time.sleep(1)
|
||||
r = p.handle("last 5")
|
||||
assert "100" in r
|
||||
p.handle("close")
|
||||
print("✓ test_long_output")
|
||||
|
||||
|
||||
def test_auto_create():
|
||||
"""不 open 直接 type,自动创建终端"""
|
||||
p = Liugin()
|
||||
p.handle("type echo auto")
|
||||
p.handle("key enter")
|
||||
time.sleep(0.5)
|
||||
r = p.handle("read")
|
||||
assert "auto" in r
|
||||
p.handle("close")
|
||||
print("✓ test_auto_create")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_basic_open_close()
|
||||
test_send_command()
|
||||
test_cwd()
|
||||
test_env()
|
||||
test_multiple_sessions()
|
||||
test_keys()
|
||||
test_input()
|
||||
test_status()
|
||||
test_read_output()
|
||||
test_type_and_enter()
|
||||
test_last_n()
|
||||
test_read_shortcut()
|
||||
test_env_persist()
|
||||
test_python_repl()
|
||||
test_session_persistence()
|
||||
test_auto_create_session()
|
||||
test_ctrl_c()
|
||||
test_multi_session()
|
||||
test_cwd()
|
||||
test_long_output()
|
||||
print("\n🎉 全部 13 个测试通过!")
|
||||
test_auto_create()
|
||||
print("\n🎉 全部 11 个测试通过!")
|
||||
Reference in new issue
Block a user