xiaoli-cli 7.0.0: 重构底层架构 + 移除工具截断

- 重构 core/ 底层 (7文件): app/runner/engine/parser/history/safety/display
- FC原生调用为主,prompt-JSON保底
- 移除所有工具返回截断 (ai_search/git_tools/cmd_executor/browser_auto/network_tools/sub_agent/tool_search)
- 删除无用文件 (旧服务器/WebUI/彩蛋/启动器等21个文件)
- 清理 constants.py 死代码
- 版本号更新为 7.0.0
This commit is contained in:
shuiyu1123 committed 2026-07-05 13:17:27 +08:00
commit 78fea7ccdd
57 files changed
+18358

No files matched your search

+219
View File
@@ -0,0 +1,219 @@
import json
import time
from urllib.parse import quote
import requests
class Liugin:
"""网页内容工具 — jina.ai 代理 + AI 直连"""
_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36'
_HEADERS = {
'User-Agent': _UA,
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
}
def __init__(self):
self.usage = """网页内容工具
【jina.ai 代理组】— 通过 jina.ai 服务,返回干净文本:
ai_url <网址> - jina 提取网页正文
ai_search <搜索词> - jina 网络搜索
ai_web <搜索词> - jina 网络搜索(同 ai_search)
【AI 直连组】— 直接 HTTP 请求,模型自行处理:
url <网址> - 直接获取网页原始内容
search <搜索词> - 直接搜索(模型自行选择搜索引擎)
web <搜索词> - 同 search
JSON 格式示例:
{"action": "use_tool", "tool": "ai_search", "args": "ai_url https://www.example.com"}
{"action": "use_tool", "tool": "ai_search", "args": "ai_search Python教程"}
{"action": "use_tool", "tool": "ai_search", "args": "url https://api.example.com/data"}
⚠️ 使用原则:
- 优先使用 AI 直连组 (url/search/web),自己获取和处理网页内容
- 仅当直连失败、网页需要 JS 渲染、或需要 jina 的智能提取时才用 jina 代理组
- 不要滥用 jina,它有速率限制
"""
self.cli = None
self.timeout = 30
self.max_retries = 2
def set_cli(self, cli):
self.cli = cli
self.cli.register_liugin_command('ai_search', self.command_handler)
def command_handler(self, args):
parts = args.strip().split(maxsplit=1)
if len(parts) < 1:
return "请提供操作: ai_url/ai_search/ai_web/url/search/web <参数>"
operation = parts[0].lower()
remaining_args = parts[1] if len(parts) > 1 else ""
return self.handle(f"{operation} {remaining_args}")
def get_tool_info(self):
return {
"name": "ai_search",
"description": "网页内容工具。ai_url/ai_search/ai_web 用 jina.ai 代理;url/search/web 直接 HTTP 请求让模型自行处理。优先用直连组,jina 仅作备用。",
"keywords": ["网址", "url", "网页", "web", "search", "搜索", "fetch", "提取", "jina",
"ai_url", "ai_search", "ai_web", "内容", "获取", "联网"],
"usage": self.usage
}
def handle(self, args):
try:
parts = args.strip().split(maxsplit=1)
if len(parts) < 1:
return "错误:参数不足。可用操作: ai_url/ai_search/ai_web/url/search/web <参数>"
operation = parts[0].lower()
param = parts[1].strip() if len(parts) > 1 else ""
if not param:
return f"错误:请提供参数。格式: {operation} <网址或搜索词>"
# ── jina.ai 代理组 ──
if operation == "ai_url":
return self._jina_extract(param)
elif operation == "ai_search":
return self._jina_search(param)
elif operation == "ai_web":
return self._jina_search(param)
# ── AI 直连组 ──
elif operation == "url":
return self._fetch_raw(param)
elif operation == "search":
return self._direct_search(param)
elif operation == "web":
return self._direct_search(param)
else:
return f"不支持的操作 '{operation}'。可用: ai_url/ai_search/ai_web/url/search/web"
except Exception as e:
return f"操作错误: {str(e)}"
# ═══════════════════════════════════════
# jina.ai 代理组
# ═══════════════════════════════════════
def _jina_extract(self, url):
"""jina.ai 提取网页正文"""
jina_url = f"https://r.jina.ai/{url}"
result = self._execute_curl(jina_url)
if result.startswith("错误:"):
return result
return f"[jina 提取] 网页内容 (来自 {url}):\n\n{result}"
def _jina_search(self, query):
"""jina.ai 网络搜索"""
encoded = quote(query, safe='')
jina_url = f"https://s.jina.ai/{encoded}"
result = self._execute_curl(jina_url)
if result.startswith("错误:"):
return result
return f"[jina 搜索] 结果 (搜索词: {query}):\n\n{result}"
# ═══════════════════════════════════════
# AI 直连组
# ═══════════════════════════════════════
def _fetch_raw(self, url):
"""直接 HTTP GET 获取网页原始内容"""
if not url.startswith('http://') and not url.startswith('https://'):
url = 'https://' + url
try:
response = requests.get(
url, timeout=self.timeout, headers=self._HEADERS, allow_redirects=True
)
if response.status_code != 200:
return f"错误:HTTP {response.status_code}"
if not response.encoding or response.encoding.lower() == 'iso-8859-1':
response.encoding = response.apparent_encoding or 'utf-8'
content = response.text.strip()
if not content:
return "错误:未获取到内容"
return f"[直连] 原始网页内容 (来自 {url}):\n\n{content}"
except requests.exceptions.Timeout:
return f"错误:请求超时 (>{self.timeout}秒)"
except requests.exceptions.ConnectionError:
return "错误:网络连接失败"
except Exception as e:
return f"错误:{str(e)}"
def _direct_search(self, query):
"""直接搜索 — 用搜索引擎获取结果页,模型自行解析"""
encoded = quote(query, safe='')
# 用必应搜索,返回结果页 HTML,模型自行提取链接和摘要
search_url = f"https://www.bing.com/search?q={encoded}"
try:
response = requests.get(
search_url, timeout=self.timeout, headers=self._HEADERS, allow_redirects=True
)
if response.status_code != 200:
return f"错误:HTTP {response.status_code}"
if not response.encoding or response.encoding.lower() == 'iso-8859-1':
response.encoding = response.apparent_encoding or 'utf-8'
content = response.text.strip()
if not content:
return "错误:未获取到搜索结果"
return f"[直连] 搜索结果页 (搜索词: {query}):\n\n{content}"
except requests.exceptions.Timeout:
return f"错误:请求超时 (>{self.timeout}秒)"
except requests.exceptions.ConnectionError:
return "错误:网络连接失败"
except Exception as e:
return f"错误:{str(e)}"
# ═══════════════════════════════════════
# 通用请求(jina 用,带重试)
# ═══════════════════════════════════════
def _execute_curl(self, url):
last_error = None
for attempt in range(self.max_retries + 1):
try:
response = requests.get(
url, timeout=self.timeout, headers=self._HEADERS, allow_redirects=True
)
if response.status_code != 200:
return f"错误:HTTP {response.status_code}"
if not response.encoding or response.encoding.lower() == 'iso-8859-1':
response.encoding = response.apparent_encoding or 'utf-8'
content = response.text.strip()
if not content:
return "错误:未获取到内容"
return content
except requests.exceptions.Timeout:
last_error = f"请求超时 (>{self.timeout}秒)"
if attempt < self.max_retries:
time.sleep(1)
except requests.exceptions.ConnectionError:
last_error = "网络连接失败"
if attempt < self.max_retries:
time.sleep(1)
except requests.exceptions.RequestException as e:
last_error = str(e)
break
except Exception as e:
last_error = str(e)
break
return f"错误:{last_error}"
+456
View File
@@ -0,0 +1,456 @@
"""
工程化工具插件 - 代码质量 + 项目分析
合并原 auto_engineer + project_analyzer,消除重复
"""
import os
import ast
import re
import json
import subprocess
from typing import Tuple, List
class Liugin:
"""工程化工具 - lint/format/test/build + 代码分析"""
def __init__(self):
self.usage = """工程化与分析工具
质量检查:
lint [目录] [文件模式] - 代码质量检查 (ruff/flake8)
format [目录] [文件模式] - 代码格式化 (ruff/black)
test [目录] [参数] - 运行测试 (pytest)
build [命令] - 构建项目
deps - 检查过时依赖
deps-check - 安全漏洞检查
pre-commit - 运行 pre-commit 检查
项目分析:
complexity [目录] - 圈复杂度分析
duplicates [目录] - 重复代码检测
security [目录] - 安全风险扫描
metrics [目录] - 代码质量指标
suggest [目录] - 改进建议
info - 项目信息摘要
init [类型] - 初始化项目 (python/web)
"""
self.cli = None
def set_cli(self, cli):
self.cli = cli
def get_tool_info(self):
return {
"name": "auto_engineer",
"description": "工程化工具 - lint/format/test/build + 代码复杂度/安全/质量分析",
"keywords": ["lint", "format", "test", "build", "代码质量", "格式化", "测试",
"复杂度", "安全", "分析", "依赖", "项目"],
"usage": self.usage
}
def get_mcp_definition(self):
return {
"name": "auto_engineer",
"description": "工程化与分析工具",
"inputSchema": {
"type": "object",
"properties": {
"operation": {"type": "string"},
"target": {"type": "string"},
"options": {"type": "string"}
},
"required": ["operation"]
}
}
def convert_mcp_args(self, arguments):
op = arguments.get("operation", "")
target = arguments.get("target", "")
return f"{op} {target}".strip()
# ── 工具方法 ──
def _run(self, cmd: str, cwd: str = None, timeout: int = 60) -> Tuple[bool, str]:
try:
result = subprocess.run(
cmd, shell=True, capture_output=True, text=True,
cwd=cwd or os.getcwd(), timeout=timeout
)
output = result.stdout + (("\n" + result.stderr) if result.stderr else "")
return result.returncode == 0, output.strip()
except subprocess.TimeoutExpired:
return False, f"命令超时 ({timeout}s)"
except Exception as e:
return False, str(e)
def _find_tool(self, *tools) -> str:
for tool in tools:
mod = tool.replace("-", "_")
ok, _ = self._run(f"python3 -c 'import {mod}'", timeout=5)
if ok:
return tool
ok, _ = self._run(f"which {tool}", timeout=5)
if ok:
return tool
return None
def _walk_py(self, target: str) -> List[str]:
files = []
skip = {'.git', '__pycache__', '.venv', 'venv', 'node_modules'}
for root, dirs, fnames in os.walk(target):
dirs[:] = [d for d in dirs if d not in skip]
for f in fnames:
if f.endswith('.py'):
files.append(os.path.join(root, f))
return files
# ── 路由 ──
def handle(self, args: str) -> str:
parts = args.strip().split(maxsplit=2)
if not parts:
return self.usage
op = parts[0].lower()
target = parts[1] if len(parts) > 1 else "."
options = parts[2] if len(parts) > 2 else ""
handlers = {
"lint": self._lint,
"format": self._format,
"test": self._test,
"build": self._build,
"deps": self._deps,
"deps-check": self._deps_check,
"pre-commit": self._pre_commit,
"complexity": self._complexity,
"duplicates": self._duplicates,
"security": self._security,
"metrics": self._metrics,
"suggest": self._suggest,
"info": self._info,
"init": self._init_project,
}
handler = handlers.get(op)
if not handler:
return f"错误:不支持的操作 '{op}'"
try:
return handler(target, options)
except Exception as e:
return f"执行错误: {e}"
# ══════════════════════════════════════
# 质量检查
# ══════════════════════════════════════
def _lint(self, target: str, options: str) -> str:
tool = self._find_tool("ruff", "flake8")
if not tool:
return " 未安装 lint 工具。安装: pip install ruff"
cmd = f"ruff check {target} {options}" if tool == "ruff" else f"flake8 {target} {options}"
ok, output = self._run(cmd, timeout=120)
if not output:
return f" lint 检查通过 ({tool})"
lines = output.split('\n')
count = len([l for l in lines if l.strip() and not l.startswith(' ')])
return f"{'' if not ok else ''} lint ({tool}): {count} 个问题\n\n{output[:2000]}"
def _format(self, target: str, options: str) -> str:
tool = self._find_tool("ruff", "black")
if not tool:
return " 未安装格式化工具。安装: pip install ruff"
cmd = f"ruff format {target} {options}" if tool == "ruff" else f"black {target} {options}"
ok, output = self._run(cmd, timeout=120)
return f"{'' if ok else ''} 格式化 ({tool})\n{output[:1000]}"
def _test(self, target: str, options: str) -> str:
ok, output = self._run(f"pytest {target} {options} -v", timeout=120)
if not ok and "no tests ran" in output.lower():
return f" 未找到测试文件\n{output[:1000]}"
for line in output.split('\n'):
if 'passed' in line or 'failed' in line:
return f"{'' if ok else ''} 测试\n{line}\n\n{output[:2000]}"
return f"{'' if ok else ''} 测试\n{output[:2000]}"
def _build(self, target: str, options: str) -> str:
if not target or target == ".":
if os.path.exists("Makefile"):
cmd = "make"
elif os.path.exists("pyproject.toml"):
cmd = "python3 -m build"
elif os.path.exists("package.json"):
cmd = "npm run build"
else:
return " 未找到构建配置"
else:
cmd = target
ok, output = self._run(cmd, timeout=300)
return f"{'' if ok else ''} 构建: {cmd}\n{output[:2000]}"
def _deps(self, target: str, options: str) -> str:
ok, output = self._run("pip list --outdated --format=json", timeout=30)
if not ok:
return f" 检查失败\n{output}"
try:
outdated = json.loads(output)
if not outdated:
return " 所有依赖都是最新版本"
lines = [f" {len(outdated)} 个依赖可更新:\n"]
for pkg in outdated[:20]:
lines.append(f" {pkg['name']:30} {pkg['version']:12} → {pkg['latest_version']}")
return '\n'.join(lines)
except json.JSONDecodeError:
return f" 依赖状态:\n{output[:2000]}"
def _deps_check(self, target: str, options: str) -> str:
tool = self._find_tool("pip-audit", "safety")
if not tool:
return " 未安装安全检查工具。安装: pip install pip-audit"
cmd = "pip-audit" if tool == "pip-audit" else "safety check"
ok, output = self._run(cmd, timeout=60)
return f"{'' if ok else ''} 安全检查 ({tool})\n{output[:2000]}"
def _pre_commit(self, target: str, options: str) -> str:
ok, output = self._run("pre-commit run --all-files", timeout=120)
return f"{'' if ok else ''} pre-commit\n{output[:2000]}"
# ══════════════════════════════════════
# 项目分析
# ══════════════════════════════════════
def _complexity(self, target: str, options: str) -> str:
files = self._walk_py(target)
results = []
for fp in files:
try:
with open(fp, 'r', encoding='utf-8') as f:
tree = ast.parse(f.read())
except Exception:
continue
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
complexity = 1
for child in ast.walk(node):
if isinstance(child, (ast.If, ast.While, ast.For, ast.ExceptHandler)):
complexity += 1
elif isinstance(child, ast.BoolOp):
complexity += len(child.values) - 1
if complexity > 5:
rel = os.path.relpath(fp, target)
results.append((rel, node.lineno, node.name, complexity))
if not results:
return " 所有函数复杂度正常 (≤5)"
results.sort(key=lambda x: x[3], reverse=True)
lines = [f" 高复杂度函数 ({len(results)} 个):\n"]
for file, line, func, c in results[:20]:
icon = "" if c > 10 else ""
lines.append(f" {icon} {file}:{line} {func}() 复杂度={c}")
return '\n'.join(lines)
def _duplicates(self, target: str, options: str) -> str:
files = self._walk_py(target)
func_defs = {}
for fp in files:
try:
with open(fp, 'r', encoding='utf-8') as f:
tree = ast.parse(f.read())
except Exception:
continue
rel = os.path.relpath(fp, target)
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
func_defs.setdefault(node.name, []).append(f"{rel}:{node.lineno}")
dupes = {k: v for k, v in func_defs.items() if len(v) > 1}
if not dupes:
return " 没有重复函数名"
lines = [f" 重复函数名 ({len(dupes)} 个):\n"]
for name, locs in sorted(dupes.items(), key=lambda x: -len(x[1])):
lines.append(f" {name} ({len(locs)} 处):")
for loc in locs[:5]:
lines.append(f" • {loc}")
return '\n'.join(lines)
def _security(self, target: str, options: str) -> str:
files = self._walk_py(target)
patterns = [
(r'\beval\s*\(', 'eval() - 代码注入', 'HIGH'),
(r'\bexec\s*\(', 'exec() - 代码注入', 'HIGH'),
(r'subprocess\.call\s*\(\s*shell\s*=\s*True', 'shell=True - 命令注入', 'HIGH'),
(r'pickle\.loads?\s*\(', 'pickle 反序列化', 'HIGH'),
(r'password\s*=\s*["\']', '硬编码密码', 'HIGH'),
(r'api_key\s*=\s*["\']', '硬编码 API Key', 'HIGH'),
]
risks = []
for fp in files:
try:
with open(fp, 'r', encoding='utf-8') as f:
for i, line in enumerate(f, 1):
for pat, desc, level in patterns:
if re.search(pat, line, re.IGNORECASE):
rel = os.path.relpath(fp, target)
risks.append((rel, i, level, desc, line.strip()))
except Exception:
continue
if not risks:
return " 没有发现安全风险"
lines = [f" 安全风险 ({len(risks)} 个):\n"]
for file, line, level, desc, code in risks[:20]:
lines.append(f" {'' if level == 'HIGH' else ''} {file}:{line} [{level}] {desc}")
lines.append(f" {code[:60]}")
return '\n'.join(lines)
def _metrics(self, target: str, options: str) -> str:
files = self._walk_py(target)
total = blank = comment = code = funcs = classes = max_lines = 0
long_files = []
for fp in files:
try:
with open(fp, 'r', encoding='utf-8') as f:
lines = f.readlines()
except Exception:
continue
fl = len(lines)
total += fl
max_lines = max(max_lines, fl)
if fl > 300:
long_files.append((os.path.relpath(fp, target), fl))
for line in lines:
s = line.strip()
if not s:
blank += 1
elif s.startswith('#'):
comment += 1
else:
code += 1
try:
with open(fp, 'r', encoding='utf-8') as f:
tree = ast.parse(f.read())
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
funcs += 1
elif isinstance(node, ast.ClassDef):
classes += 1
except Exception:
pass
rate = comment / max(code, 1) * 100
lines = [
f" 代码指标 ({target}):\n",
f" 文件: {len(files)} | 总行: {total} | 代码: {code}",
f" 注释: {comment} ({rate:.1f}%) | 空行: {blank}",
f" 函数: {funcs} | 类: {classes} | 最大文件: {max_lines} 行",
]
if long_files:
lines.append(f"\n 超过 300 行的文件 ({len(long_files)}):")
for f, l in sorted(long_files, key=lambda x: -x[1])[:5]:
lines.append(f" • {f}: {l} 行")
return '\n'.join(lines)
def _suggest(self, target: str, options: str) -> str:
files = os.listdir(target) if os.path.isdir(target) else []
py_files = self._walk_py(target)
suggestions = []
if 'README.md' not in files:
suggestions.append(" 添加 README.md")
if 'pyproject.toml' not in files:
suggestions.append(" 使用 pyproject.toml")
if '.gitignore' not in files:
suggestions.append(" 添加 .gitignore")
test_files = [f for f in py_files if 'test' in os.path.basename(f).lower()]
if not test_files and len(py_files) > 5:
suggestions.append(" 添加单元测试")
uses_print = 0
uses_log = 0
for f in py_files:
with open(f, errors='ignore') as fh:
content = fh.read()
if 'print(' in content:
uses_print += 1
if 'logging' in content:
uses_log += 1
if uses_print > uses_log and uses_print > 3:
suggestions.append(" 考虑用 logging 替代 print")
return " 改进建议:\n" + '\n'.join(f" {s}" for s in suggestions) if suggestions else " 项目结构良好"
# ══════════════════════════════════════
# 项目信息与初始化
# ══════════════════════════════════════
def _info(self, target: str, options: str) -> str:
info = []
files = os.listdir(target or ".")
if "pyproject.toml" in files:
info.append(" 项目类型: Python (pyproject.toml)")
elif "setup.py" in files:
info.append(" 项目类型: Python (setup.py)")
elif "package.json" in files:
info.append(" 项目类型: Node.js")
else:
info.append(" 项目类型: 未知")
ok, ver = self._run("python3 --version", timeout=5)
if ok:
info.append(f" Python: {ver}")
ok, req = self._run("pip list --format=json", timeout=10)
if ok:
try:
info.append(f" 依赖: {len(json.loads(req))} 个")
except Exception:
pass
ok, git = self._run("git log --oneline -1", timeout=5)
if ok:
info.append(f" 最新提交: {git}")
ok, branch = self._run("git branch --show-current", timeout=5)
if ok:
info.append(f" 分支: {branch}")
py_count = len([f for f in self._walk_py(target or ".")])
info.append(f" Python 文件: {py_count}")
return '\n'.join(info)
def _init_project(self, target: str, options: str) -> str:
ptype = target if target != "." else "python"
if ptype == "python":
dirs = ["src", "tests", "docs"]
files = {
"pyproject.toml": '[project]\nname = "my-project"\nversion = "0.1.0"\n',
"README.md": "# My Project\n",
".gitignore": "__pycache__/\n*.pyc\n.venv/\n",
}
elif ptype == "web":
dirs = ["src", "public", "tests"]
files = {
"package.json": '{\n "name": "my-project",\n "version": "1.0.0"\n}\n',
".gitignore": "node_modules/\ndist/\n",
}
else:
return f" 不支持: {ptype}。支持: python, web"
created = []
for d in dirs:
os.makedirs(d, exist_ok=True)
created.append(f" {d}/")
for f, content in files.items():
if not os.path.exists(f):
os.makedirs(os.path.dirname(f) or ".", exist_ok=True)
with open(f, 'w') as fp:
fp.write(content)
created.append(f" {f}")
return f" 已初始化 {ptype} 项目\n" + '\n'.join(created)
+545
View File
@@ -0,0 +1,545 @@
"""
浏览器自动化插件 - 无头浏览器操控
基于 Playwright,支持页面导航、元素交互、截图、JS 执行
"""
import os
import json
import base64
import time
from typing import Optional
class Liugin:
"""浏览器自动化 - 网页操控、表单填写、数据抓取、截图"""
def __init__(self):
self.usage = """浏览器自动化工具
连接:
start - 启动浏览器实例
stop - 关闭浏览器
status - 查看浏览器状态
导航:
open <URL> - 打开网页
back - 后退
forward - 前进
reload - 刷新页面
title - 获取页面标题
url - 获取当前 URL
交互:
click <选择器> - 点击元素
fill <选择器> <文本> - 填写输入框
select <选择器> <值> - 下拉选择
check <选择器> - 勾选复选框
uncheck <选择器> - 取消勾选
hover <选择器> - 鼠标悬停
press <按键> - 按键 (Enter/Tab/Escape 等)
提取:
text [选择器] - 获取元素文本 (默认 body)
html [选择器] - 获取元素 HTML
attr <选择器> <属性名> - 获取元素属性
value <选择器> - 获取输入框值
eval <JS代码> - 执行 JavaScript
query <选择器> - 查询匹配元素列表
截图:
screenshot [路径] - 页面截图 (默认 screenshot.png)
screenshot --selector <选择器> - 元素截图
等待:
wait <选择器> [超时秒数] - 等待元素出现
wait-gone <选择器> [超时秒数] - 等待元素消失
wait-url <URL片段> [超时秒数] - 等待 URL 变化
高级:
pdf [路径] - 保存页面为 PDF
cookies - 获取所有 cookies
set-cookie <JSON> - 设置 cookie
viewport <宽> <高> - 设置视口大小
download <URL> [保存路径] - 下载文件
"""
self.cli = None
self._browser = None
self._context = None
self._page = None
self._pw = None
def set_cli(self, cli):
self.cli = cli
def get_tool_info(self):
return {
"name": "browser_auto",
"description": "浏览器自动化 - 网页导航、元素交互、表单填写、数据抓取、截图、JS执行",
"keywords": ["浏览器", "browser", "网页", "点击", "截图", "screenshot", "爬虫",
"playwright", "自动化", "表单", "抓取", "导航", "puppeteer"],
"usage": self.usage
}
def get_mcp_definition(self):
return {
"name": "browser_auto",
"description": "浏览器自动化工具",
"inputSchema": {
"type": "object",
"properties": {
"operation": {"type": "string", "description": "操作类型"},
"url": {"type": "string", "description": "网页 URL"},
"selector": {"type": "string", "description": "CSS 选择器"},
"value": {"type": "string", "description": "输入值"},
"script": {"type": "string", "description": "JavaScript 代码"},
"path": {"type": "string", "description": "保存路径"}
},
"required": ["operation"]
}
}
def convert_mcp_args(self, arguments):
op = arguments.get("operation", "")
url = arguments.get("url", "")
selector = arguments.get("selector", "")
return f"{op} {url} {selector}".strip()
# ── 路由 ──
def handle(self, args: str) -> str:
try:
parts = args.strip().split(maxsplit=1)
if not parts:
return "错误:请提供操作类型"
op = parts[0].lower()
rest = parts[1] if len(parts) > 1 else ""
handlers = {
"start": self._start,
"stop": self._stop,
"status": self._status,
"open": self._open,
"back": self._back,
"forward": self._forward,
"reload": self._reload,
"title": self._title,
"url": self._url,
"click": self._click,
"fill": self._fill,
"select": self._select,
"check": self._check,
"uncheck": self._uncheck,
"hover": self._hover,
"press": self._press,
"text": self._text,
"html": self._html,
"attr": self._attr,
"value": self._value,
"eval": self._eval,
"query": self._query,
"screenshot": self._screenshot,
"wait": self._wait,
"wait-gone": self._wait_gone,
"wait-url": self._wait_url,
"pdf": self._pdf,
"cookies": self._cookies,
"set-cookie": self._set_cookie,
"viewport": self._viewport,
"download": self._download,
}
handler = handlers.get(op)
if not handler:
return f"错误:不支持的操作 '{op}'"
return handler(rest)
except Exception as e:
return f"浏览器自动化错误: {str(e)}"
# ── 浏览器生命周期 ──
def _ensure_playwright(self):
"""确保 Playwright 已安装并可用"""
try:
from playwright.sync_api import sync_playwright
return sync_playwright
except ImportError:
return None
def _start(self, args: str) -> str:
if self._browser and self._page:
return "浏览器已在运行中"
sync_playwright = self._ensure_playwright()
if not sync_playwright:
return (" 未安装 Playwright。安装步骤:\n"
" pip install playwright\n"
" playwright install chromium")
try:
self._pw = sync_playwright().start()
self._browser = self._pw.chromium.launch(headless=True)
self._context = self._browser.new_context(
viewport={"width": 1280, "height": 720},
user_agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
)
self._page = self._context.new_page()
return " 浏览器已启动 (headless Chromium, 1280×720)"
except Exception as e:
return f" 启动失败: {e}"
def _stop(self, args: str) -> str:
try:
if self._context:
self._context.close()
if self._browser:
self._browser.close()
if self._pw:
self._pw.stop()
except Exception:
pass
self._page = None
self._context = None
self._browser = None
self._pw = None
return " 浏览器已关闭"
def _status(self, args: str) -> str:
if self._page and not self._page.is_closed():
url = self._page.url
title = self._page.title()
return f" 运行中\n URL: {url}\n 标题: {title}"
return " 未运行"
def _require_page(self):
"""确保页面可用,否则自动启动"""
if self._page and not self._page.is_closed():
return self._page
self._start("")
return self._page
# ── 导航 ──
def _open(self, args: str) -> str:
url = args.strip()
if not url:
return "错误:请提供 URL"
if not url.startswith(("http://", "https://", "file://")):
url = "https://" + url
page = self._require_page()
try:
page.goto(url, wait_until="domcontentloaded", timeout=30000)
title = page.title()
return f" 已打开: {url}\n 标题: {title}"
except Exception as e:
return f" 打开失败: {e}"
def _back(self, args: str) -> str:
page = self._require_page()
page.go_back()
return f" 后退 → {page.url}"
def _forward(self, args: str) -> str:
page = self._require_page()
page.go_forward()
return f" 前进 → {page.url}"
def _reload(self, args: str) -> str:
page = self._require_page()
page.reload()
return f" 已刷新: {page.url}"
def _title(self, args: str) -> str:
page = self._require_page()
return page.title() or "(无标题)"
def _url(self, args: str) -> str:
page = self._require_page()
return page.url
# ── 交互 ──
def _click(self, selector: str) -> str:
selector = selector.strip()
if not selector:
return "错误:请提供 CSS 选择器"
page = self._require_page()
try:
page.click(selector, timeout=10000)
return f" 已点击: {selector}"
except Exception as e:
return f" 点击失败: {e}"
def _fill(self, args: str) -> str:
parts = args.strip().split(maxsplit=1)
if len(parts) < 2:
return "错误:格式: fill <选择器> <文本>"
selector, text = parts[0], parts[1]
page = self._require_page()
try:
page.fill(selector, text, timeout=10000)
return f" 已填写: {selector}"
except Exception as e:
return f" 填写失败: {e}"
def _select(self, args: str) -> str:
parts = args.strip().split(maxsplit=1)
if len(parts) < 2:
return "错误:格式: select <选择器> <值>"
selector, value = parts[0], parts[1]
page = self._require_page()
try:
page.select_option(selector, value, timeout=10000)
return f" 已选择: {selector} = {value}"
except Exception as e:
return f" 选择失败: {e}"
def _check(self, selector: str) -> str:
page = self._require_page()
try:
page.check(selector.strip(), timeout=10000)
return f" 已勾选: {selector}"
except Exception as e:
return f" 勾选失败: {e}"
def _uncheck(self, selector: str) -> str:
page = self._require_page()
try:
page.uncheck(selector.strip(), timeout=10000)
return f" 已取消勾选: {selector}"
except Exception as e:
return f" 取消勾选失败: {e}"
def _hover(self, selector: str) -> str:
page = self._require_page()
try:
page.hover(selector.strip(), timeout=10000)
return f" 悬停: {selector}"
except Exception as e:
return f" 悬停失败: {e}"
def _press(self, key: str) -> str:
page = self._require_page()
page.keyboard.press(key.strip())
return f" 按键: {key}"
# ── 提取 ──
def _text(self, selector: str) -> str:
selector = selector.strip() or "body"
page = self._require_page()
try:
el = page.query_selector(selector)
if el:
text = el.inner_text()
return text
return f"未找到元素: {selector}"
except Exception as e:
return f" 获取文本失败: {e}"
def _html(self, selector: str) -> str:
selector = selector.strip() or "body"
page = self._require_page()
try:
el = page.query_selector(selector)
if el:
html = el.inner_html()
return html
return f"未找到元素: {selector}"
except Exception as e:
return f" 获取 HTML 失败: {e}"
def _attr(self, args: str) -> str:
parts = args.strip().split(maxsplit=1)
if len(parts) < 2:
return "错误:格式: attr <选择器> <属性名>"
selector, attr_name = parts[0], parts[1]
page = self._require_page()
try:
el = page.query_selector(selector)
if el:
val = el.get_attribute(attr_name)
return f"{attr_name} = {val}"
return f"未找到元素: {selector}"
except Exception as e:
return f" 获取属性失败: {e}"
def _value(self, selector: str) -> str:
page = self._require_page()
try:
el = page.query_selector(selector.strip())
if el:
return el.input_value()
return f"未找到元素: {selector}"
except Exception as e:
return f" 获取值失败: {e}"
def _eval(self, script: str) -> str:
script = script.strip()
if not script:
return "错误:请提供 JavaScript 代码"
page = self._require_page()
try:
result = page.evaluate(script)
if isinstance(result, (dict, list)):
return json.dumps(result, ensure_ascii=False, indent=2)
return str(result)
except Exception as e:
return f" JS 执行失败: {e}"
def _query(self, selector: str) -> str:
selector = selector.strip()
if not selector:
return "错误:请提供 CSS 选择器"
page = self._require_page()
try:
elements = page.query_selector_all(selector)
if not elements:
return f"未找到匹配 '{selector}' 的元素"
result = [f"找到 {len(elements)} 个元素:\n"]
for i, el in enumerate(elements[:20]):
tag = el.evaluate("el => el.tagName.toLowerCase()")
text = el.inner_text()[:80].replace('\n', ' ').strip()
result.append(f" [{i}] <{tag}> {text}")
if len(elements) > 20:
result.append(f" ... 还有 {len(elements) - 20} 个")
return '\n'.join(result)
except Exception as e:
return f" 查询失败: {e}"
# ── 截图 ──
def _screenshot(self, args: str) -> str:
page = self._require_page()
# 解析参数
path = "screenshot.png"
selector = None
parts = args.strip().split()
i = 0
while i < len(parts):
if parts[i] == "--selector" and i + 1 < len(parts):
selector = parts[i + 1]
i += 2
elif not parts[i].startswith("-"):
path = parts[i]
i += 1
else:
i += 1
try:
if selector:
el = page.query_selector(selector)
if not el:
return f"未找到元素: {selector}"
el.screenshot(path=path)
else:
page.screenshot(path=path, full_page=True)
size = os.path.getsize(path)
return f" 截图已保存: {path} ({size/1024:.1f} KB)"
except Exception as e:
return f" 截图失败: {e}"
# ── 等待 ──
def _wait(self, args: str) -> str:
parts = args.strip().split()
if not parts:
return "错误:请提供选择器"
selector = parts[0]
timeout = int(parts[1]) * 1000 if len(parts) > 1 else 30000
page = self._require_page()
try:
page.wait_for_selector(selector, timeout=timeout)
return f" 元素已出现: {selector}"
except Exception:
return f" 等待超时: {selector}"
def _wait_gone(self, args: str) -> str:
parts = args.strip().split()
if not parts:
return "错误:请提供选择器"
selector = parts[0]
timeout = int(parts[1]) * 1000 if len(parts) > 1 else 30000
page = self._require_page()
try:
page.wait_for_selector(selector, state="hidden", timeout=timeout)
return f" 元素已消失: {selector}"
except Exception:
return f" 等待超时: {selector}"
def _wait_url(self, args: str) -> str:
parts = args.strip().split()
if not parts:
return "错误:请提供 URL 片段"
url_part = parts[0]
timeout = int(parts[1]) * 1000 if len(parts) > 1 else 30000
page = self._require_page()
try:
page.wait_for_url(f"**{url_part}**", timeout=timeout)
return f" URL 已变化: {page.url}"
except Exception:
return f" 等待超时: URL 未变化"
# ── 高级 ──
def _pdf(self, args: str) -> str:
path = args.strip() or "page.pdf"
page = self._require_page()
try:
page.pdf(path=path)
size = os.path.getsize(path)
return f" PDF 已保存: {path} ({size/1024:.1f} KB)"
except Exception as e:
return f" PDF 生成失败: {e}"
def _cookies(self, args: str) -> str:
page = self._require_page()
cookies = page.context.cookies()
if not cookies:
return "无 cookies"
result = [f"Cookies ({len(cookies)} 个):\n"]
for c in cookies[:20]:
result.append(f" {c['name']}: {c['value'][:50]} (domain: {c.get('domain', '-')})")
return '\n'.join(result)
def _set_cookie(self, args: str) -> str:
try:
cookie = json.loads(args.strip())
except json.JSONDecodeError:
return "错误:cookie 必须是 JSON 格式: {\"name\": \"...\", \"value\": \"...\", \"domain\": \"...\"}"
page = self._require_page()
page.context.add_cookies([cookie])
return f" 已设置 cookie: {cookie.get('name', '?')}"
def _viewport(self, args: str) -> str:
parts = args.strip().split()
if len(parts) < 2:
return "错误:格式: viewport <宽> <高>"
try:
w, h = int(parts[0]), int(parts[1])
except ValueError:
return "错误:宽高必须是整数"
page = self._require_page()
page.set_viewport_size({"width": w, "height": h})
return f" 视口已设置: {w}×{h}"
def _download(self, args: str) -> str:
parts = args.strip().split()
if not parts:
return "错误:格式: download <URL> [保存路径]"
url = parts[0]
save_path = parts[1] if len(parts) > 1 else os.path.basename(url) or "download"
page = self._require_page()
try:
with page.expect_download() as download_info:
page.evaluate(f"window.open('{url}')")
download = download_info.value
download.save_as(save_path)
size = os.path.getsize(save_path)
return f" 已下载: {save_path} ({size/1024:.1f} KB)"
except Exception as e:
return f" 下载失败: {e}"
+147
View File
@@ -0,0 +1,147 @@
"""
命令执行器插件 - 跨平台 Shell 命令执行
"""
import os
import re
import subprocess
import platform
import sys
# 添加项目根目录到 path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from xcli_core.tool_result import ToolResult, ErrorCode
class Liugin:
"""Shell 命令执行器 - 跨平台命令行工具"""
def __init__(self):
self.usage = """Shell 命令执行器
操作:
run <命令> [--timeout 秒] - 执行 Shell 命令 (默认超时 30s)
示例:
run ls -la - 列出当前目录
run python3 --version - 查看 Python 版本
run git status - 查看 Git 状态
run make build --timeout 60 - 执行构建 (60秒超时)
注意:
- 跨平台兼容 (Linux/macOS/Windows)
- 输出限制 5000 字符
- 某些危险命令会被拦截
"""
self.cli = None
def set_cli(self, cli):
self.cli = cli
def get_tool_info(self):
return {
"name": "cmd_executor",
"description": "Shell 命令执行器 - 跨平台执行系统命令,支持超时控制",
"keywords": ["命令", "执行", "shell", "cmd", "终端", "命令行", "subprocess"],
"usage": self.usage
}
def get_mcp_definition(self):
return {
"name": "cmd_executor",
"description": "Shell 命令执行器",
"inputSchema": {
"type": "object",
"properties": {
"command": {"type": "string", "description": "要执行的命令"},
"timeout": {"type": "integer", "description": "超时秒数", "default": 30}
},
"required": ["command"]
}
}
def convert_mcp_args(self, arguments):
cmd = arguments.get("command", "")
timeout = arguments.get("timeout", "")
result = f"run {cmd}"
if timeout:
result += f" --timeout {timeout}"
return result
# 危险命令黑名单
BLOCKED = {
'rm -rf /', 'rm -rf ~', 'mkfs', 'dd if=', 'wipefs', 'shred',
':(){ :|:& };:', 'chmod -R 777 /', 'mv / ', 'wget http',
}
def handle(self, args: str):
try:
parts = args.strip().split(maxsplit=1)
if not parts:
return ToolResult.missing("操作。可用: run <命令>")
operation = parts[0].lower()
if operation != "run":
return ToolResult.fail(f"不支持的操作 '{operation}'。可用: run", ErrorCode.UNSUPPORTED_OP)
if len(parts) < 2:
return ToolResult.missing("要执行的命令")
rest = parts[1]
# 解析 --timeout
timeout = 30
timeout_match = re.search(r'--timeout\s+(\d+)', rest)
if timeout_match:
timeout = int(timeout_match.group(1))
rest = re.sub(r'--timeout\s+\d+', '', rest).strip()
if not rest:
return ToolResult.missing("要执行的命令")
# 安全检查
for blocked in self.BLOCKED:
if blocked in rest:
return ToolResult.fail(f"安全拦截: 检测到危险命令模式 '{blocked}'", ErrorCode.PERMISSION_DENIED)
return self._execute(rest, timeout)
except Exception as e:
return ToolResult.fail(str(e), ErrorCode.INTERNAL_ERROR, tool_name="cmd_executor")
def _execute(self, command: str, timeout: int):
try:
use_shell = platform.system() == 'Windows'
result = subprocess.run(
command,
shell=True,
capture_output=True,
text=True,
timeout=timeout,
encoding='utf-8',
errors='replace'
)
stdout = result.stdout.strip()
stderr = result.stderr.strip()
rc = result.returncode
output = ""
if stdout:
output += stdout + "\n"
if stderr:
output += f"[stderr]\n{stderr}\n"
if rc != 0:
output += f"[exit code: {rc}]\n"
if not output:
output = f"命令执行完成,无输出 (超时: {timeout}s)"
if rc != 0:
return ToolResult.fail(output.strip(), ErrorCode.EXEC_FAILED, tool_name="cmd_executor")
return ToolResult.ok(output.strip(), tool_name="cmd_executor")
except subprocess.TimeoutExpired:
return ToolResult.timeout(timeout, tool_name="cmd_executor")
except Exception as e:
return ToolResult.fail(str(e), ErrorCode.EXEC_FAILED, tool_name="cmd_executor")
File diff suppressed because it is too large. Load diff
+564
View File
@@ -0,0 +1,564 @@
"""
编译验证插件 - 批量语法/编译检查
对项目内 Python 文件做 ast.parse + py_compile 双重验证,
支持按目录、单文件或全项目扫描,并发执行加速。
"""
import os
import sys
import ast
import time
import shutil
import py_compile
import concurrent.futures
from pathlib import Path
from typing import List, Tuple, Optional
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from xcli_core.tool_result import ToolResult, ErrorCode
try:
from colorama import Fore, Style, init as _colorama_init
_colorama_init(autoreset=False)
_HAS_COLOR = True
except Exception:
_HAS_COLOR = False
class _Stub:
def __getattr__(self, _): return ""
Fore = Style = _Stub()
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SKIP_DIRS = {
"__pycache__", ".git", ".venv", "venv", "env", ".env",
"node_modules", ".idea", ".vscode", "dist", "build",
".mypy_cache", ".pytest_cache", ".ruff_cache", "site-packages",
}
SKIP_FILES = {
"setup.py",
}
class Liugin:
"""编译验证 - 批量语法检查 / 编译验证 / 清理 __pycache__"""
def __init__(self):
self.usage = """编译验证工具
操作:
all - 验证项目内全部 Python 文件
core - 仅验证 xcli_core/ 目录
engines - 仅验证 ai_engines/ 目录
plugins - 仅验证 plugins/ 目录
dir <目录> - 验证指定目录
check <文件> - 验证单个文件 (可多次指定)
clean [目录] - 清理 __pycache__ (默认项目根)
stats - 统计各目录 Python 文件数量
选项:
-v / --verbose - 显示每个文件的结果
-j / --jobs <N> - 并发线程数 (默认 8)
-q / --quiet - 仅在结束时打印汇总
输出:
✅ 绿色 = 通过
❌ 红色 = 失败
汇总: 总数/通过/失败 + 失败文件详情
示例:
check all -v
check core
check plugins/compile_check.py
check dir ai_engines -j 16
check clean
"""
self.cli = None
def set_cli(self, cli):
self.cli = cli
def get_tool_info(self):
return {
"name": "compile_check",
"description": "编译验证工具 — 批量语法/编译检查(ast.parse + py_compile),支持目录、单文件、全项目扫描,并发加速,清理 __pycache__",
"keywords": ["编译", "验证", "语法", "检查", "compile", "syntax",
"check", "lint", "py_compile", "ast", "批量",
"validate", "verify", "clean", "pycache"],
"usage": self.usage,
}
def get_mcp_definition(self):
return {
"name": "compile_check",
"description": "编译验证工具",
"inputSchema": {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["all", "core", "engines", "plugins",
"dir", "check", "clean", "stats"],
"description": "操作类型"
},
"path": {"type": "string",
"description": "目录或文件路径"},
"paths": {"type": "array",
"items": {"type": "string"},
"description": "多文件路径列表"},
"verbose": {"type": "boolean", "default": False},
"jobs": {"type": "integer", "default": 8},
"quiet": {"type": "boolean", "default": False},
},
"required": ["operation"]
}
}
def convert_mcp_args(self, arguments):
op = arguments.get("operation", "")
path = arguments.get("path", "")
return f"{op} {path}".strip()
def handle(self, args: str):
try:
parts = self._parse_args(args)
if not parts:
return self._print_usage()
operation = parts[0].lower()
opts = self._extract_options(parts[1:])
if operation in ("help", "-h", "--help"):
return self._print_usage()
routes = {
"all": self._op_all,
"core": self._op_core,
"engines": self._op_engines,
"plugins": self._op_plugins,
"dir": self._op_dir,
"check": self._op_check,
"clean": self._op_clean,
"stats": self._op_stats,
}
handler = routes.get(operation)
if not handler:
return ToolResult.fail(
f"不支持的操作 '{operation}'",
ErrorCode.UNSUPPORTED_OP,
tool_name="compile_check",
)
return handler(opts)
except Exception as e:
return ToolResult.fail(
f"编译验证错误: {e}",
ErrorCode.EXEC_FAILED,
tool_name="compile_check",
)
def _print_usage(self):
self._emit(self.usage)
return ToolResult.ok("usage_printed", tool_name="compile_check")
def _out(self, msg: str):
"""统一输出"""
if self.cli is not None and hasattr(self.cli, "_output"):
self.cli._output(msg)
else:
print(msg)
def _emit(self, msg: str, end: str = "\n"):
"""统一输出,适配 CLI
- CLI 模式: 直接输出,尊重 end,支持 end="" 的进度条刷新
"""
if self.cli is not None and hasattr(self.cli, "_output"):
self.cli._output(msg)
else:
print(msg, end=end)
def _ok(self, msg: str) -> str:
return f"{Fore.GREEN}{msg}{Style.RESET_ALL}"
def _err(self, msg: str) -> str:
return f"{Fore.RED}{msg}{Style.RESET_ALL}"
def _warn(self, msg: str) -> str:
return f"{Fore.YELLOW}{msg}{Style.RESET_ALL}"
def _dim(self, msg: str) -> str:
return f"{Fore.CYAN}{msg}{Style.RESET_ALL}"
def _banner(self, title: str) -> str:
bar = "═" * max(20, len(title) + 4)
return f"{Fore.CYAN}{bar}\n {title}\n{bar}{Style.RESET_ALL}"
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 _extract_options(self, args: List[str]) -> dict:
opts = {
"positional": [],
"verbose": False,
"quiet": False,
"jobs": 8,
}
i = 0
while i < len(args):
a = args[i]
if a in ("-v", "--verbose"):
opts["verbose"] = True
elif a in ("-q", "--quiet"):
opts["quiet"] = True
elif a in ("-j", "--jobs") and i + 1 < len(args):
try:
opts["jobs"] = max(1, int(args[i + 1]))
except ValueError:
pass
i += 1
elif not a.startswith("-"):
opts["positional"].append(a)
i += 1
return opts
def _collect_py_files(self, root: str) -> List[str]:
root = os.path.abspath(root)
if not os.path.isdir(root):
return []
result: List[str] = []
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
for fn in filenames:
if not fn.endswith(".py"):
continue
if fn in SKIP_FILES:
continue
result.append(os.path.join(dirpath, fn))
result.sort()
return result
def _verify_one(self, filepath: str) -> Tuple[str, bool, str]:
try:
with open(filepath, "r", encoding="utf-8") as f:
src = f.read()
except UnicodeDecodeError:
try:
with open(filepath, "r", encoding="gbk") as f:
src = f.read()
except Exception as e:
return filepath, False, f"读取失败: {e}"
except Exception as e:
return filepath, False, f"读取失败: {e}"
try:
ast.parse(src, filename=filepath)
except SyntaxError as e:
line = getattr(e, "lineno", "?")
col = getattr(e, "offset", "?")
msg = getattr(e, "msg", str(e))
return filepath, False, f"SyntaxError 第{line}行第{col}列: {msg}"
try:
py_compile.compile(filepath, doraise=True, quiet=2)
except py_compile.PyCompileError as e:
return filepath, False, f"PyCompileError: {e.msg if hasattr(e, 'msg') else e}"
except Exception as e:
return filepath, False, f"编译失败: {e}"
return filepath, True, ""
def _run_batch(
self,
files: List[str],
title: str,
verbose: bool = False,
quiet: bool = False,
jobs: int = 8,
) -> Tuple[int, int, List[Tuple[str, str]]]:
total = len(files)
if total == 0:
self._emit(self._warn(f"{title}: 没有可验证的 Python 文件"))
return 0, 0, []
if not quiet:
self._emit(self._banner(f"{title} ({total} 个文件, {jobs} 线程)"))
self._emit("")
passed = 0
failed: List[Tuple[str, str]] = []
start = time.time()
results: List[Tuple[str, bool, str]] = []
if jobs == 1:
for f in files:
results.append(self._verify_one(f))
else:
with concurrent.futures.ThreadPoolExecutor(max_workers=jobs) as ex:
futures = {ex.submit(self._verify_one, f): f for f in files}
done = 0
for fut in concurrent.futures.as_completed(futures):
results.append(fut.result())
done += 1
if not quiet and not verbose and total > 0:
pct = done * 100 // total
rel = os.path.relpath(futures[fut], PROJECT_ROOT)
self._emit(f"\r 进度: [{done}/{total}] {pct:3d}% {rel[:40]:<40}", end="")
if not quiet and not verbose:
self._emit("")
results.sort(key=lambda x: x[0])
if verbose and not quiet:
self._emit("")
for filepath, ok, err in results:
rel = os.path.relpath(filepath, PROJECT_ROOT)
if ok:
passed += 1
if verbose and not quiet:
self._emit(f" {self._ok('✓')} {rel}")
else:
failed.append((rel, err))
if not quiet:
self._emit(f" {self._err('✗')} {rel}")
self._emit(f" {self._err(err)}")
elapsed = time.time() - start
self._emit("")
self._emit("─" * 60)
if failed:
self._emit(f" {self._err('✗ 失败:')} {len(failed)}/{total}")
self._emit(f" {self._ok('✓ 通过:')} {passed}/{total}")
self._emit(f" {self._dim(f'耗时: {elapsed:.2f}s')}")
self._emit("")
self._emit(self._err("失败列表:"))
for rel, err in failed:
self._emit(f" • {self._err(rel)}")
first_line = err.split("\n")[0][:120]
self._emit(f" {first_line}")
else:
self._emit(f" {self._ok(f'✓ 全部通过:')} {passed}/{total} 个文件")
self._emit(f" {self._dim(f'耗时: {elapsed:.2f}s')}")
self._emit("─" * 60)
return passed, len(failed), failed
def _op_all(self, opts: dict):
files = self._collect_py_files(PROJECT_ROOT)
passed, failed, _ = self._run_batch(
files,
"全项目编译验证",
verbose=opts["verbose"],
quiet=opts["quiet"],
jobs=opts["jobs"],
)
if failed == 0:
return ToolResult.ok(
f"全部通过 ({passed}/{passed})",
tool_name="compile_check",
)
return ToolResult.fail(
f"有 {failed} 个文件编译失败",
ErrorCode.EXEC_FAILED,
tool_name="compile_check",
)
def _op_core(self, opts: dict):
return self._scan_subdir("xcli_core", "xcli_core 编译验证", opts)
def _op_engines(self, opts: dict):
return self._scan_subdir("ai_engines", "ai_engines 编译验证", opts)
def _op_plugins(self, opts: dict):
return self._scan_subdir("plugins", "plugins 编译验证", opts)
def _scan_subdir(self, sub: str, title: str, opts: dict):
target = os.path.join(PROJECT_ROOT, sub)
if not os.path.isdir(target):
return ToolResult.not_found(f"目录: {sub}", tool_name="compile_check")
files = self._collect_py_files(target)
passed, failed, _ = self._run_batch(
files, title,
verbose=opts["verbose"],
quiet=opts["quiet"],
jobs=opts["jobs"],
)
if failed == 0:
return ToolResult.ok(
f"{sub} 全部通过 ({passed})",
tool_name="compile_check",
)
return ToolResult.fail(
f"{sub} 有 {failed} 个文件编译失败",
ErrorCode.EXEC_FAILED,
tool_name="compile_check",
)
def _op_dir(self, opts: dict):
if not opts["positional"]:
return ToolResult.missing("目录路径", tool_name="compile_check")
target = os.path.abspath(opts["positional"][0])
if not os.path.isdir(target):
return ToolResult.not_found(f"目录: {target}", tool_name="compile_check")
files = self._collect_py_files(target)
passed, failed, _ = self._run_batch(
files,
f"目录验证: {os.path.relpath(target, PROJECT_ROOT)}",
verbose=opts["verbose"],
quiet=opts["quiet"],
jobs=opts["jobs"],
)
if failed == 0:
return ToolResult.ok(
f"目录全部通过 ({passed})",
tool_name="compile_check",
)
return ToolResult.fail(
f"目录有 {failed} 个文件编译失败",
ErrorCode.EXEC_FAILED,
tool_name="compile_check",
)
def _op_check(self, opts: dict):
if not opts["positional"]:
return ToolResult.missing("文件路径", tool_name="compile_check")
targets = []
for p in opts["positional"]:
fp = os.path.abspath(p)
if not os.path.isfile(fp):
return ToolResult.not_found(
f"文件: {p}", tool_name="compile_check",
)
targets.append(fp)
if len(targets) == 1:
self._emit(self._banner(
f"单文件验证: {os.path.relpath(targets[0], PROJECT_ROOT)}"
))
filepath, ok, err = self._verify_one(targets[0])
rel = os.path.relpath(filepath, PROJECT_ROOT)
self._emit("")
if ok:
self._emit(f" {self._ok('✓ 通过')} {rel}")
self._emit("─" * 60)
return ToolResult.ok(f"通过: {rel}", tool_name="compile_check")
else:
self._emit(f" {self._err('✗ 失败')} {rel}")
self._emit(f" {self._err(err)}")
self._emit("─" * 60)
return ToolResult.fail(
f"编译失败: {rel}",
ErrorCode.EXEC_FAILED,
tool_name="compile_check",
)
passed, failed, _ = self._run_batch(
targets,
f"批量验证 ({len(targets)} 个文件)",
verbose=opts["verbose"],
quiet=opts["quiet"],
jobs=opts["jobs"],
)
if failed == 0:
return ToolResult.ok(
f"全部通过 ({passed})",
tool_name="compile_check",
)
return ToolResult.fail(
f"有 {failed} 个文件编译失败",
ErrorCode.EXEC_FAILED,
tool_name="compile_check",
)
def _op_clean(self, opts: dict):
target = PROJECT_ROOT
if opts["positional"]:
target = os.path.abspath(opts["positional"][0])
if not os.path.isdir(target):
return ToolResult.not_found(
f"目录: {target}", tool_name="compile_check",
)
self._emit(self._banner(f"清理 __pycache__: {os.path.relpath(target, PROJECT_ROOT)}"))
removed = 0
for dirpath, dirnames, _ in os.walk(target):
for d in list(dirnames):
if d == "__pycache__":
full = os.path.join(dirpath, d)
try:
shutil.rmtree(full)
removed += 1
rel = os.path.relpath(full, PROJECT_ROOT)
if not opts["quiet"]:
self._emit(f" {self._ok('✓ 已删除')} {rel}")
except Exception as e:
rel = os.path.relpath(full, PROJECT_ROOT)
self._emit(f" {self._err('✗ 失败')} {rel}: {e}")
self._emit("")
self._emit("─" * 60)
if removed:
self._emit(f" {self._ok(f'✓ 已清理')} {removed} 个 __pycache__")
else:
self._emit(f" {self._dim('没有可清理的 __pycache__')}")
self._emit("─" * 60)
return ToolResult.ok(
f"清理完成 ({removed} 个目录)",
tool_name="compile_check",
)
def _op_stats(self, opts: dict):
self._emit(self._banner("项目 Python 文件统计"))
subdirs = [
("xcli_core", "xcli_core"),
("ai_engines", "ai_engines"),
("plugins", "plugins"),
]
total = 0
rows: List[Tuple[str, int]] = []
for sub, label in subdirs:
target = os.path.join(PROJECT_ROOT, sub)
if os.path.isdir(target):
count = len(self._collect_py_files(target))
rows.append((label, count))
total += count
other = len(self._collect_py_files(PROJECT_ROOT)) - total
rows.append(("其他(根目录等)", other))
total += other
self._emit("")
self._emit(f" {'目录':<20} {'文件数':>8}")
self._emit(" " + "─" * 30)
for label, count in rows:
bar = "█" * min(count, 40) if count else ""
color = Fore.GREEN if count else Fore.CYAN
self._emit(f" {label:<20} {count:>8} {color}{bar}{Style.RESET_ALL}")
self._emit(" " + "─" * 30)
self._emit(f" {'合计':<20} {total:>8}")
self._emit("")
self._emit("─" * 60)
return ToolResult.ok(
f"统计完成 (共 {total} 个文件)",
tool_name="compile_check",
)
+354
View File
@@ -0,0 +1,354 @@
"""
文件管理器插件 - 文件系统操作
聚焦文件系统操作,代码编辑交给 code_editor
"""
import os
import json
import shutil
import sys
from datetime import datetime
from pathlib import Path
from typing import List
# 添加项目根目录到 path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from xcli_core.tool_result import ToolResult, ErrorCode
class Liugin:
"""文件管理器 - 文件和目录的增删改查"""
def __init__(self):
self.usage = """文件管理器
操作:
list <路径> [-p "模式"] - 列出目录内容
read <文件> [-n 行数] - 读取文件内容
write <文件> -c "内容" - 写入文件
append <文件> -c "内容" - 追加内容
copy <源> <目标> - 复制文件/目录
move <源> <目标> - 移动/重命名
delete <路径> [-r] - 删除文件/目录
search <目录> [-p 模式] [-c 内容] - 搜索文件
info <路径> - 文件详细信息
mkdir <路径> - 创建目录
"""
self.cli = None
def set_cli(self, cli):
self.cli = cli
def get_tool_info(self):
return {
"name": "file_manager",
"description": "文件管理器 - 文件和目录的创建、读写、复制、移动、删除、搜索",
"keywords": ["文件", "目录", "管理", "file", "复制", "删除", "搜索", "mkdir",
"读取", "写入", "移动", "重命名"],
"usage": self.usage
}
def get_mcp_definition(self):
return {
"name": "file_manager",
"description": "文件管理器",
"inputSchema": {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["list", "read", "write", "append", "copy",
"move", "delete", "search", "info", "mkdir"],
"description": "操作类型"
},
"path": {"type": "string"},
"content": {"type": "string"},
"destination": {"type": "string"},
"pattern": {"type": "string"},
"recursive": {"type": "boolean", "default": False}
},
"required": ["operation"]
}
}
def convert_mcp_args(self, arguments):
op = arguments.get("operation", "")
path = arguments.get("path", "")
return f"{op} {path}".strip()
def handle(self, args: str):
try:
parts = self._parse_args(args)
if not parts:
return ToolResult.missing("操作类型")
operation = parts[0].lower()
handlers = {
"list": self._op_list,
"read": self._op_read,
"write": self._op_write,
"append": self._op_append,
"copy": self._op_copy,
"move": self._op_move,
"delete": self._op_delete,
"search": self._op_search,
"info": self._op_info,
"mkdir": self._op_mkdir,
}
handler = handlers.get(operation)
if not handler:
return ToolResult.fail(f"不支持的操作 '{operation}'", ErrorCode.UNSUPPORTED_OP)
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_list(self, args: List[str]):
path, pattern = ".", None
i = 0
while i < len(args):
if args[i] == "-p" and i + 1 < len(args):
pattern = args[i + 1]; i += 1
elif not args[i].startswith("-"):
path = args[i]
i += 1
path = os.path.abspath(path)
if not os.path.isdir(path):
return ToolResult.not_found(f"目录: {path}")
items = []
try:
for item in os.listdir(path):
full = os.path.join(path, item)
if pattern and not Path(item).match(pattern):
continue
if os.path.isdir(full):
items.append(('dir', item, ''))
else:
size = os.path.getsize(full)
size_str = f"{size/1024:.1f}KB" if size > 1024 else f"{size}B"
items.append(('file', item, size_str))
items.sort(key=lambda x: (0 if x[0] == 'dir' else 1, x[1].lower()))
dirs = sum(1 for x in items if x[0] == 'dir')
files = len(items) - dirs
result = [f" {path}", "─" * 50]
for t, name, size in items[:100]:
result.append(f" {'' if t == 'dir' else ''} {name}{'/' if t == 'dir' else f' ({size})'}")
if len(items) > 100:
result.append(f" ... 还有 {len(items) - 100} 项")
result.append("─" * 50)
result.append(f"共 {dirs} 个文件夹, {files} 个文件")
return ToolResult.ok('\n'.join(result), tool_name="file_manager")
except Exception as e:
return ToolResult.fail(str(e), ErrorCode.EXEC_FAILED, tool_name="file_manager")
def _op_read(self, args: List[str]):
if not args:
return ToolResult.missing("文件路径")
file_path = os.path.abspath(args[0])
max_lines = None
i = 1
while i < len(args):
if args[i] == "-n" and i + 1 < len(args):
try:
max_lines = int(args[i + 1])
except ValueError:
return ToolResult.fail("行数必须是整数", ErrorCode.INVALID_ARGS)
i += 1
i += 1
if not os.path.isfile(file_path):
return ToolResult.not_found(f"文件: {file_path}")
size = os.path.getsize(file_path)
if size > 10 * 1024 * 1024:
return ToolResult.fail(f"文件过大 ({size/1024/1024:.1f}MB),请用 -n 限制行数", ErrorCode.INVALID_ARGS)
try:
with open(file_path, 'r', encoding='utf-8') as f:
if max_lines:
lines = []
total = 0
for i, line in enumerate(f):
total = i + 1
if i < max_lines:
lines.append(f"{i+1:4d} | {line.rstrip()}")
content = '\n'.join(lines)
header = f" {file_path} ({size}B, 前 {min(max_lines, total)}/{total} 行)"
else:
all_lines = f.readlines()
content = '\n'.join(f"{i+1:4d} | {l.rstrip()}" for i, l in enumerate(all_lines))
header = f" {file_path} ({size}B, {len(all_lines)} 行)"
return ToolResult.ok(f"{header}\n{'─' * 50}\n{content}", tool_name="file_manager")
except UnicodeDecodeError:
return ToolResult.fail("无法解码(可能是二进制文件)", ErrorCode.DECODE_ERROR)
def _op_write(self, args: List[str]):
if len(args) < 2:
return ToolResult.fail("格式: write <文件> -c <内容>", ErrorCode.MISSING_ARGS)
fp = os.path.abspath(args[0])
content = None
i = 1
while i < len(args):
if args[i] == "-c" and i + 1 < len(args):
content = args[i + 1]; i += 1
i += 1
if content is None:
return ToolResult.missing("内容 (-c 参数)")
os.makedirs(os.path.dirname(fp) or ".", exist_ok=True)
with open(fp, 'w', encoding='utf-8') as f:
f.write(content)
return ToolResult.ok(f"已写入: {fp} ({len(content)} 字符)", tool_name="file_manager")
def _op_append(self, args: List[str]):
if len(args) < 2:
return ToolResult.fail("格式: append <文件> -c <内容>", ErrorCode.MISSING_ARGS)
fp = os.path.abspath(args[0])
content = None
i = 1
while i < len(args):
if args[i] == "-c" and i + 1 < len(args):
content = args[i + 1]; i += 1
i += 1
if content is None:
return ToolResult.missing("内容 (-c 参数)")
if not os.path.isfile(fp):
return ToolResult.not_found(f"文件: {fp}")
with open(fp, 'a', encoding='utf-8') as f:
f.write(content)
return ToolResult.ok(f"已追加: {fp}", tool_name="file_manager")
def _op_copy(self, args: List[str]):
if len(args) < 2:
return ToolResult.missing("源和目标路径")
src, dst = os.path.abspath(args[0]), os.path.abspath(args[1])
if not os.path.exists(src):
return ToolResult.not_found(f"源: {src}")
os.makedirs(os.path.dirname(dst) or ".", exist_ok=True)
if os.path.isfile(src):
shutil.copy2(src, dst)
else:
shutil.copytree(src, dst, dirs_exist_ok=True)
return ToolResult.ok(f"已复制:\n {src}\n → {dst}", tool_name="file_manager")
def _op_move(self, args: List[str]):
if len(args) < 2:
return ToolResult.missing("源和目标路径")
src, dst = os.path.abspath(args[0]), os.path.abspath(args[1])
if not os.path.exists(src):
return ToolResult.not_found(f"源: {src}")
os.makedirs(os.path.dirname(dst) or ".", exist_ok=True)
shutil.move(src, dst)
return ToolResult.ok(f"已移动:\n {src}\n → {dst}", tool_name="file_manager")
def _op_delete(self, args: List[str]):
if not args:
return ToolResult.missing("路径")
path = os.path.abspath(args[0])
recursive = "-r" in args
if not os.path.exists(path):
return ToolResult.not_found(f"路径: {path}")
if os.path.isfile(path):
os.remove(path)
return ToolResult.ok(f"已删除文件: {path}", tool_name="file_manager")
elif os.path.isdir(path):
if recursive:
shutil.rmtree(path)
return ToolResult.ok(f"已删除目录: {path}", tool_name="file_manager")
elif not os.listdir(path):
os.rmdir(path)
return ToolResult.ok(f"已删除空目录: {path}", tool_name="file_manager")
else:
return ToolResult.fail("目录不为空,用 -r 递归删除", ErrorCode.INVALID_ARGS)
def _op_search(self, args: List[str]):
if not args:
return ToolResult.missing("搜索目录")
directory = os.path.abspath(args[0])
pattern, content_search = None, None
i = 1
while i < len(args):
if args[i] == "-p" and i + 1 < len(args):
pattern = args[i + 1]; i += 1
elif args[i] == "-c" and i + 1 < len(args):
content_search = args[i + 1]; i += 1
i += 1
if not os.path.isdir(directory):
return ToolResult.not_found(f"目录: {directory}")
results = []
for root, dirs, files in os.walk(directory):
for fn in files:
if pattern and not Path(fn).match(pattern):
continue
fp = os.path.join(root, fn)
rel = os.path.relpath(fp, directory)
if content_search:
try:
with open(fp, 'r', encoding='utf-8') as f:
for i, line in enumerate(f, 1):
if content_search in line:
results.append(f"{rel}:{i}: {line.strip()[:80]}")
if len(results) >= 50:
break
except (UnicodeDecodeError, IOError):
continue
else:
results.append(rel)
if len(results) >= 50:
break
if len(results) >= 50:
break
return ToolResult.ok(f"搜索结果 ({len(results)}):\n" + '\n'.join(results), tool_name="file_manager") if results else ToolResult.ok("未找到匹配", tool_name="file_manager")
def _op_info(self, args: List[str]):
if not args:
return ToolResult.missing("路径")
path = os.path.abspath(args[0])
if not os.path.exists(path):
return ToolResult.not_found(f"路径: {path}")
stat = os.stat(path)
result = [
f"路径: {path}",
f"类型: {'目录' if os.path.isdir(path) else '文件'}",
f"大小: {stat.st_size:,} 字节 ({stat.st_size/1024:.1f} KB)",
f"修改: {datetime.fromtimestamp(stat.st_mtime):%Y-%m-%d %H:%M:%S}",
f"访问: {datetime.fromtimestamp(stat.st_atime):%Y-%m-%d %H:%M:%S}",
]
if os.path.isfile(path):
ext = os.path.splitext(path)[1]
result.append(f"扩展名: {ext or '无'}")
return ToolResult.ok('\n'.join(result), tool_name="file_manager")
def _op_mkdir(self, args: List[str]):
if not args:
return ToolResult.missing("目录路径")
path = os.path.abspath(args[0])
if os.path.exists(path):
return ToolResult.fail(f"已存在: {path}", ErrorCode.INVALID_ARGS)
os.makedirs(path, exist_ok=True)
return ToolResult.ok(f"已创建: {path}", tool_name="file_manager")
+479
View File
@@ -0,0 +1,479 @@
"""
前端测试验证插件 - 类似 iFlow CLI 的 frontend-tester
支持HTML/CSS/JS自动化测试和UI验证
"""
import os
import re
import subprocess
from colorama import Fore, Style
class Liugin:
"""前端测试验证插件 - 自动化测试Web界面"""
def __init__(self):
self.usage = """前端测试工具使用方法:
JSON格式示例:
{"action": "use_tool", "tool": "frontend_tester", "args": "validate index.html"} - 验证HTML文件
{"action": "use_tool", "tool": "frontend_tester", "args": "test style.css"} - 测试CSS文件
{"action": "use_tool", "tool": "frontend_tester", "args": "check app.js"} - 检查JS文件
{"action": "use_tool", "tool": "frontend_tester", "args": "preview index.html"} - 在浏览器中预览
{"action": "use_tool", "tool": "frontend_tester", "args": "analyze project"} - 分析整个项目
功能说明:
- validate <HTML文件路径> - 验证HTML文件的语法和结构
- test <CSS文件路径> - 测试CSS文件的样式和兼容性
- check <JS文件路径> - 检查JS文件的语法和潜在问题
- preview <HTML文件路径> - 在默认浏览器中打开预览
- analyze <项目路径> - 分析整个前端项目的结构和问题
支持的检查项:
HTML语法验证
CSS语法和兼容性检查
JavaScript语法检查
响应式设计检查
可访问性检查
性能优化建议"""
self.cli = None
def set_cli(self, cli):
"""设置CLI实例引用"""
self.cli = cli
# 注册插件命令
self.cli.register_liugin_command('ftest', self.command_handler)
self.cli.register_liugin_command('frontend', self.command_handler)
def command_handler(self, args):
"""处理 /ftest 或 /frontend 命令"""
return self.handle(args)
def get_tool_info(self):
return {
"name": "frontend_tester",
"description": "前端测试验证工具,支持HTML/CSS/JS自动化测试和UI验证,包括语法检查、响应式设计、可访问性检查等",
"keywords": ["前端", "测试", "HTML", "CSS", "JavaScript", "UI", "验证", "preview", "validate", "frontend", "test"],
"usage": self.usage
}
def get_mcp_definition(self):
return {
"name": "frontend_tester",
"description": "前端测试验证工具,支持HTML/CSS/JS自动化测试",
"inputSchema": {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["validate", "lint", "test"],
"description": "操作类型"
},
"target": {
"type": "string",
"description": "目标文件路径"
}
},
"required": ["operation", "target"]
}
}
def convert_mcp_args(self, arguments):
op = arguments.get("operation", "")
target = arguments.get("target", "")
return f"{op} {target}"
def handle(self, args):
"""处理前端测试请求"""
try:
parts = args.strip().split(maxsplit=1)
if not parts:
return "错误:请提供操作类型和参数。使用 'frontend_tester help' 查看帮助"
operation = parts[0].lower()
if operation == "help":
return self.usage
elif operation == "validate":
# 验证HTML文件
if len(parts) < 2:
return "错误:请提供HTML文件路径。格式: validate <HTML文件路径>"
file_path = parts[1]
return self._validate_html(file_path)
elif operation == "test":
# 测试CSS文件
if len(parts) < 2:
return "错误:请提供CSS文件路径。格式: test <CSS文件路径>"
file_path = parts[1]
return self._test_css(file_path)
elif operation == "check":
# 检查JS文件
if len(parts) < 2:
return "错误:请提供JS文件路径。格式: check <JS文件路径>"
file_path = parts[1]
return self._check_js(file_path)
elif operation == "preview":
# 在浏览器中预览
if len(parts) < 2:
return "错误:请提供HTML文件路径。格式: preview <HTML文件路径>"
file_path = parts[1]
return self._preview_in_browser(file_path)
elif operation == "analyze":
# 分析整个项目
if len(parts) < 2:
return "错误:请提供项目路径。格式: analyze <项目路径>"
project_path = parts[1]
return self._analyze_project(project_path)
else:
return f"错误:不支持的操作 '{operation}'。支持的操作有: validate, test, check, preview, analyze, help"
except Exception as e:
return f"前端测试操作错误: {str(e)}"
def _validate_html(self, file_path):
"""验证HTML文件"""
if not os.path.exists(file_path):
return f" 文件不存在: {file_path}"
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
result = f"\n HTML验证结果: {file_path}\n"
result += "=" * 60 + "\n\n"
# 基本检查
issues = []
# 检查DOCTYPE
if not content.strip().startswith('<!DOCTYPE'):
issues.append({
"level": "warning",
"message": "缺少DOCTYPE声明,建议添加 <!DOCTYPE html>"
})
# 检查lang属性
if '<html' in content and 'lang=' not in content:
issues.append({
"level": "warning",
"message": "建议在<html>标签中添加lang属性,如 lang=\"zh-CN\""
})
# 检查meta标签
if '<meta charset=' not in content:
issues.append({
"level": "error",
"message": "缺少字符集meta标签,建议添加 <meta charset=\"UTF-8\">"
})
if '<meta name="viewport"' not in content:
issues.append({
"level": "warning",
"message": "缺少viewport meta标签,建议添加以支持响应式设计"
})
# 检查title标签
if '<title>' not in content:
issues.append({
"level": "error",
"message": "缺少<title>标签"
})
# 统计元素
div_count = content.count('<div')
img_count = content.count('<img')
img_alt_count = content.count('alt=')
link_count = content.count('<link')
script_count = content.count('<script')
result += f" 文件统计:\n"
result += f" - div元素: {div_count}\n"
result += f" - img元素: {img_count}\n"
result += f" - img alt属性: {img_alt_count}\n"
result += f" - link标签: {link_count}\n"
result += f" - script标签: {script_count}\n\n"
# 检查img alt属性
if img_count > 0 and img_alt_count < img_count:
issues.append({
"level": "warning",
"message": f"有 {img_count - img_alt_count} 个img元素缺少alt属性"
})
# 显示问题
if issues:
result += f" 发现 {len(issues)} 个问题:\n\n"
for i, issue in enumerate(issues, 1):
level = issue['level']
if level == 'error':
icon = f"{Fore.RED}{Style.RESET_ALL}"
elif level == 'warning':
icon = f"{Fore.YELLOW}{Style.RESET_ALL}"
else:
icon = f"{Fore.GREEN}ℹ{Style.RESET_ALL}"
result += f"{icon} {i}. {issue['message']}\n"
else:
result += f"{Fore.GREEN} 未发现问题{Style.RESET_ALL}\n"
result += "\n" + "=" * 60 + "\n"
return result
except UnicodeDecodeError:
return " 文件编码问题,无法读取"
except Exception as e:
return f" 验证失败: {str(e)}"
def _test_css(self, file_path):
"""测试CSS文件"""
if not os.path.exists(file_path):
return f" 文件不存在: {file_path}"
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
result = f"\n CSS测试结果: {file_path}\n"
result += "=" * 60 + "\n\n"
issues = []
# 检查常见的CSS问题
if content.count('{') != content.count('}'):
issues.append({
"level": "error",
"message": "大括号不匹配"
})
# 检查颜色使用
if '#000000' in content or '#ffffff' in content:
issues.append({
"level": "info",
"message": "使用了纯黑或纯白,建议使用更柔和的颜色"
})
# 检查单位使用
if content.count('px') > 10 and 'rem' not in content and 'em' not in content:
issues.append({
"level": "warning",
"message": "大量使用px单位,建议考虑使用rem或em以支持更好的缩放"
})
# 统计
rule_count = content.count('{')
selector_count = len([line for line in content.split('\n') if '{' in line])
result += f" 文件统计:\n"
result += f" - CSS规则: {rule_count}\n"
result += f" - 选择器: {selector_count}\n\n"
# 显示问题
if issues:
result += f" 发现 {len(issues)} 个问题:\n\n"
for i, issue in enumerate(issues, 1):
level = issue['level']
if level == 'error':
icon = f"{Fore.RED}{Style.RESET_ALL}"
elif level == 'warning':
icon = f"{Fore.YELLOW}{Style.RESET_ALL}"
else:
icon = f"{Fore.GREEN}ℹ{Style.RESET_ALL}"
result += f"{icon} {i}. {issue['message']}\n"
else:
result += f"{Fore.GREEN} 未发现问题{Style.RESET_ALL}\n"
result += "\n" + "=" * 60 + "\n"
return result
except UnicodeDecodeError:
return " 文件编码问题,无法读取"
except Exception as e:
return f" 测试失败: {str(e)}"
def _check_js(self, file_path):
"""检查JS文件"""
if not os.path.exists(file_path):
return f" 文件不存在: {file_path}"
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
result = f"\n JavaScript检查结果: {file_path}\n"
result += "=" * 60 + "\n\n"
issues = []
# 检查常见问题
if 'var ' in content:
issues.append({
"level": "warning",
"message": "使用了var声明,建议使用let或const"
})
if re.search(r'(?<![=!])==(?!=)', content):
issues.append({
"level": "warning",
"message": "使用了==比较,建议使用===严格相等"
})
if content.count('console.log') > 5:
issues.append({
"level": "info",
"message": f"有{content.count('console.log')}个console.log,生产环境建议移除"
})
if 'eval(' in content:
issues.append({
"level": "error",
"message": "使用了eval(),存在安全风险"
})
# 统计
line_count = len(content.split('\n'))
function_count = content.count('function ')
result += f" 文件统计:\n"
result += f" - 代码行数: {line_count}\n"
result += f" - 函数数量: {function_count}\n\n"
# 显示问题
if issues:
result += f" 发现 {len(issues)} 个问题:\n\n"
for i, issue in enumerate(issues, 1):
level = issue['level']
if level == 'error':
icon = f"{Fore.RED}{Style.RESET_ALL}"
elif level == 'warning':
icon = f"{Fore.YELLOW}{Style.RESET_ALL}"
else:
icon = f"{Fore.GREEN}ℹ{Style.RESET_ALL}"
result += f"{icon} {i}. {issue['message']}\n"
else:
result += f"{Fore.GREEN} 未发现问题{Style.RESET_ALL}\n"
result += "\n" + "=" * 60 + "\n"
return result
except UnicodeDecodeError:
return " 文件编码问题,无法读取"
except Exception as e:
return f" 检查失败: {str(e)}"
def _preview_in_browser(self, file_path):
"""在浏览器中预览"""
if not os.path.exists(file_path):
return f" 文件不存在: {file_path}"
try:
# Windows系统
os.startfile(file_path)
return f" 已在默认浏览器中打开: {file_path}"
except Exception as e:
return f" 无法打开浏览器: {str(e)}"
def _analyze_project(self, project_path):
"""分析整个前端项目"""
if not os.path.exists(project_path):
return f" 项目路径不存在: {project_path}"
result = f"\n 前端项目分析: {project_path}\n"
result += "=" * 60 + "\n\n"
try:
# 统计文件
html_files = []
css_files = []
js_files = []
for root, dirs, files in os.walk(project_path):
for file in files:
if file.endswith('.html'):
html_files.append(os.path.join(root, file))
elif file.endswith('.css'):
css_files.append(os.path.join(root, file))
elif file.endswith('.js'):
js_files.append(os.path.join(root, file))
result += f" 项目文件统计:\n"
result += f" - HTML文件: {len(html_files)}\n"
result += f" - CSS文件: {len(css_files)}\n"
result += f" - JavaScript文件: {len(js_files)}\n\n"
# 建议检查
result += f" 建议检查:\n"
if html_files:
result += f" - HTML文件:\n"
for html_file in html_files[:3]: # 只显示前3个
result += f" • {os.path.basename(html_file)}\n"
if len(html_files) > 3:
result += f" • ... 还有 {len(html_files) - 3} 个文件\n"
if css_files:
result += f" - CSS文件:\n"
for css_file in css_files[:3]:
result += f" • {os.path.basename(css_file)}\n"
if len(css_files) > 3:
result += f" • ... 还有 {len(css_files) - 3} 个文件\n"
if js_files:
result += f" - JavaScript文件:\n"
for js_file in js_files[:3]:
result += f" • {os.path.basename(js_file)}\n"
if len(js_files) > 3:
result += f" • ... 还有 {len(js_files) - 3} 个文件\n"
result += f"\n{Fore.GREEN} 分析完成{Style.RESET_ALL}\n"
result += "\n" + "=" * 60 + "\n"
return result
except Exception as e:
return f" 分析失败: {str(e)}"
# 测试函数
def test_plugin():
"""测试插件功能"""
plugin = Liugin()
print("前端测试验证插件测试:")
print("\n1. 创建测试HTML文件:")
test_html = """<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>测试页面</title>
</head>
<body>
<h1>测试</h1>
</body>
</html>"""
with open('test.html', 'w', encoding='utf-8') as f:
f.write(test_html)
print("\n2. 验证HTML:")
print(plugin.handle("validate test.html"))
print("\n3. 在浏览器中预览:")
print(plugin.handle("preview test.html"))
print("\n4. 分析项目:")
print(plugin.handle("analyze ."))
# 清理测试文件
if os.path.exists('test.html'):
os.remove('test.html')
if __name__ == "__main__":
test_plugin()
+426
View File
@@ -0,0 +1,426 @@
"""
Git 工具插件 - 统一的版本控制能力
合并原 git_tools + git_workflow,消除重复
"""
import os
import re
import subprocess
from typing import Tuple, Optional
class Liugin:
"""Git 版本控制 - 基础操作 + 工作流自动化"""
def __init__(self):
self.usage = """Git 版本控制工具
基础操作:
status - 查看仓库状态
diff [文件路径] - 查看未暂存变更
diff --cached - 查看已暂存变更
log [数量] - 提交历史 (默认10条)
log --oneline [数量] - 简洁提交历史
add <文件路径> - 暂存文件
add . - 暂存所有变更
commit <提交信息> - 提交变更
branch - 查看分支列表
branch <名称> - 创建新分支
checkout <分支名> - 切换分支
checkout -b <分支名> - 创建并切换分支
stash - 暂存当前变更
stash pop - 恢复暂存的变更
show <commit> - 查看指定提交
blame <文件路径> - 逐行修改记录
remote - 查看远程仓库
init - 初始化仓库
clone <URL> [目录] - 克隆仓库
工作流:
smart-commit [消息] - 智能提交 (自动 add + 规范化消息)
changelog [版本] - 生成 CHANGELOG
summary - 变更摘要 (未提交改动)
contributors - 贡献者统计
stale [天数] - 查找长期未更新文件
"""
self.cli = None
def set_cli(self, cli):
self.cli = cli
def get_tool_info(self):
return {
"name": "git_tools",
"description": "Git 版本控制 - 状态查看、提交、分支管理、diff、历史、blame、工作流自动化",
"keywords": ["git", "版本控制", "提交", "分支", "commit", "branch", "status", "diff",
"changelog", "log", "blame", "stash", "checkout", "merge"],
"usage": self.usage
}
def get_mcp_definition(self):
return {
"name": "git_tools",
"description": "Git 版本控制工具",
"inputSchema": {
"type": "object",
"properties": {
"operation": {"type": "string", "description": "Git 操作"},
"args": {"type": "string", "description": "操作参数"}
},
"required": ["operation"]
}
}
def convert_mcp_args(self, arguments):
op = arguments.get("operation", "")
args = arguments.get("args", "")
return f"{op} {args}".strip()
# ── 执行 Git 命令 ──
def _run_git(self, args: list, cwd: Optional[str] = None) -> Tuple[bool, str]:
try:
result = subprocess.run(
["git"] + args,
capture_output=True, text=True,
cwd=cwd, timeout=30
)
output = (result.stdout + result.stderr).strip()
return result.returncode == 0, output
except FileNotFoundError:
return False, "git 未安装,请先安装 Git"
except subprocess.TimeoutExpired:
return False, "Git 命令执行超时"
except Exception as e:
return False, str(e)
def _run_shell(self, cmd: str, cwd: Optional[str] = None) -> Tuple[bool, str]:
try:
result = subprocess.run(
cmd, shell=True, capture_output=True, text=True,
cwd=cwd or os.getcwd(), timeout=30
)
return result.returncode == 0, (result.stdout + result.stderr).strip()
except Exception as e:
return False, str(e)
def _get_cwd(self):
if self.cli and hasattr(self.cli, 'current_dir'):
return self.cli.current_dir
return None
# ── 路由 ──
def handle(self, args: str) -> str:
try:
parts = args.strip().split(maxsplit=1)
if not parts:
return self._status()
op = parts[0].lower()
rest = parts[1] if len(parts) > 1 else ""
cwd = self._get_cwd()
handlers = {
# 基础
"status": lambda r: self._status(cwd),
"diff": lambda r: self._diff(r, cwd),
"log": lambda r: self._log(r, cwd),
"add": lambda r: self._add(r, cwd),
"commit": lambda r: self._commit(r, cwd),
"branch": lambda r: self._branch(r, cwd),
"checkout": lambda r: self._checkout(r, cwd),
"stash": lambda r: self._stash(r, cwd),
"show": lambda r: self._show(r, cwd),
"blame": lambda r: self._blame(r, cwd),
"remote": lambda r: self._remote(cwd),
"init": lambda r: self._init(r, cwd),
"clone": lambda r: self._clone(r, cwd),
# 工作流
"smart-commit": lambda r: self._smart_commit(r, cwd),
"changelog": lambda r: self._changelog(r, cwd),
"summary": lambda r: self._summary(cwd),
"contributors": lambda r: self._contributors(cwd),
"stale": lambda r: self._stale(r, cwd),
}
handler = handlers.get(op)
if handler:
return handler(rest)
# 回退:直接执行 git 命令
ok, output = self._run_git(args.split(), cwd)
return output if ok else f"Git 错误: {output}"
except Exception as e:
return f"Git 工具错误: {str(e)}"
# ── 基础操作 ──
def _status(self, cwd=None) -> str:
ok, output = self._run_git(["status", "--porcelain"], cwd)
if not ok:
return f"Git 错误: {output}"
if not output:
# 获取分支
_, branch = self._run_git(["branch", "--show-current"], cwd)
return f" 工作区干净 (分支: {branch})"
lines = output.split('\n')
staged, unstaged, untracked = [], [], []
for line in lines:
if len(line) < 4:
continue
s, filename = line[:2], line[3:]
if s[0] in 'MADRC':
staged.append(f" {s[0]} {filename}")
if s[1] in 'MD':
unstaged.append(f" ! {s[1]} {filename}")
if s == '??':
untracked.append(f" ? {filename}")
_, branch = self._run_git(["branch", "--show-current"], cwd)
result = [f" Git 状态 (分支: {branch})"]
if staged:
result.append(f"\n已暂存 ({len(staged)}):")
result.extend(staged)
if unstaged:
result.append(f"\n未暂存 ({len(unstaged)}):")
result.extend(unstaged)
if untracked:
result.append(f"\n未跟踪 ({len(untracked)}):")
result.extend(untracked)
return '\n'.join(result)
def _diff(self, args: str, cwd=None) -> str:
args = args.strip()
if args == "--cached":
git_args = ["diff", "--cached"]
elif args:
git_args = ["diff", args]
else:
git_args = ["diff"]
ok, output = self._run_git(git_args, cwd)
if not ok:
return f"Git 错误: {output}"
if not output:
return "没有变更"
return output
def _log(self, args: str, cwd=None) -> str:
parts = args.split()
oneline = "--oneline" in parts
count = 10
for p in parts:
if p.isdigit():
count = int(p)
if oneline:
ok, output = self._run_git(
["log", "--oneline", f"-{count}", "--graph", "--decorate"], cwd)
else:
ok, output = self._run_git(
["log", f"-{count}", "--format=%h %an %ad %s", "--date=short"], cwd)
if not ok:
return f"Git 错误: {output}"
return output or "暂无提交历史"
def _add(self, args: str, cwd=None) -> str:
target = args.strip()
if not target:
return "错误:请提供文件路径(使用 . 暂存所有)"
ok, output = self._run_git(["add", target], cwd)
return f" 已暂存: {target}" if ok else f"Git add 失败: {output}"
def _commit(self, args: str, cwd=None) -> str:
message = args.strip()
if not message:
return "错误:请提供提交信息"
ok, output = self._run_git(["commit", "-m", message], cwd)
return f" 提交成功:\n{output}" if ok else f"Git commit 失败: {output}"
def _branch(self, args: str, cwd=None) -> str:
if not args.strip():
ok, output = self._run_git(["branch", "-a"], cwd)
return f" 分支列表:\n{output}" if ok else f"Git 错误: {output}"
else:
ok, output = self._run_git(["branch", args.strip()], cwd)
return f" 已创建分支: {args.strip()}" if ok else f"创建分支失败: {output}"
def _checkout(self, args: str, cwd=None) -> str:
args = args.strip()
if args.startswith("-b "):
branch = args[3:].strip()
ok, output = self._run_git(["checkout", "-b", branch], cwd)
else:
branch = args
ok, output = self._run_git(["checkout", branch], cwd)
return f" 已切换到: {branch}" if ok else f"切换失败: {output}"
def _stash(self, args: str, cwd=None) -> str:
if args.strip() == "pop":
ok, output = self._run_git(["stash", "pop"], cwd)
else:
ok, output = self._run_git(["stash"], cwd)
return f" {output}" if ok else f"Git stash 失败: {output}"
def _show(self, args: str, cwd=None) -> str:
commit = args.strip() or "HEAD"
ok, output = self._run_git(["show", "--stat", commit], cwd)
if not ok:
return f"Git show 失败: {output}"
return output
def _blame(self, args: str, cwd=None) -> str:
path = args.strip()
if not path:
return "错误:请提供文件路径"
ok, output = self._run_git(["blame", "--line-porcelain", path], cwd)
if not ok:
return f"Git blame 失败: {output}"
lines = output.split('\n')
result = []
author = ""
for line in lines:
if line.startswith('author '):
author = line[7:]
elif line.startswith('\t'):
result.append(f" {author}: {line[1:]}")
if len(result) > 100:
result = result[:100]
result.append(f" ... (共 {len(result)} 行)")
return f" {path} 修改记录:\n" + '\n'.join(result)
def _remote(self, cwd=None) -> str:
ok, output = self._run_git(["remote", "-v"], cwd)
if not ok:
return f"Git 错误: {output}"
return output or "没有配置远程仓库"
def _init(self, args: str, cwd=None) -> str:
path = args.strip() or "."
ok, output = self._run_git(["init", path], cwd)
return f" {output}" if ok else f"Git init 失败: {output}"
def _clone(self, args: str, cwd=None) -> str:
parts = args.split()
if not parts:
return "错误:请提供仓库 URL"
cmd = ["clone", parts[0]]
if len(parts) > 1:
cmd.append(parts[1])
ok, output = self._run_git(cmd, cwd)
return f" 克隆成功:\n{output}" if ok else f"Git clone 失败: {output}"
# ── 工作流 ──
def _smart_commit(self, message: str, cwd=None) -> str:
"""智能提交:自动 add + 规范化消息"""
ok, status = self._run_git(["status", "--porcelain"], cwd)
if not status:
return " 工作区干净,没有需要提交的变更"
self._run_git(["add", "-A"], cwd)
if not message:
message = self._auto_commit_message(status)
# conventional commits 检查
if not re.match(r'^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?: .+', message):
if 'test' in message.lower():
message = f"test: {message}"
elif 'doc' in message.lower() or 'readme' in message.lower():
message = f"docs: {message}"
else:
message = f"feat: {message}"
ok, output = self._run_git(["commit", "-m", message], cwd)
if ok:
_, hash_out = self._run_git(["log", "--oneline", "-1"], cwd)
return f" 已提交: {message}\n {hash_out}"
return f" 提交失败: {output}"
def _auto_commit_message(self, status: str) -> str:
lines = status.strip().split('\n')
added = len([l for l in lines if l.startswith('A ') or (len(l) > 1 and l.startswith('A') and l[1] == ' ')])
modified = len([l for l in lines if l.startswith('M ') or (len(l) > 1 and l[1] == 'M')])
deleted = len([l for l in lines if l.startswith('D ') or (len(l) > 1 and l[1] == 'D')])
parts = []
if added:
parts.append(f"添加 {added} 个文件")
if modified:
parts.append(f"修改 {modified} 个文件")
if deleted:
parts.append(f"删除 {deleted} 个文件")
return ", ".join(parts) if parts else "更新代码"
def _changelog(self, version: str, cwd=None) -> str:
ok, output = self._run_shell(
'git log --oneline --format="%s" | head -50', cwd)
if not ok:
return f" {output}"
commits = output.strip().split('\n')
categories = {
'feat': [], 'fix': [], 'docs': [], 'refactor': [],
'test': [], 'chore': [], 'other': []
}
for commit in commits:
matched = False
for prefix in categories:
if commit.startswith(prefix + ':') or commit.startswith(prefix + '('):
categories[prefix].append(commit)
matched = True
break
if not matched:
categories['other'].append(commit)
labels = {
'feat': ' 新增', 'fix': ' 修复', 'docs': ' 文档',
'refactor': ' 重构', 'test': ' 测试', 'chore': ' 杂项',
'other': ' 其他'
}
ver = version or "未发布"
result = [f"## {ver}\n"]
for prefix, label in labels.items():
items = categories[prefix]
if items:
result.append(f"\n### {label}")
for item in items[:10]:
result.append(f"- {item}")
return '\n'.join(result)
def _summary(self, cwd=None) -> str:
ok, output = self._run_git(["diff", "--stat"], cwd)
if not output:
return " 没有未提交的变更"
_, short = self._run_git(["diff", "--shortstat"], cwd)
return f" 变更摘要:\n\n{short}\n\n{output[:2000]}"
def _contributors(self, cwd=None) -> str:
ok, output = self._run_git(["shortlog", "-sn", "--all", "--no-merges"], cwd)
if not ok:
return f" {output}"
lines = output.strip().split('\n')
result = [f" 贡献者 ({len(lines)} 人):\n"]
for line in lines[:20]:
result.append(f" {line.strip()}")
return '\n'.join(result)
def _stale(self, args: str, cwd=None) -> str:
days = int(args) if args.strip().isdigit() else 90
ok, output = self._run_shell(
f"git log --diff-filter=A --format='%aI %n' --since='{days} days ago' | head -20", cwd)
return f" 最近 {days} 天新增的文件:\n{output[:2000]}" if ok else f" {output}"
+1036
View File
File diff suppressed because it is too large. Load diff
+383
View File
@@ -0,0 +1,383 @@
"""
UUID / ID 生成器插件 - 生成各种格式的唯一标识符
支持 UUID v1/v4/v5、NanoID、短ID、雪花ID、自定义格式
"""
import uuid
import hashlib
import time
import random
import string
import json
import os
import struct
from datetime import datetime
from typing import List
class Liugin:
"""UUID / ID 生成器 - 多格式唯一标识符生成"""
def __init__(self):
self.usage = """UUID / ID 生成器
操作:
uuid [-v 版本] [-n 数量] - 生成 UUID(默认 v4)
v1=时间戳 v4=随机 v5=命名
nanoid [-s 长度] [-n 数量] - 生成 NanoID(默认21位)
short [-s 长度] [-n 数量] - 生成短ID(默认8位)
snowflake [-n 数量] - 生成雪花ID(时间排序)
custom <模板> [-n 数量] - 自定义格式生成
模板: {U}=大写字母 {L}=小写 {D}=数字 {X}=十六进制
ulid [-n 数量] - 生成 ULID(时间排序+随机)
timestamp - 当前时间戳(秒/毫秒/微秒)
from_name <命名空间> <名称> - 基于名称生成确定性 UUID (v5)
batch <类型> [-n 数量] [-o 文件] - 批量生成并保存到文件
示例:
id_generator uuid
id_generator uuid -v 1 -n 5
id_generator nanoid -s 16
id_generator short -s 6 -n 10
id_generator custom "USER-{D}{D}{D}{D}{U}{U}" -n 5
id_generator snowflake -n 3
id_generator batch uuid -n 100 -o ids.txt
"""
self.cli = None
self._machine_id = random.randint(0, 1023)
self._sequence = 0
self._last_ts = 0
def set_cli(self, cli):
self.cli = cli
def get_tool_info(self):
return {
"name": "id_generator",
"description": "UUID/ID 生成器 - 支持 UUID、NanoID、短ID、雪花ID、ULID、自定义格式",
"keywords": ["uuid", "id", "生成", "唯一", "标识", "nanoid", "snowflake", "ulid", "短id"],
"usage": self.usage
}
def get_mcp_definition(self):
return {
"name": "id_generator",
"description": "UUID/ID 生成器,支持多种格式",
"inputSchema": {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["uuid", "nanoid", "short", "snowflake", "custom",
"ulid", "timestamp", "from_name", "batch"],
"description": "操作类型"
},
"version": {"type": "integer", "description": "UUID 版本 (1/4/5)"},
"length": {"type": "integer", "description": "ID 长度"},
"count": {"type": "integer", "description": "生成数量"},
"template": {"type": "string", "description": "自定义模板"},
"namespace": {"type": "string", "description": "命名空间"},
"name": {"type": "string", "description": "名称"},
"output": {"type": "string", "description": "输出文件"}
},
"required": ["operation"]
}
}
def convert_mcp_args(self, arguments):
op = arguments.get("operation", "")
count = arguments.get("count", "")
parts = [op]
if count:
parts.extend(["-n", str(count)])
return " ".join(parts)
def handle(self, args: str) -> str:
try:
parts = self._parse_args(args)
if not parts:
return self._op_uuid([])
operation = parts[0].lower()
handlers = {
"uuid": self._op_uuid,
"nanoid": self._op_nanoid,
"short": self._op_short,
"snowflake": self._op_snowflake,
"custom": self._op_custom,
"ulid": self._op_ulid,
"timestamp": self._op_timestamp,
"from_name": self._op_from_name,
"batch": self._op_batch,
}
handler = handlers.get(operation)
if not handler:
return f"错误:不支持的操作 '{operation}'"
return handler(parts[1:])
except Exception as e:
return f"ID 生成错误: {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 _op_uuid(self, args: List[str]) -> str:
opts = self._parse_opts(args)
version = int(opts.get("v", 4))
count = int(opts.get("n", 1))
count = min(count, 1000)
ids = []
for _ in range(count):
if version == 1:
ids.append(str(uuid.uuid1()))
elif version == 4:
ids.append(str(uuid.uuid4()))
elif version == 5:
ids.append(str(uuid.uuid5(uuid.NAMESPACE_DNS, "example.com")))
else:
return f"错误:不支持的 UUID 版本: {version}"
header = f" UUID v{version} ({count} 个):\n"
if count <= 20:
return header + "\n".join(f" {u}" for u in ids)
else:
return header + "\n".join(f" {u}" for u in ids[:10]) + f"\n ... (共 {count} 个)"
def _op_nanoid(self, args: List[str]) -> str:
opts = self._parse_opts(args)
size = int(opts.get("s", 21))
count = int(opts.get("n", 1))
count = min(count, 1000)
alphabet = string.ascii_letters + string.digits + "_-"
ids = []
for _ in range(count):
ids.append(''.join(random.choices(alphabet, k=size)))
header = f" NanoID (长度 {size}, {count} 个):\n"
if count <= 20:
return header + "\n".join(f" {i}" for i in ids)
else:
return header + "\n".join(f" {i}" for i in ids[:10]) + f"\n ... (共 {count} 个)"
def _op_short(self, args: List[str]) -> str:
opts = self._parse_opts(args)
size = int(opts.get("s", 8))
count = int(opts.get("n", 1))
count = min(count, 1000)
chars = string.ascii_letters + string.digits
ids = []
for _ in range(count):
ids.append(''.join(random.choices(chars, k=size)))
header = f" 短ID (长度 {size}, {count} 个):\n"
if count <= 20:
return header + "\n".join(f" {i}" for i in ids)
else:
return header + "\n".join(f" {i}" for i in ids[:10]) + f"\n ... (共 {count} 个)"
def _op_snowflake(self, args: List[str]) -> str:
opts = self._parse_opts(args)
count = int(opts.get("n", 1))
count = min(count, 1000)
# Twitter Snowflake: 41位时间戳 + 10位机器ID + 12位序列号
epoch = 1288834974657 # Twitter epoch (2010-11-04)
ids = []
for _ in range(count):
ts = int(time.time() * 1000) - epoch
if ts == self._last_ts:
self._sequence = (self._sequence + 1) & 0xFFF
if self._sequence == 0:
while ts <= self._last_ts:
ts = int(time.time() * 1000) - epoch
else:
self._sequence = 0
self._last_ts = ts
snowflake_id = (ts << 22) | (self._machine_id << 12) | self._sequence
ids.append(str(snowflake_id))
header = f"❄️ 雪花ID ({count} 个):\n"
if count <= 20:
return header + "\n".join(f" {i}" for i in ids)
else:
return header + "\n".join(f" {i}" for i in ids[:10]) + f"\n ... (共 {count} 个)"
def _op_custom(self, args: List[str]) -> str:
if not args:
return "错误:请提供模板\n示例: custom \"ORDER-{D}{D}{D}{D}-{U}{U}{U}\""
template = args[0]
opts = self._parse_opts(args[1:])
count = int(opts.get("n", 1))
count = min(count, 1000)
ids = []
for _ in range(count):
result = []
for ch in template:
if ch == '{':
continue # handled below
result.append(ch)
# Actually use proper template parsing
ids.append(self._expand_template(template))
header = f" ️ 自定义ID (模板: {template}, {count} 个):\n"
if count <= 20:
return header + "\n".join(f" {i}" for i in ids)
else:
return header + "\n".join(f" {i}" for i in ids[:10]) + f"\n ... (共 {count} 个)"
def _expand_template(self, template: str) -> str:
result = []
i = 0
while i < len(template):
if template[i] == '{' and i + 2 < len(template) and template[i + 2] == '}':
token = template[i + 1]
if token == 'U':
result.append(random.choice(string.ascii_uppercase))
elif token == 'L':
result.append(random.choice(string.ascii_lowercase))
elif token == 'D':
result.append(random.choice(string.digits))
elif token == 'X':
result.append(random.choice('0123456789ABCDEF'))
elif token == 'x':
result.append(random.choice('0123456789abcdef'))
elif token == 'A':
result.append(random.choice(string.ascii_letters))
else:
result.append(template[i:i + 3])
i += 3
else:
result.append(template[i])
i += 1
return ''.join(result)
def _op_ulid(self, args: List[str]) -> str:
opts = self._parse_opts(args)
count = int(opts.get("n", 1))
count = min(count, 1000)
# ULID: 48位时间戳 + 80位随机数, Crockford Base32
alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
ids = []
for _ in range(count):
ts = int(time.time() * 1000)
# 时间部分 (10 chars)
t_chars = []
t = ts
for _ in range(10):
t_chars.append(alphabet[t & 0x1F])
t >>= 5
t_chars.reverse()
# 随机部分 (16 chars)
r_chars = [random.choice(alphabet) for _ in range(16)]
ids.append(''.join(t_chars) + ''.join(r_chars))
header = f" ULID ({count} 个):\n"
if count <= 20:
return header + "\n".join(f" {i}" for i in ids)
else:
return header + "\n".join(f" {i}" for i in ids[:10]) + f"\n ... (共 {count} 个)"
def _op_timestamp(self, args: List[str]) -> str:
now = time.time()
return (f"⏱ 当前时间戳:\n"
f" 秒: {int(now)}\n"
f" 毫秒: {int(now * 1000)}\n"
f" 微秒: {int(now * 1000000)}\n"
f" ISO: {datetime.now().isoformat()}")
def _op_from_name(self, args: List[str]) -> str:
if len(args) < 2:
return "错误:格式: from_name <命名空间> <名称>\n命名空间: dns, url, oid, x500"
ns_map = {"dns": uuid.NAMESPACE_DNS, "url": uuid.NAMESPACE_URL,
"oid": uuid.NAMESPACE_OID, "x500": uuid.NAMESPACE_X500}
ns = ns_map.get(args[0].lower(), uuid.NAMESPACE_DNS)
name = args[1]
result = uuid.uuid5(ns, name)
return f" 确定性 UUID v5:\n 命名空间: {args[0]}\n 名称: {name}\n UUID: {result}"
def _op_batch(self, args: List[str]) -> str:
if not args:
return "错误:请提供类型 (uuid/nanoid/short/snowflake/ulid)"
id_type = args[0]
opts = self._parse_opts(args[1:])
count = int(opts.get("n", 10))
output = opts.get("o", "")
# 生成对应类型的ID
gen_args = [f"-n", str(count)]
handlers = {
"uuid": self._op_uuid,
"nanoid": self._op_nanoid,
"short": self._op_short,
"snowflake": self._op_snowflake,
"ulid": self._op_ulid,
}
handler = handlers.get(id_type)
if not handler:
return f"错误:不支持的类型 '{id_type}'"
result = handler(gen_args)
if output:
# 提取纯ID保存到文件
ids = [line.strip().lstrip(' ') for line in result.split('\n')
if line.strip() and not any(c in line for c in [' ', '️', '❄️'])]
try:
with open(output, 'w', encoding='utf-8') as f:
f.write('\n'.join(ids))
return f"{result}\n\n 已保存到: {output}"
except Exception as e:
return f"{result}\n\n 保存失败: {e}"
return result
def test_plugin():
plugin = Liugin()
print("ID 生成器测试:")
print(plugin.handle('uuid'))
print(plugin.handle('uuid -v 1 -n 3'))
print(plugin.handle('nanoid -s 16'))
print(plugin.handle('short -s 6 -n 5'))
print(plugin.handle('snowflake -n 3'))
print(plugin.handle('ulid'))
print(plugin.handle('timestamp'))
print(plugin.handle('custom "ORD-{D}{D}{D}{D}-{U}{U}{U}" -n 5'))
if __name__ == "__main__":
test_plugin()
+293
View File
@@ -0,0 +1,293 @@
"""
图片读取插件 — 从图片中提取文字,发送给 AI
=============================================
功能:读取图片 → OCR 提取文字 → 返回给 AI
支持:Tesseract OCR / Windows PowerShell OCR / 纯 Python 元数据
"""
import os
import sys
import json
import base64
import hashlib
import subprocess
import platform
import tempfile
from pathlib import Path
_IS_WIN = platform.system() == "Windows"
class Liugin:
"""图片读取 — 从图片中提取文字"""
def __init__(self):
self.cli = None
def set_cli(self, cli):
self.cli = cli
def get_tool_info(self):
return {
"name": "image_reader",
"description": "图片读取工具 — 从图片中提取文字(OCR),获取图片信息,"
"将图片内容转为文本发送给 AI",
"keywords": [
"图片", "image", "OCR", "文字识别", "读图", "截图",
"提取文字", "图片转文字", "识别", "tesseract",
],
"usage": (
"image_reader <操作> <路径>\n\n"
"操作:\n"
" ocr <图片路径> - 从图片中提取文字\n"
" info <图片路径> - 获取图片信息(尺寸/格式/大小)\n"
" read <图片路径> - 综合读取(信息 + OCR)\n"
" ascii <图片路径> [宽] - 转 ASCII 字符画(终端显示)\n"
" base64 <图片路径> - 转 Base64 编码\n"
" hash <图片路径> - 计算图片哈希值\n\n"
"示例:\n"
" image_reader ocr screenshot.png - 识别截图中的文字\n"
" image_reader read photo.jpg - 综合读取图片\n"
" image_reader ascii logo.png 40 - 转40列宽ASCII画\n"
),
}
def get_mcp_definition(self):
return {
"name": "image_reader",
"description": "图片读取工具 — 从图片中提取文字(OCR)",
"inputSchema": {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["ocr", "info", "read", "ascii", "base64", "hash"],
"description": "操作类型",
},
"path": {"type": "string", "description": "图片文件路径"},
"width": {"type": "integer", "description": "ASCII 画宽度"},
},
"required": ["action", "path"],
},
}
def convert_mcp_args(self, arguments):
action = arguments.get("action", "")
path = arguments.get("path", "")
width = arguments.get("width", "")
parts = [action, path]
if width:
parts.append(str(width))
return " ".join(parts)
def handle(self, args: str) -> str:
parts = args.strip().split(maxsplit=2)
if not parts:
return "请提供操作和图片路径"
action = parts[0].lower()
path = parts[1] if len(parts) > 1 else ""
extra = parts[2] if len(parts) > 2 else ""
if not path:
return "请提供图片路径"
# 展开 ~ 和环境变量
path = os.path.expanduser(os.path.expandvars(path))
if not os.path.isfile(path):
return f"文件不存在: {path}"
handlers = {
"ocr": self._ocr,
"info": self._info,
"read": self._read,
"ascii": self._ascii,
"base64": self._b64,
"hash": self._hash,
}
h = handlers.get(action)
if not h:
return f"不支持的操作 '{action}'"
try:
return h(path, extra)
except Exception as e:
return f"错误: {e}"
# ── 图片信息 ──
def _info(self, path: str, _: str) -> str:
size = os.path.getsize(path)
ext = os.path.splitext(path)[1].lower()
lines = [
f"图片: {os.path.basename(path)}",
f"格式: {ext}",
f"大小: {self._human_size(size)}",
]
# 尝试用 PIL 获取尺寸
try:
from PIL import Image
with Image.open(path) as img:
lines.append(f"尺寸: {img.width} x {img.height}")
lines.append(f"模式: {img.mode}")
if hasattr(img, 'info'):
dpi = img.info.get('dpi')
if dpi:
lines.append(f"DPI: {dpi}")
except ImportError:
pass
except Exception as e:
lines.append(f"读取详情失败: {e}")
return "\n".join(lines)
# ── OCR 文字识别 ──
def _ocr(self, path: str, _: str) -> str:
# 方案1: Tesseract
result = self._ocr_tesseract(path)
if result:
return result
# 方案2: Windows PowerShell OCR
if _IS_WIN:
result = self._ocr_windows(path)
if result:
return result
# 方案3: 用 PIL 读取图片元数据,至少告诉用户图片是什么
return (
"未找到 OCR 引擎。请安装以下任一工具:\n"
" - Tesseract: https://github.com/tesseract-ocr/tesseract\n"
" - Ubuntu: sudo apt install tesseract-ocr tesseract-ocr-chi-sim\n"
" - macOS: brew install tesseract\n"
" - Windows: 下载 UB-Mannheim installer\n\n"
f"图片信息:\n{self._info(path, '')}"
)
def _ocr_tesseract(self, path: str) -> str:
"""用 Tesseract OCR 提取文字"""
# 检查 tesseract 是否可用
try:
subprocess.run(["tesseract", "--version"],
capture_output=True, timeout=5)
except (FileNotFoundError, subprocess.TimeoutExpired):
return ""
# 执行 OCR
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as tmp:
tmp_path = tmp.name
try:
# 检测语言:中文+英文
cmd = ["tesseract", path, tmp_path[:-4], "-l", "chi_sim+eng"]
subprocess.run(cmd, capture_output=True, timeout=30)
if os.path.exists(tmp_path):
with open(tmp_path, "r", encoding="utf-8") as f:
text = f.read().strip()
if text:
return f"OCR 识别结果:\n{text}"
return "OCR 未识别到文字"
except subprocess.TimeoutExpired:
return "OCR 超时"
except Exception as e:
return f"OCR 失败: {e}"
finally:
try:
os.unlink(tmp_path)
except OSError:
pass
return ""
def _ocr_windows(self, path: str) -> str:
"""用 Windows 内置 OCR (PowerShell + Windows.Media.Ocr)"""
try:
ps_script = f'''
Add-Type -AssemblyName System.Runtime.WindowsRuntime
$bitmap = [Windows.Graphics.Imaging.SoftwareBitmap, Windows.Graphics.Imaging, ContentType=WindowsRuntime]
$ocrEngine = [Windows.Media.Ocr.OcrEngine, Windows.Media.Ocr, ContentType=WindowsRuntime]
$ocr = $ocrEngine.TryCreateFromUserProfileLanguages()
$file = Get-Item "{path}"
$stream = [System.IO.File]::OpenRead($file.FullName)
$decoder = [Windows.Graphics.Imaging.BitmapDecoder]::CreateAsync($stream).GetAwaiter().GetResult()
$bitmap = $decoder.GetSoftwareBitmapAsync().GetAwaiter().GetResult()
$result = $ocr.RecognizeAsync($bitmap).GetAwaiter().GetResult()
$result.Text
'''
result = subprocess.run(
["powershell", "-Command", ps_script],
capture_output=True, text=True, timeout=30
)
if result.returncode == 0 and result.stdout.strip():
return f"OCR 识别结果:\n{result.stdout.strip()}"
except Exception:
pass
return ""
# ── 综合读取 ──
def _read(self, path: str, _: str) -> str:
"""信息 + OCR"""
info = self._info(path, "")
ocr = self._ocr(path, "")
return f"{info}\n\n{ocr}"
# ── ASCII 字符画 ──
def _ascii(self, path: str, extra: str) -> str:
"""将图片转为 ASCII 字符画"""
width = int(extra) if extra.strip().isdigit() else 60
try:
from PIL import Image
except ImportError:
return "需要 Pillow 库: pip install Pillow"
chars = " .:-=+*#%@"
with Image.open(path) as img:
# 转灰度
img = img.convert("L")
# 缩放
ratio = img.height / img.width
new_height = int(width * ratio * 0.5) # 字符高宽比约 2:1
img = img.resize((width, new_height))
lines = []
for y in range(new_height):
row = ""
for x in range(width):
pixel = img.getpixel((x, y))
idx = pixel * (len(chars) - 1) // 255
row += chars[idx]
lines.append(row)
return "\n".join(lines)
# ── Base64 编码 ──
def _b64(self, path: str, _: str) -> str:
with open(path, "rb") as f:
data = f.read()
ext = os.path.splitext(path)[1].lower().lstrip(".")
mime = {"png": "image/png", "jpg": "image/jpeg", "jpeg": "image/jpeg",
"gif": "image/gif", "webp": "image/webp", "bmp": "image/bmp"
}.get(ext, "image/png")
b64 = base64.b64encode(data).decode("ascii")
return f"data:{mime};base64,{b64[:100]}... ({len(b64)} 字符)"
# ── 哈希 ──
def _hash(self, path: str, _: str) -> str:
with open(path, "rb") as f:
data = f.read()
md5 = hashlib.md5(data).hexdigest()
sha = hashlib.sha256(data).hexdigest()
return f"MD5: {md5}\nSHA256: {sha}"
# ── 工具 ──
@staticmethod
def _human_size(size: int) -> str:
for unit in ["B", "KB", "MB", "GB"]:
if size < 1024:
return f"{size:.1f} {unit}"
size /= 1024
return f"{size:.1f} TB"
+386
View File
@@ -0,0 +1,386 @@
"""
JSON 工具箱插件 - JSON 格式化、校验、查询、转换
提供 JSON 美化、压缩、验证、路径查询、类型转换等功能
"""
import json
import os
import re
from typing import List
class Liugin:
"""JSON 工具箱 - 格式化/校验/查询/转换"""
def __init__(self):
self.usage = """JSON 工具箱
操作:
format <文件或JSON> - 美化 JSON(自动缩进)
minify <文件或JSON> - 压缩 JSON(去除空白)
validate <文件或JSON> - 校验 JSON 是否合法
query <文件或JSON> <路径> - 按路径查询值(如 users[0].name)
keys <文件或JSON> - 列出所有键(扁平化)
diff <文件1> <文件2> - 比较两个 JSON 的差异
to_csv <文件或JSON> - 将 JSON 数组转为 CSV
merge <文件1> <文件2> - 合并两个 JSON 对象
sample <文件或JSON> [-n 数量] - 从数组中随机采样
schema <文件或JSON> - 推断 JSON Schema
示例:
json_toolkit format data.json
json_toolkit validate '{"name": "test"}'
json_toolkit query config.json "database.host"
json_toolkit keys data.json
json_toolkit diff a.json b.json
"""
self.cli = None
def set_cli(self, cli):
self.cli = cli
def get_tool_info(self):
return {
"name": "json_toolkit",
"description": "JSON 工具箱 - 格式化、压缩、校验、路径查询、键提取、差异比较、转换",
"keywords": ["json", "格式化", "校验", "查询", "格式", "转换", "美化", "压缩", "validate", "format"],
"usage": self.usage
}
def get_mcp_definition(self):
return {
"name": "json_toolkit",
"description": "JSON 工具箱,支持格式化、压缩、校验、路径查询、键提取、差异比较",
"inputSchema": {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["format", "minify", "validate", "query", "keys",
"diff", "to_csv", "merge", "sample", "schema"],
"description": "操作类型"
},
"input": {"type": "string", "description": "JSON 字符串或文件路径"},
"path": {"type": "string", "description": "查询路径(query 操作用)"},
"input2": {"type": "string", "description": "第二个 JSON 输入(diff/merge 用)"},
"count": {"type": "integer", "description": "采样数量(sample 操作用)"}
},
"required": ["operation", "input"]
}
}
def convert_mcp_args(self, arguments):
op = arguments.get("operation", "")
inp = arguments.get("input", "")
path = arguments.get("path", "")
parts = [op, inp]
if path:
parts.append(path)
return " ".join(parts)
def handle(self, args: str) -> str:
try:
parts = self._parse_args(args)
if not parts:
return "错误:请提供操作类型"
operation = parts[0].lower()
handlers = {
"format": self._op_format,
"minify": self._op_minify,
"validate": self._op_validate,
"query": self._op_query,
"keys": self._op_keys,
"diff": self._op_diff,
"to_csv": self._op_to_csv,
"merge": self._op_merge,
"sample": self._op_sample,
"schema": self._op_schema,
}
handler = handlers.get(operation)
if not handler:
return f"错误:不支持的操作 '{operation}'\n支持: {', '.join(handlers.keys())}"
return handler(parts[1:])
except Exception as e:
return f"JSON 工具错误: {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 _load_json(self, text: str):
"""加载 JSON(支持文件路径或 JSON 字符串)"""
if os.path.isfile(text):
with open(text, 'r', encoding='utf-8') as f:
return json.load(f), f"文件: {text}"
try:
return json.loads(text), "JSON 字符串"
except json.JSONDecodeError as e:
raise ValueError(f"无效的 JSON: {e}")
def _op_format(self, args: List[str]) -> str:
if not args:
return "错误:请提供 JSON 文件或字符串"
data, source = self._load_json(args[0])
formatted = json.dumps(data, indent=2, ensure_ascii=False)
return f"✅ 格式化结果 ({source}):\n{formatted}"
def _op_minify(self, args: List[str]) -> str:
if not args:
return "错误:请提供 JSON 文件或字符串"
data, source = self._load_json(args[0])
minified = json.dumps(data, separators=(',', ':'), ensure_ascii=False)
original_len = len(json.dumps(data, indent=2, ensure_ascii=False))
saved = original_len - len(minified)
return f"✅ 压缩结果 ({source}):\n{minified}\n\n📊 大小: {len(minified)} 字符 (节省 {saved} 字符, {saved/original_len*100:.1f}%)"
def _op_validate(self, args: List[str]) -> str:
if not args:
return "错误:请提供 JSON 文件或字符串"
text = args[0]
try:
if os.path.isfile(text):
with open(text, 'r', encoding='utf-8') as f:
data = json.load(f)
source = f"文件: {text}"
else:
data = json.loads(text)
source = "JSON 字符串"
depth = self._get_depth(data)
keys_count = self._count_keys(data)
result = f"✅ JSON 合法 ({source})\n"
result += f" 类型: {type(data).__name__}\n"
result += f" 嵌套深度: {depth}\n"
result += f" 键总数: {keys_count}"
return result
except json.JSONDecodeError as e:
return f"❌ JSON 无效:\n 行 {e.lineno}, 列 {e.colno}: {e.msg}"
except Exception as e:
return f"❌ 验证失败: {e}"
def _op_query(self, args: List[str]) -> str:
if len(args) < 2:
return "错误:格式: query <JSON或文件> <路径>\n示例: query data.json users[0].name"
data, _ = self._load_json(args[0])
path = args[1]
result = data
parts = re.findall(r'\w+|\[\d+\]', path)
current_path = ""
for part in parts:
if part.startswith('['):
idx = int(part[1:-1])
current_path += part
if isinstance(result, list) and 0 <= idx < len(result):
result = result[idx]
else:
return f"❌ 路径 '{current_path}' 无效: 索引越界或非数组"
else:
current_path += f".{part}" if current_path else part
if isinstance(result, dict) and part in result:
result = result[part]
else:
return f"❌ 路径 '{current_path}' 无效: 键不存在"
output = json.dumps(result, indent=2, ensure_ascii=False) if isinstance(result, (dict, list)) else str(result)
return f"🔍 查询结果 ({path}):\n{output}"
def _op_keys(self, args: List[str]) -> str:
if not args:
return "错误:请提供 JSON 文件或字符串"
data, _ = self._load_json(args[0])
keys = []
self._flatten_keys(data, "", keys)
result = f"🔑 键列表 (共 {len(keys)} 个):\n"
for k in keys:
result += f" {k}\n"
return result.rstrip()
def _flatten_keys(self, obj, prefix, keys):
if isinstance(obj, dict):
for k, v in obj.items():
full_key = f"{prefix}.{k}" if prefix else k
keys.append(full_key)
self._flatten_keys(v, full_key, keys)
elif isinstance(obj, list) and obj:
self._flatten_keys(obj[0], f"{prefix}[0]", keys)
def _op_diff(self, args: List[str]) -> str:
if len(args) < 2:
return "错误:请提供两个 JSON 文件或字符串"
data1, src1 = self._load_json(args[0])
data2, src2 = self._load_json(args[1])
diffs = []
self._compare(data1, data2, "", diffs)
if not diffs:
return "✅ 两个 JSON 完全相同"
result = f"🔀 差异比较 ({len(diffs)} 处不同):\n"
for d in diffs[:50]:
result += f" {d}\n"
if len(diffs) > 50:
result += f" ... 还有 {len(diffs) - 50} 处差异"
return result.rstrip()
def _compare(self, a, b, path, diffs):
if type(a) != type(b):
diffs.append(f" 类型不同 @ {path or '根'}: {type(a).__name__} vs {type(b).__name__}")
return
if isinstance(a, dict):
all_keys = set(a.keys()) | set(b.keys())
for k in sorted(all_keys):
p = f"{path}.{k}" if path else k
if k not in a:
diffs.append(f"➕ 新增 @ {p}: {b[k]}")
elif k not in b:
diffs.append(f"➖ 删除 @ {p}: {a[k]}")
else:
self._compare(a[k], b[k], p, diffs)
elif isinstance(a, list):
if len(a) != len(b):
diffs.append(f" 数组长度不同 @ {path}: {len(a)} vs {len(b)}")
for i in range(min(len(a), len(b))):
self._compare(a[i], b[i], f"{path}[{i}]", diffs)
else:
if a != b:
diffs.append(f" 值不同 @ {path}: {a!r} → {b!r}")
def _op_to_csv(self, args: List[str]) -> str:
if not args:
return "错误:请提供 JSON 文件或字符串"
data, _ = self._load_json(args[0])
if not isinstance(data, list) or not data:
return "错误:需要非空 JSON 数组"
if not isinstance(data[0], dict):
return "错误:数组元素必须是对象"
headers = list(data[0].keys())
lines = [",".join(headers)]
for item in data:
row = []
for h in headers:
val = str(item.get(h, ""))
if "," in val or '"' in val or "\n" in val:
val = '"' + val.replace('"', '""') + '"'
row.append(val)
lines.append(",".join(row))
csv_content = "\n".join(lines)
return f"📊 CSV 转换结果 ({len(data)} 行):\n{csv_content}"
def _op_merge(self, args: List[str]) -> str:
if len(args) < 2:
return "错误:请提供两个 JSON 文件或字符串"
data1, _ = self._load_json(args[0])
data2, _ = self._load_json(args[1])
if not isinstance(data1, dict) or not isinstance(data2, dict):
return "错误:合并仅支持 JSON 对象"
merged = self._deep_merge(data1, data2)
return f"✅ 合并结果:\n{json.dumps(merged, indent=2, ensure_ascii=False)}"
def _deep_merge(self, a, b):
result = a.copy()
for k, v in b.items():
if k in result and isinstance(result[k], dict) and isinstance(v, dict):
result[k] = self._deep_merge(result[k], v)
else:
result[k] = v
return result
def _op_sample(self, args: List[str]) -> str:
if not args:
return "错误:请提供 JSON 文件或字符串"
data, _ = self._load_json(args[0])
count = 3
for i, a in enumerate(args):
if a == "-n" and i + 1 < len(args):
count = int(args[i + 1])
if not isinstance(data, list):
return "错误:需要 JSON 数组"
import random
sampled = random.sample(data, min(count, len(data)))
return f"🎲 随机采样 ({len(sampled)}/{len(data)}):\n{json.dumps(sampled, indent=2, ensure_ascii=False)}"
def _op_schema(self, args: List[str]) -> str:
if not args:
return "错误:请提供 JSON 文件或字符串"
data, _ = self._load_json(args[0])
schema = self._infer_schema(data)
return f"📋 推断 Schema:\n{json.dumps(schema, indent=2, ensure_ascii=False)}"
def _infer_schema(self, obj, depth=0):
if depth > 10:
return {"type": "any"}
if isinstance(obj, dict):
props = {}
for k, v in obj.items():
props[k] = self._infer_schema(v, depth + 1)
return {"type": "object", "properties": props}
elif isinstance(obj, list):
if obj:
return {"type": "array", "items": self._infer_schema(obj[0], depth + 1)}
return {"type": "array", "items": {}}
elif isinstance(obj, bool):
return {"type": "boolean"}
elif isinstance(obj, int):
return {"type": "integer"}
elif isinstance(obj, float):
return {"type": "number"}
else:
return {"type": "string"}
def _get_depth(self, obj, depth=0):
if depth > 50:
return depth
if isinstance(obj, dict):
return max((self._get_depth(v, depth + 1) for v in obj.values()), default=depth)
elif isinstance(obj, list):
return max((self._get_depth(v, depth + 1) for v in obj), default=depth)
return depth
def _count_keys(self, obj):
if isinstance(obj, dict):
return len(obj) + sum(self._count_keys(v) for v in obj.values())
elif isinstance(obj, list):
return sum(self._count_keys(v) for v in obj)
return 0
def test_plugin():
plugin = Liugin()
print("JSON 工具箱测试:")
print(plugin.handle('validate \'{"name": "test", "age": 25}\''))
print(plugin.handle('format \'{"name":"test","items":[1,2,3]}\''))
print(plugin.handle('keys \'{"database": {"host": "localhost", "port": 3306}}\''))
print(plugin.handle('query \'{"users": [{"name": "Alice"}]}\' users[0].name'))
if __name__ == "__main__":
test_plugin()
+540
View File
@@ -0,0 +1,540 @@
"""
Markdown 工具插件 - Markdown 渲染、目录生成、格式转换
支持 TOC 生成、标题提取、表格生成、格式化、统计
"""
import re
import json
import os
from typing import List, Tuple
class Liugin:
"""Markdown 工具 - TOC/渲染/格式转换/统计"""
def __init__(self):
self.usage = """Markdown 工具
操作:
toc <文件或Markdown> - 生成目录 (Table of Contents)
headings <文件或Markdown> - 提取所有标题
stats <文件或Markdown> - 统计信息(字数/行数/段落数等)
table <数据> - 从 JSON 数组生成 Markdown 表格
checklist <文件> - 提取所有待办事项
links <文件或Markdown> - 提取所有链接
images <文件或Markdown> - 提取所有图片
frontmatter <文件> - 提取 YAML frontmatter
format <文件或Markdown> - 格式化(标题层级、列表缩进等)
wordcount <文件或Markdown> - 详细字数统计
outline <文件或Markdown> - 生成大纲视图
lint <文件> - Markdown 语法检查
示例:
markdown_tool toc README.md
markdown_tool stats article.md
markdown_tool table '[{"name":"Alice","age":25},{"name":"Bob","age":30}]'
markdown_tool checklist TODO.md
markdown_tool links README.md
markdown_tool lint README.md
"""
self.cli = None
def set_cli(self, cli):
self.cli = cli
def get_tool_info(self):
return {
"name": "markdown_tool",
"description": "Markdown 工具 - TOC 生成、标题提取、统计、表格生成、语法检查",
"keywords": ["markdown", "md", "目录", "toc", "标题", "表格", "统计", "链接", "检查"],
"usage": self.usage
}
def get_mcp_definition(self):
return {
"name": "markdown_tool",
"description": "Markdown 工具,支持 TOC、统计、表格生成、链接提取、语法检查",
"inputSchema": {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["toc", "headings", "stats", "table", "checklist",
"links", "images", "frontmatter", "format",
"wordcount", "outline", "lint"],
"description": "操作类型"
},
"input": {"type": "string", "description": "Markdown 文件路径或内容"},
"data": {"type": "string", "description": "JSON 数据(table 操作用)"}
},
"required": ["operation", "input"]
}
}
def convert_mcp_args(self, arguments):
op = arguments.get("operation", "")
inp = arguments.get("input", "")
return f"{op} {inp}".strip()
def _load_content(self, text: str) -> Tuple[str, str]:
"""加载内容(文件路径或文本)"""
if os.path.isfile(text):
with open(text, 'r', encoding='utf-8') as f:
return f.read(), f"文件: {text}"
return text, "文本输入"
def handle(self, args: str) -> str:
try:
parts = self._parse_args(args)
if not parts:
return "错误:请提供操作类型"
operation = parts[0].lower()
handlers = {
"toc": self._op_toc,
"headings": self._op_headings,
"stats": self._op_stats,
"table": self._op_table,
"checklist": self._op_checklist,
"links": self._op_links,
"images": self._op_images,
"frontmatter": self._op_frontmatter,
"format": self._op_format,
"wordcount": self._op_wordcount,
"outline": self._op_outline,
"lint": self._op_lint,
}
handler = handlers.get(operation)
if not handler:
return f"错误:不支持的操作 '{operation}'"
return handler(parts[1:])
except Exception as e:
return f"Markdown 工具错误: {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 _extract_headings(self, content: str) -> List[Tuple[int, str, str]]:
"""提取标题: [(level, text, slug)]"""
headings = []
in_code = False
for line in content.split('\n'):
if line.strip().startswith('```'):
in_code = not in_code
continue
if in_code:
continue
match = re.match(r'^(#{1,6})\s+(.+)', line)
if match:
level = len(match.group(1))
text = match.group(2).strip()
# 生成 slug
slug = re.sub(r'[^\w\s-]', '', text.lower())
slug = re.sub(r'\s+', '-', slug.strip())
headings.append((level, text, slug))
return headings
def _op_toc(self, args: List[str]) -> str:
if not args:
return "错误:请提供 Markdown 文件或内容"
content, source = self._load_content(args[0])
headings = self._extract_headings(content)
if not headings:
return "📭 未找到标题"
result = f" 目录 ({source}):\n"
result += "─" * 40 + "\n"
for level, text, slug in headings:
indent = " " * (level - 1)
result += f"{indent}- [{text}](#{slug})\n"
return result.rstrip()
def _op_headings(self, args: List[str]) -> str:
if not args:
return "错误:请提供 Markdown 文件或内容"
content, source = self._load_content(args[0])
headings = self._extract_headings(content)
if not headings:
return "📭 未找到标题"
result = f" 标题列表 ({len(headings)} 个):\n"
for level, text, _ in headings:
prefix = "#" * level
result += f" {prefix:8s} {text}\n"
return result.rstrip()
def _op_stats(self, args: List[str]) -> str:
if not args:
return "错误:请提供 Markdown 文件或内容"
content, source = self._load_content(args[0])
lines = content.split('\n')
total_lines = len(lines)
blank_lines = sum(1 for l in lines if not l.strip())
code_blocks = content.count('```') // 2
headings = len(self._extract_headings(content))
links = len(re.findall(r'\[([^\]]+)\]\([^)]+\)', content))
images = len(re.findall(r'!\[([^\]]*)\]\([^)]+\)', content))
# 中文字符
chinese = len(re.findall(r'[\u4e00-\u9fff]', content))
# 英文单词
english = len(re.findall(r'[a-zA-Z]+', content))
# 段落数(连续非空行)
paragraphs = 0
in_para = False
for l in lines:
if l.strip():
if not in_para:
paragraphs += 1
in_para = True
else:
in_para = False
result = f"📊 Markdown 统计 ({source})\n"
result += "─" * 35 + "\n"
result += f" 总行数: {total_lines}\n"
result += f" 空行: {blank_lines}\n"
result += f" 段落: {paragraphs}\n"
result += f" 代码块: {code_blocks}\n"
result += f" 标题: {headings}\n"
result += f" 链接: {links}\n"
result += f" 图片: {images}\n"
result += f" 中文字: {chinese}\n"
result += f" 英文词: {english}\n"
result += f" 字符数: {len(content)}\n"
return result.rstrip()
def _op_table(self, args: List[str]) -> str:
if not args:
return "错误:请提供 JSON 数组"
data_str = args[0]
try:
data = json.loads(data_str)
except json.JSONDecodeError as e:
return f"❌ JSON 解析错误: {e}"
if not isinstance(data, list) or not data:
return "❌ 需要非空 JSON 数组"
if not isinstance(data[0], dict):
return "❌ 数组元素必须是对象"
headers = list(data[0].keys())
# 计算列宽
widths = {h: len(h) for h in headers}
for row in data:
for h in headers:
widths[h] = max(widths[h], len(str(row.get(h, ""))))
# 生成表格
header_line = "| " + " | ".join(h.ljust(widths[h]) for h in headers) + " |"
sep_line = "| " + " | ".join("-" * widths[h] for h in headers) + " |"
rows = []
for row in data:
rows.append("| " + " | ".join(str(row.get(h, "")).ljust(widths[h]) for h in headers) + " |")
result = f" Markdown 表格 ({len(data)} 行):\n\n"
result += header_line + "\n" + sep_line + "\n" + "\n".join(rows)
return result
def _op_checklist(self, args: List[str]) -> str:
if not args:
return "错误:请提供 Markdown 文件"
content, source = self._load_content(args[0])
todos = []
done = []
for i, line in enumerate(content.split('\n'), 1):
match = re.match(r'^(\s*)[-*]\s*\[([ xX])\]\s*(.+)', line)
if match:
indent = match.group(1)
checked = match.group(2).lower() == 'x'
text = match.group(3).strip()
if checked:
done.append((i, text))
else:
todos.append((i, text))
result = f" 待办事项 ({source}):\n"
result += "─" * 45 + "\n"
result += f" ☑ 已完成: {len(done)} 个\n"
result += f" 未完成: {len(todos)} 个\n"
result += "─" * 45 + "\n"
if todos:
result += "\n 未完成:\n"
for line_no, text in todos[:30]:
result += f" L{line_no:4d} {text}\n"
if done:
result += "\n 已完成:\n"
for line_no, text in done[:20]:
result += f" L{line_no:4d} {text}\n"
return result.rstrip()
def _op_links(self, args: List[str]) -> str:
if not args:
return "错误:请提供 Markdown 文件或内容"
content, source = self._load_content(args[0])
links = re.findall(r'\[([^\]]+)\]\(([^)]+)\)', content)
if not links:
return f"📭 未找到链接 ({source})"
result = f" 链接列表 ({len(links)} 个, {source}):\n"
result += "─" * 50 + "\n"
seen = set()
for text, url in links:
if url not in seen:
seen.add(url)
result += f" {text[:30]:30s} → {url}\n"
return result.rstrip()
def _op_images(self, args: List[str]) -> str:
if not args:
return "错误:请提供 Markdown 文件或内容"
content, source = self._load_content(args[0])
images = re.findall(r'!\[([^\]]*)\]\(([^)]+)\)', content)
if not images:
return f"📭 未找到图片 ({source})"
result = f" ️ 图片列表 ({len(images)} 个, {source}):\n"
result += "─" * 50 + "\n"
seen = set()
for alt, url in images:
if url not in seen:
seen.add(url)
result += f" {alt[:25]:25s} → {url}\n"
return result.rstrip()
def _op_frontmatter(self, args: List[str]) -> str:
if not args:
return "错误:请提供 Markdown 文件"
content, source = self._load_content(args[0])
match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
if not match:
return f"📭 未找到 frontmatter ({source})"
fm = match.group(1)
result = f" Frontmatter ({source}):\n"
result += "─" * 35 + "\n"
result += fm
return result
def _op_format(self, args: List[str]) -> str:
if not args:
return "错误:请提供 Markdown 文件或内容"
content, source = self._load_content(args[0])
lines = content.split('\n')
formatted = []
prev_blank = False
for line in lines:
stripped = line.strip()
# 确保标题前后有空行
if re.match(r'^#{1,6}\s', stripped):
if formatted and not prev_blank:
formatted.append('')
formatted.append(stripped)
prev_blank = False
continue
# 确保列表项格式一致
if re.match(r'^[-*]\s', stripped):
formatted.append(re.sub(r'^[-*]\s', '- ', stripped))
prev_blank = False
continue
# 去除多余空行
if not stripped:
if not prev_blank:
formatted.append('')
prev_blank = True
continue
prev_blank = False
formatted.append(line.rstrip())
# 确保文件末尾有换行
while formatted and not formatted[-1]:
formatted.pop()
formatted.append('')
return f"✅ 格式化完成 ({source}):\n" + '\n'.join(formatted)
def _op_wordcount(self, args: List[str]) -> str:
if not args:
return "错误:请提供 Markdown 文件或内容"
content, source = self._load_content(args[0])
# 移除代码块
no_code = re.sub(r'```[\s\S]*?```', '', content)
# 移除行内代码
no_code = re.sub(r'`[^`]+`', '', no_code)
# 移除链接标记
no_code = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', no_code)
# 移除图片标记
no_code = re.sub(r'!\[([^\]]*)\]\([^)]+\)', '', no_code)
# 移除标题标记
no_code = re.sub(r'^#{1,6}\s+', '', no_code, flags=re.MULTILINE)
# 移除加粗/斜体标记
no_code = re.sub(r'\*{1,2}([^*]+)\*{1,2}', r'\1', no_code)
chinese = len(re.findall(r'[\u4e00-\u9fff]', no_code))
english_words = re.findall(r'[a-zA-Z]+', no_code)
english = len(english_words)
total_chars = len(no_code.strip())
# 阅读时间估算(中文 300字/分钟, 英文 200词/分钟)
read_time_cn = chinese / 300
read_time_en = english / 200
read_time = read_time_cn + read_time_en
result = f" 详细字数统计 ({source}):\n"
result += "─" * 35 + "\n"
result += f" 中文字数: {chinese}\n"
result += f" 英文单词: {english}\n"
result += f" 总字符数: {total_chars}\n"
result += f" 预计阅读: {read_time:.1f} 分钟\n"
return result.rstrip()
def _op_outline(self, args: List[str]) -> str:
if not args:
return "错误:请提供 Markdown 文件或内容"
content, source = self._load_content(args[0])
headings = self._extract_headings(content)
if not headings:
return "📭 未找到标题"
result = f" 大纲视图 ({source}):\n"
result += "═" * 40 + "\n"
for level, text, _ in headings:
if level == 1:
result += f"\n {text}\n"
result += "─" * 30 + "\n"
elif level == 2:
result += f" {text}\n"
elif level == 3:
result += f" {text}\n"
else:
result += f" {'·' * (level - 3)} {text}\n"
return result.rstrip()
def _op_lint(self, args: List[str]) -> str:
if not args:
return "错误:请提供 Markdown 文件"
file_path = args[0]
if not os.path.isfile(file_path):
return f"❌ 文件不存在: {file_path}"
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
lines = content.split('\n')
issues = []
in_code = False
prev_blank = False
heading_levels = []
for i, line in enumerate(lines, 1):
stripped = line.strip()
# 代码块跟踪
if stripped.startswith('```'):
in_code = not in_code
continue
if in_code:
continue
# 检查标题格式
heading_match = re.match(r'^(#{1,6})\s+(.+)', line)
if heading_match:
level = len(heading_match.group(1))
text = heading_match.group(2)
# 检查标题前后空行
if i > 1 and lines[i - 2].strip() and not lines[i - 2].startswith('#'):
issues.append(f" L{i:4d} 标题前缺少空行")
# 检查标题层级跳跃
if heading_levels and level > heading_levels[-1] + 1:
issues.append(f" L{i:4d} 标题层级跳跃 ({heading_levels[-1]} → {level})")
heading_levels.append(level)
# 检查标题末尾空格
if text != text.rstrip():
issues.append(f" L{i:4d} 标题末尾有多余空格")
else:
# 检查行尾空格
if line != line.rstrip() and stripped:
trailing = len(line) - len(line.rstrip())
if trailing >= 2: # 2个以上空格是换行符
pass # 这是合法的 Markdown 换行
elif trailing > 0:
issues.append(f" L{i:4d} 行尾有多余空格")
# 检查连续空行
if not stripped:
if prev_blank:
issues.append(f" L{i:4d} 连续空行")
prev_blank = True
else:
prev_blank = False
# 检查列表格式
list_match = re.match(r'^(\s*)[-*+]\s', line)
if list_match:
indent = len(list_match.group(1))
if indent % 2 != 0 and indent > 0:
issues.append(f" L{i:4d} 列表缩进不是2的倍数")
# 检查链接格式
if re.search(r'\[[^\]]*\]\(\s*\)', line):
issues.append(f" L{i:4d} 链接URL为空")
# 检查重复词
words = stripped.lower().split()
for j in range(len(words) - 1):
if len(words[j]) > 2 and words[j] == words[j + 1]:
issues.append(f" L{i:4d} 重复词: '{words[j]}'")
result = f" ️ Markdown 语法检查 ({file_path}):\n"
result += "─" * 45 + "\n"
if not issues:
result += " 未发现问题!\n"
else:
result += f" 发现 {len(issues)} 个问题:\n\n"
for issue in issues[:50]:
result += f" {issue}\n"
if len(issues) > 50:
result += f"\n ... 还有 {len(issues) - 50} 个问题\n"
return result.rstrip()
def test_plugin():
plugin = Liugin()
print("Markdown 工具测试:")
print(plugin.handle('headings "# Hello\\n## World\\n### Sub"'))
print(plugin.handle('stats "# Hello\\n\\nThis is a test."'))
print(plugin.handle('table \'[{"name":"Alice","age":25},{"name":"Bob","age":30}]\''))
if __name__ == "__main__":
test_plugin()
+253
View File
@@ -0,0 +1,253 @@
"""
记忆系统插件 — 包装 xcli_core/memory.py 中的 MemoryManager
使 AI 可以通过工具接口读写日记、搜索记忆、管理聊天记录、操作永久性记忆
"""
import os
import sys
# 添加项目根目录到 path
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(current_dir)
sys.path.insert(0, project_root)
from xcli_core.memory import MemoryManager
class Liugin:
"""持久化记忆系统 — AI 可读写永久记忆、日记、搜索历史对话"""
def __init__(self):
self.cli = None
self.memory = None
self.usage = """持久化记忆系统工具
操作:
diary <内容> - 写当日日记
diary_ai <内容> - AI 写观察日记(标记为 AI 观察)
remember <内容> - 记录到长期记忆 (MEMORY.md 备忘 section)
search <关键词> - 全局搜索(日记+记忆+聊天记录)
context - 查看当前注入的记忆上下文
diary_list - 列出所有日记文件
diary_read <日期> - 读取指定日期的日记(如 2026-04-27)
永久性记忆 (MEMORY.md) 操作:
memory_read - 读取整个 MEMORY.md
memory_write <内容> - 追加到 MEMORY.md 备忘 section
memory_sections - 列出所有记忆分类(section)
memory_read_section <名称> - 读取指定 section 的内容
memory_write_section <名称> <内容> - 写入/覆盖指定 section(不存在则新建)
memory_delete_section <名称> - 删除指定 section
聊天记录:
chat_list - 列出最近的聊天记录
chat_search <关键词> - 在聊天记录中搜索
JSON 格式示例:
{"action": "use_tool", "tool": "memory_plugin", "args": "remember 用户偏好深色主题"}
{"action": "use_tool", "tool": "memory_plugin", "args": "memory_write_section 用户偏好 - 喜欢简洁的代码风格\n- 使用 Python 3.10+"}
{"action": "use_tool", "tool": "memory_plugin", "args": "memory_read_section 用户偏好"}
{"action": "use_tool", "tool": "memory_plugin", "args": "search 定时任务"}
"""
def set_cli(self, cli):
self.cli = cli
project_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
self.memory = MemoryManager(project_dir)
cli.memory_manager = self.memory
def get_tool_info(self):
return {
"name": "memory_plugin",
"description": "持久化记忆系统 — AI 可读写永久记忆(MEMORY.md)、写日记、搜索历史对话。支持按 section 分类存储和修改记忆。",
"keywords": ["记忆", "memory", "日记", "diary", "搜索", "search", "历史", "history",
"remember", "聊天记录", "chat", "上下文", "context", "压缩", "compress",
"永久", "persistent", "存储", "store", "读取", "read", "修改", "modify"],
"usage": self.usage
}
def handle(self, args: str) -> str:
if not self.memory:
return "错误:记忆系统未初始化"
if not args or not args.strip():
return self._help()
parts = args.strip().split(None, 1)
action = parts[0].lower()
rest = parts[1].strip() if len(parts) > 1 else ""
handlers = {
"diary": self._diary,
"diary_ai": self._diary_ai,
"remember": self._remember,
"search": self._search,
"context": self._context,
"diary_list": self._diary_list,
"diary_read": self._diary_read,
"chat_list": self._chat_list,
"chat_search": self._chat_search,
"memory_read": self._memory_read,
"memory_write": self._memory_write,
"memory_sections": self._memory_sections,
"memory_read_section": self._memory_read_section,
"memory_write_section": self._memory_write_section,
"memory_delete_section": self._memory_delete_section,
"help": lambda _: self._help(),
}
handler = handlers.get(action)
if not handler:
return f"未知操作: {action}\n" + self._help()
return handler(rest)
# ── 日记 ──
def _diary(self, content: str) -> str:
if not content:
return "错误:请提供日记内容\n用法: diary <内容>"
self.memory.write_diary(content)
return f"日记已写入 {self.memory._today_file()}"
def _diary_ai(self, content: str) -> str:
if not content:
return "错误:请提供 AI 观察内容\n用法: diary_ai <内容>"
self.memory.write_ai_diary(content)
return "AI 观察日记已写入"
def _diary_list(self, _: str) -> str:
files = self.memory.list_diary_files()
if not files:
return "还没有日记文件"
return "日记文件:\n" + "\n".join(f" {f}" for f in files[:20])
def _diary_read(self, date_str: str) -> str:
if not date_str:
return "错误:请提供日期\n用法: diary_read <日期,如 2026-04-27>"
content = self.memory.load_diary(date_str)
return content if content else f"没有找到 {date_str} 的日记"
# ── 永久性记忆 (MEMORY.md) ──
def _remember(self, content: str) -> str:
"""快速记录到备忘 section"""
if not content:
return "错误:请提供要记住的内容\n用法: remember <内容>"
self.memory.append_memory("备忘", f"- {content}")
return "已记录到 MEMORY.md 长期记忆 (备忘 section)"
def _memory_read(self, _: str) -> str:
content = self.memory.load_memory()
return content if content else "MEMORY.md 为空"
def _memory_write(self, content: str) -> str:
"""追加到备忘 section"""
if not content:
return "错误:请提供内容\n用法: memory_write <内容>"
self.memory.append_memory("备忘", f"- {content}")
return "已追加到 MEMORY.md (备忘 section)"
def _memory_sections(self, _: str) -> str:
"""列出所有 section"""
sections = self.memory.list_memory_sections()
if not sections:
return "MEMORY.md 为空,没有任何 section"
return "MEMORY.md 中的 section:\n" + "\n".join(f" {i+1}. {s}" for i, s in enumerate(sections))
def _memory_read_section(self, section: str) -> str:
"""读取指定 section"""
if not section:
return "错误:请提供 section 名称\n用法: memory_read_section <名称>"
content = self.memory.read_memory_section(section)
if not content:
return f"section '{section}' 不存在或为空"
return f"## {section}\n{content}"
def _memory_write_section(self, args: str) -> str:
"""写入/覆盖指定 section"""
if not args:
return "错误:请提供 section 名称和内容\n用法: memory_write_section <名称> <内容>"
parts = args.split(None, 1)
section = parts[0]
content = parts[1] if len(parts) > 1 else ""
if not content:
return "错误:请提供内容\n用法: memory_write_section <名称> <内容>"
self.memory.write_memory_section(section, content)
return f"已写入 MEMORY.md section '{section}'"
def _memory_delete_section(self, section: str) -> str:
"""删除指定 section"""
if not section:
return "错误:请提供 section 名称\n用法: memory_delete_section <名称>"
if self.memory.delete_memory_section(section):
return f"已删除 MEMORY.md section '{section}'"
return f"section '{section}' 不存在"
# ── 搜索 ──
def _search(self, keyword: str) -> str:
if not keyword:
return "错误:请提供搜索关键词\n用法: search <关键词>"
results = self.memory.search_memory(keyword)
if not results:
return f"未找到与 '{keyword}' 相关的记忆"
lines = [f"搜索 '{keyword}' 的结果 ({len(results)} 条):"]
for r in results:
source = r.get("source", "")
snippet = r.get("snippet", "")
role = r.get("role", "")
line_num = r.get("line", "")
prefix = f"[{role}] " if role else ""
line_info = f" (L{line_num})" if line_num else ""
lines.append(f" {source}{line_info}: {prefix}{snippet}")
return "\n".join(lines)
def _context(self, _: str) -> str:
ctx = self.memory.build_memory_context()
return ctx if ctx else "当前没有记忆上下文"
# ── 聊天记录 ──
def _chat_list(self, _: str) -> str:
chats = self.memory.list_chats()
if not chats:
return "没有保存的聊天记录"
lines = ["最近的聊天记录:"]
for c in chats:
lines.append(f" {c['name']} ({c['time']}, {c['size_kb']}KB)")
return "\n".join(lines)
def _chat_search(self, keyword: str) -> str:
if not keyword:
return "错误:请提供关键词\n用法: chat_search <关键词>"
results = self.memory.search_chats(keyword)
if not results:
return f"未在聊天记录中找到 '{keyword}'"
lines = [f"聊天记录搜索 '{keyword}':"]
for r in results:
name = r.get("name", r.get("file", ""))
lines.append(f" {name} [{r.get('role', '')}]: {r.get('snippet', '')}")
return "\n".join(lines)
def _help(self) -> str:
return """记忆系统用法:
日记:
diary <内容> - 写当日日记
diary_ai <内容> - AI 写观察日记
diary_list - 列出日记
diary_read <日期> - 读取日记
永久性记忆 (MEMORY.md):
remember <内容> - 快速记到备忘
memory_read - 读取整个 MEMORY.md
memory_write <内容> - 追加到备忘
memory_sections - 列出所有 section
memory_read_section <名称> - 读取指定 section
memory_write_section <名称> <内容> - 写入/覆盖 section
memory_delete_section <名称> - 删除 section
搜索:
search <关键词> - 全局搜索
chat_search <关键词> - 搜索聊天记录
context - 查看记忆上下文"""
+295
View File
@@ -0,0 +1,295 @@
import requests
import time
import socket
import ssl
import json
from urllib.parse import urlparse
from colorama import Fore, Style
class Liugin:
"""网络工具插件"""
_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36'
def __init__(self):
self.cli = None
self.timeout = 10
self.max_retries = 2
def set_cli(self, cli):
self.cli = cli
self.cli.register_liugin_command('net', self.command_handler)
self.cli.register_liugin_command('ping', lambda a: self.handle(f"ping {a}"))
self.cli.register_liugin_command('website', lambda a: self.handle(f"status {a}"))
def command_handler(self, args):
parts = args.strip().split(maxsplit=1)
if not parts:
return "用法: network_tools <操作> <参数>"
return self.handle(f"{parts[0].lower()} {parts[1] if len(parts) > 1 else ''}")
def get_tool_info(self):
return {
"name": "network_tools",
"description": "网络工具:ping/get/status/headers/ip/dns/ssl/port/post/put/delete",
"keywords": ["网络", "ping", "get", "post", "dns", "ssl", "port", "ip", "headers", "status"],
"usage": (
"network_tools <操作> <参数>\n\n"
"读取: get/status/headers <URL>\n"
"诊断: ping/dns/ssl <域名>\n"
"扫描: port <域名> [端口范围]\n"
"请求: post/put/delete <URL> [JSON体]\n"
"解析: ip <域名>"
),
}
def get_mcp_definition(self):
return {
"name": "network_tools",
"description": "网络工具:读取网页、诊断连接、发送HTTP请求、DNS/SSL/端口扫描",
"inputSchema": {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["ping", "get", "status", "headers", "ip", "dns", "ssl", "port", "post", "put", "delete"],
},
"target": {"type": "string", "description": "URL、域名或IP"},
"extra": {"type": "string", "description": "附加参数:JSON body 或端口范围"}
},
"required": ["operation", "target"]
}
}
def convert_mcp_args(self, arguments):
op = arguments.get("operation", "")
target = arguments.get("target", "")
extra = arguments.get("extra", "")
return f"{op} {target} {extra}".strip()
def handle(self, args):
try:
parts = args.strip().split(maxsplit=2)
if not parts:
return "参数不足"
op = parts[0].lower()
target = parts[1].strip() if len(parts) > 1 else ""
extra = parts[2].strip() if len(parts) > 2 else ""
if op == "post":
return self._api(target, "POST", extra)
elif op == "put":
return self._api(target, "PUT", extra)
elif op == "delete":
return self._api(target, "DELETE", extra)
elif op == "ping":
return self._ping(target)
elif op == "get":
return self._get(target)
elif op == "status":
return self._status(target)
elif op == "headers":
return self._headers(target)
elif op == "ip":
return self._ip(target)
elif op == "dns":
return self._dns(target)
elif op == "ssl":
return self._ssl(target)
elif op == "port":
return self._port(target, extra)
else:
return f"不支持: {op}。支持: ping/get/status/headers/ip/dns/ssl/port/post/put/delete"
except Exception as e:
return f"错误: {e}"
def _url(self, t):
return t if t.startswith(('http://', 'https://')) else 'https://' + t
def _req(self, url, method='GET', data=None, ct=None):
headers = {'User-Agent': self._UA, 'Accept': '*/*'}
if ct:
headers['Content-Type'] = ct
for i in range(self.max_retries + 1):
try:
r = requests.request(method, url, timeout=self.timeout, headers=headers, allow_redirects=True,
json=data if ct == 'application/json' else None,
data=data if (ct and ct != 'application/json') else None)
if not r.encoding or r.encoding.lower() == 'iso-8859-1':
r.encoding = r.apparent_encoding or 'utf-8'
return r
except requests.exceptions.Timeout:
if i < self.max_retries:
time.sleep(1)
continue
return None
except Exception:
return None
return None
def _ping(self, t):
url = self._url(t)
start = time.time()
r = self._req(url)
ms = (time.time() - start) * 1000
if not r:
return f"Ping {t}: 连接失败"
status = "成功" if r.status_code < 400 else f"HTTP {r.status_code}"
extra = f"\n 重定向: {r.url}" if r.url != url else ""
return f"Ping {t}:\n 状态: {status}\n 延迟: {ms:.0f}ms\n HTTP: {r.status_code}{extra}"
def _get(self, t):
url = self._url(t)
r = self._req(url)
if not r:
return f"GET {t}: 失败"
content = r.text
return f"GET {url} [{r.status_code}]:\n{content}"
def _status(self, t):
url = self._url(t)
r = self._req(url)
if not r:
return f"STATUS {t}: 失败"
return (
f"STATUS {url}:\n"
f" 状态码: {r.status_code} {r.reason}\n"
f" 延迟: {r.elapsed.total_seconds()*1000:.0f}ms\n"
f" 大小: {len(r.content)} bytes\n"
f" 服务器: {r.headers.get('Server', '?')}\n"
f" 类型: {r.headers.get('Content-Type', '?')}\n"
f" 最终URL: {r.url}"
)
def _headers(self, t):
url = self._url(t)
r = self._req(url, method='HEAD')
if not r:
r = self._req(url)
if not r:
return f"HEADERS {t}: 失败"
lines = [f"HEADERS {url}:"]
for k, v in r.headers.items():
lines.append(f" {k}: {v}")
return "\n".join(lines)
def _ip(self, t):
if t.startswith(('http://', 'https://')):
t = urlparse(t).netloc
try:
ips = list(set(addr[4][0] for addr in socket.getaddrinfo(t, None)))
return f"IP {t}:\n" + "\n".join(f" {ip}" for ip in ips)
except socket.gaierror:
return f"IP {t}: 解析失败"
def _dns(self, t):
if t.startswith(('http://', 'https://')):
t = urlparse(t).netloc
lines = [f"DNS {t}:"]
try:
seen = set()
for item in socket.getaddrinfo(t, None):
ip = item[4][0]
fam = "IPv4" if item[0] == socket.AF_INET else "IPv6"
if (ip, fam) not in seen:
seen.add((ip, fam))
lines.append(f" {fam}: {ip}")
except socket.gaierror as e:
lines.append(f" 失败: {e}")
fqdn = socket.getfqdn(t)
if fqdn != t:
lines.append(f" FQDN: {fqdn}")
return "\n".join(lines)
def _ssl(self, t):
if t.startswith(('http://', 'https://')):
t = urlparse(t).netloc
host = t.split(':')[0]
lines = [f"SSL {t}:"]
try:
ctx = ssl.create_default_context()
with ctx.wrap_socket(socket.socket(), server_hostname=host) as s:
s.settimeout(self.timeout)
s.connect((host, 443))
cert = s.getpeercert()
subj = dict(x[0] for x in cert.get('subject', []))
iss = dict(x[0] for x in cert.get('issuer', []))
lines.append(f" 主题: {subj.get('commonName', '?')}")
lines.append(f" 颁发: {iss.get('organizationName', iss.get('commonName', '?'))}")
lines.append(f" 生效: {cert.get('notBefore', '?')}")
lines.append(f" 过期: {cert.get('notAfter', '?')}")
san = cert.get('subjectAltName', [])
if san:
doms = [v for _, v in san[:5]]
lines.append(f" SAN: {', '.join(doms)}{'...' if len(san) > 5 else ''}")
lines.append(f" 状态: 有效")
except ssl.SSLCertVerificationError as e:
lines.append(f" 状态: 验证失败 - {e}")
except socket.timeout:
lines.append(f" 状态: 超时")
except ConnectionRefusedError:
lines.append(f" 状态: 连接被拒绝")
except Exception as e:
lines.append(f" 状态: {e}")
return "\n".join(lines)
def _port(self, t, rng=''):
if t.startswith(('http://', 'https://')):
t = urlparse(t).netloc.split(':')[0]
if '-' in rng:
try:
s, e = rng.split('-', 1)
s, e = max(1, int(s)), min(65535, int(e))
except ValueError:
s, e = 1, 1024
else:
s, e = 1, 1024
try:
ip = socket.gethostbyname(t)
except socket.gaierror:
return f"PORT {t}: 解析失败"
common = {21:'FTP',22:'SSH',23:'Telnet',25:'SMTP',53:'DNS',80:'HTTP',110:'POP3',143:'IMAP',443:'HTTPS',993:'IMAPS',995:'POP3S',3306:'MySQL',3389:'RDP',5432:'PostgreSQL',6379:'Redis',8080:'HTTP-Alt',8443:'HTTPS-Alt',27017:'MongoDB'}
open_p = []
for p in range(s, e + 1):
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(0.3)
if sock.connect_ex((ip, p)) == 0:
open_p.append((p, common.get(p, '?')))
sock.close()
except Exception:
pass
lines = [f"PORT {t} ({s}-{e}), IP={ip}:"]
if open_p:
for p, svc in sorted(open_p):
lines.append(f" {p}/{svc}")
else:
lines.append(f" 无开放端口")
return "\n".join(lines)
def _api(self, url, method, body_str):
url = self._url(url)
data = None
ct = None
if body_str:
try:
data = json.loads(body_str)
ct = 'application/json'
except json.JSONDecodeError:
data = body_str
ct = 'text/plain'
r = self._req(url, method=method, data=data, ct=ct)
if not r:
return f"{method} {url}: 失败"
lines = [f"{method} {url} [{r.status_code}]"]
ctype = r.headers.get('Content-Type', '')
if 'json' in ctype:
try:
body = json.dumps(r.json(), ensure_ascii=False, indent=2)
lines.append(body)
except Exception:
lines.append(r.text)
else:
lines.append(r.text)
return "\n".join(lines)
+442
View File
@@ -0,0 +1,442 @@
"""
正则表达式测试器插件 - 测试、调试、管理正则表达式
支持匹配测试、提取、替换、常用模式库
"""
import re
import json
import os
from typing import List
class Liugin:
"""正则表达式测试器 - 测试、调试、管理正则"""
def __init__(self):
self.usage = """正则表达式测试器
操作:
test <正则> <文本> [-f flags] - 测试正则是否匹配
match <正则> <文本> [-f flags] - 提取所有匹配项
replace <正则> <文本> <替换> - 测试替换
groups <正则> <文本> - 显示分组信息
explain <正则> - 解释正则含义
flags <正则> <文本> - 测试不同标志组合
pattern [名称] - 列出/查看常用模式
save <名称> <正则> [-d 描述] - 保存自定义模式
delete_pattern <名称> - 删除自定义模式
bench <正则> <文本> [-n 次数] - 性能测试
标志 (flags):
i - 忽略大小写 m - 多行模式 s - 点匹配换行
x - 详细模式 a - ASCII模式
示例:
regex_tester test "\\d+" "abc123def456"
regex_tester match "[a-z]+@[a-z]+\\.[a-z]+" "test@example.com foo@bar.org"
regex_tester groups "(\\w+)@(\\w+)\\.(\\w+)" "user@domain.com"
regex_tester explain "[a-z]+@\\w+\\.\\w+"
regex_tester pattern email
"""
self.cli = None
self.patterns_file = "regex_patterns.json"
self.custom_patterns = self._load_patterns()
def set_cli(self, cli):
self.cli = cli
def get_tool_info(self):
return {
"name": "regex_tester",
"description": "正则表达式测试器 - 测试匹配、提取、替换、解释、常用模式库",
"keywords": ["正则", "regex", "匹配", "表达式", "测试", "替换", "提取", "模式"],
"usage": self.usage
}
def get_mcp_definition(self):
return {
"name": "regex_tester",
"description": "正则表达式测试器,支持匹配测试、提取、替换、解释、性能测试",
"inputSchema": {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["test", "match", "replace", "groups", "explain",
"flags", "pattern", "save", "bench"],
"description": "操作类型"
},
"pattern": {"type": "string", "description": "正则表达式"},
"text": {"type": "string", "description": "测试文本"},
"replacement": {"type": "string", "description": "替换文本"},
"flags": {"type": "string", "description": "标志 (i/m/s/x)"},
"name": {"type": "string", "description": "模式名称"},
"description": {"type": "string", "description": "模式描述"},
"count": {"type": "integer", "description": "测试次数"}
},
"required": ["operation"]
}
}
def convert_mcp_args(self, arguments):
op = arguments.get("operation", "")
pattern = arguments.get("pattern", "")
text = arguments.get("text", "")
return f"{op} {pattern} {text}".strip()
def _load_patterns(self):
try:
if os.path.exists(self.patterns_file):
with open(self.patterns_file, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception:
pass
return self._default_patterns()
def _save_patterns(self):
try:
with open(self.patterns_file, 'w', encoding='utf-8') as f:
json.dump(self.custom_patterns, f, ensure_ascii=False, indent=2)
except Exception:
pass
def _default_patterns(self):
return {
"email": {"pattern": r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}", "desc": "邮箱地址"},
"phone_cn": {"pattern": r"1[3-9]\d{9}", "desc": "中国手机号"},
"url": {"pattern": r"https?://[^\s<>\"']+|www\.[^\s<>\"']+", "desc": "URL链接"},
"ipv4": {"pattern": r"\b(?:\d{1,3}\.){3}\d{1,3}\b", "desc": "IPv4地址"},
"date": {"pattern": r"\d{4}[-/]\d{1,2}[-/]\d{1,2}", "desc": "日期格式"},
"time": {"pattern": r"\d{2}:\d{2}(:\d{2})?", "desc": "时间格式"},
"chinese": {"pattern": r"[\u4e00-\u9fff]+", "desc": "中文字符"},
"id_card": {"pattern": r"\d{17}[\dXx]", "desc": "身份证号"},
"html_tag": {"pattern": r"<[^>]+>", "desc": "HTML标签"},
"ip_port": {"pattern": r"\b(?:\d{1,3}\.){3}\d{1,3}:\d{1,5}\b", "desc": "IP:端口"},
"mac_addr": {"pattern": r"([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}", "desc": "MAC地址"},
"hex_color": {"pattern": r"#[0-9A-Fa-f]{3,8}", "desc": "十六进制颜色"},
"number": {"pattern": r"-?\d+\.?\d*", "desc": "数字(整数/浮点)"},
"word": {"pattern": r"\b\w+\b", "desc": "单词"},
}
def handle(self, args: str) -> str:
try:
parts = self._parse_args(args)
if not parts:
return "错误:请提供操作类型"
operation = parts[0].lower()
handlers = {
"test": self._op_test,
"match": self._op_match,
"replace": self._op_replace,
"groups": self._op_groups,
"explain": self._op_explain,
"flags": self._op_flags,
"pattern": self._op_pattern,
"save": self._op_save,
"delete_pattern": self._op_delete_pattern,
"bench": self._op_bench,
}
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_flags(self, flags_str: str) -> int:
flags = 0
for c in flags_str.lower():
if c == 'i': flags |= re.IGNORECASE
elif c == 'm': flags |= re.MULTILINE
elif c == 's': flags |= re.DOTALL
elif c == 'x': flags |= re.VERBOSE
return flags
def _op_test(self, args: List[str]) -> str:
if len(args) < 2:
return "错误:格式: test <正则> <文本> [-f 标志]"
pattern_str, text = args[0], args[1]
flags = 0
for i, a in enumerate(args):
if a == "-f" and i + 1 < len(args):
flags = self._parse_flags(args[i + 1])
try:
pattern = re.compile(pattern_str, flags)
match = pattern.search(text)
if match:
return (f"✅ 匹配成功!\n"
f" 正则: {pattern_str}\n"
f" 位置: {match.start()}-{match.end()}\n"
f" 匹配: '{match.group()}'\n"
f" 全文: {pattern.findall(text)}")
else:
return f"❌ 无匹配\n 正则: {pattern_str}\n 文本: {text[:100]}"
except re.error as e:
return f"❌ 正则语法错误: {e}"
def _op_match(self, args: List[str]) -> str:
if len(args) < 2:
return "错误:格式: match <正则> <文本>"
pattern_str, text = args[0], args[1]
flags = 0
for i, a in enumerate(args):
if a == "-f" and i + 1 < len(args):
flags = self._parse_flags(args[i + 1])
try:
pattern = re.compile(pattern_str, flags)
matches = pattern.finditer(text)
results = []
for m in matches:
results.append(f" [{m.start():3d}-{m.end():3d}] '{m.group()}'")
if results:
return f" 找到 {len(results)} 个匹配:\n" + "\n".join(results)
else:
return "❌ 未找到匹配"
except re.error as e:
return f"❌ 正则语法错误: {e}"
def _op_replace(self, args: List[str]) -> str:
if len(args) < 3:
return "错误:格式: replace <正则> <文本> <替换>"
pattern_str, text, replacement = args[0], args[1], args[2]
try:
result = re.sub(pattern_str, replacement, text)
count = len(re.findall(pattern_str, text))
return (f" 替换结果 ({count} 处):\n"
f" 原文: {text}\n"
f" 结果: {result}")
except re.error as e:
return f"❌ 正则语法错误: {e}"
def _op_groups(self, args: List[str]) -> str:
if len(args) < 2:
return "错误:格式: groups <正则> <文本>"
pattern_str, text = args[0], args[1]
try:
pattern = re.compile(pattern_str)
match = pattern.search(text)
if not match:
return "❌ 无匹配"
result = f" 分组信息:\n"
result += f" 完整匹配: '{match.group()}' (span: {match.span()})\n"
for i, group in enumerate(match.groups(), 1):
result += f" 分组 {i}: '{group}' (span: {match.span(i) if group else 'N/A'})\n"
if match.groupdict():
result += f" 命名分组:\n"
for name, value in match.groupdict().items():
result += f" {name}: '{value}'\n"
return result.rstrip()
except re.error as e:
return f"❌ 正则语法错误: {e}"
def _op_explain(self, args: List[str]) -> str:
if not args:
return "错误:请提供正则表达式"
pattern_str = args[0]
explanations = []
i = 0
while i < len(pattern_str):
ch = pattern_str[i]
if ch == '\\':
if i + 1 < len(pattern_str):
next_ch = pattern_str[i + 1]
escape_map = {
'd': '数字 [0-9]', 'D': '非数字', 'w': '字母/数字/下划线',
'W': '非字母数字下划线', 's': '空白字符', 'S': '非空白字符',
'b': '单词边界', 'B': '非单词边界', 'n': '换行', 't': '制表符',
}
if next_ch in escape_map:
explanations.append(('\\' + next_ch, escape_map[next_ch]))
else:
explanations.append(('\\' + next_ch, f'转义字符 {next_ch}'))
i += 2
continue
if ch == '.':
explanations.append(('.', '任意字符(除换行)'))
elif ch == '^':
explanations.append(('^', '行/字符串开头'))
elif ch == '$':
explanations.append(('$', '行/字符串结尾'))
elif ch == '*':
explanations.append(('*', '0次或多次'))
elif ch == '+':
explanations.append(('+', '1次或多次'))
elif ch == '?':
explanations.append(('?', '0次或1次'))
elif ch == '{':
end = pattern_str.find('}', i)
if end != -1:
quantifier = pattern_str[i:end + 1]
explanations.append((quantifier, f'量词: {quantifier}'))
i = end + 1
continue
elif ch == '[':
end = pattern_str.find(']', i)
if end != -1:
char_class = pattern_str[i:end + 1]
explanations.append((char_class, f'字符类: {char_class}'))
i = end + 1
continue
elif ch == '(':
if i + 1 < len(pattern_str) and pattern_str[i + 1] == '?':
if i + 2 < len(pattern_str):
special = pattern_str[i + 2]
if special == ':':
explanations.append(('(?:', '非捕获组'))
elif special == '=':
explanations.append(('(?=', '正向前瞻'))
elif special == '!':
explanations.append(('(?!', '负向前瞻'))
i += 3
continue
explanations.append(('(', '捕获组开始'))
elif ch == ')':
explanations.append((')', '组结束'))
elif ch == '|':
explanations.append(('|', '或 (交替)'))
i += 1
result = f" 正则解释: {pattern_str}\n"
result += "─" * 45 + "\n"
for token, meaning in explanations:
result += f" {token:12s} → {meaning}\n"
return result.rstrip()
def _op_flags(self, args: List[str]) -> str:
if len(args) < 2:
return "错误:格式: flags <正则> <文本>"
pattern_str, text = args[0], args[1]
flag_combos = [
("", 0, "默认"),
("i", re.IGNORECASE, "忽略大小写"),
("m", re.MULTILINE, "多行模式"),
("s", re.DOTALL, "点匹配换行"),
("im", re.IGNORECASE | re.MULTILINE, "忽略大小写+多行"),
]
result = f" ️ 标志测试: {pattern_str}\n"
result += "─" * 45 + "\n"
for flag_str, flag_val, desc in flag_combos:
try:
matches = re.findall(pattern_str, text, flag_val)
count = len(matches) if isinstance(matches, list) else 1
result += f" [{flag_str or '-':3s}] {desc:12s} → {count} 个匹配: {matches[:3]}\n"
except re.error as e:
result += f" [{flag_str or '-':3s}] {desc:12s} → 错误: {e}\n"
return result.rstrip()
def _op_pattern(self, args: List[str]) -> str:
if args:
name = args[0]
if name in self.custom_patterns:
p = self.custom_patterns[name]
return f" 模式 '{name}':\n 正则: {p['pattern']}\n 描述: {p.get('desc', '')}"
return f"❌ 未找到模式: {name}"
result = " 常用正则模式:\n"
result += "─" * 50 + "\n"
for name, p in self.custom_patterns.items():
result += f" {name:15s} {p['pattern'][:35]:35s} {p.get('desc', '')}\n"
return result.rstrip()
def _op_save(self, args: List[str]) -> str:
if len(args) < 2:
return "错误:格式: save <名称> <正则> [-d 描述]"
name, pattern_str = args[0], args[1]
desc = ""
for i, a in enumerate(args):
if a == "-d" and i + 1 < len(args):
desc = args[i + 1]
# 验证正则
try:
re.compile(pattern_str)
except re.error as e:
return f"❌ 正则语法错误: {e}"
self.custom_patterns[name] = {"pattern": pattern_str, "desc": desc}
self._save_patterns()
return f"✅ 已保存模式 '{name}': {pattern_str}"
def _op_delete_pattern(self, args: List[str]) -> str:
if not args:
return "错误:请提供模式名称"
name = args[0]
if name in self.custom_patterns:
del self.custom_patterns[name]
self._save_patterns()
return f" ️ 已删除模式: {name}"
return f"❌ 未找到模式: {name}"
def _op_bench(self, args: List[str]) -> str:
if len(args) < 2:
return "错误:格式: bench <正则> <文本> [-n 次数]"
pattern_str, text = args[0], args[1]
count = 10000
for i, a in enumerate(args):
if a == "-n" and i + 1 < len(args):
count = int(args[i + 1])
try:
import time
pattern = re.compile(pattern_str)
start = time.perf_counter()
for _ in range(count):
pattern.findall(text)
elapsed = time.perf_counter() - start
per_op = elapsed / count * 1_000_000 # 微秒
return (f"⏱ 性能测试:\n"
f" 正则: {pattern_str}\n"
f" 次数: {count}\n"
f" 总耗时: {elapsed:.3f}s\n"
f" 每次: {per_op:.2f}μs\n"
f" 吞吐: {count / elapsed:.0f} ops/s")
except re.error as e:
return f"❌ 正则语法错误: {e}"
def test_plugin():
plugin = Liugin()
print("正则表达式测试器测试:")
print(plugin.handle('test "\\d+" "abc123def456"'))
print(plugin.handle('match "[a-z]+" "hello world foo bar"'))
print(plugin.handle('groups "(\\w+)@(\\w+)\\.(\\w+)" "user@domain.com"'))
print(plugin.handle('explain "\\d{4}-\\d{2}-\\d{2}"'))
if __name__ == "__main__":
test_plugin()
+441
View File
@@ -0,0 +1,441 @@
"""
定时任务插件 - AI 可设定定时任务,到时自动激活提醒
支持一次性和重复性定时任务
"""
import json
import os
import time
import threading
from datetime import datetime, timedelta
from colorama import Fore, Style
class Liugin:
"""定时任务插件"""
def __init__(self):
self.usage = """定时任务工具使用方法:
JSON格式示例:
{"action": "use_tool", "tool": "scheduler", "args": "add 30m 提醒我检查代码"} - 30分钟后提醒
{"action": "use_tool", "tool": "scheduler", "args": "add 2h 提交代码"} - 2小时后提醒
{"action": "use_tool", "tool": "scheduler", "args": "add 14:30 开会"} - 今天14:30提醒
{"action": "use_tool", "tool": "scheduler", "args": "add 2026-04-28 10:00 发布v5.2"} - 指定日期时间提醒
{"action": "use_tool", "tool": "scheduler", "args": "list"} - 列出所有定时任务
{"action": "use_tool", "tool": "scheduler", "args": "delete 1"} - 删除任务ID为1的任务
{"action": "use_tool", "tool": "scheduler", "args": "clear"} - 清除所有已完成/已取消的任务
时间格式说明:
- 相对时间: 30s / 30m / 2h / 1d(秒/分/时/天)
- 今日时间: HH:MM(如 14:30)
- 绝对时间: YYYY-MM-DD HH:MM(如 2026-04-28 10:00)
- 重复任务: 每个时间前加 r: 前缀,如 r:30m / r:2h / r:14:30
重复任务会自动重新调度,不会自动删除
功能说明:
- add <时间> <描述> - 添加定时任务
- list - 列出所有定时任务
- delete <任务ID> - 删除指定任务
- clear - 清除已完成/已取消的任务
当定时任务到期时,AI 会自动被激活并收到提醒。"""
self.cli = None
self.tasks_file = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"scheduler_tasks.json"
)
self.tasks = []
self._lock = threading.Lock()
self._monitor_thread = None
self._monitor_running = False
self._load_tasks()
def set_cli(self, cli):
"""设置CLI实例引用"""
self.cli = cli
self.cli.register_liugin_command('scheduler', self.command_handler)
self.cli.register_liugin_command('remind', self.command_handler)
# 启动后台监控线程
self._start_monitor()
def command_handler(self, args):
"""处理 /scheduler 或 /remind 命令"""
return self.handle(args)
def get_tool_info(self):
return {
"name": "scheduler",
"description": "定时任务工具,AI 可设定定时提醒。支持相对时间(30m/2h)、今日时间(14:30)、绝对时间(2026-04-28 10:00)、重复任务(r:30m)",
"keywords": ["定时", "提醒", "scheduler", "remind", "闹钟", "alarm", "schedule", "计划"],
"usage": self.usage
}
# ── 数据持久化 ──
def _load_tasks(self):
"""从文件加载任务"""
try:
if os.path.exists(self.tasks_file):
with open(self.tasks_file, 'r', encoding='utf-8') as f:
self.tasks = json.load(f)
except Exception:
self.tasks = []
def _save_tasks(self):
"""保存任务到文件"""
try:
with open(self.tasks_file, 'w', encoding='utf-8') as f:
json.dump(self.tasks, f, ensure_ascii=False, indent=2)
except Exception:
pass
# ── 时间解析 ──
def _parse_time(self, time_str: str) -> tuple:
"""
解析时间字符串
返回: (datetime, is_repeat, repeat_seconds)
"""
time_str = time_str.strip()
now = datetime.now()
is_repeat = False
repeat_seconds = 0
# 检查是否是重复任务
if time_str.startswith('r:'):
is_repeat = True
time_str = time_str[2:].strip()
# 相对时间: 30s / 30m / 2h / 1d
if time_str[-1:].lower() in ('s', 'm', 'h', 'd') and time_str[:-1].strip().isdigit():
num = int(time_str[:-1].strip())
unit = time_str[-1].lower()
multipliers = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}
seconds = num * multipliers[unit]
if is_repeat:
repeat_seconds = seconds
return now + timedelta(seconds=seconds), is_repeat, repeat_seconds
# 绝对时间: YYYY-MM-DD HH:MM
try:
dt = datetime.strptime(time_str, "%Y-%m-%d %H:%M")
if is_repeat:
# 重复任务的绝对时间当作每日重复
repeat_seconds = 86400 # 24h
return dt, is_repeat, repeat_seconds
except ValueError:
pass
# 今日时间: HH:MM
try:
dt = datetime.strptime(time_str, "%H:%M")
target = now.replace(hour=dt.hour, minute=dt.minute, second=0, microsecond=0)
if target <= now:
target += timedelta(days=1)
if is_repeat:
repeat_seconds = 86400
return target, is_repeat, repeat_seconds
except ValueError:
pass
return None, False, 0
# ── 核心操作 ──
def _add_task(self, time_str: str, message: str) -> str:
"""添加定时任务"""
target_time, is_repeat, repeat_seconds = self._parse_time(time_str)
if target_time is None:
return f"无法解析时间: {time_str}。支持格式: 30m / 2h / 14:30 / 2026-04-28 10:00 / r:30m"
with self._lock:
task_id = max([t.get('id', 0) for t in self.tasks], default=0) + 1
task = {
"id": task_id,
"message": message,
"trigger_at": target_time.strftime("%Y-%m-%d %H:%M:%S"),
"repeat": is_repeat,
"repeat_seconds": repeat_seconds,
"status": "pending", # pending / fired / canceled
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
}
self.tasks.append(task)
self._save_tasks()
time_display = target_time.strftime("%Y-%m-%d %H:%M:%S")
repeat_info = f" (每{self._format_seconds(repeat_seconds)}重复)" if is_repeat else ""
return f"定时任务 #{task_id} 已创建{repeat_info}\n⏰ {time_display} → {message}"
def _list_tasks(self) -> str:
"""列出所有定时任务"""
with self._lock:
if not self.tasks:
return "暂无定时任务"
lines = []
now = datetime.now()
for t in self.tasks:
tid = t['id']
status = t['status']
msg = t['message']
trigger = t['trigger_at']
repeat = t.get('repeat', False)
# 状态图标
if status == 'pending':
remaining = self._calc_remaining(trigger, now)
icon = "⏰"
suffix = f" ({remaining})" if remaining else ""
repeat_tag = f" [每{self._format_seconds(t.get('repeat_seconds', 0))}重复]" if repeat else ""
lines.append(f" {icon} #{tid}{suffix}{repeat_tag}: {msg}")
elif status == 'fired':
lines.append(f" ✅ #{tid} (已触发): {msg}")
elif status == 'canceled':
lines.append(f" ❌ #{tid} (已取消): {msg}")
return "\n".join(lines) if lines else "暂无定时任务"
def _delete_task(self, task_id_str: str) -> str:
"""删除指定任务"""
try:
task_id = int(task_id_str)
except ValueError:
return f"无效的任务ID: {task_id_str}"
with self._lock:
for t in self.tasks:
if t['id'] == task_id:
t['status'] = 'canceled'
self._save_tasks()
return f"定时任务 #{task_id} 已取消: {t['message']}"
return f"未找到任务 #{task_id}"
def _clear_tasks(self) -> str:
"""清除已完成/已取消的任务"""
with self._lock:
before = len(self.tasks)
self.tasks = [t for t in self.tasks if t['status'] == 'pending']
removed = before - len(self.tasks)
self._save_tasks()
return f"已清除 {removed} 个已结束的任务" if removed else "没有需要清除的任务"
# ── 后台监控 ──
def _start_monitor(self):
"""启动后台监控线程"""
if self._monitor_running:
return
self._monitor_running = True
self._monitor_thread = threading.Thread(target=self._monitor_loop, daemon=True)
self._monitor_thread.start()
def _stop_monitor(self):
"""停止后台监控"""
self._monitor_running = False
def _monitor_loop(self):
"""后台监控循环 — 每秒检查到期任务"""
while self._monitor_running:
try:
self._check_due_tasks()
except Exception:
pass
time.sleep(1)
def _check_due_tasks(self):
"""检查并触发到期任务"""
now = datetime.now()
now_str = now.strftime("%Y-%m-%d %H:%M:%S")
with self._lock:
for task in self.tasks:
if task['status'] != 'pending':
continue
trigger_at = task['trigger_at']
if now_str >= trigger_at:
# 触发任务
self._fire_task(task, now)
# 保存可能的变更
self._save_tasks()
def _fire_task(self, task: dict, now: datetime):
"""触发一个到期任务"""
task_id = task['id']
message = task['message']
is_repeat = task.get('repeat', False)
repeat_seconds = task.get('repeat_seconds', 0)
# 标记为已触发
task['status'] = 'fired'
task['fired_at'] = now.strftime("%Y-%m-%d %H:%M:%S")
# 重复任务:重新调度
if is_repeat and repeat_seconds > 0:
next_time = now + timedelta(seconds=repeat_seconds)
# 创建新的重复任务
new_task = {
"id": max([t.get('id', 0) for t in self.tasks], default=0) + 1,
"message": message,
"trigger_at": next_time.strftime("%Y-%m-%d %H:%M:%S"),
"repeat": True,
"repeat_seconds": repeat_seconds,
"status": "pending",
"created_at": now.strftime("%Y-%m-%d %H:%M:%S"),
"parent_id": task_id,
}
self.tasks.append(new_task)
# 发送系统通知
try:
from xcli_core.notification import notify_task_complete
notify_task_complete(f"定时提醒: {message}")
except Exception:
pass
# 打印醒目提醒
if not is_generating:
# CLI 模式
print()
print(f"{Fore.YELLOW}{'═' * 50}{Style.RESET_ALL}")
print(f"{Fore.YELLOW} ⏰ 定时任务提醒 #{task_id}{Style.RESET_ALL}")
print(f"{Fore.YELLOW}{'─' * 50}{Style.RESET_ALL}")
print(f" {Fore.CYAN}任务:{Style.RESET_ALL} {message}")
print(f" {Fore.CYAN}时间:{Style.RESET_ALL} {now.strftime('%H:%M:%S')}")
if is_repeat:
next_str = (now + timedelta(seconds=repeat_seconds)).strftime("%Y-%m-%d %H:%M:%S")
print(f" {Fore.CYAN}下次:{Style.RESET_ALL} {next_str}")
print(f"{Fore.YELLOW}{'═' * 50}{Style.RESET_ALL}")
# 自动激活 AI — 将提醒注入对话
self._activate_ai(task_id, message)
def _activate_ai(self, task_id: int, message: str):
"""激活 AI 处理定时任务提醒"""
if not self.cli:
return
reminder = (
f"[系统定时提醒] 定时任务 #{task_id} 已到期。\n"
f"任务内容: {message}\n"
f"请根据任务内容进行相应的操作或提醒用户。"
)
# CLI 模式:在新线程中调用,避免阻塞监控
def _cli_activate():
try:
# 先给用户一个视觉提示
print(f"\n{Fore.GREEN} AI 正在处理定时提醒...{Style.RESET_ALL}")
print(f"{Fore.WHITE}> {Style.RESET_ALL}", end='', flush=True)
self.cli.process_conversation(reminder)
# 重新显示输入提示
print(f"\n{Fore.WHITE}> {Style.RESET_ALL}", end='', flush=True)
except Exception:
pass
threading.Thread(target=_cli_activate, daemon=True).start()
# ── 辅助函数 ──
@staticmethod
def _format_seconds(seconds: int) -> str:
"""格式化秒数为可读文本"""
if seconds <= 0:
return ""
if seconds < 60:
return f"{seconds}秒"
if seconds < 3600:
return f"{seconds // 60}分钟"
if seconds < 86400:
h = seconds // 3600
m = (seconds % 3600) // 60
return f"{h}小时{m}分钟" if m else f"{h}小时"
d = seconds // 86400
return f"{d}天"
@staticmethod
def _calc_remaining(trigger_str: str, now: datetime) -> str:
"""计算剩余时间"""
try:
trigger = datetime.strptime(trigger_str, "%Y-%m-%d %H:%M:%S")
diff = trigger - now
if diff.total_seconds() <= 0:
return "即将触发"
secs = int(diff.total_seconds())
if secs < 60:
return f"剩余 {secs}秒"
if secs < 3600:
return f"剩余 {secs // 60}分钟"
if secs < 86400:
h = secs // 3600
m = (secs % 3600) // 60
return f"剩余 {h}小时{m}分钟" if m else f"剩余 {h}小时"
d = secs // 86400
return f"剩余 {d}天"
except Exception:
return ""
# ── 插件入口 ──
def handle(self, args: str) -> str:
"""处理工具调用"""
if not args or not args.strip():
return self.usage
parts = args.strip().split(None, 1)
action = parts[0].lower()
rest = parts[1].strip() if len(parts) > 1 else ""
if action == 'add':
if not rest:
return "用法: add <时间> <描述>\n示例: add 30m 检查代码 / add 14:30 开会 / add r:2h 站会"
# 分离时间和描述
time_parts = rest.split(None, 1)
if len(time_parts) < 2:
return "请同时提供时间和描述。示例: add 30m 检查代码"
return self._add_task(time_parts[0], time_parts[1])
elif action == 'list':
return self._list_tasks()
elif action in ('delete', 'cancel', 'rm'):
if not rest:
return "用法: delete <任务ID>"
return self._delete_task(rest)
elif action == 'clear':
return self._clear_tasks()
else:
return f"未知操作: {action}\n可用操作: add / list / delete / clear"
def get_mcp_definition(self):
"""返回 MCP 工具定义"""
return {
"name": "scheduler",
"description": "定时任务工具,设定定时提醒。到期时 AI 会自动激活并收到提醒。",
"parameters": {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["add", "list", "delete", "clear"],
"description": "操作类型"
},
"time": {
"type": "string",
"description": "时间: 30m(相对) / 14:30(今日) / 2026-04-28 10:00(绝对) / r:30m(重复)"
},
"message": {
"type": "string",
"description": "任务描述/提醒内容"
},
"task_id": {
"type": "integer",
"description": "任务ID (用于 delete)"
}
},
"required": ["action"]
}
}
+449
View File
@@ -0,0 +1,449 @@
"""
代码片段管理器插件 - 保存、搜索、复用常用代码片段
支持分类、标签、搜索、导出,JSON 持久化存储
"""
import json
import os
import time
import hashlib
from datetime import datetime
from typing import List, Optional
class Liugin:
"""代码片段管理器 - 保存和复用代码片段"""
def __init__(self):
self.usage = """代码片段管理器
操作:
add <名称> -l <语言> -c "代码" [-t 标签1,标签2] [-d 描述]
- 添加代码片段
list [-l 语言] [-t 标签] - 列出片段(可按语言/标签过滤)
get <名称或ID> - 获取片段详情
search <关键词> - 搜索片段(名称/描述/代码/标签)
delete <名称或ID> - 删除片段
edit <名称或ID> -c "新代码" - 更新片段代码
export [文件] - 导出所有片段
import <文件> - 导入片段
tags - 列出所有标签
stats - 统计信息
copy <名称或ID> - 复制到剪贴板(如可用)
recent [-n 数量] - 最近添加的片段
示例:
snippet_manager add quick_sort -l python -c "def qsort(arr): ..." -t 排序,算法
snippet_manager list -l python
snippet_manager search 排序
snippet_manager get quick_sort
"""
self.cli = None
self.snippets_file = "snippets.json"
self.snippets = []
self._load_snippets()
def set_cli(self, cli):
self.cli = cli
def get_tool_info(self):
return {
"name": "snippet_manager",
"description": "代码片段管理器 - 保存、搜索、复用常用代码片段,支持分类和标签",
"keywords": ["代码", "片段", "snippet", "模板", "复用", "保存", "搜索", "代码片段"],
"usage": self.usage
}
def get_mcp_definition(self):
return {
"name": "snippet_manager",
"description": "代码片段管理器,支持添加、搜索、获取、删除代码片段",
"inputSchema": {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["add", "list", "get", "search", "delete",
"edit", "export", "import", "tags", "stats", "recent", "copy"],
"description": "操作类型"
},
"name": {"type": "string", "description": "片段名称或ID"},
"language": {"type": "string", "description": "编程语言"},
"code": {"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", "")
parts = [op]
if name:
parts.append(name)
if arguments.get("keyword"):
parts.append(arguments["keyword"])
if arguments.get("file"):
parts.append(arguments["file"])
if arguments.get("count"):
parts.append(str(arguments["count"]))
if arguments.get("language"):
parts.extend(["-l", arguments["language"]])
if arguments.get("code"):
parts.extend(["-c", f'"{arguments["code"]}"'])
if arguments.get("tags"):
parts.extend(["-t", arguments["tags"]])
if arguments.get("description"):
parts.extend(["-d", f'"{arguments["description"]}"'])
return " ".join(parts)
def _load_snippets(self):
try:
if os.path.exists(self.snippets_file):
with open(self.snippets_file, 'r', encoding='utf-8') as f:
self.snippets = json.load(f)
except Exception:
self.snippets = []
def _save_snippets(self):
try:
with open(self.snippets_file, 'w', encoding='utf-8') as f:
json.dump(self.snippets, f, ensure_ascii=False, indent=2)
return None
except Exception as e:
return f"保存失败: {e}"
def _gen_id(self, name: str) -> str:
return hashlib.md5(f"{name}{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,
"get": self._op_get,
"search": self._op_search,
"delete": self._op_delete,
"edit": self._op_edit,
"export": self._op_export,
"import": self._op_import,
"tags": self._op_tags,
"stats": self._op_stats,
"recent": self._op_recent,
"copy": self._op_copy,
}
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 "错误:请提供片段名称"
name = args[0]
language, code, tags, desc = "", "", [], ""
i = 1
while i < len(args):
if args[i] == "-l" and i + 1 < len(args):
language = args[i + 1]; i += 1
elif args[i] == "-c" and i + 1 < len(args):
code = 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 code:
return "错误:请用 -c 提供代码内容"
# 检查重名
for s in self.snippets:
if s["name"] == name:
return f"错误:片段 '{name}' 已存在,用 edit 更新"
snippet = {
"id": self._gen_id(name),
"name": name,
"language": language,
"code": code,
"tags": tags,
"description": desc,
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"usage_count": 0
}
self.snippets.append(snippet)
err = self._save_snippets()
if err:
return err
return f"✅ 已添加片段 '{name}' (ID: {snippet['id']}, 语言: {language or '未指定'})"
def _op_list(self, args: List[str]) -> str:
lang_filter, tag_filter = None, None
i = 0
while i < len(args):
if args[i] == "-l" and i + 1 < len(args):
lang_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.snippets
if lang_filter:
filtered = [s for s in filtered if s.get("language", "").lower() == lang_filter]
if tag_filter:
filtered = [s for s in filtered if any(tag_filter in t.lower() for t in s.get("tags", []))]
if not filtered:
return "📭 没有找到代码片段"
result = f"\n📝 代码片段列表 (共 {len(filtered)} 个)\n"
result += "─" * 55 + "\n"
for s in filtered:
lang = s.get("language", "未知")
tags = ", ".join(s.get("tags", [])) if s.get("tags") else ""
code_preview = s.get("code", "")[:60].replace("\n", "↵")
result += f" [{s['id']}] {s['name']} ({lang})\n"
if tags:
result += f" ️ {tags}\n"
result += f" {code_preview}...\n"
result += f" ⏱ {s.get('created_at', '未知')}\n\n"
result += "─" * 55
return result
def _op_get(self, args: List[str]) -> str:
if not args:
return "错误:请提供片段名称或ID"
key = args[0]
snippet = self._find_snippet(key)
if not snippet:
return f"❌ 未找到片段: {key}"
snippet["usage_count"] = snippet.get("usage_count", 0) + 1
err = self._save_snippets()
result = f"\n 片段: {snippet['name']}\n"
result += "─" * 50 + "\n"
result += f" ID: {snippet['id']}\n"
result += f" 语言: {snippet.get('language', '未指定')}\n"
if snippet.get("tags"):
result += f" ️ 标签: {', '.join(snippet['tags'])}\n"
if snippet.get("description"):
result += f" 描述: {snippet['description']}\n"
result += f" 创建: {snippet.get('created_at', '未知')}\n"
result += f" 使用: {snippet.get('usage_count', 0)} 次\n"
result += "─" * 50 + "\n"
result += f" 代码:\n{snippet['code']}\n"
result += "─" * 50
return result
def _op_search(self, args: List[str]) -> str:
if not args:
return "错误:请提供搜索关键词"
keyword = " ".join(args).lower()
results = []
for s in self.snippets:
searchable = f"{s['name']} {s.get('description', '')} {s.get('code', '')} {' '.join(s.get('tags', []))} {s.get('language', '')}".lower()
if keyword in searchable:
results.append(s)
if not results:
return f" 未找到匹配 '{keyword}' 的片段"
output = f" 搜索结果 ({len(results)} 个):\n"
output += "─" * 50 + "\n"
for s in results:
lang = s.get("language", "")
output += f" [{s['id']}] {s['name']} ({lang}) - {s.get('description', '')[:40]}\n"
return output.rstrip()
def _op_delete(self, args: List[str]) -> str:
if not args:
return "错误:请提供片段名称或ID"
key = args[0]
for i, s in enumerate(self.snippets):
if s["name"] == key or s["id"] == key:
removed = self.snippets.pop(i)
err = self._save_snippets()
return f" ️ 已删除片段: {removed['name']}" + (f" (警告: {err})" if err else "")
return f"❌ 未找到片段: {key}"
def _op_edit(self, args: List[str]) -> str:
if not args:
return "错误:请提供片段名称或ID"
key = args[0]
snippet = self._find_snippet(key)
if not snippet:
return f"❌ 未找到片段: {key}"
new_code = None
new_desc = None
i = 1
while i < len(args):
if args[i] == "-c" and i + 1 < len(args):
new_code = args[i + 1]; i += 1
elif args[i] == "-d" and i + 1 < len(args):
new_desc = args[i + 1]; i += 1
i += 1
if new_code:
snippet["code"] = new_code
if new_desc:
snippet["description"] = new_desc
snippet["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
if self._save_snippets():
return f"✅ 已更新片段: {snippet['name']}"
return f"❌ 更新片段失败: 写入磁盘出错"
def _op_export(self, args: List[str]) -> str:
output_file = args[0] if args else "snippets_export.json"
try:
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(self.snippets, f, ensure_ascii=False, indent=2)
return f"✅ 已导出 {len(self.snippets)} 个片段到: {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_names = {s["name"] for s in self.snippets}
added = 0
for s in imported:
if isinstance(s, dict) and s.get("name") and s["name"] not in existing_names:
s["id"] = self._gen_id(s["name"])
self.snippets.append(s)
existing_names.add(s["name"])
added += 1
if self._save_snippets():
return f"✅ 已导入 {added} 个片段 (跳过 {len(imported) - added} 个重复)"
return f"❌ 导入失败: 写入磁盘出错"
except Exception as e:
return f"❌ 导入失败: {e}"
def _op_tags(self, args: List[str]) -> str:
tag_count = {}
for s in self.snippets:
for t in s.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_stats(self, args: List[str]) -> str:
total = len(self.snippets)
if not total:
return "📊 暂无片段"
lang_count = {}
total_usage = 0
for s in self.snippets:
lang = s.get("language", "未知")
lang_count[lang] = lang_count.get(lang, 0) + 1
total_usage += s.get("usage_count", 0)
result = f"📊 片段统计\n"
result += "─" * 30 + "\n"
result += f" 总数: {total}\n"
result += f" 总使用: {total_usage} 次\n"
result += f" 语言分布:\n"
for lang, count in sorted(lang_count.items(), key=lambda x: -x[1]):
result += f" {lang}: {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.snippets, key=lambda s: s.get("created_at", ""), reverse=True)[:count]
if not recent:
return "📭 暂无片段"
result = f"⏰ 最近 {len(recent)} 个片段:\n"
for s in recent:
result += f" [{s['id']}] {s['name']} - {s.get('created_at', '')}\n"
return result.rstrip()
def _op_copy(self, args: List[str]) -> str:
if not args:
return "错误:请提供片段名称或ID"
snippet = self._find_snippet(args[0])
if not snippet:
return f"❌ 未找到片段: {args[0]}"
snippet["usage_count"] = snippet.get("usage_count", 0) + 1
err = self._save_snippets()
if err:
return err
return f" 代码内容 ({snippet['name']}):\n{snippet['code']}"
def _find_snippet(self, key: str) -> Optional[dict]:
for s in self.snippets:
if s["name"] == key or s["id"] == key:
return s
return None
def test_plugin():
plugin = Liugin()
print("代码片段管理器测试:")
print(plugin.handle('add hello -l python -c "print(\'Hello, World!\')" -t 示例,入门 -d 最简单的Python程序'))
print(plugin.handle('add fib -l python -c "def fib(n): return n if n < 2 else fib(n-1)+fib(n-2)" -t 算法,递归'))
print(plugin.handle('list'))
print(plugin.handle('search python'))
if __name__ == "__main__":
test_plugin()
+549
View File
@@ -0,0 +1,549 @@
"""
语音识别插件 - 弹窗录音界面
AI 触发后弹出独立录音窗口
"""
import os
import json
import subprocess
import sys
import time
from typing import Optional, Dict, Any, List
# 结果保存路径
RESULT_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "speech_temp", "last_result.txt")
TEMP_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "speech_temp")
class Liugin:
"""语音识别插件"""
def __init__(self):
self.plugin_dir = os.path.dirname(os.path.abspath(__file__))
self.temp_dir = TEMP_DIR
os.makedirs(self.temp_dir, exist_ok=True)
self.usage = """语音识别插件使用方法:
speech_recognition <操作> [参数]
录音识别:
- record - 弹出录音窗口,录音并识别
文件识别:
- file <音频文件路径> - 识别音频文件内容
其他:
- status - 查看状态
- result - 获取上次识别结果
支持的音频格式: wav, mp3, m4a, flac, ogg
"""
self.cli = None
def set_cli(self, cli):
self.cli = cli
def get_tool_info(self):
return {
"name": "speech_recognition",
"description": "语音识别插件,弹出录音窗口进行语音识别,将语音转换为文字",
"keywords": ["语音", "识别", "录音", "转文字", "speech", "voice", "recognition"],
"usage": self.usage
}
def get_mcp_definition(self):
return {
"name": "speech_recognition",
"description": "语音识别插件,弹出录音窗口进行语音识别",
"inputSchema": {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["record", "file", "status", "result"],
"description": "操作类型"
},
"file_path": {"type": "string", "description": "音频文件路径"}
},
"required": ["operation"]
}
}
def convert_mcp_args(self, arguments: Dict[str, Any]) -> str:
op = arguments.get("operation", "")
parts = [op]
if op == "file":
parts.append(arguments.get("file_path", ""))
return " ".join(filter(None, parts))
def handle(self, args: str) -> str:
try:
parts = self._parse_args(args)
if not parts:
return "错误:请提供操作类型。\n" + self.usage
operation = parts[0].lower()
handlers = {
"record": self._op_record,
"file": self._op_file,
"status": self._op_status,
"result": self._op_result,
}
if operation in handlers:
return handlers[operation](parts[1:])
else:
return f"错误:未知操作 '{operation}'\n可用操作: " + ", ".join(handlers.keys())
except Exception as e:
return f"处理请求失败: {e}"
def _parse_args(self, args: str) -> List[str]:
parts = []
current = ""
in_quotes = False
quote_char = None
for char in args:
if char in ('"', "'") and not in_quotes:
in_quotes = True
quote_char = char
elif char == quote_char and in_quotes:
in_quotes = False
quote_char = None
elif char == ' ' and not in_quotes:
if current:
parts.append(current)
current = ""
else:
current += char
if current:
parts.append(current)
return parts
# ==================== 弹窗录音 ====================
def _op_record(self, args: List[str]) -> str:
"""弹出录音窗口"""
# 创建录音窗口脚本
recorder_script = os.path.join(self.temp_dir, "recorder_gui.py")
gui_code = f'''import tkinter as tk
from tkinter import messagebox
import os
import sys
import threading
import time
RESULT_FILE = r"{RESULT_FILE}"
TEMP_DIR = r"{TEMP_DIR}"
class VoiceRecorder:
def __init__(self):
self.root = tk.Tk()
self.root.title("语音识别录音")
self.root.geometry("450x350")
self.root.resizable(False, False)
# 居中显示
self.root.update_idletasks()
x = (self.root.winfo_screenwidth() - 450) // 2
y = (self.root.winfo_screenheight() - 350) // 2
self.root.geometry(f"450x350+{{x}}+{{y}}")
# 录音状态
self.is_recording = False
self.record_file = None
self.frames = []
self.start_time = None
self.p = None
self.stream = None
# 创建界面
self._create_ui()
# 检查 pyaudio
self.has_pyaudio = self._check_pyaudio()
if not self.has_pyaudio:
self.status_label.config(text="请安装 pyaudio: pip install pyaudio", fg="red")
def _check_pyaudio(self):
try:
import pyaudio
return True
except:
return False
def _create_ui(self):
# 标题
title_label = tk.Label(self.root, text=" 语音识别录音", font=("Arial", 18, "bold"))
title_label.pack(pady=15)
# 状态标签
self.status_label = tk.Label(self.root, text="点击开始录音", font=("Arial", 11))
self.status_label.pack(pady=5)
# 时间标签
self.time_label = tk.Label(self.root, text="00:00", font=("Arial", 28, "bold"), fg="#2196F3")
self.time_label.pack(pady=10)
# 按钮框架
btn_frame = tk.Frame(self.root)
btn_frame.pack(pady=15)
# 开始/停止按钮
self.record_btn = tk.Button(
btn_frame, text=" 开始录音", font=("Arial", 12),
width=12, height=2, command=self.toggle_record,
bg="#4CAF50", fg="white", cursor="hand2"
)
self.record_btn.pack(side=tk.LEFT, padx=10)
# 识别按钮
self.recognize_btn = tk.Button(
btn_frame, text=" 识别", font=("Arial", 12),
width=12, height=2, command=self.recognize,
bg="#2196F3", fg="white", cursor="hand2", state=tk.DISABLED
)
self.recognize_btn.pack(side=tk.LEFT, padx=10)
# 结果显示
result_frame = tk.LabelFrame(self.root, text="识别结果", font=("Arial", 10))
result_frame.pack(pady=10, padx=15, fill=tk.BOTH, expand=True)
self.result_text = tk.Text(result_frame, height=5, width=50, font=("Arial", 10))
self.result_text.pack(pady=5, padx=5, fill=tk.BOTH, expand=True)
def toggle_record(self):
if not self.has_pyaudio:
messagebox.showerror("错误", "请先安装 pyaudio:\npip install pyaudio")
return
if self.is_recording:
self.stop_record()
else:
self.start_record()
def start_record(self):
import pyaudio
self.is_recording = True
self.frames = []
self.start_time = time.time()
self.record_btn.config(text=" 停止录音", bg="#f44336")
self.status_label.config(text=" 正在录音...", fg="red")
self.recognize_btn.config(state=tk.DISABLED)
CHUNK = 1024
FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 16000
self.p = pyaudio.PyAudio()
self.stream = self.p.open(
format=FORMAT, channels=CHANNELS, rate=RATE,
input=True, frames_per_buffer=CHUNK
)
# 更新时间
self._update_time()
# 录音线程
def record_loop():
while self.is_recording:
try:
data = self.stream.read(CHUNK, exception_on_overflow=False)
self.frames.append(data)
except:
break
self.record_thread = threading.Thread(target=record_loop, daemon=True)
self.record_thread.start()
def stop_record(self):
self.is_recording = False
self.record_btn.config(text=" 开始录音", bg="#4CAF50")
self.status_label.config(text="录音完成,可以识别", fg="green")
self.recognize_btn.config(state=tk.NORMAL)
# 停止录音
if self.stream:
self.stream.stop_stream()
self.stream.close()
if self.p:
self.p.terminate()
# 保存文件
import wave
import pyaudio
self.record_file = os.path.join(TEMP_DIR, f"record_{{int(time.time())}}.wav")
wf = wave.open(self.record_file, "wb")
wf.setnchannels(1)
wf.setsampwidth(pyaudio.get_sample_size(pyaudio.paInt16))
wf.setframerate(16000)
wf.writeframes(b"".join(self.frames))
wf.close()
self.result_text.delete(1.0, tk.END)
self.result_text.insert(tk.END, " 录音已保存\n点击 [识别] 按钮进行语音识别")
def _update_time(self):
if self.is_recording:
elapsed = int(time.time() - self.start_time)
mins = elapsed // 60
secs = elapsed % 60
self.time_label.config(text=f"{{mins:02d}}:{{secs:02d}}")
self.root.after(100, self._update_time)
def recognize(self):
if not self.record_file or not os.path.exists(self.record_file):
messagebox.showerror("错误", "没有录音文件,请先录音")
return
self.status_label.config(text="正在识别...", fg="blue")
self.result_text.delete(1.0, tk.END)
self.result_text.insert(tk.END, " 识别中,请稍候...")
self.root.update()
# 尝试识别
result = self._do_recognize()
self.result_text.delete(1.0, tk.END)
if result:
self.result_text.insert(tk.END, result)
self.status_label.config(text="识别完成", fg="green")
# 保存结果
with open(RESULT_FILE, "w", encoding="utf-8") as f:
f.write(result)
else:
self.result_text.insert(tk.END, " 识别失败\n\n请安装识别引擎:\n• pip install openai-whisper\n• pip install SpeechRecognition")
self.status_label.config(text="识别失败", fg="red")
def _do_recognize(self):
# 尝试 Whisper
try:
import whisper
self.status_label.config(text="使用 Whisper 识别...", fg="blue")
self.root.update()
model = whisper.load_model("base")
result = model.transcribe(self.record_file, language="zh")
text = result.get("text", "").strip()
if text:
return f" {{text}}"
except Exception as e:
print(f"Whisper error: {{e}}")
# 尝试 SpeechRecognition
try:
import speech_recognition as sr
self.status_label.config(text="使用 Google 识别...", fg="blue")
self.root.update()
r = sr.Recognizer()
with sr.AudioFile(self.record_file) as source:
audio = r.record(source)
text = r.recognize_google(audio, language="zh-CN")
if text:
return f" {{text}}"
except Exception as e:
print(f"SpeechRecognition error: {{e}}")
return None
def run(self):
self.root.mainloop()
# 清理
if self.is_recording:
self.is_recording = False
if self.stream:
self.stream.close()
if self.p:
self.p.terminate()
if __name__ == "__main__":
app = VoiceRecorder()
app.run()
'''
# 写入脚本
with open(recorder_script, "w", encoding="utf-8") as f:
f.write(gui_code)
# 启动录音窗口
try:
subprocess.Popen([sys.executable, recorder_script],
creationflags=subprocess.CREATE_NEW_CONSOLE)
return " 已弹出录音窗口,请在窗口中进行录音和识别\n\n使用方法:\n1. 点击「开始录音」\n2. 对着麦克风说话\n3. 点击「停止录音」\n4. 点击「识别」获取文字结果"
except Exception as e:
return f" 启动录音窗口失败: {e}"
# ==================== 文件识别 ====================
def _op_file(self, args: List[str]) -> str:
"""识别音频文件"""
if not args:
return "错误:请提供音频文件路径"
file_path = args[0]
if not os.path.exists(file_path):
return f" 文件不存在: {file_path}"
return self._recognize_file(file_path)
def _recognize_file(self, file_path: str) -> str:
"""识别音频文件内容"""
# 尝试使用 whisper(本地)
result = self._try_whisper(file_path)
if result:
self._save_result(result)
return result
# 尝试使用 speech_recognition 库
result = self._try_speech_recognition(file_path)
if result:
self._save_result(result)
return result
return " 语音识别失败:未安装识别引擎\n\n安装方法:\n1. Whisper (推荐): pip install openai-whisper\n2. SpeechRecognition: pip install SpeechRecognition\n3. PyAudio (录音): pip install pyaudio"
def _save_result(self, result: str):
"""保存识别结果"""
try:
with open(RESULT_FILE, "w", encoding="utf-8") as f:
f.write(result)
except:
pass
def _try_whisper(self, file_path: str) -> Optional[str]:
"""使用 Whisper 识别"""
try:
import whisper
print(f"[语音识别] 使用 Whisper 识别中...")
model = whisper.load_model("base")
result = model.transcribe(file_path, language="zh")
text = result.get("text", "").strip()
if text:
return f" 识别结果:\n{text}"
return None
except ImportError:
return None
except Exception as e:
print(f"[语音识别] Whisper 错误: {e}")
return None
def _try_speech_recognition(self, file_path: str) -> Optional[str]:
"""使用 SpeechRecognition 库识别"""
wav_file = file_path
try:
import speech_recognition as sr
print(f"[语音识别] 使用 SpeechRecognition 识别中...")
r = sr.Recognizer()
# 转换为 wav 格式
wav_file = self._convert_to_wav(file_path)
with sr.AudioFile(wav_file) as source:
audio = r.record(source)
# 尝试 Google 语音识别
try:
text = r.recognize_google(audio, language="zh-CN")
if text:
return f" 识别结果:\n{text}"
except:
pass
return None
except ImportError:
return None
except Exception as e:
print(f"[语音识别] SpeechRecognition 错误: {e}")
return None
finally:
# 清理转换的临时文件
if wav_file != file_path and os.path.exists(wav_file):
try:
os.remove(wav_file)
except:
pass
def _convert_to_wav(self, file_path: str) -> str:
"""转换音频为 wav 格式"""
if file_path.lower().endswith('.wav'):
return file_path
try:
wav_file = os.path.join(self.temp_dir, f"convert_{int(time.time())}.wav")
# 使用 ffmpeg 转换
subprocess.run([
'ffmpeg', '-y', '-i', file_path,
'-acodec', 'pcm_s16le',
'-ar', '16000',
'-ac', '1',
wav_file
], capture_output=True, check=True)
return wav_file
except Exception as e:
print(f"[语音识别] 转换错误: {e}")
return file_path
# ==================== 其他 ====================
def _op_status(self, args: List[str]) -> str:
"""查看状态"""
lines = [" 语音识别插件状态"]
# 检查可用引擎
engines = []
try:
import whisper
engines.append("whisper ")
except:
engines.append("whisper (pip install openai-whisper)")
try:
import speech_recognition
engines.append("speech_recognition ")
except:
engines.append("speech_recognition (pip install SpeechRecognition)")
try:
import pyaudio
engines.append("pyaudio (录音支持)")
except:
engines.append("pyaudio (pip install pyaudio)")
lines.append("\n可用引擎:")
for e in engines:
lines.append(f" - {e}")
return "\n".join(lines)
def _op_result(self, args: List[str]) -> str:
"""获取上次识别结果"""
if os.path.exists(RESULT_FILE):
try:
with open(RESULT_FILE, "r", encoding="utf-8") as f:
return f.read()
except:
pass
return "暂无识别结果"
if __name__ == "__main__":
plugin = Liugin()
print("语音识别插件")
print(f"临时目录: {plugin.temp_dir}")
print("\n" + plugin.usage)
+523
View File
@@ -0,0 +1,523 @@
"""
子 Agent 插件 - 多 Agent 协作系统
主 AI 可以创建子 Agent、下达指令、获取结果
"""
import os
import json
import uuid
import time
import threading
from datetime import datetime
from typing import Dict, List, Optional
class SubAgent:
"""单个子 Agent 实例"""
def __init__(self, agent_id: str, name: str, task: str, system_prompt: str):
self.id = agent_id
self.name = name
self.task = task
self.status = "idle" # idle / running / done / failed
self.system_prompt = system_prompt
self.conversation = [] # 独立对话历史
self.result = None
self.error = None
self.created_at = datetime.now().strftime("%H:%M:%S")
self.finished_at = None
self.steps = 0 # 已执行步数
self.max_steps = 50
self._lock = threading.Lock()
def to_dict(self):
return {
"id": self.id,
"name": self.name,
"task": self.task,
"status": self.status,
"steps": self.steps,
"created_at": self.created_at,
"finished_at": self.finished_at,
"has_result": self.result is not None,
"has_error": self.error is not None,
}
class Liugin:
"""子 Agent 系统 - 创建、管理、协作"""
def __init__(self):
self.usage = """子 Agent 系统 - 多 Agent 协作
管理:
spawn <名称> <任务描述> - 创建子 Agent 并分配任务
spawn <名称> <任务> --system <提示词> - 创建并指定系统提示词
list - 列出所有子 Agent
status <ID> - 查看子 Agent 状态
result <ID> - 获取子 Agent 执行结果
send <ID> <消息> - 向子 Agent 发送消息
kill <ID> - 终止子 Agent
clean - 清理已完成的 Agent
stats - 统计信息
消息格式 (AI 调用):
{"action": "use_tool", "tool": "sub_agent", "args": "spawn coder 请帮我重构 main.py"}
{"action": "use_tool", "tool": "sub_agent", "args": "result agent_xxx"}
{"action": "use_tool", "tool": "sub_agent", "args": "send agent_xxx 用户要求加上类型提示"}
说明:
- 子 Agent 独立运行,有自己的对话历史
- 子 Agent 可以使用 code_editor、git_tools 等工具
- 主 AI 可以同时管理多个子 Agent
- 子 Agent 完成后通过 result 获取结果
"""
self.cli = None
self.agents: Dict[str, SubAgent] = {}
self._lock = threading.Lock()
def set_cli(self, cli):
self.cli = cli
def get_tool_info(self):
return {
"name": "sub_agent",
"description": "子 Agent 系统 - 创建子 Agent 执行独立任务,支持并行多任务、结果回收、消息传递",
"keywords": ["agent", "子agent", "并行", "任务", "协程", "多agent", "spawn", "sub"],
"usage": self.usage
}
def get_mcp_definition(self):
return {
"name": "sub_agent",
"description": "子 Agent 多任务协作系统",
"inputSchema": {
"type": "object",
"properties": {
"operation": {"type": "string", "description": "操作: spawn/list/status/result/send/kill/clean/stats"},
"name": {"type": "string", "description": "Agent 名称"},
"task": {"type": "string", "description": "任务描述"},
"agent_id": {"type": "string", "description": "Agent ID"},
"message": {"type": "string", "description": "发送的消息"}
},
"required": ["operation"]
}
}
def convert_mcp_args(self, arguments):
op = arguments.get("operation", "")
name = arguments.get("name", "")
task = arguments.get("task", "")
return f"{op} {name} {task}".strip()
# ── 路由 ──
def handle(self, args: str) -> str:
try:
parts = args.strip().split(maxsplit=1)
if not parts:
return "错误:请提供操作类型"
op = parts[0].lower()
rest = parts[1] if len(parts) > 1 else ""
handlers = {
"spawn": self._spawn,
"list": self._list,
"status": self._status,
"result": self._result,
"send": self._send,
"kill": self._kill,
"clean": self._clean,
"stats": self._stats,
}
handler = handlers.get(op)
if not handler:
return f"错误:不支持的操作 '{op}'"
return handler(rest)
except Exception as e:
return f"子 Agent 错误: {str(e)}"
# ── 操作 ──
def _spawn(self, args: str) -> str:
"""创建子 Agent"""
# 解析 --system 参数
system_prompt = ""
if " --system " in args:
args, system_prompt = args.split(" --system ", 1)
system_prompt = system_prompt.strip()
parts = args.strip().split(maxsplit=1)
if len(parts) < 2:
return "错误:格式: spawn <名称> <任务描述>"
name = parts[0].strip()
task = parts[1].strip()
# 生成 ID
agent_id = f"agent_{name}_{uuid.uuid4().hex[:6]}"
# 默认系统提示词
if not system_prompt:
system_prompt = (
f"你是子 Agent「{name}」,负责执行以下任务:\n"
f"{task}\n\n"
"你可以使用 code_editor、git_tools、cmd_executor、file_manager 等工具。\n"
"完成任务后,用以下格式报告结果:\n"
'{"status": "done", "summary": "简要总结", "details": "详细结果"}\n\n'
"请开始执行任务。"
)
agent = SubAgent(agent_id, name, task, system_prompt)
with self._lock:
self.agents[agent_id] = agent
# 在后台线程执行
thread = threading.Thread(target=self._run_agent, args=(agent,), daemon=True)
thread.start()
return (f" 已创建子 Agent\n"
f" ID: {agent_id}\n"
f" 名称: {name}\n"
f" 任务: {task}\n"
f" 状态: running")
def _run_agent(self, agent: SubAgent):
"""在后台执行子 Agent"""
with agent._lock:
agent.status = "running"
try:
engine = None
if self.cli and hasattr(self.cli, 'current_engine'):
engine = self.cli.current_engine
if not engine:
with agent._lock:
agent.status = "failed"
agent.error = "无可用的 AI 引擎"
agent.finished_at = datetime.now().strftime("%H:%M:%S")
return
# 第一轮:让子 Agent 开始执行任务
current_input = agent.task
max_steps = agent.max_steps
while agent.steps < max_steps:
with agent._lock:
if agent.status != "running":
break
agent.steps += 1
# 调用引擎
response = engine.generate_response(
current_input,
system_prompt=agent.system_prompt
)
if not response:
with agent._lock:
agent.status = "failed"
agent.error = "引擎返回空响应"
break
# 记录对话
with agent._lock:
agent.conversation.append({"role": "user", "content": current_input})
agent.conversation.append({"role": "assistant", "content": response})
# 检查是否包含完成信号
if self._is_task_done(response):
with agent._lock:
agent.status = "done"
agent.result = response
break
# 检查是否需要工具调用
tool_result = self._try_execute_tool(response)
if tool_result:
current_input = f"工具执行结果:\n{tool_result}\n\n请继续执行任务。"
else:
with agent._lock:
agent.status = "done"
agent.result = response
break
with agent._lock:
if agent.status == "running":
agent.status = "done"
agent.result = "任务执行结束。"
agent.finished_at = datetime.now().strftime("%H:%M:%S")
except Exception as e:
with agent._lock:
agent.status = "failed"
agent.error = str(e)
agent.finished_at = datetime.now().strftime("%H:%M:%S")
def _is_task_done(self, response: str) -> bool:
"""检查响应中是否包含完成信号"""
done_markers = ['"status": "done"', '"status":"done"',
'任务完成', '已完成', '执行完毕']
return any(m in response for m in done_markers)
def _try_execute_tool(self, response: str) -> Optional[str]:
"""尝试从响应中提取并执行工具调用"""
if not self.cli:
return None
# 查找 JSON 工具调用
import re
# 模式: {"action": "use_tool", "tool": "...", "args": "..."}
patterns = [
r'\{"action"\s*:\s*"use_tool"\s*,\s*"tool"\s*:\s*"([^"]+)"\s*,\s*"args"\s*:\s*"([^"]*(?:\\.[^"]*)*)"\s*\}',
r'\{"action"\s*:\s*"use_tool"\s*,\s*"tool"\s*:\s*"([^"]+)"\s*,\s*"args"\s*:\s*(\{[^}]+\})\s*\}',
]
for pattern in patterns:
match = re.search(pattern, response, re.DOTALL)
if match:
tool_name = match.group(1)
tool_args = match.group(2)
# 反转义
tool_args = tool_args.replace('\\"', '"').replace('\\n', '\n')
# 查找并执行工具
if hasattr(self.cli, 'liugin_manager'):
for tool in self.cli.liugin_manager.tools:
if tool.get('name') == tool_name:
handler = tool.get('handler')
if handler:
try:
result = handler(tool_args)
return result
except Exception as e:
return f"工具执行错误: {e}"
break
# 模式2: 多个 JSON 指令 (数组)
array_match = re.search(r'\[\s*\{.*?\}\s*\]', response, re.DOTALL)
if array_match:
try:
commands = json.loads(array_match.group())
if isinstance(commands, list):
results = []
for cmd in commands:
if isinstance(cmd, dict) and cmd.get('action') == 'use_tool':
tool_name = cmd.get('tool', '')
tool_args = cmd.get('args', '')
if isinstance(tool_args, dict):
tool_args = json.dumps(tool_args, ensure_ascii=False)
for tool in self.cli.liugin_manager.tools:
if tool.get('name') == tool_name:
handler = tool.get('handler')
if handler:
try:
r = handler(tool_args)
results.append(f"[{tool_name}] {r}")
except Exception as e:
results.append(f"[{tool_name}] 错误: {e}")
break
if results:
return '\n'.join(results)
except json.JSONDecodeError:
pass
return None
def _find_agent(self, agent_id: str):
"""线程安全地查找 Agent(精确匹配 + 模糊匹配)"""
with self._lock:
agent = self.agents.get(agent_id)
if not agent:
matches = [a for a in self.agents.values() if agent_id in a.id or agent_id == a.name]
if matches:
agent = matches[0]
return agent
def _list(self, args: str) -> str:
"""列出所有子 Agent"""
with self._lock:
if not self.agents:
return "没有子 Agent"
agents_list = list(self.agents.values())
result = [f"子 Agent 列表 ({len(agents_list)} 个):\n"]
# 按状态分组
running = [a for a in agents_list if a.status == "running"]
done = [a for a in agents_list if a.status == "done"]
failed = [a for a in agents_list if a.status == "failed"]
if running:
result.append(f" 运行中 ({len(running)}):")
for a in running:
result.append(f" {a.id} [{a.name}] 步骤:{a.steps} 任务:{a.task[:40]}")
if done:
result.append(f" 已完成 ({len(done)}):")
for a in done:
result.append(f" {a.id} [{a.name}] 任务:{a.task[:40]}")
if failed:
result.append(f" 失败 ({len(failed)}):")
for a in failed:
result.append(f" {a.id} [{a.name}] 错误:{a.error[:40] if a.error else '?'}")
return '\n'.join(result)
def _status(self, args: str) -> str:
"""查看子 Agent 状态"""
agent_id = args.strip()
if not agent_id:
return "错误:请提供 Agent ID"
agent = self._find_agent(agent_id)
if not agent:
return f"错误:未找到 Agent '{agent_id}'"
lines = [
f"Agent: {agent.id}",
f" 名称: {agent.name}",
f" 任务: {agent.task}",
f" 状态: {agent.status}",
f" 步骤: {agent.steps}",
f" 创建: {agent.created_at}",
]
if agent.finished_at:
lines.append(f" 完成: {agent.finished_at}")
if agent.error:
lines.append(f" 错误: {agent.error}")
lines.append(f" 对话轮次: {len(agent.conversation)}")
return '\n'.join(lines)
def _result(self, args: str) -> str:
"""获取子 Agent 结果"""
agent_id = args.strip()
if not agent_id:
return "错误:请提供 Agent ID"
agent = self._find_agent(agent_id)
if not agent:
return f"错误:未找到 Agent '{agent_id}'"
if agent.status == "running":
return f"Agent {agent.id} 仍在运行中 (步骤 {agent.steps})"
if agent.status == "failed":
return f"Agent {agent.id} 执行失败: {agent.error}"
if agent.result:
result = agent.result
return f"Agent {agent.id} 结果:\n{result}"
return f"Agent {agent.id} 暂无结果"
def _send(self, args: str) -> str:
"""向子 Agent 发送消息"""
parts = args.strip().split(maxsplit=1)
if len(parts) < 2:
return "错误:格式: send <ID> <消息>"
agent_id = parts[0]
message = parts[1]
agent = self._find_agent(agent_id)
if not agent:
return f"错误:未找到 Agent '{agent_id}'"
if agent.status != "running":
return f"Agent {agent.id} 未在运行 (状态: {agent.status})"
# 在后台线程处理消息
thread = threading.Thread(target=self._process_message, args=(agent, message), daemon=True)
thread.start()
return f" 消息已发送给 {agent.id}"
def _process_message(self, agent: SubAgent, message: str, depth: int = 0):
"""处理发送给子 Agent 的消息"""
if depth > 10:
return "递归深度超限"
try:
engine = None
if self.cli and hasattr(self.cli, 'current_engine'):
engine = self.cli.current_engine
if not engine:
return
with agent._lock:
agent.conversation.append({"role": "user", "content": message})
agent.steps += 1
response = engine.generate_response(
message,
system_prompt=agent.system_prompt
)
if response:
with agent._lock:
agent.conversation.append({"role": "assistant", "content": response})
if self._is_task_done(response):
with agent._lock:
agent.status = "done"
agent.result = response
agent.finished_at = datetime.now().strftime("%H:%M:%S")
else:
tool_result = self._try_execute_tool(response)
if tool_result:
# 继续执行
self._process_message(agent, f"工具结果:\n{tool_result}\n请继续。", depth + 1)
except Exception as e:
with agent._lock:
agent.error = str(e)
agent.status = "failed"
agent.finished_at = datetime.now().strftime("%H:%M:%S")
def _kill(self, args: str) -> str:
"""终止子 Agent"""
agent_id = args.strip()
agent = self._find_agent(agent_id)
if not agent:
return f"错误:未找到 Agent '{agent_id}'"
with agent._lock:
agent.status = "failed"
agent.error = "被主 Agent 终止"
agent.finished_at = datetime.now().strftime("%H:%M:%S")
return f" 已终止 {agent.id}"
def _clean(self, args: str) -> str:
"""清理已完成的 Agent"""
with self._lock:
to_remove = [aid for aid, a in self.agents.items()
if a.status in ("done", "failed")]
for aid in to_remove:
del self.agents[aid]
return f" 已清理 {len(to_remove)} 个已完成的 Agent"
def _stats(self, args: str) -> str:
"""统计信息"""
with self._lock:
total = len(self.agents)
running = sum(1 for a in self.agents.values() if a.status == "running")
done = sum(1 for a in self.agents.values() if a.status == "done")
failed = sum(1 for a in self.agents.values() if a.status == "failed")
total_steps = sum(a.steps for a in self.agents.values())
return (f" 子 Agent 统计\n"
f" 总数: {total}\n"
f" 运行中: {running}\n"
f" 已完成: {done}\n"
f" 失败: {failed}\n"
f" 总步数: {total_steps}")
+289
View File
@@ -0,0 +1,289 @@
"""
任务管理插件 - 提供任务追踪和进度管理功能
"""
import json
import os
from datetime import datetime
from colorama import Fore, Style
class Liugin:
"""任务管理插件 - 类似 iFlow CLI 的 todo 功能"""
def __init__(self):
self.usage = """任务管理工具使用方法:
JSON格式示例:
{"action": "use_tool", "tool": "task_manager", "args": "add 创建登录页面"} - 添加新任务
{"action": "use_tool", "tool": "task_manager", "args": "list"} - 列出所有任务
{"action": "use_tool", "tool": "task_manager", "args": "complete 1"} - 完成任务ID为1的任务
{"action": "use_tool", "tool": "task_manager", "args": "in_progress 2"} - 将任务ID为2标记为进行中
{"action": "use_tool", "tool": "task_manager", "args": "delete 3"} - 删除任务ID为3的任务
{"action": "use_tool", "tool": "task_manager", "args": "clear"} - 清除所有已完成的任务
功能说明:
- add <任务描述> - 添加新任务(自动分配ID)
- list - 列出所有任务及其状态
- complete <任务ID> - 标记任务为已完成
- in_progress <任务ID> - 标记任务为进行中
- delete <任务ID> - 删除指定任务
- clear - 清除所有已完成的任务
状态说明:
- pending: 待处理
- in_progress: 进行中
- completed: 已完成
- failed: 失败"""
self.cli = None
self.tasks_file = "tasks.json"
self.tasks = []
self._load_tasks()
def set_cli(self, cli):
"""设置CLI实例引用"""
self.cli = cli
# 注册插件命令
self.cli.register_liugin_command('task', self.command_handler)
self.cli.register_liugin_command('todo', self.command_handler)
def command_handler(self, args):
"""处理 /task 或 /todo 命令"""
return self.handle(args)
def get_tool_info(self):
return {
"name": "task_manager",
"description": "任务管理工具,提供任务追踪和进度管理功能,支持添加、完成、删除任务",
"keywords": ["任务", "task", "todo", "管理", "进度", "追踪", "list", "complete"],
"usage": self.usage
}
def _load_tasks(self):
"""从文件加载任务"""
try:
if os.path.exists(self.tasks_file):
with open(self.tasks_file, 'r', encoding='utf-8') as f:
self.tasks = json.load(f)
except Exception as e:
self.tasks = []
def _save_tasks(self):
"""保存任务到文件"""
try:
with open(self.tasks_file, 'w', encoding='utf-8') as f:
json.dump(self.tasks, f, ensure_ascii=False, indent=2)
except Exception as e:
return f"保存任务失败: {e}"
return None
def get_mcp_definition(self):
return {
"name": "task_manager",
"description": "任务管理工具,支持添加、完成、删除任务",
"inputSchema": {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["add", "done", "delete", "list", "clear"],
"description": "操作类型"
},
"task": {
"type": "string",
"description": "任务内容(add/done/delete 时使用)"
}
},
"required": ["operation"]
}
}
def convert_mcp_args(self, arguments):
op = arguments.get("operation", "")
task = arguments.get("task", "")
return f"{op} {task}".strip()
def handle(self, args):
"""处理任务管理请求"""
try:
parts = args.strip().split()
if not parts:
return self._list_tasks()
operation = parts[0].lower()
if operation == "add":
# 添加任务
if len(parts) < 2:
return "错误:请提供任务描述。格式: add <任务描述>"
task_desc = " ".join(parts[1:])
return self._add_task(task_desc)
elif operation == "list":
# 列出任务
return self._list_tasks()
elif operation == "complete":
# 完成任务
if len(parts) < 2:
return "错误:请提供任务ID。格式: complete <任务ID>"
task_id = parts[1]
return self._update_task_status(task_id, "completed")
elif operation == "in_progress":
# 标记为进行中
if len(parts) < 2:
return "错误:请提供任务ID。格式: in_progress <任务ID>"
task_id = parts[1]
return self._update_task_status(task_id, "in_progress")
elif operation == "delete":
# 删除任务
if len(parts) < 2:
return "错误:请提供任务ID。格式: delete <任务ID>"
task_id = parts[1]
return self._delete_task(task_id)
elif operation == "clear":
# 清除已完成任务
return self._clear_completed()
else:
return f"错误:不支持的操作 '{operation}'。支持的操作有: add, list, complete, in_progress, delete, clear"
except Exception as e:
return f"任务管理操作错误: {str(e)}"
def _add_task(self, description):
"""添加新任务"""
# 使用最大ID+1的方式分配ID,避免删除后ID重复
existing_ids = [int(t.get("id", 0)) for t in self.tasks if t.get("id", "").isdigit()]
task_id = str(max(existing_ids, default=0) + 1)
new_task = {
"id": task_id,
"task": description,
"status": "pending",
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"priority": "medium"
}
self.tasks.append(new_task)
error = self._save_tasks()
if error:
return error
return f" 已添加任务 #{task_id}: {description}"
def _list_tasks(self):
"""列出所有任务"""
if not self.tasks:
return " 当前没有任务"
result = f"\n 任务列表 (共 {len(self.tasks)} 个任务)\n"
result += "=" * 50 + "\n"
pending_count = 0
in_progress_count = 0
completed_count = 0
for task in self.tasks:
status = task.get('status', 'pending')
task_id = task.get('id', '?')
description = task.get('task', '')
created_at = task.get('created_at', '')
# 统计
if status == 'pending':
pending_count += 1
status_icon = ""
status_color = Fore.YELLOW
elif status == 'in_progress':
in_progress_count += 1
status_icon = ""
status_color = Fore.CYAN
elif status == 'completed':
completed_count += 1
status_icon = ""
status_color = Fore.GREEN
else:
status_icon = ""
status_color = Fore.RED
result += f"{status_icon} [{task_id}] {status_color}{status}{Style.RESET_ALL} - {description}\n"
result += f" 创建时间: {created_at}\n\n"
result += "=" * 50 + "\n"
result += f" 统计: {Fore.YELLOW}待处理 {pending_count}{Style.RESET_ALL} | {Fore.CYAN}进行中 {in_progress_count}{Style.RESET_ALL} | {Fore.GREEN}已完成 {completed_count}{Style.RESET_ALL}\n"
return result
def _update_task_status(self, task_id, new_status):
"""更新任务状态"""
for task in self.tasks:
if task.get('id') == task_id:
old_status = task.get('status')
task['status'] = new_status
if new_status == 'completed':
task['completed_at'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
error = self._save_tasks()
if error:
return error
return f" 任务 #{task_id} 状态已更新: {old_status} → {new_status}"
return f" 未找到任务ID: {task_id}"
def _delete_task(self, task_id):
"""删除任务"""
for i, task in enumerate(self.tasks):
if task.get('id') == task_id:
deleted_task = self.tasks.pop(i)
error = self._save_tasks()
if error:
return error
return f" 已删除任务 #{task_id}: {deleted_task.get('task', '')}"
return f" 未找到任务ID: {task_id}"
def _clear_completed(self):
"""清除所有已完成的任务"""
completed_count = len([t for t in self.tasks if t.get('status') == 'completed'])
if completed_count == 0:
return " 没有已完成的任务需要清除"
self.tasks = [t for t in self.tasks if t.get('status') != 'completed']
error = self._save_tasks()
if error:
return error
return f" 已清除 {completed_count} 个已完成的任务"
# 测试函数
def test_plugin():
"""测试插件功能"""
plugin = Liugin()
print("任务管理插件测试:")
print("\n1. 添加任务:")
print(plugin.handle("add 创建登录页面"))
print(plugin.handle("add 实现用户认证"))
print(plugin.handle("add 添加数据库连接"))
print("\n2. 列出任务:")
print(plugin.handle("list"))
print("\n3. 标记任务为进行中:")
print(plugin.handle("in_progress 1"))
print("\n4. 完成任务:")
print(plugin.handle("complete 1"))
print("\n5. 再次列出任务:")
print(plugin.handle("list"))
print("\n6. 清除已完成任务:")
print(plugin.handle("clear"))
print("\n7. 最终任务列表:")
print(plugin.handle("list"))
if __name__ == "__main__":
test_plugin()
+411
View File
@@ -0,0 +1,411 @@
"""
终端模拟器 v2 — AI 的虚拟键盘和屏幕
=====================================
设计理念:AI 面前有一个终端,它能做的事就是:
1. 打字(type)
2. 按键(key)
3. 看屏幕(read)
就这么简单。
"""
import os
import re
import sys
import time
import uuid
import json
import threading
import subprocess
import platform
from collections import deque
_IS_WIN = platform.system() == "Windows"
class Terminal:
"""一个终端 = 一个子进程 + 一个屏幕缓冲"""
def __init__(self, sid: str, shell: str = "", cwd: str = ""):
self.id = sid
self.shell = shell or self._default_shell()
self.cwd = cwd or os.getcwd()
self._proc = None
self._screen = deque(maxlen=5000) # 屏幕行
self._raw = "" # 未处理原始输出
self._lock = threading.Lock()
self._start()
@staticmethod
def _default_shell():
if _IS_WIN:
return os.environ.get("COMSPEC", "cmd.exe")
for s in ["/bin/bash", "/bin/sh"]:
if os.path.isfile(s):
return s
return "/bin/sh"
def _start(self):
env = {**os.environ, "TERM": "dumb", "NO_COLOR": "1",
"PYTHONUNBUFFERED": "1", "PROMPT_COMMAND": ""}
if not _IS_WIN:
base = os.path.basename(self.shell)
if "bash" in base:
env["PS1"] = ""
args = [self.shell, "--norc", "--noprofile"]
elif "python" in base:
args = [self.shell, "-i"]
else:
args = [self.shell]
flags = 0
else:
args = [self.shell, "/K", "chcp 65001 >nul"]
flags = subprocess.CREATE_NO_WINDOW
self._proc = subprocess.Popen(
args, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, bufsize=0, cwd=self.cwd,
env=env, creationflags=flags if _IS_WIN else 0,
)
threading.Thread(target=self._reader, daemon=True).start()
time.sleep(0.3)
def _reader(self):
fd = self._proc.stdout.fileno()
while self._proc.poll() is None:
try:
chunk = os.read(fd, 4096)
if not chunk:
break
text = chunk.decode("utf-8", errors="replace")
with self._lock:
self._raw += text
for line in text.split("\n"):
self._screen.append(line)
except (OSError, ValueError):
break
def alive(self):
return self._proc and self._proc.poll() is None
# ── 核心操作:打字 ──
def type(self, text: str):
"""像人一样打字,每个字符发到 stdin"""
if not self.alive():
return
try:
self._proc.stdin.write(text.encode("utf-8"))
self._proc.stdin.flush()
except (OSError, BrokenPipeError):
pass
# ── 核心操作:按键 ──
_KEYS = {
"enter": "\n", "return": "\n",
"tab": "\t",
"backspace": "\x7f", "bs": "\x7f",
"esc": "\x1b", "escape": "\x1b",
"space": " ",
"delete": "\x1b[3~", "del": "\x1b[3~",
"insert": "\x1b[2~",
"up": "\x1b[A", "down": "\x1b[B",
"right": "\x1b[C", "left": "\x1b[D",
"home": "\x1b[H", "end": "\x1b[F",
"pageup": "\x1b[5~", "pagedown": "\x1b[6~",
"f1": "\x1bOP", "f2": "\x1bOQ", "f3": "\x1bOR", "f4": "\x1bOS",
"f5": "\x1b[15~", "f6": "\x1b[17~", "f7": "\x1b[18~", "f8": "\x1b[19~",
"f9": "\x1b[20~", "f10": "\x1b[21~", "f11": "\x1b[23~", "f12": "\x1b[24~",
}
# Ctrl+ 字母映射
_CTRL = {chr(i): chr(i - 64) for i in range(65, 91)} # A-Z → \x01-\x1a
def key(self, name: str):
"""
按一个键。
支持: enter, tab, esc, up, down, left, right,
ctrl+c, ctrl+d, ctrl+z, ctrl+l, ctrl+a, ctrl+e,
f1-f12, backspace, delete, home, end, space, ...
"""
if not self.alive():
return
name = name.lower().strip()
# Ctrl+ 组合
if name.startswith("ctrl+"):
letter = name[5:]
seq = self._CTRL.get(letter.upper())
if seq:
self._send_raw(seq)
return
# 普通按键
seq = self._KEYS.get(name)
if seq:
self._send_raw(seq)
elif len(name) == 1:
# 单个字符直接发
self._send_raw(name)
def combo(self, *keys):
"""
组合键,如 combo("ctrl", "shift", "t")
目前主要支持 ctrl+单字母
"""
parts = [k.lower() for k in keys]
if "ctrl" in parts and len(parts) == 2:
other = [k for k in parts if k != "ctrl"][0]
self.key(f"ctrl+{other}")
elif "alt" in parts:
# Alt+key → ESC + key
other = [k for k in parts if k != "alt"][0]
self._send_raw("\x1b" + other)
elif "shift" in parts:
other = [k for k in parts if k != "shift"][0]
self._send_raw(other.upper() if len(other) == 1 else other)
def _send_raw(self, data: str):
try:
self._proc.stdin.write(data.encode("utf-8"))
self._proc.stdin.flush()
except (OSError, BrokenPipeError):
pass
# ── 核心操作:看屏幕 ──
def read(self, lines: int = 0) -> str:
"""读取屏幕当前内容"""
with self._lock:
if lines > 0:
buf = list(self._screen)[-lines:]
else:
buf = list(self._screen)
return self._clean("\n".join(buf))
def read_new(self, timeout: float = 1) -> str:
"""等待并读取新输出"""
with self._lock:
self._raw = ""
time.sleep(timeout)
with self._lock:
out = self._raw
self._raw = ""
return self._clean(out)
def wait(self, seconds: float = 1):
"""等待,让终端输出完"""
time.sleep(seconds)
def _clean(self, text: str) -> str:
if not text:
return ""
text = re.sub(r'\x1b\[[0-9;]*[a-zA-Z]', '', text)
text = re.sub(r'\x1b\][^\x07]*\x07', '', text)
text = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f]', '', text)
text = text.replace('\r\n', '\n').replace('\r', '\n')
text = re.sub(r'\n{4,}', '\n\n\n', text)
return text.strip()
def close(self):
try:
if self._proc and self._proc.poll() is None:
self._send_raw("exit\n")
time.sleep(0.2)
if self._proc.poll() is None:
self._proc.terminate()
try:
self._proc.wait(timeout=3)
except subprocess.TimeoutExpired:
self._proc.kill()
except Exception:
pass
self._proc = None
# ═══════════════════════════════════════════════════════════════
# Liugin 插件
# ═══════════════════════════════════════════════════════════════
class Liugin:
"""终端模拟器 v2 — AI 的虚拟键盘和屏幕"""
def __init__(self):
self.cli = None
self._terminals: dict = {}
self._current = "main"
def set_cli(self, cli):
self.cli = cli
def get_tool_info(self):
return {
"name": "terminal",
"description": "终端模拟器 — AI 的虚拟键盘和屏幕。"
"启动终端、打字、按键、看屏幕,像人一样操作。",
"keywords": [
"终端", "terminal", "shell", "键盘", "按键", "打字",
"屏幕", "交互", "REPL", "cmd", "bash", "python",
],
"usage": (
"terminal <操作> [参数]\n\n"
" open [shell] - 打开终端,返回初始画面\n"
" type <文字> - 打字(发送到终端)\n"
" key <按键名> - 按一个键(enter/tab/ctrl+c/方向键...)\n"
" read [行数] - 看屏幕内容\n"
" last [行数] - 看最近N行(默认20,避免上下文爆炸)\n"
" wait [秒] - 等待输出完毕\n"
" switch <ID> - 切换终端\n"
" list - 列出所有终端\n"
" close [ID] - 关闭终端\n\n"
"按键: enter tab esc space backspace delete\n"
" up down left right home end\n"
" ctrl+c ctrl+d ctrl+z ctrl+l ctrl+a ctrl+e\n"
" f1-f12\n\n"
"示例:\n"
" terminal open - 打开 bash\n"
" terminal open python3 - 打开 Python\n"
" terminal type ls -la - 打字\n"
" terminal key enter - 按回车\n"
" terminal read - 看屏幕\n"
" terminal key ctrl+c - 中断"
),
}
def get_mcp_definition(self):
return {
"name": "terminal",
"description": "终端模拟器 — AI 的虚拟键盘和屏幕",
"inputSchema": {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["open", "type", "key", "read", "last", "wait",
"switch", "list", "close"],
},
"text": {"type": "string", "description": "要打的字 / shell路径"},
"key": {"type": "string", "description": "按键名"},
"last": {"type": "integer", "description": "最近N行"},
"seconds": {"type": "number", "description": "等待秒数"},
"session": {"type": "string", "description": "终端 ID"},
},
"required": ["action"],
},
}
def convert_mcp_args(self, arguments):
a = arguments.get("action", "")
parts = [a]
for k in ["text", "key", "lines", "seconds", "session"]:
v = arguments.get(k)
if v is not None:
parts.append(str(v))
return " ".join(parts)
def handle(self, args: str) -> str:
parts = args.strip().split(maxsplit=1)
if not parts:
return self._help()
action = parts[0].lower()
rest = parts[1].strip() if len(parts) > 1 else ""
# 支持 `read 20` 省写
if rest.isdigit() and action in ('read', 'last'):
action, rest = 'last', rest
h = {
"open": self._h_open, "type": self._h_type,
"key": self._h_key, "read": self._h_read,
"last": self._h_last, "wait": self._h_wait,
"switch": self._h_switch, "list": self._h_list,
"close": self._h_close,
}.get(action)
if not h:
return f"不支持 '{action}'\n{self._help()}"
try:
return h(rest)
except Exception as e:
return f"终端错误: {e}"
def _help(self):
return (
"terminal open [shell] - 打开终端\n"
"terminal type <文字> - 打字\n"
"terminal key <按键> - 按键\n"
"terminal read [行数] - 看屏幕\n"
"terminal wait [秒] - 等待\n"
"terminal list - 列出终端\n"
"terminal close - 关闭"
)
def _get(self, sid: str = "") -> Terminal:
sid = sid or self._current
if sid not in self._terminals:
self._terminals[sid] = Terminal(sid)
return self._terminals[sid]
def _h_open(self, rest: str) -> str:
sid = f"s{len(self._terminals) + 1}" if self._terminals else "main"
t = Terminal(sid, shell=rest.strip())
self._terminals[sid] = t
self._current = sid
screen = t.read()
return f"✓ 终端 {sid} 已打开\n{screen}"
def _h_type(self, rest: str) -> str:
if not rest:
return "请提供要打的字"
t = self._get()
t.type(rest)
t.wait(0.3)
return f"← 打字: {rest}"
def _h_key(self, rest: str) -> str:
if not rest:
return "请提供按键名"
t = self._get()
t.key(rest)
t.wait(0.3)
return f"← 按键: {rest}"
def _h_read(self, rest: str) -> str:
t = self._get()
return t.read() or "(屏幕为空)"
def _h_last(self, rest: str) -> str:
t = self._get()
n = int(rest) if rest.strip().isdigit() else 20
return t.read(n) or "(屏幕为空)"
def _h_wait(self, rest: str) -> str:
secs = float(rest) if rest.strip() else 1.0
t = self._get()
t.wait(secs)
return t.read()
def _h_switch(self, rest: str) -> str:
sid = rest.strip()
if sid not in self._terminals:
return f"终端 '{sid}' 不存在"
self._current = sid
return f"✓ 切换到 {sid}"
def _h_list(self, _: str) -> str:
if not self._terminals:
return "无终端"
lines = []
for sid, t in self._terminals.items():
cur = " ← 当前" if sid == self._current else ""
dot = "●" if t.alive() else "○"
lines.append(f" {dot} {sid} [{t.shell}]{cur}")
return "\n".join(lines)
def _h_close(self, rest: str) -> str:
sid = rest.strip() or self._current
if sid not in self._terminals:
return f"终端 '{sid}' 不存在"
self._terminals[sid].close()
del self._terminals[sid]
if self._current == sid:
self._current = next(iter(self._terminals), "main")
return f"✓ 终端 {sid} 已关闭"
+121
View File
@@ -0,0 +1,121 @@
"""
工具搜索助手 - 支持按需发现工具
"""
import json
class Liugin:
"""工具搜索助手 - 允许 AI 按需搜索和发现可用工具"""
def __init__(self):
self.usage = """工具搜索使用方法:
tool_search <关键词> - 根据关键词搜索可用工具
例如:
- tool_search 文件 - 搜索与文件相关的工具
- tool_search 网络 - 搜索与网络相关的工具
- tool_search 图片 - 搜索与图片相关的工具
此工具帮助 AI 在需要特定功能时快速找到合适的工具,
而不是预加载所有工具到上下文中。"""
self.cli = None
def set_cli(self, cli):
"""设置 CLI 实例引用"""
self.cli = cli
def get_tool_info(self):
return {
"name": "tool_search",
"description": "工具搜索助手,支持按需发现和搜索可用工具。当你不确定有哪些工具可用时,可以使用此工具搜索相关功能的工具。",
"keywords": ["搜索", "工具", "发现", "search", "tool", "find", "可用工具", "工具列表"],
"usage": """tool_search 工具使用说明:
JSON格式示例:
{"action": "use_tool", "tool": "tool_search", "args": "文件"} - 搜索与文件相关的工具
{"action": "use_tool", "tool": "tool_search", "args": "网络"} - 搜索与网络相关的工具
{"action": "use_tool", "tool": "tool_search", "args": "图片"} - 搜索与图片相关的工具
功能说明:
- 支持关键词匹配(工具名称、描述、关键词)
- 返回匹配度最高的前 5 个工具
- 包含工具的详细使用说明
- 帮助 AI 快速找到合适的工具而不需要预加载所有工具"""
}
def get_mcp_definition(self):
return {
"name": "tool_search",
"description": "工具搜索助手,搜索可用工具的用法和信息",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "搜索关键词或工具名"
}
},
"required": ["query"]
}
}
def convert_mcp_args(self, arguments):
return arguments.get("query", "")
def handle(self, args):
"""处理工具搜索请求"""
try:
# 解析参数
query = args.strip()
if not query:
return "错误:请提供搜索关键词。格式: tool_search <关键词>"
# 搜索工具
if self.cli and hasattr(self.cli, 'liugin_manager'):
matches = self.cli.liugin_manager.search_tools(query, limit=5)
if not matches:
return f"未找到与 '{query}' 相关的工具。"
# 格式化搜索结果
result_lines = [f"找到 {len(matches)} 个与 '{query}' 相关的工具:\n"]
for i, match in enumerate(matches, 1):
result_lines.append(f"{i}. {match['name']}")
result_lines.append(f" 描述: {match['description']}")
# 显示协议类型
protocol = match.get('protocol', 'liugin')
result_lines.append(f" 协议: {protocol}")
# 关键词
keywords = match.get('keywords', [])
if keywords:
result_lines.append(f" 关键词: {', '.join(keywords)}")
# 根据协议类型显示不同的使用说明
if protocol == 'skill':
# Skill 协议:显示完整指令
instance = match.get('instance')
if instance:
instructions = getattr(instance, 'instructions', '')
if instructions:
result_lines.append(f" 指令:\n{instructions}")
else:
usage = match.get('usage', '')
if usage:
result_lines.append(f" 用法: {usage}")
else:
# Plugin 协议:显示 usage
if match.get('usage'):
usage = match['usage']
result_lines.append(f" 用法: {usage}")
result_lines.append("")
return "\n".join(result_lines)
else:
return "错误:无法访问工具管理器。"
except Exception as e:
return f"工具搜索失败: {str(e)}"
+808
View File
@@ -0,0 +1,808 @@
"""
工作流插件 - YAML 定义的多步骤自动化流程
支持:
- 顺序执行多步骤
- 步骤间变量传递 ({{prev.output}})
- 条件分支 (on_fail / on_success)
- 错误处理 (continue_on_error)
- 状态持久化 (断点续跑)
- 调用其他插件作为步骤
用法:
/workflow run <file.yaml>
/workflow list
/workflow status <workflow_id>
/workflow resume <workflow_id>
/workflow abort <workflow_id>
"""
import json
import os
import re
import time
import uuid
from datetime import datetime
from colorama import Fore, Style
# ── YAML 解析(内置轻量实现,不依赖 PyYAML) ──
def _parse_yaml(text: str) -> dict:
"""轻量 YAML 解析器,支持工作流所需的子集"""
import yaml # 延迟导入,失败时回退
return yaml.safe_load(text)
def _parse_yaml_fallback(text: str) -> dict:
"""内置回退解析器,仅支持简单结构"""
result = {}
current_key = None
current_list = None
current_dict = None
for line in text.split('\n'):
stripped = line.strip()
if not stripped or stripped.startswith('#'):
continue
indent = len(line) - len(line.lstrip())
if indent == 0 and ':' in stripped:
key, _, val = stripped.partition(':')
key = key.strip()
val = val.strip()
if val:
result[key] = _yaml_value(val)
else:
result[key] = None
current_key = key
current_list = None
current_dict = None
elif indent > 0 and stripped.startswith('- '):
if current_key:
if not isinstance(result.get(current_key), list):
result[current_key] = []
item_text = stripped[2:].strip()
if ':' in item_text:
item = {}
k, _, v = item_text.partition(':')
item[k.strip()] = _yaml_value(v.strip())
current_dict = item
result[current_key].append(item)
else:
result[current_key].append(_yaml_value(item_text))
elif indent > 0 and ':' in stripped and current_dict is not None:
key, _, val = stripped.partition(':')
current_dict[key.strip()] = _yaml_value(val.strip())
return result
def _yaml_value(s: str):
"""转换单个 YAML 值"""
if not s:
return None
# 去除引号
if (s.startswith('"') and s.endswith('"')) or (s.startswith("'") and s.endswith("'")):
return s[1:-1]
if s.lower() in ('true', 'yes', 'on'):
return True
if s.lower() in ('false', 'no', 'off'):
return False
if s.lower() in ('null', 'none', '~'):
return None
try:
return int(s)
except ValueError:
pass
try:
return float(s)
except ValueError:
pass
return s
def _load_yaml_file(path: str) -> dict:
"""加载 YAML 文件"""
with open(path, 'r', encoding='utf-8') as f:
text = f.read()
try:
return _parse_yaml(text)
except ImportError:
return _parse_yaml_fallback(text)
# ── 变量替换引擎 ──
def _resolve_vars(text: str, context: dict) -> str:
"""
替换 {{key}} 和 {{key.nested}} 形式的变量
支持:
{{var}} → context['var']
{{step.output}} → context['steps']['step']['output']
{{step.exit_code}} → context['steps']['step']['exit_code']
{{env.HOME}} → os.environ['HOME']
{{args.name}} → context['args']['name']
"""
def replacer(match):
path = match.group(1).strip()
parts = path.split('.')
if parts[0] == 'env':
return os.environ.get('.'.join(parts[1:]), '')
if parts[0] == 'args' and len(parts) > 1:
val = context.get('args', {})
for p in parts[1:]:
if isinstance(val, dict):
val = val.get(p, '')
else:
return ''
return str(val)
if parts[0] == 'steps' and len(parts) >= 3:
step_name = parts[1]
attr = parts[2]
step_data = context.get('steps', {}).get(step_name, {})
val = step_data.get(attr, '')
# 支持 deeper nesting: {{steps.foo.output.field}}
for p in parts[3:]:
if isinstance(val, dict):
val = val.get(p, '')
else:
return ''
return str(val)
# 顶层变量
val = context
for p in parts:
if isinstance(val, dict):
val = val.get(p, '')
else:
return ''
return str(val)
return re.sub(r'\{\{(.+?)\}\}', replacer, text)
def _resolve_value(val, context):
"""递归解析值中的变量(支持 dict/list/str)"""
if isinstance(val, str):
return _resolve_vars(val, context)
elif isinstance(val, dict):
return {k: _resolve_value(v, context) for k, v in val.items()}
elif isinstance(val, list):
return [_resolve_value(item, context) for item in val]
return val
# ── 工作流引擎 ──
class WorkflowEngine:
"""工作流执行引擎"""
def __init__(self, cli_instance=None):
self.cli = cli_instance
self.workflows_dir = "workflows"
self.state_dir = os.path.join(self.workflows_dir, ".state")
os.makedirs(self.state_dir, exist_ok=True)
def _out(self, msg):
"""统一输出"""
print(msg)
def run(self, path: str, args: dict = None) -> str:
"""执行工作流"""
if not os.path.exists(path):
return f"❌ 工作流文件不存在: {path}"
try:
wf = _load_yaml_file(path)
except Exception as e:
return f"❌ 解析工作流文件失败: {e}"
wf_name = wf.get('name', os.path.basename(path).replace('.yaml', '').replace('.yml', ''))
steps = wf.get('steps', [])
if not steps:
return f"❌ 工作流 '{wf_name}' 没有步骤"
wf_id = f"{wf_name}-{uuid.uuid4().hex[:8]}"
context = {
'args': args or {},
'steps': {},
'workflow': {
'id': wf_id,
'name': wf_name,
'path': path,
'started_at': datetime.now().isoformat(),
}
}
# 保存初始状态
self._save_state(wf_id, {
'workflow': wf_name,
'path': path,
'status': 'running',
'current_step': 0,
'context': context,
'started_at': context['workflow']['started_at'],
})
self._out(f"\n{Fore.CYAN}🔄 工作流 '{wf_name}' 开始 (ID: {wf_id}){Style.RESET_ALL}")
self._out(f" 共 {len(steps)} 个步骤\n")
failed = False
for i, step in enumerate(steps):
step_name = step.get('name', f'step_{i+1}')
step_type = step.get('type', 'tool')
condition = step.get('if', None)
continue_on_error = step.get('continue_on_error', False)
# 检查条件
if condition:
resolved_condition = _resolve_vars(condition, context)
if not self._eval_condition(resolved_condition):
self._out(f" {Fore.YELLOW}⏭ [{step_name}] 条件不满足,跳过{Style.RESET_ALL}")
context['steps'][step_name] = {
'output': '', 'exit_code': 0, 'skipped': True
}
continue
# 解析步骤参数
resolved_step = _resolve_value(step, context)
self._out(f" {Fore.CYAN}▶ [{step_name}] {step_type}{Style.RESET_ALL}")
# 更新状态
self._save_state(wf_id, {
'workflow': wf_name,
'path': path,
'status': 'running',
'current_step': i,
'current_step_name': step_name,
'context': context,
'started_at': context['workflow']['started_at'],
})
try:
result = self._execute_step(resolved_step, context)
context['steps'][step_name] = result
if result.get('exit_code', 0) == 0:
output_preview = str(result.get('output', ''))[:100]
self._out(f" {Fore.GREEN}✓ [{step_name}] 完成{Style.RESET_ALL}"
f"{' → ' + output_preview if output_preview else ''}")
else:
failed = True
self._out(f" {Fore.RED}✗ [{step_name}] 失败 (exit={result.get('exit_code')}){Style.RESET_ALL}")
self._out(f" {result.get('error', result.get('output', ''))[:200]}")
if not continue_on_error:
# 检查 on_fail 分支
on_fail = resolved_step.get('on_fail')
if on_fail:
self._out(f" {Fore.YELLOW}→ 执行 on_fail 分支{Style.RESET_ALL}")
self._execute_on_fail(on_fail, context)
break
else:
self._out(f" {Fore.YELLOW}⚠ continue_on_error=true,继续{Style.RESET_ALL}")
except Exception as e:
failed = True
context['steps'][step_name] = {
'output': '', 'exit_code': 1, 'error': str(e)
}
self._out(f" {Fore.RED}✗ [{step_name}] 异常: {e}{Style.RESET_ALL}")
if not continue_on_error:
break
# 完成状态
status = 'failed' if failed else 'completed'
context['workflow']['finished_at'] = datetime.now().isoformat()
context['workflow']['status'] = status
self._save_state(wf_id, {
'workflow': wf_name,
'path': path,
'status': status,
'current_step': len(steps),
'context': context,
'started_at': context['workflow']['started_at'],
'finished_at': context['workflow']['finished_at'],
})
icon = '✅' if not failed else '❌'
self._out(f"\n{icon} 工作流 '{wf_name}' {status} (ID: {wf_id})")
return f"{icon} 工作流 '{wf_name}' {status}"
def resume(self, wf_id: str) -> str:
"""从断点恢复执行工作流"""
state = self._load_state(wf_id)
if not state:
return f"❌ 未找到工作流状态: {wf_id}"
if state.get('status') == 'completed':
return f"ℹ️ 工作流已完成: {wf_id}"
path = state.get('path', '')
if not os.path.exists(path):
return f"❌ 工作流文件不存在: {path}"
try:
wf = _load_yaml_file(path)
except Exception as e:
return f"❌ 解析工作流文件失败: {e}"
steps = wf.get('steps', [])
start_from = state.get('current_step', 0)
context = state.get('context', {})
self._out(f"\n{Fore.CYAN}🔄 恢复工作流 '{wf.get('name', wf_id)}' 从步骤 {start_from + 1}{Style.RESET_ALL}")
failed = False
for i in range(start_from, len(steps)):
step = steps[i]
step_name = step.get('name', f'step_{i+1}')
step_type = step.get('type', 'tool')
continue_on_error = step.get('continue_on_error', False)
resolved_step = _resolve_value(step, context)
self._out(f" {Fore.CYAN}▶ [{step_name}] {step_type}{Style.RESET_ALL}")
try:
result = self._execute_step(resolved_step, context)
context['steps'][step_name] = result
if result.get('exit_code', 0) == 0:
self._out(f" {Fore.GREEN}✓ [{step_name}] 完成{Style.RESET_ALL}")
else:
failed = True
self._out(f" {Fore.RED}✗ [{step_name}] 失败{Style.RESET_ALL}")
if not continue_on_error:
break
except Exception as e:
failed = True
context['steps'][step_name] = {'output': '', 'exit_code': 1, 'error': str(e)}
self._out(f" {Fore.RED}✗ [{step_name}] 异常: {e}{Style.RESET_ALL}")
if not continue_on_error:
break
status = 'failed' if failed else 'completed'
self._save_state(wf_id, {**state, 'status': status, 'current_step': len(steps)})
return f"{'✅' if not failed else '❌'} 工作流 {status}"
def list_workflows(self) -> str:
"""列出所有工作流文件"""
if not os.path.exists(self.workflows_dir):
return "📁 workflows/ 目录不存在"
files = [f for f in os.listdir(self.workflows_dir)
if f.endswith(('.yaml', '.yml')) and not f.startswith('.')]
if not files:
return "📁 workflows/ 目录中没有工作流文件"
result = f"📁 工作流列表 ({len(files)} 个):\n"
for f in sorted(files):
path = os.path.join(self.workflows_dir, f)
try:
wf = _load_yaml_file(path)
name = wf.get('name', f.replace('.yaml', '').replace('.yml', ''))
desc = wf.get('description', '')
steps_count = len(wf.get('steps', []))
result += f" 📄 {f}\n"
result += f" 名称: {name} 步骤: {steps_count}\n"
if desc:
result += f" 描述: {desc}\n"
except Exception:
result += f" 📄 {f} (解析失败)\n"
return result
def status(self, wf_id: str) -> str:
"""查看工作流状态"""
state = self._load_state(wf_id)
if not state:
return f"❌ 未找到工作流: {wf_id}"
ctx = state.get('context', {})
wf_info = ctx.get('workflow', {})
steps_done = ctx.get('steps', {})
lines = [
f"📋 工作流: {state.get('workflow', '?')}",
f" ID: {wf_id}",
f" 状态: {state.get('status', '?')}",
f" 进度: 步骤 {state.get('current_step', 0)}",
f" 开始: {state.get('started_at', '?')}",
]
if state.get('finished_at'):
lines.append(f" 结束: {state['finished_at']}")
if steps_done:
lines.append(f" 已完成步骤:")
for name, data in steps_done.items():
icon = '✓' if data.get('exit_code', 0) == 0 else ('⏭' if data.get('skipped') else '✗')
lines.append(f" {icon} {name}")
return '\n'.join(lines)
def list_states(self) -> str:
"""列出所有工作流运行状态"""
if not os.path.exists(self.state_dir):
return "没有工作流运行记录"
files = [f for f in os.listdir(self.state_dir) if f.endswith('.json')]
if not files:
return "没有工作流运行记录"
result = f"📋 工作流运行记录 ({len(files)} 个):\n"
for f in sorted(files, reverse=True)[:20]:
wf_id = f.replace('.json', '')
try:
with open(os.path.join(self.state_dir, f), 'r') as fh:
state = json.load(fh)
status_icon = {'running': '🔄', 'completed': '✅', 'failed': '❌'}.get(state.get('status'), '❓')
result += f" {status_icon} {wf_id} — {state.get('workflow', '?')} ({state.get('status', '?')})\n"
except Exception:
result += f" ❓ {wf_id}\n"
return result
def abort(self, wf_id: str) -> str:
"""中止工作流"""
state = self._load_state(wf_id)
if not state:
return f"❌ 未找到工作流: {wf_id}"
state['status'] = 'aborted'
self._save_state(wf_id, state)
return f"🛑 工作流已中止: {wf_id}"
# ── 步骤执行 ──
def _execute_step(self, step: dict, context: dict) -> dict:
"""执行单个步骤"""
step_type = step.get('type', 'tool')
if step_type == 'tool':
return self._exec_tool_step(step, context)
elif step_type == 'shell':
return self._exec_shell_step(step, context)
elif step_type == 'set':
return self._exec_set_step(step, context)
elif step_type == 'echo':
return self._exec_echo_step(step, context)
elif step_type == 'wait':
return self._exec_wait_step(step, context)
elif step_type == 'if':
return self._exec_if_step(step, context)
else:
return {'output': '', 'exit_code': 1, 'error': f'未知步骤类型: {step_type}'}
def _exec_tool_step(self, step: dict, context: dict) -> dict:
"""执行插件工具步骤"""
tool_name = step.get('tool', '')
tool_args = step.get('args', '')
if not tool_name:
return {'output': '', 'exit_code': 1, 'error': '未指定 tool 名称'}
if not self.cli:
return {'output': '', 'exit_code': 1, 'error': 'CLI 实例不可用'}
try:
# 获取工具管理器
tool_mgr = getattr(self.cli, 'tool_manager', None) or getattr(self.cli, 'liugin_manager', None)
if not tool_mgr:
return {'output': '', 'exit_code': 1, 'error': '工具管理器不可用'}
# 解析参数
args_str = str(tool_args) if tool_args else ''
# 调用工具
if hasattr(tool_mgr, 'execute'):
result = tool_mgr.execute(tool_name, args_str)
output = result.get('result', '') if isinstance(result, dict) else str(result)
elif hasattr(tool_mgr, 'tools'):
# LiuginManager 兼容
tool = tool_mgr.get_tool_by_name(tool_name)
if tool and 'handler' in tool:
output = tool['handler'](args_str)
else:
return {'output': '', 'exit_code': 1, 'error': f'未找到工具: {tool_name}'}
else:
return {'output': '', 'exit_code': 1, 'error': '无法调用工具'}
return {'output': str(output), 'exit_code': 0}
except Exception as e:
return {'output': '', 'exit_code': 1, 'error': str(e)}
def _exec_shell_step(self, step: dict, context: dict) -> dict:
"""执行 shell 命令步骤"""
import subprocess
cmd = step.get('command', step.get('cmd', ''))
if not cmd:
return {'output': '', 'exit_code': 1, 'error': '未指定 command'}
cwd = step.get('cwd', None)
timeout = step.get('timeout', 60)
try:
result = subprocess.run(
cmd, shell=True, capture_output=True, text=True,
timeout=timeout, cwd=cwd
)
return {
'output': result.stdout.strip(),
'error': result.stderr.strip(),
'exit_code': result.returncode,
}
except subprocess.TimeoutExpired:
return {'output': '', 'exit_code': 1, 'error': f'命令超时 ({timeout}s)'}
except Exception as e:
return {'output': '', 'exit_code': 1, 'error': str(e)}
def _exec_set_step(self, step: dict, context: dict) -> dict:
"""设置变量步骤"""
variables = step.get('variables', step.get('vars', {}))
for key, val in variables.items():
context[key] = _resolve_value(val, context)
return {'output': f'设置 {len(variables)} 个变量', 'exit_code': 0}
def _exec_echo_step(self, step: dict, context: dict) -> dict:
"""打印步骤"""
message = step.get('message', step.get('text', ''))
resolved = _resolve_vars(str(message), context)
self._out(f" 💬 {resolved}")
return {'output': resolved, 'exit_code': 0}
def _exec_wait_step(self, step: dict, context: dict) -> dict:
"""等待步骤"""
seconds = step.get('seconds', step.get('time', 1))
time.sleep(seconds)
return {'output': f'等待 {seconds} 秒', 'exit_code': 0}
def _exec_if_step(self, step: dict, context: dict) -> dict:
"""条件分支步骤"""
condition = step.get('condition', '')
resolved = _resolve_vars(str(condition), context)
if self._eval_condition(resolved):
then_steps = step.get('then', [])
return self._run_sub_steps(then_steps, context)
else:
else_steps = step.get('else', [])
if else_steps:
return self._run_sub_steps(else_steps, context)
return {'output': '条件不满足,无 else 分支', 'exit_code': 0}
def _run_sub_steps(self, steps: list, context: dict) -> dict:
"""执行子步骤列表"""
outputs = []
for sub_step in steps:
result = self._execute_step(sub_step, context)
outputs.append(result.get('output', ''))
if result.get('exit_code', 0) != 0:
return result
return {'output': '\n'.join(outputs), 'exit_code': 0}
def _eval_condition(self, condition: str) -> bool:
"""评估条件表达式"""
condition = condition.strip()
if not condition:
return True
# 简单比较: "value == expected"
for op in ['==', '!=', '>=', '<=', '>', '<']:
if op in condition:
left, _, right = condition.partition(op)
left = left.strip().strip('"').strip("'")
right = right.strip().strip('"').strip("'")
try:
left_n, right_n = float(left), float(right)
if op == '==': return left_n == right_n
if op == '!=': return left_n != right_n
if op == '>=': return left_n >= right_n
if op == '<=': return left_n <= right_n
if op == '>': return left_n > right_n
if op == '<': return left_n < right_n
except ValueError:
if op == '==': return left == right
if op == '!=': return left != right
# 布尔值
if condition.lower() in ('true', '1', 'yes'):
return True
if condition.lower() in ('false', '0', 'no', ''):
return False
# 非空即真
return bool(condition)
def _execute_on_fail(self, on_fail, context: dict):
"""执行 on_fail 处理"""
if isinstance(on_fail, str):
self._out(f" 💬 {on_fail}")
elif isinstance(on_fail, list):
for step in on_fail:
self._execute_step(step, context)
# ── 状态持久化 ──
def _save_state(self, wf_id: str, state: dict):
"""保存工作流状态"""
path = os.path.join(self.state_dir, f"{wf_id}.json")
try:
with open(path, 'w', encoding='utf-8') as f:
json.dump(state, f, ensure_ascii=False, indent=2, default=str)
except Exception as e:
self._out(f"{Fore.YELLOW}⚠ 保存状态失败: {e}{Style.RESET_ALL}")
def _load_state(self, wf_id: str) -> dict:
"""加载工作流状态"""
path = os.path.join(self.state_dir, f"{wf_id}.json")
if not os.path.exists(path):
return {}
try:
with open(path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception:
return {}
# ── Liugin 插件接口 ──
class Liugin:
"""工作流插件"""
def __init__(self):
self.engine = None
self.usage = """工作流工具使用方法:
JSON格式示例:
{"action": "use_tool", "tool": "workflow", "args": "run deploy.yaml"} - 执行工作流
{"action": "use_tool", "tool": "workflow", "args": "list"} - 列出工作流文件
{"action": "use_tool", "tool": "workflow", "args": "states"} - 查看运行记录
{"action": "use_tool", "tool": "workflow", "args": "status <id>"} - 查看状态
{"action": "use_tool", "tool": "workflow", "args": "resume <id>"} - 恢复执行
{"action": "use_tool", "tool": "workflow", "args": "abort <id>"} - 中止工作流
工作流 YAML 格式示例:
name: deploy
description: 部署流程
steps:
- name: lint
type: shell
command: "npm run lint"
- name: test
type: shell
command: "npm test"
continue_on_error: true
- name: build
type: shell
command: "npm run build"
if: "{{test.exit_code}} == 0"
- name: notify
type: echo
message: "构建完成: {{build.output}}"
支持的步骤类型:
- tool: 调用插件工具 (tool, args)
- shell: 执行系统命令 (command, cwd, timeout)
- echo: 打印消息 (message)
- wait: 等待 (seconds)
- set: 设置变量 (variables)
- if: 条件分支 (condition, then, else)
变量引用:
{{steps.step_name.output}} - 步骤输出
{{steps.step_name.exit_code}} - 步骤退出码
{{env.VAR_NAME}} - 环境变量
{{args.param}} - 工作流参数"""
def set_cli(self, cli):
self.cli = cli
self.engine = WorkflowEngine(cli)
cli.register_liugin_command('workflow', self.command_handler)
cli.register_liugin_command('wf', self.command_handler)
def command_handler(self, args):
return self.handle(args)
def get_tool_info(self):
return {
"name": "workflow",
"description": "工作流引擎 - YAML 定义的多步骤自动化流程,支持变量传递、条件分支、断点续跑",
"keywords": ["workflow", "工作流", "流程", "自动化", "pipeline", "deploy", "build", "run"],
"usage": self.usage
}
def get_mcp_definition(self):
return {
"name": "workflow",
"description": "工作流引擎 - 执行、管理 YAML 定义的自动化工作流",
"inputSchema": {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["run", "list", "states", "status", "resume", "abort"],
"description": "操作类型"
},
"target": {
"type": "string",
"description": "工作流文件路径(run) 或工作流ID(status/resume/abort)"
},
"args": {
"type": "object",
"description": "工作流参数 (run 时使用)"
}
},
"required": ["operation"]
}
}
def convert_mcp_args(self, arguments):
op = arguments.get("operation", "")
target = arguments.get("target", "")
args = arguments.get("args", "")
if args and isinstance(args, dict):
args_str = ' '.join(f'{k}={v}' for k, v in args.items())
return f"{op} {target} {args_str}".strip()
return f"{op} {target}".strip()
def handle(self, args):
try:
args = args.strip()
if not args:
return self.usage
parts = args.split(None, 2)
action = parts[0].lower()
if action == 'run':
if len(parts) < 2:
return "❌ 请指定工作流文件: workflow run <file.yaml>"
path = parts[1]
# 解析内联参数
wf_args = {}
if len(parts) > 2:
for part in parts[2].split():
if '=' in part:
k, v = part.split('=', 1)
wf_args[k] = v
return self.engine.run(path, wf_args)
elif action == 'list':
return self.engine.list_workflows()
elif action == 'states':
return self.engine.list_states()
elif action == 'status':
if len(parts) < 2:
return "❌ 请指定工作流 ID: workflow status <id>"
return self.engine.status(parts[1])
elif action == 'resume':
if len(parts) < 2:
return "❌ 请指定工作流 ID: workflow resume <id>"
return self.engine.resume(parts[1])
elif action == 'abort':
if len(parts) < 2:
return "❌ 请指定工作流 ID: workflow abort <id>"
return self.engine.abort(parts[1])
else:
# 默认当作 run
if os.path.exists(action):
return self.engine.run(action)
return f"❌ 未知操作: {action}\n\n{self.usage}"
except Exception as e:
return f"❌ 工作流操作错误: {e}"