it0/docs/flutter-guide.md

2106 lines
72 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 ChatAI 对话交互)— 核心 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 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 使用 **Pipecat**GitHub 10.1k stars自部署实时语音对话引擎
- **STT**faster-whisper本地 GPU中英文自动识别
- **TTS**Kokoro-82M82M 参数中英双语Apache 开源)
- **LLM**Claude APIPipecat 原生 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 InjectionRiverpod 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 层零依赖**不导入 FlutterRiverpodDio 等任何外部包
- **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
- [ ] 核心基础设施DioClientWebSocketClient主题
- [ ] 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
```