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

+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"]
}
}