zhipeng 1 год назад
Родитель
Сommit
d16299341d

BIN
assets/images/icon_chat_add_file.webp


BIN
assets/images/icon_chat_send.webp


BIN
assets/images/icon_chat_xiao_tin.webp


+ 2 - 3
lib/base/base_app_request.dart

@@ -1,4 +1,5 @@
 import 'package:electronic_assistant/base/base_request.dart';
+import 'package:electronic_assistant/data/repositories/account_repository.dart';
 import 'package:json_annotation/json_annotation.dart';
 
 part 'base_app_request.g.dart';
@@ -6,9 +7,7 @@ part 'base_app_request.g.dart';
 @JsonSerializable()
 class BaseAppRequest extends BaseRequest {
   @JsonKey(name: "authToken")
-  late String authToken;
-
-  BaseAppRequest(this.authToken);
+  String? authToken = accountRepository.token;
 
   @override
   Map<String, dynamic> toJson() => _$BaseAppRequestToJson(this);

+ 4 - 1
lib/base/base_request.dart

@@ -1,5 +1,6 @@
 import 'dart:io';
 
+import 'package:electronic_assistant/data/repositories/account_repository.dart';
 import 'package:electronic_assistant/utils/app_info_util.dart';
 import 'package:flutter/cupertino.dart';
 import 'package:json_annotation/json_annotation.dart';
@@ -112,5 +113,7 @@ class BaseRequest {
     channelName = "";
   }
 
-  void initDeviceInfo() {}
+  void initDeviceInfo() {
+    androidId = "testtesttest";
+  }
 }

+ 5 - 2
lib/data/api/atmob_api.dart

@@ -2,6 +2,7 @@ import 'package:dio/dio.dart';
 import 'package:electronic_assistant/base/base_response.dart';
 import 'package:electronic_assistant/data/api/network_module.dart';
 import 'package:electronic_assistant/data/api/request/login_request.dart';
+import 'package:electronic_assistant/data/api/request/user_info_update_request.dart';
 import 'package:electronic_assistant/data/api/request/verification_code_request.dart';
 import 'package:electronic_assistant/data/api/response/login_response.dart';
 import 'package:electronic_assistant/data/consts/constants.dart';
@@ -19,7 +20,9 @@ abstract class AtmobApi {
 
   @POST("/project/secretary/v1/user/login")
   Future<BaseResponse<LoginResponse>> login(@Body() LoginRequest request);
+
+  @POST("/project/secretary/v1/user/info/update")
+  Future<BaseResponse> updateUserInfo(@Body() UserInfoUpdateRequest request);
 }
 
-// final atmobApi = AtmobApi(defaultDio, baseUrl: 'https://api.atmob.com');
-final atmobApi = AtmobApi(defaultDio, baseUrl: Constants.baseUrl);
+final atmobApi = AtmobApi(defaultDio, baseUrl: Constants.baseUrl);

+ 77 - 0
lib/data/api/atmob_stream_api.c.dart

@@ -0,0 +1,77 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'atmob_stream_api.dart';
+
+// **************************************************************************
+// RetrofitGenerator
+// **************************************************************************
+
+// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers
+
+class _AtmobStreamApi implements AtmobStreamApi {
+  _AtmobStreamApi(
+    this._dio, {
+    this.baseUrl,
+  });
+
+  final Dio _dio;
+
+  String? baseUrl;
+
+  @override
+  Future<ResponseBody> chat(ChatRequest request) async {
+    const _extra = <String, dynamic>{};
+    final queryParameters = <String, dynamic>{};
+    final _headers = <String, dynamic>{};
+    final _data = <String, dynamic>{};
+    _data.addAll(request.toJson());
+    final _result =
+        await _dio.fetch<ResponseBody>(_setStreamType<ResponseBody>(Options(
+      method: 'POST',
+      headers: _headers,
+      extra: _extra,
+    )
+            .compose(
+              _dio.options,
+              '/project/secretary/v1/chat/stream',
+              queryParameters: queryParameters,
+              data: _data,
+            )
+            .copyWith(
+                baseUrl: _combineBaseUrls(
+              _dio.options.baseUrl,
+              baseUrl,
+            ))));
+    return _result.data!;
+  }
+
+  RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
+    if (T != dynamic &&
+        !(requestOptions.responseType == ResponseType.bytes ||
+            requestOptions.responseType == ResponseType.stream)) {
+      if (T == String) {
+        requestOptions.responseType = ResponseType.plain;
+      } else {
+        requestOptions.responseType = ResponseType.json;
+      }
+    }
+    return requestOptions;
+  }
+
+  String _combineBaseUrls(
+    String dioBaseUrl,
+    String? baseUrl,
+  ) {
+    if (baseUrl == null || baseUrl.trim().isEmpty) {
+      return dioBaseUrl;
+    }
+
+    final url = Uri.parse(baseUrl);
+
+    if (url.isAbsolute) {
+      return url.toString();
+    }
+
+    return Uri.parse(dioBaseUrl).resolveUri(url).toString();
+  }
+}

