feat: 添加自动化任务(automation_jobs)功能模块

This commit is contained in:
qzl
2026-03-24 12:38:11 +08:00
parent f4b7eb7e09
commit 23359c2d01
43 changed files with 4266 additions and 1139 deletions
@@ -1,110 +0,0 @@
import 'dart:convert';
enum Intent { createEvent, searchEvent, unknown }
/// 意图匹配规则(顺序敏感:searchEvent 优先级高于 createEvent
final _orderedPatterns = <(RegExp, Intent)>[
(RegExp(r'^查看|^有什么|^今天.*日程|^明天.*安排|^查询'), Intent.searchEvent),
(RegExp(r'提醒|开会|预约|安排.*时间|创建.*日程'), Intent.createEvent),
(RegExp(r'明天.*\d|今天.*\d|后天.*\d|\d{1,2}点|\d{1,2}:\d{2}'), Intent.createEvent),
];
/// 时区常量
const _defaultTimezone = 'Asia/Shanghai';
const _dayToday = '今天';
const _dayTomorrow = '明天';
const _dayAfterTomorrow = '后天';
const _tomorrowOffset = 1;
const _dayAfterTomorrowOffset = 2;
const _defaultMinute = 0;
class AiDecisionEngine {
Intent matchIntent(String text) {
for (final (pattern, intent) in _orderedPatterns) {
if (pattern.hasMatch(text)) return intent;
}
return Intent.unknown;
}
Map<String, dynamic>? tryExtractEventArgs(String text) {
if (matchIntent(text) != Intent.createEvent) return null;
final args = <String, dynamic>{};
final titleMatch = RegExp(r'提醒(.+?)(?:明天|今天|几点|$)').firstMatch(text);
if (titleMatch != null) {
args['title'] = titleMatch.group(1)?.trim() ?? text;
} else if (RegExp(r'\d{1,2}[:点]|\d{1,2}点').hasMatch(text)) {
args['title'] = text
.replaceAll(RegExp(r'\d{1,2}[:点]\d{0,2}|明天|今天|后天'), '')
.trim();
}
final title = args['title'];
if (title == null || (title as String).isEmpty) return null;
final timeMatch = RegExp(
r'(明天|今天|后天)?\s*(\d{1,2})[:点](\d{2})?',
).firstMatch(text);
if (timeMatch != null) {
final dayStr = timeMatch.group(1) ?? _dayToday;
final hour = int.parse(timeMatch.group(2)!);
final minute = int.parse(timeMatch.group(3) ?? '$_defaultMinute');
final now = DateTime.now();
final dayOffset = switch (dayStr) {
_dayTomorrow => _tomorrowOffset,
_dayAfterTomorrow => _dayAfterTomorrowOffset,
_ => 0,
};
final startAt = DateTime(
now.year,
now.month,
now.day + dayOffset,
hour,
minute,
);
args['startAt'] = startAt.toIso8601String();
args['timezone'] = _defaultTimezone;
}
if (!args.containsKey('startAt')) return null;
return args;
}
bool shouldTriggerToolCall(String text) =>
matchIntent(text) == Intent.createEvent;
Map<String, dynamic>? getToolCallArgs(String text) {
if (!shouldTriggerToolCall(text)) return null;
return tryExtractEventArgs(text);
}
ForceTriggerResult? tryForceTrigger(String text) {
final match = RegExp(
r'#tool:([A-Za-z0-9_.-]+)\s*(\{.*\})?',
).firstMatch(text);
if (match == null) return null;
final toolName = match.group(1)!;
final argsJson = match.group(2);
Map<String, dynamic>? args;
if (argsJson != null) {
try {
args = jsonDecode(argsJson) as Map<String, dynamic>;
} catch (_) {
args = {};
}
}
return ForceTriggerResult(toolName: toolName, args: args ?? {});
}
}
class ForceTriggerResult {
final String toolName;
final Map<String, dynamic> args;
ForceTriggerResult({required this.toolName, required this.args});
}
@@ -1,75 +0,0 @@
import '../../../../core/router/app_routes.dart';
typedef RouteNavigator = void Function(String target, {bool replace});
const Set<String> _allowedRoutes = {
AppRoutes.settingsMain,
AppRoutes.todoList,
AppRoutes.todoCreate,
AppRoutes.calendarDayWeek,
AppRoutes.calendarMonth,
AppRoutes.calendarEventCreate,
AppRoutes.messageInviteList,
AppRoutes.contactsList,
AppRoutes.contactsAdd,
};
const List<String> _allowedRoutePrefixes = [
'/calendar/events/',
'/todo/',
'/messages/invites/',
];
class RouteNavigationTool {
RouteNavigationTool._();
static final RouteNavigationTool instance = RouteNavigationTool._();
RouteNavigator? _navigator;
void bindNavigator(RouteNavigator navigator) {
_navigator = navigator;
}
void clearNavigator() {
_navigator = null;
}
Map<String, dynamic> execute(Map<String, dynamic> args) {
final target = args['target'];
if (target is! String || target.isEmpty) {
return {'ok': false, 'error': 'target is required'};
}
if (!_isAllowedTarget(target)) {
return {'ok': false, 'target': target, 'error': 'target is not allowed'};
}
final replace = args['replace'] == true;
final navigator = _navigator;
if (navigator == null) {
return {
'ok': false,
'target': target,
'replace': replace,
'error': 'navigator not bound',
};
}
navigator(target, replace: replace);
return {'ok': true, 'target': target, 'replace': replace, 'applied': true};
}
bool _isAllowedTarget(String target) {
if (!target.startsWith('/')) {
return false;
}
final normalized = target.split('?').first;
if (_allowedRoutes.contains(normalized)) {
return true;
}
for (final prefix in _allowedRoutePrefixes) {
if (normalized.startsWith(prefix)) {
return true;
}
}
return false;
}
}
@@ -1,106 +0,0 @@
import 'route_navigation_tool.dart';
typedef ToolHandler =
Future<Map<String, dynamic>> Function(Map<String, dynamic> args);
/// 工具常量
const _toolNameNavigateRoute = 'front.navigate_to_route';
class ToolDefinition {
final String name;
final String description;
final Map<String, dynamic> parameters;
final ToolHandler handler;
ToolDefinition({
required this.name,
required this.description,
required this.parameters,
required this.handler,
});
}
class ToolRegistry {
static final Map<String, ToolDefinition> _tools = {};
static bool _initialized = false;
static void initialize() {
if (_initialized) return;
_tools[_toolNameNavigateRoute] = ToolDefinition(
name: _toolNameNavigateRoute,
description: '在前端执行路由跳转',
parameters: {
'type': 'object',
'properties': {
'target': {'type': 'string', 'description': '跳转目标路由'},
'replace': {'type': 'boolean', 'description': '是否 replace 导航'},
},
'required': ['target'],
},
handler: _handleNavigateRoute,
);
_initialized = true;
}
static Future<Map<String, dynamic>> _handleNavigateRoute(
Map<String, dynamic> args,
) async {
return RouteNavigationTool.instance.execute(args);
}
static ToolDefinition? getTool(String name) => _tools[name];
static List<ToolDefinition> getAllTools() => _tools.values.toList();
static Future<Map<String, dynamic>> execute(
String toolName,
Map<String, dynamic> args,
) async {
final tool = _tools[toolName];
if (tool == null) throw ToolNotFoundException('Tool not found: $toolName');
return tool.handler(args);
}
static ToolValidationResult validateArgs(
String toolName,
Map<String, dynamic> args,
) {
final tool = _tools[toolName];
if (tool == null) {
return ToolValidationResult(
ok: false,
error: 'Tool not found: $toolName',
);
}
final required = tool.parameters['required'] as List<dynamic>? ?? [];
final missing = <String>[];
for (final field in required) {
if (!args.containsKey(field) || args[field] == null) {
missing.add(field as String);
}
}
if (missing.isNotEmpty) {
return ToolValidationResult(
ok: false,
error: 'Missing required fields: ${missing.join(', ')}',
);
}
return ToolValidationResult(ok: true);
}
}
class ToolNotFoundException implements Exception {
final String message;
ToolNotFoundException(this.message);
@override
String toString() => 'ToolNotFoundException: $message';
}
class ToolValidationResult {
final bool ok;
final String? error;
ToolValidationResult({required this.ok, this.error});
}