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

```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<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 统一结果类型

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

```dart
// 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 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<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;
}
```

```dart
// 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 层

```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<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 层

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

```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<String, dynamic> draft;
  final VoidCallback onConfirm;
  final ValueChanged<String> 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<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）

```dart
// 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（运维任务）

```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<String> 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<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（服务器管理）

```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<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（远程终端）

```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<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（设置）

```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<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;
}
```

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

```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<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. 路由配置

```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<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. 推送通知集成

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