+ 16 - 0
lib/data/api/atmob_stream_api.dart

@@ -0,0 +1,16 @@
+import 'package:dio/dio.dart' hide Headers;
+import 'package:dio/dio.dart';
+import 'package:electronic_assistant/data/api/request/chat_request.dart';
+import 'package:retrofit/retrofit.dart';
+
+import '../consts/Constants.dart';
+import 'network_module.dart';
+
+part 'atmob_stream_api.c.dart';
+
+abstract class AtmobStreamApi {
+
+  Future<ResponseBody> chat(@Body() ChatRequest request);
+}
+
+final atmobStreamApi = _AtmobStreamApi(streamDio, baseUrl: Constants.baseUrl);

+ 13 - 5
lib/data/api/network_module.dart

@@ -105,14 +105,22 @@ class _NetworkModule {
         //ListFormat? listFormat;
         ));
     dio.interceptors.add(PrettyDioLogger(
-        requestHeader: true,
-        requestBody: true,
-        responseBody: true,
-        responseHeader: true,
-        enabled: Constants.env != Constants.envProd,
+      requestHeader: true,
+      requestBody: true,
+      responseBody: true,
+      responseHeader: true,
+      enabled: Constants.env != Constants.envProd,
     ));
     return dio;
   }
+
+  static Dio _createStreamDio() {
+    return Dio(BaseOptions(
+      responseType: ResponseType.stream,
+    ));
+  }
 }
 
 final defaultDio = _NetworkModule._createDefaultDio();
+
+final streamDio = _NetworkModule._createStreamDio();

+ 16 - 0
lib/data/api/request/chat_request.dart

@@ -0,0 +1,16 @@
+import 'package:json_annotation/json_annotation.dart';
+
+import '../../../base/base_app_request.dart';
+
+part 'chat_request.g.dart';
+
+@JsonSerializable()
+class ChatRequest extends BaseAppRequest {
+  @JsonKey(name: "content")
+  late String content;
+
+  ChatRequest(this.content);
+
+  @override
+  Map<String, dynamic> toJson() => _$ChatRequestToJson(this);
+}

+ 0 - 1
lib/data/api/request/login_request.dart

@@ -1,4 +1,3 @@
-import 'package:electronic_assistant/base/base_request.dart';
 import 'package:json_annotation/json_annotation.dart';
 
 import '../../../base/app_base_request.dart';

+ 19 - 0
lib/data/api/request/user_info_update_request.dart

@@ -0,0 +1,19 @@
+import 'package:json_annotation/json_annotation.dart';
+
+import '../../../base/base_app_request.dart';
+
+part 'user_info_update_request.g.dart';
+
+@JsonSerializable()
+class UserInfoUpdateRequest extends BaseAppRequest {
+  @JsonKey(name: "profession")
+  late String profession;
+
+  @JsonKey(name: "post")
+  late String post;
+
+  UserInfoUpdateRequest(this.profession, this.post);
+
+  @override
+  Map<String, dynamic> toJson() => _$UserInfoUpdateRequestToJson(this);
+}

