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