feat: AG-UI 协议对齐与路由导航功能

- 前端: 添加 SSE 流式支持、stateSnapshot 事件、路由导航工具
- 前端: 实现工具调用审批流程,支持 pending 状态展示
- 后端: Agent 状态管理与会话持久化相关重构
- 文档: 新增 agent-agui-full-alignance 设计文档
- 测试: 补充相关单元测试和集成测试
This commit is contained in:
zl-q
2026-03-07 17:30:20 +08:00
parent ec33bb0cee
commit 120df903d2
52 changed files with 4305 additions and 1672 deletions
+27
View File
@@ -1,3 +1,4 @@
import 'dart:convert';
import 'package:dio/dio.dart';
import 'api_exception.dart';
import 'api_interceptor.dart';
@@ -92,4 +93,30 @@ class ApiClient implements IApiClient {
throw ApiException.fromDioError(e);
}
}
@override
Future<Stream<String>> getSseLines(
String path, {
Map<String, String>? headers,
}) async {
try {
final response = await _dio.get<ResponseBody>(
path,
options: Options(
responseType: ResponseType.stream,
headers: headers,
),
);
final responseBody = response.data;
if (responseBody == null) {
return const Stream<String>.empty();
}
return responseBody.stream
.cast<List<int>>()
.transform(utf8.decoder)
.transform(const LineSplitter());
} on DioException catch (e) {
throw ApiException.fromDioError(e);
}
}
}
+4
View File
@@ -5,4 +5,8 @@ abstract class IApiClient {
Future<Response<T>> post<T>(String path, {dynamic data, Options? options});
Future<Response<T>> patch<T>(String path, {dynamic data, Options? options});
Future<Response<T>> delete<T>(String path, {dynamic data, Options? options});
Future<Stream<String>> getSseLines(
String path, {
Map<String, String>? headers,
});
}
+115 -4
View File
@@ -1,18 +1,58 @@
import 'package:dio/dio.dart';
import 'i_api_client.dart';
typedef MockHandler = dynamic Function(dynamic data);
class MockRequest {
final String path;
final String method;
final dynamic data;
final Options? options;
final Map<String, String>? headers;
MockRequest({
required this.path,
required this.method,
this.data,
this.options,
this.headers,
});
}
typedef MockHandler = dynamic Function(MockRequest request);
class _PatternRoute {
final RegExp pattern;
final String method;
final MockHandler handler;
_PatternRoute({
required this.pattern,
required this.method,
required this.handler,
});
}
class MockApiClient implements IApiClient {
final Map<String, MockHandler> _handlers = {};
final List<_PatternRoute> _patternHandlers = [];
void registerHandler(String path, String method, MockHandler handler) {
final key = '$path:$method';
_handlers[key] = handler;
}
void registerPatternHandler(RegExp pattern, String method, MockHandler handler) {
_patternHandlers.add(
_PatternRoute(
pattern: pattern,
method: method.toUpperCase(),
handler: handler,
),
);
}
void clearMocks() {
_handlers.clear();
_patternHandlers.clear();
}
@override
@@ -47,6 +87,54 @@ class MockApiClient implements IApiClient {
return _handleRequest('DELETE', path, data: data, options: options);
}
@override
Future<Stream<String>> getSseLines(
String path, {
Map<String, String>? headers,
}) async {
final key = '$path:SSE';
final direct = _handlers[key];
if (direct != null) {
final response = direct(
MockRequest(
path: path,
method: 'SSE',
headers: headers,
),
);
if (response is Stream<String>) {
return response;
}
if (response is Iterable<String>) {
return Stream<String>.fromIterable(response);
}
return const Stream<String>.empty();
}
for (final route in _patternHandlers) {
if (route.method != 'SSE') {
continue;
}
if (!route.pattern.hasMatch(path)) {
continue;
}
final response = route.handler(
MockRequest(
path: path,
method: 'SSE',
headers: headers,
),
);
if (response is Stream<String>) {
return response;
}
if (response is Iterable<String>) {
return Stream<String>.fromIterable(response);
}
return const Stream<String>.empty();
}
return const Stream<String>.empty();
}
Future<Response<T>> _handleRequest<T>(
String method,
String path, {
@@ -55,11 +143,17 @@ class MockApiClient implements IApiClient {
}) async {
await Future.delayed(const Duration(milliseconds: 200));
final key = '$path:$method';
final handler = _handlers[key];
final handler = _resolveHandler(path: path, method: method);
if (handler != null) {
final response = handler(data);
final response = handler(
MockRequest(
path: path,
method: method,
data: data,
options: options,
),
);
if (response is Response) {
return response as Response<T>;
}
@@ -76,4 +170,21 @@ class MockApiClient implements IApiClient {
requestOptions: RequestOptions(path: path),
);
}
MockHandler? _resolveHandler({required String path, required String method}) {
final key = '$path:$method';
final direct = _handlers[key];
if (direct != null) {
return direct;
}
for (final route in _patternHandlers) {
if (route.method != method.toUpperCase()) {
continue;
}
if (route.pattern.hasMatch(path)) {
return route.handler;
}
}
return null;
}
}