Commit 4bf563598d62aa8de2da611b351442f5d48229fd

Authored by weixin_45529955
1 parent ea66b2f3

安卓微信截图分享到app 缺iOS

@@ -103,6 +103,18 @@ @@ -103,6 +103,18 @@
103 android:host="com.naji.book" 103 android:host="com.naji.book"
104 android:path="/book_detail" /> 104 android:path="/book_detail" />
105 </intent-filter> 105 </intent-filter>
  106 +
  107 + <!-- 接收其他App(微信等)分享的图片,用于识别预订信息 -->
  108 + <intent-filter>
  109 + <action android:name="android.intent.action.SEND"/>
  110 + <category android:name="android.intent.category.DEFAULT"/>
  111 + <data android:mimeType="image/*"/>
  112 + </intent-filter>
  113 + <intent-filter>
  114 + <action android:name="android.intent.action.SEND_MULTIPLE"/>
  115 + <category android:name="android.intent.category.DEFAULT"/>
  116 + <data android:mimeType="image/*"/>
  117 + </intent-filter>
106 </activity> 118 </activity>
107 119
108 <!-- Don't delete the meta-data below. 120 <!-- Don't delete the meta-data below.
@@ -38,6 +38,9 @@ PODS: @@ -38,6 +38,9 @@ PODS:
38 - core_location_fluttify (0.0.1): 38 - core_location_fluttify (0.0.1):
39 - Flutter 39 - Flutter
40 - foundation_fluttify 40 - foundation_fluttify
  41 + - CwlCatchException (2.2.1):
  42 + - CwlCatchExceptionSupport (~> 2.2.1)
  43 + - CwlCatchExceptionSupport (2.2.1)
41 - device_info (0.0.1): 44 - device_info (0.0.1):
42 - Flutter 45 - Flutter
43 - device_utils_my (0.0.1): 46 - device_utils_my (0.0.1):
@@ -73,6 +76,9 @@ PODS: @@ -73,6 +76,9 @@ PODS:
73 - Flutter 76 - Flutter
74 - flutter_native_image (0.0.1): 77 - flutter_native_image (0.0.1):
75 - Flutter 78 - Flutter
  79 + - flutter_open_chinese_convert (0.0.1):
  80 + - Flutter
  81 + - OpenCC
76 - flutter_sms (1.1.0): 82 - flutter_sms (1.1.0):
77 - Flutter 83 - Flutter
78 - flutter_statusbarcolor_ns (0.0.1): 84 - flutter_statusbarcolor_ns (0.0.1):
@@ -114,6 +120,7 @@ PODS: @@ -114,6 +120,7 @@ PODS:
114 - MTBBarcodeScanner (5.0.11) 120 - MTBBarcodeScanner (5.0.11)
115 - open_filex (0.0.2): 121 - open_filex (0.0.2):
116 - Flutter 122 - Flutter
  123 + - OpenCC (0.3.3)
117 - OrderedSet (6.0.3) 124 - OrderedSet (6.0.3)
118 - package_info (0.0.1): 125 - package_info (0.0.1):
119 - Flutter 126 - Flutter
@@ -126,6 +133,8 @@ PODS: @@ -126,6 +133,8 @@ PODS:
126 - Flutter 133 - Flutter
127 - FlutterMacOS 134 - FlutterMacOS
128 - ReachabilitySwift (5.2.4) 135 - ReachabilitySwift (5.2.4)
  136 + - record_darwin (1.0.0):
  137 + - Flutter
129 - SDWebImage (5.20.0): 138 - SDWebImage (5.20.0):
130 - SDWebImage/Core (= 5.20.0) 139 - SDWebImage/Core (= 5.20.0)
131 - SDWebImage/Core (5.20.0) 140 - SDWebImage/Core (5.20.0)
@@ -139,6 +148,10 @@ PODS: @@ -139,6 +148,10 @@ PODS:
139 - FlutterMacOS 148 - FlutterMacOS
140 - sign_in_with_apple (0.0.1): 149 - sign_in_with_apple (0.0.1):
141 - Flutter 150 - Flutter
  151 + - speech_to_text (7.2.0):
  152 + - CwlCatchException
  153 + - Flutter
  154 + - FlutterMacOS
142 - sqflite_darwin (0.0.4): 155 - sqflite_darwin (0.0.4):
143 - Flutter 156 - Flutter
144 - FlutterMacOS 157 - FlutterMacOS
@@ -161,6 +174,8 @@ PODS: @@ -161,6 +174,8 @@ PODS:
161 - webview_flutter (0.0.1): 174 - webview_flutter (0.0.1):
162 - Flutter 175 - Flutter
163 - WechatOpenSDK-XCFramework (2.0.4) 176 - WechatOpenSDK-XCFramework (2.0.4)
  177 + - whisper_cpp_flutter_plus (0.4.1):
  178 + - Flutter
164 179
165 DEPENDENCIES: 180 DEPENDENCIES:
166 - amap_core_fluttify (from `.symlinks/plugins/amap_core_fluttify/ios`) 181 - amap_core_fluttify (from `.symlinks/plugins/amap_core_fluttify/ios`)
@@ -185,6 +200,7 @@ DEPENDENCIES: @@ -185,6 +200,7 @@ DEPENDENCIES:
185 - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) 200 - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
186 - flutter_native_contact_picker (from `.symlinks/plugins/flutter_native_contact_picker/ios`) 201 - flutter_native_contact_picker (from `.symlinks/plugins/flutter_native_contact_picker/ios`)
187 - flutter_native_image (from `.symlinks/plugins/flutter_native_image/ios`) 202 - flutter_native_image (from `.symlinks/plugins/flutter_native_image/ios`)
  203 + - flutter_open_chinese_convert (from `.symlinks/plugins/flutter_open_chinese_convert/ios`)
188 - flutter_sms (from `.symlinks/plugins/flutter_sms/ios`) 204 - flutter_sms (from `.symlinks/plugins/flutter_sms/ios`)
189 - flutter_statusbarcolor_ns (from `.symlinks/plugins/flutter_statusbarcolor_ns/ios`) 205 - flutter_statusbarcolor_ns (from `.symlinks/plugins/flutter_statusbarcolor_ns/ios`)
190 - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) 206 - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
@@ -199,9 +215,11 @@ DEPENDENCIES: @@ -199,9 +215,11 @@ DEPENDENCIES:
199 - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) 215 - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
200 - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) 216 - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
201 - photo_manager (from `.symlinks/plugins/photo_manager/ios`) 217 - photo_manager (from `.symlinks/plugins/photo_manager/ios`)
  218 + - record_darwin (from `.symlinks/plugins/record_darwin/ios`)
202 - sendsms (from `.symlinks/plugins/sendsms/ios`) 219 - sendsms (from `.symlinks/plugins/sendsms/ios`)
203 - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) 220 - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
204 - sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`) 221 - sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`)
  222 + - speech_to_text (from `.symlinks/plugins/speech_to_text/darwin`)
205 - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) 223 - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
206 - tpns_flutter_plugin (from `.symlinks/plugins/tpns_flutter_plugin/ios`) 224 - tpns_flutter_plugin (from `.symlinks/plugins/tpns_flutter_plugin/ios`)
207 - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) 225 - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
@@ -209,6 +227,7 @@ DEPENDENCIES: @@ -209,6 +227,7 @@ DEPENDENCIES:
209 - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) 227 - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
210 - video_thumbnail (from `.symlinks/plugins/video_thumbnail/ios`) 228 - video_thumbnail (from `.symlinks/plugins/video_thumbnail/ios`)
211 - webview_flutter (from `.symlinks/plugins/webview_flutter/ios`) 229 - webview_flutter (from `.symlinks/plugins/webview_flutter/ios`)
  230 + - whisper_cpp_flutter_plus (from `.symlinks/plugins/whisper_cpp_flutter_plus/ios`)
212 231
213 SPEC REPOS: 232 SPEC REPOS:
214 trunk: 233 trunk:
@@ -218,9 +237,12 @@ SPEC REPOS: @@ -218,9 +237,12 @@ SPEC REPOS:
218 - AMapSearch-NO-IDFA 237 - AMapSearch-NO-IDFA
219 - BIJKPlayer 238 - BIJKPlayer
220 - Bugly 239 - Bugly
  240 + - CwlCatchException
  241 + - CwlCatchExceptionSupport
221 - libwebp 242 - libwebp
222 - Mantle 243 - Mantle
223 - MTBBarcodeScanner 244 - MTBBarcodeScanner
  245 + - OpenCC
224 - OrderedSet 246 - OrderedSet
225 - ReachabilitySwift 247 - ReachabilitySwift
226 - SDWebImage 248 - SDWebImage
@@ -275,6 +297,8 @@ EXTERNAL SOURCES: @@ -275,6 +297,8 @@ EXTERNAL SOURCES:
275 :path: ".symlinks/plugins/flutter_native_contact_picker/ios" 297 :path: ".symlinks/plugins/flutter_native_contact_picker/ios"
276 flutter_native_image: 298 flutter_native_image:
277 :path: ".symlinks/plugins/flutter_native_image/ios" 299 :path: ".symlinks/plugins/flutter_native_image/ios"
  300 + flutter_open_chinese_convert:
  301 + :path: ".symlinks/plugins/flutter_open_chinese_convert/ios"
278 flutter_sms: 302 flutter_sms:
279 :path: ".symlinks/plugins/flutter_sms/ios" 303 :path: ".symlinks/plugins/flutter_sms/ios"
280 flutter_statusbarcolor_ns: 304 flutter_statusbarcolor_ns:
@@ -303,12 +327,16 @@ EXTERNAL SOURCES: @@ -303,12 +327,16 @@ EXTERNAL SOURCES:
303 :path: ".symlinks/plugins/permission_handler_apple/ios" 327 :path: ".symlinks/plugins/permission_handler_apple/ios"
304 photo_manager: 328 photo_manager:
305 :path: ".symlinks/plugins/photo_manager/ios" 329 :path: ".symlinks/plugins/photo_manager/ios"
  330 + record_darwin:
  331 + :path: ".symlinks/plugins/record_darwin/ios"
306 sendsms: 332 sendsms:
307 :path: ".symlinks/plugins/sendsms/ios" 333 :path: ".symlinks/plugins/sendsms/ios"
308 shared_preferences_foundation: 334 shared_preferences_foundation:
309 :path: ".symlinks/plugins/shared_preferences_foundation/darwin" 335 :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
310 sign_in_with_apple: 336 sign_in_with_apple:
311 :path: ".symlinks/plugins/sign_in_with_apple/ios" 337 :path: ".symlinks/plugins/sign_in_with_apple/ios"
  338 + speech_to_text:
  339 + :path: ".symlinks/plugins/speech_to_text/darwin"
312 sqflite_darwin: 340 sqflite_darwin:
313 :path: ".symlinks/plugins/sqflite_darwin/darwin" 341 :path: ".symlinks/plugins/sqflite_darwin/darwin"
314 tpns_flutter_plugin: 342 tpns_flutter_plugin:
@@ -323,6 +351,8 @@ EXTERNAL SOURCES: @@ -323,6 +351,8 @@ EXTERNAL SOURCES:
323 :path: ".symlinks/plugins/video_thumbnail/ios" 351 :path: ".symlinks/plugins/video_thumbnail/ios"
324 webview_flutter: 352 webview_flutter:
325 :path: ".symlinks/plugins/webview_flutter/ios" 353 :path: ".symlinks/plugins/webview_flutter/ios"
  354 + whisper_cpp_flutter_plus:
  355 + :path: ".symlinks/plugins/whisper_cpp_flutter_plus/ios"
