# IT0 Flutter 前端开发指导文档 > IT Operations Intelligent Agent — Flutter Android 客户端(Riverpod + Clean Architecture) ## 1. 项目概述 ### 1.1 定位 IT0 Flutter App 是服务器集群运维智能体的**驾驶舱**,提供: - 实时运维仪表盘 - AI 对话交互(文字 + 语音) - 任务发起与审批 - 多渠道通知接收 - 服务器状态监控 ### 1.2 技术栈 | 层面 | 技术选型 | |------|---------| | 框架 | Flutter 3.x (Android) | | 状态管理 | Riverpod 2.x (flutter_riverpod + riverpod_annotation) | | 架构 | Clean Architecture 三层 | | 网络 | Dio (REST) + web_socket_channel (WebSocket) | | 本地存储 | Hive / SharedPreferences | | 语音 | speech_to_text + flutter_tts | | 路由 | go_router | | 代码生成 | freezed + json_serializable + riverpod_generator | | 推送 | Firebase Cloud Messaging (FCM) | | 图表 | fl_chart | | 终端模拟 | xterm (flutter_xterm) | ### 1.3 设计原则 - **Clean Architecture**:Data → Domain → Presentation 严格分层,依赖方向向内 - **Riverpod**:全局状态管理,无 context 依赖,可测试 - **离线优先**:关键数据本地缓存,断网时仍可查看历史 - **实时优先**:WebSocket 长连接,Agent 输出实时流式展示 - **多租户感知**:登录后获取可访问租户列表,Dio 拦截器自动注入 `X-Tenant-Id`,切换租户刷新全部数据 --- ## 2. 项目结构 ### 2.1 整体目录 ``` it0_app/ ├── lib/ │ ├── main.dart # App 入口 │ ├── app.dart # MaterialApp + Router 配置 │ │ │ ├── core/ # 核心基础设施(跨 Feature 共享) │ │ ├── config/ │ │ │ ├── app_config.dart # 环境配置 │ │ │ └── api_endpoints.dart # API 端点常量 │ │ ├── network/ │ │ │ ├── dio_client.dart # Dio 配置(拦截器、token 注入) │ │ │ ├── websocket_client.dart # WebSocket 管理(自动重连) │ │ │ └── api_result.dart # Result 统一响应类型 │ │ ├── error/ │ │ │ ├── failures.dart # 失败类型定义 │ │ │ └── error_handler.dart # 全局异常处理 │ │ ├── theme/ │ │ │ ├── app_theme.dart # 主题配置(暗色为主) │ │ │ ├── app_colors.dart # 颜色常量 │ │ │ └── app_typography.dart # 字体样式 │ │ ├── router/ │ │ │ └── app_router.dart # go_router 路由配置 │ │ ├── widgets/ # 通用组件 │ │ │ ├── status_badge.dart # 状态徽章(running/error/warning) │ │ │ ├── risk_level_chip.dart # 风险等级标签 │ │ │ ├── loading_overlay.dart │ │ │ └── empty_state.dart │ │ ├── tenant/ # ★ 多租户上下文 │ │ │ ├── tenant_provider.dart # 当前租户 Riverpod Provider │ │ │ └── tenant_interceptor.dart # Dio 拦截器(注入 X-Tenant-Id) │ │ └── utils/ │ │ ├── date_formatter.dart │ │ └── logger.dart │ │ │ └── features/ # 功能模块(按 Feature 划分) │ ├── auth/ # 认证 │ ├── dashboard/ # 仪表盘 │ ├── chat/ # AI 对话(核心交互 + 驻留指令定义) │ ├── tasks/ # 运维任务 │ ├── standing_orders/ # ★ 驻留指令管理与监控 │ ├── approvals/ # 审批中心 │ ├── servers/ # 服务器管理 │ ├── alerts/ # 告警中心 │ ├── agent_call/ # ★ Agent 来电响应(Pipecat 语音对话 + 通知) │ ├── terminal/ # 远程终端 │ └── settings/ # 设置 │ ├── test/ │ ├── unit/ │ ├── widget/ │ └── integration/ │ ├── pubspec.yaml └── analysis_options.yaml ``` ### 2.2 单个 Feature 模块结构(Clean Architecture 三层) 以 `chat` 模块为例: ``` features/chat/ ├── data/ # 数据层 │ ├── datasources/ │ │ ├── chat_remote_datasource.dart # REST API 调用 │ │ └── chat_local_datasource.dart # 本地缓存(Hive) │ ├── models/ │ │ ├── chat_message_model.dart # API 响应模型(fromJson/toJson) │ │ ├── stream_event_model.dart # WebSocket 流事件模型 │ │ └── session_model.dart │ └── repositories/ │ └── chat_repository_impl.dart # 仓储实现 │ ├── domain/ # 领域层(零依赖) │ ├── entities/ │ │ ├── chat_message.dart # 消息实体(freezed) │ │ ├── chat_session.dart # 会话实体 │ │ └── stream_event.dart # 流事件实体 │ ├── repositories/ │ │ └── chat_repository.dart # 仓储接口(抽象类) │ └── usecases/ │ ├── send_message.dart # 发送消息 │ ├── start_voice_input.dart # 开始语音输入 │ ├── get_session_history.dart # 获取历史 │ └── cancel_task.dart # 取消任务 │ └── presentation/ # 展示层 ├── providers/ │ ├── chat_providers.dart # Riverpod providers │ └── voice_providers.dart # 语音相关 providers ├── pages/ │ └── chat_page.dart # 对话页面 ├── widgets/ │ ├── message_bubble.dart # 消息气泡 │ ├── agent_thinking_indicator.dart # AI 思考动画 │ ├── tool_execution_card.dart # 工具执行卡片(显示 Bash 命令和输出) │ ├── approval_action_card.dart # 审批操作卡片 │ ├── voice_input_button.dart # 语音输入按钮 │ └── stream_text_widget.dart # 流式文字渲染 └── controllers/ └── chat_controller.dart # 页面控制器(如需要) ``` --- ## 3. 核心基础设施详细设计 ### 3.1 网络层 — Dio 客户端 ```dart // core/network/dio_client.dart class DioClient { late final Dio _dio; DioClient({required AppConfig config, required TokenStorage tokenStorage}) { _dio = Dio(BaseOptions( baseUrl: config.apiBaseUrl, connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 30), )); _dio.interceptors.addAll([ // Token 注入 InterceptorsWrapper( onRequest: (options, handler) async { final token = await tokenStorage.getAccessToken(); if (token != null) { options.headers['Authorization'] = 'Bearer $token'; } handler.next(options); }, ), // ★ 多租户:自动注入 X-Tenant-Id TenantInterceptor(), // 日志(debug 模式) if (config.isDebug) LogInterceptor(responseBody: true), // 错误统一处理 ErrorInterceptor(), ]); } Dio get dio => _dio; } ``` ### 3.2 网络层 — WebSocket 客户端 ```dart // core/network/websocket_client.dart class WebSocketClient { WebSocketChannel? _channel; final StreamController> _eventController = StreamController.broadcast(); Timer? _heartbeatTimer; Timer? _reconnectTimer; int _reconnectAttempts = 0; static const _maxReconnectAttempts = 10; final AppConfig _config; final TokenStorage _tokenStorage; WebSocketClient({required AppConfig config, required TokenStorage tokenStorage}) : _config = config, _tokenStorage = tokenStorage; /// 事件流 — 所有 Feature 监听此 Stream Stream> get eventStream => _eventController.stream; /// 连接 Future connect() async { final token = await _tokenStorage.getAccessToken(); final uri = Uri.parse('${_config.wsBaseUrl}/ws?token=$token'); _channel = WebSocketChannel.connect(uri); _reconnectAttempts = 0; _channel!.stream.listen( (data) { final event = jsonDecode(data as String) as Map; _eventController.add(event); }, onDone: () => _handleDisconnect(), onError: (error) => _handleDisconnect(), ); _startHeartbeat(); } /// 发送消息 void send(Map data) { _channel?.sink.add(jsonEncode(data)); } /// 自动重连(指数退避) void _handleDisconnect() { _heartbeatTimer?.cancel(); if (_reconnectAttempts < _maxReconnectAttempts) { final delay = Duration(seconds: math.pow(2, _reconnectAttempts).toInt()); _reconnectTimer = Timer(delay, () { _reconnectAttempts++; connect(); }); } } void _startHeartbeat() { _heartbeatTimer = Timer.periodic(const Duration(seconds: 30), (_) { send({'type': 'ping'}); }); } void dispose() { _heartbeatTimer?.cancel(); _reconnectTimer?.cancel(); _channel?.sink.close(); _eventController.close(); } } ``` ### 3.3 统一结果类型 ```dart // core/network/api_result.dart @freezed sealed class ApiResult with _$ApiResult { const factory ApiResult.success(T data) = Success; const factory ApiResult.failure(Failure failure) = FailureResult; } // core/error/failures.dart @freezed sealed class Failure with _$Failure { const factory Failure.network({required String message}) = NetworkFailure; const factory Failure.server({required String message, required int statusCode}) = ServerFailure; const factory Failure.auth({required String message}) = AuthFailure; const factory Failure.unknown({required String message}) = UnknownFailure; } ``` --- ## 4. Feature 模块详细设计 ### 4.1 Dashboard(仪表盘) #### 页面布局 ``` ┌──────────────────────────────────────────┐ │ IT0 Dashboard [⚙️] │ ├──────────────────────────────────────────┤ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ Servers │ │ Active │ │ Alerts │ │ │ │ 12 │ │ Tasks 3 │ │ 2 │ │ │ │ ● 10 OK │ │ 1 await │ │ ⚠ 1 🔴1│ │ │ └─────────┘ └─────────┘ └─────────┘ │ │ │ │ ── 服务器状态 ────────────────────── │ │ ┌──────────────────────────────────┐ │ │ │ prod-1 CPU 45% MEM 62% DISK 71% │ │ │ │ prod-2 CPU 23% MEM 48% DISK 92% │ │ ← 磁盘高亮红色 │ │ prod-3 CPU 67% MEM 75% DISK 55% │ │ │ └──────────────────────────────────┘ │ │ │ │ ── 最近操作 ────────────────────── │ │ │ 14:23 巡检完成 — 全部正常 │ │ │ 13:15 prod-2 日志清理 — 成功 │ │ │ 12:01 告警: prod-2 磁盘 > 90% │ │ │ │ ── 快捷操作 ────────────────────── │ │ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ │ │ 巡检 │ │ 部署 │ │ 日志 │ │ 终端 │ │ │ └──────┘ └──────┘ └──────┘ └──────┘ │ │ │ ├──────────────────────────────────────────┤ │ [Dashboard] [Chat] [Tasks] [Alerts] │ └──────────────────────────────────────────┘ ``` #### Riverpod Providers ```dart // features/dashboard/presentation/providers/dashboard_providers.dart /// 服务器状态列表(实时更新) @riverpod Stream> serverStatusStream(Ref ref) { final wsClient = ref.watch(webSocketClientProvider); return wsClient.eventStream .where((event) => event['type'] == 'server_status_update') .map((event) => (event['servers'] as List) .map((s) => ServerStatus.fromJson(s)) .toList()); } /// 仪表盘摘要(聚合数据) @riverpod Future dashboardSummary(Ref ref) async { final repo = ref.watch(dashboardRepositoryProvider); return repo.getSummary(); } /// 最近操作日志 @riverpod Future> recentOperations(Ref ref) async { final repo = ref.watch(dashboardRepositoryProvider); return repo.getRecentOperations(limit: 20); } /// 活跃告警 @riverpod Stream> activeAlerts(Ref ref) { final wsClient = ref.watch(webSocketClientProvider); return wsClient.eventStream .where((event) => event['type'] == 'alert_update') .map((event) => (event['alerts'] as List) .map((a) => Alert.fromJson(a)) .toList()); } ``` --- ### 4.2 Chat(AI 对话交互)— 核心 Feature #### 4.2.1 Domain 层 ```dart // features/chat/domain/entities/chat_message.dart @freezed class ChatMessage with _$ChatMessage { const factory ChatMessage({ required String id, required MessageRole role, // user / assistant / system required String content, required DateTime timestamp, MessageType? type, // text / tool_use / tool_result / approval / thinking ToolExecution? toolExecution, // 工具执行详情 ApprovalRequest? approvalRequest, // 审批请求 bool? isStreaming, // 是否正在流式输出 }) = _ChatMessage; } @freezed class ToolExecution with _$ToolExecution { const factory ToolExecution({ required String toolName, // Bash / Read / Grep / etc. required String input, // 命令内容 String? output, // 执行结果 required RiskLevel riskLevel, required ToolStatus status, // executing / completed / blocked / awaiting_approval }) = _ToolExecution; } @freezed class ApprovalRequest with _$ApprovalRequest { const factory ApprovalRequest({ required String taskId, required String command, required RiskLevel riskLevel, required String targetServer, required DateTime expiresAt, ApprovalStatus? status, // pending / approved / rejected }) = _ApprovalRequest; } // features/chat/domain/entities/stream_event.dart @freezed sealed class StreamEvent with _$StreamEvent { const factory StreamEvent.thinking({required String content}) = ThinkingEvent; const factory StreamEvent.text({required String content}) = TextEvent; const factory StreamEvent.toolUse({ required String toolName, required Map input, }) = ToolUseEvent; const factory StreamEvent.toolResult({ required String toolName, required String output, required bool isError, }) = ToolResultEvent; const factory StreamEvent.approvalRequired({ required String taskId, required String command, required int riskLevel, }) = ApprovalRequiredEvent; const factory StreamEvent.completed({required String summary}) = CompletedEvent; const factory StreamEvent.error({required String message}) = ErrorEvent; // ★ 驻留指令相关事件 const factory StreamEvent.standingOrderDraft({ required Map draft, // Agent 提取的指令草案 }) = StandingOrderDraftEvent; const factory StreamEvent.standingOrderConfirmed({ required String orderId, required String orderName, }) = StandingOrderConfirmedEvent; } ``` ```dart // features/chat/domain/repositories/chat_repository.dart abstract class ChatRepository { /// 发送消息并获取流式响应 Stream sendMessage({ required String sessionId, required String message, List? attachments, }); /// 语音输入转文字后发送 Stream sendVoiceMessage({ required String sessionId, required String audioPath, }); /// 获取会话历史 Future>> getSessionHistory(String sessionId); /// 审批操作 Future> approveCommand(String taskId); Future> rejectCommand(String taskId, {String? reason}); /// 取消正在执行的任务 Future> cancelTask(String sessionId); /// ★ 驻留指令:确认 Agent 提取的指令草案 Future> confirmStandingOrder(String sessionId, Map draft); /// ★ 驻留指令:获取当前租户的所有驻留指令 Future>> getStandingOrders({String? statusFilter}); } ``` #### 4.2.2 Data 层 ```dart // features/chat/data/repositories/chat_repository_impl.dart class ChatRepositoryImpl implements ChatRepository { final ChatRemoteDataSource _remote; final ChatLocalDataSource _local; final WebSocketClient _wsClient; ChatRepositoryImpl({ required ChatRemoteDataSource remote, required ChatLocalDataSource local, required WebSocketClient wsClient, }) : _remote = remote, _local = local, _wsClient = wsClient; @override Stream sendMessage({ required String sessionId, required String message, List? attachments, }) async* { // 1. 通过 REST 发起任务 final taskResult = await _remote.createTask( sessionId: sessionId, message: message, attachments: attachments, ); // 2. 通过 WebSocket 接收流式事件 yield* _wsClient.eventStream .where((event) => event['sessionId'] == sessionId) .map((event) => StreamEventModel.fromJson(event).toDomain()) .takeWhile((event) => event is! CompletedEvent && event is! ErrorEvent) .handleError((error) { // 错误处理 }); } @override Future> approveCommand(String taskId) async { try { await _remote.approveCommand(taskId); return const ApiResult.success(null); } catch (e) { return ApiResult.failure(Failure.unknown(message: e.toString())); } } } ``` #### 4.2.3 Presentation 层 ```dart // features/chat/presentation/providers/chat_providers.dart /// 当前会话 ID @riverpod class CurrentSessionId extends _$CurrentSessionId { @override String? build() => null; void set(String id) => state = id; void clear() => state = null; } /// 消息列表(历史 + 实时) @riverpod class ChatMessages extends _$ChatMessages { @override List build() => []; void addMessage(ChatMessage message) { state = [...state, message]; } void updateLastAssistantMessage(String content) { if (state.isEmpty) return; final last = state.last; if (last.role == MessageRole.assistant) { state = [ ...state.sublist(0, state.length - 1), last.copyWith(content: last.content + content), ]; } } void addToolExecution(ToolExecution tool) { state = [ ...state, ChatMessage( id: const Uuid().v4(), role: MessageRole.assistant, content: '', timestamp: DateTime.now(), type: MessageType.toolUse, toolExecution: tool, ), ]; } void updateToolResult(String toolName, String output, bool isError) { // 找到最后一个匹配的 tool_use 消息并更新 final index = state.lastIndexWhere( (m) => m.toolExecution?.toolName == toolName && m.toolExecution?.status == ToolStatus.executing, ); if (index >= 0) { final msg = state[index]; state = [ ...state.sublist(0, index), msg.copyWith( toolExecution: msg.toolExecution!.copyWith( output: output, status: isError ? ToolStatus.error : ToolStatus.completed, ), ), ...state.sublist(index + 1), ]; } } } /// Agent 状态 @riverpod class AgentStatus extends _$AgentStatus { @override AgentState build() => AgentState.idle; void setThinking() => state = AgentState.thinking; void setExecuting(String toolName) => state = AgentState.executing; void setWaitingApproval() => state = AgentState.waitingApproval; void setIdle() => state = AgentState.idle; } enum AgentState { idle, thinking, executing, waitingApproval } /// 发送消息用例 @riverpod class SendMessage extends _$SendMessage { @override FutureOr build() {} Future send(String message) async { final sessionId = ref.read(currentSessionIdProvider); if (sessionId == null) return; final repo = ref.read(chatRepositoryProvider); final messages = ref.read(chatMessagesProvider.notifier); final agentStatus = ref.read(agentStatusProvider.notifier); // 添加用户消息 messages.addMessage(ChatMessage( id: const Uuid().v4(), role: MessageRole.user, content: message, timestamp: DateTime.now(), )); // 创建空的 assistant 消息(用于流式填充) messages.addMessage(ChatMessage( id: const Uuid().v4(), role: MessageRole.assistant, content: '', timestamp: DateTime.now(), isStreaming: true, )); agentStatus.setThinking(); // 监听流式事件 await for (final event in repo.sendMessage( sessionId: sessionId, message: message, )) { switch (event) { case ThinkingEvent(:final content): agentStatus.setThinking(); // 可选:显示思考过程 break; case TextEvent(:final content): messages.updateLastAssistantMessage(content); break; case ToolUseEvent(:final toolName, :final input): agentStatus.setExecuting(toolName); messages.addToolExecution(ToolExecution( toolName: toolName, input: jsonEncode(input), riskLevel: RiskLevel.l0, status: ToolStatus.executing, )); break; case ToolResultEvent(:final toolName, :final output, :final isError): messages.updateToolResult(toolName, output, isError); break; case ApprovalRequiredEvent(:final taskId, :final command, :final riskLevel): agentStatus.setWaitingApproval(); messages.addMessage(ChatMessage( id: const Uuid().v4(), role: MessageRole.system, content: '需要您的审批', timestamp: DateTime.now(), type: MessageType.approval, approvalRequest: ApprovalRequest( taskId: taskId, command: command, riskLevel: RiskLevel.fromInt(riskLevel), targetServer: '', expiresAt: DateTime.now().add(const Duration(minutes: 30)), ), )); break; case CompletedEvent(): case ErrorEvent(): agentStatus.setIdle(); break; // ★ 驻留指令:Agent 提取出指令草案,展示确认卡片 case StandingOrderDraftEvent(:final draft): messages.addMessage(ChatMessage( id: const Uuid().v4(), role: MessageRole.system, content: '驻留指令草案', timestamp: DateTime.now(), type: MessageType.standingOrderDraft, metadata: draft, )); break; case StandingOrderConfirmedEvent(:final orderId, :final orderName): messages.addMessage(ChatMessage( id: const Uuid().v4(), role: MessageRole.assistant, content: '✅ 驻留指令「$orderName」已创建($orderId)', timestamp: DateTime.now(), )); break; } } } } ``` #### 4.2.4 对话页面 UI ```dart // features/chat/presentation/pages/chat_page.dart class ChatPage extends ConsumerWidget { const ChatPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final messages = ref.watch(chatMessagesProvider); final agentStatus = ref.watch(agentStatusProvider); return Scaffold( appBar: AppBar( title: const Text('IT0 运维助手'), actions: [ // Agent 状态指示器 AgentStatusIndicator(status: agentStatus), // 引擎切换按钮 IconButton( icon: const Icon(Icons.swap_horiz), onPressed: () => _showEngineSelector(context, ref), ), ], ), body: Column( children: [ // 消息列表 Expanded( child: ListView.builder( reverse: true, itemCount: messages.length, itemBuilder: (context, index) { final message = messages[messages.length - 1 - index]; return _buildMessageWidget(message, ref); }, ), ), // 输入区域 ChatInputBar( onSendText: (text) => ref.read(sendMessageProvider.notifier).send(text), onVoiceStart: () => ref.read(voiceInputProvider.notifier).startListening(), onVoiceEnd: () => ref.read(voiceInputProvider.notifier).stopListening(), isAgentBusy: agentStatus != AgentState.idle, ), ], ), ); } Widget _buildMessageWidget(ChatMessage message, WidgetRef ref) { return switch (message.type) { MessageType.toolUse => ToolExecutionCard( execution: message.toolExecution!, ), MessageType.approval => ApprovalActionCard( request: message.approvalRequest!, onApprove: () => ref.read(chatRepositoryProvider) .approveCommand(message.approvalRequest!.taskId), onReject: () => ref.read(chatRepositoryProvider) .rejectCommand(message.approvalRequest!.taskId), ), MessageType.standingOrderDraft => StandingOrderDraftCard( draft: message.metadata!, onConfirm: () => ref.read(chatRepositoryProvider) .confirmStandingOrder( ref.read(currentSessionIdProvider)!, message.metadata!, ), onModify: (feedback) => ref.read(sendMessageProvider.notifier) .send(feedback), // 用户反馈修改意见,继续对话 ), _ => MessageBubble(message: message), }; } } ``` #### 4.2.5 ★ 驻留指令草案确认卡片 ```dart // features/chat/presentation/widgets/standing_order_draft_card.dart /// 当 Agent 从对话中提取出驻留指令草案时,展示此确认卡片 /// /// ┌─────────────────────────────────────────┐ /// │ 📋 驻留指令草案 │ /// │ │ /// │ 名称: 每日生产巡检 │ /// │ ⏰ 触发: 每天 08:00 │ /// │ 🎯 目标: 所有 prod 服务器 │ /// │ │ /// │ 📌 自治操作: │ /// │ • 检查 CPU/内存/磁盘 │ /// │ • 磁盘 >85% 自动清理日志 │ /// │ │ /// │ 🚨 升级规则: │ /// │ • 磁盘 >95% → 📞 电话通知 │ /// │ • 其他异常 → 📱 短信通知 │ /// │ │ /// │ ⚠️ 不会执行: 重启服务、修改配置 │ /// │ │ /// │ [修改] [确认创建] │ /// │ (语音/文字反馈) │ /// └─────────────────────────────────────────┘ /// /// 用户可以: /// - 点击「确认创建」→ 调用 confirmStandingOrder API /// - 点击「修改」→ 弹出输入框,用户语音/文字说明修改意见 /// - 直接语音说 "把时间改成7点" → Agent 更新草案后重新展示 class StandingOrderDraftCard extends StatelessWidget { final Map draft; final VoidCallback onConfirm; final ValueChanged onModify; // ... } ``` --- ### 4.3 语音交互 #### 4.3.1 语音输入(STT) ```dart // features/chat/presentation/providers/voice_providers.dart @riverpod class VoiceInput extends _$VoiceInput { SpeechToText? _speech; @override VoiceInputState build() => const VoiceInputState.idle(); Future startListening() async { _speech ??= SpeechToText(); final available = await _speech!.initialize( onError: (error) => state = VoiceInputState.error(error.errorMsg), ); if (!available) { state = const VoiceInputState.error('语音识别不可用'); return; } state = const VoiceInputState.listening(partialText: ''); _speech!.listen( onResult: (result) { if (result.finalResult) { state = VoiceInputState.completed(text: result.recognizedWords); // 自动发送 ref.read(sendMessageProvider.notifier).send(result.recognizedWords); } else { state = VoiceInputState.listening(partialText: result.recognizedWords); } }, localeId: 'zh_CN', // 中文识别 listenMode: ListenMode.dictation, ); } Future stopListening() async { await _speech?.stop(); } } @freezed sealed class VoiceInputState with _$VoiceInputState { const factory VoiceInputState.idle() = VoiceIdle; const factory VoiceInputState.listening({required String partialText}) = VoiceListening; const factory VoiceInputState.completed({required String text}) = VoiceCompleted; const factory VoiceInputState.error(String message) = VoiceError; } ``` #### 4.3.2 语音输出(TTS) ```dart // features/chat/presentation/providers/tts_provider.dart @riverpod class TtsPlayer extends _$TtsPlayer { FlutterTts? _tts; @override bool build() => false; // isPlaying Future speak(String text) async { _tts ??= FlutterTts(); await _tts!.setLanguage('zh-CN'); await _tts!.setSpeechRate(0.5); await _tts!.setVolume(1.0); state = true; await _tts!.speak(text); _tts!.setCompletionHandler(() => state = false); } Future stop() async { await _tts?.stop(); state = false; } } ``` --- ### 4.4 Tasks(运维任务) ```dart // features/tasks/domain/entities/ops_task.dart @freezed class OpsTask with _$OpsTask { const factory OpsTask({ required String id, required TaskType type, // inspection / deployment / rollback / scaling / recovery required String title, String? description, required TaskStatus status, // pending / running / waitingApproval / completed / failed required TaskPriority priority, required List targetServerIds, String? runbookId, String? agentSessionId, required DateTime createdAt, DateTime? completedAt, String? resultSummary, }) = _OpsTask; } ``` #### 预设快捷任务 ```dart // features/tasks/presentation/widgets/quick_task_panel.dart class QuickTaskPanel extends ConsumerWidget { // 预设的常用运维任务 static const quickTasks = [ QuickTask( icon: Icons.health_and_safety, label: '全面巡检', prompt: '对所有服务器执行全面健康检查,包括 CPU、内存、磁盘、网络、关键服务状态,给出摘要报告', type: TaskType.inspection, ), QuickTask( icon: Icons.cleaning_services, label: '日志清理', prompt: '检查所有服务器的日志目录大小,清理 30 天前的旧日志文件,报告释放的空间', type: TaskType.inspection, ), QuickTask( icon: Icons.security, label: '安全检查', prompt: '检查各服务器的安全状态:SSH 登录记录、异常进程、端口开放情况、SSL 证书到期时间', type: TaskType.inspection, ), QuickTask( icon: Icons.backup, label: '备份验证', prompt: '检查所有数据库备份的状态、最后备份时间、备份文件大小,验证备份完整性', type: TaskType.inspection, ), QuickTask( icon: Icons.rocket_launch, label: '部署应用', prompt: '请告诉我要部署什么应用到哪个环境,我会帮你执行部署流程', type: TaskType.deployment, ), QuickTask( icon: Icons.terminal, label: '自由对话', prompt: null, // 直接打开对话页面 type: null, ), ]; @override Widget build(BuildContext context, WidgetRef ref) { return GridView.builder( shrinkWrap: true, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, mainAxisSpacing: 12, crossAxisSpacing: 12, ), itemCount: quickTasks.length, itemBuilder: (context, index) { final task = quickTasks[index]; return QuickTaskCard( task: task, onTap: () => _executeQuickTask(context, ref, task), ); }, ); } } ``` --- ### 4.5 Approvals(审批中心) ```dart // features/approvals/presentation/pages/approval_page.dart /// 审批中心 — 展示所有待审批的命令 /// 每个审批卡片包含: /// - 命令内容(高亮语法) /// - 风险等级标签(L1/L2 颜色区分) /// - 目标服务器 /// - 剩余审批时间(倒计时) /// - 批准 / 拒绝按钮 /// - 可选:拒绝理由输入 /// 审批列表 provider @riverpod Stream> pendingApprovals(Ref ref) { final wsClient = ref.watch(webSocketClientProvider); // 初始加载 + WebSocket 实时更新 return wsClient.eventStream .where((event) => event['type'] == 'approval_update') .map((event) => (event['approvals'] as List) .map((a) => ApprovalRequest.fromJson(a)) .toList()); } ``` --- ### 4.6 Servers(服务器管理) ```dart // features/servers/domain/entities/server.dart @freezed class Server with _$Server { const factory Server({ required String id, required String name, required String host, required int port, required Environment environment, // dev / staging / prod required ServerRole role, // web / db / cache / worker / gateway String? clusterId, required ServerStatus status, // online / offline / warning / error ServerMetrics? latestMetrics, }) = _Server; } @freezed class ServerMetrics with _$ServerMetrics { const factory ServerMetrics({ required double cpuPercent, required double memoryPercent, required double diskPercent, required double networkInMbps, required double networkOutMbps, required int uptimeSeconds, required DateTime recordedAt, }) = _ServerMetrics; } ``` #### 服务器详情页 ``` ┌──────────────────────────────────────────┐ │ ← prod-1 [SSH] [More] │ ├──────────────────────────────────────────┤ │ Status: ● Online Uptime: 45d 12h │ │ IP: 10.0.1.11 Role: Web Server │ │ Env: Production Cluster: main-k8s │ ├──────────────────────────────────────────┤ │ │ │ CPU ▓▓▓▓▓▓▓▓░░░░░░░░ 45% │ │ MEM ▓▓▓▓▓▓▓▓▓▓░░░░░░ 62% │ │ DISK ▓▓▓▓▓▓▓▓▓▓▓░░░░░ 71% │ │ NET ↑ 12.3 Mbps ↓ 45.6 Mbps │ │ │ │ ── CPU 趋势 (24h) ────────────── │ │ [========== 折线图 ==========] │ │ │ │ ── 最近告警 ────────────────── │ │ ⚠ 14:23 内存使用率超过 80% │ │ ● 13:15 已恢复 │ │ │ │ ── 快捷操作 ────────────────── │ │ [查看日志] [重启服务] [健康检查] │ │ │ └──────────────────────────────────────────┘ ``` --- ### 4.7 Alerts(告警中心) ```dart // features/alerts/presentation/providers/alert_providers.dart /// 实时告警流 @riverpod class AlertNotifier extends _$AlertNotifier { @override List build() => []; /// 监听 WebSocket 告警事件 void startListening() { final wsClient = ref.read(webSocketClientProvider); wsClient.eventStream .where((event) => event['type'] == 'alert_fired' || event['type'] == 'alert_resolved') .listen((event) { if (event['type'] == 'alert_fired') { final alert = Alert.fromJson(event); state = [alert, ...state]; // 触发本地通知 _showLocalNotification(alert); } else { // 更新已恢复的告警 state = state.map((a) => a.id == event['alertId'] ? a.copyWith(status: AlertStatus.resolved) : a ).toList(); } }); } void _showLocalNotification(Alert alert) { // 使用 flutter_local_notifications 弹出系统通知 // 严重告警振动 + 声音 // 紧急告警使用全屏通知 } } ``` --- ### 4.8 Terminal(远程终端) ```dart // features/terminal/presentation/pages/terminal_page.dart /// 远程终端 — 通过 Agent 执行 SSH 命令 /// 使用 xterm.dart 渲染终端 UI /// 用户输入的命令通过 WebSocket 发送到 agent-service /// agent-service 通过 Claude Code 的 Bash 工具执行 /// 输出实时回传渲染 class TerminalPage extends ConsumerStatefulWidget { final String serverId; const TerminalPage({super.key, required this.serverId}); @override ConsumerState createState() => _TerminalPageState(); } class _TerminalPageState extends ConsumerState { late Terminal terminal; // xterm 终端实例 + WebSocket 双向绑定 // 用户输入 → WebSocket → agent-service → SSH → 服务器 // 服务器输出 → SSH → agent-service → WebSocket → xterm 渲染 } ``` --- ### 4.9 Settings(设置) ```dart // features/settings/presentation/pages/settings_page.dart /// 设置页面包含: /// /// 1. 引擎配置 /// - 当前引擎类型(Claude Code CLI / Claude API / Custom) /// - 切换引擎(下拉选择 + 确认对话框) /// - Claude Code 路径配置 /// - API Key 配置(仅 Claude API 模式) /// /// 2. 安全设置 /// - 风险等级策略(Level 1/2 的超时时间) /// - 命令黑名单管理 /// - 自动审批的 Runbook 列表 /// /// 3. 通知设置 /// - 各渠道开关(推送/短信/电话/邮件/Telegram/微信) /// - 升级策略配置 /// - 免打扰时段 /// /// 4. 语音设置 /// - STT 语言选择 /// - TTS 语速/音色 /// - 语音唤醒词(可选) /// /// 5. 外观 /// - 主题(深色/浅色/跟随系统) /// - 终端字体大小 /// - 仪表盘布局自定义 /// /// 6. ★ 租户管理 /// - 当前租户名称 + ID /// - 切换租户(弹出 TenantSelectPage 底部弹窗) /// - 租户资源用量概览(服务器/用户/Sessions 配额) ``` --- ### 4.10 ★ Standing Orders(驻留指令监控) ```dart // features/standing_orders/domain/entities/standing_order.dart @freezed class StandingOrder with _$StandingOrder { const factory StandingOrder({ required String id, // SO-20260208-001 required String name, required String description, required TriggerType triggerType, // cron / event / threshold / manual required Map triggerConfig, required Map targets, required String agentPrompt, required List skills, required int maxRiskLevel, required List escalationRules, required OrderStatus status, // active / paused / archived required DateTime createdAt, DateTime? lastExecutedAt, ExecutionSummary? lastExecution, }) = _StandingOrder; } @freezed class StandingOrderExecution with _$StandingOrderExecution { const factory StandingOrderExecution({ required String id, required String orderId, required ExecutionStatus status, // running / completed / failed / awaiting_human / aborted required DateTime startedAt, DateTime? completedAt, String? resultSummary, List? actionsTaken, String? error, String? escalationChannel, }) = _StandingOrderExecution; } ``` ```dart // features/standing_orders/presentation/pages/standing_orders_page.dart /// 驻留指令列表页 /// /// ┌──────────────────────────────────────────┐ /// │ 驻留指令 [筛选 ▾]│ /// ├──────────────────────────────────────────┤ /// │ │ /// │ ┌──────────────────────────────────┐ │ /// │ │ 📋 每日生产巡检 │ │ /// │ │ ⏰ 每天 08:00 · ● 活跃 │ │ /// │ │ 上次: 今天 08:00 ✓ 正常 │ │ /// │ │ 下次: 明天 08:00 │ │ /// │ │ [详情] [⏸ 暂停] │ │ /// │ └──────────────────────────────────┘ │ /// │ │ /// │ ┌──────────────────────────────────┐ │ /// │ │ 📋 磁盘空间监控 │ │ /// │ │ ⚡ 告警触发 · ● 活跃 │ │ /// │ │ 上次: 1小时前 ⚠ 已升级(电话) │ │ /// │ │ [详情] [⏸ 暂停] │ │ /// │ └──────────────────────────────────┘ │ /// │ │ /// │ ─── 今日执行摘要 ─── │ /// │ 执行 5 次 · 成功 4 · 升级 1 │ /// │ │ /// │ 提示: 通过 AI 对话创建新的驻留指令 │ /// │ [打开对话 →] │ /// │ │ /// ├──────────────────────────────────────────┤ /// │ [Dashboard] [Chat] [Tasks] [Orders] │ /// └──────────────────────────────────────────┘ ``` --- ### 4.11 ★ Agent Call(Agent 来电响应) 当 Agent 在无人值守执行中遇到紧急情况,通过 `comm-service` 发起语音电话或 IM 消息联系管理员。Flutter App 需要处理这种「**被动接收 Agent 通知**」的场景。 #### 4.11.1 来电通知流程(三层递进) ``` Agent 执行驻留指令 → 遇到紧急状况 ↓ report_escalation tool ops-service → comm-service → 三层递进升级 │ ├── ⏱ 第一层:App 推送唤醒 (0s) │ ├── FCM 推送 + WebSocket → Flutter App 全屏通知 │ ├── 用户点击「接听」→ 进入 App 内语音对话 │ │ └── WebSocket 音频流 ←→ voice-service (Pipecat) │ │ Agent 用语音详细汇报 + 听取指示(支持打断) │ ├── 用户直接点击「批准/拒绝」→ 快速决策 │ └── 用户点击「给 Agent 指示」→ 文字/语音输入 │ ├── ⏱ 第二层:Pipecat 电话语音对话 (2分钟无响应) │ ├── Pipecat 通过 Twilio 拨出电话 → 手机铃声响起 │ ├── 管理员接听 → Agent 直接开口说话(无 IVR 菜单): │ │ "您好,我是 IT0 Agent。prod-2 磁盘 97%, │ │ 我建议清理 /var/log 下超过7天的日志。您同意吗?" │ ├── 管理员语音回复 → Agent 理解并执行 │ └── 使用与 App 内完全相同的 Pipecat 引擎(Twilio 只是电话线路) │ └── ⏱ 第三层:扩大通知 (5+ 分钟无响应) ├── IM (Telegram/企业微信) → 结构化消息 + 操作按钮 ├── 短信 → 简要描述 + 回复 Y/N ├── 通知备用联系人(重复第一层) └── 10+ 分钟 → 邮件通知管理组 ``` **语音对话引擎说明**: IT0 使用 **Pipecat**(GitHub 10.1k stars)自部署实时语音对话引擎: - **STT**:faster-whisper(本地 GPU,中英文自动识别) - **TTS**:Kokoro-82M(82M 参数,中英双语,Apache 开源) - **LLM**:Claude API(Pipecat 原生 Anthropic 集成) - **VAD**:Silero(语音活动检测,支持打断 barge-in) - **延迟**:< 500ms 端到端(本地 GPU 推理) - **电话**:2 分钟 App 无响应 → Pipecat 通过 Twilio 拨号 → Agent 接通即说话(无 IVR) #### 4.11.2 App 内 Agent 通知卡片 ```dart // features/agent_call/presentation/widgets/agent_escalation_card.dart /// 当 Agent 通过 WebSocket/FCM 推送紧急升级通知时, /// App 展示全屏覆盖式通知(类似来电界面) /// /// ┌──────────────────────────────────────────┐ /// │ │ /// │ 🤖 IT0 Agent │ /// │ 紧 急 通 知 │ /// │ │ /// │ ───────────────────────────────── │ /// │ │ /// │ 驻留指令: 每日生产巡检 │ /// │ 服务器: prod-2 (10.0.1.22) │ /// │ │ /// │ ⚠️ 磁盘使用率已达 97% │ /// │ Agent 建议: 紧急清理 /var/log 目录 │ /// │ │ /// │ 当前等待您的决策... │ /// │ │ /// │ ───────────────────────────────── │ /// │ │ /// │ ┌──────────┐ ┌──────────────┐ │ /// │ │ ❌ 拒绝 │ │ ✅ 批准执行 │ │ /// │ │ (中止) │ │ │ │ /// │ └──────────┘ └──────────────┘ │ /// │ │ /// │ ┌──────────────────────────────┐ │ /// │ │ 🎙️ 语音对话(详细沟通) │ │ /// │ │ → 接入 voice-service Pipecat │ │ /// │ └──────────────────────────────┘ │ /// │ │ /// │ ┌──────────────────────────────┐ │ /// │ │ 💬 文字指示... │ │ /// │ └──────────────────────────────┘ │ /// │ │ /// │ ⏱ 2 分钟内未响应将升级为电话通知 │ /// │ │ /// └──────────────────────────────────────────┘ /// /// 交互方式: /// - 「批准执行」→ 调用 API 回复 approve → Agent 继续执行 /// - 「拒绝」→ 调用 API 回复 reject → Agent 中止任务 /// - 「语音对话」→ 跳转 VoiceCallPage,建立 WebSocket 音频流到 voice-service /// Agent 用语音详细汇报 + 听取管理员语音指示(支持打断) /// - 「文字指示」→ 弹出输入框,输入文字指令 /// 例如 "不要清理,先查看哪些日志最大" → Agent 按指示执行 /// - 2分钟无响应 → Pipecat 通过 Twilio 拨出电话,Agent 接通即语音对话 class AgentEscalationCard extends ConsumerWidget { /* ... */ } ``` #### 4.11.3 可打断式语音对话(Pipecat Voice Call) ```dart // features/agent_call/presentation/pages/voice_call_page.dart /// App 内实时语音对话页面 — 连接 voice-service (Pipecat) /// /// ┌──────────────────────────────────────────┐ /// │ │ /// │ 🤖 IT0 Agent │ /// │ 实 时 语 音 对 话 │ /// │ │ /// │ ◉ 正在通话 02:35 │ /// │ │ /// │ Agent: "prod-2 的磁盘使用率已达97%, │ /// │ 我建议清理 /var/log 下超过7天的 │ /// │ 日志文件。您同意吗?" │ /// │ │ /// │ [===== 语音波形可视化 =====] │ /// │ │ /// │ 💡 您可以随时打断说话 │ /// │ │ /// │ ┌────┐ ┌──────┐ ┌────┐ │ /// │ │ 🔇 │ │ ✅ │ │ 📞 │ │ /// │ │静音 │ │快速批准│ │挂断 │ │ /// │ └────┘ └──────┘ └────┘ │ /// │ │ /// │ ── 对话记录(实时转写)── │ /// │ Agent: prod-2 磁盘使用率97%... │ /// │ You: 先看看是哪些文件最大 │ /// │ Agent: 好的,正在执行 du -sh... │ /// │ Agent: 最大的文件是 access.log 2.1GB... │ /// │ You: 清理超过3天的日志吧 │ /// │ Agent: 好的,正在执行清理... │ /// │ │ /// └──────────────────────────────────────────┘ /// /// ★ 技术实现(基于 Pipecat voice-service): /// /// ┌─ Flutter App ─────────────────────────────────────┐ /// │ │ /// │ 麦克风录音 (PCM 16kHz) │ /// │ ↓ │ /// │ WebSocket 发送音频帧 ───→ voice-service:3008 │ /// │ │ │ /// │ ├─ Silero VAD │ /// │ │ (检测说话/沉默) │ /// │ ├─ faster-whisper │ /// │ │ (语音→文字) │ /// │ ├─ Claude API │ /// │ │ (理解+决策+回复) │ /// │ └─ Kokoro-82M │ /// │ (文字→语音) │ /// │ ↓ │ /// │ WebSocket 接收音频帧 ←──── 回复音频流 │ /// │ ↓ │ /// │ AudioPlayer 播放回复语音 │ /// │ + │ /// │ 实时转写文字显示在底部(辅助理解 + 留痕) │ /// │ │ /// └────────────────────────────────────────────────────┘ /// /// 关键特性: /// - 打断(barge-in):用户开始说话 → Pipecat VAD 检测 → /// 自动停止 TTS 播放 + 取消 LLM 正在生成的回复 /// - 延迟 < 500ms:所有 STT/TTS 在 voice-service 本地 GPU 运行 /// - 实时转写:voice-service 通过 WebSocket 同时发送音频帧 + 文字转写 /// - 自动检测语言:中文/英文自动切换(faster-whisper 多语言支持) /// - 对话上下文:voice-service 从 agent-service 获取驻留指令上下文 class VoiceCallPage extends ConsumerStatefulWidget { /* ... */ } ``` ```dart // features/agent_call/data/voice_session_service.dart /// 管理与 voice-service 的 WebSocket 音频流连接 class VoiceSessionService { WebSocketChannel? _channel; final AudioRecorder _recorder; // record 包 final AudioPlayer _player; // just_audio 包 /// 建立语音对话连接 Future connect({ required String voiceSessionId, required String baseUrl, // voice-service URL }) async { _channel = WebSocketChannel.connect( Uri.parse('wss://$baseUrl/ws/voice/$voiceSessionId'), ); // 接收 voice-service 的消息(音频帧 + 转写文字) _channel!.stream.listen((data) { if (data is List) { // 二进制:音频帧 → 播放 _player.feedPcmData(Uint8List.fromList(data)); } else { // JSON:转写文字 / 状态更新 final msg = jsonDecode(data as String); switch (msg['type']) { case 'transcript': // Agent 或用户的转写文字 onTranscript?.call(msg['role'], msg['text']); case 'agent_action': // Agent 正在执行的操作 onAgentAction?.call(msg['action']); case 'session_end': // 对话结束 disconnect(); } } }); // 开始录音,持续发送音频帧到 voice-service await _startStreaming(); } Future _startStreaming() async { final stream = await _recorder.startStream(RecordConfig( encoder: AudioEncoder.pcm16bits, sampleRate: 16000, numChannels: 1, )); stream.listen((data) { _channel?.sink.add(data); // PCM 音频帧 → voice-service }); } Future disconnect() async { await _recorder.stop(); await _channel?.sink.close(); } } ``` #### 4.11.4 Agent Call Provider ```dart // features/agent_call/presentation/providers/agent_call_providers.dart /// 监听 WebSocket 的升级通知事件 @Riverpod(keepAlive: true) class AgentCallListener extends _$AgentCallListener { VoiceSessionService? _voiceSession; @override AgentCallState build() => const AgentCallState.idle(); void initialize() { final wsClient = ref.read(webSocketClientProvider); wsClient.eventStream .where((event) => event['type'] == 'escalation_notification') .listen((event) { final notification = EscalationNotification.fromJson(event); switch (notification.priority) { case 'urgent': // 紧急:全屏通知 + 振动 + 声音(类似来电) state = AgentCallState.incoming(notification: notification); _showFullScreenNotification(notification); break; case 'high': // 高优先级:弹出通知卡片 state = AgentCallState.incoming(notification: notification); _showOverlayNotification(notification); break; case 'normal': // 普通:系统通知栏 _showSystemNotification(notification); break; case 'low': // 低优先级:静默记录 break; } }); } /// 用户快速响应(批准/拒绝) Future respond(String action, {String? instruction}) async { final notification = (state as AgentCallIncoming).notification; await ref.read(chatRepositoryProvider).respondToEscalation( executionId: notification.executionId, action: action, // 'approve' | 'reject' | 'instruct' instruction: instruction, ); state = const AgentCallState.idle(); } /// ★ 用户选择「语音对话」→ 连接 voice-service (Pipecat) Future startVoiceCall() async { final notification = (state as AgentCallIncoming).notification; _voiceSession = VoiceSessionService(); state = AgentCallState.inCall( notification: notification, duration: Duration.zero, ); // 连接 Pipecat voice-service WebSocket 音频流 await _voiceSession!.connect( voiceSessionId: notification.voiceSessionId, // 预创建的语音会话 ID baseUrl: ref.read(configProvider).voiceServiceUrl, ); // 启动通话计时 _startTimer(); } /// 挂断语音对话 Future endVoiceCall() async { await _voiceSession?.disconnect(); _voiceSession = null; state = const AgentCallState.idle(); } } @freezed sealed class AgentCallState with _$AgentCallState { const factory AgentCallState.idle() = AgentCallIdle; const factory AgentCallState.incoming({ required EscalationNotification notification, }) = AgentCallIncoming; const factory AgentCallState.inCall({ required EscalationNotification notification, required Duration duration, }) = AgentCallInProgress; } /// 升级通知数据 @freezed class EscalationNotification with _$EscalationNotification { const factory EscalationNotification({ required String executionId, required String orderId, required String orderName, required String serverName, required String summary, required String suggestion, required String priority, // urgent/high/normal/low required String voiceSessionId, // ★ 预创建的 voice-service 会话 ID }) = _EscalationNotification; factory EscalationNotification.fromJson(Map json) => _$EscalationNotificationFromJson(json); } ``` --- ## 5. 路由配置 ```dart // core/router/app_router.dart final appRouter = GoRouter( initialLocation: '/dashboard', redirect: (context, state) { // 未登录重定向到登录页 final isLoggedIn = /* check auth state */; if (!isLoggedIn && state.matchedLocation != '/login') { return '/login'; } // ★ 多租户:已登录但未选择租户 → 跳转租户选择页 final hasTenant = /* check currentTenantProvider != null */; if (isLoggedIn && !hasTenant && state.matchedLocation != '/select-tenant') { return '/select-tenant'; } return null; }, routes: [ GoRoute(path: '/login', builder: (_, __) => const LoginPage()), // ★ 登录后的租户选择页(多租户时才显示,单租户自动跳过) GoRoute(path: '/select-tenant', builder: (_, __) => const TenantSelectPage()), // 底部导航 Shell ShellRoute( builder: (_, __, child) => MainShell(child: child), routes: [ GoRoute( path: '/dashboard', builder: (_, __) => const DashboardPage(), ), GoRoute( path: '/chat', builder: (_, __) => const ChatPage(), routes: [ GoRoute( path: ':sessionId', builder: (_, state) => ChatPage( sessionId: state.pathParameters['sessionId'], ), ), ], ), GoRoute( path: '/tasks', builder: (_, __) => const TasksPage(), routes: [ GoRoute( path: ':taskId', builder: (_, state) => TaskDetailPage( taskId: state.pathParameters['taskId']!, ), ), ], ), GoRoute( path: '/alerts', builder: (_, __) => const AlertsPage(), ), GoRoute( path: '/servers', builder: (_, __) => const ServersPage(), routes: [ GoRoute( path: ':serverId', builder: (_, state) => ServerDetailPage( serverId: state.pathParameters['serverId']!, ), ), GoRoute( path: ':serverId/terminal', builder: (_, state) => TerminalPage( serverId: state.pathParameters['serverId']!, ), ), ], ), GoRoute( path: '/approvals', builder: (_, __) => const ApprovalsPage(), ), // ★ 驻留指令 GoRoute( path: '/standing-orders', builder: (_, __) => const StandingOrdersPage(), routes: [ GoRoute( path: ':orderId', builder: (_, state) => StandingOrderDetailPage( orderId: state.pathParameters['orderId']!, ), ), ], ), ], ), GoRoute( path: '/settings', builder: (_, __) => const SettingsPage(), ), // ★ Agent 来电响应(全屏覆盖式,通过 overlay 或 push 导航进入) GoRoute( path: '/agent-call', builder: (_, __) => const AgentEscalationPage(), ), GoRoute( path: '/agent-voice-call', builder: (_, __) => const VoiceCallPage(), ), ], ); ``` --- ## 6. 主题设计 ```dart // core/theme/app_theme.dart class AppTheme { /// IT0 使用深色主题为主(运维工具审美) static ThemeData get dark => ThemeData( brightness: Brightness.dark, colorScheme: ColorScheme.dark( primary: const Color(0xFF6C63FF), // 主色:偏紫的蓝 secondary: const Color(0xFF03DAC6), // 辅助色:青绿 surface: const Color(0xFF1E1E2E), // 卡片背景 error: const Color(0xFFCF6679), ), scaffoldBackgroundColor: const Color(0xFF0D0D1A), // 深色背景 cardTheme: CardTheme( color: const Color(0xFF1E1E2E), elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), // 终端风格字体 textTheme: const TextTheme( bodyMedium: TextStyle(fontFamily: 'JetBrains Mono', fontSize: 14), ), ); /// 状态颜色 static const statusColors = { 'online': Color(0xFF4CAF50), // 绿色 'warning': Color(0xFFFF9800), // 橙色 'error': Color(0xFFF44336), // 红色 'offline': Color(0xFF9E9E9E), // 灰色 }; /// 风险等级颜色 static const riskColors = { 0: Color(0xFF4CAF50), // L0 绿色 — 安全 1: Color(0xFFFF9800), // L1 橙色 — 需确认 2: Color(0xFFF44336), // L2 红色 — 需审批 3: Color(0xFF9C27B0), // L3 紫色 — 禁止 }; } ``` --- ## 7. Dependency Injection(Riverpod Provider 树) ```dart // 全局基础设施 Providers @Riverpod(keepAlive: true) AppConfig appConfig(Ref ref) => AppConfig.fromEnvironment(); @Riverpod(keepAlive: true) DioClient dioClient(Ref ref) { final config = ref.watch(appConfigProvider); final tokenStorage = ref.watch(tokenStorageProvider); return DioClient(config: config, tokenStorage: tokenStorage); } @Riverpod(keepAlive: true) WebSocketClient webSocketClient(Ref ref) { final config = ref.watch(appConfigProvider); final tokenStorage = ref.watch(tokenStorageProvider); final client = WebSocketClient(config: config, tokenStorage: tokenStorage); // App 启动时自动连接 client.connect(); ref.onDispose(() => client.dispose()); return client; } @Riverpod(keepAlive: true) TokenStorage tokenStorage(Ref ref) => TokenStorage(); // ★ 多租户 Providers /// 当前用户可访问的租户列表(登录后获取) @Riverpod(keepAlive: true) class TenantList extends _$TenantList { @override Future> build() async { final dio = ref.watch(dioClientProvider); final response = await dio.dio.get('/tenants'); return (response.data as List) .map((e) => TenantSummary.fromJson(e)) .toList(); } } /// 当前选中的租户(持久化到本地存储) @Riverpod(keepAlive: true) class CurrentTenant extends _$CurrentTenant { @override String? build() { // 从 SharedPreferences 恢复上次选中的租户 final prefs = ref.watch(sharedPreferencesProvider); return prefs.getString('current_tenant_id'); } void switchTenant(String tenantId) { state = tenantId; final prefs = ref.read(sharedPreferencesProvider); prefs.setString('current_tenant_id', tenantId); // 切换后使所有数据 Provider 失效 → 触发重新拉取 ref.invalidateSelf(); } } /// Dio 拦截器:自动读取 currentTenantProvider 并注入到每个请求 // core/tenant/tenant_interceptor.dart class TenantInterceptor extends Interceptor { // 通过 ProviderContainer 全局访问(非 widget 上下文) static String? _currentTenantId; static void updateTenantId(String? id) => _currentTenantId = id; @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { if (_currentTenantId != null) { options.headers['X-Tenant-Id'] = _currentTenantId; } handler.next(options); } } // Feature-level Providers(以 chat 为例) @riverpod ChatRemoteDataSource chatRemoteDataSource(Ref ref) { final dio = ref.watch(dioClientProvider); return ChatRemoteDataSource(dio: dio.dio); } @riverpod ChatLocalDataSource chatLocalDataSource(Ref ref) { return ChatLocalDataSource(); } @riverpod ChatRepository chatRepository(Ref ref) { return ChatRepositoryImpl( remote: ref.watch(chatRemoteDataSourceProvider), local: ref.watch(chatLocalDataSourceProvider), wsClient: ref.watch(webSocketClientProvider), ); } ``` --- ## 8. 推送通知集成 ```dart // core/notifications/fcm_service.dart /// Firebase Cloud Messaging 集成 /// /// 1. 后端 comm-service 在以下场景发送 FCM 推送: /// - 告警触发(severity >= warning) /// - 审批请求(需要用户操作) /// - 任务完成/失败 /// - 升级通知(短信/电话后仍需 App 确认) /// /// 2. 前端处理推送: /// - 前台:显示应用内 Snackbar / Banner /// - 后台:系统通知栏 /// - 点击通知:导航到对应页面(告警详情/审批/任务详情) /// /// 3. 通知 channel 分级: /// - high: 紧急告警、审批请求(振动+声音+全屏) /// - default: 一般告警、任务完成 /// - low: 信息通知 ``` --- ## 9. 开发规范 ### 9.1 命名规范 | 类型 | 规范 | 示例 | |------|------|------| | 文件名 | snake_case | `chat_repository_impl.dart` | | 类名 | PascalCase | `ChatRepositoryImpl` | | 变量/方法 | camelCase | `sendMessage()` | | 常量 | camelCase | `maxReconnectAttempts` | | Provider | camelCase + Provider 后缀 | `chatRepositoryProvider` | | 枚举值 | camelCase | `TaskStatus.waitingApproval` | ### 9.2 分层规则 ``` presentation → domain ← data ↓ ↑ ↓ widgets entities models providers usecases datasources pages repos(抽象) repos(实现) ``` - **domain 层零依赖**:不导入 Flutter、Riverpod、Dio 等任何外部包 - **data 层实现 domain 接口**:Repository 实现、Model ↔ Entity 转换 - **presentation 层**:只依赖 domain 层的 entities 和 usecases ### 9.3 代码生成 ```bash # freezed + json_serializable + riverpod_generator flutter pub run build_runner build --delete-conflicting-outputs ``` 每个 `@freezed` 类都需要 `part '文件名.freezed.dart'` 和 `part '文件名.g.dart'`。 --- ## 10. 开发路线图 ### Phase 1: 基础骨架(Week 1-2) - [ ] Flutter 项目初始化(Riverpod + go_router + Dio) - [ ] 核心基础设施(DioClient、WebSocketClient、主题) - [ ] auth feature(登录页、JWT 存储、自动刷新) - [ ] 底部导航 Shell + 空白页面框架 ### Phase 2: 核心交互(Week 3-4) - [ ] chat feature 完整实现(文字对话 + 流式渲染) - [ ] 工具执行卡片(显示 Bash 命令和输出) - [ ] 审批操作卡片(内联批准/拒绝) - [ ] Agent 状态指示器(thinking/executing/waiting) ### Phase 3: 运维功能(Week 5-6) - [ ] dashboard feature(仪表盘 + 快捷操作) - [ ] servers feature(服务器列表 + 详情 + 指标图表) - [ ] tasks feature(任务列表 + 创建 + 详情) - [ ] 预设快捷任务面板 ### Phase 4: 语音与通知(Week 7-8) - [ ] 语音输入(speech_to_text) - [ ] 语音输出(flutter_tts) - [ ] FCM 推送集成 - [ ] 告警中心 + 本地通知 ### Phase 5: 高级功能(Week 9-10) - [ ] terminal feature(远程终端) - [ ] approvals feature(审批中心独立页面) - [ ] settings feature(引擎切换、通知配置、安全策略) - [ ] 离线缓存 + 暗色/亮色主题切换 --- ## 11. pubspec.yaml 参考 ```yaml name: it0_app description: IT Operations Intelligent Agent environment: sdk: '>=3.2.0 <4.0.0' dependencies: flutter: sdk: flutter # 状态管理 flutter_riverpod: ^2.5.0 riverpod_annotation: ^2.3.0 # 路由 go_router: ^14.0.0 # 网络 dio: ^5.4.0 web_socket_channel: ^3.0.0 # 数据模型 freezed_annotation: ^2.4.0 json_annotation: ^4.9.0 # 本地存储 hive: ^4.0.0 hive_flutter: ^2.0.0 shared_preferences: ^2.3.0 # 语音 speech_to_text: ^7.0.0 flutter_tts: ^4.0.0 # 推送通知 firebase_messaging: ^15.0.0 flutter_local_notifications: ^18.0.0 # UI 组件 fl_chart: ^0.69.0 # 图表 flutter_xterm: ^4.0.0 # 终端模拟 flutter_syntax_view: ^4.0.0 # 代码高亮 shimmer: ^3.0.0 # 加载骨架屏 lottie: ^3.0.0 # 动画 # 工具 uuid: ^4.4.0 intl: ^0.19.0 logger: ^2.4.0 connectivity_plus: ^6.0.0 # 网络状态检测 dev_dependencies: flutter_test: sdk: flutter # 代码生成 build_runner: ^2.4.0 freezed: ^2.5.0 json_serializable: ^6.8.0 riverpod_generator: ^2.4.0 # Lint flutter_lints: ^4.0.0 # 测试 mockito: ^5.4.0 mocktail: ^1.0.0 ```