+ 2 - 0
lib/data/api/request/verification_code_request.dart

@@ -2,6 +2,8 @@ import 'package:json_annotation/json_annotation.dart';
 
 import '../../../base/app_base_request.dart';
 
+import '../../../base/base_app_request.dart';
+
 part 'verification_code_request.g.dart';
 
 @JsonSerializable()

+ 36 - 0
lib/data/repositories/chat_repository.dart

@@ -0,0 +1,36 @@
+import 'dart:convert';
+
+import 'package:electronic_assistant/base/base_response.dart';
+import 'package:electronic_assistant/data/api/request/chat_request.dart';
+import 'package:electronic_assistant/utils/http_handler.dart';
+import 'package:electronic_assistant/utils/sse_parse_util.dart';
+
+import '../api/atmob_stream_api.dart';
+
+class ChatRepository {
+  ChatRepository._() {}
+
+  Future<Stream<Message>> streamChat(String chatContent) {
+    return atmobStreamApi.chat(ChatRequest(chatContent)).then((response) async {
+      List<String>? contentType = response.headers['Content-Type'];
+      if (contentType != null) {
+        for (var value in contentType) {
+          if (value.contains('text/event-stream')) {
+            return response.stream;
+          } else if (value.contains('application/json')) {
+            BaseResponse<String> baseResponse = BaseResponse.fromJson(
+                jsonDecode(await response.stream
+                    .map((bytes) => utf8.decoder.convert(bytes))
+                    .toList()
+                    .then((value) => value.join())),
+                (json) => json as String);
+            throw ServerErrorException(baseResponse.code, baseResponse.message);
+          }
+        }
+      }
+      throw Exception('Invalid content type');
+    }).then((stream) => SSEParseUtil.parse(stream));
+  }
+}
+
+final chatRepository = ChatRepository._();

+ 19 - 0
lib/module/chat/controller.dart

@@ -0,0 +1,19 @@
+import 'package:electronic_assistant/base/base_controller.dart';
+import 'package:electronic_assistant/data/repositories/chat_repository.dart';
+import 'package:flutter/cupertino.dart';
+
+class ChatController extends BaseController {
+  @override
+  void onInit() {
+    super.onInit();
+
+    chatRepository.streamChat("以《黑神话:悟空》为题,写一篇小说").then((stream) {
+      stream.listen((event) {
+        debugPrint(
+            "id: ${event.id}, event: ${event.event}, data: ${event.data}");
+      });
+    }).catchError((e) {
+      debugPrint("error: $e");
+    });
+  }
+}

+ 170 - 0
lib/module/chat/view.dart