326 356
327 SPEC CHECKSUMS: 357 SPEC CHECKSUMS:
328 AMap3DMap-NO-IDFA: 6660036f9a8c28ed3e9037936210121599055a2e 358 AMap3DMap-NO-IDFA: 6660036f9a8c28ed3e9037936210121599055a2e
@@ -340,6 +370,8 @@ SPEC CHECKSUMS: @@ -340,6 +370,8 @@ SPEC CHECKSUMS:
340 connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf 370 connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf
341 contacts_service: 7c2ed10147f55891576c6a582f431e385d8de92d 371 contacts_service: 7c2ed10147f55891576c6a582f431e385d8de92d
342 core_location_fluttify: d76555ed03db792a010b362a52a72134a69fe32b 372 core_location_fluttify: d76555ed03db792a010b362a52a72134a69fe32b
  373 + CwlCatchException: 7acc161b299a6de7f0a46a6ed741eae2c8b4d75a
  374 + CwlCatchExceptionSupport: 54ccab8d8c78907b57f99717fb19d4cc3bce02dc
343 device_info: 52e8c0c9c61def8d0a92bf175f5f500abbea04bc 375 device_info: 52e8c0c9c61def8d0a92bf175f5f500abbea04bc
344 device_utils_my: de33d0a3965bd76cf7e9fb58b7ea71d698686969 376 device_utils_my: de33d0a3965bd76cf7e9fb58b7ea71d698686969
345 fijkplayer: 2dd31ec312ba554bb6a9163cb2f44cdb528e50da 377 fijkplayer: 2dd31ec312ba554bb6a9163cb2f44cdb528e50da
@@ -353,6 +385,7 @@ SPEC CHECKSUMS: @@ -353,6 +385,7 @@ SPEC CHECKSUMS:
353 flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619 385 flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619
354 flutter_native_contact_picker: 22508d3ab5a583068d4a588cb37c0e0b8764722d 386 flutter_native_contact_picker: 22508d3ab5a583068d4a588cb37c0e0b8764722d
355 flutter_native_image: c0f200e4bc5da392b15878df38654cc85a78b090 387 flutter_native_image: c0f200e4bc5da392b15878df38654cc85a78b090
  388 + flutter_open_chinese_convert: 8b440f57a1666d3deb57b3c9da6318685a061812
356 flutter_sms: bb63bc226f41fad2b7cc9f0b1d0c15f7730a8310 389 flutter_sms: bb63bc226f41fad2b7cc9f0b1d0c15f7730a8310
357 flutter_statusbarcolor_ns: 28ea17050f37c962fe05a4722a6d43afc4fca76b 390 flutter_statusbarcolor_ns: 28ea17050f37c962fe05a4722a6d43afc4fca76b
358 fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1 391 fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
@@ -366,17 +399,20 @@ SPEC CHECKSUMS: @@ -366,17 +399,20 @@ SPEC CHECKSUMS:
366 Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d 399 Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
367 MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb 400 MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
368 open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1 401 open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1
  402 + OpenCC: 290dcf27e18ef926aa4fbc0f0a9c116c36145bc5
369 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 403 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
370 package_info: cce50adca9873c79f931618469d2114b91d71189 404 package_info: cce50adca9873c79f931618469d2114b91d71189
371 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 405 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
372 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d 406 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
373 photo_manager: d2fbcc0f2d82458700ee6256a15018210a81d413 407 photo_manager: d2fbcc0f2d82458700ee6256a15018210a81d413
374 ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda 408 ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda
  409 + record_darwin: fb1f375f1d9603714f55b8708a903bbb91ffdb0a
375 SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8 410 SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8
376 SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 411 SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
377 sendsms: 43a465a88ce570d59cc10ef0f3bdb922cc303c7b 412 sendsms: 43a465a88ce570d59cc10ef0f3bdb922cc303c7b
378 shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 413 shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
379 sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418 414 sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418
  415 + speech_to_text: 3b313d98516d3d0406cea424782ec25470c59d19
380 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 416 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
381 SwiftProtobuf: 4dbaffec76a39a8dc5da23b40af1a5dc01a4c02d 417 SwiftProtobuf: 4dbaffec76a39a8dc5da23b40af1a5dc01a4c02d
382 TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654 418 TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654
@@ -388,6 +424,7 @@ SPEC CHECKSUMS: @@ -388,6 +424,7 @@ SPEC CHECKSUMS:
388 video_thumbnail: b637e0ad5f588ca9945f6e2c927f73a69a661140 424 video_thumbnail: b637e0ad5f588ca9945f6e2c927f73a69a661140
389 webview_flutter: fa13f657fd0a01527ab6ba905b3be0fb3e3bfd4d 425 webview_flutter: fa13f657fd0a01527ab6ba905b3be0fb3e3bfd4d
390 WechatOpenSDK-XCFramework: 36fb2bea0754266c17184adf4963d7e6ff98b69f 426 WechatOpenSDK-XCFramework: 36fb2bea0754266c17184adf4963d7e6ff98b69f
  427 + whisper_cpp_flutter_plus: b268c1770195390aecafa38e7d3b4711ec007c14
391 428
392 PODFILE CHECKSUM: 9435259418d39cbc3076dcc2f688f77abaddd31d 429 PODFILE CHECKSUM: 9435259418d39cbc3076dcc2f688f77abaddd31d
393 430
@@ -355,14 +355,10 @@ @@ -355,14 +355,10 @@
355 inputFileListPaths = ( 355 inputFileListPaths = (
356 "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", 356 "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
357 ); 357 );
358 - inputPaths = (  
359 - );  
360 name = "[CP] Embed Pods Frameworks"; 358 name = "[CP] Embed Pods Frameworks";
361 outputFileListPaths = ( 359 outputFileListPaths = (
362 "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", 360 "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
363 ); 361 );
364 - outputPaths = (  
365 - );  
366 runOnlyForDeploymentPostprocessing = 0; 362 runOnlyForDeploymentPostprocessing = 0;
367 shellPath = /bin/sh; 363 shellPath = /bin/sh;
368 shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; 364 shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
@@ -376,14 +372,10 @@ @@ -376,14 +372,10 @@
376 inputFileListPaths = ( 372 inputFileListPaths = (
377 "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", 373 "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
378 ); 374 );
379 - inputPaths = (  
380 - );  
381 name = "[CP] Copy Pods Resources"; 375 name = "[CP] Copy Pods Resources";
382 outputFileListPaths = ( 376 outputFileListPaths = (
383 "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", 377 "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
384 ); 378 );
385 - outputPaths = (  
386 - );  
387 runOnlyForDeploymentPostprocessing = 0; 379 runOnlyForDeploymentPostprocessing = 0;
388 shellPath = /bin/sh; 380 shellPath = /bin/sh;
389 shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; 381 shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
@@ -37,6 +37,9 @@ import 'package:one_key/provider/upload_img_provider.dart'; @@ -37,6 +37,9 @@ import 'package:one_key/provider/upload_img_provider.dart';
37 import 'package:one_key/route/navigator_util.dart'; 37 import 'package:one_key/route/navigator_util.dart';
38 import 'package:one_key/utils/utils.dart'; 38 import 'package:one_key/utils/utils.dart';
39 import 'package:provider/provider.dart'; 39 import 'package:provider/provider.dart';
  40 +import 'package:receive_sharing_intent/receive_sharing_intent.dart';
  41 +import 'package:one_key/pages/agent/agent_chat_page.dart';
  42 +import 'package:one_key/pages/agent/shared_reservation_receiver.dart';
40 import 'package:tpns_flutter_plugin/tpns_flutter_plugin.dart'; 43 import 'package:tpns_flutter_plugin/tpns_flutter_plugin.dart';
41 44
42 import 'app.dart'; 45 import 'app.dart';
@@ -88,6 +91,7 @@ FutureOr<void> main() async { @@ -88,6 +91,7 @@ FutureOr<void> main() async {
88 // await FlutterDownloader.initialize(debug: true, ignoreSsl: true); 91 // await FlutterDownloader.initialize(debug: true, ignoreSsl: true);
89 92
90 await App.initSp(); 93 await App.initSp();
  94 + _initSharedReservationReceiver();
91 SystemChrome.setPreferredOrientations( 95 SystemChrome.setPreferredOrientations(
92 [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]) 96 [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown])
93 .then((_) { 97 .then((_) {
@@ -333,3 +337,70 @@ class MyAppState extends State<MyApp> { @@ -333,3 +337,70 @@ class MyAppState extends State<MyApp> {
333 }); 337 });
334 } 338 }
335 } 339 }
  340 +
  341 +void _initSharedReservationReceiver() {
  342 + try {
  343 + ReceiveSharingIntent.instance.getMediaStream().listen(
  344 + (List<SharedMediaFile> media) => _handleSharedMedia(media),
  345 + onError: (Object error) => debugPrint('分享接收失败: $error'),
  346 + );
  347 + // 延迟到首帧后再读取初始分享内容,确保 Android Activity 已挂载。
  348 + WidgetsBinding.instance.addPostFrameCallback((_) {
  349 + unawaited(_handleInitialSharedMedia());
  350 + });
  351 + } catch (error) {
  352 + debugPrint('分享接收初始化失败: $error');
  353 + }
  354 +}
  355 +
  356 +Future<void> _handleInitialSharedMedia() async {
  357 + try {
  358 + final List<SharedMediaFile> media =
  359 + await ReceiveSharingIntent.instance.getInitialMedia();
  360 + if (media.isNotEmpty) {
  361 + _handleSharedMedia(media);
  362 + }
  363 + unawaited(ReceiveSharingIntent.instance.reset());
  364 + } catch (error) {
  365 + debugPrint('读取初始分享内容失败: $error');
  366 + }
  367 +}
  368 +
  369 +void _handleSharedMedia(List<SharedMediaFile> media) {
  370 + if (media.isEmpty) return;
  371 + final SharedMediaFile? image = media
  372 + .where((SharedMediaFile file) => file.type == SharedMediaType.image)
  373 + .firstOrNull;
  374 + if (image == null) return;
  375 +
  376 + SharedReservationReceiver.instance.acceptImage(image.path);
  377 + _openAgentChatForSharedImage();
  378 +}
  379 +
  380 +bool _agentChatPushPending = false;
  381 +int _agentChatPushRetry = 0;
  382 +
  383 +void _openAgentChatForSharedImage() {
  384 + final NavigatorState? navigator = App.navigatorKey.currentState;
  385 + if (navigator == null) {
  386 + // 冷启动时首帧可能尚未构建完成,稍后重试。
  387 + if (_agentChatPushRetry < 40) {
  388 + _agentChatPushRetry++;
  389 + WidgetsBinding.instance.addPostFrameCallback((_) {
  390 + _openAgentChatForSharedImage();
  391 + });
  392 + }
  393 + return;
  394 + }
  395 + _agentChatPushRetry = 0;
  396 + if (_agentChatPushPending) return;
  397 +
  398 + _agentChatPushPending = true;
  399 + navigator
  400 + .push(
  401 + MaterialPageRoute<void>(builder: (_) => const AgentChatPage()),
  402 + )
  403 + .whenComplete(() {
  404 + _agentChatPushPending = false;
  405 + });
  406 +}
1 import 'dart:async'; 1 import 'dart:async';
  2 +import 'dart:io';
2 import 'package:flutter/foundation.dart'; 3 import 'package:flutter/foundation.dart';
3 import 'package:flutter/material.dart'; 4 import 'package:flutter/material.dart';
4 import 'package:flutter/services.dart'; 5 import 'package:flutter/services.dart';
@@ -6,9 +7,10 @@ import 'package:image_picker/image_picker.dart'; @@ -6,9 +7,10 @@ import 'package:image_picker/image_picker.dart';
6 import 'package:one_key/app.dart'; 7 import 'package:one_key/app.dart';
7 import 'package:one_key/models/notebook_detail.dart'; 8 import 'package:one_key/models/notebook_detail.dart';
8 import 'package:one_key/pages/agent/agent_chat_store.dart'; 9 import 'package:one_key/pages/agent/agent_chat_store.dart';
9 -import 'package:one_key/pages/agent/baidu_ocr_client.dart'; 10 +import 'package:one_key/pages/agent/qwen_ocr_client.dart';
10 import 'package:one_key/pages/agent/reservation_image_parser.dart'; 11 import 'package:one_key/pages/agent/reservation_image_parser.dart';
11 -import 'package:one_key/pages/agent/tencent_asr_client.dart'; 12 +import 'package:one_key/pages/agent/shared_reservation_receiver.dart';
  13 +import 'package:one_key/pages/agent/paraformer_asr_client.dart';
