feat: AG-UI 协议对齐与路由导航功能
- 前端: 添加 SSE 流式支持、stateSnapshot 事件、路由导航工具 - 前端: 实现工具调用审批流程,支持 pending 状态展示 - 后端: Agent 状态管理与会话持久化相关重构 - 文档: 新增 agent-agui-full-alignance 设计文档 - 测试: 补充相关单元测试和集成测试
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
typedef RouteNavigator = void Function(String target, {bool replace});
|
||||
|
||||
const Set<String> _allowedRoutes = {
|
||||
'/settings',
|
||||
'/todo',
|
||||
'/calendar/dayweek',
|
||||
'/messages/invites',
|
||||
};
|
||||
|
||||
const List<String> _allowedRoutePrefixes = [
|
||||
'/calendar/events/',
|
||||
];
|
||||
|
||||
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,8 +1,11 @@
|
||||
import 'route_navigation_tool.dart';
|
||||
|
||||
typedef ToolHandler =
|
||||
Future<Map<String, dynamic>> Function(Map<String, dynamic> args);
|
||||
|
||||
/// 工具常量
|
||||
const _toolNameCreateCalendar = 'create_calendar_event';
|
||||
const _toolNameNavigateRoute = 'navigate_to_route';
|
||||
const _defaultTimezone = 'Asia/Shanghai';
|
||||
const _defaultEventColor = '#4F46E5';
|
||||
const _defaultSourceType = 'agentGenerated';
|
||||
@@ -62,6 +65,20 @@ class ToolRegistry {
|
||||
handler: _handleCreateCalendarEvent,
|
||||
);
|
||||
|
||||
_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;
|
||||
}
|
||||
|
||||
@@ -84,6 +101,12 @@ class ToolRegistry {
|
||||
};
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user