@@ -0,0 +1,170 @@
+import 'package:electronic_assistant/base/base_page.dart';
+import 'package:electronic_assistant/module/chat/controller.dart';
+import 'package:electronic_assistant/resource/colors.gen.dart';
+import 'package:electronic_assistant/utils/expand.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+
+import '../../resource/assets.gen.dart';
+
+class ChatPage extends BasePage<ChatController> {
+  const ChatPage({super.key});
+
+  @override
+  bool immersive() {
+    return true;
+  }
+
+  @override
+  Widget buildBody(BuildContext context) {
+    var controller = this.controller;
+    return Stack(
+      children: [
+        buildBackgroundGradient(),
+        buildTopGradient(),
+        Scaffold(
+          backgroundColor: Colors.transparent,
+          appBar: AppBar(
+            leading: IconButton(
+              icon: const Icon(Icons.arrow_back_ios),
+              onPressed: () {
+                Navigator.pop(context);
+              },
+            ),
+            scrolledUnderElevation: 0,
+            backgroundColor: Colors.transparent,
+            systemOverlayStyle: SystemUiOverlayStyle.dark,
+            centerTitle: true,
+            title: IntrinsicWidth(
+              child: Row(
+                mainAxisAlignment: MainAxisAlignment.center,
+                children: [
+                  Image(
+                      image: Assets.images.iconChatXiaoTin.provider(),
+                      width: 28.w,
+                      height: 28.w),
+                  Container(
+                      margin: EdgeInsets.only(left: 6.w),
+                      child: Text('聊天',
+                          style: TextStyle(
+                              fontSize: 16.w,
+                              fontWeight: FontWeight.bold,
+                              color: ColorName.primaryTextColor))),
+                ],
+              ),
+            ),
+          ),
+          body: buildBodyContent(),
+        )
+      ],
+    );
+  }
+
+  Widget buildBodyContent() {
+    return Column(
+      children: [
+        Expanded(
+            child: Padding(
+          padding: EdgeInsets.symmetric(horizontal: 12.w),
+          child: AnimatedList(
+            itemBuilder: _chatItemBuilder,
+            initialItemCount: 20,
+          ),
+        )),
+        Container(
+          margin: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h),
+          width: 1.sw,
+          decoration: BoxDecoration(
+              color: Colors.white,
+              borderRadius: BorderRadius.circular(24.w),
+              boxShadow: const [
+                BoxShadow(
+                  color: Color(0x4CDDDEE8),
+                  blurRadius: 10,
+                  offset: Offset(0, 4),
+                  spreadRadius: 0,
+                )
+              ]),
+          child: Padding(
+            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
+            child: Column(
+              children: [
+                Row(
+                  crossAxisAlignment: CrossAxisAlignment.end,
+                  children: [
+                    Expanded(
+                        child: Container(
+                      margin: EdgeInsets.only(right: 6.w),
+                      child: CupertinoTextField(
+                        padding: EdgeInsets.symmetric(vertical: 3.w),
+                        style: TextStyle(
+                            fontSize: 14.w, color: ColorName.primaryTextColor),
+                        placeholder: '有问题尽管问我~',
+                        placeholderStyle: TextStyle(
+                            fontSize: 14.w, color: const Color(0xFFAFAFAF)),
+                        textCapitalization: TextCapitalization.sentences,
+                        textInputAction: TextInputAction.newline,
+                        cursorColor: ColorName.colorPrimary,
+                        decoration: const BoxDecoration(),
+                        expands: true,
+                        maxLines: null,
+                        minLines: null,
+                      ),
+                    )),
+                    Image(
+                        image: Assets.images.iconChatAddFile.provider(),
+                        width: 26.w,
+                        height: 26.w),
+                    Container(
+                      margin: EdgeInsets.only(left: 16.w),
+                      child: Image(
+                          image: Assets.images.iconChatSend.provider(),
+                          width: 26.w,
+                          height: 26.w),
+                    )
+                  ],
+                )
+              ],
+            ),
+          ),
+        ),
+      ],
+    );
+  }
+
+  Widget _chatItemBuilder(
+      BuildContext context, int index, Animation<double> animation) {
+    return Text('聊天内容 $index');
+  }
+
+  Widget buildTopGradient() {
+    return Container(
+        width: 1.sw,
+        height: 128.h,
+        decoration: BoxDecoration(
+          gradient: LinearGradient(
+            colors: ['#E8EBFF'.toColor(), '#00E8EBFF'.toColor()],
+            begin: Alignment.topCenter,
+            end: Alignment.bottomCenter,
+            stops: const [0.5, 1.0],
+          ),
+        ));
+  }
+
+  Widget buildBackgroundGradient() {
+    return Container(
+      width: 1.sw,
+      height: 1.sh,
+      decoration: BoxDecoration(
+        gradient: LinearGradient(
+          colors: ['#F2F8F4'.toColor(), '#F6F6F6'.toColor()],
+          begin: Alignment.topCenter,
+          end: Alignment.bottomCenter,
+          stops: const [0, 1.0],
+        ),
+      ),
+    );
+  }
+}

+ 16 - 10
lib/module/main/view.dart

@@ -10,6 +10,7 @@ import 'package:flutter/material.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:get/get.dart';
 
+import '../../router/app_pages.dart';
 import '../files/view.dart';
 import '../home/view.dart';
 import 'drawer_view.dart';
@@ -47,17 +48,22 @@ class MainTabPage extends BasePage<MainController> {
     return true;
   }
 