12 import 'package:one_key/provider/user_model.dart'; 14 import 'package:one_key/provider/user_model.dart';
13 import 'package:one_key/res/colors.dart'; 15 import 'package:one_key/res/colors.dart';
14 import 'package:one_key/route/navigator_util.dart'; 16 import 'package:one_key/route/navigator_util.dart';
@@ -75,11 +77,8 @@ class AgentChatPage extends StatefulWidget { @@ -75,11 +77,8 @@ class AgentChatPage extends StatefulWidget {
75 class _AgentChatPageState extends State<AgentChatPage> { 77 class _AgentChatPageState extends State<AgentChatPage> {
76 static const String _greeting = '你好,我是经营助手。今天想问点什么?比如:“今天营业额多少?”'; 78 static const String _greeting = '你好,我是经营助手。今天想问点什么?比如:“今天营业额多少?”';
77 // TODO: 正式上线时应由服务端下发密钥,避免客户端泄露。 79 // TODO: 正式上线时应由服务端下发密钥,避免客户端泄露。
78 - static const String _tencentSecretId = 'AKID9Vgy8FEC5qSxLcllVCmXzJWaVSI0Z2Nb';  
79 - static const String _tencentSecretKey = 'c2c5MTOBo5d9XilhaLzVSG4DE7qRuCJF';  
80 - static const String _tencentAppId = '1308375824';  
81 - static const String _baiduApiKey = 'ChpdwH1VCou4B7eWF1hBqmn6';  
82 - static const String _baiduSecretKey = 'ayognpdpOYkoHQmBxjQJQzvjnSpqLA6w'; 80 + static const String _qwenApiKey =
  81 + 'sk-ws-H.PILIHPE.GQc0.MEUCIFS4lSx9mCvznDxgNHzPthawcyEIlr3W8eTtquLAalb3AiEAivaBkCp122lHoIFBiAeWYaUsIyrVOUbUsqQnPj8ApIs';
83 static const MethodChannel _speechChannel = MethodChannel('com.naji/book'); 82 static const MethodChannel _speechChannel = MethodChannel('com.naji/book');
84 83
85 final ScrollController _scrollController = ScrollController(); 84 final ScrollController _scrollController = ScrollController();
@@ -98,7 +97,7 @@ class _AgentChatPageState extends State<AgentChatPage> { @@ -98,7 +97,7 @@ class _AgentChatPageState extends State<AgentChatPage> {
98 bool _historyLoading = true; 97 bool _historyLoading = true;
99 bool _usingNativeSpeech = false; 98 bool _usingNativeSpeech = false;
100 bool _usingWhisper = false; 99 bool _usingWhisper = false;
101 - bool _usingTencentAsr = false; 100 + bool _usingCloudAsr = false;
102 bool _whisperModelReady = false; 101 bool _whisperModelReady = false;
103 bool _voicePressActive = false; 102 bool _voicePressActive = false;
104 bool _imageProcessing = false; 103 bool _imageProcessing = false;
@@ -114,8 +113,9 @@ class _AgentChatPageState extends State<AgentChatPage> { @@ -114,8 +113,9 @@ class _AgentChatPageState extends State<AgentChatPage> {
114 WhisperEngine? _whisperEngine; 113 WhisperEngine? _whisperEngine;
115 WhisperStreamTask? _whisperTask; 114 WhisperStreamTask? _whisperTask;
116 StreamSubscription<WhisperStreamUpdate>? _whisperUpdateSub; 115 StreamSubscription<WhisperStreamUpdate>? _whisperUpdateSub;
  116 + VoidCallback? _sharedImageListener;
117 Future<bool>? _whisperPrepareFuture; 117 Future<bool>? _whisperPrepareFuture;
118 - TencentAsrSession? _tencentSession; 118 + ParaformerAsrSession? _cloudAsrSession;
119 119
120 bool get _supportsWhisper => 120 bool get _supportsWhisper =>
121 defaultTargetPlatform == TargetPlatform.android || 121 defaultTargetPlatform == TargetPlatform.android ||
@@ -142,6 +142,7 @@ class _AgentChatPageState extends State<AgentChatPage> { @@ -142,6 +142,7 @@ class _AgentChatPageState extends State<AgentChatPage> {
142 unawaited(_prewarmWhisperEngine()); 142 unawaited(_prewarmWhisperEngine());
143 } 143 }
144 _loadHistory(); 144 _loadHistory();
  145 + _initSharedReservationListener();
145 } 146 }
146 147
147 @override 148 @override
@@ -150,8 +151,14 @@ class _AgentChatPageState extends State<AgentChatPage> { @@ -150,8 +151,14 @@ class _AgentChatPageState extends State<AgentChatPage> {
150 _voiceTimer?.cancel(); 151 _voiceTimer?.cancel();
151 _voiceRequestId++; 152 _voiceRequestId++;
152 _whisperUpdateSub?.cancel(); 153 _whisperUpdateSub?.cancel();
  154 + final VoidCallback? sharedListener = _sharedImageListener;
  155 + _sharedImageListener = null;
  156 + if (sharedListener != null) {
  157 + SharedReservationReceiver.instance.pendingImage
  158 + .removeListener(sharedListener);
  159 + }
153 unawaited(_whisperTask?.cancel()); 160 unawaited(_whisperTask?.cancel());
154 - unawaited(_tencentSession?.cancel()); 161 + unawaited(_cloudAsrSession?.cancel());
155 try { 162 try {
156 _whisperEngine?.dispose(); 163 _whisperEngine?.dispose();
157 } catch (_) {} 164 } catch (_) {}
@@ -188,8 +195,7 @@ class _AgentChatPageState extends State<AgentChatPage> { @@ -188,8 +195,7 @@ class _AgentChatPageState extends State<AgentChatPage> {
188 try { 195 try {
189 final WhisperModelManager manager = 196 final WhisperModelManager manager =
190 _whisperModels ??= WhisperModelManager(); 197 _whisperModels ??= WhisperModelManager();
191 - final model =  
192 - await manager.findCatalogModel(WhisperModelCatalog.base); 198 + final model = await manager.findCatalogModel(WhisperModelCatalog.base);
193 if (model == null || _whisperEngine != null) return; 199 if (model == null || _whisperEngine != null) return;
194 final WhisperEngine engine = await WhisperEngine.load(model.path); 200 final WhisperEngine engine = await WhisperEngine.load(model.path);
195 if (!mounted) { 201 if (!mounted) {
@@ -370,7 +376,8 @@ class _AgentChatPageState extends State<AgentChatPage> { @@ -370,7 +376,8 @@ class _AgentChatPageState extends State<AgentChatPage> {
370 final String trimmed = text.trim(); 376 final String trimmed = text.trim();
371 if (trimmed.isEmpty) return false; 377 if (trimmed.isEmpty) return false;
372 378
373 - final ReservationImageParseResult result = ReservationImageParser.parse(trimmed); 379 + final ReservationImageParseResult result =
  380 + ReservationImageParser.parse(trimmed);
374 if (!_isReservationText(result)) return false; 381 if (!_isReservationText(result)) return false;
375 382
376 unawaited(_showReservationConfirm(_normalizeReservationDate(result))); 383 unawaited(_showReservationConfirm(_normalizeReservationDate(result)));
@@ -623,8 +630,14 @@ class _AgentChatPageState extends State<AgentChatPage> { @@ -623,8 +630,14 @@ class _AgentChatPageState extends State<AgentChatPage> {
623 kind: _CardKind.orderList, 630 kind: _CardKind.orderList,
624 title: '桌${table.group(1)} · 近期订单', 631 title: '桌${table.group(1)} · 近期订单',
625 items: [ 632 items: [
626 - {'name': '#2026091718 · 桌${table.group(1)}', 'status': '已支付 ¥386'},  
627 - {'name': '#2026091721 · 桌${table.group(1)}', 'status': '待支付 ¥168'}, 633 + {
  634 + 'name': '#2026091718 · 桌${table.group(1)}',
  635 + 'status': '已支付 ¥386'
  636 + },
  637 + {
  638 + 'name': '#2026091721 · 桌${table.group(1)}',
  639 + 'status': '待支付 ¥168'
  640 + },
628 {'name': '#2026091723 · 桌${table.group(1)}', 'status': '已支付 ¥96'}, 641 {'name': '#2026091723 · 桌${table.group(1)}', 'status': '已支付 ¥96'},
629 ], 642 ],
630 ), 643 ),
@@ -840,7 +853,7 @@ class _AgentChatPageState extends State<AgentChatPage> { @@ -840,7 +853,7 @@ class _AgentChatPageState extends State<AgentChatPage> {
840 _voiceTimer?.cancel(); 853 _voiceTimer?.cancel();
841 854
842 if (_supportsWhisper) { 855 if (_supportsWhisper) {
843 - await _startTencentVoice(requestId); 856 + await _startCloudVoice(requestId);
844 return; 857 return;
845 } 858 }
846 859
@@ -848,9 +861,9 @@ class _AgentChatPageState extends State<AgentChatPage> { @@ -848,9 +861,9 @@ class _AgentChatPageState extends State<AgentChatPage> {
848 await _startLegacyVoice(); 861 await _startLegacyVoice();
849 } 862 }
850 863
851 - /// 移动端优先使用腾讯云实时语音识别,失败时回退到离线 Whisper。  
852 - Future<void> _startTencentVoice(int requestId) async {  
853 - _usingTencentAsr = true; 864 + /// 移动端优先使用百炼 Paraformer 实时语音识别,失败时回退到离线 Whisper。
  865 + Future<void> _startCloudVoice(int requestId) async {
  866 + _usingCloudAsr = true;
854 _usingWhisper = false; 867 _usingWhisper = false;
855 setState(() { 868 setState(() {
856 _voiceStage = _VoiceStage.preparing; 869 _voiceStage = _VoiceStage.preparing;
@@ -859,24 +872,22 @@ class _AgentChatPageState extends State<AgentChatPage> { @@ -859,24 +872,22 @@ class _AgentChatPageState extends State<AgentChatPage> {
859 _voiceHint = '正在连接云端语音识别…'; 872 _voiceHint = '正在连接云端语音识别…';
860 }); 873 });
861 874
862 - final TencentAsrSession session = TencentAsrSession(  
863 - secretId: _tencentSecretId,  
864 - secretKey: _tencentSecretKey,  
865 - appId: _tencentAppId, 875 + final ParaformerAsrSession session = ParaformerAsrSession(
  876 + apiKey: _qwenApiKey,
866 onPartial: (String text) { 877 onPartial: (String text) {
867 if (!mounted || requestId != _voiceRequestId) return; 878 if (!mounted || requestId != _voiceRequestId) return;
868 setState(() => _recognizedText = text); 879 setState(() => _recognizedText = text);
869 }, 880 },
870 ); 881 );
871 - _tencentSession = session; 882 + _cloudAsrSession = session;
872 883
873 try { 884 try {
874 await session.prepare(); 885 await session.prepare();
875 } catch (error) { 886 } catch (error) {
876 - debugPrint('Tencent ASR prepare failed: $error'); 887 + debugPrint('Paraformer ASR prepare failed: $error');
877 if (!mounted || requestId != _voiceRequestId) return; 888 if (!mounted || requestId != _voiceRequestId) return;
878 - _tencentSession = null;  
879 - _usingTencentAsr = false; 889 + _cloudAsrSession = null;
  890 + _usingCloudAsr = false;
880 await session.dispose(); 891 await session.dispose();
881 await _startWhisperVoice(requestId); 892 await _startWhisperVoice(requestId);
882 return; 893 return;
@@ -888,8 +899,8 @@ class _AgentChatPageState extends State<AgentChatPage> { @@ -888,8 +899,8 @@ class _AgentChatPageState extends State<AgentChatPage> {
888 } 899 }
889 900
890 if (!_voicePressActive) { 901 if (!_voicePressActive) {
891 - _tencentSession = null;  
892 - _usingTencentAsr = false; 902 + _cloudAsrSession = null;
  903 + _usingCloudAsr = false;
893 await session.cancel(); 904 await session.cancel();
894 if (mounted && requestId == _voiceRequestId) { 905 if (mounted && requestId == _voiceRequestId) {
895 setState(() { 906 setState(() {
@@ -904,10 +915,10 @@ class _AgentChatPageState extends State<AgentChatPage> { @@ -904,10 +915,10 @@ class _AgentChatPageState extends State<AgentChatPage> {
904 try { 915 try {
905 await session.startAudio(); 916 await session.startAudio();
906 } catch (error) { 917 } catch (error) {
907 - debugPrint('Tencent ASR start failed: $error'); 918 + debugPrint('Paraformer ASR start failed: $error');
908 if (!mounted || requestId != _voiceRequestId) return; 919 if (!mounted || requestId != _voiceRequestId) return;
909 - _tencentSession = null;  
910 - _usingTencentAsr = false; 920 + _cloudAsrSession = null;
  921 + _usingCloudAsr = false;
911 await session.dispose(); 922 await session.dispose();
912 await _startWhisperVoice(requestId); 923 await _startWhisperVoice(requestId);
913 return; 924 return;
@@ -972,20 +983,20 @@ class _AgentChatPageState extends State<AgentChatPage> { @@ -972,20 +983,20 @@ class _AgentChatPageState extends State<AgentChatPage> {
972 await _startWhisperRecording(requestId); 983 await _startWhisperRecording(requestId);
973 } 984 }
974 985
975 - Future<void> _stopTencentVoice(int requestId) async { 986 + Future<void> _stopCloudVoice(int requestId) async {
976 _voiceTimer?.cancel(); 987 _voiceTimer?.cancel();
977 - final TencentAsrSession? session = _tencentSession; 988 + final ParaformerAsrSession? session = _cloudAsrSession;
978 if (session == null) return; 989 if (session == null) return;
979 990
980 String? text; 991 String? text;
981 try { 992 try {
982 text = await session.stop(); 993 text = await session.stop();
983 } catch (error) { 994 } catch (error) {
984 - debugPrint('Tencent ASR stop failed: $error'); 995 + debugPrint('Paraformer ASR stop failed: $error');
985 } 996 }
986 unawaited(session.dispose()); 997 unawaited(session.dispose());
987 - _tencentSession = null;  
988 - _usingTencentAsr = false; 998 + _cloudAsrSession = null;
  999 + _usingCloudAsr = false;
989 if (!mounted || requestId != _voiceRequestId) return; 1000 if (!mounted || requestId != _voiceRequestId) return;
990 await _finishVoiceWithText(text); 1001 await _finishVoiceWithText(text);
991 } 1002 }
@@ -1240,9 +1251,9 @@ class _AgentChatPageState extends State<AgentChatPage> { @@ -1240,9 +1251,9 @@ class _AgentChatPageState extends State<AgentChatPage> {
1240 if (!mounted) return; 1251 if (!mounted) return;
1241 if (_voiceStage != _VoiceStage.recording) return; 1252 if (_voiceStage != _VoiceStage.recording) return;
1242 1253
1243 - if (_usingTencentAsr) { 1254 + if (_usingCloudAsr) {
1244 final int requestId = _voiceRequestId; 1255 final int requestId = _voiceRequestId;
1245 - await _stopTencentVoice(requestId); 1256 + await _stopCloudVoice(requestId);
1246 return; 1257 return;
1247 } 1258 }
1248 1259
@@ -1308,8 +1319,8 @@ class _AgentChatPageState extends State<AgentChatPage> { @@ -1308,8 +1319,8 @@ class _AgentChatPageState extends State<AgentChatPage> {
1308 _voiceTimer?.cancel(); 1319 _voiceTimer?.cancel();
1309 _usingNativeSpeech = false; 1320 _usingNativeSpeech = false;
1310 _usingWhisper = false; 1321 _usingWhisper = false;
1311 - _usingTencentAsr = false;  
1312 - _tencentSession = null; 1322 + _usingCloudAsr = false;
  1323 + _cloudAsrSession = null;
1313 _whisperUpdateSub?.cancel(); 1324 _whisperUpdateSub?.cancel();
1314 _whisperUpdateSub = null; 1325 _whisperUpdateSub = null;
1315 _whisperTask = null; 1326 _whisperTask = null;
@@ -1344,9 +1355,9 @@ class _AgentChatPageState extends State<AgentChatPage> { @@ -1344,9 +1355,9 @@ class _AgentChatPageState extends State<AgentChatPage> {
1344 _voiceRequestId++; 1355 _voiceRequestId++;
1345 _usingWhisper = false; 1356 _usingWhisper = false;
1346 _usingNativeSpeech = false; 1357 _usingNativeSpeech = false;
1347 - _usingTencentAsr = false;  
1348 - unawaited(_tencentSession?.cancel());  
1349 - _tencentSession = null; 1358 + _usingCloudAsr = false;
  1359 + unawaited(_cloudAsrSession?.cancel());
  1360 + _cloudAsrSession = null;
1350 _whisperUpdateSub?.cancel(); 1361 _whisperUpdateSub?.cancel();
1351 _whisperUpdateSub = null; 1362 _whisperUpdateSub = null;
1352 _whisperTask = null; 1363 _whisperTask = null;
@@ -1367,10 +1378,10 @@ class _AgentChatPageState extends State<AgentChatPage> { @@ -1367,10 +1378,10 @@ class _AgentChatPageState extends State<AgentChatPage> {
1367 _voiceTimer?.cancel(); 1378 _voiceTimer?.cancel();
1368 _voiceRequestId++; 1379 _voiceRequestId++;
1369 _usingWhisper = false; 1380 _usingWhisper = false;
1370 - if (_usingTencentAsr) {  
1371 - _usingTencentAsr = false;  
1372 - unawaited(_tencentSession?.cancel());  
1373 - _tencentSession = null; 1381 + if (_usingCloudAsr) {
  1382 + _usingCloudAsr = false;
  1383 + unawaited(_cloudAsrSession?.cancel());
  1384 + _cloudAsrSession = null;
1374 } 1385 }
1375 if (!mounted) return; 1386 if (!mounted) return;
1376 setState(() { 1387 setState(() {
@@ -1384,10 +1395,10 @@ class _AgentChatPageState extends State<AgentChatPage> { @@ -1384,10 +1395,10 @@ class _AgentChatPageState extends State<AgentChatPage> {
1384 _voiceTimer?.cancel(); 1395 _voiceTimer?.cancel();
1385 _voiceRequestId++; 1396 _voiceRequestId++;
1386 _usingWhisper = false; 1397 _usingWhisper = false;
1387 - if (_usingTencentAsr) {  
1388 - _usingTencentAsr = false;  
1389 - final TencentAsrSession? session = _tencentSession;  
1390 - _tencentSession = null; 1398 + if (_usingCloudAsr) {
  1399 + _usingCloudAsr = false;
  1400 + final ParaformerAsrSession? session = _cloudAsrSession;
  1401 + _cloudAsrSession = null;
1391 if (session != null) { 1402 if (session != null) {
1392 try { 1403 try {
1393 await session.cancel(); 1404 await session.cancel();
@@ -1429,27 +1440,62 @@ class _AgentChatPageState extends State<AgentChatPage> { @@ -1429,27 +1440,62 @@ class _AgentChatPageState extends State<AgentChatPage> {
1429 1440
1430 Future<void> _pickReservationImage() async { 1441 Future<void> _pickReservationImage() async {
1431 if (_imageProcessing) return; 1442 if (_imageProcessing) return;
1432 - setState(() => _imageProcessing = true); 1443 + final XFile? file = await ImagePicker().pickImage(
  1444 + source: ImageSource.gallery,
  1445 + maxWidth: 1600,
  1446 + imageQuality: 80,
  1447 + );
  1448 + if (file == null) return;
  1449 + await _processReservationImageBytes(await file.readAsBytes());
  1450 + }
  1451 +
  1452 + /// 订阅外部分享(如微信截图)进来的图片,自动进入预订识别流程。
  1453 + void _initSharedReservationListener() {
  1454 + final SharedReservationReceiver receiver =
  1455 + SharedReservationReceiver.instance;
  1456 + _sharedImageListener = () {
  1457 + final String? path = receiver.pendingImage.value;
  1458 + if (path == null) return;
  1459 + unawaited(_processSharedReservationImage(path));
  1460 + };
  1461 + receiver.pendingImage.addListener(_sharedImageListener!);
  1462 +
  1463 + final String? initialPath = receiver.pendingImage.value;
  1464 + if (initialPath != null) {
  1465 + WidgetsBinding.instance.addPostFrameCallback((_) {
  1466 + unawaited(_processSharedReservationImage(initialPath));
  1467 + });
  1468 + }
  1469 + }
1433 1470
  1471 + Future<void> _processSharedReservationImage(String path) async {
  1472 + if (_imageProcessing) return;
1434 try { 1473 try {
1435 - final XFile? file = await ImagePicker().pickImage(  
1436 - source: ImageSource.gallery,  
1437 - maxWidth: 1600,  
1438 - imageQuality: 80,  
1439 - );  
1440 - if (file == null) return;  
1441 -  
1442 - final Uint8List bytes = await file.readAsBytes();  
1443 - final BaiduOcrClient ocr = BaiduOcrClient(  
1444 - apiKey: _baiduApiKey,  
1445 - secretKey: _baiduSecretKey,  
1446 - );  
1447 - final String text = await ocr.recognize(bytes);  
1448 - debugPrint('OCR识别成功: ${text.length}字'); 1474 + final Uint8List bytes = await File(path).readAsBytes();
1449 if (!mounted) return; 1475 if (!mounted) return;
  1476 + await _processReservationImageBytes(bytes);
  1477 + } catch (error) {
  1478 + debugPrint('读取分享图片失败: $error');
  1479 + if (mounted) {
  1480 + ScaffoldMessenger.of(context).showSnackBar(
  1481 + const SnackBar(content: Text('读取分享图片失败,请重试')),
  1482 + );
  1483 + }
  1484 + } finally {
  1485 + SharedReservationReceiver.instance.consumeImage();
  1486 + }
  1487 + }
1450 1488
  1489 + Future<void> _processReservationImageBytes(Uint8List bytes) async {
  1490 + if (_imageProcessing) return;
  1491 + setState(() => _imageProcessing = true);
  1492 + try {
  1493 + final QwenOcrClient ocr = QwenOcrClient(apiKey: _qwenApiKey);
1451 final ReservationImageParseResult result = 1494 final ReservationImageParseResult result =
1452 - ReservationImageParser.parse(text); 1495 + await ocr.parseReservation(bytes);
  1496 + debugPrint('千问OCR识别成功: ${result.rawText.length}字');
  1497 + if (!mounted) return;
  1498 +
1453 if (!result.hasAny) { 1499 if (!result.hasAny) {
1454 ScaffoldMessenger.of(context).showSnackBar( 1500 ScaffoldMessenger.of(context).showSnackBar(
1455 const SnackBar(content: Text('没有从图片中识别到预订信息')), 1501 const SnackBar(content: Text('没有从图片中识别到预订信息')),
@@ -1459,7 +1505,7 @@ class _AgentChatPageState extends State<AgentChatPage> { @@ -1459,7 +1505,7 @@ class _AgentChatPageState extends State<AgentChatPage> {
1459 1505
1460 setState(() => _imageProcessing = false); 1506 setState(() => _imageProcessing = false);
1461 await _showReservationConfirm(_normalizeReservationDate(result)); 1507 await _showReservationConfirm(_normalizeReservationDate(result));
1462 - } on BaiduOcrException catch (error) { 1508 + } on QwenOcrException catch (error) {
1463 debugPrint('OCR失败: ${error.toString()}'); 1509 debugPrint('OCR失败: ${error.toString()}');
1464 if (mounted) { 1510 if (mounted) {
1465 ScaffoldMessenger.of(context).showSnackBar( 1511 ScaffoldMessenger.of(context).showSnackBar(
@@ -1511,10 +1557,11 @@ class _AgentChatPageState extends State<AgentChatPage> { @@ -1511,10 +1557,11 @@ class _AgentChatPageState extends State<AgentChatPage> {
1511 if (timeText.isNotEmpty) timeText, 1557 if (timeText.isNotEmpty) timeText,
1512 ].join(' '); 1558 ].join(' ');
1513 1559
1514 - final DateTime? bookingTime = _parseReservationTime(combined); 1560 + DateTime? bookingTime = _parseReservationTime(combined);
1515 if (bookingTime == null || combined.isEmpty) { 1561 if (bookingTime == null || combined.isEmpty) {
1516 return result; 1562 return result;
1517 } 1563 }
  1564 + bookingTime = _adjustFutureDate(bookingTime, result.rawText);
1518 1565
1519 final String month = bookingTime.month.toString().padLeft(2, '0'); 1566 final String month = bookingTime.month.toString().padLeft(2, '0');
1520 final String day = bookingTime.day.toString().padLeft(2, '0'); 1567 final String day = bookingTime.day.toString().padLeft(2, '0');
@@ -1563,6 +1610,67 @@ class _AgentChatPageState extends State<AgentChatPage> { @@ -1563,6 +1610,67 @@ class _AgentChatPageState extends State<AgentChatPage> {
1563 NavigatorUtil.goNoteBookChangeFPage(context, detail, ''); 1610 NavigatorUtil.goNoteBookChangeFPage(context, detail, '');
1564 } 1611 }
1565 1612
  1613 + DateTime _adjustFutureDate(DateTime date, String rawText) {
  1614 + final DateTime now = DateTime.now();
  1615 + final DateTime today = DateTime(now.year, now.month, now.day);
  1616 + if (!DateTime(date.year, date.month, date.day).isBefore(today)) {
  1617 + return date;
  1618 + }
  1619 +
  1620 + final int? weekday = _weekdayFromText(rawText);
  1621 + final bool hasMonth =
  1622 + RegExp(r'\d{1,2}\s*月|\d{4}[-/.]\d{1,2}').hasMatch(rawText);
  1623 +
  1624 + DateTime? result;
  1625 + if (weekday != null) {
  1626 + for (int i = 1; i <= 3660; i++) {
  1627 + final DateTime cursor = today.add(Duration(days: i));
  1628 + if (cursor.day == date.day && cursor.weekday == weekday) {
  1629 + result = cursor;
  1630 + break;
  1631 + }
  1632 + }
  1633 + } else if (hasMonth) {
  1634 + DateTime candidate = DateTime(now.year, date.month, date.day);
  1635 + if (candidate.isBefore(today)) {
  1636 + candidate = DateTime(now.year + 1, date.month, date.day);
  1637 + }
  1638 + result = candidate;
  1639 + } else {
  1640 + DateTime candidate = DateTime(now.year, now.month, date.day);
  1641 + if (candidate.isBefore(today)) {
  1642 + candidate = DateTime(now.year, now.month + 1, date.day);
  1643 + }
  1644 + result = candidate;
  1645 + }
  1646 +
  1647 + final DateTime safe = result ?? date;
  1648 + return DateTime(safe.year, safe.month, safe.day, date.hour, date.minute);
  1649 + }
  1650 +
  1651 + int? _weekdayFromText(String text) {
  1652 + if (RegExp(r'周一').hasMatch(text)) return 1;
  1653 + const Map<String, int> mapping = <String, int>{
  1654 + '周二': 2,
  1655 + '星期二': 2,
  1656 + '周三': 3,
  1657 + '星期三': 3,
  1658 + '周四': 4,
  1659 + '星期四': 4,
  1660 + '周五': 5,
  1661 + '星期五': 5,
  1662 + '周六': 6,
  1663 + '星期六': 6,
  1664 + '周日': 7,
  1665 + '星期日': 7,
  1666 + '星期天': 7,
  1667 + };
  1668 + for (final MapEntry<String, int> entry in mapping.entries) {
  1669 + if (text.contains(entry.key)) return entry.value;
  1670 + }
  1671 + return null;
  1672 + }
  1673 +
1566 String _sexCode(String sex) { 1674 String _sexCode(String sex) {
1567 switch (sex) { 1675 switch (sex) {
1568 case '先生': 1676 case '先生':
@@ -1593,8 +1701,7 @@ class _AgentChatPageState extends State<AgentChatPage> { @@ -1593,8 +1701,7 @@ class _AgentChatPageState extends State<AgentChatPage> {
1593 int.parse(fullDate.group(3)!), 1701 int.parse(fullDate.group(3)!),
1594 ); 1702 );
1595 } else { 1703 } else {
1596 - final Match? monthDay =  
1597 - RegExp(r'(\d{1,2})月(\d{1,2})日').firstMatch(text); 1704 + final Match? monthDay = RegExp(r'(\d{1,2})月(\d{1,2})日').firstMatch(text);
1598 if (monthDay != null) { 1705 if (monthDay != null) {
1599 date = DateTime( 1706 date = DateTime(
1600 now.year, 1707 now.year,
@@ -1674,103 +1781,103 @@ class _AgentChatPageState extends State<AgentChatPage> { @@ -1674,103 +1781,103 @@ class _AgentChatPageState extends State<AgentChatPage> {
1674 statusBarBrightness: Brightness.light, 1781 statusBarBrightness: Brightness.light,
1675 ), 1782 ),
1676 child: Scaffold( 1783 child: Scaffold(
1677 - backgroundColor: const Color(0xFFF3F8F8),  
1678 - appBar: AppBar(  
1679 - backgroundColor: themeColor,  
1680 - foregroundColor: Colors.white,  
1681 - elevation: 0,  
1682 - centerTitle: true,  
1683 - title: Column(  
1684 - mainAxisSize: MainAxisSize.min,  
1685 - children: [  
1686 - const Text(  
1687 - 'AI 经营助手',  
1688 - style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600),  
1689 - ),  
1690 - Text(  
1691 - _storeName,  
1692 - style: const TextStyle(  
1693 - fontSize: 11,  
1694 - color: Colors.white70, 1784 + backgroundColor: const Color(0xFFF3F8F8),
  1785 + appBar: AppBar(
  1786 + backgroundColor: themeColor,
  1787 + foregroundColor: Colors.white,
  1788 + elevation: 0,
  1789 + centerTitle: true,
  1790 + title: Column(
  1791 + mainAxisSize: MainAxisSize.min,
  1792 + children: [
  1793 + const Text(
  1794 + 'AI 经营助手',
  1795 + style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600),
1695 ), 1796 ),
1696 - ),  
1697 - ], 1797 + Text(
  1798 + _storeName,
  1799 + style: const TextStyle(
  1800 + fontSize: 11,
  1801 + color: Colors.white70,
  1802 + ),
  1803 + ),
  1804 + ],
  1805 + ),
1698 ), 1806 ),
1699 - ),  
1700 - body: Stack(  
1701 - children: [  
1702 - Column(  
1703 - children: [  
1704 - Expanded(  
1705 - child: _historyLoading  
1706 - ? const Center(  
1707 - child: CircularProgressIndicator(  
1708 - color: MyColours.color_199095,  
1709 - ),  
1710 - )  
1711 - : ListView.builder(  
1712 - controller: _scrollController,  
1713 - padding: const EdgeInsets.symmetric(  
1714 - horizontal: 12,  
1715 - vertical: 14, 1807 + body: Stack(
  1808 + children: [
  1809 + Column(
  1810 + children: [
  1811 + Expanded(
  1812 + child: _historyLoading
  1813 + ? const Center(
  1814 + child: CircularProgressIndicator(
  1815 + color: MyColours.color_199095,
  1816 + ),
  1817 + )
  1818 + : ListView.builder(
  1819 + controller: _scrollController,
  1820 + padding: const EdgeInsets.symmetric(
  1821 + horizontal: 12,
  1822 + vertical: 14,
  1823 + ),
  1824 + itemCount: _messages.length,
  1825 + itemBuilder: (_, int index) {
  1826 + return _MessageBubble(
  1827 + message: _messages[index],
  1828 + themeColor: themeColor,
  1829 + );
  1830 + },
1716 ), 1831 ),
1717 - itemCount: _messages.length,  
1718 - itemBuilder: (_, int index) {  
1719 - return _MessageBubble(  
1720 - message: _messages[index],  
1721 - themeColor: themeColor,  
1722 - );  
1723 - },  
1724 - ), 1832 + ),
  1833 + _QuickPrompts(
  1834 + controller: _promptScrollController,
  1835 + themeColor: themeColor,
  1836 + onSend: _send,
  1837 + ),
  1838 + _InputBar(
  1839 + controller: _inputController,
  1840 + streaming: _streaming,
  1841 + themeColor: themeColor,
  1842 + onSend: _send,
  1843 + onStop: _stop,
  1844 + onVoiceLongPressStart: _startVoice,
  1845 + onVoiceLongPressEnd: _stopVoice,
  1846 + onVoiceTap: _showVoiceTapHint,
  1847 + onPickImage: _pickReservationImage,
  1848 + imageProcessing: _imageProcessing,
  1849 + ),
  1850 + ],
  1851 + ),
  1852 + if (_voiceStage == _VoiceStage.preparing)
  1853 + _PreparingVoiceOverlay(
  1854 + hint: _voiceHint,
  1855 + progress: _whisperDownloadProgress,
  1856 + themeColor: themeColor,
  1857 + onCancel: _cancelVoicePreparation,
1725 ), 1858 ),
1726 - _QuickPrompts(  
1727 - controller: _promptScrollController, 1859 + if (_voiceStage == _VoiceStage.recording)
  1860 + _RecordOverlay(
  1861 + seconds: _voiceSeconds,
1728 themeColor: themeColor, 1862 themeColor: themeColor,
1729 - onSend: _send, 1863 + recognizedText: _recognizedText,
  1864 + onStop: _stopVoice,
  1865 + onCancel: _cancelVoiceRecording,
1730 ), 1866 ),
1731 - _InputBar(  
1732 - controller: _inputController,  
1733 - streaming: _streaming, 1867 + if (_voiceStage == _VoiceStage.confirming)
  1868 + _ConfirmVoiceBox(
  1869 + initialText: _confirmText,
1734 themeColor: themeColor, 1870 themeColor: themeColor,
1735 - onSend: _send,  
1736 - onStop: _stop,  
1737 - onVoiceLongPressStart: _startVoice,  
1738 - onVoiceLongPressEnd: _stopVoice,  
1739 - onVoiceTap: _showVoiceTapHint,  
1740 - onPickImage: _pickReservationImage,  
1741 - imageProcessing: _imageProcessing, 1871 + onCancel: () {
  1872 + setState(() => _voiceStage = _VoiceStage.idle);
  1873 + },
  1874 + onConfirm: _confirmVoice,
  1875 + onChanged: (String value) => _confirmText = value,
1742 ), 1876 ),
1743 - ],  
1744 - ),  
1745 - if (_voiceStage == _VoiceStage.preparing)  
1746 - _PreparingVoiceOverlay(  
1747 - hint: _voiceHint,  
1748 - progress: _whisperDownloadProgress,  
1749 - themeColor: themeColor,  
1750 - onCancel: _cancelVoicePreparation,  
1751 - ),  
1752 - if (_voiceStage == _VoiceStage.recording)  
1753 - _RecordOverlay(  
1754 - seconds: _voiceSeconds,  
1755 - themeColor: themeColor,  
1756 - recognizedText: _recognizedText,  
1757 - onStop: _stopVoice,  
1758 - onCancel: _cancelVoiceRecording,  
1759 - ),  
1760 - if (_voiceStage == _VoiceStage.confirming)  
1761 - _ConfirmVoiceBox(  
1762 - initialText: _confirmText,  
1763 - themeColor: themeColor,  
1764 - onCancel: () {  
1765 - setState(() => _voiceStage = _VoiceStage.idle);  
1766 - },  
1767 - onConfirm: _confirmVoice,  
1768 - onChanged: (String value) => _confirmText = value,  
1769 - ),  
1770 - ], 1877 + ],
  1878 + ),
1771 ), 1879 ),
1772 - ),  
1773 - ); 1880 + );
1774 } 1881 }
1775 } 1882 }
1776 1883
@@ -2087,7 +2194,8 @@ class _ReservationConfirmSheetState extends State<_ReservationConfirmSheet> { @@ -2087,7 +2194,8 @@ class _ReservationConfirmSheetState extends State<_ReservationConfirmSheet> {
2087 _field('预订人', _nameController), 2194 _field('预订人', _nameController),
2088 _field('称谓', _sexController), 2195 _field('称谓', _sexController),
2089 _field('联系电话', _phoneController, keyboardType: TextInputType.phone), 2196 _field('联系电话', _phoneController, keyboardType: TextInputType.phone),
2090 - _field('用餐人数', _partyController, keyboardType: TextInputType.number), 2197 + _field('用餐人数', _partyController,
  2198 + keyboardType: TextInputType.number),
2091 _field('预订日期', _dateController), 2199 _field('预订日期', _dateController),
2092 _field('用餐时段', _mealController), 2200 _field('用餐时段', _mealController),
2093 _field('预订时间', _timeController), 2201 _field('预订时间', _timeController),
@@ -2640,7 +2748,8 @@ class _AgentCardDetailPage extends StatelessWidget { @@ -2640,7 +2748,8 @@ class _AgentCardDetailPage extends StatelessWidget {
2640 ]); 2748 ]);
2641 return rows; 2749 return rows;
2642 case _CardKind.tableList: 2750 case _CardKind.tableList:
2643 - final List<_DetailRow> rows = card.items.map((Map<String, dynamic> item) { 2751 + final List<_DetailRow> rows =
  2752 + card.items.map((Map<String, dynamic> item) {
2644 final String rawName = '${item['name'] ?? ''}'; 2753 final String rawName = '${item['name'] ?? ''}';
2645 final String tableName = rawName.split(' · ').first.trim(); 2754 final String tableName = rawName.split(' · ').first.trim();
2646 final String status = '${item['status'] ?? ''}'; 2755 final String status = '${item['status'] ?? ''}';
1 import 'dart:async'; 1 import 'dart:async';
2 import 'dart:convert'; 2 import 'dart:convert';
3 -import 'package:crypto/crypto.dart'; 3 +import 'dart:math';
  4 +
4 import 'package:flutter/foundation.dart'; 5 import 'package:flutter/foundation.dart';
5 import 'package:permission_handler/permission_handler.dart'; 6 import 'package:permission_handler/permission_handler.dart';
6 import 'package:record/record.dart'; 7 import 'package:record/record.dart';
  8 +import 'package:web_socket_channel/io.dart';
7 import 'package:web_socket_channel/web_socket_channel.dart'; 9 import 'package:web_socket_channel/web_socket_channel.dart';
8 10
9 -/// 腾讯云实时语音识别(WebSocket)客户端。 11 +/// 阿里云百炼 Paraformer 实时语音识别(WebSocket)客户端。
10 /// 12 ///
11 -/// 协议说明:https://cloud.tencent.com/document/product/1093/48982  
12 -class TencentAsrClient {  
13 - TencentAsrClient({  
14 - required this.secretId,  
15 - required this.secretKey,  
16 - required this.appId,  
17 - this.engineModelType = '16k_zh', 13 +/// 协议说明:
  14 +/// https://help.aliyun.com/zh/model-studio/websocket-for-paraformer-real-time-service
  15 +class ParaformerAsrClient {
  16 + ParaformerAsrClient({
  17 + required this.apiKey,
  18 + this.model = 'paraformer-realtime-v2',
18 }); 19 });
19 20
20 - final String secretId;  
21 - final String secretKey;  
22 - final String appId;  
23 - final String engineModelType; 21 + /// 统一复用百炼 API Key(与图片识别共用一个 Key)。
  22 + final String apiKey;
  23 + final String model;
24 24
25 static const int _frameBytes = 6400; 25 static const int _frameBytes = 6400;
26 26
@@ -35,22 +35,25 @@ class TencentAsrClient { @@ -35,22 +35,25 @@ class TencentAsrClient {
35 final List<String> _sentences = <String>[]; 35 final List<String> _sentences = <String>[];
36 String _partial = ''; 36 String _partial = '';
37 void Function(String text)? _onPartial; 37 void Function(String text)? _onPartial;
38 - bool _handshakeDone = false; 38 + String _taskId = '';
39 39
40 String _combinedText() => (_sentences.join() + _partial).trim(); 40 String _combinedText() => (_sentences.join() + _partial).trim();
41 41
42 - /// 建立连接并等待云端握手成功。失败时抛出 [TencentAsrException]。 42 + /// 建立连接、发送 run-task 并等待服务端 task-started。
  43 + /// 失败时抛出 [ParaformerAsrException]。
43 Future<void> connect() async { 44 Future<void> connect() async {
44 - final String voiceId = _newVoiceId();  
45 - final Uri uri = _buildUri(voiceId);  
46 - final WebSocketChannel channel = WebSocketChannel.connect(uri); 45 + _taskId = _newUuidV4();
  46 + final WebSocketChannel channel = IOWebSocketChannel.connect(
  47 + Uri.parse('wss://dashscope.aliyuncs.com/api-ws/v1/inference'),
  48 + headers: <String, dynamic>{'Authorization': 'Bearer $apiKey'},
  49 + );
47 _channel = channel; 50 _channel = channel;
48 51
49 try { 52 try {
50 await channel.ready.timeout(const Duration(seconds: 8)); 53 await channel.ready.timeout(const Duration(seconds: 8));
51 } catch (error) { 54 } catch (error) {
52 await _closeSocket(); 55 await _closeSocket();
53 - throw const TencentAsrException(0, '无法连接腾讯云语音服务,请检查网络'); 56 + throw const ParaformerAsrException(0, '无法连接百炼语音服务,请检查网络');
54 } 57 }
55 58
56 _handshake = Completer<void>(); 59 _handshake = Completer<void>();
@@ -65,19 +68,20 @@ class TencentAsrClient { @@ -65,19 +68,20 @@ class TencentAsrClient {
65 }, 68 },
66 ); 69 );
67 70
  71 + _sendText(jsonEncode(_runTaskPayload()));
  72 +
68 try { 73 try {
69 await _handshake!.future.timeout(const Duration(seconds: 8)); 74 await _handshake!.future.timeout(const Duration(seconds: 8));
70 - _handshakeDone = true;  
71 - debugPrint('Tencent ASR connected'); 75 + debugPrint('Paraformer ASR connected');
72 } catch (error) { 76 } catch (error) {
73 await _teardown(); 77 await _teardown();
74 - if (error is TencentAsrException) rethrow;  
75 - throw const TencentAsrException(0, '语音服务握手超时'); 78 + if (error is ParaformerAsrException) rethrow;
  79 + throw const ParaformerAsrException(0, '语音服务握手超时');
76 } 80 }
77 } 81 }
78 82
79 - /// 开始转发 PCM 音频流。按腾讯云协议要求,以 200ms/6400 字节为  
80 - /// 一个数据包发送;音频流结束时自动发送 `{"type":"end"}`。 83 + /// 开始转发 PCM 音频流。按协议以 200ms/6400 字节为一个数据包发送;
  84 + /// 音频流结束时自动发送 finish-task。
81 void startForwarding(Stream<Uint8List> audio) { 85 void startForwarding(Stream<Uint8List> audio) {
82 _audioSub = audio.listen( 86 _audioSub = audio.listen(
83 (Uint8List chunk) => _pcmBuffer.addAll(chunk), 87 (Uint8List chunk) => _pcmBuffer.addAll(chunk),
@@ -109,7 +113,14 @@ class TencentAsrClient { @@ -109,7 +113,14 @@ class TencentAsrClient {
109 _pcmBuffer.removeRange(0, length); 113 _pcmBuffer.removeRange(0, length);
110 _send(frame); 114 _send(frame);
111 } 115 }
112 - _sendText(jsonEncode(<String, String>{'type': 'end'})); 116 + _sendText(jsonEncode(<String, Object>{
  117 + 'header': <String, Object>{
  118 + 'action': 'finish-task',
  119 + 'task_id': _taskId,
  120 + 'streaming': 'duplex',
  121 + },
  122 + 'payload': <String, Object>{'input': <String, Object>{}},
  123 + }));
113 } 124 }
114 125
115 /// 停止后等待最终识别结果。 126 /// 停止后等待最终识别结果。
@@ -120,8 +131,8 @@ class TencentAsrClient { @@ -120,8 +131,8 @@ class TencentAsrClient {
120 return await result.future.timeout(const Duration(seconds: 6)); 131 return await result.future.timeout(const Duration(seconds: 6));
121 } on TimeoutException { 132 } on TimeoutException {
122 return _combinedText(); 133 return _combinedText();
123 - } on TencentAsrException catch (error) {  
124 - debugPrint('Tencent ASR error: $error'); 134 + } on ParaformerAsrException catch (error) {
  135 + debugPrint('Paraformer ASR error: $error');
125 return null; 136 return null;
126 } finally { 137 } finally {
127 await _teardown(); 138 await _teardown();
@@ -145,48 +156,75 @@ class TencentAsrClient { @@ -145,48 +156,75 @@ class TencentAsrClient {
145 _onPartial = callback; 156 _onPartial = callback;
146 } 157 }
147 158
  159 + Map<String, Object> _runTaskPayload() => <String, Object>{
  160 + 'header': <String, Object>{
  161 + 'action': 'run-task',
  162 + 'task_id': _taskId,
  163 + 'streaming': 'duplex',
  164 + },
  165 + 'payload': <String, Object>{
  166 + 'task_group': 'audio',
  167 + 'task': 'asr',
  168 + 'function': 'recognition',
  169 + 'model': model,
  170 + 'parameters': <String, Object>{
  171 + 'format': 'pcm',
  172 + 'sample_rate': 16000,
  173 + 'language_hints': <String>['zh'],
  174 + 'disfluency_removal_enabled': false,
  175 + 'semantic_punctuation_enabled': false,
  176 + 'max_sentence_silence': 600,
  177 + 'punctuation_prediction_enabled': true,
  178 + 'inverse_text_normalization_enabled': true,
  179 + },
  180 + 'input': <String, Object>{},
  181 + },
  182 + };
  183 +
148 void _onMessage(dynamic message) { 184 void _onMessage(dynamic message) {
  185 + if (message is! String) return;
149 try { 186 try {
150 final Map<String, dynamic> json = 187 final Map<String, dynamic> json =
151 - jsonDecode(message.toString()) as Map<String, dynamic>;  
152 - final int? code = json['code'] as int?;  
153 - if (code != null && code != 0) {  
154 - _fail(  
155 - TencentAsrException(  
156 - code,  
157 - json['message']?.toString() ?? '语音识别失败',  
158 - ),  
159 - );  
160 - return;  
161 - }  
162 -  
163 - if (!_handshakeDone) {  
164 - _handshakeDone = true;  
165 - if (_handshake != null && !_handshake!.isCompleted) {  
166 - _handshake!.complete();  
167 - }  
168 - }  
169 -  
170 - final Map<String, dynamic>? result =  
171 - json['result'] as Map<String, dynamic>?;  
172 - if (result != null) {  
173 - final int? sliceType = result['slice_type'] as int?;  
174 - final String text = (result['voice_text_str'] ?? '').toString();  
175 - if (sliceType == 2) {  
176 - final int index = (result['index'] as int?) ?? _sentences.length;  
177 - while (_sentences.length <= index) {  
178 - _sentences.add(''); 188 + jsonDecode(message) as Map<String, dynamic>;
  189 + final Map<String, dynamic>? header =
  190 + json['header'] as Map<String, dynamic>?;
  191 + final String? event = header?['event'] as String?;
  192 + switch (event) {
  193 + case 'task-started':
  194 + if (_handshake != null && !_handshake!.isCompleted) {
  195 + _handshake!.complete();
179 } 196 }
180 - _sentences[index] = text;  
181 - _partial = '';  
182 - } else if (sliceType == 1) {  
183 - _partial = text;  
184 - }  
185 - _onPartial?.call(_combinedText());  
186 - }  
187 -  
188 - if (json['final'] == 1) {  
189 - _completeResult(); 197 + return;
  198 + case 'result-generated':
  199 + final Map<String, dynamic>? payload =
  200 + json['payload'] as Map<String, dynamic>?;
  201 + final Map<String, dynamic>? output =
  202 + payload?['output'] as Map<String, dynamic>?;
  203 + final Map<String, dynamic>? sentence =
  204 + output?['sentence'] as Map<String, dynamic>?;
  205 + if (sentence == null) return;
  206 + if (sentence['heartbeat'] == true) return;
  207 + final String text = (sentence['text'] ?? '').toString();
  208 + final bool sentenceEnd = sentence['sentence_end'] == true;
  209 + if (sentenceEnd) {
  210 + _sentences.add(text);
  211 + _partial = '';
  212 + } else {
  213 + _partial = text;
  214 + }
  215 + _onPartial?.call(_combinedText());
  216 + return;
  217 + case 'task-finished':
  218 + _completeResult();
  219 + return;
  220 + case 'task-failed':
  221 + _fail(
  222 + ParaformerAsrException(
  223 + 0,
  224 + (header?['error_message'] ?? '语音识别失败').toString(),
  225 + ),
  226 + );
  227 + return;
190 } 228 }
191 } catch (_) { 229 } catch (_) {
192 // 忽略无法解析的单条消息。 230 // 忽略无法解析的单条消息。
@@ -243,85 +281,41 @@ class TencentAsrClient { @@ -243,85 +281,41 @@ class TencentAsrClient {
243 } catch (_) {} 281 } catch (_) {}
244 } 282 }
245 283
246 - Uri _buildUri(String voiceId) {  
247 - final int now = DateTime.now().millisecondsSinceEpoch ~/ 1000;  
248 - final Map<String, String> params = <String, String>{  
249 - 'secretid': secretId,  
250 - 'timestamp': '$now',  
251 - 'expired': '${now + 86400}',  
252 - 'nonce': '${_randomInt(999999999)}',  
253 - 'engine_model_type': engineModelType,  
254 - 'voice_id': voiceId,  
255 - 'voice_format': '1',  
256 - 'needvad': '1',  
257 - 'filter_modal': '1',  
258 - 'filter_punc': '1',  
259 - 'convert_num_mode': '1',  
260 - };  
261 -  
262 - final List<MapEntry<String, String>> sorted = params.entries.toList()  
263 - ..sort((MapEntry<String, String> a, MapEntry<String, String> b) =>  
264 - a.key.compareTo(b.key));  
265 -  
266 - final String canonical = sorted  
267 - .map((MapEntry<String, String> e) => '${e.key}=${e.value}')  
268 - .join('&');  
269 - final String signText = 'asr.cloud.tencent.com/asr/v2/$appId?$canonical';  
270 -  
271 - final List<int> digest = Hmac(sha1, utf8.encode(secretKey))  
272 - .convert(utf8.encode(signText))  
273 - .bytes;  
274 - final String signature = base64.encode(digest);  
275 -  
276 - final String query = sorted  
277 - .map((MapEntry<String, String> e) =>  
278 - '${e.key}=${Uri.encodeQueryComponent(e.value)}')  
279 - .join('&');  
280 -  
281 - return Uri.parse(  
282 - 'wss://asr.cloud.tencent.com/asr/v2/$appId?$query'  
283 - '&signature=${Uri.encodeQueryComponent(signature)}',  
284 - );  
285 - }  
286 -  
287 - String _newVoiceId() {  
288 - final int now = DateTime.now().microsecondsSinceEpoch;  
289 - final int rand = _randomInt(0x7fffffff);  
290 - return 'voice_${now}_$rand';  
291 - }  
292 -  
293 - int _randomInt(int max) {  
294 - final int seed = DateTime.now().microsecondsSinceEpoch;  
295 - return (seed % max).abs() + 1; 284 + String _newUuidV4() {
  285 + final Random random = Random.secure();
  286 + final List<int> bytes = List<int>.generate(16, (_) => random.nextInt(256));
  287 + bytes[6] = (bytes[6] & 0x0f) | 0x40;
  288 + bytes[8] = (bytes[8] & 0x3f) | 0x80;
  289 + final String hex =
  290 + bytes.map((int byte) => byte.toRadixString(16).padLeft(2, '0')).join();
  291 + return '${hex.substring(0, 8)}-${hex.substring(8, 12)}-'
  292 + '${hex.substring(12, 16)}-${hex.substring(16, 20)}-'
  293 + '${hex.substring(20)}';
296 } 294 }
297 } 295 }
298 296
299 -class TencentAsrException implements Exception {  
300 - const TencentAsrException(this.code, this.message); 297 +class ParaformerAsrException implements Exception {
  298 + const ParaformerAsrException(this.code, this.message);
301 299
302 final int code; 300 final int code;
303 final String message; 301 final String message;
304 302
305 @override 303 @override
306 - String toString() => 'TencentAsrException($code, $message)'; 304 + String toString() => 'ParaformerAsrException($code, $message)';
307 } 305 }
308 306
309 -/// 负责录音并把 PCM 流交给 [TencentAsrClient] 的会话封装。  
310 -class TencentAsrSession {  
311 - TencentAsrSession({  
312 - required this.secretId,  
313 - required this.secretKey,  
314 - required this.appId, 307 +/// 负责录音并把 PCM 流交给 [ParaformerAsrClient] 的会话封装。
  308 +class ParaformerAsrSession {
  309 + ParaformerAsrSession({
  310 + required this.apiKey,
315 this.onPartial, 311 this.onPartial,
316 }); 312 });
317 313
318 - final String secretId;  
319 - final String secretKey;  
320 - final String appId; 314 + final String apiKey;
321 final void Function(String text)? onPartial; 315 final void Function(String text)? onPartial;
322 316
323 final AudioRecorder _recorder = AudioRecorder(); 317 final AudioRecorder _recorder = AudioRecorder();
324 - TencentAsrClient? _client; 318 + ParaformerAsrClient? _client;
325 bool _recording = false; 319 bool _recording = false;
326 320
327 /// 检查权限、建立连接并完成云端握手,此时尚未开始录音。 321 /// 检查权限、建立连接并完成云端握手,此时尚未开始录音。
@@ -329,23 +323,19 @@ class TencentAsrSession { @@ -329,23 +323,19 @@ class TencentAsrSession {
329 if (!await _recorder.hasPermission()) { 323 if (!await _recorder.hasPermission()) {
330 final PermissionStatus status = await Permission.microphone.request(); 324 final PermissionStatus status = await Permission.microphone.request();
331 if (!status.isGranted) { 325 if (!status.isGranted) {
332 - throw const TencentAsrException(0, '未获得麦克风权限'); 326 + throw const ParaformerAsrException(0, '未获得麦克风权限');
333 } 327 }
334 } 328 }
335 - final TencentAsrClient client = TencentAsrClient(  
336 - secretId: secretId,  
337 - secretKey: secretKey,  
338 - appId: appId,  
339 - ); 329 + final ParaformerAsrClient client = ParaformerAsrClient(apiKey: apiKey);
340 _client = client; 330 _client = client;
341 await client.connect(); 331 await client.connect();
342 } 332 }
343 333
344 /// 开始录音并发送音频。 334 /// 开始录音并发送音频。
345 Future<void> startAudio() async { 335 Future<void> startAudio() async {
346 - final TencentAsrClient? client = _client; 336 + final ParaformerAsrClient? client = _client;
347 if (client == null) { 337 if (client == null) {
348 - throw const TencentAsrException(0, '语音会话未建立'); 338 + throw const ParaformerAsrException(0, '语音会话未建立');
349 } 339 }
350 340
351 final Stream<Uint8List> audio = await _recorder.startStream( 341 final Stream<Uint8List> audio = await _recorder.startStream(
@@ -379,7 +369,7 @@ class TencentAsrSession { @@ -379,7 +369,7 @@ class TencentAsrSession {
379 try { 369 try {
380 await _recorder.stop(); 370 await _recorder.stop();
381 } catch (_) {} 371 } catch (_) {}
382 - final TencentAsrClient? client = _client; 372 + final ParaformerAsrClient? client = _client;
383 _client = null; 373 _client = null;
384 try { 374 try {
385 await client?.cancel(); 375 await client?.cancel();
  1 +import 'dart:convert';
  2 +import 'dart:typed_data';
  3 +
  4 +import 'package:dio/dio.dart';
  5 +
  6 +import 'package:one_key/pages/agent/reservation_image_parser.dart';
  7 +
  8 +/// 阿里云百炼(DashScope)千问视觉模型客户端。
  9 +///
  10 +/// 使用 OpenAI 兼容模式,让 qwen-vl-plus 直接从图片中提取预订字段,
  11 +/// 并返回结构化 JSON,匹配 [ReservationImageParseResult] 的字段。
  12 +class QwenOcrClient {
  13 + QwenOcrClient({
  14 + required this.apiKey,
  15 + this.model = 'qwen-vl-plus',
  16 + });
  17 +
  18 + final String apiKey;
  19 + final String model;
  20 +
  21 + static const String _endpoint =
  22 + 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions';
  23 +
  24 + final Dio _dio = Dio(
  25 + BaseOptions(
  26 + connectTimeout: const Duration(seconds: 20),
  27 + receiveTimeout: const Duration(seconds: 60),
  28 + responseType: ResponseType.json,
  29 + ),
  30 + );
  31 +
  32 + String _systemPrompt() {
  33 + final DateTime now = DateTime.now();
  34 + final String today = '${now.year}-${now.month.toString().padLeft(2, '0')}-'
  35 + '${now.day.toString().padLeft(2, '0')}';
  36 + return '今天是 $today。'
  37 + '你是餐厅预订信息提取助手。请仔细识别图片中的文字,并从微信聊天记录、'
  38 + '预订截图或手写信息中提取餐厅预订字段。'
  39 + '只输出一个 JSON 对象,不要输出任何解释或 Markdown 代码块标记。\n\n'
  40 + 'JSON 字段说明:\n'
  41 + '- name: 预订人姓名,去掉“先生/女士”等称谓\n'
  42 + '- sex: 称谓,只能是 先生、女士、公司、贵宾 之一,没有则为空字符串\n'
  43 + '- phone: 手机号,没有则为空字符串\n'
  44 + '- party: 用餐人数,纯数字字符串,没有则为空字符串\n'
  45 + '- date: 预订日期,格式 YYYY-MM-DD;必须是今天或未来日期;只有月日或星期时,按今天 $today 推断为下一个未来日期并补全年份\n'
  46 + '- meal: 用餐时段,只能是 早餐、午餐、晚餐 之一;根据时间和上下文推断,没有则为空字符串\n'
  47 + '- time: 到店时间,格式 HH:mm,没有则为空字符串\n'
  48 + '- table: 桌位号或包间号,没有则为空字符串\n'
  49 + '- remark: 备注/特殊要求,没有则为空字符串\n'
  50 + '- rawText: 图片中识别出的完整文字,按原顺序用换行拼接\n\n'
  51 + '输出示例:\n'
  52 + '{"name":"王先生","sex":"先生","phone":"13800138000",'
  53 + '"party":"8","date":"$today","meal":"晚餐",'
  54 + '"time":"18:30","table":"A8","remark":"靠窗",'
  55 + '"rawText":"王先生 13800138000\\n9月25日晚餐 8人"}';
  56 + }
  57 +
  58 + /// 从图片中提取预订字段;结构化识别失败时自动降级为正则解析。
  59 + Future<ReservationImageParseResult> parseReservation(
  60 + Uint8List imageBytes,
  61 + ) async {
  62 + if (imageBytes.isEmpty) {
  63 + throw const QwenOcrException(0, '图片内容为空');
  64 + }
  65 +
  66 + final String content = await _chat(imageBytes);
  67 + final Map<String, dynamic>? json = _parseJsonObject(content);
  68 +
  69 + final ReservationImageParseResult parsed =
  70 + ReservationImageParser.parse(content);
  71 +
  72 + if (json == null) {
  73 + return parsed;
  74 + }
  75 +
  76 + String field(String key) {
  77 + final dynamic value = json[key];
  78 + if (value == null) return '';
  79 + return value.toString().trim();
  80 + }
  81 +
  82 + final String rawText =
  83 + field('rawText').isNotEmpty ? field('rawText') : content;
  84 + final ReservationImageParseResult regexParsed =
  85 + ReservationImageParser.parse(rawText);
  86 +
  87 + String firstNonEmpty(String primary, String fallback) {
  88 + return primary.isNotEmpty ? primary : fallback;
  89 + }
  90 +
  91 + return ReservationImageParseResult(
  92 + name: firstNonEmpty(field('name'), regexParsed.name),
  93 + sex: _normalizeSex(firstNonEmpty(field('sex'), regexParsed.sex)),
  94 + phone: firstNonEmpty(field('phone'), regexParsed.phone),
  95 + party: firstNonEmpty(field('party'), regexParsed.party),
  96 + date: firstNonEmpty(field('date'), regexParsed.date),
  97 + meal: _normalizeMeal(firstNonEmpty(field('meal'), regexParsed.meal)),
  98 + time: firstNonEmpty(field('time'), regexParsed.time),
  99 + table: firstNonEmpty(field('table'), regexParsed.table),
  100 + remark: firstNonEmpty(field('remark'), regexParsed.remark),
  101 + rawText: rawText,
  102 + );
  103 + }
  104 +
  105 + Future<String> _chat(Uint8List imageBytes) async {
  106 + final String base64 = base64Encode(imageBytes);
  107 + final String mime = _inferMimeType(imageBytes);
  108 +
  109 + final Response<Map<String, dynamic>> response;
  110 + try {
  111 + response = await _dio.post<Map<String, dynamic>>(
  112 + _endpoint,
  113 + data: <String, dynamic>{
  114 + 'model': model,
  115 + 'temperature': 0.1,
  116 + 'messages': <Map<String, dynamic>>[
  117 + <String, dynamic>{
  118 + 'role': 'system',
  119 + 'content': _systemPrompt(),
  120 + },
  121 + <String, dynamic>{
  122 + 'role': 'user',
  123 + 'content': <Map<String, dynamic>>[
  124 + <String, dynamic>{
  125 + 'type': 'image_url',
  126 + 'image_url': <String, String>{
  127 + 'url': 'data:$mime;base64,$base64',
  128 + },
  129 + },
  130 + const <String, String>{
  131 + 'type': 'text',
  132 + 'text': '请提取图片中的餐厅预订信息并返回 JSON。',
  133 + },
  134 + ],
  135 + },
  136 + ],
  137 + },
  138 + options: Options(
  139 + headers: <String, String>{
  140 + 'Authorization': 'Bearer $apiKey',
  141 + 'Content-Type': 'application/json',
  142 + },
  143 + ),
  144 + );
  145 + } on DioException catch (error) {
  146 + throw QwenOcrException(
  147 + error.response?.statusCode ?? 0,
  148 + _friendlyDioError(error),
  149 + );
  150 + }
  151 +
  152 + final Map<String, dynamic>? data = response.data;
  153 + if (data == null) {
  154 + throw const QwenOcrException(0, '图片识别返回数据异常');
  155 + }
  156 +
  157 + final dynamic error = data['error'];
  158 + if (error is Map) {
  159 + throw QwenOcrException(
  160 + error['code']?.toString() ?? '0',
  161 + error['message']?.toString() ?? '图片识别失败',
  162 + );
  163 + }
  164 +
  165 + final List<dynamic>? choices = data['choices'] as List<dynamic>?;
  166 + final dynamic message =
  167 + choices != null && choices.isNotEmpty
  168 + ? (choices.first as Map<String, dynamic>)['message']
  169 + : null;
  170 + if (message is! Map) {
  171 + throw const QwenOcrException(0, '图片识别未返回有效内容');
  172 + }
  173 +
  174 + final dynamic content = message['content'];
  175 + if (content is String) {
  176 + return content.trim();
  177 + }
  178 + if (content is List) {
  179 + final StringBuffer buffer = StringBuffer();
  180 + for (final dynamic item in content) {
  181 + if (item is Map) {
  182 + final dynamic text = item['text'];
  183 + if (text != null) {
  184 + buffer.write(text.toString());
  185 + }
  186 + }
  187 + }
  188 + return buffer.toString().trim();
  189 + }
  190 + return '';
  191 + }
  192 +
  193 + Map<String, dynamic>? _parseJsonObject(String content) {
  194 + final String trimmed = content.trim();
  195 + if (trimmed.isEmpty) return null;
  196 +
  197 + final int start = trimmed.indexOf('{');
  198 + final int end = trimmed.lastIndexOf('}');
  199 + if (start < 0 || end <= start) return null;
  200 +
  201 + try {
  202 + final dynamic decoded = jsonDecode(trimmed.substring(start, end + 1));
  203 + if (decoded is Map<String, dynamic>) {
  204 + return decoded;
  205 + }
  206 + } catch (_) {
  207 + return null;
  208 + }
  209 + return null;
  210 + }
  211 +
  212 + String _normalizeSex(String value) {
  213 + if (value == '男') return '先生';
  214 + if (value == '女' || value == '小姐') return '女士';
  215 + return value;
  216 + }
  217 +
  218 + String _normalizeMeal(String value) {
  219 + if (value.contains('早') || value.contains('上午') || value.contains('早上')) {
  220 + return '早餐';
  221 + }
  222 + if (value.contains('晚') ||
  223 + value.contains('夜') ||
  224 + value.contains('宵') ||
  225 + value.contains('下午')) {
  226 + return '晚餐';
  227 + }
  228 + if (value.contains('午') || value.contains('中')) {
  229 + return '午餐';
  230 + }
  231 + return value;
  232 + }
  233 +
  234 + String _friendlyDioError(DioException error) {
  235 + final int? statusCode = error.response?.statusCode;
  236 + if (statusCode == 401) return '千问 API Key 无效,请检查百炼密钥';
  237 + if (statusCode == 429) return '请求太频繁或额度不足,请稍后重试';
  238 + if (error.type == DioExceptionType.connectionTimeout ||
  239 + error.type == DioExceptionType.receiveTimeout) {
  240 + return '识别请求超时,请检查网络后重试';
  241 + }
  242 + return '图片识别网络请求失败,请检查网络';
  243 + }
  244 +
  245 + String _inferMimeType(Uint8List bytes) {
  246 + if (bytes.length >= 3 &&
  247 + bytes[0] == 0xFF &&
  248 + bytes[1] == 0xD8 &&
  249 + bytes[2] == 0xFF) {
  250 + return 'image/jpeg';
  251 + }
  252 + if (bytes.length >= 8 &&
  253 + bytes[0] == 0x89 &&
  254 + bytes[1] == 0x50 &&
  255 + bytes[2] == 0x4E &&
  256 + bytes[3] == 0x47) {
  257 + return 'image/png';
  258 + }
  259 + if (bytes.length >= 6 &&
  260 + bytes[0] == 0x47 &&
  261 + bytes[1] == 0x49 &&
  262 + bytes[2] == 0x46 &&
  263 + bytes[3] == 0x38) {
  264 + return 'image/gif';
  265 + }
  266 + if (bytes.length >= 2 && bytes[0] == 0x42 && bytes[1] == 0x4D) {
  267 + return 'image/bmp';
  268 + }
  269 + return 'image/jpeg';
  270 + }
  271 +}
  272 +
  273 +class QwenOcrException implements Exception {
  274 + const QwenOcrException(this.code, this.message);
  275 +
  276 + final Object code;
  277 + final String message;
  278 +
  279 + @override
  280 + String toString() => 'QwenOcrException($code, $message)';
  281 +}
  1 +import 'package:flutter/foundation.dart';
  2 +
  3 +/// 承接从其他 App(如微信)分享进来的预订截图。
  4 +///
  5 +/// 分享回调把图片文件路径写入 [pendingImage],经营助手页监听后
  6 +/// 自动走「识别预订信息 → 确认预订」流程。
  7 +class SharedReservationReceiver {
  8 + SharedReservationReceiver._();
  9 +
  10 + static final SharedReservationReceiver instance =
  11 + SharedReservationReceiver._();
  12 +
  13 + /// 待处理的分享图片路径;为 null 表示当前没有待识别图片。
  14 + final ValueNotifier<String?> pendingImage = ValueNotifier<String?>(null);
  15 +
  16 + /// 设置待识别图片,并通知监听者。
  17 + void acceptImage(String path) {
  18 + pendingImage.value = path;
  19 + }
  20 +
  21 + /// 标记该图片已被消费。
  22 + void consumeImage() {
  23 + if (pendingImage.value != null) {
  24 + pendingImage.value = null;
  25 + }
  26 + }
  27 +}
@@ -1590,6 +1590,14 @@ packages: @@ -1590,6 +1590,14 @@ packages:
1590 url: "https://pub.flutter-io.cn" 1590 url: "https://pub.flutter-io.cn"
1591 source: hosted 1591 source: hosted
1592 version: "2.2.3" 1592 version: "2.2.3"
  1593 + receive_sharing_intent:
  1594 + dependency: "direct main"
  1595 + description:
  1596 + name: receive_sharing_intent
  1597 + sha256: ec76056e4d258ad708e76d85591d933678625318e411564dcb9059048ca3a593
  1598 + url: "https://pub.flutter-io.cn"
  1599 + source: hosted
  1600 + version: "1.8.1"
1593 record: 1601 record:
1594 dependency: "direct main" 1602 dependency: "direct main"
1595 description: 1603 description:
@@ -74,6 +74,8 @@ dependencies: @@ -74,6 +74,8 @@ dependencies:
74 cached_network_image: ^3.4.1 74 cached_network_image: ^3.4.1
75 # websocket 75 # websocket
76 web_socket_channel: ^2.4.0 76 web_socket_channel: ^2.4.0
  77 +# 接收外部App分享(微信截图等)
  78 + receive_sharing_intent: ^1.8.1
77 # sqlite数据库 79 # sqlite数据库
78 sqflite: ^2.2.8+4 80 sqflite: ^2.2.8+4
79 # Bugly 81 # Bugly