it0/docs/flutter-guide.md

72 KiB
Raw Permalink Blame History

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 ArchitectureData → 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<T> 统一响应类型
│   │   ├── 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 客户端

// 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 客户端

// core/network/websocket_client.dart

class WebSocketClient {
  WebSocketChannel? _channel;
  final StreamController<Map<String, dynamic>> _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<Map<String, dynamic>> get eventStream => _eventController.stream;

  /// 连接
  Future<void> 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<String, dynamic>;
        _eventController.add(event);
      },
      onDone: () => _handleDisconnect(),
      onError: (error) => _handleDisconnect(),
    );

    _startHeartbeat();
  }

  /// 发送消息
  void send(Map<String, dynamic> 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 统一结果类型

// core/network/api_result.dart

@freezed
sealed class ApiResult<T> with _$ApiResult<T> {
  const factory ApiResult.success(T data) = Success<T>;
  const factory ApiResult.failure(Failure failure) = FailureResult<T>;
}

// 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

// features/dashboard/presentation/providers/dashboard_providers.dart

/// 服务器状态列表(实时更新)
@riverpod
Stream<List<ServerStatus>> 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> dashboardSummary(Ref ref) async {
  final repo = ref.watch(dashboardRepositoryProvider);
  return repo.getSummary();
}

/// 最近操作日志
@riverpod
Future<List<RecentOperation>> recentOperations(Ref ref) async {
  final repo = ref.watch(dashboardRepositoryProvider);
  return repo.getRecentOperations(limit: 20);
}

/// 活跃告警
@riverpod
Stream<List<Alert>> 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 ChatAI 对话交互)— 核心 Feature

4.2.1 Domain 层

// 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<String, dynamic> 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<String, dynamic> draft,  // Agent 提取的指令草案
  }) = StandingOrderDraftEvent;
  const factory StreamEvent.standingOrderConfirmed({
    required String orderId,
    required String orderName,
  }) = StandingOrderConfirmedEvent;
}
// features/chat/domain/repositories/chat_repository.dart

abstract class ChatRepository {
  /// 发送消息并获取流式响应
  Stream<StreamEvent> sendMessage({
    required String sessionId,
    required String message,
    List<String>? attachments,
  });

  /// 语音输入转文字后发送
  Stream<StreamEvent> sendVoiceMessage({
    required String sessionId,
    required String audioPath,
  });

  /// 获取会话历史
  Future<ApiResult<List<ChatMessage>>> getSessionHistory(String sessionId);

  /// 审批操作
  Future<ApiResult<void>> approveCommand(String taskId);
  Future<ApiResult<void>> rejectCommand(String taskId, {String? reason});

  /// 取消正在执行的任务
  Future<ApiResult<void>> cancelTask(String sessionId);

  /// ★ 驻留指令:确认 Agent 提取的指令草案
  Future<ApiResult<void>> confirmStandingOrder(String sessionId, Map<String, dynamic> draft);

  /// ★ 驻留指令:获取当前租户的所有驻留指令
  Future<ApiResult<List<StandingOrder>>> getStandingOrders({String? statusFilter});
}

4.2.2 Data 层

// 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<StreamEvent> sendMessage({
    required String sessionId,
    required String message,
    List<String>? 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<ApiResult<void>> 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 层

// 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<ChatMessage> 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<void> build() {}

  Future<void> 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

// 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 ★ 驻留指令草案确认卡片

// 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<String, dynamic> draft;
  final VoidCallback onConfirm;
  final ValueChanged<String> onModify;
  // ...
}

4.3 语音交互

4.3.1 语音输入STT

// features/chat/presentation/providers/voice_providers.dart

@riverpod
class VoiceInput extends _$VoiceInput {
  SpeechToText? _speech;

  @override
  VoiceInputState build() => const VoiceInputState.idle();

  Future<void> 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<void> 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

// features/chat/presentation/providers/tts_provider.dart

@riverpod
class TtsPlayer extends _$TtsPlayer {
  FlutterTts? _tts;

  @override
  bool build() => false; // isPlaying

  Future<void> 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<void> stop() async {
    await _tts?.stop();
    state = false;
  }
}

4.4 Tasks运维任务

// 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<String> targetServerIds,
    String? runbookId,
    String? agentSessionId,
    required DateTime createdAt,
    DateTime? completedAt,
    String? resultSummary,
  }) = _OpsTask;
}

预设快捷任务

// 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审批中心

// features/approvals/presentation/pages/approval_page.dart

/// 审批中心 — 展示所有待审批的命令
/// 每个审批卡片包含:
/// - 命令内容(高亮语法)
/// - 风险等级标签L1/L2 颜色区分)
/// - 目标服务器
/// - 剩余审批时间(倒计时)
/// - 批准 / 拒绝按钮
/// - 可选:拒绝理由输入

/// 审批列表 provider
@riverpod
Stream<List<ApprovalRequest>> 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服务器管理

// 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告警中心

// features/alerts/presentation/providers/alert_providers.dart

/// 实时告警流
@riverpod
class AlertNotifier extends _$AlertNotifier {
  @override
  List<Alert> 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远程终端

// 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<TerminalPage> createState() => _TerminalPageState();
}

class _TerminalPageState extends ConsumerState<TerminalPage> {
  late Terminal terminal;
  // xterm 终端实例 + WebSocket 双向绑定
  // 用户输入 → WebSocket → agent-service → SSH → 服务器
  // 服务器输出 → SSH → agent-service → WebSocket → xterm 渲染
}

4.9 Settings设置

// 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驻留指令监控

// 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<String, dynamic> triggerConfig,
    required Map<String, dynamic> targets,
    required String agentPrompt,
    required List<String> skills,
    required int maxRiskLevel,
    required List<EscalationRule> 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<String>? actionsTaken,
    String? error,
    String? escalationChannel,
  }) = _StandingOrderExecution;
}
// 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 CallAgent 来电响应)

当 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 使用 PipecatGitHub 10.1k stars自部署实时语音对话引擎

  • STTfaster-whisper本地 GPU中英文自动识别
  • TTSKokoro-82M82M 参数中英双语Apache 开源)
  • LLMClaude APIPipecat 原生 Anthropic 集成)
  • VADSilero语音活动检测支持打断 barge-in
  • 延迟< 500ms 端到端(本地 GPU 推理)
  • 电话2 分钟 App 无响应 → Pipecat 通过 Twilio 拨号 → Agent 接通即说话(无 IVR

4.11.2 App 内 Agent 通知卡片

// 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

// 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 { /* ... */ }
// 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<void> 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<int>) {
        // 二进制:音频帧 → 播放
        _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<void> _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<void> disconnect() async {
    await _recorder.stop();
    await _channel?.sink.close();
  }
}

4.11.4 Agent Call Provider

// 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<void> 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<void> 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<void> 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<String, dynamic> json) =>
      _$EscalationNotificationFromJson(json);
}

5. 路由配置

// 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. 主题设计

// 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 InjectionRiverpod Provider 树)

// 全局基础设施 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<List<TenantSummary>> 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. 推送通知集成

// 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 代码生成

# 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 参考

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