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:
commit
78fea7ccdd
57 files changed
+18358
No files matched your search
@@ -0,0 +1 @@
|
||||
"""xcli_core - 兼容层(旧插件依赖)"""
|
||||
@@ -0,0 +1,108 @@
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
from colorama import Fore, Style
|
||||
|
||||
# 配置文件路径
|
||||
CONFIG_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config.json")
|
||||
|
||||
|
||||
def load_config():
|
||||
"""加载配置文件"""
|
||||
try:
|
||||
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except FileNotFoundError:
|
||||
return {"api": {"engines": {}}, "system": {}, "plugins": {}}
|
||||
except json.JSONDecodeError:
|
||||
return {"api": {"engines": {}}, "system": {}, "plugins": {}}
|
||||
|
||||
|
||||
def save_config(config):
|
||||
"""保存配置文件"""
|
||||
try:
|
||||
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(config, f, ensure_ascii=False, indent=2)
|
||||
except Exception as e:
|
||||
print(f"保存配置失败: {e}")
|
||||
|
||||
|
||||
def get_system_config(key, default=None):
|
||||
"""获取系统配置"""
|
||||
config = load_config()
|
||||
return config.get("system", {}).get(key, default)
|
||||
|
||||
|
||||
def set_system_config(key, value):
|
||||
"""设置系统配置"""
|
||||
config = load_config()
|
||||
if "system" not in config:
|
||||
config["system"] = {}
|
||||
config["system"][key] = value
|
||||
save_config(config)
|
||||
|
||||
|
||||
# ── 引擎配置档案管理 ──
|
||||
# 存储结构: config["engine_profiles"][engine_name] = [ {name, base_url, api_key, model}, ... ]
|
||||
|
||||
def get_profiles(engine_name):
|
||||
"""获取指定引擎的所有配置档案"""
|
||||
config = load_config()
|
||||
return config.get("engine_profiles", {}).get(engine_name, [])
|
||||
|
||||
|
||||
def save_profile(engine_name, profile):
|
||||
"""保存/更新一个配置档案(按 name 去重,已存在则覆盖)"""
|
||||
config = load_config()
|
||||
if "engine_profiles" not in config:
|
||||
config["engine_profiles"] = {}
|
||||
profiles = config["engine_profiles"].setdefault(engine_name, [])
|
||||
# 按 name 去重
|
||||
for i, p in enumerate(profiles):
|
||||
if p.get("name") == profile.get("name"):
|
||||
profiles[i] = profile
|
||||
break
|
||||
else:
|
||||
profiles.append(profile)
|
||||
config["engine_profiles"][engine_name] = profiles
|
||||
save_config(config)
|
||||
|
||||
|
||||
def delete_profile(engine_name, profile_name):
|
||||
"""删除指定引擎的一个配置档案"""
|
||||
config = load_config()
|
||||
profiles = config.get("engine_profiles", {}).get(engine_name, [])
|
||||
profiles = [p for p in profiles if p.get("name") != profile_name]
|
||||
if "engine_profiles" not in config:
|
||||
config["engine_profiles"] = {}
|
||||
config["engine_profiles"][engine_name] = profiles
|
||||
save_config(config)
|
||||
|
||||
|
||||
def apply_profile(engine_name, profile):
|
||||
"""将档案应用到 config.json 的 api.engines 节点(下次启动或重载时生效)"""
|
||||
config = load_config()
|
||||
if "api" not in config:
|
||||
config["api"] = {}
|
||||
if "engines" not in config["api"]:
|
||||
config["api"]["engines"] = {}
|
||||
engine_cfg = config["api"]["engines"].setdefault(engine_name, {})
|
||||
if "base_url" in profile:
|
||||
engine_cfg["base_url"] = profile["base_url"]
|
||||
if "api_key" in profile:
|
||||
engine_cfg["api_key"] = profile["api_key"]
|
||||
if "model" in profile:
|
||||
engine_cfg["model"] = profile["model"]
|
||||
save_config(config)
|
||||
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler('xiaoli_cli.log', encoding='utf-8'),
|
||||
logging.StreamHandler() # 同时输出到控制台
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -0,0 +1,87 @@
|
||||
# 常量定义
|
||||
WEB_SERVER_DEFAULT_PORT = 8080
|
||||
MAX_FILE_SIZE = 52428800 # 50MB
|
||||
MAX_VIDEO_DURATION = 10000 # 10秒
|
||||
VIDEO_FRAME_DELAY = 0.04 # 40毫秒
|
||||
LOVE_FILE_PATH = "love.txt"
|
||||
DEFAULT_MAX_HISTORY = 999999 # 无限制对话历史
|
||||
|
||||
import os
|
||||
import sys
|
||||
import importlib.util
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import time
|
||||
import threading
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from colorama import init, Fore, Style
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
import socket
|
||||
import webbrowser
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import asyncio
|
||||
from typing import Optional
|
||||
from io import StringIO
|
||||
|
||||
# ── 第三方依赖(全部 try/except 保护) ──
|
||||
|
||||
try:
|
||||
import requests
|
||||
REQUESTS_AVAILABLE = True
|
||||
except ImportError:
|
||||
requests = None
|
||||
REQUESTS_AVAILABLE = False
|
||||
|
||||
try:
|
||||
from openai import OpenAI
|
||||
OPENAI_AVAILABLE = True
|
||||
except ImportError:
|
||||
OpenAI = None
|
||||
OPENAI_AVAILABLE = False
|
||||
|
||||
try:
|
||||
import numpy as np
|
||||
NUMPY_AVAILABLE = True
|
||||
except ImportError:
|
||||
np = None
|
||||
NUMPY_AVAILABLE = False
|
||||
|
||||
# tkinter 仅在视频播放功能中使用,服务器环境可能不可用
|
||||
try:
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
TKINTER_AVAILABLE = True
|
||||
except ImportError:
|
||||
TKINTER_AVAILABLE = False
|
||||
|
||||
# cv2 仅在视频播放功能中使用
|
||||
try:
|
||||
import cv2
|
||||
CV2_AVAILABLE = True
|
||||
except ImportError:
|
||||
CV2_AVAILABLE = False
|
||||
|
||||
# 图像显示相关(可选)
|
||||
try:
|
||||
from PIL import Image
|
||||
PIL_AVAILABLE = True
|
||||
except ImportError:
|
||||
PIL_AVAILABLE = False
|
||||
|
||||
try:
|
||||
from ascii_magic import AsciiArt
|
||||
ASCII_MAGIC_AVAILABLE = True
|
||||
except ImportError:
|
||||
ASCII_MAGIC_AVAILABLE = False
|
||||
|
||||
# 统一工具管理器 - 支持 Liugin 和 Skill 双协议
|
||||
try:
|
||||
from unified_tool_manager import UnifiedToolManager
|
||||
UNIFIED_TOOL_MANAGER_AVAILABLE = True
|
||||
except ImportError:
|
||||
UNIFIED_TOOL_MANAGER_AVAILABLE = False
|
||||
|
||||
# TUI 模式已移除,全面使用 CLI 模式
|
||||
@@ -0,0 +1,800 @@
|
||||
"""
|
||||
持久化记忆系统
|
||||
- MEMORY.md: 长期记忆(AI 自己维护的精华)
|
||||
- memory/YYYY-MM-DD.md: 每日日记/日志
|
||||
- chat_history/: 自动保存的聊天记录
|
||||
- 支持关键词检索、上下文压缩、AI 写日记
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import time
|
||||
import glob
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Dict, Optional, Tuple
|
||||
|
||||
|
||||
class MemoryManager:
|
||||
"""持久化记忆管理器"""
|
||||
|
||||
# 记忆模式常量
|
||||
MODE_COMPANION = "companion" # 陪伴模式(长期记忆)
|
||||
MODE_WORK = "work" # 工作模式(短期记忆)
|
||||
|
||||
def __init__(self, project_dir: str):
|
||||
self.project_dir = project_dir
|
||||
self.memory_dir = os.path.join(project_dir, "memory")
|
||||
self.memory_file = os.path.join(project_dir, "MEMORY.md")
|
||||
self.chat_dir = os.path.join(project_dir, "chat_history")
|
||||
self.config_file = os.path.join(project_dir, "config.json")
|
||||
self.compress_threshold = 50 # 超过 50 条消息触发压缩
|
||||
self._ensure_dirs()
|
||||
self._memory_mode = self._load_memory_mode()
|
||||
|
||||
def _load_memory_mode(self) -> str:
|
||||
"""加载记忆模式配置"""
|
||||
try:
|
||||
if os.path.exists(self.config_file):
|
||||
with open(self.config_file, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
return config.get("system", {}).get("memory_mode", "")
|
||||
except Exception:
|
||||
pass
|
||||
return ""
|
||||
|
||||
def _save_memory_mode(self, mode: str):
|
||||
"""保存记忆模式配置"""
|
||||
try:
|
||||
config = {}
|
||||
if os.path.exists(self.config_file):
|
||||
with open(self.config_file, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
if "system" not in config:
|
||||
config["system"] = {}
|
||||
config["system"]["memory_mode"] = mode
|
||||
with open(self.config_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(config, f, ensure_ascii=False, indent=2)
|
||||
self._memory_mode = mode
|
||||
except Exception as e:
|
||||
print(f"保存记忆模式配置失败: {e}")
|
||||
|
||||
def get_memory_mode(self) -> str:
|
||||
"""获取当前记忆模式"""
|
||||
return self._memory_mode
|
||||
|
||||
def is_mode_selected(self) -> bool:
|
||||
"""检查是否已选择记忆模式"""
|
||||
return self._memory_mode in [self.MODE_COMPANION, self.MODE_WORK]
|
||||
|
||||
def select_mode(self, mode: str) -> bool:
|
||||
"""选择记忆模式(只能选择一次)"""
|
||||
if self.is_mode_selected():
|
||||
return False # 已经选择过,不能更改
|
||||
if mode not in [self.MODE_COMPANION, self.MODE_WORK]:
|
||||
return False # 无效的模式
|
||||
self._save_memory_mode(mode)
|
||||
return True
|
||||
|
||||
def get_mode_description(self) -> str:
|
||||
"""获取当前模式的描述"""
|
||||
if self._memory_mode == self.MODE_COMPANION:
|
||||
return "陪伴模式(长期记忆):拥有成熟的记忆功能,记住用户的偏好、习惯、历史对话等"
|
||||
elif self._memory_mode == self.MODE_WORK:
|
||||
return "工作模式(短期记忆):每次对话是独立的短期记忆,可通过指令手动存储聊天记录"
|
||||
return "未选择模式"
|
||||
|
||||
def _ensure_dirs(self):
|
||||
"""确保目录存在"""
|
||||
os.makedirs(self.memory_dir, exist_ok=True)
|
||||
os.makedirs(self.chat_dir, exist_ok=True)
|
||||
|
||||
# ══════════════════════════════════════════════
|
||||
# MEMORY.md — 长期记忆
|
||||
# ══════════════════════════════════════════════
|
||||
|
||||
def load_memory(self) -> str:
|
||||
"""读取 MEMORY.md 内容"""
|
||||
if os.path.exists(self.memory_file):
|
||||
try:
|
||||
with open(self.memory_file, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
except Exception:
|
||||
return ""
|
||||
return ""
|
||||
|
||||
def save_memory(self, content: str):
|
||||
"""保存 MEMORY.md"""
|
||||
with open(self.memory_file, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
def append_memory(self, section: str, content: str):
|
||||
"""向 MEMORY.md 的指定 section 追加内容"""
|
||||
existing = self.load_memory()
|
||||
marker = f"## {section}"
|
||||
if marker in existing:
|
||||
# 在该 section 末尾追加
|
||||
parts = existing.split(marker, 1)
|
||||
after = parts[1]
|
||||
# 找到下一个 ## 或文件末尾
|
||||
next_section = after.find("\n## ")
|
||||
if next_section == -1:
|
||||
new_content = parts[0] + marker + after.rstrip() + "\n" + content + "\n"
|
||||
else:
|
||||
new_content = (parts[0] + marker + after[:next_section].rstrip() +
|
||||
"\n" + content + "\n" + after[next_section:])
|
||||
self.save_memory(new_content)
|
||||
else:
|
||||
# 新增 section
|
||||
new_content = existing.rstrip() + f"\n\n{marker}\n{content}\n"
|
||||
self.save_memory(new_content)
|
||||
|
||||
def list_memory_sections(self) -> list:
|
||||
"""列出 MEMORY.md 中所有的 section 标题"""
|
||||
content = self.load_memory()
|
||||
if not content:
|
||||
return []
|
||||
sections = []
|
||||
for line in content.split("\n"):
|
||||
line = line.strip()
|
||||
if line.startswith("## "):
|
||||
sections.append(line[3:].strip())
|
||||
return sections
|
||||
|
||||
def read_memory_section(self, section: str) -> str:
|
||||
"""读取 MEMORY.md 中指定 section 的内容"""
|
||||
content = self.load_memory()
|
||||
if not content:
|
||||
return ""
|
||||
marker = f"## {section}"
|
||||
if marker not in content:
|
||||
return ""
|
||||
parts = content.split(marker, 1)
|
||||
after = parts[1]
|
||||
next_section = after.find("\n## ")
|
||||
if next_section == -1:
|
||||
return after.strip()
|
||||
return after[:next_section].strip()
|
||||
|
||||
def write_memory_section(self, section: str, content: str):
|
||||
"""写入/覆盖 MEMORY.md 中指定 section(如不存在则新建)"""
|
||||
existing = self.load_memory()
|
||||
marker = f"## {section}"
|
||||
if marker in existing:
|
||||
# 替换该 section 的内容
|
||||
parts = existing.split(marker, 1)
|
||||
after = parts[1]
|
||||
next_section = after.find("\n## ")
|
||||
if next_section == -1:
|
||||
new_content = parts[0] + marker + "\n" + content + "\n"
|
||||
else:
|
||||
new_content = (parts[0] + marker + "\n" + content + "\n" + after[next_section:])
|
||||
self.save_memory(new_content)
|
||||
else:
|
||||
# 新增 section
|
||||
new_content = existing.rstrip() + f"\n\n{marker}\n{content}\n"
|
||||
self.save_memory(new_content)
|
||||
|
||||
def delete_memory_section(self, section: str) -> bool:
|
||||
"""删除 MEMORY.md 中指定的 section"""
|
||||
existing = self.load_memory()
|
||||
if not existing:
|
||||
return False
|
||||
marker = f"## {section}"
|
||||
if marker not in existing:
|
||||
return False
|
||||
parts = existing.split(marker, 1)
|
||||
after = parts[1]
|
||||
next_section = after.find("\n## ")
|
||||
if next_section == -1:
|
||||
new_content = parts[0].rstrip() + "\n"
|
||||
else:
|
||||
new_content = parts[0] + after[next_section:]
|
||||
self.save_memory(new_content)
|
||||
return True
|
||||
|
||||
# ══════════════════════════════════════════════
|
||||
# 每日日记 — memory/YYYY-MM-DD.md
|
||||
# ══════════════════════════════════════════════
|
||||
|
||||
def _today_file(self) -> str:
|
||||
"""今天的日记文件路径"""
|
||||
return os.path.join(self.memory_dir, f"{datetime.now().strftime('%Y-%m-%d')}.md")
|
||||
|
||||
def load_today(self) -> str:
|
||||
"""读取今天的日记"""
|
||||
path = self._today_file()
|
||||
if os.path.exists(path):
|
||||
try:
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
except Exception:
|
||||
return ""
|
||||
return ""
|
||||
|
||||
def write_diary(self, content: str, tag: str = ""):
|
||||
"""写日记条目"""
|
||||
path = self._today_file()
|
||||
now = datetime.now().strftime("%H:%M")
|
||||
tag_str = f" [{tag}]" if tag else ""
|
||||
entry = f"\n### {now}{tag_str}\n{content}\n"
|
||||
|
||||
existing = ""
|
||||
if os.path.exists(path):
|
||||
try:
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
existing = f.read()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not existing:
|
||||
date_str = datetime.now().strftime("%Y-%m-%d")
|
||||
existing = f"# {date_str} 开发日志\n"
|
||||
|
||||
with open(path, 'a', encoding='utf-8') as f:
|
||||
f.write(entry)
|
||||
|
||||
def write_ai_diary(self, content: str):
|
||||
"""AI 写日记(自动标记为 AI 观察)"""
|
||||
self.write_diary(content, tag="AI观察")
|
||||
|
||||
def list_diary_files(self) -> List[str]:
|
||||
"""列出所有日记文件"""
|
||||
files = glob.glob(os.path.join(self.memory_dir, "*.md"))
|
||||
files.sort(reverse=True)
|
||||
return [os.path.basename(f) for f in files]
|
||||
|
||||
def load_diary(self, date_str: str) -> str:
|
||||
"""读取指定日期的日记"""
|
||||
path = os.path.join(self.memory_dir, f"{date_str}.md")
|
||||
if os.path.exists(path):
|
||||
try:
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
except Exception:
|
||||
return ""
|
||||
return ""
|
||||
|
||||
# ══════════════════════════════════════════════
|
||||
# 聊天记录 — 自动保存
|
||||
# ══════════════════════════════════════════════
|
||||
|
||||
# ══════════════════════════════════════════════
|
||||
# 聊天记录 — 单文件 JSON 存储
|
||||
# ══════════════════════════════════════════════
|
||||
|
||||
CHAT_STORE_FILE = "chats.json" # 单文件存储
|
||||
|
||||
def _chat_store_path(self) -> str:
|
||||
"""聊天记录单文件路径"""
|
||||
return os.path.join(self.chat_dir, self.CHAT_STORE_FILE)
|
||||
|
||||
def _load_chat_store(self) -> dict:
|
||||
"""加载聊天记录存储"""
|
||||
path = self._chat_store_path()
|
||||
try:
|
||||
if os.path.exists(path):
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
pass
|
||||
return {"chats": []}
|
||||
|
||||
def _save_chat_store(self, store: dict):
|
||||
"""保存聊天记录存储"""
|
||||
path = self._chat_store_path()
|
||||
try:
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
json.dump(store, f, ensure_ascii=False, indent=2, default=str)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def auto_save_chat(self, conversation: list, label: str = ""):
|
||||
"""保存聊天记录到单文件存储"""
|
||||
if not conversation:
|
||||
return
|
||||
store = self._load_chat_store()
|
||||
now = datetime.now()
|
||||
chat_name = label or now.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
chat_entry = {
|
||||
"name": chat_name,
|
||||
"timestamp": now.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"label": label,
|
||||
"message_count": len(conversation),
|
||||
"conversation": conversation
|
||||
}
|
||||
|
||||
# 按名称去重:如果同名已存在则覆盖
|
||||
chats = store.get("chats", [])
|
||||
for i, c in enumerate(chats):
|
||||
if c.get("name") == chat_name:
|
||||
chats[i] = chat_entry
|
||||
break
|
||||
else:
|
||||
chats.append(chat_entry)
|
||||
|
||||
store["chats"] = chats
|
||||
self._save_chat_store(store)
|
||||
|
||||
def list_chats(self, limit: int = 20) -> List[Dict]:
|
||||
"""列出最近的聊天记录"""
|
||||
store = self._load_chat_store()
|
||||
chats = store.get("chats", [])
|
||||
# 按时间倒序
|
||||
chats_sorted = sorted(chats, key=lambda c: c.get("timestamp", ""), reverse=True)
|
||||
results = []
|
||||
for c in chats_sorted[:limit]:
|
||||
results.append({
|
||||
"name": c.get("name", ""),
|
||||
"time": c.get("timestamp", ""),
|
||||
"size_kb": round(len(json.dumps(c, ensure_ascii=False)) / 1024, 1),
|
||||
})
|
||||
return results
|
||||
|
||||
def load_chat(self, name: str) -> Optional[list]:
|
||||
"""加载指定的聊天记录(按名称查找)"""
|
||||
store = self._load_chat_store()
|
||||
for c in store.get("chats", []):
|
||||
if c.get("name") == name:
|
||||
return c.get("conversation", [])
|
||||
return None
|
||||
|
||||
def delete_chat(self, name: str) -> bool:
|
||||
"""删除指定的聊天记录"""
|
||||
store = self._load_chat_store()
|
||||
chats = store.get("chats", [])
|
||||
new_chats = [c for c in chats if c.get("name") != name]
|
||||
if len(new_chats) == len(chats):
|
||||
return False
|
||||
store["chats"] = new_chats
|
||||
self._save_chat_store(store)
|
||||
return True
|
||||
|
||||
def search_chats(self, keyword: str, limit: int = 10) -> List[Dict]:
|
||||
"""在聊天记录中搜索关键词"""
|
||||
keyword_lower = keyword.lower()
|
||||
store = self._load_chat_store()
|
||||
results = []
|
||||
for c in store.get("chats", []):
|
||||
for msg in c.get("conversation", []):
|
||||
content = msg.get("content", "")
|
||||
if isinstance(content, str) and keyword_lower in content.lower():
|
||||
results.append({
|
||||
"name": c.get("name", ""),
|
||||
"time": c.get("timestamp", ""),
|
||||
"snippet": content[:120],
|
||||
"role": msg.get("role", ""),
|
||||
})
|
||||
break # 每条记录只匹配一次
|
||||
if len(results) >= limit:
|
||||
break
|
||||
return results
|
||||
|
||||
# ══════════════════════════════════════════════
|
||||
# 关键词检索 — 全局记忆搜索
|
||||
# ══════════════════════════════════════════════
|
||||
|
||||
def search_memory(self, keyword: str, limit: int = 20) -> List[Dict]:
|
||||
"""
|
||||
在所有记忆文件中搜索关键词
|
||||
搜索范围: MEMORY.md + memory/*.md + chat_history/*.json
|
||||
"""
|
||||
keyword_lower = keyword.lower()
|
||||
results = []
|
||||
|
||||
# 搜索 MEMORY.md
|
||||
if os.path.exists(self.memory_file):
|
||||
results.extend(self._search_file(
|
||||
self.memory_file, keyword_lower, "MEMORY.md", limit - len(results)
|
||||
))
|
||||
|
||||
# 搜索日记文件
|
||||
for md_file in sorted(glob.glob(os.path.join(self.memory_dir, "*.md")), reverse=True):
|
||||
if len(results) >= limit:
|
||||
break
|
||||
name = f"memory/{os.path.basename(md_file)}"
|
||||
results.extend(self._search_file(
|
||||
md_file, keyword_lower, name, limit - len(results)
|
||||
))
|
||||
|
||||
# 搜索聊天记录(单文件存储)
|
||||
store = self._load_chat_store()
|
||||
for c in store.get("chats", []):
|
||||
if len(results) >= limit:
|
||||
break
|
||||
name = f"chat/{c.get('name', '')}"
|
||||
for msg in c.get("conversation", []):
|
||||
content = msg.get("content", "")
|
||||
if isinstance(content, str) and keyword_lower in content.lower():
|
||||
results.append({
|
||||
"source": name,
|
||||
"line": 0,
|
||||
"snippet": content[:120],
|
||||
})
|
||||
break
|
||||
|
||||
return results[:limit]
|
||||
|
||||
def _search_file(self, path: str, keyword: str, source: str, limit: int) -> List[Dict]:
|
||||
"""在单个文件中搜索"""
|
||||
results = []
|
||||
try:
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
for i, line in enumerate(lines):
|
||||
if keyword in line.lower():
|
||||
snippet = line.strip()[:120]
|
||||
results.append({
|
||||
"source": source,
|
||||
"line": i + 1,
|
||||
"snippet": snippet,
|
||||
})
|
||||
if len(results) >= limit:
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
return results
|
||||
|
||||
def _search_json_file(self, path: str, keyword: str, source: str, limit: int) -> List[Dict]:
|
||||
"""在 JSON 聊天记录中搜索"""
|
||||
results = []
|
||||
try:
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
for msg in data.get("conversation", []):
|
||||
content = msg.get("content", "")
|
||||
if isinstance(content, str) and keyword in content.lower():
|
||||
results.append({
|
||||
"source": source,
|
||||
"role": msg.get("role", ""),
|
||||
"snippet": content[:120],
|
||||
})
|
||||
if len(results) >= limit:
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
return results
|
||||
|
||||
# ══════════════════════════════════════════════
|
||||
# 上下文压缩
|
||||
# ══════════════════════════════════════════════
|
||||
|
||||
def should_compress(self, conversation: list) -> bool:
|
||||
"""判断是否需要压缩上下文"""
|
||||
return len(conversation) > self.compress_threshold
|
||||
|
||||
def compress_context(self, conversation: list, engine=None) -> list:
|
||||
"""
|
||||
压缩对话历史
|
||||
策略: 保留最近 10 条 + 用 AI 总结前面的内容
|
||||
"""
|
||||
if len(conversation) <= 10:
|
||||
return conversation
|
||||
|
||||
keep_recent = 10
|
||||
to_compress = conversation[:-keep_recent]
|
||||
recent = conversation[-keep_recent:]
|
||||
|
||||
# 尝试用 AI 生成摘要
|
||||
summary = self._ai_summarize(to_compress, engine)
|
||||
|
||||
# 构建压缩后的历史
|
||||
compressed = [{
|
||||
"role": "system",
|
||||
"content": f"[上下文压缩] 以下是之前 {len(to_compress)} 条对话的摘要:\n{summary}"
|
||||
}]
|
||||
compressed.extend(recent)
|
||||
return compressed
|
||||
|
||||
def _ai_summarize(self, messages: list, engine=None) -> str:
|
||||
"""用 AI 生成对话摘要"""
|
||||
if not engine:
|
||||
return self._simple_summarize(messages)
|
||||
|
||||
# 构建摘要请求
|
||||
conversation_text = ""
|
||||
for msg in messages:
|
||||
role = msg.get("role", "unknown")
|
||||
content = msg.get("content", "")
|
||||
if isinstance(content, str):
|
||||
conversation_text += f"[{role}]: {content[:200]}\n"
|
||||
|
||||
prompt = (
|
||||
"请用简洁的中文总结以下对话的关键信息(工具调用、代码修改、重要决策)。"
|
||||
"控制在 200 字以内。\n\n"
|
||||
f"{conversation_text}"
|
||||
)
|
||||
|
||||
try:
|
||||
summary = engine.generate_response(
|
||||
prompt,
|
||||
system_prompt="你是摘要生成器。只输出摘要,不要多余内容。"
|
||||
)
|
||||
if summary and len(summary) > 10:
|
||||
return summary
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return self._simple_summarize(messages)
|
||||
|
||||
def _simple_summarize(self, messages: list) -> str:
|
||||
"""简单摘要(不依赖 AI)"""
|
||||
topics = []
|
||||
tool_calls = []
|
||||
for msg in messages:
|
||||
content = msg.get("content", "")
|
||||
role = msg.get("role", "")
|
||||
if role == "user" and isinstance(content, str):
|
||||
# 提取用户请求
|
||||
if len(content) < 100:
|
||||
topics.append(content)
|
||||
else:
|
||||
topics.append(content[:50] + "...")
|
||||
if role == "assistant" and isinstance(content, str):
|
||||
# 提取工具调用
|
||||
if '"action": "use_tool"' in content or '"tool":' in content:
|
||||
try:
|
||||
# 尝试解析工具名
|
||||
for part in content.split('"tool":'):
|
||||
if len(part) > 2:
|
||||
tool_name = part.split('"')[1] if '"' in part else ""
|
||||
if tool_name and tool_name not in tool_calls:
|
||||
tool_calls.append(tool_name)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
summary_parts = []
|
||||
if topics:
|
||||
summary_parts.append(f"讨论了 {len(topics)} 个话题: {'; '.join(topics[:5])}")
|
||||
if tool_calls:
|
||||
summary_parts.append(f"使用了工具: {', '.join(tool_calls[:8])}")
|
||||
summary_parts.append(f"共 {len(messages)} 条对话")
|
||||
|
||||
return "。".join(summary_parts)
|
||||
|
||||
# ══════════════════════════════════════════════
|
||||
# 构建记忆上下文(注入系统提示词)
|
||||
# ══════════════════════════════════════════════
|
||||
|
||||
def build_memory_context(self) -> str:
|
||||
"""构建记忆上下文,用于注入系统提示词"""
|
||||
parts = []
|
||||
|
||||
# 加载 MEMORY.md
|
||||
memory = self.load_memory()
|
||||
if memory:
|
||||
parts.append(f"【长期记忆 MEMORY.md】\n{memory[:2000]}")
|
||||
|
||||
# 加载今天的日记
|
||||
today = self.load_today()
|
||||
if today:
|
||||
parts.append(f"【今日日志】\n{today[:1000]}")
|
||||
|
||||
if not parts:
|
||||
return ""
|
||||
|
||||
return (
|
||||
"\n\n---\n"
|
||||
"以下是你的持久化记忆,帮助你了解之前的工作和上下文:\n\n"
|
||||
+ "\n\n".join(parts)
|
||||
+ "\n---\n"
|
||||
)
|
||||
|
||||
# ══════════════════════════════════════════════
|
||||
# 工具接口(供 AI 调用)
|
||||
# ══════════════════════════════════════════════
|
||||
|
||||
def handle_tool(self, args: str) -> str:
|
||||
"""处理 memory 工具调用"""
|
||||
if not args or not args.strip():
|
||||
return self._tool_help()
|
||||
|
||||
parts = args.strip().split(None, 1)
|
||||
action = parts[0].lower()
|
||||
rest = parts[1].strip() if len(parts) > 1 else ""
|
||||
|
||||
if action == "write":
|
||||
if not rest:
|
||||
return "用法: memory write <内容>"
|
||||
self.write_diary(rest)
|
||||
return f"日记已写入 {datetime.now().strftime('%Y-%m-%d')}"
|
||||
|
||||
elif action == "diary":
|
||||
if not rest:
|
||||
return "用法: memory diary <AI观察内容>"
|
||||
self.write_ai_diary(rest)
|
||||
return f"AI 日记已写入"
|
||||
|
||||
elif action == "remember":
|
||||
if not rest:
|
||||
return "用法: memory remember <要记住的内容>"
|
||||
self.append_memory("备忘", f"- {rest}")
|
||||
return f"已记录到 MEMORY.md"
|
||||
|
||||
elif action == "search":
|
||||
if not rest:
|
||||
return "用法: memory search <关键词>"
|
||||
results = self.search_memory(rest)
|
||||
if not results:
|
||||
return f"未找到与 '{rest}' 相关的记忆"
|
||||
lines = [f"搜索 '{rest}' 的结果:"]
|
||||
for r in results:
|
||||
source = r.get("source", "")
|
||||
snippet = r.get("snippet", "")
|
||||
role = r.get("role", "")
|
||||
prefix = f"[{role}] " if role else ""
|
||||
lines.append(f" 📄 {source}: {prefix}{snippet}")
|
||||
return "\n".join(lines)
|
||||
|
||||
elif action == "context":
|
||||
ctx = self.build_memory_context()
|
||||
return ctx if ctx else "当前没有记忆上下文"
|
||||
|
||||
elif action == "diary_list":
|
||||
files = self.list_diary_files()
|
||||
if not files:
|
||||
return "还没有日记文件"
|
||||
return "日记文件:\n" + "\n".join(f" 📅 {f}" for f in files[:20])
|
||||
|
||||
elif action == "diary_read":
|
||||
if not rest:
|
||||
return "用法: memory diary_read <日期,如 2026-04-27>"
|
||||
content = self.load_diary(rest)
|
||||
return content if content else f"没有找到 {rest} 的日记"
|
||||
|
||||
elif action == "chat_list":
|
||||
chats = self.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)
|
||||
|
||||
elif action == "chat_search":
|
||||
if not rest:
|
||||
return "用法: memory chat_search <关键词>"
|
||||
results = self.search_chats(rest)
|
||||
if not results:
|
||||
return f"未在聊天记录中找到 '{rest}'"
|
||||
lines = [f"聊天记录搜索 '{rest}':"]
|
||||
for r in results:
|
||||
lines.append(f" 💬 {r['file']} [{r['role']}]: {r['snippet']}")
|
||||
return "\n".join(lines)
|
||||
|
||||
else:
|
||||
return f"未知操作: {action}\n" + self._tool_help()
|
||||
|
||||
def _tool_help(self) -> str:
|
||||
return """memory 工具用法:
|
||||
memory write <内容> - 写日记
|
||||
memory diary <内容> - AI 写观察日记
|
||||
memory remember <内容> - 记录到长期记忆 (MEMORY.md)
|
||||
memory search <关键词> - 搜索所有记忆
|
||||
memory context - 查看当前记忆上下文
|
||||
memory diary_list - 列出所有日记
|
||||
memory diary_read <日期> - 读取指定日期日记
|
||||
memory chat_list - 列出聊天记录
|
||||
memory chat_search <关键词> - 在聊天记录中搜索"""
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════
|
||||
# 记忆插件(注册到插件系统)
|
||||
# ══════════════════════════════════════════════
|
||||
|
||||
def save_conversation_by_mode(self, conversation: list, label: str = ""):
|
||||
"""根据模式保存对话"""
|
||||
if self._memory_mode == self.MODE_COMPANION:
|
||||
# 陪伴模式:自动保存到长期记忆
|
||||
self.auto_save_chat(conversation, label)
|
||||
# 提取关键信息保存到MEMORY.md
|
||||
self._extract_and_save_key_info(conversation)
|
||||
elif self._memory_mode == self.MODE_WORK:
|
||||
# 工作模式:自动保存聊天记录
|
||||
self.auto_save_chat(conversation, label)
|
||||
|
||||
|
||||
def _extract_and_save_key_info(self, conversation: list):
|
||||
"""从对话中提取关键信息保存到MEMORY.md(陪伴模式专用)"""
|
||||
# 这里可以添加AI提取关键信息的逻辑
|
||||
# 暂时简单实现:保存最后一条助手消息
|
||||
for msg in reversed(conversation):
|
||||
if msg.get("role") == "assistant":
|
||||
content = msg.get("content", "")
|
||||
if content and len(content) > 20:
|
||||
self.append_memory("对话记录", f"- {content[:200]}")
|
||||
break
|
||||
|
||||
|
||||
def load_context_by_mode(self, conversation: list) -> list:
|
||||
"""根据模式加载上下文"""
|
||||
if self._memory_mode == self.MODE_COMPANION:
|
||||
# 陪伴模式:加载长期记忆作为上下文
|
||||
memory_content = self.load_memory()
|
||||
if memory_content:
|
||||
context_msg = {"role": "system", "content": f"长期记忆:{memory_content}"}
|
||||
return [context_msg] + conversation
|
||||
return conversation
|
||||
elif self._memory_mode == self.MODE_WORK:
|
||||
# 工作模式:只返回当前对话
|
||||
return conversation
|
||||
return conversation
|
||||
|
||||
class Liugin:
|
||||
"""记忆系统插件 — AI 可读写记忆、搜索历史"""
|
||||
|
||||
def __init__(self):
|
||||
self.cli = None
|
||||
self.memory = None
|
||||
self.usage = """记忆系统工具使用方法:
|
||||
|
||||
JSON格式示例:
|
||||
{"action": "use_tool", "tool": "memory", "args": "write 今天完成了v5.2的开发"} - 写日记
|
||||
{"action": "use_tool", "tool": "memory", "args": "diary 用户偏好使用中文交流"} - AI观察日记
|
||||
{"action": "use_tool", "tool": "memory", "args": "remember 项目使用MIT协议"} - 记到长期记忆
|
||||
{"action": "use_tool", "tool": "memory", "args": "search 定时任务"} - 搜索所有记忆
|
||||
{"action": "use_tool", "tool": "memory", "args": "context"} - 查看记忆上下文
|
||||
{"action": "use_tool", "tool": "memory", "args": "diary_list"} - 列出日记
|
||||
{"action": "use_tool", "tool": "memory", "args": "diary_read 2026-04-27"} - 读日记
|
||||
{"action": "use_tool", "tool": "memory", "args": "chat_list"} - 列出聊天记录
|
||||
{"action": "use_tool", "tool": "memory", "args": "chat_search 关键词"} - 搜聊天记录
|
||||
|
||||
功能说明:
|
||||
- write: 写当日日记
|
||||
- diary: AI 自己写观察日记
|
||||
- remember: 写入 MEMORY.md 长期记忆
|
||||
- search: 全局关键词搜索(日记+记忆+聊天记录)
|
||||
- context: 查看当前注入的上下文
|
||||
- diary_list / diary_read: 管理日记
|
||||
- chat_list / chat_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 实例上供其他模块使用
|
||||
cli.memory_manager = self.memory
|
||||
# 注册命令
|
||||
self.cli.register_liugin_command('memory', self.command_handler)
|
||||
|
||||
def command_handler(self, args):
|
||||
return self.handle(args)
|
||||
|
||||
def get_tool_info(self):
|
||||
return {
|
||||
"name": "memory",
|
||||
"description": "持久化记忆系统。AI 可写日记、记录长期记忆、搜索历史记忆和聊天记录。支持关键词检索。",
|
||||
"keywords": ["记忆", "memory", "日记", "diary", "搜索", "search", "历史", "history", "remember"],
|
||||
"usage": self.usage
|
||||
}
|
||||
|
||||
def handle(self, args: str) -> str:
|
||||
if not self.memory:
|
||||
return "记忆系统未初始化"
|
||||
return self.memory.handle_tool(args)
|
||||
|
||||
def get_mcp_definition(self):
|
||||
return {
|
||||
"name": "memory",
|
||||
"description": "持久化记忆系统",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["write", "diary", "remember", "search", "context",
|
||||
"diary_list", "diary_read", "chat_list", "chat_search"],
|
||||
"description": "操作类型"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "内容/关键词"
|
||||
}
|
||||
},
|
||||
"required": ["action"]
|
||||
}
|
||||
}
|
||||
# ══════════════════════════════════════════════
|
||||
# 模式特定的方法
|
||||
# ══════════════════════════════════════════════
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
"""
|
||||
跨平台系统通知模块
|
||||
当 AI 完成任务时,发送系统通知提醒用户
|
||||
支持 Windows / macOS / Linux
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import threading
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class NotificationManager:
|
||||
"""跨平台系统通知管理器"""
|
||||
|
||||
def __init__(self):
|
||||
self.enabled = True
|
||||
self._platform = self._detect_platform()
|
||||
self._last_title = ""
|
||||
self._last_message = ""
|
||||
self._cooldown_seconds = 2 # 防止重复通知的冷却时间
|
||||
self._last_notify_time = 0
|
||||
|
||||
@staticmethod
|
||||
def _detect_platform() -> str:
|
||||
"""检测当前平台"""
|
||||
if os.name == 'nt':
|
||||
return 'windows'
|
||||
elif sys.platform == 'darwin':
|
||||
return 'macos'
|
||||
else:
|
||||
return 'linux'
|
||||
|
||||
def set_enabled(self, enabled: bool):
|
||||
"""启用/禁用通知"""
|
||||
self.enabled = enabled
|
||||
|
||||
def notify(self, title: str, message: str, force: bool = False):
|
||||
"""
|
||||
发送系统通知
|
||||
|
||||
Args:
|
||||
title: 通知标题
|
||||
message: 通知内容
|
||||
force: 是否忽略冷却时间强制发送
|
||||
"""
|
||||
if not self.enabled:
|
||||
return
|
||||
|
||||
# 防止重复通知
|
||||
import time
|
||||
now = time.time()
|
||||
if (not force and
|
||||
title == self._last_title and
|
||||
message == self._last_message and
|
||||
now - self._last_notify_time < self._cooldown_seconds):
|
||||
return
|
||||
|
||||
self._last_title = title
|
||||
self._last_message = message
|
||||
self._last_notify_time = now
|
||||
|
||||
# 在后台线程发送,不阻塞主流程
|
||||
thread = threading.Thread(
|
||||
target=self._send_notification,
|
||||
args=(title, message),
|
||||
daemon=True
|
||||
)
|
||||
thread.start()
|
||||
|
||||
def notify_task_complete(self, task_summary: str = ""):
|
||||
"""通知任务完成的快捷方法"""
|
||||
title = "小狸 Pro-CLI"
|
||||
if task_summary:
|
||||
# 截断过长的摘要
|
||||
if len(task_summary) > 80:
|
||||
task_summary = task_summary[:77] + "..."
|
||||
message = f"任务已完成: {task_summary}"
|
||||
else:
|
||||
message = "任务已完成 ✓"
|
||||
|
||||
self.notify(title, message)
|
||||
|
||||
def notify_tool_complete(self, tool_name: str, result_summary: str = ""):
|
||||
"""通知工具执行完成"""
|
||||
title = "小狸 Pro-CLI"
|
||||
if result_summary:
|
||||
if len(result_summary) > 60:
|
||||
result_summary = result_summary[:57] + "..."
|
||||
message = f"{tool_name}: {result_summary}"
|
||||
else:
|
||||
message = f"{tool_name} 执行完成 ✓"
|
||||
|
||||
self.notify(title, message)
|
||||
|
||||
# ── 平台实现 ──
|
||||
|
||||
def _send_notification(self, title: str, message: str):
|
||||
"""根据平台发送通知"""
|
||||
try:
|
||||
if self._platform == 'windows':
|
||||
self._notify_windows(title, message)
|
||||
elif self._platform == 'macos':
|
||||
self._notify_macos(title, message)
|
||||
else:
|
||||
self._notify_linux(title, message)
|
||||
except Exception:
|
||||
# 通知失败不应影响正常流程
|
||||
pass
|
||||
|
||||
def _notify_windows(self, title: str, message: str):
|
||||
"""Windows 通知 — 使用 PowerShell(win10toast 在 Python 3.12+ 上不兼容)"""
|
||||
# 方法1: PowerShell BurntToast 模块
|
||||
try:
|
||||
ps_script = f'''
|
||||
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null
|
||||
$template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)
|
||||
$textNodes = $template.GetElementsByTagName("text")
|
||||
$textNodes.Item(0).AppendChild($template.CreateTextNode("{title}")) > $null
|
||||
$textNodes.Item(1).AppendChild($template.CreateTextNode("{message}")) > $null
|
||||
$toast = [Windows.UI.Notifications.ToastNotification]::new($template)
|
||||
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("{title}").Show($toast)
|
||||
'''
|
||||
subprocess.run(
|
||||
["powershell", "-ExecutionPolicy", "Bypass", "-Command", ps_script],
|
||||
capture_output=True, timeout=5
|
||||
)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 方法2: PowerShell 简单弹窗 (最后降级)
|
||||
try:
|
||||
escaped_msg = message.replace("'", "''")
|
||||
escaped_title = title.replace("'", "''")
|
||||
ps_cmd = (
|
||||
f"Add-Type -AssemblyName System.Windows.Forms; "
|
||||
f"[System.Windows.Forms.MessageBox]::Show('{escaped_msg}', '{escaped_title}')"
|
||||
)
|
||||
subprocess.run(
|
||||
["powershell", "-Command", ps_cmd],
|
||||
capture_output=True, timeout=5
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _notify_macos(self, title: str, message: str):
|
||||
"""macOS 通知 — 使用 osascript"""
|
||||
escaped_title = title.replace('"', '\\"')
|
||||
escaped_message = message.replace('"', '\\"')
|
||||
script = f'display notification "{escaped_message}" with title "{escaped_title}"'
|
||||
subprocess.run(
|
||||
["osascript", "-e", script],
|
||||
capture_output=True, timeout=5
|
||||
)
|
||||
|
||||
def _notify_linux(self, title: str, message: str):
|
||||
"""Linux 通知 — 优先 notify-send,降级到 D-Bus"""
|
||||
# 方法1: notify-send (libnotify)
|
||||
try:
|
||||
subprocess.run(
|
||||
["notify-send", title, message, "-i", "dialog-information", "-t", "3000"],
|
||||
capture_output=True, timeout=5
|
||||
)
|
||||
return
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
# 方法2: D-Bus 直接调用
|
||||
try:
|
||||
dbus_script = f'''
|
||||
import dbus
|
||||
bus = dbus.SessionBus()
|
||||
notify_obj = bus.get_object('org.freedesktop.Notifications', '/org/freedesktop/Notifications')
|
||||
notify_iface = dbus.Interface(notify_obj, 'org.freedesktop.Notifications')
|
||||
notify_iface.Notify('xiaoli-cli', 0, 'dialog-information', '{title}', '{message}', [], {{}}, 3000)
|
||||
'''
|
||||
subprocess.run(
|
||||
[sys.executable, "-c", dbus_script],
|
||||
capture_output=True, timeout=5
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# ── 全局单例 ──
|
||||
_notification_manager = None
|
||||
|
||||
|
||||
def get_notification_manager() -> NotificationManager:
|
||||
"""获取全局通知管理器单例"""
|
||||
global _notification_manager
|
||||
if _notification_manager is None:
|
||||
_notification_manager = NotificationManager()
|
||||
return _notification_manager
|
||||
|
||||
|
||||
def notify_task_complete(task_summary: str = ""):
|
||||
"""快捷函数:通知任务完成"""
|
||||
get_notification_manager().notify_task_complete(task_summary)
|
||||
|
||||
|
||||
def notify_tool_complete(tool_name: str, result_summary: str = ""):
|
||||
"""快捷函数:通知工具执行完成"""
|
||||
get_notification_manager().notify_tool_complete(tool_name, result_summary)
|
||||
@@ -0,0 +1,119 @@
|
||||
"""
|
||||
统一工具返回格式 - ToolResult
|
||||
|
||||
所有插件和工具管理器使用统一的返回格式,
|
||||
替代原来混乱的字符串/Dict 返回。
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
# 错误码常量
|
||||
class ErrorCode:
|
||||
"""工具错误码"""
|
||||
SUCCESS = 0
|
||||
# 参数错误 (1xx)
|
||||
MISSING_ARGS = 100
|
||||
INVALID_ARGS = 101
|
||||
UNSUPPORTED_OP = 102
|
||||
# 资源错误 (2xx)
|
||||
NOT_FOUND = 200
|
||||
FILE_NOT_FOUND = 201
|
||||
PERMISSION_DENIED = 202
|
||||
# 执行错误 (3xx)
|
||||
EXEC_FAILED = 300
|
||||
TIMEOUT = 301
|
||||
DECODE_ERROR = 302
|
||||
# 系统错误 (4xx)
|
||||
INTERNAL_ERROR = 400
|
||||
NOT_IMPLEMENTED = 401
|
||||
|
||||
_NAMES = {
|
||||
0: "成功",
|
||||
100: "缺少参数",
|
||||
101: "参数无效",
|
||||
102: "不支持的操作",
|
||||
200: "未找到",
|
||||
201: "文件不存在",
|
||||
202: "权限不足",
|
||||
300: "执行失败",
|
||||
301: "超时",
|
||||
302: "解码错误",
|
||||
400: "内部错误",
|
||||
401: "未实现",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def name(cls, code: int) -> str:
|
||||
return cls._NAMES.get(code, f"未知({code})")
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolResult:
|
||||
"""统一工具返回格式"""
|
||||
|
||||
success: bool
|
||||
data: Any = None
|
||||
error_code: int = ErrorCode.SUCCESS
|
||||
error_message: str = ""
|
||||
|
||||
# 可选元数据
|
||||
tool_name: str = ""
|
||||
execution_time: float = 0.0
|
||||
metadata: dict = field(default_factory=dict)
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.success:
|
||||
return str(self.data) if self.data is not None else "执行完成"
|
||||
return f"[{ErrorCode.name(self.error_code)}] {self.error_message}"
|
||||
|
||||
# ── 便捷构造方法 ──
|
||||
|
||||
@classmethod
|
||||
def ok(cls, data: Any = None, **kwargs) -> "ToolResult":
|
||||
"""成功"""
|
||||
return cls(success=True, data=data, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def fail(cls, message: str, code: int = ErrorCode.EXEC_FAILED, **kwargs) -> "ToolResult":
|
||||
"""失败"""
|
||||
return cls(success=False, error_code=code, error_message=message, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def missing(cls, what: str = "参数", **kwargs) -> "ToolResult":
|
||||
"""缺少参数"""
|
||||
return cls.fail(f"缺少{what}", ErrorCode.MISSING_ARGS, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def not_found(cls, what: str = "资源", **kwargs) -> "ToolResult":
|
||||
"""未找到"""
|
||||
return cls.fail(f"{what}不存在", ErrorCode.NOT_FOUND, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def timeout(cls, seconds: int = 0, **kwargs) -> "ToolResult":
|
||||
"""超时"""
|
||||
msg = f"执行超时"
|
||||
if seconds:
|
||||
msg += f" ({seconds}秒)"
|
||||
return cls.fail(msg, ErrorCode.TIMEOUT, **kwargs)
|
||||
|
||||
# ── 格式转换 ──
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""转为普通 dict(兼容旧接口)"""
|
||||
if self.success:
|
||||
return {"result": self.data if self.data is not None else "执行完成"}
|
||||
return {"result": str(self)}
|
||||
|
||||
def to_mcp(self) -> dict:
|
||||
"""转为 MCP 协议格式"""
|
||||
text = str(self)
|
||||
return {
|
||||
"content": [{"type": "text", "text": text}],
|
||||
"isError": not self.success,
|
||||
}
|
||||
|
||||
def to_string(self) -> str:
|
||||
"""转为字符串(兼容旧插件 handle() 返回值)"""
|
||||
return str(self)
|
||||
Reference in new issue
Block a user