feat: 添加自动化任务(automation_jobs)功能模块
This commit is contained in:
@@ -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});
|
||||
}
|
||||
Reference in New Issue
Block a user