-  Container buildAIChatBtn() {
-    return Container(
-      width: 68.w,
-      height: 68.w,
-      decoration: const BoxDecoration(
-        color: ColorName.white,
-        shape: BoxShape.circle,
+  Widget buildAIChatBtn() {
+    return GestureDetector(
+      onTap: () {
+        Get.toNamed(RoutePath.chat);
+      },
+      child: Container(
+        width: 68.w,
+        height: 68.w,
+        decoration: const BoxDecoration(
+          color: ColorName.white,
+          shape: BoxShape.circle,
+        ),
+        margin: EdgeInsets.only(top: 36.w),
+        padding: EdgeInsets.all(6.w),
+        child: Assets.images.mainTabSecretary.image(),
       ),
-      margin: EdgeInsets.only(top: 36.w),
-      padding: EdgeInsets.all(6.w),
-      child: Assets.images.mainTabSecretary.image(),
     );
   }
 

+ 6 - 0
lib/router/app_pages.dart

@@ -1,6 +1,8 @@
 import 'package:electronic_assistant/module/main/controller.dart';
 import 'package:get/get.dart';
 
+import '../module/chat/controller.dart';
+import '../module/chat/view.dart';
 import '../module/files/search/view.dart';
 import '../module/files/view.dart';
 import '../module/home/controller.dart';
@@ -25,6 +27,8 @@ abstract class RoutePath {
   static const files = '/files';
 
   static const fileSearch = '/fileSearch';
+
+  static const chat = '/chat';
 }
 
 class AppBinding extends Bindings {
@@ -33,6 +37,7 @@ class AppBinding extends Bindings {
     lazyPut(() => MainController());
     lazyPut(() => HomePageController());
     lazyPut(() => LoginController());
+    lazyPut(() => ChatController());
   }
 
   void lazyPut<S>(InstanceBuilderCallback<S> builder) {
@@ -46,4 +51,5 @@ final generalPages = [
   GetPage(name: RoutePath.mainTab, page: () => MainTabPage()),
   GetPage(name: RoutePath.files, page: () => const FilesPage()),
   GetPage(name: RoutePath.fileSearch, page: () => const FileSearchPage()),
+  GetPage(name: RoutePath.chat, page: () => const ChatPage()),
 ];

+ 6 - 0
lib/utils/http_handler.dart

@@ -1,4 +1,5 @@
 import 'dart:async';
+
 import 'package:electronic_assistant/base/base_response.dart';
 
 class HttpHandler {
@@ -23,4 +24,9 @@ class ServerErrorException implements Exception {
   final String? message;
 
   ServerErrorException(this.code, this.message);
+
+  @override
+  String toString() {
+    return 'ServerErrorException: code: $code, message: $message';
+  }
 }

+ 117 - 0
lib/utils/sse_parse_util.dart

@@ -0,0 +1,117 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:typed_data';
+
+import 'package:flutter/cupertino.dart';
+
+class SSEParseUtil {
+  static Stream<Message> parse(Stream<Uint8List> stream) {
+    return stream.transform(SSETransformer());
+  }
+}
+
+class Message {
+  final String id;
+  final String event;
+  final String data;
+  final int? retry;
+
+  Message(
+      {required this.id,
+      required this.event,
+      required this.data,
+      required this.retry});
+}
+
+class SSETransformer extends StreamTransformerBase<Uint8List, Message> {
+  @override
+  Stream<Message> bind(Stream<Uint8List> stream) {
+    return Stream.eventTransformed(
+      stream.map((bytes) => utf8.decoder.convert(bytes)),
+      (sink) => SseEventSink(sink),
+    );
+  }
+}
+
+class SseEventSink implements EventSink<String> {
+  static const _eventSeparator = "\n\n";
+  static const _fieldSeparator = "\n";
+  static const _dataPrefix = "data:";
+  static const _dataPrefixR = "data: ";
+  static const _idPrefix = "id:";
+  static const _idPrefixR = "id: ";
+  static const _eventPrefix = "event:";
+  static const _eventPrefixR = "event: ";
+  static const _retryPrefix = "retry:";
+  static const _retryPrefixR = "retry: ";
+  static const _commentPrefix = ":";
+  static const _commentPrefixR = ": ";
+
+  final EventSink<Message> _eventSink;
+
+  String? _id;
+  String? _event;
+  String _data = "";
+  int? _retry;
+
+  String buffer = "";
+  String completedEvent = "";
+
+  SseEventSink(this._eventSink);
+
+  @override
+  void add(String event) {
+    buffer += event;
+
+    if (buffer.endsWith("\n\n")) {
+      completedEvent = buffer;
+      buffer = "";
+      parseEvent();
+    }
+  }
+
+  @override
+  void addError(Object error, [StackTrace? stackTrace]) {
+    _eventSink.addError(error, stackTrace);
+  }
+
+  @override
+  void close() {
+    _eventSink.close();
+  }
+
+  void parseEvent() {
+    completedEvent = completedEvent.substring(0, completedEvent.length - 2);
+    completedEvent.split("\n").forEach((element) {
+      element = element.trim();
+      if (element.isEmpty) return;
+      if (element.startsWith(_commentPrefix) ||
+          element.startsWith(_commentPrefixR)) {
+        return;
+      }
+      if (element.startsWith(_retryPrefixR)) {
+        _retry = int.tryParse(element.substring(_retryPrefixR.length));
+      } else if (element.startsWith(_dataPrefixR)) {
+        _data += element.substring(_dataPrefixR.length);
+      } else if (element.startsWith(_eventPrefixR)) {
+        _event = element.substring(_eventPrefixR.length);
+      } else if (element.startsWith(_idPrefixR)) {
+        _id = element.substring(_idPrefixR.length);
+      } else if (element.startsWith(_idPrefix)) {
+        _id = element.substring(_idPrefix.length);
+      } else if (element.startsWith(_eventPrefix)) {
+        _event = element.substring(_eventPrefix.length);
+      } else if (element.startsWith(_dataPrefix)) {
+        _data += element.substring(_dataPrefix.length);
+      } else if (element.startsWith(_retryPrefix)) {
+        _retry = int.tryParse(element.substring(_retryPrefix.length));
+      }
+    });
+    _eventSink.add(Message(
+        id: _id ?? "", event: _event ?? "", data: _data, retry: _retry));
+    _id = null;
+    _event = null;
+    _data = "";
+    _retry = null;
+  }
+}

+ 12 - 12
pubspec.lock

@@ -423,18 +423,18 @@ packages:
     dependency: transitive
     description:
       name: leak_tracker
-      sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
+      sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
       url: "https://pub.dev"
     source: hosted
-    version: "10.0.5"
+    version: "10.0.4"
   leak_tracker_flutter_testing:
     dependency: transitive
     description:
       name: leak_tracker_flutter_testing
-      sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
+      sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
       url: "https://pub.dev"
     source: hosted
-    version: "3.0.5"
+    version: "3.0.3"
   leak_tracker_testing:
     dependency: transitive
     description:
@@ -471,18 +471,18 @@ packages:
     dependency: transitive
     description:
       name: material_color_utilities
-      sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
+      sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
       url: "https://pub.dev"
     source: hosted
-    version: "0.11.1"
+    version: "0.8.0"
   meta:
     dependency: transitive
     description:
       name: meta
-      sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
+      sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
       url: "https://pub.dev"
     source: hosted
-    version: "1.15.0"
+    version: "1.12.0"
   mime:
     dependency: transitive
     description:
@@ -772,10 +772,10 @@ packages:
     dependency: transitive
     description:
       name: test_api
-      sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
+      sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
       url: "https://pub.dev"
     source: hosted
-    version: "0.7.2"
+    version: "0.7.0"
   time:
     dependency: transitive
     description:
@@ -836,10 +836,10 @@ packages:
     dependency: transitive
     description:
       name: vm_service
-      sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc
+      sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
       url: "https://pub.dev"
     source: hosted
-    version: "14.2.4"
+    version: "14.2.1"
   watcher:
     dependency: transitive
     description: