Преглед изворни кода

[feat]亲密度分析,截图回复-识图回复,识图回复接口,改为流式SSE

hezihao пре 7 месеци
родитељ
комит
85f8781c69

+ 0 - 12
lib/data/api/atmob_api.dart

@@ -280,23 +280,11 @@ abstract class AtmobApi {
   Future<BaseResponse<IntimacyAnalyzeChatConfigResponse>>
   getIntimacyAnalyzeChatConfig(@Body() AppBaseRequest request);
 
-  /// 对话分析
-  @POST("/project/keyboard/v1/intimacy/chat/analyze")
-  Future<BaseResponse<IntimacyChatAnalyzeResponse>> intimacyChatAnalyze(
-    @Body() IntimacyChatAnalyzeRequest request,
-  );
-
   /// 获取识图回复配置
   @POST("/project/keyboard/v1/intimacy/reply/config")
   Future<BaseResponse<IntimacyAnalyzeReplyConfigResponse>>
   getIntimacyAnalyzeReplyConfig(@Body() AppBaseRequest request);
 
-  /// 识图回复
-  @POST("/project/keyboard/v1/intimacy/reply/analyze")
-  Future<BaseResponse<IntimacyReplyAnalyzeResponse>> intimacyReplyAnalyze(
-    @Body() IntimacyReplyAnalyzeRequest request,
-  );
-
   // 获取星座梗语与解读
   @POST("/project/keyboard/v1/keyboard/memeExplain")
   Future<BaseResponse<KeyboardMemeExplainResponse>> getKeyboardMemeExplain(

+ 0 - 68
lib/data/api/atmob_api.g.dart

@@ -1275,40 +1275,6 @@ class _AtmobApi implements AtmobApi {
   }
 
   @override
-  Future<BaseResponse<IntimacyChatAnalyzeResponse>> intimacyChatAnalyze(
-    IntimacyChatAnalyzeRequest request,
-  ) async {
-    final _extra = <String, dynamic>{};
-    final queryParameters = <String, dynamic>{};
-    final _headers = <String, dynamic>{};
-    final _data = <String, dynamic>{};
-    _data.addAll(request.toJson());
-    final _options = _setStreamType<BaseResponse<IntimacyChatAnalyzeResponse>>(
-      Options(method: 'POST', headers: _headers, extra: _extra)
-          .compose(
-            _dio.options,
-            '/project/keyboard/v1/intimacy/chat/analyze',
-            queryParameters: queryParameters,
-            data: _data,
-          )
-          .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)),
-    );
-    final _result = await _dio.fetch<Map<String, dynamic>>(_options);
-    late BaseResponse<IntimacyChatAnalyzeResponse> _value;
-    try {
-      _value = BaseResponse<IntimacyChatAnalyzeResponse>.fromJson(
-        _result.data!,
-        (json) =>
-            IntimacyChatAnalyzeResponse.fromJson(json as Map<String, dynamic>),
-      );
-    } on Object catch (e, s) {
-      errorLogger?.logError(e, s, _options);
-      rethrow;
-    }
-    return _value;
-  }
-
-  @override
   Future<BaseResponse<IntimacyAnalyzeReplyConfigResponse>>
   getIntimacyAnalyzeReplyConfig(AppBaseRequest request) async {
     final _extra = <String, dynamic>{};
@@ -1346,40 +1312,6 @@ class _AtmobApi implements AtmobApi {
   }
 
   @override
-  Future<BaseResponse<IntimacyReplyAnalyzeResponse>> intimacyReplyAnalyze(
-    IntimacyReplyAnalyzeRequest request,
-  ) async {
-    final _extra = <String, dynamic>{};
-    final queryParameters = <String, dynamic>{};
-    final _headers = <String, dynamic>{};
-    final _data = <String, dynamic>{};
-    _data.addAll(request.toJson());
-    final _options = _setStreamType<BaseResponse<IntimacyReplyAnalyzeResponse>>(
-      Options(method: 'POST', headers: _headers, extra: _extra)
-          .compose(
-            _dio.options,
-            '/project/keyboard/v1/intimacy/reply/analyze',
-            queryParameters: queryParameters,
-            data: _data,
-          )
-          .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)),
-    );
-    final _result = await _dio.fetch<Map<String, dynamic>>(_options);
-    late BaseResponse<IntimacyReplyAnalyzeResponse> _value;
-    try {
-      _value = BaseResponse<IntimacyReplyAnalyzeResponse>.fromJson(
-        _result.data!,
-        (json) =>
-            IntimacyReplyAnalyzeResponse.fromJson(json as Map<String, dynamic>),
-      );
-    } on Object catch (e, s) {
-      errorLogger?.logError(e, s, _options);
-      rethrow;
-    }
-    return _value;
-  }
-
-  @override
   Future<BaseResponse<KeyboardMemeExplainResponse>> getKeyboardMemeExplain(
     KeyboardMemeExplainRequest request,
   ) async {

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

@@ -61,6 +61,29 @@ class _AtmobStreamApi implements AtmobStreamApi {
     return _result.data!;
   }
 
+  @override
+  Future<ResponseBody> intimacyReplyAnalyze(
+    IntimacyReplyAnalyzeRequest request,
+  ) async {
+    final _extra = <String, dynamic>{};
+    final queryParameters = <String, dynamic>{};
+    final _headers = <String, dynamic>{};
+    final _data = <String, dynamic>{};
+    _data.addAll(request.toJson());
+    final _options = _setStreamType<ResponseBody>(
+      Options(method: 'POST', headers: _headers, extra: _extra)
+          .compose(
+            _dio.options,
+            '/project/keyboard/v1/intimacy/reply/analyze',
+            queryParameters: queryParameters,
+            data: _data,
+          )
+          .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)),
+    );
+    final _result = await _dio.fetch<ResponseBody>(_options);
+    return _result.data!;
+  }
+
   RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
     if (T != dynamic &&
         !(requestOptions.responseType == ResponseType.bytes ||

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

@@ -1,5 +1,6 @@
 import 'package:dio/dio.dart';
 import 'package:keyboard/data/api/request/deep_seek_chat_request.dart';
+import 'package:keyboard/data/api/request/intimacy_reply_analyze_request.dart';
 import 'package:keyboard/data/api/request/intimacy_reply_chat_request.dart';
 import 'package:retrofit/error_logger.dart';
 import 'package:retrofit/http.dart';
@@ -28,4 +29,10 @@ abstract class AtmobStreamApi {
   Future<ResponseBody> intimacyChatAnalyze(
     @Body() IntimacyChatAnalyzeRequest request,
   );
+
+  /// 识图回复
+  @POST("/project/keyboard/v1/intimacy/reply/analyze")
+  Future<ResponseBody> intimacyReplyAnalyze(
+    @Body() IntimacyReplyAnalyzeRequest request,
+  );
 }

+ 1 - 1
lib/data/api/response/intimacy_reply_analyze_response.dart

@@ -19,7 +19,7 @@ class IntimacyReplyAnalyzeResponse {
 
   /// Ai模型
   @JsonKey(name: "model")
-  int? model;
+  String? model;
 
   /// 选择项列表
   @JsonKey(name: "choices")

+ 1 - 1
lib/data/api/response/intimacy_reply_analyze_response.g.dart

@@ -12,7 +12,7 @@ IntimacyReplyAnalyzeResponse _$IntimacyReplyAnalyzeResponseFromJson(
   json['id'] as String?,
   json['object'] as String?,
   (json['created'] as num?)?.toInt(),
-  (json['model'] as num?)?.toInt(),
+  json['model'] as String?,
   (json['choices'] as List<dynamic>?)
       ?.map((e) => AnalyzeChoiceItem.fromJson(e as Map<String, dynamic>))
       .toList(),

+ 30 - 7
lib/data/repository/intimacy_analyze_repository.dart

@@ -106,7 +106,7 @@ class IntimacyAnalyzeRepository {
         .then(HttpHandler.handle(true));
   }
 
-  /// 获取对话分析配置(SSE流式)
+  /// 对话分析(SSE流式)
   Future<Stream<Message>> intimacyChatAnalyze(IntimacyChatAnalyzeRequest request) {
     return atmobStreamApi
         .intimacyChatAnalyze(request)
@@ -153,12 +153,35 @@ class IntimacyAnalyzeRepository {
     return atmobApi.getIntimacyAnalyze(request).then(HttpHandler.handle(true));
   }
 
-  /// 识图回复
-  Future<IntimacyReplyAnalyzeResponse> intimacyReplyAnalyze(
-    IntimacyReplyAnalyzeRequest request,
-  ) {
-    return atmobApi
+  /// 识图回复(SSE流式)
+  Future<Stream<Message>> intimacyReplyAnalyze(IntimacyReplyAnalyzeRequest request) {
+    return atmobStreamApi
         .intimacyReplyAnalyze(request)
-        .then(HttpHandler.handle(true));
+        .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));
   }
 }

+ 1 - 1
lib/module/intimacy_analyse/screenshot_reply/conversation_analysis/conversation_analysis_controller.dart

@@ -66,7 +66,7 @@ class ConversationAnalysisController extends BaseController {
   /// 上传图片列表
   RxList<UploadInfo> uploadInfoList = <UploadInfo>[].obs;
 
-  /// 对话分析的订阅
+  /// 对话分析的订阅(SSE)
   StreamSubscription<Message>? _intimacyChatAnalyzeSubscription;
 
   ConversationAnalysisController(

+ 63 - 27
lib/module/intimacy_analyse/screenshot_reply/scan_image_reply/scan_image_reply_controller.dart

@@ -1,3 +1,5 @@
+import 'dart:async';
+import 'dart:convert';
 import 'dart:io';
 
 import 'package:flutter/cupertino.dart';
@@ -21,6 +23,7 @@ import '../../../../utils/error_handler.dart';
 import '../../../../utils/http_handler.dart';
 import '../../../../utils/image_picker_util.dart';
 import '../../../../utils/intimacy_analyze_config_helper.dart';
+import '../../../../utils/sse_parse_util.dart';
 import '../../../../utils/upload/upload_file_manager.dart';
 import '../../../../utils/upload/upload_scene_type.dart';
 import '../../../store/store_page.dart';
@@ -66,6 +69,9 @@ class ScanImageReplyController extends BaseController {
   /// 回复语气列表
   RxList<String> replyToneList = <String>[].obs;
 
+  /// 对话分析的订阅(SSE)
+  StreamSubscription<Message>? _intimacyReplyAnalyzeSubscription;
+
   ScanImageReplyController(
     this.intimacyAnalyzeConfigHelper,
     this.uploadFileManager,
@@ -80,6 +86,13 @@ class ScanImageReplyController extends BaseController {
     _initReplyModeList();
   }
 
+  @override
+  void onClose() {
+    // 取消SSE流数据订阅
+    _intimacyReplyAnalyzeSubscription?.cancel();
+    super.onClose();
+  }
+
   /// 初始化回复语气列表
   void _initReplyToneOptionSelectConfigList() {
     // 语气列表
@@ -171,7 +184,8 @@ class ScanImageReplyController extends BaseController {
     // 上传的图片后端地址
     List<String> imageList = [uploadInfo.value?.fileBackendPath ?? ""];
     // 选择的回复语气
-    String title = currentSelectReplyToneOption.value?.name ?? "";
+    // String title = currentSelectReplyToneOption.value?.name ?? "";
+    String title = "高冷";
     // 当前选择的回复模式
     String mode = currentReplyMode.value?.value ?? "";
 
@@ -188,34 +202,56 @@ class ScanImageReplyController extends BaseController {
       return;
     }
 
-    try {
-      // 请求分析截图
-      IntimacyReplyAnalyzeResponse response = await intimacyAnalyzeRepository
-          .intimacyReplyAnalyze(
-            IntimacyReplyAnalyzeRequest(imageList, title, mode),
-          );
-      var newList =
-          response.choices?.map((item) {
-            return item.delta?.content ?? "";
-          }) ??
-          [];
-      replyToneList.clear();
-      replyToneList.addAll(newList);
-      replyToneList.refresh();
-    } catch (error) {
-      AtmobLog.e(_tag, error.toString());
-      if (error is ServerErrorException) {
-        // 需要Vip权限
-        if (error.code == 1005) {
-          ToastUtil.show(error.message);
-          StorePage.start();
+    StringBuffer buffer = StringBuffer();
+
+    replyToneList.clear();
+    replyToneList.refresh();
+
+    // 请求分析截图
+    Stream<Message> stream = await intimacyAnalyzeRepository
+        .intimacyReplyAnalyze(
+          IntimacyReplyAnalyzeRequest(imageList, title, mode),
+        );
+    _intimacyReplyAnalyzeSubscription = stream.listen(
+      (message) {
+        // 流数据更新
+        String json = message.data;
+        // 解析json为实体类
+        IntimacyReplyAnalyzeResponse response =
+            IntimacyReplyAnalyzeResponse.fromJson(jsonDecode(json));
+
+        List<String> contentList = response.choices?.map((item) {
+          return item.delta?.content ?? "";
+        }).toList() ?? [];
+        // 数组转字符串
+        String content = contentList.join("");
+
+        // 累计内容
+        buffer.write(content);
+
+        // 刷新列表
+        replyToneList.clear();
+        replyToneList.add(buffer.toString());
+        replyToneList.refresh();
+      },
+      onError: (error) {
+        AtmobLog.e(_tag, error.toString());
+        if (error is ServerErrorException) {
+          // 需要Vip权限
+          if (error.code == 1005) {
+            ToastUtil.show(error.message);
+            StorePage.start();
+          } else {
+            ToastUtil.show(error.message);
+          }
         } else {
-          ToastUtil.show(error.message);
+          ErrorHandler.toastError(error);
         }
-      } else {
-        ErrorHandler.toastError(error);
-      }
-    }
+      },
+      onDone: () {
+        // 流关闭
+      },
+    );
   }
 
   /// 删除上传信息