diff --git a/apps/lib/features/chat/data/models/ag_ui_event.dart b/apps/lib/features/chat/data/models/ag_ui_event.dart index 88e70bb..5863558 100644 --- a/apps/lib/features/chat/data/models/ag_ui_event.dart +++ b/apps/lib/features/chat/data/models/ag_ui_event.dart @@ -177,7 +177,7 @@ class TextMessageEndEvent extends AgUiEvent { answer: _asString(json['answer']), role: _asString(json['role'], fallback: 'assistant'), status: _asString(json['status'], fallback: 'success'), - uiSchema: _asMap(json['ui_schema']) ?? _asMap(json['uiSchema']), + uiSchema: _asMap(json['ui_schema']), ); } @@ -238,17 +238,12 @@ class ToolCallResultEvent extends AgUiEvent { factory ToolCallResultEvent.fromJson(Map json) => ToolCallResultEvent( - messageId: _asString( - json['messageId'], - fallback: 'tool-${_asString(json['tool_call_id'])}', - ), - toolCallId: _asString(json['tool_call_id'] ?? json['toolCallId']), - toolName: _asString(json['tool_name'] ?? json['toolName']), - resultSummary: _asString( - json['result_summary'] ?? json['resultSummary'], - ), + messageId: _asString(json['messageId']), + toolCallId: _asString(json['tool_call_id']), + toolName: _asString(json['tool_name']), + resultSummary: _asString(json['result']), status: _asString(json['status'], fallback: 'success'), - uiSchema: _asMap(json['ui_schema']) ?? _asMap(json['uiSchema']), + uiSchema: _asMap(json['ui_schema']), ); } @@ -328,7 +323,7 @@ class HistoryMessage { timestamp: DateTime.tryParse(_asString(json['timestamp'])) ?? DateTime.now(), attachments: _parseHistoryAttachments(json['attachments']), - uiSchema: _asMap(json['ui_schema']) ?? _asMap(json['uiSchema']), + uiSchema: _asMap(json['ui_schema']), ); } diff --git a/apps/lib/features/chat/data/tools/route_navigation_tool.dart b/apps/lib/features/chat/data/tools/route_navigation_tool.dart index c84f07b..615447b 100644 --- a/apps/lib/features/chat/data/tools/route_navigation_tool.dart +++ b/apps/lib/features/chat/data/tools/route_navigation_tool.dart @@ -1,14 +1,23 @@ +import '../../../../core/router/app_routes.dart'; + typedef RouteNavigator = void Function(String target, {bool replace}); const Set _allowedRoutes = { - '/settings', - '/todo', - '/calendar/dayweek', - '/messages/invites', + AppRoutes.settingsMain, + AppRoutes.todoList, + AppRoutes.todoCreate, + AppRoutes.calendarDayWeek, + AppRoutes.calendarMonth, + AppRoutes.calendarEventCreate, + AppRoutes.messageInviteList, + AppRoutes.contactsList, + AppRoutes.contactsAdd, }; const List _allowedRoutePrefixes = [ '/calendar/events/', + '/todo/', + '/messages/invites/', ]; class RouteNavigationTool { @@ -29,17 +38,10 @@ class RouteNavigationTool { Map execute(Map args) { final target = args['target']; if (target is! String || target.isEmpty) { - return { - 'ok': false, - 'error': 'target is required', - }; + return {'ok': false, 'error': 'target is required'}; } if (!_isAllowedTarget(target)) { - return { - 'ok': false, - 'target': target, - 'error': 'target is not allowed', - }; + return {'ok': false, 'target': target, 'error': 'target is not allowed'}; } final replace = args['replace'] == true; final navigator = _navigator; @@ -52,12 +54,7 @@ class RouteNavigationTool { }; } navigator(target, replace: replace); - return { - 'ok': true, - 'target': target, - 'replace': replace, - 'applied': true, - }; + return {'ok': true, 'target': target, 'replace': replace, 'applied': true}; } bool _isAllowedTarget(String target) { diff --git a/apps/lib/features/chat/presentation/bloc/agent_stage.dart b/apps/lib/features/chat/presentation/bloc/agent_stage.dart index d3199d6..821aabf 100644 --- a/apps/lib/features/chat/presentation/bloc/agent_stage.dart +++ b/apps/lib/features/chat/presentation/bloc/agent_stage.dart @@ -1,11 +1,11 @@ -enum AgentStage { intent, execution } +enum AgentStage { execution, memory } AgentStage? stageFromStepName(String value) { switch (value) { - case 'router': - return AgentStage.intent; case 'worker': return AgentStage.execution; + case 'memory': + return AgentStage.memory; default: return null; } @@ -13,8 +13,8 @@ AgentStage? stageFromStepName(String value) { String stageLabel(AgentStage? stage) { return switch (stage) { - AgentStage.intent => '意图识别中', AgentStage.execution => '任务执行中', + AgentStage.memory => '记忆提取中', null => '任务处理中', }; } diff --git a/apps/lib/features/chat/ui/navigation/ui_schema_navigation.dart b/apps/lib/features/chat/ui/navigation/ui_schema_navigation.dart new file mode 100644 index 0000000..42b8b8a --- /dev/null +++ b/apps/lib/features/chat/ui/navigation/ui_schema_navigation.dart @@ -0,0 +1,35 @@ +bool isValidInternalNavigationPath(String path) { + if (path.isEmpty || !path.startsWith('/')) { + return false; + } + return !path.startsWith('//') && + !path.contains('://') && + !path.contains('?') && + !path.contains('#') && + !path.contains(':'); +} + +String buildUiSchemaNavigationTarget({ + required String path, + Map? params, +}) { + final baseUri = Uri.parse(path); + final queryParams = {}; + + if (params != null) { + for (final entry in params.entries) { + final value = entry.value; + if (value is String && value.isNotEmpty) { + queryParams[entry.key] = value; + } else if (value is num || value is bool) { + queryParams[entry.key] = value.toString(); + } + } + } + + final mergedQueryParams = {...baseUri.queryParameters, ...queryParams}; + final targetUri = baseUri.replace( + queryParameters: mergedQueryParams.isEmpty ? null : mergedQueryParams, + ); + return targetUri.toString(); +} diff --git a/apps/lib/features/chat/ui/widgets/ui_schema_renderer.dart b/apps/lib/features/chat/ui/widgets/ui_schema_renderer.dart index d644835..d639f65 100644 --- a/apps/lib/features/chat/ui/widgets/ui_schema_renderer.dart +++ b/apps/lib/features/chat/ui/widgets/ui_schema_renderer.dart @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:social_app/core/theme/design_tokens.dart'; import 'package:social_app/shared/widgets/toast/toast.dart'; import 'package:social_app/shared/widgets/toast/toast_type.dart'; +import '../navigation/ui_schema_navigation.dart'; class UiSchemaRenderer { static Widget renderSchema(Map? schema) { @@ -226,22 +227,16 @@ class UiSchemaRenderer { } final path = _asString(action['path']).trim(); - if (!_isValidInternalPath(path)) { + if (!isValidInternalNavigationPath(path)) { Toast.show(context, '导航路径无效', type: ToastType.warning); return; } final params = _asMap(action['params']); - final replace = action['replace'] == true; - final queryParams = _extractNavigationQueryParams(params); + final shouldReplace = action['replace'] == true; try { - final baseUri = Uri.parse(path); - final mergedQueryParams = {...baseUri.queryParameters, ...queryParams}; - final targetUri = baseUri.replace( - queryParameters: mergedQueryParams.isEmpty ? null : mergedQueryParams, - ); - final target = targetUri.toString(); - if (replace) { + final target = buildUiSchemaNavigationTarget(path: path, params: params); + if (shouldReplace) { context.replace(target); return; } @@ -251,38 +246,6 @@ class UiSchemaRenderer { } } - static bool _isValidInternalPath(String path) { - if (path.isEmpty || !path.startsWith('/')) { - return false; - } - if (path.startsWith('//') || path.contains('://')) { - return false; - } - if (path.contains('?') || path.contains('#') || path.contains(':')) { - return false; - } - return true; - } - - static Map _extractNavigationQueryParams( - Map? params, - ) { - if (params == null || params.isEmpty) { - return const {}; - } - final query = {}; - params.forEach((key, value) { - if (value is String && value.isNotEmpty) { - query[key] = value; - return; - } - if (value is num || value is bool) { - query[key] = value.toString(); - } - }); - return query; - } - static Widget _renderKv(Map node) { final items = _asList( node['items'], @@ -406,17 +369,14 @@ class UiSchemaRenderer { } static List _withGap(List widgets, double gap) { - if (widgets.isEmpty) { - return const []; - } - final result = []; - for (var i = 0; i < widgets.length; i++) { - if (i > 0) { - result.add(SizedBox(height: gap)); - } - result.add(widgets[i]); - } - return result; + if (widgets.isEmpty) return const []; + return [ + widgets.first, + for (int i = 1; i < widgets.length; i++) ...[ + SizedBox(height: gap), + widgets[i], + ], + ]; } static Color _statusBackground(String status) { @@ -454,13 +414,7 @@ class UiSchemaRenderer { return value; } if (value is Map) { - final result = {}; - for (final entry in value.entries) { - if (entry.key is String) { - result[entry.key as String] = entry.value; - } - } - return result; + return Map.from(value); } return null; } diff --git a/apps/test/features/chat/ag_ui_event_test.dart b/apps/test/features/chat/ag_ui_event_test.dart index 024aa86..811c824 100644 --- a/apps/test/features/chat/ag_ui_event_test.dart +++ b/apps/test/features/chat/ag_ui_event_test.dart @@ -37,7 +37,7 @@ void main() { 'tool_call_id': 'call_1', 'tool_name': 'calendar_read', 'status': 'success', - 'result_summary': '找到 2 条结果', + 'result': '找到 2 条结果', }); expect(event, isA()); diff --git a/apps/test/features/chat/presentation/agent_stage_mapping_test.dart b/apps/test/features/chat/presentation/agent_stage_mapping_test.dart index a783a87..6b1d5cb 100644 --- a/apps/test/features/chat/presentation/agent_stage_mapping_test.dart +++ b/apps/test/features/chat/presentation/agent_stage_mapping_test.dart @@ -3,13 +3,6 @@ import 'package:social_app/features/chat/presentation/bloc/agent_stage.dart'; void main() { group('agent stage mapping', () { - test('maps protocol step router to intent stage label', () { - final stage = stageFromStepName('router'); - - expect(stage, AgentStage.intent); - expect(stageLabel(stage), '意图识别中'); - }); - test('maps protocol step worker to execution stage label', () { final stage = stageFromStepName('worker'); @@ -17,6 +10,13 @@ void main() { expect(stageLabel(stage), '任务执行中'); }); + test('maps protocol step memory to memory stage label', () { + final stage = stageFromStepName('memory'); + + expect(stage, AgentStage.memory); + expect(stageLabel(stage), '记忆提取中'); + }); + test('uses processing label when step is unknown', () { final stage = stageFromStepName('unexpected'); diff --git a/apps/test/features/chat/ui_schema_navigation_test.dart b/apps/test/features/chat/ui_schema_navigation_test.dart new file mode 100644 index 0000000..d913ddc --- /dev/null +++ b/apps/test/features/chat/ui_schema_navigation_test.dart @@ -0,0 +1,29 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:social_app/features/chat/ui/navigation/ui_schema_navigation.dart'; + +void main() { + test('buildUiSchemaNavigationTarget merges scalar params only', () { + final target = buildUiSchemaNavigationTarget( + path: '/calendar/dayweek', + params: { + 'date': '2026-03-18', + 'from': 'home', + 'count': 2, + 'enabled': true, + 'ignored': {'nested': true}, + }, + ); + + expect( + target, + '/calendar/dayweek?date=2026-03-18&from=home&count=2&enabled=true', + ); + }); + + test('isValidInternalNavigationPath follows protocol constraints', () { + expect(isValidInternalNavigationPath('/todo/123/edit'), true); + expect(isValidInternalNavigationPath('https://evil.com'), false); + expect(isValidInternalNavigationPath('/todo/123?x=1'), false); + expect(isValidInternalNavigationPath('/todo/:id'), false); + }); +}