From 3b028befdeaf8118dc55c7e13d5e1c9c5e626860 Mon Sep 17 00:00:00 2001
From: lxm <1274844347@qq.com>
Date: Thu, 9 Oct 2025 17:24:56 +0800
Subject: [PATCH 1/4] =?UTF-8?q?Revert=20"feat(ios):=20=E5=AE=9E=E7=8E=B0?=
=?UTF-8?q?=E5=9F=BA=E4=BA=8EReplayKit=E7=9A=84=E5=85=A8=E5=B1=80=E6=82=AC?=
=?UTF-8?q?=E6=B5=AE=E7=AA=97=E5=8A=9F=E8=83=BD"?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This reverts commit 26ffe461a0e1f59986c3a55897d25dc675e6ae0d.
---
.../BroadcastExtension.entitlements | 10 -
ios/BroadcastExtension/Info.plist | 38 --
ios/BroadcastExtension/SampleHandler.swift | 216 -----------
ios/ReplayKit_Implementation_Summary.md | 180 ---------
ios/ReplayKit_Integration_Guide.md | 142 --------
ios/Runner/AppDelegate.swift | 10 -
ios/Runner/FloatingWindow.swift | 341 ------------------
ios/Runner/FloatingWindowPlugin.swift | 154 --------
ios/Runner/OverlayPermissionPlugin.swift | 40 --
ios/Runner/ReplayKitManager.swift | 247 -------------
ios/Runner/Runner.entitlements | 4 -
lib/core/utils/overlay_permission_util.dart | 15 +-
test/ios_floating_window_test.dart | 149 --------
13 files changed, 3 insertions(+), 1543 deletions(-)
delete mode 100644 ios/BroadcastExtension/BroadcastExtension.entitlements
delete mode 100644 ios/BroadcastExtension/Info.plist
delete mode 100644 ios/BroadcastExtension/SampleHandler.swift
delete mode 100644 ios/ReplayKit_Implementation_Summary.md
delete mode 100644 ios/ReplayKit_Integration_Guide.md
delete mode 100644 ios/Runner/FloatingWindow.swift
delete mode 100644 ios/Runner/FloatingWindowPlugin.swift
delete mode 100644 ios/Runner/OverlayPermissionPlugin.swift
delete mode 100644 ios/Runner/ReplayKitManager.swift
delete mode 100644 test/ios_floating_window_test.dart
diff --git a/ios/BroadcastExtension/BroadcastExtension.entitlements b/ios/BroadcastExtension/BroadcastExtension.entitlements
deleted file mode 100644
index ea41065d0..000000000
--- a/ios/BroadcastExtension/BroadcastExtension.entitlements
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
- com.apple.security.application-groups
-
- group.com.saitong.voitrans.shared
-
-
-
diff --git a/ios/BroadcastExtension/Info.plist b/ios/BroadcastExtension/Info.plist
deleted file mode 100644
index 7c4ad275a..000000000
--- a/ios/BroadcastExtension/Info.plist
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
-
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- CFBundleDisplayName
- Voitrans 字幕悬浮窗
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- $(PRODUCT_NAME)
- CFBundlePackageType
- $(PRODUCT_BUNDLE_PACKAGE_TYPE)
- CFBundleShortVersionString
- $(FLUTTER_BUILD_NAME)
- CFBundleVersion
- $(FLUTTER_BUILD_NUMBER)
- NSExtension
-
- NSExtensionPointIdentifier
- com.apple.broadcast-services-upload
- NSExtensionPrincipalClass
- $(PRODUCT_MODULE_NAME).SampleHandler
- RPBroadcastProcessMode
- RPBroadcastProcessModeSampleBuffer
-
- NSAppTransportSecurity
-
- NSAllowsArbitraryLoads
-
-
-
-
diff --git a/ios/BroadcastExtension/SampleHandler.swift b/ios/BroadcastExtension/SampleHandler.swift
deleted file mode 100644
index d2748b12f..000000000
--- a/ios/BroadcastExtension/SampleHandler.swift
+++ /dev/null
@@ -1,216 +0,0 @@
-import ReplayKit
-import UIKit
-import AVFoundation
-
-/// Broadcast Upload Extension 的核心处理类
-/// 负责接收屏幕内容并叠加字幕悬浮窗
-class SampleHandler: RPBroadcastSampleHandler {
-
- // MARK: - 属性
- private var overlayWindow: UIWindow?
- private var subtitleLabel: UILabel?
- private var containerView: UIView?
- private let appGroupIdentifier = "group.com.saitong.voitrans.shared"
-
- // MARK: - 生命周期方法
-
- override func broadcastStarted(withSetupInfo setupInfo: [String : NSObject]?) {
- print("🎬 ReplayKit 广播开始")
- setupOverlayWindow()
- startMonitoringSubtitleUpdates()
- }
-
- override func broadcastPaused() {
- print("⏸️ ReplayKit 广播暂停")
- hideOverlay()
- }
-
- override func broadcastResumed() {
- print("▶️ ReplayKit 广播恢复")
- showOverlay()
- }
-
- override func broadcastFinished() {
- print("🛑 ReplayKit 广播结束")
- cleanupOverlay()
- }
-
- // MARK: - 样本处理方法
-
- override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) {
- switch sampleBufferType {
- case RPSampleBufferType.video:
- // 处理视频帧 - 这里我们主要用于触发悬浮窗显示
- break
- case RPSampleBufferType.audioApp:
- // 处理应用音频
- break
- case RPSampleBufferType.audioMic:
- // 处理麦克风音频
- break
- @unknown default:
- break
- }
- }
-
- // MARK: - 悬浮窗设置
-
- /// 设置悬浮窗口
- private func setupOverlayWindow() {
- DispatchQueue.main.async { [weak self] in
- guard let self = self else { return }
-
- // 创建悬浮窗口
- self.overlayWindow = UIWindow(frame: UIScreen.main.bounds)
- self.overlayWindow?.windowLevel = UIWindow.Level.alert + 1000
- self.overlayWindow?.backgroundColor = UIColor.clear
- self.overlayWindow?.isHidden = false
-
- // 创建容器视图
- self.containerView = UIView()
- self.containerView?.backgroundColor = UIColor.black.withAlphaComponent(0.8)
- self.containerView?.layer.cornerRadius = 12
- self.containerView?.layer.masksToBounds = true
- self.containerView?.translatesAutoresizingMaskIntoConstraints = false
-
- // 创建字幕标签
- self.subtitleLabel = UILabel()
- self.subtitleLabel?.textColor = .white
- self.subtitleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium)
- self.subtitleLabel?.numberOfLines = 0
- self.subtitleLabel?.textAlignment = .center
- self.subtitleLabel?.translatesAutoresizingMaskIntoConstraints = false
- self.subtitleLabel?.text = "等待翻译内容..."
-
- // 添加视图层次
- guard let containerView = self.containerView,
- let subtitleLabel = self.subtitleLabel,
- let overlayWindow = self.overlayWindow else { return }
-
- containerView.addSubview(subtitleLabel)
- overlayWindow.addSubview(containerView)
-
- // 设置约束
- NSLayoutConstraint.activate([
- // 容器视图约束 - 位于屏幕顶部
- containerView.topAnchor.constraint(equalTo: overlayWindow.safeAreaLayoutGuide.topAnchor, constant: 20),
- containerView.leadingAnchor.constraint(greaterThanOrEqualTo: overlayWindow.leadingAnchor, constant: 20),
- containerView.trailingAnchor.constraint(lessThanOrEqualTo: overlayWindow.trailingAnchor, constant: -20),
- containerView.centerXAnchor.constraint(equalTo: overlayWindow.centerXAnchor),
- containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: 60),
-
- // 字幕标签约束
- subtitleLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 12),
- subtitleLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16),
- subtitleLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16),
- subtitleLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -12)
- ])
-
- print("✅ 悬浮窗口设置完成")
- }
- }
-
- /// 显示悬浮窗
- private func showOverlay() {
- DispatchQueue.main.async { [weak self] in
- self?.overlayWindow?.isHidden = false
- self?.overlayWindow?.alpha = 1.0
- }
- }
-
- /// 隐藏悬浮窗
- private func hideOverlay() {
- DispatchQueue.main.async { [weak self] in
- UIView.animate(withDuration: 0.3) {
- self?.overlayWindow?.alpha = 0.0
- } completion: { _ in
- self?.overlayWindow?.isHidden = true
- }
- }
- }
-
- /// 清理悬浮窗
- private func cleanupOverlay() {
- DispatchQueue.main.async { [weak self] in
- self?.overlayWindow?.isHidden = true
- self?.overlayWindow = nil
- self?.subtitleLabel = nil
- self?.containerView = nil
- }
- }
-
- // MARK: - 字幕更新监听
-
- /// 开始监听字幕更新
- private func startMonitoringSubtitleUpdates() {
- // 使用 UserDefaults 监听字幕更新
- if let userDefaults = UserDefaults(suiteName: appGroupIdentifier) {
- // 设置初始值监听
- userDefaults.addObserver(self, forKeyPath: "subtitle_text", options: [.new], context: nil)
- userDefaults.addObserver(self, forKeyPath: "subtitle_source_language", options: [.new], context: nil)
- userDefaults.addObserver(self, forKeyPath: "subtitle_target_language", options: [.new], context: nil)
-
- print("✅ 开始监听字幕更新")
- }
- }
-
- /// KVO 监听回调
- override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
- if keyPath == "subtitle_text" || keyPath == "subtitle_source_language" || keyPath == "subtitle_target_language" {
- updateSubtitleDisplay()
- }
- }
-
- /// 更新字幕显示
- private func updateSubtitleDisplay() {
- guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else { return }
-
- let subtitleText = userDefaults.string(forKey: "subtitle_text") ?? ""
- let sourceLanguage = userDefaults.string(forKey: "subtitle_source_language") ?? ""
- let targetLanguage = userDefaults.string(forKey: "subtitle_target_language") ?? ""
- let isIntermediate = userDefaults.bool(forKey: "subtitle_is_intermediate")
-
- DispatchQueue.main.async { [weak self] in
- guard let self = self, let subtitleLabel = self.subtitleLabel else { return }
-
- if subtitleText.isEmpty {
- subtitleLabel.text = "等待翻译内容..."
- subtitleLabel.alpha = 0.6
- } else {
- // 构建显示文本
- var displayText = subtitleText
- if !sourceLanguage.isEmpty && !targetLanguage.isEmpty {
- displayText = "\(sourceLanguage) → \(targetLanguage)\n\(subtitleText)"
- }
-
- subtitleLabel.text = displayText
- subtitleLabel.alpha = isIntermediate ? 0.8 : 1.0
-
- // 添加淡入动画
- UIView.animate(withDuration: 0.2) {
- self.containerView?.transform = CGAffineTransform(scaleX: 1.05, y: 1.05)
- } completion: { _ in
- UIView.animate(withDuration: 0.2) {
- self.containerView?.transform = .identity
- }
- }
- }
-
- print("📝 字幕更新: \(subtitleText)")
- }
- }
-
- // MARK: - 清理
-
- deinit {
- // 移除 KVO 监听
- if let userDefaults = UserDefaults(suiteName: appGroupIdentifier) {
- userDefaults.removeObserver(self, forKeyPath: "subtitle_text")
- userDefaults.removeObserver(self, forKeyPath: "subtitle_source_language")
- userDefaults.removeObserver(self, forKeyPath: "subtitle_target_language")
- }
-
- cleanupOverlay()
- print("🗑️ SampleHandler 已清理")
- }
-}
diff --git a/ios/ReplayKit_Implementation_Summary.md b/ios/ReplayKit_Implementation_Summary.md
deleted file mode 100644
index f596e0fdd..000000000
--- a/ios/ReplayKit_Implementation_Summary.md
+++ /dev/null
@@ -1,180 +0,0 @@
-# iOS ReplayKit 悬浮窗实现总结
-
-## 🎯 实现目标
-
-✅ **已完成**:使用 ReplayKit + Broadcast Upload Extension 实现 iOS 全局字幕悬浮窗功能,与 Android 版本保持功能一致。
-
-## 📁 文件结构
-
-### 主应用文件
-```
-ios/Runner/
-├── FloatingWindowPlugin.swift # Flutter 平台通道插件
-├── ReplayKitManager.swift # ReplayKit 管理器
-├── OverlayPermissionPlugin.swift # 权限管理插件(兼容性)
-├── AppDelegate.swift # 应用委托(已更新)
-└── Runner.entitlements # 主应用权限配置
-```
-
-### Extension 文件
-```
-ios/BroadcastExtension/
-├── SampleHandler.swift # 广播处理器(核心实现)
-├── Info.plist # Extension 配置
-└── BroadcastExtension.entitlements # Extension 权限配置
-```
-
-### 配置和文档
-```
-ios/
-├── ReplayKit_Integration_Guide.md # Xcode 集成指南
-└── ReplayKit_Implementation_Summary.md # 实现总结(本文件)
-```
-
-## 🔧 核心技术实现
-
-### 1. ReplayKit 广播控制 (`ReplayKitManager.swift`)
-- **功能**:管理屏幕广播的启动、停止和状态监控
-- **关键方法**:
- - `startBroadcast()`: 启动 ReplayKit 广播
- - `stopBroadcast()`: 停止广播
- - `updateSubtitle()`: 更新字幕内容
-- **数据通信**:使用 App Groups 与 Extension 共享数据
-
-### 2. 广播处理器 (`SampleHandler.swift`)
-- **功能**:接收屏幕内容并显示悬浮窗字幕
-- **关键特性**:
- - 创建全局悬浮窗口 (`UIWindow`)
- - 实时监听字幕更新 (KVO + UserDefaults)
- - 自适应字幕显示和动画效果
-- **显示位置**:屏幕顶部中央,半透明黑色背景
-
-### 3. Flutter 接口 (`FloatingWindowPlugin.swift`)
-- **功能**:提供与 Android 一致的 Flutter 调用接口
-- **方法映射**:
- - `enableFloatingWindow` → 启动 ReplayKit 广播
- - `disableFloatingWindow` → 停止广播
- - `updateContent` → 更新字幕内容
- - `setPosition` → 兼容性方法(位置固定)
-
-## 📱 用户体验流程
-
-### 启动流程
-1. 用户在翻译界面点击"悬浮窗"按钮
-2. 系统弹出"开始直播屏幕"选择界面
-3. 用户选择 "Voitrans 字幕悬浮窗" 选项
-4. 系统开始屏幕录制,状态栏显示红色录制指示器
-5. 悬浮窗出现在屏幕顶部,显示"等待翻译内容..."
-
-### 使用流程
-1. 应用进行语音识别和翻译
-2. 实时更新悬浮窗中的字幕内容
-3. 支持源语言和目标语言显示
-4. 中间结果和最终结果有不同的透明度显示
-5. 用户可以在任何应用中看到翻译字幕
-
-### 停止流程
-1. 用户再次点击"悬浮窗"按钮或停止翻译
-2. 应用调用停止广播方法
-3. 悬浮窗消失,录制指示器消失
-
-## 🔐 权限和配置
-
-### App Groups 配置
-- **标识符**:`group.com.saitong.voitrans.shared`
-- **用途**:主应用与 Extension 之间的数据共享
-- **数据键**:
- - `subtitle_text`: 字幕文本
- - `subtitle_source_language`: 源语言
- - `subtitle_target_language`: 目标语言
- - `subtitle_is_intermediate`: 是否为中间结果
-
-### 系统权限
-- **屏幕录制权限**:用户首次使用时系统自动请求
-- **麦克风权限**:如果需要录制音频(可选)
-- **摄像头权限**:ReplayKit 框架要求(实际不使用)
-
-## 🎨 UI 设计特点
-
-### 悬浮窗样式
-- **位置**:屏幕顶部中央,距离安全区域 20pt
-- **背景**:黑色半透明 (alpha: 0.8)
-- **圆角**:12pt 圆角矩形
-- **字体**:系统字体 16pt,白色文字
-- **布局**:垂直布局,支持多行文本
-
-### 动画效果
-- **出现动画**:淡入效果 (0.3s)
-- **更新动画**:轻微缩放效果 (1.05x → 1.0x)
-- **状态指示**:中间结果透明度 0.8,最终结果透明度 1.0
-
-## 🔄 与 Android 版本对比
-
-### 功能一致性
-| 功能 | Android | iOS (ReplayKit) | 状态 |
-|------|---------|-----------------|------|
-| 全局悬浮窗 | ✅ 系统悬浮窗 | ✅ ReplayKit 悬浮窗 | 一致 |
-| 实时字幕更新 | ✅ | ✅ | 一致 |
-| 拖拽移动 | ✅ | ❌ 位置固定 | 差异 |
-| 最小化/展开 | ✅ | ❌ 固定样式 | 差异 |
-| 权限管理 | ✅ 悬浮窗权限 | ✅ 录制权限 | 一致 |
-
-### Flutter 接口一致性
-- ✅ 所有方法签名保持一致
-- ✅ 回调事件保持一致
-- ✅ 数据模型保持一致
-- ✅ 错误处理保持一致
-
-## 🚀 部署和发布
-
-### 开发环境要求
-- Xcode 14.0+
-- iOS 16.0+ (ReplayKit 2 要求)
-- 有效的 Apple Developer 账号
-- App Groups 权限配置
-
-### App Store 审核要点
-- ✅ 使用官方 ReplayKit API,符合审核规范
-- ✅ 明确的用户权限请求和说明
-- ✅ 合理的功能用途(翻译字幕显示)
-- ✅ 不涉及隐私数据收集
-
-### 发布注意事项
-1. 确保 Extension 的 Bundle ID 正确配置
-2. App Groups 在 Developer Portal 中正确设置
-3. 两个 target 使用相同的签名证书
-4. 测试在不同 iOS 版本上的兼容性
-
-## 📊 技术优势
-
-### 相比应用内悬浮窗
-- ✅ 真正的全局显示能力
-- ✅ 符合 iOS 平台规范
-- ✅ 通过 App Store 审核
-- ✅ 系统级权限管理
-
-### 相比其他方案
-- ✅ 无需越狱或私有 API
-- ✅ 稳定可靠的系统支持
-- ✅ 良好的用户体验
-- ✅ 与现有架构无缝集成
-
-## 🔮 未来扩展
-
-### 可能的改进
-1. **自定义样式**:支持更多字幕样式选项
-2. **位置调整**:探索在录制流中实现位置调整
-3. **多语言支持**:优化多语言字幕显示
-4. **性能优化**:减少内存占用和电池消耗
-
-### 技术演进
-- 关注 iOS 新版本的 ReplayKit 功能更新
-- 考虑集成 Live Activities (iOS 16+) 作为补充方案
-- 探索 WidgetKit 在锁屏界面的字幕显示
-
----
-
-**实现状态**: ✅ 完成
-**测试状态**: ✅ 基础功能测试通过
-**文档状态**: ✅ 完整的集成和使用文档
-**发布准备**: ⏳ 需要 Xcode 项目配置
diff --git a/ios/ReplayKit_Integration_Guide.md b/ios/ReplayKit_Integration_Guide.md
deleted file mode 100644
index 505d72590..000000000
--- a/ios/ReplayKit_Integration_Guide.md
+++ /dev/null
@@ -1,142 +0,0 @@
-# iOS ReplayKit 悬浮窗集成指南
-
-## 📋 概述
-
-本指南说明如何在 Xcode 中配置 ReplayKit Broadcast Upload Extension 以实现全局字幕悬浮窗功能。
-
-## 🔧 Xcode 项目配置步骤
-
-### 1. 添加 Broadcast Upload Extension Target
-
-1. 在 Xcode 中打开 `ios/Runner.xcworkspace`
-2. 选择项目根节点 → 点击 "+" 添加新 Target
-3. 选择 "Broadcast Upload Extension"
-4. 配置信息:
- - Product Name: `BroadcastExtension`
- - Bundle Identifier: `com.saitong.voitrans.BroadcastExtension`
- - Language: Swift
- - 确保 "Include UI Extension" 未选中
-
-### 2. 配置 Extension Target
-
-#### 2.1 替换默认文件
-- 删除自动生成的 `SampleHandler.swift`
-- 将项目中的 `ios/BroadcastExtension/SampleHandler.swift` 复制到 Extension target
-- 将项目中的 `ios/BroadcastExtension/Info.plist` 替换 Extension 的 Info.plist
-
-#### 2.2 添加 Entitlements
-- 在 Extension target 的 Build Settings 中设置 Code Signing Entitlements
-- 指向 `ios/BroadcastExtension/BroadcastExtension.entitlements`
-
-#### 2.3 配置 Build Settings
-- iOS Deployment Target: 16.0 (与主应用保持一致)
-- Swift Language Version: Swift 5
-- Enable Bitcode: No
-
-### 3. 配置 App Groups
-
-#### 3.1 在 Apple Developer Portal 中:
-1. 创建 App Group: `group.com.saitong.voitrans.shared`
-2. 将主应用和 Extension 都添加到此 App Group
-
-#### 3.2 在 Xcode 中:
-1. 主应用 Target → Signing & Capabilities → 添加 "App Groups" capability
-2. Extension Target → Signing & Capabilities → 添加 "App Groups" capability
-3. 两个 target 都勾选 `group.com.saitong.voitrans.shared`
-
-### 4. 更新主应用配置
-
-#### 4.1 添加 ReplayKit 框架
-- 主应用 Target → Build Phases → Link Binary With Libraries
-- 添加 `ReplayKit.framework`
-
-#### 4.2 更新 Info.plist
-在主应用的 Info.plist 中添加:
-```xml
-NSCameraUsageDescription
-需要摄像头权限以支持屏幕录制功能
-NSMicrophoneUsageDescription
-需要麦克风权限以支持音频录制功能
-```
-
-## 📱 使用说明
-
-### Flutter 调用方式
-
-```dart
-// 启动悬浮窗(会弹出系统录屏选择界面)
-await floatingWindowManager.enableFloatingWindow();
-
-// 更新字幕内容
-await floatingWindowManager.updateFloatingWindowContent(
- FloatingWindowData(
- sourceText: '你好',
- translatedText: 'Hello',
- sourceLanguage: '中文',
- targetLanguage: '英语',
- timestamp: DateTime.now(),
- isIntermediate: false,
- ),
-);
-
-// 停止悬浮窗
-await floatingWindowManager.disableFloatingWindow();
-```
-
-### 用户操作流程
-
-1. 用户点击"启用悬浮窗"按钮
-2. 系统弹出"开始直播屏幕"选择界面
-3. 用户选择 "Voitrans 字幕悬浮窗" 选项
-4. 系统开始屏幕录制,悬浮窗显示在屏幕顶部
-5. 应用实时更新字幕内容
-6. 用户可以在任何应用中看到翻译字幕
-
-## ⚠️ 注意事项
-
-### 开发阶段
-- 确保两个 target 使用相同的开发者账号签名
-- App Groups 必须在 Apple Developer Portal 中正确配置
-- Extension 的 Bundle ID 必须是主应用的子域名
-
-### 发布阶段
-- Extension 会随主应用一起打包发布
-- 用户首次使用时需要授权屏幕录制权限
-- 符合 App Store 审核规范,因为使用的是官方 ReplayKit API
-
-### 用户体验
-- 录制期间状态栏会显示红色录制指示器
-- 悬浮窗位置固定在屏幕顶部中央
-- 支持实时字幕更新和语言切换
-- 可以通过停止录制来关闭悬浮窗
-
-## 🔍 故障排除
-
-### 常见问题
-
-1. **Extension 无法启动**
- - 检查 Bundle ID 配置
- - 确认 App Groups 权限配置正确
- - 验证代码签名设置
-
-2. **字幕不更新**
- - 检查 App Groups 数据同步
- - 确认 UserDefaults 键名一致
- - 查看 Extension 日志输出
-
-3. **编译错误**
- - 确保 iOS Deployment Target 一致
- - 检查 Swift 版本设置
- - 验证框架依赖配置
-
-### 调试方法
-
-1. 使用 Xcode 的 Device Console 查看 Extension 日志
-2. 在 SampleHandler 中添加 print 语句进行调试
-3. 检查 App Groups 中的数据是否正确写入
-
-## 📚 相关文档
-
-- [Apple ReplayKit Documentation](https://developer.apple.com/documentation/replaykit)
-- [Broadcast Upload Extension Guide](https://developer.apple.com/documentation/replaykit/rpbroadcastsamplehandler)
-- [App Groups Documentation](https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_application-groups)
diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift
index 7eff33866..e04182a8a 100644
--- a/ios/Runner/AppDelegate.swift
+++ b/ios/Runner/AppDelegate.swift
@@ -8,16 +8,6 @@ import UIKit
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
-
- // 注册悬浮窗相关插件
- if let registrar = self.registrar(forPlugin: "FloatingWindowPlugin") {
- FloatingWindowPlugin.register(with: registrar)
- }
-
- if let registrar = self.registrar(forPlugin: "OverlayPermissionPlugin") {
- OverlayPermissionPlugin.register(with: registrar)
- }
-
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
diff --git a/ios/Runner/FloatingWindow.swift b/ios/Runner/FloatingWindow.swift
deleted file mode 100644
index a60108a24..000000000
--- a/ios/Runner/FloatingWindow.swift
+++ /dev/null
@@ -1,341 +0,0 @@
-import UIKit
-
-/// 悬浮窗实现类
-/// 使用 UIWindow 创建应用内悬浮窗口
-class FloatingWindow: UIWindow {
-
- // MARK: - 属性
- private weak var delegate: FloatingWindowDelegate?
- private var floatingViewController: FloatingViewController?
- private var isMinimized = true
- private var lastPosition = CGPoint(x: 100, y: 100)
-
- // MARK: - 初始化
- init(delegate: FloatingWindowDelegate) {
- self.delegate = delegate
-
- // 创建窗口
- super.init(frame: CGRect(x: 100, y: 100, width: 300, height: 120))
-
- setupWindow()
- setupViewController()
- }
-
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- // MARK: - 私有方法
-
- /// 设置窗口属性
- private func setupWindow() {
- // 设置窗口层级,确保在最顶层显示
- windowLevel = UIWindow.Level.alert + 1
- backgroundColor = UIColor.clear
-
- // 设置窗口为可见但不成为主窗口
- isHidden = true
-
- // 设置圆角
- layer.cornerRadius = 12
- layer.masksToBounds = true
- }
-
- /// 设置视图控制器
- private func setupViewController() {
- floatingViewController = FloatingViewController()
- floatingViewController?.delegate = self
- rootViewController = floatingViewController
- }
-
- // MARK: - 公共方法
-
- /// 显示悬浮窗
- func show() {
- isHidden = false
- makeKeyAndVisible()
-
- // 添加显示动画
- alpha = 0
- UIView.animate(withDuration: 0.3) {
- self.alpha = 1
- }
-
- delegate?.floatingWindowDidChangeState(isMinimized ? "minimized" : "expanded")
- }
-
- /// 隐藏悬浮窗
- func hide() {
- UIView.animate(withDuration: 0.3, animations: {
- self.alpha = 0
- }) { _ in
- self.isHidden = true
- self.resignKey()
- }
-
- delegate?.floatingWindowDidChangeState("hidden")
- }
-
- /// 更新内容
- func updateContent(_ data: FloatingWindowContentData) {
- floatingViewController?.updateContent(data)
- }
-
- /// 设置位置
- func setPosition(x: Double, y: Double) {
- let newPosition = CGPoint(x: x, y: y)
- lastPosition = newPosition
-
- UIView.animate(withDuration: 0.3) {
- self.frame.origin = newPosition
- }
- }
-
- /// 切换最小化/展开状态
- func toggleMinimized() {
- isMinimized.toggle()
-
- let newSize: CGSize
- if isMinimized {
- newSize = CGSize(width: 120, height: 60)
- } else {
- newSize = CGSize(width: 300, height: 120)
- }
-
- UIView.animate(withDuration: 0.3) {
- self.frame.size = newSize
- }
-
- floatingViewController?.setMinimized(isMinimized)
- delegate?.floatingWindowDidChangeState(isMinimized ? "minimized" : "expanded")
- }
-}
-
-// MARK: - FloatingViewControllerDelegate
-extension FloatingWindow: FloatingViewControllerDelegate {
-
- func floatingViewControllerDidTap() {
- toggleMinimized()
- delegate?.floatingWindowDidClick()
- }
-
- func floatingViewControllerDidPan(translation: CGPoint) {
- let newOrigin = CGPoint(
- x: lastPosition.x + translation.x,
- y: lastPosition.y + translation.y
- )
-
- // 限制在屏幕范围内
- let screenBounds = UIScreen.main.bounds
- let constrainedX = max(0, min(newOrigin.x, screenBounds.width - frame.width))
- let constrainedY = max(0, min(newOrigin.y, screenBounds.height - frame.height))
-
- frame.origin = CGPoint(x: constrainedX, y: constrainedY)
- }
-
- func floatingViewControllerDidEndPan() {
- lastPosition = frame.origin
- }
-
- func floatingViewControllerDidRequestClose() {
- hide()
- delegate?.floatingWindowDidClose()
- }
-}
-
-/// 悬浮窗视图控制器
-private class FloatingViewController: UIViewController {
-
- // MARK: - 属性
- weak var delegate: FloatingViewControllerDelegate?
-
- // UI 组件
- private let containerView = UIView()
- private let sourceTextLabel = UILabel()
- private let translatedTextLabel = UILabel()
- private let languageInfoLabel = UILabel()
- private let timestampLabel = UILabel()
- private let closeButton = UIButton(type: .system)
-
- // 手势识别器
- private var panGestureRecognizer: UIPanGestureRecognizer!
- private var tapGestureRecognizer: UITapGestureRecognizer!
-
- private var isMinimized = true
-
- // MARK: - 生命周期
- override func viewDidLoad() {
- super.viewDidLoad()
- setupUI()
- setupGestures()
- }
-
- // MARK: - 私有方法
-
- /// 设置 UI
- private func setupUI() {
- view.backgroundColor = UIColor.clear
-
- // 容器视图
- containerView.backgroundColor = UIColor.black.withAlphaComponent(0.8)
- containerView.layer.cornerRadius = 12
- containerView.translatesAutoresizingMaskIntoConstraints = false
- view.addSubview(containerView)
-
- // 源文本标签
- sourceTextLabel.textColor = .white
- sourceTextLabel.font = UIFont.systemFont(ofSize: 14)
- sourceTextLabel.numberOfLines = 2
- sourceTextLabel.translatesAutoresizingMaskIntoConstraints = false
- containerView.addSubview(sourceTextLabel)
-
- // 翻译文本标签
- translatedTextLabel.textColor = .systemBlue
- translatedTextLabel.font = UIFont.systemFont(ofSize: 14, weight: .medium)
- translatedTextLabel.numberOfLines = 2
- translatedTextLabel.translatesAutoresizingMaskIntoConstraints = false
- containerView.addSubview(translatedTextLabel)
-
- // 语言信息标签
- languageInfoLabel.textColor = .lightGray
- languageInfoLabel.font = UIFont.systemFont(ofSize: 10)
- languageInfoLabel.translatesAutoresizingMaskIntoConstraints = false
- containerView.addSubview(languageInfoLabel)
-
- // 时间戳标签
- timestampLabel.textColor = .lightGray
- timestampLabel.font = UIFont.systemFont(ofSize: 10)
- timestampLabel.textAlignment = .right
- timestampLabel.translatesAutoresizingMaskIntoConstraints = false
- containerView.addSubview(timestampLabel)
-
- // 关闭按钮
- closeButton.setTitle("×", for: .normal)
- closeButton.setTitleColor(.white, for: .normal)
- closeButton.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .bold)
- closeButton.translatesAutoresizingMaskIntoConstraints = false
- closeButton.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside)
- containerView.addSubview(closeButton)
-
- setupConstraints()
- }
-
- /// 设置约束
- private func setupConstraints() {
- NSLayoutConstraint.activate([
- // 容器视图
- containerView.topAnchor.constraint(equalTo: view.topAnchor),
- containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
- containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
-
- // 关闭按钮
- closeButton.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 4),
- closeButton.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -4),
- closeButton.widthAnchor.constraint(equalToConstant: 20),
- closeButton.heightAnchor.constraint(equalToConstant: 20),
-
- // 源文本标签
- sourceTextLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 8),
- sourceTextLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 8),
- sourceTextLabel.trailingAnchor.constraint(equalTo: closeButton.leadingAnchor, constant: -4),
-
- // 翻译文本标签
- translatedTextLabel.topAnchor.constraint(equalTo: sourceTextLabel.bottomAnchor, constant: 4),
- translatedTextLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 8),
- translatedTextLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -8),
-
- // 语言信息标签
- languageInfoLabel.topAnchor.constraint(equalTo: translatedTextLabel.bottomAnchor, constant: 4),
- languageInfoLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 8),
- languageInfoLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -4),
-
- // 时间戳标签
- timestampLabel.topAnchor.constraint(equalTo: translatedTextLabel.bottomAnchor, constant: 4),
- timestampLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -8),
- timestampLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -4)
- ])
- }
-
- /// 设置手势识别器
- private func setupGestures() {
- // 拖拽手势
- panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
- view.addGestureRecognizer(panGestureRecognizer)
-
- // 点击手势
- tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
- view.addGestureRecognizer(tapGestureRecognizer)
- }
-
- // MARK: - 手势处理
-
- @objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
- let translation = gesture.translation(in: view.superview)
-
- switch gesture.state {
- case .changed:
- delegate?.floatingViewControllerDidPan(translation: translation)
- case .ended, .cancelled:
- delegate?.floatingViewControllerDidEndPan()
- default:
- break
- }
- }
-
- @objc private func handleTap(_ gesture: UITapGestureRecognizer) {
- delegate?.floatingViewControllerDidTap()
- }
-
- @objc private func closeButtonTapped() {
- delegate?.floatingViewControllerDidRequestClose()
- }
-
- // MARK: - 公共方法
-
- /// 更新内容
- func updateContent(_ data: FloatingWindowContentData) {
- sourceTextLabel.text = data.sourceText.isEmpty ? "正在识别..." : data.sourceText
- translatedTextLabel.text = data.translatedText.isEmpty ? "翻译中..." : data.translatedText
- languageInfoLabel.text = "\(data.sourceLanguage) → \(data.targetLanguage)"
-
- // 格式化时间戳
- if data.timestamp > 0 {
- let date = Date(timeIntervalSince1970: TimeInterval(data.timestamp / 1000))
- let formatter = DateFormatter()
- formatter.dateFormat = "HH:mm:ss"
- timestampLabel.text = formatter.string(from: date)
- }
-
- // 中间结果的视觉提示
- translatedTextLabel.alpha = data.isIntermediate ? 0.7 : 1.0
- }
-
- /// 设置最小化状态
- func setMinimized(_ minimized: Bool) {
- isMinimized = minimized
-
- // 在最小化状态下隐藏部分元素
- sourceTextLabel.isHidden = minimized
- languageInfoLabel.isHidden = minimized
- timestampLabel.isHidden = minimized
- closeButton.isHidden = minimized
-
- // 调整翻译文本标签的约束
- if minimized {
- translatedTextLabel.numberOfLines = 1
- translatedTextLabel.font = UIFont.systemFont(ofSize: 12, weight: .medium)
- } else {
- translatedTextLabel.numberOfLines = 2
- translatedTextLabel.font = UIFont.systemFont(ofSize: 14, weight: .medium)
- }
- }
-}
-
-/// 悬浮窗视图控制器代理协议
-private protocol FloatingViewControllerDelegate: AnyObject {
- func floatingViewControllerDidTap()
- func floatingViewControllerDidPan(translation: CGPoint)
- func floatingViewControllerDidEndPan()
- func floatingViewControllerDidRequestClose()
-}
diff --git a/ios/Runner/FloatingWindowPlugin.swift b/ios/Runner/FloatingWindowPlugin.swift
deleted file mode 100644
index 76d71222d..000000000
--- a/ios/Runner/FloatingWindowPlugin.swift
+++ /dev/null
@@ -1,154 +0,0 @@
-import Flutter
-import UIKit
-
-/// iOS 悬浮窗插件 - ReplayKit 方案
-/// 使用 ReplayKit + Broadcast Upload Extension 实现全局字幕悬浮窗
-public class FloatingWindowPlugin: NSObject, FlutterPlugin {
-
- // MARK: - 常量
- private static let channelName = "floating_window"
-
- // MARK: - 属性
- private var methodChannel: FlutterMethodChannel?
- private let replayKitManager = ReplayKitManager.shared
-
- // MARK: - 插件注册
- public static func register(with registrar: FlutterPluginRegistrar) {
- let instance = FloatingWindowPlugin()
-
- // 创建方法通道
- let methodChannel = FlutterMethodChannel(
- name: channelName,
- binaryMessenger: registrar.messenger()
- )
- instance.methodChannel = methodChannel
- registrar.addMethodCallDelegate(instance, channel: methodChannel)
-
- // 设置 ReplayKit 回调
- instance.setupReplayKitCallbacks()
-
- print("FloatingWindowPlugin (ReplayKit) 注册成功")
- }
-
- // MARK: - 方法调用处理
- public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
- switch call.method {
- case "enableFloatingWindow":
- enableFloatingWindow(result: result)
-
- case "disableFloatingWindow":
- disableFloatingWindow(result: result)
-
- case "showFloatingWindow":
- showFloatingWindow(result: result)
-
- case "hideFloatingWindow":
- hideFloatingWindow(result: result)
-
- case "updateContent":
- if let arguments = call.arguments as? [String: Any] {
- updateContent(arguments: arguments, result: result)
- } else {
- result(FlutterError(code: "INVALID_ARGUMENTS", message: "无效的参数", details: nil))
- }
-
- case "setPosition":
- // ReplayKit 方案中位置固定,但保持接口兼容性
- result(true)
-
- default:
- result(FlutterMethodNotImplemented)
- }
- }
-
- // MARK: - ReplayKit 回调设置
-
- /// 设置 ReplayKit 回调
- private func setupReplayKitCallbacks() {
- replayKitManager.onBroadcastStarted = { [weak self] in
- self?.methodChannel?.invokeMethod("onFloatingWindowStateChanged", arguments: ["state": "minimized"])
- }
-
- replayKitManager.onBroadcastFinished = { [weak self] in
- self?.methodChannel?.invokeMethod("onFloatingWindowClosed", arguments: nil)
- }
-
- replayKitManager.onBroadcastError = { [weak self] error in
- self?.methodChannel?.invokeMethod("onFloatingWindowError", arguments: ["error": error.localizedDescription])
- }
- }
-
- // MARK: - 悬浮窗控制方法
-
- /// 启用悬浮窗功能(启动 ReplayKit 广播)
- private func enableFloatingWindow(result: @escaping FlutterResult) {
- replayKitManager.startBroadcast { success, error in
- if success {
- result(true)
- } else {
- result(FlutterError(
- code: "ENABLE_ERROR",
- message: "启用悬浮窗失败: \(error?.localizedDescription ?? "未知错误")",
- details: nil
- ))
- }
- }
- }
-
- /// 禁用悬浮窗功能(停止 ReplayKit 广播)
- private func disableFloatingWindow(result: @escaping FlutterResult) {
- replayKitManager.stopBroadcast { success, error in
- if success {
- result(true)
- } else {
- result(FlutterError(
- code: "DISABLE_ERROR",
- message: "禁用悬浮窗失败: \(error?.localizedDescription ?? "未知错误")",
- details: nil
- ))
- }
- }
- }
-
- /// 显示悬浮窗(ReplayKit 方案中自动显示)
- private func showFloatingWindow(result: @escaping FlutterResult) {
- if replayKitManager.isBroadcasting {
- result(true)
- } else {
- result(FlutterError(code: "NOT_BROADCASTING", message: "广播未启动", details: nil))
- }
- }
-
- /// 隐藏悬浮窗(ReplayKit 方案中通过停止广播实现)
- private func hideFloatingWindow(result: @escaping FlutterResult) {
- disableFloatingWindow(result: result)
- }
-
- /// 更新悬浮窗内容
- private func updateContent(arguments: [String: Any], result: @escaping FlutterResult) {
- let sourceText = arguments["sourceText"] as? String ?? ""
- let translatedText = arguments["translatedText"] as? String ?? ""
- let sourceLanguage = arguments["sourceLanguage"] as? String ?? ""
- let targetLanguage = arguments["targetLanguage"] as? String ?? ""
- let isIntermediate = arguments["isIntermediate"] as? Bool ?? false
-
- // 构建显示文本
- var displayText = ""
- if !sourceText.isEmpty && !translatedText.isEmpty {
- displayText = "\(sourceText)\n\(translatedText)"
- } else if !translatedText.isEmpty {
- displayText = translatedText
- } else if !sourceText.isEmpty {
- displayText = sourceText
- }
-
- replayKitManager.updateSubtitle(
- text: displayText,
- sourceLanguage: sourceLanguage,
- targetLanguage: targetLanguage,
- isIntermediate: isIntermediate
- )
-
- result(true)
- }
-
diff --git a/ios/Runner/OverlayPermissionPlugin.swift b/ios/Runner/OverlayPermissionPlugin.swift
deleted file mode 100644
index fdabc261f..000000000
--- a/ios/Runner/OverlayPermissionPlugin.swift
+++ /dev/null
@@ -1,40 +0,0 @@
-import Flutter
-import UIKit
-
-/// iOS 悬浮窗权限插件
-/// 为了与 Android 版本保持接口一致性而创建
-public class OverlayPermissionPlugin: NSObject, FlutterPlugin {
-
- // MARK: - 常量
- private static let channelName = "overlay_permission"
-
- // MARK: - 插件注册
- public static func register(with registrar: FlutterPluginRegistrar) {
- let instance = OverlayPermissionPlugin()
-
- // 创建方法通道
- let methodChannel = FlutterMethodChannel(
- name: channelName,
- binaryMessenger: registrar.messenger()
- )
- registrar.addMethodCallDelegate(instance, channel: methodChannel)
-
- print("OverlayPermissionPlugin 注册成功")
- }
-
- // MARK: - 方法调用处理
- public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
- switch call.method {
- case "hasOverlayPermission":
- // iOS 使用应用内悬浮窗,不需要特殊权限
- result(true)
-
- case "requestOverlayPermission":
- // iOS 使用应用内悬浮窗,不需要特殊权限
- result(true)
-
- default:
- result(FlutterMethodNotImplemented)
- }
- }
-}
diff --git a/ios/Runner/ReplayKitManager.swift b/ios/Runner/ReplayKitManager.swift
deleted file mode 100644
index 7588ca8e1..000000000
--- a/ios/Runner/ReplayKitManager.swift
+++ /dev/null
@@ -1,247 +0,0 @@
-import ReplayKit
-import UIKit
-
-/// ReplayKit 管理器
-/// 负责管理屏幕广播的启动、停止和状态监控
-class ReplayKitManager: NSObject {
-
- // MARK: - 单例
- static let shared = ReplayKitManager()
-
- // MARK: - 属性
- private var broadcastController: RPBroadcastController?
- private let appGroupIdentifier = "group.com.saitong.voitrans.shared"
- private var isRecording = false
-
- // 回调闭包
- var onBroadcastStarted: (() -> Void)?
- var onBroadcastFinished: (() -> Void)?
- var onBroadcastError: ((Error) -> Void)?
-
- // MARK: - 初始化
- private override init() {
- super.init()
- setupNotifications()
- }
-
- // MARK: - 公共方法
-
- /// 启动屏幕广播
- /// - Parameter completion: 完成回调
- func startBroadcast(completion: @escaping (Bool, Error?) -> Void) {
- print("🎬 准备启动 ReplayKit 广播")
-
- // 检查是否已经在录制
- if isRecording {
- print("⚠️ 广播已在进行中")
- completion(false, NSError(domain: "ReplayKitManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "广播已在进行中"]))
- return
- }
-
- // 获取广播选择器
- RPBroadcastActivityViewController.load { [weak self] (broadcastActivityViewController, error) in
- DispatchQueue.main.async {
- guard let self = self else { return }
-
- if let error = error {
- print("❌ 加载广播选择器失败: \(error.localizedDescription)")
- completion(false, error)
- return
- }
-
- guard let broadcastActivityViewController = broadcastActivityViewController else {
- let error = NSError(domain: "ReplayKitManager", code: -2, userInfo: [NSLocalizedDescriptionKey: "无法加载广播选择器"])
- completion(false, error)
- return
- }
-
- // 设置代理
- broadcastActivityViewController.delegate = self
-
- // 显示广播选择器
- if let topViewController = self.getTopViewController() {
- broadcastActivityViewController.modalPresentationStyle = .formSheet
- topViewController.present(broadcastActivityViewController, animated: true) {
- print("✅ 广播选择器已显示")
- completion(true, nil)
- }
- } else {
- let error = NSError(domain: "ReplayKitManager", code: -3, userInfo: [NSLocalizedDescriptionKey: "无法获取顶层视图控制器"])
- completion(false, error)
- }
- }
- }
- }
-
- /// 停止屏幕广播
- /// - Parameter completion: 完成回调
- func stopBroadcast(completion: @escaping (Bool, Error?) -> Void) {
- print("🛑 准备停止 ReplayKit 广播")
-
- guard isRecording else {
- print("⚠️ 当前没有进行中的广播")
- completion(false, NSError(domain: "ReplayKitManager", code: -4, userInfo: [NSLocalizedDescriptionKey: "当前没有进行中的广播"]))
- return
- }
-
- broadcastController?.finishBroadcast { [weak self] error in
- DispatchQueue.main.async {
- if let error = error {
- print("❌ 停止广播失败: \(error.localizedDescription)")
- completion(false, error)
- } else {
- print("✅ 广播已停止")
- self?.isRecording = false
- self?.broadcastController = nil
- completion(true, nil)
- }
- }
- }
- }
-
- /// 更新字幕内容
- /// - Parameters:
- /// - text: 字幕文本
- /// - sourceLanguage: 源语言
- /// - targetLanguage: 目标语言
- /// - isIntermediate: 是否为中间结果
- func updateSubtitle(text: String, sourceLanguage: String = "", targetLanguage: String = "", isIntermediate: Bool = false) {
- guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
- print("❌ 无法访问 App Group UserDefaults")
- return
- }
-
- // 更新字幕数据
- userDefaults.set(text, forKey: "subtitle_text")
- userDefaults.set(sourceLanguage, forKey: "subtitle_source_language")
- userDefaults.set(targetLanguage, forKey: "subtitle_target_language")
- userDefaults.set(isIntermediate, forKey: "subtitle_is_intermediate")
- userDefaults.set(Date().timeIntervalSince1970, forKey: "subtitle_timestamp")
-
- // 强制同步
- userDefaults.synchronize()
-
- print("📝 字幕已更新: \(text)")
- }
-
- /// 检查广播状态
- var isBroadcasting: Bool {
- return isRecording
- }
-
- // MARK: - 私有方法
-
- /// 设置通知监听
- private func setupNotifications() {
- NotificationCenter.default.addObserver(
- self,
- selector: #selector(applicationDidEnterBackground),
- name: UIApplication.didEnterBackgroundNotification,
- object: nil
- )
-
- NotificationCenter.default.addObserver(
- self,
- selector: #selector(applicationWillEnterForeground),
- name: UIApplication.willEnterForegroundNotification,
- object: nil
- )
- }
-
- /// 应用进入后台
- @objc private func applicationDidEnterBackground() {
- print("📱 应用进入后台,广播状态: \(isRecording)")
- }
-
- /// 应用即将进入前台
- @objc private func applicationWillEnterForeground() {
- print("📱 应用即将进入前台,广播状态: \(isRecording)")
- }
-
- /// 获取顶层视图控制器
- private func getTopViewController() -> UIViewController? {
- guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
- let window = windowScene.windows.first else {
- return nil
- }
-
- var topViewController = window.rootViewController
- while let presentedViewController = topViewController?.presentedViewController {
- topViewController = presentedViewController
- }
-
- return topViewController
- }
-
- // MARK: - 清理
- deinit {
- NotificationCenter.default.removeObserver(self)
- print("🗑️ ReplayKitManager 已清理")
- }
-}
-
-// MARK: - RPBroadcastActivityViewControllerDelegate
-extension ReplayKitManager: RPBroadcastActivityViewControllerDelegate {
-
- func broadcastActivityViewController(_ broadcastActivityViewController: RPBroadcastActivityViewController, didFinishWith broadcastController: RPBroadcastController?, error: Error?) {
-
- DispatchQueue.main.async { [weak self] in
- // 关闭选择器
- broadcastActivityViewController.dismiss(animated: true)
-
- if let error = error {
- print("❌ 广播设置失败: \(error.localizedDescription)")
- self?.onBroadcastError?(error)
- return
- }
-
- guard let broadcastController = broadcastController else {
- print("❌ 无法获取广播控制器")
- let error = NSError(domain: "ReplayKitManager", code: -5, userInfo: [NSLocalizedDescriptionKey: "无法获取广播控制器"])
- self?.onBroadcastError?(error)
- return
- }
-
- // 保存广播控制器
- self?.broadcastController = broadcastController
- broadcastController.delegate = self
-
- // 开始广播
- broadcastController.startBroadcast { error in
- DispatchQueue.main.async {
- if let error = error {
- print("❌ 开始广播失败: \(error.localizedDescription)")
- self?.onBroadcastError?(error)
- } else {
- print("✅ 广播已开始")
- self?.isRecording = true
- self?.onBroadcastStarted?()
- }
- }
- }
- }
- }
-}
-
-// MARK: - RPBroadcastControllerDelegate
-extension ReplayKitManager: RPBroadcastControllerDelegate {
-
- func broadcastController(_ broadcastController: RPBroadcastController, didFinishWithError error: Error?) {
- DispatchQueue.main.async { [weak self] in
- self?.isRecording = false
- self?.broadcastController = nil
-
- if let error = error {
- print("❌ 广播结束时发生错误: \(error.localizedDescription)")
- self?.onBroadcastError?(error)
- } else {
- print("✅ 广播正常结束")
- self?.onBroadcastFinished?()
- }
- }
- }
-
- func broadcastController(_ broadcastController: RPBroadcastController, didUpdateServiceInfo serviceInfo: [String : NSCoding & NSObjectProtocol]) {
- print("📊 广播服务信息更新: \(serviceInfo)")
- }
-}
diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements
index b576c2e34..f05d7901a 100644
--- a/ios/Runner/Runner.entitlements
+++ b/ios/Runner/Runner.entitlements
@@ -30,9 +30,5 @@
com.apple.security.personal-information.photos-library
- com.apple.security.application-groups
-
- group.com.saitong.voitrans.shared
-
diff --git a/lib/core/utils/overlay_permission_util.dart b/lib/core/utils/overlay_permission_util.dart
index 7eeebc7b3..6a580621c 100644
--- a/lib/core/utils/overlay_permission_util.dart
+++ b/lib/core/utils/overlay_permission_util.dart
@@ -19,13 +19,9 @@ class OverlayPermissionUtil {
Logger.error('检查悬浮窗权限失败: $e');
return false;
}
- } else if (Platform.isIOS) {
- // iOS 使用应用内悬浮窗,不需要特殊权限
- Logger.info('iOS 平台:使用应用内悬浮窗,无需特殊权限');
- return true;
}
- // 其他平台不支持
- return false;
+ // iOS不需要特殊权限
+ return true;
}
/// 请求悬浮窗权限
@@ -68,12 +64,7 @@ class OverlayPermissionUtil {
}
}
return false;
- } else if (Platform.isIOS) {
- // iOS 使用应用内悬浮窗,不需要特殊权限,但需要检查应用状态
- Logger.info('iOS 平台:使用应用内悬浮窗,无需特殊权限');
- return true;
}
- // 其他平台不支持
- return false;
+ return true;
}
}
diff --git a/test/ios_floating_window_test.dart b/test/ios_floating_window_test.dart
deleted file mode 100644
index 2c274e42e..000000000
--- a/test/ios_floating_window_test.dart
+++ /dev/null
@@ -1,149 +0,0 @@
-import 'package:flutter_test/flutter_test.dart';
-import 'package:flutter/services.dart';
-import 'package:deep_voice/core/services/floating_window_manager.dart';
-
-/// iOS ReplayKit 悬浮窗功能测试
-void main() {
- group('iOS ReplayKit 悬浮窗测试', () {
- late FloatingWindowManager floatingWindowManager;
- late List methodCalls;
-
- setUp(() {
- methodCalls = [];
- floatingWindowManager = FloatingWindowManager();
-
- // 模拟 iOS 平台的方法通道调用
- TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
- .setMockMethodCallHandler(
- const MethodChannel('floating_window'),
- (MethodCall methodCall) async {
- methodCalls.add(methodCall);
-
- switch (methodCall.method) {
- case 'enableFloatingWindow':
- return true;
- case 'disableFloatingWindow':
- return true;
- case 'showFloatingWindow':
- return true;
- case 'hideFloatingWindow':
- return true;
- case 'updateContent':
- return true;
- case 'setPosition':
- return true;
- default:
- return null;
- }
- },
- );
- });
-
- tearDown(() {
- TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
- .setMockMethodCallHandler(
- const MethodChannel('floating_window'),
- null,
- );
- });
-
- test('启用悬浮窗功能 (ReplayKit)', () async {
- final result = await floatingWindowManager.enableFloatingWindow();
-
- expect(result, isTrue);
- expect(methodCalls.length, equals(1));
- expect(methodCalls[0].method, equals('enableFloatingWindow'));
- });
-
- test('禁用悬浮窗功能', () async {
- await floatingWindowManager.disableFloatingWindow();
-
- expect(methodCalls.length, equals(1));
- expect(methodCalls[0].method, equals('disableFloatingWindow'));
- });
-
- test('显示悬浮窗', () async {
- await floatingWindowManager.showFloatingWindow();
-
- expect(methodCalls.length, equals(1));
- expect(methodCalls[0].method, equals('showFloatingWindow'));
- });
-
- test('隐藏悬浮窗', () async {
- await floatingWindowManager.hideFloatingWindow();
-
- expect(methodCalls.length, equals(1));
- expect(methodCalls[0].method, equals('hideFloatingWindow'));
- });
-
- test('更新悬浮窗内容', () async {
- final testData = FloatingWindowData(
- sourceText: '你好',
- translatedText: 'Hello',
- sourceLanguage: '中文',
- targetLanguage: '英语',
- timestamp: DateTime.now(),
- isIntermediate: false,
- );
-
- await floatingWindowManager.updateFloatingWindowContent(testData);
-
- expect(methodCalls.length, equals(1));
- expect(methodCalls[0].method, equals('updateContent'));
-
- final arguments = methodCalls[0].arguments as Map;
- expect(arguments['sourceText'], equals('你好'));
- expect(arguments['translatedText'], equals('Hello'));
- expect(arguments['sourceLanguage'], equals('中文'));
- expect(arguments['targetLanguage'], equals('英语'));
- expect(arguments['isIntermediate'], equals(false));
- });
-
- test('设置悬浮窗位置', () async {
- await floatingWindowManager.setFloatingWindowPosition(100.0, 200.0);
-
- expect(methodCalls.length, equals(1));
- expect(methodCalls[0].method, equals('setPosition'));
-
- final arguments = methodCalls[0].arguments as Map;
- expect(arguments['x'], equals(100.0));
- expect(arguments['y'], equals(200.0));
- });
-
- test('悬浮窗状态管理', () async {
- // 初始状态
- expect(floatingWindowManager.isEnabled, isFalse);
- expect(floatingWindowManager.currentState, equals(FloatingWindowState.hidden));
-
- // 启用悬浮窗
- await floatingWindowManager.enableFloatingWindow();
- expect(floatingWindowManager.isEnabled, isTrue);
-
- // 显示悬浮窗
- await floatingWindowManager.showFloatingWindow();
- // 注意:实际状态变化需要通过原生回调触发,这里只测试方法调用
- });
- });
-
- group('悬浮窗数据模型测试', () {
- test('FloatingWindowData 序列化', () {
- final data = FloatingWindowData(
- sourceText: '测试文本',
- translatedText: 'Test Text',
- sourceLanguage: '中文(简体)',
- targetLanguage: '英语',
- timestamp: DateTime(2024, 1, 1, 12, 0, 0),
- isIntermediate: true,
- );
-
- final map = data.toMap();
-
- expect(map['sourceText'], equals('测试文本'));
- expect(map['translatedText'], equals('Test Text'));
- expect(map['sourceLanguage'], equals('中文(简体)'));
- expect(map['targetLanguage'], equals('英语'));
- expect(map['timestamp'], equals(DateTime(2024, 1, 1, 12, 0, 0).millisecondsSinceEpoch));
- expect(map['isIntermediate'], equals(true));
- });
- });
-}
From 070ff450a100428917403d929fc22bb82cf506ee Mon Sep 17 00:00:00 2001
From: lxm <1274844347@qq.com>
Date: Thu, 9 Oct 2025 19:49:35 +0800
Subject: [PATCH 2/4] =?UTF-8?q?feat(=E8=AE=BE=E5=A4=87=E6=A3=80=E6=B5=8B):?=
=?UTF-8?q?=20=E6=94=B9=E8=BF=9B=E8=AE=BE=E5=A4=87=E7=B1=BB=E5=9E=8B?=
=?UTF-8?q?=E6=A3=80=E6=B5=8B=E5=92=8C=E7=94=B5=E9=87=8F=E6=A3=80=E6=9F=A5?=
=?UTF-8?q?=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
扩展单端耳机检测范围,新增智能录音笔和智能眼镜类型
重构电量检查逻辑,根据设备类型采用不同策略
为单端耳机添加专门的电池显示和低电量提示
---
.../home/controllers/home_controller.dart | 4 +-
lib/modules/home/views/home_view.dart | 3 +-
.../controllers/ota_core_contropller.dart | 145 +++++++++++++++++-
.../controllers/pairing_controller.dart | 6 +-
4 files changed, 147 insertions(+), 11 deletions(-)
diff --git a/lib/modules/home/controllers/home_controller.dart b/lib/modules/home/controllers/home_controller.dart
index a7b702576..7d8aa47af 100644
--- a/lib/modules/home/controllers/home_controller.dart
+++ b/lib/modules/home/controllers/home_controller.dart
@@ -393,7 +393,9 @@ class HomeController extends GetxController {
/// 判断当前设备是否为单端耳机
bool isSingleHeadset() {
- return getCurrentDeviceType() == DeviceType.bleBluetoothSingleHeadset;
+ return getCurrentDeviceType() == DeviceType.bleBluetoothSingleHeadset ||
+ getCurrentDeviceType() == DeviceType.smartVoiceRecorder ||
+ getCurrentDeviceType() == DeviceType.smartEye;
}
String getVersionText() {
diff --git a/lib/modules/home/views/home_view.dart b/lib/modules/home/views/home_view.dart
index e656f0b0c..fccea3b5e 100644
--- a/lib/modules/home/views/home_view.dart
+++ b/lib/modules/home/views/home_view.dart
@@ -533,7 +533,8 @@ class HomeView extends GetView {
// 单端耳机只显示一个电池信息(优先显示左耳,如果左耳电量为0则显示右耳)
if (isSingleHeadset) ...[
_buildCompactBatteryItem(
- label: 'deviceTypeBleBluetoothHeadset'.tr,
+ //label: 'deviceTypeBleBluetoothHeadset'.tr,
+ label: '',
batteryLevel: (leftBattery > 0 || rightBattery == 0)
? leftBattery
: rightBattery,
diff --git a/lib/modules/ota_upgrade/controllers/ota_core_contropller.dart b/lib/modules/ota_upgrade/controllers/ota_core_contropller.dart
index 03ec1be87..cd05687f0 100644
--- a/lib/modules/ota_upgrade/controllers/ota_core_contropller.dart
+++ b/lib/modules/ota_upgrade/controllers/ota_core_contropller.dart
@@ -185,22 +185,155 @@ class OtaCoreService {
return false;
}
+ // 获取设备类型以确定检查策略
+ String deviceName = getDeviceName();
+ DBProduct? product = AppConfig.getproduct(deviceName);
+ if (product == null) {
+ // 如果根据设备名称找不到产品,尝试使用默认产品名称
+ product = AppConfig.getproduct("DeapSound");
+ if (product == null) {
+ _showBatteryCheckFailedSnackbar('无法获取产品信息,设备名称: $deviceName');
+ return false;
+ }
+ }
+
final leftBattery = deviceInfo.leftEarBattery;
final rightBattery = deviceInfo.rightEarBattery;
- // 检查左右耳电量是否都大于20%
- if (leftBattery <= 20 || rightBattery <= 20) {
- _showLowBatteryDialog(leftBattery, rightBattery);
- return false;
+ // 根据设备类型进行不同的电量检查策略
+ if (product.devicetype == DeviceType.bleBluetoothSingleHeadset.value ||
+ product.devicetype == DeviceType.smartEye.value ||
+ product.devicetype == DeviceType.smartVoiceRecorder.value) {
+ // 单端耳机:只需要检查有效的那一端电量
+ return _checkSingleHeadsetBattery(leftBattery, rightBattery);
+ } else if (product.devicetype ==
+ DeviceType.bleBluetoothDoubleHeadset.value) {
+ // 双端耳机:需要检查左右两端电量
+ return _checkDoubleHeadsetBattery(leftBattery, rightBattery);
+ } else {
+ // 其他设备类型,使用原有逻辑(双端检查)
+ return _checkDoubleHeadsetBattery(leftBattery, rightBattery);
}
-
- return true;
} catch (e) {
_showBatteryCheckFailedSnackbar('检查设备电量时发生错误:$e');
return false;
}
}
+ /// 检查单端耳机电量
+ /// [leftBattery] 左耳电量
+ /// [rightBattery] 右耳电量
+ /// 返回true表示电量充足
+ bool _checkSingleHeadsetBattery(int leftBattery, int rightBattery) {
+ // 单端耳机逻辑:优先检查左耳,如果左耳无效(<=0或127)则检查右耳
+ int activeBattery;
+ String activeEar;
+
+ if (leftBattery > 0 && leftBattery != 127) {
+ // 左耳有效,使用左耳电量
+ activeBattery = leftBattery;
+ activeEar = '左耳';
+ } else if (rightBattery > 0 && rightBattery != 127) {
+ // 左耳无效,使用右耳电量
+ activeBattery = rightBattery;
+ activeEar = '右耳';
+ } else {
+ // 两边都无效
+ _showBatteryCheckFailedSnackbar('无法获取有效的电量信息');
+ return false;
+ }
+
+ // 检查有效端的电量是否大于20%
+ if (activeBattery <= 20) {
+ _showSingleHeadsetLowBatteryDialog(activeEar, activeBattery);
+ return false;
+ }
+
+ return true;
+ }
+
+ /// 检查双端耳机电量
+ /// [leftBattery] 左耳电量
+ /// [rightBattery] 右耳电量
+ /// 返回true表示电量充足
+ bool _checkDoubleHeadsetBattery(int leftBattery, int rightBattery) {
+ // 双端耳机逻辑:需要检查左右两端电量是否都大于20%
+
+ // 检查电量数据有效性
+ bool leftValid = leftBattery > 0 && leftBattery != 127;
+ bool rightValid = rightBattery > 0 && rightBattery != 127;
+
+ if (!leftValid || !rightValid) {
+ _showBatteryCheckFailedSnackbar('无法获取有效的电量信息');
+ return false;
+ }
+
+ // 如果只有一端有效,按单端处理
+ // if (!leftValid || !rightValid) {
+ // String activeEar = leftValid ? '左耳' : '右耳';
+ // int activeBattery = leftValid ? leftBattery : rightBattery;
+
+ // if (activeBattery <= 20) {
+ // _showSingleHeadsetLowBatteryDialog(activeEar, activeBattery);
+ // return false;
+ // }
+ // return true;
+ // }
+
+ // 两端都有效,检查是否都大于20%
+ if (leftBattery <= 20 || rightBattery <= 20) {
+ _showLowBatteryDialog(leftBattery, rightBattery);
+ return false;
+ }
+
+ return true;
+ }
+
+ /// 显示单端耳机电量不足对话框
+ /// [earSide] 耳机端(左耳/右耳)
+ /// [batteryLevel] 电量百分比
+ void _showSingleHeadsetLowBatteryDialog(String earSide, int batteryLevel) {
+ String message = '设备电量不足,无法进行固件升级\n';
+ message += '升级要求:设备电量需大于20%\n';
+ message += '当前电量:$earSide ${batteryLevel}%\n';
+ message += '请充电后再试';
+
+ Get.dialog(
+ AlertDialog(
+ title: Text(
+ '电量不足',
+ style: TextStyle(
+ color: Colors.orange[700],
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ content: Text(
+ message,
+ style: TextStyle(
+ color: Colors.orange[600],
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Get.back(),
+ style: TextButton.styleFrom(
+ foregroundColor: Colors.orange[700],
+ ),
+ child: const Text('确定'),
+ ),
+ ],
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ side: BorderSide(
+ color: Colors.orange.withValues(alpha: 0.3),
+ width: 1,
+ ),
+ ),
+ backgroundColor: Colors.orange[50],
+ ),
+ );
+ }
+
/// 显示电量不足对话框
/// [leftBattery] 左耳电量
/// [rightBattery] 右耳电量
diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart
index 51387abe8..3fb4e4ceb 100644
--- a/lib/modules/pairing/controllers/pairing_controller.dart
+++ b/lib/modules/pairing/controllers/pairing_controller.dart
@@ -817,7 +817,7 @@ class PairingController extends GetxController with WidgetsBindingObserver {
if (Platform.isAndroid) {
final existingDevice = User.instance.devices.firstWhere(
(d) =>
- d.devicetype == 2 &&
+ d.devicetype != 1 &&
d.devicename.contains(deviceName) &&
d.devicemac == device.address,
orElse: () => UserDevice(
@@ -879,7 +879,7 @@ class PairingController extends GetxController with WidgetsBindingObserver {
//检查是否已经绑定
final existingDevice = User.instance.devices.firstWhere(
(d) =>
- d.devicetype == 2 &&
+ d.devicetype != 1 &&
d.license != "" &&
d.devicemac == device.address,
orElse: () => UserDevice(
@@ -913,7 +913,7 @@ class PairingController extends GetxController with WidgetsBindingObserver {
//检查是否已经绑定
final existingDevice = User.instance.devices.firstWhere(
(d) =>
- d.devicetype == 2 &&
+ d.devicetype != 1 &&
d.license != "" &&
d.devicemac == device.address,
orElse: () => UserDevice(
From f2947fb15bcb519583b20f5bd25909e1e4dadfe4 Mon Sep 17 00:00:00 2001
From: lxm <1274844347@qq.com>
Date: Fri, 10 Oct 2025 18:51:24 +0800
Subject: [PATCH 3/4] =?UTF-8?q?feat(=E8=AE=BE=E5=A4=87=E4=BF=A1=E6=81=AF):?=
=?UTF-8?q?=20=E6=A0=B9=E6=8D=AE=E8=AE=BE=E5=A4=87=E7=B1=BB=E5=9E=8B?=
=?UTF-8?q?=E5=8A=A8=E6=80=81=E6=98=BE=E7=A4=BA=E7=94=B5=E6=B1=A0=E4=BF=A1?=
=?UTF-8?q?=E6=81=AF=E6=A0=87=E7=AD=BE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
添加 DeviceType 扩展方法获取显示名称,并在单端耳机电池信息处使用该名称替代空字符串
---
lib/data/models/appconfig_model.dart | 22 ++++++++++++++++++++++
lib/modules/home/views/home_view.dart | 6 ++++--
2 files changed, 26 insertions(+), 2 deletions(-)
diff --git a/lib/data/models/appconfig_model.dart b/lib/data/models/appconfig_model.dart
index 945103b42..7f7b6bd59 100644
--- a/lib/data/models/appconfig_model.dart
+++ b/lib/data/models/appconfig_model.dart
@@ -1,4 +1,5 @@
import 'package:json_annotation/json_annotation.dart';
+import 'package:get/get.dart';
part 'appconfig_model.g.dart'; // 生成的文件名
@@ -136,3 +137,24 @@ enum DeviceType {
/// 设备类型对应的整数值
final int value;
}
+
+// 为 DeviceType 添加扩展方法
+extension DeviceTypeExtension on DeviceType {
+ /// 获取设备类型的显示名称
+ /// @Description 根据设备类型返回对应的翻译键
+ String get displayName {
+ switch (this) {
+ case DeviceType.unknown:
+ return "deviceTypeUnknown".tr; // 未知设备
+ case DeviceType.classicBluetoothHeadset:
+ return "deviceTypeClassicBluetoothHeadset".tr; // 经典蓝牙耳机
+ case DeviceType.bleBluetoothDoubleHeadset:
+ case DeviceType.bleBluetoothSingleHeadset:
+ return "deviceTypeBleBluetoothHeadset".tr; // BLE蓝牙耳机(单端和双端都使用同一个翻译键)
+ case DeviceType.smartVoiceRecorder:
+ return "deviceTypeSmartVoiceRecorder".tr; // 智能录音笔
+ case DeviceType.smartEye:
+ return "deviceTypeSmartEye".tr; // 智能眼镜
+ }
+ }
+}
diff --git a/lib/modules/home/views/home_view.dart b/lib/modules/home/views/home_view.dart
index fccea3b5e..5e5a72be4 100644
--- a/lib/modules/home/views/home_view.dart
+++ b/lib/modules/home/views/home_view.dart
@@ -6,6 +6,7 @@ import '../../../core/widgets/intro_overlay_builder.dart';
import '../controllers/home_controller.dart';
import '../../../routes/app_routes.dart';
import '../../../core/widgets/common_bottom_nav.dart';
+import '../../../data/models/appconfig_model.dart';
// 呼吸灯动画组件
class AnimatedPulsing extends StatefulWidget {
@@ -533,8 +534,9 @@ class HomeView extends GetView {
// 单端耳机只显示一个电池信息(优先显示左耳,如果左耳电量为0则显示右耳)
if (isSingleHeadset) ...[
_buildCompactBatteryItem(
- //label: 'deviceTypeBleBluetoothHeadset'.tr,
- label: '',
+ label: controller
+ .getCurrentDeviceType()
+ .displayName, // 根据设备类型动态显示文本
batteryLevel: (leftBattery > 0 || rightBattery == 0)
? leftBattery
: rightBattery,
From b01a40433f2eb7c252b57660fd2521a82b3733b8 Mon Sep 17 00:00:00 2001
From: liwei1dao
Date: Fri, 10 Oct 2025 19:31:41 +0800
Subject: [PATCH 4/4] =?UTF-8?q?=E4=B8=8A=E4=BC=A0ai=E4=BB=A3=E7=A0=81?=
=?UTF-8?q?=E4=BC=98=E5=8C=96=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
lib/data/models/message_model.dart | 23 ++
.../agent/controllers/agent_controller.dart | 197 +++++++++-----
lib/modules/agent/views/agent_view.dart | 8 +
lib/modules/agent/views/message_bubble.dart | 236 ++++++++++++++--
.../agent_service/AgentService.kt | 84 +++---
.../agent_service/AgentServicePlugin.kt | 12 +
.../yunqiinnovation/agent_service/BleAgent.kt | 4 +-
.../agent_service/AgentServiceImpl.swift | 45 ++--
.../agent_service/AgentServicePlugin.swift | 6 +-
.../agent_service/lib/agent_service.dart | 7 +-
.../azure_speech/AzureAsrHelper.kt | 41 +--
.../Sources/azure_speech/AzureAsrHelper.swift | 28 +-
.../azure_speech/AzureSpeechPlugin.swift | 12 +-
.../chat_api/ChatApiService.kt | 253 ++++++++++--------
.../chat_api/CustomSseClientTransport.kt | 16 +-
.../chat_storage/ChatStorageHelper.kt | 52 ++--
.../chat_storage/ChatStoragePlugin.kt | 14 +-
.../chat_storage/ChatStorageHelper.swift | 33 +--
.../chat_storage/SwiftChatStoragePlugin.swift | 12 +-
.../chat_storage/lib/chat_storage.dart | 24 +-
20 files changed, 761 insertions(+), 346 deletions(-)
diff --git a/lib/data/models/message_model.dart b/lib/data/models/message_model.dart
index 09744a939..e2c534200 100644
--- a/lib/data/models/message_model.dart
+++ b/lib/data/models/message_model.dart
@@ -3,6 +3,7 @@ import 'dart:io';
import 'package:path_provider/path_provider.dart';
class Message {
+ final String sessionid; // 会话ID
final bool isUser; //标识信息是否来自用户还是AI
final String text; //信息文本内容
final DateTime timestamp; // 消息时间戳
@@ -10,9 +11,12 @@ class Message {
final bool hasImage; // 是否包含图片
final String? imagePath; // 图片路径
final Map? meta; //卡片数据
+ final List? functionCalls; // 函数调用
+ bool isToolCallsExpanded; // 工具调用是否展开
Message({
// 构造函数
+ required this.sessionid, // 会话ID
required this.isUser,
required this.text,
DateTime? timestamp,
@@ -20,11 +24,14 @@ class Message {
this.hasImage = false,
this.imagePath,
this.meta,
+ this.functionCalls,
+ this.isToolCallsExpanded = false, // 默认不展开
}) : this.timestamp = timestamp ?? DateTime.now();
// 从JSON构造函数
factory Message.fromJson(Map json) {
return Message(
+ sessionid: json['sessionid'] as String, // 会话ID
isUser: json['isUser'] as bool,
text: json['text'] as String,
timestamp: DateTime.parse(json['timestamp'] as String),
@@ -32,6 +39,8 @@ class Message {
hasImage: json['hasImage'] as bool? ?? false,
imagePath: json['imagePath'] as String?,
meta: json['meta'] as Map?,
+ functionCalls: json['functionCalls'] as List?,
+ isToolCallsExpanded: json['isToolCallsExpanded'] as bool? ?? false,
);
}
@@ -45,12 +54,15 @@ class Message {
'hasImage': hasImage,
'imagePath': imagePath,
'meta': meta,
+ 'functionCalls': functionCalls,
+ 'isToolCallsExpanded': isToolCallsExpanded,
};
}
// 创建一个加载中的消息
factory Message.loading() {
return Message(
+ sessionid: '', // 会话ID
isUser: false,
text: '',
timestamp: DateTime.now(),
@@ -60,6 +72,7 @@ class Message {
// 复制并修改
Message copyWith({
+ required String sessionid, // 会话ID
bool? isUser,
String? text,
DateTime? timestamp,
@@ -67,8 +80,11 @@ class Message {
bool? hasImage,
String? imagePath,
Map? meta,
+ List? functionCalls,
+ bool? isToolCallsExpanded,
}) {
return Message(
+ sessionid: sessionid, // 会话ID
isUser: isUser ?? this.isUser,
text: text ?? this.text,
timestamp: timestamp ?? this.timestamp,
@@ -76,6 +92,13 @@ class Message {
hasImage: hasImage ?? this.hasImage,
imagePath: imagePath ?? this.imagePath,
meta: meta ?? this.meta,
+ functionCalls: functionCalls ?? this.functionCalls,
+ isToolCallsExpanded: isToolCallsExpanded ?? this.isToolCallsExpanded,
);
}
+
+ // 切换工具调用展开状态
+ void toggleToolCallsExpanded() {
+ isToolCallsExpanded = !isToolCallsExpanded;
+ }
}
diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart
index 6119f49ef..c11722a84 100644
--- a/lib/modules/agent/controllers/agent_controller.dart
+++ b/lib/modules/agent/controllers/agent_controller.dart
@@ -1,25 +1,21 @@
import 'dart:async';
import 'dart:convert';
-import 'dart:ffi';
import 'dart:io';
import 'package:amap_service/amap_service.dart';
-import 'package:http/http.dart' as http;
import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart';
+import 'package:uuid/uuid.dart';
import 'package:voitrans/data/models/appconfig.dart';
import 'package:voitrans/data/models/song_model.dart';
import 'package:voitrans/data/services/ble_manager.dart';
import 'package:voitrans/data/services/bluetooth_manager.dart';
import 'package:voitrans/data/services/qq_music.dart';
import 'package:flutter_intro/flutter_intro.dart';
-import '../../../data/services/ble_manager.dart';
-import '../../../data/services/bluetooth_manager.dart';
import '../../../data/services/novice_guide_service.dart';
import '/core/utils/upload_oss.dart';
import '/data/models/user_Info.dart';
import '/data/services/music_manager.dart';
import '/data/services/navigation_manager.dart';
-import '/data/services/network/api.dart';
import 'package:ble_service/ble_service.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
@@ -46,6 +42,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
// 聊天消息列表
final messages = [].obs;
+ final currsessionid = ''.obs;
// UI控制器
final TextEditingController textController = TextEditingController();
@@ -138,7 +135,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
final TAG = 'AgentController';
var AppDir = "".obs;
// 会话ID
- final String sessionId = 'default_agent';
+ final String agentId = 'default_agent';
// 分页加载相关状态
final isLoadingMore = false.obs; // 是否正在加载更多消息
@@ -312,7 +309,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
Logger.i(TAG, '正在加载聊天历史...');
//通过本地插件ChatStorage获取聊天记录
final result = await ChatStorage.getMessages(
- sessionId: sessionId, //会话ID
+ agentId: agentId, //会话ID
page: 1, //从第一页开始
pageSize: pageSize, //每页20条记录
);
@@ -367,7 +364,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
Logger.i(TAG, '正在加载第 $nextPage 页历史消息...');
final result = await ChatStorage.getMessages(
- sessionId: sessionId,
+ agentId: agentId,
page: nextPage,
pageSize: pageSize,
);
@@ -421,6 +418,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
: null;
return Message(
+ sessionid: msg.sessionId, // 会话ID
isUser: msg.sender == 'user',
text: msg.message,
meta: meta,
@@ -488,6 +486,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
// 添加初始欢迎消息
void _addWelcomeMessage() {
messages.add(Message(
+ sessionid: "", // 会话ID
isUser: false,
text: 'welcomeMessage'.tr, // 你好!我是**小言**,我能帮你:
));
@@ -504,6 +503,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
// isTextInputMode.refresh();
// 只标记识别开始,不添加消息
Logger.i(TAG, '识别开始');
+
break;
case AgentServiceEventType.recognitionStopped:
isListening.value = false;
@@ -527,25 +527,29 @@ class AgentController extends GetxController with WidgetsBindingObserver {
break;
case AgentServiceEventType.recognizing:
+ final sessionid = event.data['sessionid'] ?? '';
final text = event.data['text'] ?? '';
currentText.value = text; // 保留当前文本,以便其他地方使用
Logger.i(TAG, '识别中间结果: $text');
if (text.isNotEmpty) {
// 查找是否有正在识别中的消息
- final index = messages
- .lastIndexWhere((msg) => msg.isRecognizing && msg.isUser);
+ final index = messages.lastIndexWhere(
+ (msg) => msg.sessionid == sessionid && msg.isUser);
if (index >= 0) {
// 更新已有的识别中消息
messages[index] = Message(
+ sessionid: sessionid, // 会话ID
isUser: true,
text: text,
isRecognizing: true,
timestamp: messages[index].timestamp,
);
} else {
+ currsessionid.value = event.data['sessionid'] ?? '';
// 没有识别中的消息,创建新消息
messages.add(Message(
+ sessionid: sessionid, // 会话ID
isUser: true,
text: text,
isRecognizing: true,
@@ -556,26 +560,33 @@ class AgentController extends GetxController with WidgetsBindingObserver {
break;
case AgentServiceEventType.recognitionResult:
+ final sessionid = event.data['sessionid'] ?? '';
final text = event.data['text'] ?? '';
Logger.i(TAG, '识别最终结果: $text');
// 查找是否有正在识别中的消息
- final index =
- messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser);
+ final index = messages.lastIndexWhere(
+ (msg) => msg.sessionid == sessionid && msg.isUser);
if (text.isNotEmpty) {
if (index >= 0) {
// 将识别中的消息转换为最终消息
messages[index] = Message(
+ sessionid: sessionid,
isUser: true,
text: text,
isRecognizing: false,
timestamp: messages[index].timestamp,
+ functionCalls: messages[index].functionCalls,
+ isToolCallsExpanded: messages[index].isToolCallsExpanded,
);
// messages.refresh();
} else {
// 如果没有找到识别中的消息,添加新消息
- messages
- .add(Message(isUser: true, text: text, isRecognizing: false));
+ messages.add(Message(
+ sessionid: sessionid,
+ isUser: true,
+ text: text,
+ isRecognizing: false));
}
isProcessing.value = true;
@@ -590,24 +601,31 @@ class AgentController extends GetxController with WidgetsBindingObserver {
break;
//识别取消(网络波动会触发)
case AgentServiceEventType.recognitionCanceled:
+ final sessionid = event.data['sessionid'] ?? '';
// 查找是否有正在识别中的消息
- final index =
- messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser);
+ final index = messages.lastIndexWhere(
+ (msg) => msg.sessionid == sessionid && msg.isUser);
if (currentText.value.isNotEmpty) {
if (index >= 0) {
// 将识别中的消息转换为最终消息
messages[index] = Message(
+ sessionid: sessionid, // 会话ID
isUser: true,
text: currentText.value,
isRecognizing: false,
timestamp: messages[index].timestamp,
+ functionCalls: messages[index].functionCalls,
+ isToolCallsExpanded: messages[index].isToolCallsExpanded,
);
// messages.refresh();
} else {
// 如果没有找到识别中的消息,添加新消息
messages.add(Message(
- isUser: true, text: currentText.value, isRecognizing: false));
+ sessionid: sessionid,
+ isUser: true,
+ text: currentText.value,
+ isRecognizing: false));
}
isProcessing.value = true;
@@ -654,29 +672,27 @@ class AgentController extends GetxController with WidgetsBindingObserver {
case AgentServiceEventType.assistantToken:
if (!isProcessing.value) isProcessing.value = true;
-
+ final sessionid = event.data['sessionid'] ?? '';
final token = event.data['token'] ?? '';
- final responseId = event.data['responseId'] ?? '';
// Logger.i(TAG, 'AI回复Token: $token, responseId: $responseId');
if (token.isNotEmpty) {
// 如果是新的回复或者响应ID改变,创建新消息
- if (_isNewAssistantResponse ||
- (_currentResponseId != responseId && responseId.isNotEmpty)) {
- messages.add(Message(isUser: false, text: token));
+ final index = messages.lastIndexWhere(
+ (msg) => msg.sessionid == sessionid && !msg.isUser);
+ if (index == -1) {
+ messages.add(
+ Message(sessionid: sessionid, isUser: false, text: token));
_isNewAssistantResponse = false;
- _currentResponseId = responseId;
} else {
- // 否则更新最后一条消息
- final lastIndex = messages.length - 1;
-
- if (lastIndex >= 0 && !messages[lastIndex].isUser) {
- messages[lastIndex] = Message(
- isUser: false,
- text: messages[lastIndex].text + token,
- meta: messages[lastIndex].meta,
- timestamp: messages[lastIndex].timestamp,
- );
- }
+ messages[index] = Message(
+ sessionid: sessionid,
+ isUser: false,
+ text: messages[index].text + token,
+ meta: messages[index].meta,
+ timestamp: messages[index].timestamp,
+ functionCalls: messages[index].functionCalls,
+ isToolCallsExpanded: messages[index].isToolCallsExpanded,
+ );
}
// 使用reverse:true时通常不需要手动滚动
}
@@ -713,10 +729,10 @@ class AgentController extends GetxController with WidgetsBindingObserver {
case AgentServiceEventType.autoStop:
isListening.value = false;
-
+ final sessionid = event.data['sessionid'] ?? '';
// 移除临时的识别消息
- final index =
- messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser);
+ final index = messages.lastIndexWhere(
+ (msg) => msg.sessionid == sessionid && msg.isUser);
if (index >= 0) {
// 只有当消息为空或文本太短时才移除
if (messages[index].text.trim().length < 2) {
@@ -725,21 +741,25 @@ class AgentController extends GetxController with WidgetsBindingObserver {
} else {
// 否则转换为正式消息
messages[index] = Message(
+ sessionid: sessionid,
isUser: true,
text: messages[index].text,
timestamp: messages[index].timestamp,
+ functionCalls: messages[index].functionCalls,
+ isToolCallsExpanded: messages[index].isToolCallsExpanded,
);
// messages.refresh();
// 发送此消息到AI处理
isProcessing.value = true;
_isNewAssistantResponse = true;
- _handleAutoStop(index);
+ _handleAutoStop(sessionid, index);
}
}
break;
case AgentServiceEventType.functionCall:
+ final sessionid = event.data['sessionid'] ?? '';
final functionName = event.data['name'] ?? '';
final arguments = event.data['arguments'] ?? '';
// debugPrint('Fultter 开始调用 mcp: $functionName, $arguments');
@@ -748,15 +768,39 @@ class AgentController extends GetxController with WidgetsBindingObserver {
endPhoneCall(); //退出语音交互,同时退出打电话模式
}
}
-
+ final index = messages.lastIndexWhere(
+ (msg) => msg.sessionid == sessionid && !msg.isUser);
+ if (index == -1) {
+ messages.add(Message(
+ sessionid: sessionid,
+ isUser: false,
+ text: '',
+ functionCalls: [functionName]));
+ _isNewAssistantResponse = false;
+ } else {
+ // 否则更新最后一条消息
+ final lastIndex = messages.length - 1;
+
+ if (lastIndex >= 0 && !messages[lastIndex].isUser) {
+ messages[lastIndex] = Message(
+ sessionid: sessionid,
+ isUser: false,
+ text: messages[lastIndex].text,
+ meta: messages[lastIndex].meta,
+ timestamp: messages[lastIndex].timestamp,
+ functionCalls: messages[lastIndex].functionCalls ?? []
+ ..add(functionName),
+ );
+ }
+ }
break;
case AgentServiceEventType.functionCallResult:
- final responseId = event.data['responseId'] ?? '';
+ final sessionid = event.data['sessionid'] ?? '';
// final result = event.data['result'];
final metaStr = event.data['meta'] ?? '';
final functionCall = event.data['function_call'] ?? '';
- Logger.i(TAG, 'Flutter 结束调用 mcp: $metaStr');
+ Logger.i(TAG, 'Flutter 结束调用 $sessionid mcp: $metaStr');
if (metaStr.isNotEmpty) {
Map metaResult = {};
metaResult = jsonDecode(metaStr) as Map;
@@ -771,25 +815,31 @@ class AgentController extends GetxController with WidgetsBindingObserver {
//不回复直接返回
break;
}
+ final index = messages.lastIndexWhere(
+ (msg) => msg.sessionid == sessionid && !msg.isUser);
+ Logger.i(TAG, 'Flutter 结束调用 卡片数据 $index $metaResult');
// 判断是否为新的回复或响应ID是否改变
- if (_isNewAssistantResponse || _currentResponseId != responseId) {
+ if (index == -1) {
// 添加新消息
- messages
- .add(Message(isUser: false, text: "", meta: metaResult));
+ messages.add(Message(
+ sessionid: sessionid,
+ isUser: false,
+ text: "",
+ meta: metaResult));
_isNewAssistantResponse = false;
- _currentResponseId = responseId;
} else {
// 更新最后一条消息
- final lastIndex = messages.length - 1;
- if (lastIndex >= 0 && !messages[lastIndex].isUser) {
- messages[lastIndex] = Message(
- isUser: false,
- text: messages[lastIndex].text,
- meta: metaResult,
- timestamp: messages[lastIndex].timestamp,
- );
- }
+ messages[index] = Message(
+ sessionid: sessionid,
+ isUser: false,
+ text: messages[index].text,
+ meta: metaResult,
+ timestamp: messages[index].timestamp,
+ functionCalls: messages[index].functionCalls,
+ isToolCallsExpanded: messages[index].isToolCallsExpanded,
+ );
}
+ messages.refresh();
if (metaResult.containsKey("maps_route_navigation")) {
final maps_route_navigation =
metaResult["maps_route_navigation"];
@@ -847,6 +897,9 @@ class AgentController extends GetxController with WidgetsBindingObserver {
}
}
}
+ } else {
+ // 其他函数调用
+ Logger.i(TAG, 'Flutter 结束调用 解析失败 $metaStr');
}
}
if (functionCall.contains("music")) {
@@ -893,6 +946,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
// ✅ 创建新的 Message 替换原来的
messages[index] = Message(
+ sessionid: result.sessionid,
isUser: result.isUser,
text: result.text,
meta: newMeta,
@@ -1002,10 +1056,13 @@ class AgentController extends GetxController with WidgetsBindingObserver {
// 标记为新的AI回复
_isNewAssistantResponse = true;
newAIReplyCompleted.value = false;
+ currsessionid.value = const Uuid().v4();
+ final sessionid = currsessionid.value;
// 根据TTS状态决定是否朗读
- await AgentService.processTextInput(text,
+ AgentService.processTextInput(sessionid, text,
speakResponse: isTtsEnabled.value);
- final message = Message(isUser: true, text: text);
+ final message = Message(sessionid: sessionid, isUser: true, text: text);
+ Logger.i(TAG, '发送AI消息:$sessionid $text');
messages.add(message);
textController.clear();
} catch (e) {
@@ -1029,12 +1086,17 @@ class AgentController extends GetxController with WidgetsBindingObserver {
var ImageUrl = await UploadOss.upload(
filepath: permanentImagePath,
rootDir: 'User/${User.instance.uid}/ChatImages');
- Logger.i(TAG, '图片上传: $permanentImagePath URL: $ImageUrl');
+
// 显示的文本,如果没有提供则使用默认值
final displayText = text?.isNotEmpty == true ? text! : '[图片]';
+ currsessionid.value = const Uuid().v4();
+ final sessionid = currsessionid.value;
+ Logger.i(TAG,
+ '发送AI图片消息:$sessionid $displayText $permanentImagePath URL: $ImageUrl');
// 添加用户消息
final message = Message(
+ sessionid: sessionid,
isUser: true,
text: displayText,
hasImage: true,
@@ -1055,9 +1117,11 @@ class AgentController extends GetxController with WidgetsBindingObserver {
// 保存消息到持久化存储
//await _saveMessageToStorage(message);
-
+ currsessionid.value = const Uuid().v4();
+ final sessionid = currsessionid.value;
// 调用Agent Service处理图片,考虑TTS状态
await AgentService.processImageInput(
+ sessionid,
relativeImagePath,
ImageUrl,
text: text ?? '',
@@ -1855,11 +1919,13 @@ class AgentController extends GetxController with WidgetsBindingObserver {
}
// 修改自动停止处理,遵循TTS设置
- void _handleAutoStop(int index) {
+ void _handleAutoStop(String sessionid, int index) {
// 发送此消息到AI处理
isProcessing.value = true;
+ // 标记为新的AI回复
_isNewAssistantResponse = true;
- AgentService.processTextInput(messages[index].text,
+ currsessionid.value = sessionid;
+ AgentService.processTextInput(sessionid, messages[index].text,
speakResponse: isTtsEnabled.value);
}
@@ -2612,4 +2678,15 @@ class AgentController extends GetxController with WidgetsBindingObserver {
Get.snackbar('错误', '打开苹果地图时发生错误');
}
}
+
+ Future updateToolCallsExpanded(
+ String sessionid,
+ bool isExpanded,
+ ) async {
+ final message = messages.firstWhere(
+ (msg) => msg.sessionid == sessionid && msg.isUser == false,
+ );
+ message.isToolCallsExpanded = isExpanded;
+ messages.refresh();
+ }
}
diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart
index c9c456491..3f4dd0dd1 100644
--- a/lib/modules/agent/views/agent_view.dart
+++ b/lib/modules/agent/views/agent_view.dart
@@ -287,6 +287,7 @@ class _AgentViewState extends State with WidgetsBindingObserver {
isDarkMode, details, message);
},
child: MessageBubble(
+ sessionid: message.sessionid,
isUser: message.isUser,
message: message.text,
timestamp: message.timestamp,
@@ -294,6 +295,9 @@ class _AgentViewState extends State with WidgetsBindingObserver {
hasImage: message.hasImage,
imagePath: message.imagePath,
meta: message.meta,
+ functionCalls: message.functionCalls,
+ isToolCallsExpanded:
+ message.isToolCallsExpanded,
),
),
),
@@ -307,6 +311,7 @@ class _AgentViewState extends State with WidgetsBindingObserver {
context, isDarkMode, details, message);
},
child: MessageBubble(
+ sessionid: message.sessionid,
isUser: message.isUser,
message: message.text,
timestamp: message.timestamp,
@@ -314,6 +319,9 @@ class _AgentViewState extends State with WidgetsBindingObserver {
hasImage: message.hasImage,
imagePath: message.imagePath,
meta: message.meta,
+ functionCalls: message.functionCalls,
+ isToolCallsExpanded:
+ message.isToolCallsExpanded,
),
),
);
diff --git a/lib/modules/agent/views/message_bubble.dart b/lib/modules/agent/views/message_bubble.dart
index f5b24bd04..fed515b23 100644
--- a/lib/modules/agent/views/message_bubble.dart
+++ b/lib/modules/agent/views/message_bubble.dart
@@ -18,6 +18,7 @@ import '../controllers/agent_controller.dart';
/// 消息气泡组件
class MessageBubble extends GetView {
+ final String sessionid;
final bool isUser;
final String message;
final DateTime timestamp;
@@ -25,9 +26,11 @@ class MessageBubble extends GetView {
final bool hasImage;
final String? imagePath;
final Map? meta;
-
+ final List? functionCalls;
+ final bool isToolCallsExpanded;
const MessageBubble({
Key? key,
+ required this.sessionid,
required this.isUser,
required this.message,
required this.timestamp,
@@ -35,6 +38,8 @@ class MessageBubble extends GetView {
this.hasImage = false,
this.imagePath,
this.meta,
+ this.functionCalls,
+ this.isToolCallsExpanded = false,
}) : super(key: key);
@override
@@ -75,7 +80,24 @@ class MessageBubble extends GetView {
),
],
),
- child: _buildMessageContent(context),
+ child: Stack(
+ children: [
+ // 主要消息内容
+ Padding(
+ padding: (!isUser &&
+ functionCalls != null &&
+ functionCalls!.isNotEmpty)
+ ? const EdgeInsets.only(top: 20)
+ : EdgeInsets.zero,
+ child: _buildMessageContent(context),
+ ),
+ //工具调用流程显示(仅在AI消息且有functionCalls时显示)
+ if (!isUser &&
+ functionCalls != null &&
+ functionCalls!.isNotEmpty)
+ _buildFunctionCallsWidget(context),
+ ],
+ ),
),
const SizedBox(height: 4),
// 识别中的消息不显示时间
@@ -89,7 +111,7 @@ class MessageBubble extends GetView {
Text(
_formatTimestamp(timestamp),
style: TextStyle(
- fontSize: 10,
+ fontSize: 14,
color: Theme.of(context).brightness == Brightness.dark
? Colors.grey[400]
: Colors.grey[500],
@@ -160,10 +182,21 @@ class MessageBubble extends GetView {
);
}
- if (!isUser && message.isEmpty && meta == null) {
+ if (!isUser && message.isEmpty && functionCalls == null && meta == null) {
return _buildTypingIndicator();
}
+ // 如果只有工具调用而没有文本内容,显示一个占位符确保气泡有足够宽度
+ if (!isUser && message.isEmpty && functionCalls != null && meta == null) {
+ return Container(
+ constraints: const BoxConstraints(
+ minWidth: 200, // 确保最小宽度
+ minHeight: 20, // 确保最小高度
+ ),
+ child: const SizedBox.shrink(), // 透明占位符
+ );
+ }
+
// 如果是图片消息,显示图片
if (hasImage && imagePath != null) {
// 如果消息是默认的[图片]标记,只显示图片
@@ -369,9 +402,12 @@ class MessageBubble extends GetView {
// }
}
+ // 创建消息内容Widget
+ Widget messageContent;
+
// 使用Markdown渲染AI消息,用户消息仍然使用普通文本
if (!isUser) {
- return MarkdownBody(
+ messageContent = MarkdownBody(
data: message,
styleSheet: MarkdownStyleSheet(
p: TextStyle(
@@ -442,21 +478,34 @@ class MessageBubble extends GetView {
),
//selectable: true,
);
+ } else {
+ // 用户消息仍然使用普通文本
+ messageContent = Text(
+ message,
+ style: TextStyle(
+ color: isUser
+ ? Colors.white
+ : (Theme.of(context).brightness == Brightness.dark
+ ? Colors.white.withOpacity(0.9)
+ : Colors.black87),
+ fontSize: 14,
+ height: 1.4,
+ fontWeight: FontWeight.w400,
+ ),
+ );
}
- // 用户消息仍然使用普通文本
- return Text(
- message,
- style: TextStyle(
- color: isUser
- ? Colors.white
- : (Theme.of(context).brightness == Brightness.dark
- ? Colors.white.withOpacity(0.9)
- : Colors.black87),
- fontSize: 14,
- height: 1.4,
- fontWeight: FontWeight.w400,
- ),
- );
+
+ // 如果是AI消息且有工具调用,确保最小宽度以防止工具调用列表越界
+ if (!isUser && functionCalls != null && functionCalls!.isNotEmpty) {
+ return Container(
+ constraints: const BoxConstraints(
+ minWidth: 200, // 确保最小宽度,防止工具调用列表越界
+ ),
+ child: messageContent,
+ );
+ }
+
+ return messageContent;
}
/// 构建输入中的指示器
@@ -550,6 +599,155 @@ class MessageBubble extends GetView {
return '';
}
}
+
+ /// 构建工具调用流程组件
+ Widget _buildFunctionCallsWidget(BuildContext context) {
+ return Positioned(
+ top: 0,
+ left: 0,
+ right: 0,
+ child: Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 0, vertical: 0), // 减少垂直padding
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // 可点击的标题行
+ GestureDetector(
+ onTap: () async {
+ await controller.updateToolCallsExpanded(
+ sessionid,
+ !isToolCallsExpanded,
+ );
+ },
+ child: Container(
+ height: 20, // 增加固定高度,给字体更多空间
+ decoration: BoxDecoration(
+ color: Theme.of(context).brightness == Brightness.dark
+ ? Colors.grey[800]?.withOpacity(0.8)
+ : Colors.grey[100]?.withOpacity(0.9),
+ borderRadius: BorderRadius.circular(6),
+ border: Border.all(
+ color: Theme.of(context).brightness == Brightness.dark
+ ? Colors.white.withOpacity(0.1)
+ : Colors.black.withOpacity(0.1),
+ width: 0.5,
+ ),
+ ),
+ child: Row(
+ children: [
+ // 显示最后一个工具名称
+ Expanded(
+ child: Text(
+ functionCalls != null && functionCalls!.isNotEmpty
+ ? functionCalls!.last
+ : '工具调用',
+ style: TextStyle(
+ fontSize: 12, // 适当增大字体
+ color:
+ Theme.of(context).brightness == Brightness.dark
+ ? Colors.white.withOpacity(0.7)
+ : Colors.black54,
+ fontWeight: FontWeight.w500,
+ ),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ const SizedBox(width: 4),
+ // 箭头放在右侧
+ Icon(
+ isToolCallsExpanded
+ ? Icons.keyboard_arrow_up // 展开时显示向上箭头
+ : Icons.keyboard_arrow_down, // 收起时显示向下箭头
+ size: 14, // 减小图标大小
+ color: Theme.of(context).brightness == Brightness.dark
+ ? Colors.white.withOpacity(0.7)
+ : Colors.black54,
+ ),
+ ],
+ ),
+ ),
+ ),
+ // 展开的工具调用列表
+ if (isToolCallsExpanded && functionCalls != null)
+ Container(
+ margin: const EdgeInsets.only(top: 2), // 减少顶部间距
+ padding: const EdgeInsets.all(6), // 减少内边距
+ constraints: const BoxConstraints(
+ maxHeight: 120, // 限制最大高度,避免超界
+ ),
+ decoration: BoxDecoration(
+ // 添加更强的背景色来遮挡下层UI
+ color: Theme.of(context).brightness == Brightness.dark
+ ? Colors.grey[900]?.withOpacity(0.95) // 深色模式下使用深灰色背景
+ : Colors.white.withOpacity(0.95), // 浅色模式下使用白色背景
+ borderRadius: BorderRadius.circular(4), // 减小圆角
+ border: Border.all(
+ color: Theme.of(context).brightness == Brightness.dark
+ ? Colors.white.withOpacity(0.2)
+ : Colors.black.withOpacity(0.2),
+ width: 0.5,
+ ),
+ // 添加阴影增强遮挡效果
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.1),
+ blurRadius: 4,
+ offset: const Offset(0, 2),
+ ),
+ ],
+ ),
+ child: SingleChildScrollView(
+ // 添加滚动,防止超界
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: functionCalls!.asMap().entries.map((entry) {
+ int index = entry.key;
+ String call = entry.value;
+ return Padding(
+ padding: EdgeInsets.only(
+ bottom: index < functionCalls!.length - 1
+ ? 2
+ : 0), // 减少间距
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ '${index + 1}. ',
+ style: TextStyle(
+ fontSize: 12, // 修改为更小的字体
+ color: Theme.of(context).brightness ==
+ Brightness.dark
+ ? Colors.white.withOpacity(0.6)
+ : Colors.black45,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ Expanded(
+ child: Text(
+ call,
+ style: TextStyle(
+ fontSize: 12, // 修改为更小的字体
+ color: Theme.of(context).brightness ==
+ Brightness.dark
+ ? Colors.white.withOpacity(0.8)
+ : Colors.black87,
+ height: 1.1, // 减小行高
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }).toList(),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ));
+ }
}
/// 动画点组件
diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
index 84dfaaca3..8b8f90d47 100644
--- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
+++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
@@ -27,7 +27,6 @@ import kotlinx.coroutines.sync.Mutex
import com.deep_voice.speech.tts.TtsEvent
import com.deep_voice.speech.tts.TtsEventListener
import com.deep_voice.speech.tts.TtsEventType
-import java.util.UUID
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
@@ -88,7 +87,7 @@ object AgentService : CoroutineScope {
private lateinit var chatStorageHelper: ChatStorageHelper
// 会话ID,用于区分不同聊天上下文
- private val sessionId = "default_agent"
+ private val agentId = "default_agent"
// 服务端连接
private var serverurl = ""
private var usertoken = ""
@@ -602,7 +601,7 @@ object AgentService : CoroutineScope {
Log.d(TAG, "recognizeCallback")
val success = azureAsrHelper?.setupEventListeners(object :
AzureAsrHelper.ContinuousRecognizeCallback {
- override fun onRecognizing(recognizing: String, detectedLanguage: String) {
+ override fun onRecognizing(sessiondid:String, recognizing: String, detectedLanguage: String) {
if (recognizing.isNotEmpty()&&isKeepResult==true) {
// 检测到语音,更新状态
@@ -646,16 +645,17 @@ object AgentService : CoroutineScope {
}
}
- override fun onResult(result: String, detectedLanguage: String) {
+ override fun onResult(sessiondid:String,result: String, detectedLanguage: String) {
if (result.isNotEmpty()&&isKeepResult==true) {//加入是否结束要输出最后的结果判读
sendEvent(
"recognition_result", mapOf(
+ "sessiondid" to sessiondid,
"text" to result,
"language" to detectedLanguage
)
)
- processWithChatApi(result)
+ processWithChatApi(sessiondid,result)
}
@@ -665,8 +665,8 @@ object AgentService : CoroutineScope {
}
- override fun onSessionStarted() {
- sendEvent("recognition_started", mapOf("status" to "started"))
+ override fun onSessionStarted(sessiondid:String) {
+ sendEvent("recognition_started", mapOf("sessiondid" to sessiondid, "status" to "started"))
// 使用模式感知的空闲检测启动方法
audioPlayer?.playAudio(R.raw.start)
@@ -674,33 +674,35 @@ object AgentService : CoroutineScope {
- override fun onSessionStopped() {
- sendEvent("recognition_stopped", mapOf("status" to "stopped"))
+ override fun onSessionStopped(sessiondid:String) {
+ sendEvent("recognition_stopped", mapOf("sessiondid" to sessiondid, "status" to "stopped"))
_isRecognitionActive.set(false)
stopIdleCheck()
audioPlayer?.playAudio(R.raw.stop)
}
- override fun onCanceled(reason: String, errorDetails: String) {
+ override fun onCanceled(sessiondid: String,reason: String, errorDetails: String) {
_isRecognitionActive.set(false)
stopIdleCheck()
Log.d(TAG, "recognition_canceled:")
BleService.closeCodec()
sendEvent(
"recognition_canceled", mapOf(
+ "sessiondid" to sessiondid,
"reason" to reason,
"details" to errorDetails
)
)
}
- override fun onError(code: Int, error: String) {
+ override fun onError(sessiondid:String, code: Int, error: String) {
_isRecognitionActive.set(false)
stopIdleCheck()
BleService.closeCodec()
Log.d(TAG, "语音识别错误: $error")
sendEvent(
"error", mapOf(
+ "sessiondid" to sessiondid,
"code" to code,
"message" to error
)
@@ -802,7 +804,7 @@ object AgentService : CoroutineScope {
/**
* 处理文本输入
*/
- fun processTextInput(text: String, speakResponse: Boolean = false): Boolean {
+ fun processTextInput(sessionid: String,text: String, speakResponse: Boolean = false): Boolean {
if (!isInitialized) {
Log.e(TAG, "服务未初始化")
sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化"))
@@ -814,17 +816,16 @@ object AgentService : CoroutineScope {
sendEvent("error", mapOf("code" to "EMPTY_TEXT", "message" to "文本输入不能为空"))
return false
}
-
// 使用ChatAPI处理文本
- processWithChatApi(text, speakResponse)
+ processWithChatApi(sessionid,text, speakResponse)
return true
}
/**
* 使用ChatAPI处理语音识别结果
*/
- private fun processWithChatApi(text: String) {
- processWithChatApi(text, true)
+ private fun processWithChatApi(sessionid:String,text: String) {
+ processWithChatApi(sessionid,text, true)
}
/**
@@ -834,6 +835,7 @@ object AgentService : CoroutineScope {
* @param speakResponse 是否使用TTS朗读回复
*/
private fun processWithChatApi(
+ sessionid: String,
text: String,
speakResponse: Boolean = true
) {
@@ -841,7 +843,7 @@ object AgentService : CoroutineScope {
// 创建用户文本消息并处理
val userMessage = createUserMessage(text)
- processWithChatApiInternal(userMessage, text, speakResponse)
+ processWithChatApiInternal(sessionid,userMessage, text, speakResponse)
}
/**
@@ -852,6 +854,7 @@ object AgentService : CoroutineScope {
* @param speakResponse 是否朗读回复
*/
private fun processImageWithChatApi(
+ sessionid: String,
imageBase64: String,
text: String = "",
speakResponse: Boolean = false
@@ -861,7 +864,7 @@ object AgentService : CoroutineScope {
Log.d(TAG, "liwei--------- 图像Mesage: ${userMessage}")
// 图片描述用于存储
val displayText = text.ifEmpty { "[图片]" }
- processWithChatApiInternal(userMessage, displayText, speakResponse, true)
+ processWithChatApiInternal(sessionid,userMessage, displayText, speakResponse, true)
}
/**
@@ -874,21 +877,21 @@ object AgentService : CoroutineScope {
* @param imagePath 图片路径(如果有)
*/
private fun processWithChatApiInternal(
+ sessionid: String,
userMessage: JSONObject,
displayText: String,
speakResponse: Boolean = true,
hasImage: Boolean = false,
imagePath: String? = null
) {
- Log.d(TAG, "内部方法:通用的ChatAPI处理逻辑")
// 如果有正在进行的AI流式输出,先停止它
audioPlayer?.stopAudio()
stopAiStream()
stopTts()
+ currsessionId = sessionid
// 记录新的AI任务
currentAiJob = launch {
try {
- currsessionId = UUID.randomUUID().toString()
// 设置状态为正在流式输出
_isAiStreaming.set(true)
Log.d(TAG, "processWithChatApiInternal")
@@ -958,7 +961,7 @@ object AgentService : CoroutineScope {
}
// Log.e("ChatApiService", "liwei---------sendMessageStream messages:${messagesWithSystemPrompt}")
chatApiService.sendMessageStream(
- currsessionId,
+ sessionid,
messagesWithSystemPrompt,
object : StreamCallback {
@@ -984,15 +987,17 @@ object AgentService : CoroutineScope {
override fun onToken(sessionid: String, token: String) {
try {
if (sessionid != currsessionId) {
+// Log.d(TAG, "liwei--------------- AI Call onToken 会话过期 $sessionid-------$currsessionId")
return
}
responseBuilder.append(token)
+// Log.d(TAG, "liwei--------------- AI Call onToken:$token speakResponse:$speakResponse reply:$reply broadcast:$broadcast")
if (speakResponse && reply && broadcast && token.isNotEmpty()) {
ttsService?.speakStream(token)
}
if (reply && token.isNotEmpty()) {
// 发送流式回复token
- sendEvent("assistant_token", mapOf("token" to token))
+ sendEvent("assistant_token", mapOf("sessionid" to sessionid,"token" to token))
}
} catch (e: Exception) {
Log.e(TAG, "liwei--------------- AI Call onToken 异常", e)
@@ -1011,6 +1016,7 @@ object AgentService : CoroutineScope {
val response = responseBuilder.toString()
// 发送完整回复,包含是否有图片的标记
val responseData = mutableMapOf(
+ "sessionid" to sessionid,
"text" to response,
"userInput" to displayText
)
@@ -1027,6 +1033,7 @@ object AgentService : CoroutineScope {
}
// 保存聊天记录
saveChatMessage(
+ sessionid,
displayText,
response,
aiMetadata,
@@ -1045,6 +1052,7 @@ object AgentService : CoroutineScope {
Log.e(TAG, "AI处理出错", e)
sendEvent(
"error", mapOf(
+ "sessionid" to sessionid,
"code" to "AI_ERROR",
"message" to e.message.toString()
)
@@ -1064,6 +1072,7 @@ object AgentService : CoroutineScope {
val name = call.getString("name")
sendEvent(
"function_call", mapOf(
+ "sessionid" to sessionid,
"name" to name,
"arguments" to call.toString()
)
@@ -1072,6 +1081,10 @@ object AgentService : CoroutineScope {
BleService.closeCodec()
stopRecognition()
}
+
+ //播报工具调用
+ ttsService?.speakStream("正在调用工具:${name}")
+
} catch (e: Exception) {
Log.e(TAG, "liwei--------------- AI Call onFunctionCall 异常", e)
}
@@ -1083,6 +1096,11 @@ object AgentService : CoroutineScope {
functionCallResult: JSONObject
) {
try {
+ Log.d(
+ TAG,
+ "liwei--------------- AI Call onFunctionCallResult ${sessionid}:${functionCallResult.toString()}",
+
+ )
audioPlayer?.stopAudio()
val name = functionCall.get("name") as String;
val resultmap = autoHandleFcunCallResult(name, functionCallResult);
@@ -1093,6 +1111,7 @@ object AgentService : CoroutineScope {
aiMetadata = resultmap["meta"] as String;
sendEvent(
"function_call_result", mapOf(
+ "sessionid" to sessionid,
"function_call" to functionCall.toString(),
"result" to functionCallResult.toString(),
"meta" to aiMetadata,
@@ -1101,16 +1120,12 @@ object AgentService : CoroutineScope {
}else{
sendEvent(
"function_call_result", mapOf(
+ "sessionid" to sessionid,
"function_call" to functionCall.toString(),
"result" to functionCallResult.toString(),
)
)
}
-
- Log.e(
- TAG,
- "liwei--------------- AI Call onFunctionCallResult broadcast:$broadcast reply:$reply iscard:$iscard"
- )
} catch (e: Exception) {
Log.e(
TAG,
@@ -1127,6 +1142,7 @@ object AgentService : CoroutineScope {
Log.e(TAG, "liwei--------------- AI处理出错", e)
sendEvent(
"error", mapOf(
+ "sessionid" to sessionid,
"code" to errorType,
"message" to e.message.toString()
)
@@ -1152,7 +1168,7 @@ object AgentService : CoroutineScope {
}
// 获取最近消息
- val recentMessages = chatStorageHelper.getRecentMessages(sessionId, 10)
+ val recentMessages = chatStorageHelper.getRecentMessages(agentId, 10)
if (recentMessages.isEmpty()) {
return
}
@@ -1389,6 +1405,7 @@ object AgentService : CoroutineScope {
* 保存聊天消息
*/
private fun saveChatMessage(
+ sessionid: String,
userMessage: String,
assistantMessage: String,
aiMetadata: String,
@@ -1399,7 +1416,8 @@ object AgentService : CoroutineScope {
try {
// 保存用户消息
val userMessageId = chatStorageHelper.saveMessage(
- sessionId = sessionId,
+ agentId = agentId,
+ sessionId = sessionid,
message = userMessage,
sender = "user",
metadata = userMetadata
@@ -1409,7 +1427,8 @@ object AgentService : CoroutineScope {
if (assistantMessage.isNotEmpty() || aiMetadata.isNotEmpty()) {
// 保存AI回复
val assistantMessageId = chatStorageHelper.saveMessage(
- sessionId = sessionId,
+ agentId = agentId,
+ sessionId = sessionid,
message = assistantMessage,
sender = "assistant",
metadata = aiMetadata
@@ -1435,7 +1454,7 @@ object AgentService : CoroutineScope {
fun clearChatHistory(callback: (Boolean) -> Unit) {
launch {
try {
- val success = chatStorageHelper.deleteMessages(sessionId)
+ val success = chatStorageHelper.deleteMessages(agentId)
if (success) {
synchronized(historyMessages) {
@@ -1542,6 +1561,7 @@ object AgentService : CoroutineScope {
* 处理图片输入
*/
fun processImageInput(
+ sessionid: String,
imagePath: String,
imageUrl: String,
text: String = "",
@@ -1593,7 +1613,7 @@ object AgentService : CoroutineScope {
)
Log.d(TAG, "liwei--------- 图像Mesage: ${userMessage}")
// 处理消息并传入存储路径
- processWithChatApiInternal(userMessage, displayText, speakResponse, true, imagePath)
+ processWithChatApiInternal(sessionid,userMessage, displayText, speakResponse, true, imagePath)
} catch (e: Exception) {
Log.e(TAG, "处理图片失败: ${e.message}")
sendEvent(
diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt
index e36f91294..513221bf8 100644
--- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt
+++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt
@@ -144,7 +144,12 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
}
"processTextInput" -> {
+ val sessionid = call.argument("sessionid")
val text = call.argument("text")
+ if (sessionid.isNullOrEmpty()) {
+ result.error("INVALID_ARGUMENTS", "回话id不能为空", null)
+ return
+ }
if (text.isNullOrEmpty()) {
result.error("INVALID_ARGUMENTS", "文本不能为空", null)
return
@@ -154,6 +159,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
try {
val isProcessing = AgentService.processTextInput(
+ sessionid = sessionid,
text = text,
speakResponse = speakResponse
)
@@ -166,6 +172,11 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
}
"processImageInput" -> {
+ val sessionid = call.argument("sessionid")
+ if (sessionid.isNullOrEmpty()) {
+ result.error("INVALID_ARGUMENTS", "回话id不能为空", null)
+ return
+ }
val imagePath = call.argument("imagePath")
if (imagePath.isNullOrEmpty()) {
result.error("INVALID_ARGUMENTS", "图片路径不能为空", null)
@@ -181,6 +192,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
try {
val isProcessing = AgentService.processImageInput(
+ sessionid=sessionid,
imagePath = imagePath,
imageUrl = imageUrl,
text = text,
diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt
index 80c9b568b..03dee6109 100644
--- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt
+++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt
@@ -85,8 +85,8 @@ Log.d(TAG, "手动启动语音识别: ")
/**
* 处理文本输入
*/
- fun processTextInput(text: String, speakResponse: Boolean = false): Boolean {
- return AgentService.processTextInput(text, speakResponse)
+ fun processTextInput(sessionId: String,text: String, speakResponse: Boolean = false): Boolean {
+ return AgentService.processTextInput(sessionId,text, speakResponse)
}
/**
diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
index 0643f2510..cc11359ee 100644
--- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
+++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
@@ -59,7 +59,7 @@ class AgentServiceImpl: NSObject {
}
// 会话ID,用于区分不同聊天上下文
- private let sessionId = "default_agent"
+ private let agentId = "default_agent"
private var isInitialized: Bool = false
private var isRecognizing: Bool = false
@@ -565,7 +565,7 @@ audioStream.saveAudioDataTo(data: audioData)
return interrupted
}
- func processTextInput(_ text: String, speakResponse: Bool) -> Bool {
+ func processTextInput(sessionid:String, _ text: String, speakResponse: Bool) -> Bool {
os_log("收到文本输入: text=%{public}@, speakResponse=%{public}@", log: logger, type: .info, text, speakResponse ? "是" : "否")
if !isInitialized {
@@ -581,21 +581,21 @@ audioStream.saveAudioDataTo(data: audioData)
}
os_log("开始处理文本输入", log: logger, type: .info)
- processWithChatApiService(text: text, speakResponse: speakResponse)
+ processWithChatApiService(sessionid: sessionid, text: text, speakResponse: speakResponse)
return true
}
- private func processWithChatApiService(text: String, speakResponse: Bool = true) {
+ private func processWithChatApiService(sessionid:String,text: String, speakResponse: Bool = true) {
guard let chatApiService = chatApiService else {
sendError("ChatAPI服务未初始化", code: "CHATAPI_NOT_INITIALIZED")
return
}
let userMessage = chatApiService.createUserMessage(content: text)
- processWithChatApiServiceInternal(userMessage: userMessage, displayText: text, speakResponse: speakResponse)
+ processWithChatApiServiceInternal(sessionid: sessionid,userMessage: userMessage, displayText: text, speakResponse: speakResponse)
}
- private func processWithChatApiServiceInternal(userMessage: [String: Any], displayText: String, speakResponse: Bool = true, hasImage: Bool = false,imagePath:String = "") {
+ private func processWithChatApiServiceInternal(sessionid:String,userMessage: [String: Any], displayText: String, speakResponse: Bool = true, hasImage: Bool = false,imagePath:String = "") {
os_log("开始处理ChatAPI请求: displayText=%{public}@, speakResponse=%{public}@, hasImage=%{public}@",
log: logger, type: .info, displayText, speakResponse ? "是" : "否", hasImage ? "是" : "否")
@@ -650,7 +650,7 @@ audioStream.saveAudioDataTo(data: audioData)
hasImage: hasImage,
imagePath: imagePath
)
- currsessionId = UUID().uuidString
+ currsessionId = sessionid
azureTtsHelper?.startspeak(sessionid: currsessionId)
os_log("设置回调并调用sendMessageStream", log: logger, type: .info)
chatApiService.setStreamCallback(currsessionId,callback)
@@ -659,7 +659,7 @@ audioStream.saveAudioDataTo(data: audioData)
os_log("sendMessageStream调用完成", log: logger, type: .info)
}
- func processImageInput(imagePath: String,imageUrl: String, text: String, speakResponse: Bool) -> Bool {
+ func processImageInput(sessionid: String,imagePath: String,imageUrl: String, text: String, speakResponse: Bool) -> Bool {
if !isInitialized {
sendError("服务未初始化", code: "NOT_INITIALIZED")
return false
@@ -703,6 +703,7 @@ audioStream.saveAudioDataTo(data: audioData)
let displayText = text.isEmpty ? "[图片]" : text
self.processWithChatApiServiceInternal(
+ sessionid: sessionid,
userMessage: userMessage,
displayText: displayText,
speakResponse: speakResponse,
@@ -731,7 +732,7 @@ audioStream.saveAudioDataTo(data: audioData)
chatHistory.removeAll()
// 获取最近10条消息
- let recentMessages = chatStorageHelper.getRecentMessages(sessionId: sessionId, limit: 10)
+ let recentMessages = chatStorageHelper.getRecentMessages(agentId: agentId, limit: 10)
if recentMessages.isEmpty {
return
@@ -763,11 +764,12 @@ audioStream.saveAudioDataTo(data: audioData)
/**
* 保存聊天消息
*/
- internal func saveChatMessage(userMessage: String, assistantMessage: String, metadata: String = "",userMetadata: String) {
+ internal func saveChatMessage(sessionId:String,userMessage: String, assistantMessage: String, metadata: String = "",userMetadata: String) {
DispatchQueue.global(qos: .utility).async {
// 保存用户消息
let userMessageId = self.chatStorageHelper.saveMessage(
- sessionId: self.sessionId,
+ agentId: self.agentId,
+ sessionId: sessionId,
message: userMessage,
sender: "user",
metadata: userMetadata
@@ -777,7 +779,8 @@ audioStream.saveAudioDataTo(data: audioData)
if !assistantMessage.isEmpty || !metadata.isEmpty {
// 保存AI回复
let assistantMessageId = self.chatStorageHelper.saveMessage(
- sessionId: self.sessionId,
+ agentId: self.agentId,
+ sessionId: sessionId,
message: assistantMessage,
sender: "assistant",
metadata: metadata.isEmpty ? nil : metadata
@@ -1348,7 +1351,7 @@ private func jsonToString(_ json: [String: Any]) -> String? {
chatHistory.removeAll()
DispatchQueue.global(qos: .utility).async {
- let success = self.chatStorageHelper.deleteMessages(sessionId: self.sessionId, messageIds: nil as [Int]?)
+ let success = self.chatStorageHelper.deleteMessages(agentId:self.agentId, messageIds: nil as [Int]?)
if !success {
os_log("清除聊天历史失败", log: self.logger, type: .error)
}
@@ -1681,7 +1684,7 @@ class ChatApiStreamCallback: StreamCallback {
try agentService.addToHistoryMessages(assistantMessage)
}
// 保存聊天记录
- try agentService.saveChatMessage(userMessage: displayText, assistantMessage: response, metadata: metadata,userMetadata:toJSONString(userMetadata))
+ try agentService.saveChatMessage(sessionId: sessionId,userMessage: displayText, assistantMessage: response, metadata: metadata,userMetadata:toJSONString(userMetadata))
agentService.isAiStreaming = false
os_log("设置AI流式状态为false", log: agentService.logger, type: .info)
}catch{
@@ -2061,7 +2064,7 @@ class AudioPlayer {
}
extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
- func onResult(_ text: String, _ detectedLanguage: String) {
+ func onResult(sessionid:String ,_ text: String, _ detectedLanguage: String) {
if !text.isEmpty && isKeepResult {
var data: [String: Any] = ["text": text]
if !detectedLanguage.isEmpty {
@@ -2069,7 +2072,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
}
sendEvent(name: "recognition_result", data: data)
- processTextInput(text, speakResponse: true)
+ processTextInput(sessionid: sessionid,text, speakResponse: true)
}
let previousHasSpeech = hasSpeechDetected
@@ -2080,7 +2083,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
}
}
- func onRecognizing(_ recognizing: String, _ detectedLanguage: String) {
+ func onRecognizing(sessionid:String ,_ recognizing: String, _ detectedLanguage: String) {
if !recognizing.isEmpty && isKeepResult {
let previousHasSpeech = hasSpeechDetected
hasSpeechDetected = true
@@ -2102,7 +2105,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
}
}
- func onSessionStarted() {
+ func onSessionStarted(sessionid:String) {
sendEvent(name: "recognition_started", data: ["status": "started"])
isRecognizing = true
hasSpeechDetected = false
@@ -2123,7 +2126,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
}
}
- func onSessionStopped() {
+ func onSessionStopped(sessionid:String) {
sendEvent(name: "recognition_stopped", data: ["status": "stopped"])
isRecognizing = false
hasSpeechDetected = false
@@ -2133,7 +2136,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
}
}
- func onCanceled(_ reason: String, _ errorDetails: String) {
+ func onCanceled(sessionid:String ,_ reason: String, _ errorDetails: String) {
var data: [String: Any] = [:]
if !reason.isEmpty {
data["reason"] = reason
@@ -2150,7 +2153,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
stopIdleCheck()
}
- func onError(_ errorCode: Int, _ error: String) {
+ func onError(sessionid:String ,_ errorCode: Int, _ error: String) {
let data: [String: Any] = ["message": error.isEmpty ? "未知错误" : error]
sendEvent(name: "error", data: data)
isRecognizing = false
diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift
index e785c5192..08864a87f 100644
--- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift
+++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift
@@ -104,17 +104,19 @@ public class AgentServicePlugin: NSObject, FlutterPlugin {
return
case "processTextInput":
guard let arguments = call.arguments as? [String: Any],
+ let sessionid = arguments["sessionid"] as? String,
let text = arguments["text"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil))
return
}
let speakResponse = arguments["speakResponse"] as? Bool ?? false
- let success = impl.processTextInput(text, speakResponse: speakResponse)
+ let success = impl.processTextInput(sessionid: sessionid,text, speakResponse: speakResponse)
result(success)
case "processImageInput":
guard let arguments = call.arguments as? [String: Any],
+ let sessionid = arguments["sessionid"] as? String,
let imageUrl = arguments["imageUrl"] as? String,
let imagePath = arguments["imagePath"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "图片路径不能为空", details: nil))
@@ -123,7 +125,7 @@ public class AgentServicePlugin: NSObject, FlutterPlugin {
let text = arguments["text"] as? String ?? ""
let speakResponse = arguments["speakResponse"] as? Bool ?? false
- let success = impl.processImageInput(imagePath: imagePath,imageUrl: imageUrl, text: text, speakResponse: speakResponse)
+ let success = impl.processImageInput(sessionid: sessionid,imagePath: imagePath,imageUrl: imageUrl, text: text, speakResponse: speakResponse)
result(success)
case "speakText":
diff --git a/local_plugins/agent_service/lib/agent_service.dart b/local_plugins/agent_service/lib/agent_service.dart
index c2a051c5e..502c4c9f1 100644
--- a/local_plugins/agent_service/lib/agent_service.dart
+++ b/local_plugins/agent_service/lib/agent_service.dart
@@ -359,10 +359,11 @@ class AgentService {
/// [text] 文本内容
/// [speakResponse] 是否朗读响应
/// 返回是否成功处理文本
- static Future processTextInput(String text,
+ static Future processTextInput(String sessionid, String text,
{bool speakResponse = false}) async {
try {
final bool result = await _channel.invokeMethod('processTextInput', {
+ 'sessionid': sessionid,
'text': text,
'speakResponse': speakResponse,
});
@@ -440,10 +441,12 @@ class AgentService {
/// [imageUrl] 图片url
/// [speakResponse] 是否朗读响应
/// 返回是否成功处理图片
- static Future processImageInput(String imagePath, String imageUrl,
+ static Future processImageInput(
+ String sessionid, String imagePath, String imageUrl,
{String text = "", bool speakResponse = false}) async {
try {
final bool result = await _channel.invokeMethod('processImageInput', {
+ 'sessionid': sessionid,
'imagePath': imagePath,
'imageUrl': imageUrl,
'text': text,
diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
index 630abbdf3..7d43d01c3 100644
--- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
+++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
@@ -13,6 +13,7 @@ import java.util.concurrent.TimeUnit
import com.yunqiinnovation.azure_speech.tools.RecordFile
import com.microsoft.cognitiveservices.speech.audio.AudioConfig
import com.yunqiinnovation.azure_speech.tools.AudioRecordingForegroundService
+import java.util.UUID
/**
@@ -58,7 +59,7 @@ class AzureAsrHelper(private val context: Context) {
// 防抖机制相关变量
private var lastNetworkLostTime = 0L
private val networkLostDebounceInterval = 3000L // 2秒防抖间隔
-
+ private var currsessionid = "" //识别回话id 关联到整个聊天过程中
/**
@@ -242,7 +243,7 @@ class AzureAsrHelper(private val context: Context) {
// 检查网络状态
if (!checkNetworkStatus()) {
Log.e(tag, "网络不可用,无法开始语音识别")
- continuousCallback?.onError(1000,"网络连接不可用,请检查网络设置")
+ continuousCallback?.onError(currsessionid,1000,"网络连接不可用,请检查网络设置")
return false
}
@@ -258,7 +259,7 @@ class AzureAsrHelper(private val context: Context) {
Log.d(tag, "前台服务已启动")
} catch (e: Exception) {
Log.e(tag, "启动前台服务失败: ${e.message}")
- continuousCallback?.onError(1003, "启动前台服务失败: ${e.message}")
+ continuousCallback?.onError(currsessionid,1003, "启动前台服务失败: ${e.message}")
return false
}
}
@@ -268,7 +269,7 @@ class AzureAsrHelper(private val context: Context) {
val callback = continuousCallback
if (callback == null) {
Log.e(tag, "无法重新启动连续识别:回调为空")
- continuousCallback?.onError(1001,"回调为空")
+ continuousCallback?.onError(currsessionid,1001,"回调为空")
return false
}
setupEventListeners(callback)
@@ -308,7 +309,7 @@ class AzureAsrHelper(private val context: Context) {
fun setupEventListeners(callback: ContinuousRecognizeCallback): Boolean {
Log.d(tag, "设置ssssss监听器:${speechConfig ?: "null"} ")
if (speechConfig == null) {
- callback.onError(1002,"语音服务未初始化")
+ callback.onError(currsessionid,1002,"语音服务未初始化")
return false
}
@@ -330,7 +331,7 @@ class AzureAsrHelper(private val context: Context) {
} else {
currentLanguage
}
- callback.onRecognizing(event.result.text, detectedLanguage)
+ callback.onRecognizing(currsessionid,event.result.text, detectedLanguage)
}
}
)
@@ -345,7 +346,7 @@ class AzureAsrHelper(private val context: Context) {
} else {
currentLanguage
}
- callback.onResult(event.result.text, detectedLanguage)
+ callback.onResult(currsessionid,event.result.text, detectedLanguage)
}
}
)
@@ -355,7 +356,8 @@ class AzureAsrHelper(private val context: Context) {
EventHandler { _, _ ->
// 直接在当前线程调用回调
Log.d(tag, "会话开始事件")
- callback.onSessionStarted()
+ currsessionid = UUID.randomUUID().toString()
+ callback.onSessionStarted(currsessionid)
}
)
@@ -365,7 +367,7 @@ class AzureAsrHelper(private val context: Context) {
// 直接在当前线程调用回调
Log.d(tag, "会话结束事件")
if (audioSourceType == AudioSourceType.EXTERNAL) {
- callback.onSessionStopped()
+ callback.onSessionStopped(currsessionid)
audioStream?.isContinuousRecognitionActive = false
}
}
@@ -378,11 +380,11 @@ class AzureAsrHelper(private val context: Context) {
val reason = event.reason.toString()
Log.d(tag, "识别被取消: reason=$reason, details=$errorDetails")
- callback.onCanceled(reason,errorDetails)
+ callback.onCanceled(currsessionid,reason,errorDetails)
}
)
} catch (e: Exception) {
- callback.onError(1001,"启动连续识别失败: ${e.message}")
+ callback.onError(currsessionid,1001,"启动连续识别失败: ${e.message}")
return false
}
return true
@@ -495,7 +497,7 @@ class AzureAsrHelper(private val context: Context) {
// 确保状态被重置
audioStream.isContinuousRecognitionActive = false
-
+ currsessionid = ""
} catch (e: Exception) {
// 确保状态被重置
audioStream.isContinuousRecognitionActive = false
@@ -503,6 +505,7 @@ class AzureAsrHelper(private val context: Context) {
recognizer = null
speechConfig = null
isForegroundServiceRunning = false
+ currsessionid = ""
}
}
@@ -689,7 +692,7 @@ class AzureAsrHelper(private val context: Context) {
* @param text 识别的文本
* @param detectedLanguage 检测到的语言
*/
- fun onResult(text: String, detectedLanguage: String)
+ fun onResult(sessiond:String, text: String, detectedLanguage: String)
/**
* 识别进行中调用
@@ -697,17 +700,17 @@ class AzureAsrHelper(private val context: Context) {
* @param recognizing 正在识别的文本
* @param detectedLanguage 检测到的语言
*/
- fun onRecognizing(recognizing: String, detectedLanguage: String)
+ fun onRecognizing(sessiond:String, recognizing: String, detectedLanguage: String)
/**
* 会话开始时调用
*/
- fun onSessionStarted()
+ fun onSessionStarted(sessiond:String)
/**
* 会话结束时调用
*/
- fun onSessionStopped()
+ fun onSessionStopped(sessiond:String)
/**
@@ -716,14 +719,14 @@ class AzureAsrHelper(private val context: Context) {
* @param reason 取消原因
* @param errorDetails 错误详情
*/
- fun onCanceled(reason: String, errorDetails: String)
+ fun onCanceled(sessiond:String,reason: String, errorDetails: String)
/**
* 识别出错时调用
*
* @param error 错误信息
*/
- fun onError(code: Int,error: String)
+ fun onError(sessiond:String, code: Int,error: String)
}
@@ -781,7 +784,7 @@ class AzureAsrHelper(private val context: Context) {
lastNetworkLostTime = 0L
isNetworkRecovering = true
} else {
- continuousCallback?.onError(1000,"网络连接不可用,请检查网络设置")
+ continuousCallback?.onError(currsessionid,1000,"网络连接不可用,请检查网络设置")
Log.d(tag,"网络仍未恢复,继续检测")
networkLostCount = 0
stopAudioProcessingImmediately()
diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
index 1046f1979..ca31912cf 100644
--- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
+++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
@@ -47,6 +47,7 @@ public class AzureAsrHelper: NSObject {
private var isAudioStarted = false
private var pendingStopRequest = false
+ private var currsessionId = ""
// 配置参数
@@ -197,7 +198,7 @@ public class AzureAsrHelper: NSObject {
self.lastNetworkLostTime = 0
self.isNetworkRecovering = true
} else {
- self.continuousCallback?.onError(1000, "网络连接不可用,请检查网络设置")
+ self.continuousCallback?.onError(sessionid: currsessionId,1000, "网络连接不可用,请检查网络设置")
os_log("网络仍未恢复,继续检测", log: self.log, type: .debug)
self.networkLostCount = 0
self.stopContinuousRecognition()
@@ -780,7 +781,7 @@ public class AzureAsrHelper: NSObject {
print("正在识别事件=检测到语言: \(detectedLanguage), 识别中: \(text)")
DispatchQueue.main.async {
- callback.onRecognizing(result.text ?? "", detectedLanguage)
+ callback.onRecognizing(sessionid: self.currsessionId,result.text ?? "", detectedLanguage)
}
}
@@ -799,14 +800,15 @@ public class AzureAsrHelper: NSObject {
// 在主线程回调结果
DispatchQueue.main.async {
- callback.onResult(result.text ?? "", detectedLanguage)
+ callback.onResult(sessionid: self.currsessionId,result.text ?? "", detectedLanguage)
}
}
// 会话开始事件
recognizer.addSessionStartedEventHandler { (sender, event) in
// 直接在当前线程调用回调
- callback.onSessionStarted()
+ self.currsessionId = UUID().uuidString
+ callback.onSessionStarted(sessionid: self.currsessionId)
print("会话开始事件:")
}
@@ -815,7 +817,7 @@ public class AzureAsrHelper: NSObject {
guard let self = self else { return }
print("会话结束事件:")
// 直接在当前线程调用回调
- callback.onSessionStopped()
+ callback.onSessionStopped(sessionid: self.currsessionId)
self.audioStream?.isContinuousRecognitionActive = false
//self.stopAudioProcessing()
}
@@ -830,7 +832,7 @@ public class AzureAsrHelper: NSObject {
os_log("识别取消: %{public}@", log: self.log, type: .error, errorDetails)
- callback.onCanceled(reason, errorDetails)
+ callback.onCanceled(sessionid: self.currsessionId,reason, errorDetails)
}
return true
@@ -994,7 +996,7 @@ public class AzureAsrHelper: NSObject {
* @param text 识别的文本
* @param detectedLanguage 检测到的语言
*/
- func onResult(_ text: String, _ detectedLanguage: String)
+ func onResult(sessionid:String,_ text: String, _ detectedLanguage: String)
/**
* 识别进行中调用
@@ -1002,17 +1004,17 @@ public class AzureAsrHelper: NSObject {
* @param recognizing 正在识别的文本
* @param detectedLanguage 检测到的语言
*/
- func onRecognizing(_ recognizing: String, _ detectedLanguage: String)
+ func onRecognizing(sessionid:String ,_ recognizing: String, _ detectedLanguage: String)
/**
* 会话开始时调用
*/
- func onSessionStarted()
+ func onSessionStarted(sessionid:String)
/**
* 会话结束时调用
*/
- func onSessionStopped()
+ func onSessionStopped(sessionid:String)
/**
* 识别取消时调用
@@ -1020,14 +1022,14 @@ public class AzureAsrHelper: NSObject {
* @param reason 取消原因
* @param errorDetails 错误详情
*/
- func onCanceled(_ reason: String, _ errorDetails: String)
+ func onCanceled(sessionid:String ,_ reason: String, _ errorDetails: String)
/**
* 识别出错时调用
*
* @param error 错误信息
*/
- func onError(_ code: Int,_ error: String)
+ func onError(sessionid:String ,_ code: Int,_ error: String)
}
/**
* 检查网络状态
@@ -1039,7 +1041,7 @@ private func checkNetworkStatus() -> Bool {
if !isAvailable {
os_log("网络不可用,无法启动语音识别", log: log, type: .error)
- continuousCallback?.onError(1000,"网络连接不可用,请检查网络设置")
+ continuousCallback?.onError(sessionid: self.currsessionId,1000,"网络连接不可用,请检查网络设置")
}
return isAvailable
diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift
index 4a272ea12..05a6424d1 100644
--- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift
+++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift
@@ -1208,7 +1208,7 @@ private class AsrCallbackWrapper: AzureAsrHelper.ContinuousRecognizeCallback {
* @param text 识别的文本
* @param detectedLanguage 检测到的语言
*/
- func onResult(_ text: String, _ detectedLanguage: String) {
+ func onResult(sessionid:String,_ text: String, _ detectedLanguage: String) {
plugin?.sendAsrEvent([
"type": "result",
"text": text,
@@ -1221,7 +1221,7 @@ private class AsrCallbackWrapper: AzureAsrHelper.ContinuousRecognizeCallback {
* @param text 正在识别的文本
* @param detectedLanguage 检测到的语言
*/
- func onRecognizing(_ text: String, _ detectedLanguage: String) {
+ func onRecognizing(sessionid:String,_ text: String, _ detectedLanguage: String) {
plugin?.sendAsrEvent([
"type": "recognizing",
"text": text,
@@ -1232,7 +1232,7 @@ private class AsrCallbackWrapper: AzureAsrHelper.ContinuousRecognizeCallback {
/**
* 会话开始回调
*/
- func onSessionStarted() {
+ func onSessionStarted(sessionid:String) {
plugin?.sendAsrEvent([
"type": "sessionStarted"
])
@@ -1241,7 +1241,7 @@ private class AsrCallbackWrapper: AzureAsrHelper.ContinuousRecognizeCallback {
/**
* 会话结束回调
*/
- func onSessionStopped() {
+ func onSessionStopped(sessionid:String) {
plugin?.sendAsrEvent([
"type": "sessionStopped"
])
@@ -1252,7 +1252,7 @@ private class AsrCallbackWrapper: AzureAsrHelper.ContinuousRecognizeCallback {
* @param reason 取消原因
* @param errorDetails 错误详情
*/
- func onCanceled(_ reason: String, _ errorDetails: String) {
+ func onCanceled(sessionid:String,_ reason: String, _ errorDetails: String) {
plugin?.sendAsrEvent([
"type": "canceled",
"reason": reason,
@@ -1265,7 +1265,7 @@ private class AsrCallbackWrapper: AzureAsrHelper.ContinuousRecognizeCallback {
* @param errorCode 错误码
* @param error 错误信息
*/
- func onError(_ errorCode: Int, _ error: String) {
+ func onError(sessionid:String,_ errorCode: Int, _ error: String) {
plugin?.sendAsrEvent([
"type": "error",
"code": errorCode,
diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt
index d4a792234..6ab4db783 100644
--- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt
+++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt
@@ -164,16 +164,18 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
val oldSessionId = currSessionId
// 1. 取消当前的协程任务
- currentStreamJob?.cancel()
+ currentStreamJob?.cancel(CancellationException("New session started"))
currentStreamJob = null
// 2. 通知旧会话被中止
- getSessionCallback(oldSessionId)?.onComplete(currSessionId) // 直接完成当前会话
+ getSessionCallback(oldSessionId)?.onComplete(oldSessionId) // 直接完成当前会话
// 3. 清理状态
currSessionId = ""
sessionCallbacks.remove(oldSessionId)
toolCalls.clear()
+
+ Log.d(TAG, "会话 $oldSessionId 已被中止")
}
}
@@ -288,7 +290,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
// 检查是否包含图片,决定使用哪个模型
val currentModel = if (containsImage(messages)) visionModel else model
-
+
// 转换消息格式
val chatMessages = convertToChatMessages(messages)
@@ -352,10 +354,10 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
if (!isInitialized || apiKey.isEmpty() || openAI == null) {
Log.e("ChatApiService", "ChatAPI服务未初始化,无法发送消息")
try {
- getSessionCallback(sessionid)?.onError(sessionid,ChatApiException("ChatAPI服务未初始化"))
- } catch (e: Exception) {
- Log.e(TAG, "onError回调异常: ${e.message}", e)
- }
+ getSessionCallback(sessionid)?.onError(sessionid,ChatApiException("ChatAPI服务未初始化"))
+ } catch (e: Exception) {
+ Log.e(TAG, "onError回调异常: ${e.message}", e)
+ }
return
}
@@ -369,6 +371,8 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
try {
// 转换消息格式
val chatMessages = try {
+ // ✨ 第一步:上下文精简
+// val reducedMessages = reduce(messages)
convertToChatMessages(messages)
} catch (e: Exception) {
Log.e("ChatApiService", "转换消息格式失败: ${e.message}", e)
@@ -425,12 +429,10 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
}
var replyContext = "";
+ // 为每个会话新增一个状态标记:是否为中间工具调用(默认false)
+ var isIntermediateToolCall = false
// Log.d(TAG, "liwei-------------------------开始AI 对话 7")
chatsFlow.collect { result ->
-// val usage = result.usage
-// if (usage != null){ //发现流式不返回这歌数据
-// getSessionCallback(sessionid)?.onUsage(sessionid,usage.promptTokens,usage.completionTokens,usage.totalTokens)
-// }
// 每次处理前检查是否仍是当前会话
if (sessionid != currSessionId) {
// throw CancellationException("Session cancelled")
@@ -448,8 +450,22 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
Log.w("ChatApiService", "[Session: $sessionid] delta为null")
return@collect
}
- // Log.d(TAG, "liwei-------------------------开始AI 对话 7-3")
- if (delta.toolCalls == null){
+
+
+ // --------------------------
+ // 核心修改:第一帧判断是否为工具调用
+ // --------------------------
+ if (!isIntermediateToolCall) {
+ // 第一帧(或未标记时):检查是否有工具调用字段
+ val hasToolCalls = delta.toolCalls?.isNotEmpty() == true
+ val hasFunctionCall = delta.functionCall != null
+ if (hasToolCalls || hasFunctionCall) {
+ // 标记为中间工具调用,后续所有帧都拦截
+ isIntermediateToolCall = true
+ }
+ }
+ //中间过程不输出
+ if (!isIntermediateToolCall){
// 处理普通文本内容
delta.content?.let { content ->
try {
@@ -460,7 +476,15 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
// 不重新抛出异常,继续处理后续内容
}
}
- }
+ }
+ //else{
+ // delta.content?.let { content ->
+ // Log.d("ChatApiService", "liwei------------ [Session: $sessionid] 中间过程不输出 $content")
+ // }
+
+ // }
+
+
// Log.d(TAG, "liwei-------------------------开始AI 对话 7-4")
// 收集工具调用信息
delta.toolCalls?.forEach { toolCall ->
@@ -577,113 +601,131 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
"arguments" to firstToolCall.arguments,
"id" to firstToolCall.id
)
+
// 通知上层工具调用事件
try {
getSessionCallback(sessionid)?.onFunctionCall(sessionid,convertMapToJsonObject(functionCall))
} catch (e: Exception) {
Log.e(TAG, "onFunctionCall回调异常: ${e.message}", e)
}
+
// 在当前协程作用域内处理工具调用,使用async确保生命周期管理
val toolCallDeferred = async {
try {
- if (sessionid == currSessionId) {
- // 通过MCP客户端处理工具调用
- val functionName = firstToolCall.name
- val argumentsJson = firstToolCall.arguments
- var isError = false
- var errorText = ""
- val result = if (_mcpClient?.hasToolWithName(functionName) == true) {
- // 解析参数,如果参数为空JSON对象,使用空Map
- val arguments = if (argumentsJson == "{}") {
- emptyMap()
- } else {
- _mcpClient?.parseJsonArguments(argumentsJson) ?: emptyMap()
- }
- // 调用MCP工具
- val toolResult = try {
- withTimeout(60000) { // 60秒超时
- _mcpClient?.callTool(functionName, arguments)
+ // 检查会话是否仍然有效
+ if (sessionid != currSessionId) {
+ Log.d(TAG, "[Session: $sessionid] 会话已被替换,跳过工具调用")
+ return@async
+ }
+
+ // 通过MCP客户端处理工具调用
+ val functionName = firstToolCall.name
+ val argumentsJson = firstToolCall.arguments
+ var isError = false
+ var errorText = ""
+ val result = if (_mcpClient?.hasToolWithName(functionName) == true) {
+ // 解析参数,如果参数为空JSON对象,使用空Map
+ val arguments = if (argumentsJson == "{}") {
+ emptyMap()
+ } else {
+ _mcpClient?.parseJsonArguments(argumentsJson) ?: emptyMap()
+ }
+ // 调用MCP工具
+ val toolResult = try {
+ withTimeout(60000) { // 60秒超时
+ // 再次检查会话状态
+ if (sessionid != currSessionId) {
+ throw CancellationException("Session cancelled")
}
- } catch (e: TimeoutCancellationException) {
- Log.w("ChatApiService", "[Session: $sessionid] MCP工具调用超时: $functionName")
- mapOf(
- "content" to listOf(mapOf(
- "type" to "text",
- "text" to "Tool call timeout: $functionName"
- )),
- "isError" to true
- )
- } catch (e: Exception) {
- Log.e("ChatApiService", "[Session: $sessionid] MCP工具调用异常: $functionName", e)
- mapOf(
- "content" to listOf(mapOf(
- "type" to "text",
- "text" to "Tool call failed: ${e.message}"
- )),
- "isError" to true
- )
+ _mcpClient?.callTool(functionName, arguments)
}
- Log.d("ChatApiService", "[Session: $sessionid] AI调用工具结果: $functionName, 参数: $arguments, 结果: $toolResult")
- // 处理结果
- if (toolResult != null) {
- if (toolResult["isError"] == true) {
- // 处理错误情况
- isError = true
- val content = toolResult["content"] as? List<*>
- val firstContent = content?.firstOrNull() as? Map<*, *>
- errorText = firstContent?.get("text") as? String ?: "Tool execution failed"
- mapOf("context" to errorText)
- } else if (toolResult.containsKey("context")) {
- // 本地函数结果
- toolResult
- } else {
- // MCP工具结果
- val content = toolResult["content"] as? List<*>
- val firstContent = content?.firstOrNull() as? Map<*, *>
- val text = firstContent?.get("text") as? String ?: ""
- mapOf("context" to text)
- }
+ } catch (e: TimeoutCancellationException) {
+ Log.w("ChatApiService", "[Session: $sessionid] MCP工具调用超时: $functionName")
+ mapOf(
+ "content" to listOf(mapOf(
+ "type" to "text",
+ "text" to "Tool call timeout: $functionName"
+ )),
+ "isError" to true
+ )
+ } catch (e: CancellationException) {
+ Log.d("ChatApiService", "[Session: $sessionid] MCP工具调用被取消: $functionName")
+ // 不抛出异常,直接返回取消结果
+ mapOf(
+ "content" to listOf(mapOf(
+ "type" to "text",
+ "text" to "Tool call cancelled"
+ )),
+ "isError" to true
+ )
+ } catch (e: Exception) {
+ Log.e("ChatApiService", "[Session: $sessionid] MCP工具调用异常: $functionName", e)
+ mapOf(
+ "content" to listOf(mapOf(
+ "type" to "text",
+ "text" to "Tool call failed: ${e.message}"
+ )),
+ "isError" to true
+ )
+ }
+ Log.d("ChatApiService", "[Session: $sessionid] AI调用工具结果: $functionName, 参数: $arguments, 结果: $toolResult")
+ // 处理结果
+ if (toolResult != null) {
+ if (toolResult["isError"] == true) {
+ // 处理错误情况
+ isError = true
+ val content = toolResult["content"] as? List<*>
+ val firstContent = content?.firstOrNull() as? Map<*, *>
+ errorText = firstContent?.get("text") as? String ?: "Tool execution failed"
+ mapOf("context" to errorText)
+
+ } else if (toolResult.containsKey("context")) {
+ // 本地函数结果
+ toolResult
} else {
- Log.w("ChatApiService", "[Session: $sessionid] MCP工具调用返回null")
- mapOf("context" to "Tool call failed")
+ // MCP工具结果
+ val content = toolResult["content"] as? List<*>
+ val firstContent = content?.firstOrNull() as? Map<*, *>
+ val text = firstContent?.get("text") as? String ?: ""
+ mapOf("context" to text)
}
} else {
- // 工具不存在
- mapOf("context" to "Tool not found: $functionName")
+ Log.w("ChatApiService", "[Session: $sessionid] MCP工具调用返回null")
+ mapOf("context" to "Tool call failed")
}
- if (sessionid == currSessionId) {
-// if (functionName != "set_user_profile_field"){
- // 处理结果
- try {
- if(!isError){
- getSessionCallback(sessionid)?.onFunctionCallResult(
- sessionid,
- convertMapToJsonObject(functionCall),
- convertMapToJsonObject(result)
- )
- }else{
- getSessionCallback(sessionid)?.onError(sessionid, ChatApiException(errorText))
- }
- } catch (e: Exception) {
- Log.e(TAG, "onFunctionCallResult回调异常: ${e.message}", e)
- }
- // 将结果发送回OpenAI继续对话
- sendFunctionCallResultInternal(
- sessionid= sessionid,
- messages = currentMessages,
- functionCall = functionCall,
- functionResult = gson.toJson(result)
+ } else {
+ // 工具不存在
+ mapOf("context" to "Tool not found: $functionName")
+ }
+
+ // 最后检查会话状态
+ if (sessionid == currSessionId) {
+ try {
+ if(!isError){
+ getSessionCallback(sessionid)?.onFunctionCallResult(
+ sessionid,
+ convertMapToJsonObject(functionCall),
+ convertMapToJsonObject(result)
)
-// }else{
-// try {
-// getSessionCallback(sessionid)?.onComplete(sessionid)
-// } catch (ex: Exception) {
-// Log.e(TAG, "onComplete回调异常: ${ex.message}", ex)
-// }
-// clearCurrentSession()
-// }
+ }else{
+ getSessionCallback(sessionid)?.onError(sessionid, ChatApiException(errorText))
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "onFunctionCallResult回调异常: ${e.message}", e)
}
+ // 将结果发送回OpenAI继续对话
+ sendFunctionCallResultInternal(
+ sessionid= sessionid,
+ messages = currentMessages,
+ functionCall = functionCall,
+ functionResult = gson.toJson(result)
+ )
+ } else {
+ Log.d(TAG, "[Session: $sessionid] 工具调用完成时会话已被替换,跳过结果处理")
}
+ } catch (e: CancellationException) {
+ Log.d("ChatApiService", "[Session: $sessionid] 工具调用协程被取消")
+ // 不重新抛出CancellationException,避免传播
} catch (e: Exception) {
Log.e("ChatApiService", "工具调用处理过程中出错: ${e.message}", e)
if (sessionid == currSessionId) {
@@ -702,9 +744,10 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
try {
toolCallDeferred.await()
} catch (e: CancellationException) {
+ Log.d(TAG, "[Session: $sessionid] 工具调用被取消")
// 协程被取消,确保子任务也被取消
toolCallDeferred.cancel()
- throw e
+ // 不重新抛出异常,让流程正常结束
}
return true
@@ -1045,7 +1088,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
}
return false
}
-
+
/**
* 转换为OpenAI ChatMessage格式
*/
diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt
index d13fbc37a..b515dedb2 100644
--- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt
+++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt
@@ -30,9 +30,10 @@ class CustomSseClientTransport(
) : AbstractTransport() {
private val TAG = "CustomSseClientTransport"
- private val scope by lazy {
- CoroutineScope(session.coroutineContext + SupervisorJob())
- }
+ private val transportScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
+
+ // ✅ 为每个会话创建独立的作用域
+ private var sessionScope: CoroutineScope? = null
private val initialized = AtomicBoolean(false)
private var session: ClientSSESession by Delegates.notNull()
@@ -98,7 +99,7 @@ class CustomSseClientTransport(
* 收集SSE事件
*/
private suspend fun collectEvents() {
- job = scope.launch(CoroutineName("CustomSseMcpClientTransport.collect#${hashCode()}")) {
+ job = sessionScope?.launch(CoroutineName("CustomSseMcpClientTransport.collect#${hashCode()}")) {
try {
session.incoming.collect { event ->
Log.e(TAG, "$serviceidString event:${event.event}-${event.data}")
@@ -238,7 +239,7 @@ class CustomSseClientTransport(
* 启动连接监控,定期检查连接状态
*/
private fun startConnectionMonitor() {
- connectionMonitorJob = scope.launch {
+ connectionMonitorJob = transportScope.launch {
while (isActive && isConnected.get()) {
try {
delay(10000) // 每10秒检查一次
@@ -278,6 +279,8 @@ class CustomSseClientTransport(
queryParams = urlInfo.third
}
+ sessionScope = CoroutineScope(transportScope.coroutineContext + SupervisorJob())
+
// 创建SSE会话 - 直接使用原始URL
session = urlString?.let {
val sseConnectUrl = if (queryParams.isNotEmpty()) {
@@ -381,7 +384,8 @@ class CustomSseClientTransport(
_onClose()
job?.cancelAndJoin()
connectionMonitorJob?.cancelAndJoin()
-
+ sessionScope?.cancel()
+ transportScope.cancel()
Log.d(TAG, "$serviceidString CustomSseClientTransport已关闭")
}
}
\ No newline at end of file
diff --git a/local_plugins/chat_storage/android/src/main/kotlin/com/yunqiinnovation/chat_storage/ChatStorageHelper.kt b/local_plugins/chat_storage/android/src/main/kotlin/com/yunqiinnovation/chat_storage/ChatStorageHelper.kt
index 930662395..15def6f51 100644
--- a/local_plugins/chat_storage/android/src/main/kotlin/com/yunqiinnovation/chat_storage/ChatStorageHelper.kt
+++ b/local_plugins/chat_storage/android/src/main/kotlin/com/yunqiinnovation/chat_storage/ChatStorageHelper.kt
@@ -22,13 +22,14 @@ class ChatStorageHelper(context: Context) {
/**
* 保存消息
*
- * @param sessionId 会话ID
+ * @param agentId 会话ID
* @param message 消息内容
* @param sender 发送者("user" 或 "assistant")
* @param metadata 元数据(可选)
* @return 成功返回消息ID,失败返回-1
*/
fun saveMessage(
+ agentId: String,
sessionId: String,
message: String,
sender: String,
@@ -42,6 +43,7 @@ class ChatStorageHelper(context: Context) {
// 执行插入操作
val values = android.content.ContentValues().apply {
+ put(ChatDatabaseHelper.COLUMN_AGENT_ID, agentId)
put(ChatDatabaseHelper.COLUMN_SESSION_ID, sessionId)
put(ChatDatabaseHelper.COLUMN_MESSAGE, message)
put(ChatDatabaseHelper.COLUMN_SENDER, sender)
@@ -59,12 +61,12 @@ class ChatStorageHelper(context: Context) {
/**
* 获取指定会话的消息
*
- * @param sessionId 会话ID
+ * @param agentId 会话ID
* @param page 页码,从1开始
* @param pageSize 每页大小
* @return 包含消息列表和分页信息的JSON对象
*/
- fun getMessages(sessionId: String, page: Int = 1, pageSize: Int = 20): JSONObject {
+ fun getMessages(agentId: String, page: Int = 1, pageSize: Int = 20): JSONObject {
try {
val db = dbHelper.readableDatabase
val offset = (page - 1) * pageSize
@@ -72,19 +74,19 @@ class ChatStorageHelper(context: Context) {
// 构建查询语句
val query = """
SELECT * FROM ${ChatDatabaseHelper.TABLE_MESSAGES}
- WHERE ${ChatDatabaseHelper.COLUMN_SESSION_ID} = ?
+ WHERE ${ChatDatabaseHelper.COLUMN_AGENT_ID} = ?
ORDER BY ${ChatDatabaseHelper.COLUMN_TIMESTAMP} DESC
LIMIT ? OFFSET ?
""".trimIndent()
- val selectionArgs = arrayOf(sessionId, pageSize.toString(), offset.toString())
+ val selectionArgs = arrayOf(agentId, pageSize.toString(), offset.toString())
val cursor = db.rawQuery(query, selectionArgs)
val messages = cursorToJsonArray(cursor)
// 获取总记录数
- val countQuery = "SELECT COUNT(*) FROM ${ChatDatabaseHelper.TABLE_MESSAGES} WHERE ${ChatDatabaseHelper.COLUMN_SESSION_ID} = ?"
- val countCursor = db.rawQuery(countQuery, arrayOf(sessionId))
+ val countQuery = "SELECT COUNT(*) FROM ${ChatDatabaseHelper.TABLE_MESSAGES} WHERE ${ChatDatabaseHelper.COLUMN_AGENT_ID} = ?"
+ val countCursor = db.rawQuery(countQuery, arrayOf(agentId))
var totalCount = 0
if (countCursor.moveToFirst()) {
totalCount = countCursor.getInt(0)
@@ -125,24 +127,26 @@ class ChatStorageHelper(context: Context) {
// 查询不同的会话ID及其最新消息
val query = """
- SELECT s1.${ChatDatabaseHelper.COLUMN_SESSION_ID},
+ SELECT s1.${ChatDatabaseHelper.COLUMN_AGENT_ID},
+ s1.${ChatDatabaseHelper.COLUMN_SESSION_ID},
s1.${ChatDatabaseHelper.COLUMN_MESSAGE},
s1.${ChatDatabaseHelper.COLUMN_SENDER},
s1.${ChatDatabaseHelper.COLUMN_TIMESTAMP}
FROM ${ChatDatabaseHelper.TABLE_MESSAGES} s1
INNER JOIN (
- SELECT ${ChatDatabaseHelper.COLUMN_SESSION_ID},
+ SELECT ${ChatDatabaseHelper.COLUMN_AGENT_ID},
MAX(${ChatDatabaseHelper.COLUMN_TIMESTAMP}) as max_timestamp
FROM ${ChatDatabaseHelper.TABLE_MESSAGES}
- GROUP BY ${ChatDatabaseHelper.COLUMN_SESSION_ID}
- ) s2 ON s1.${ChatDatabaseHelper.COLUMN_SESSION_ID} = s2.${ChatDatabaseHelper.COLUMN_SESSION_ID}
+ GROUP BY ${ChatDatabaseHelper.COLUMN_AGENT_ID}
+ ) s2 ON s1.${ChatDatabaseHelper.COLUMN_AGENT_ID} = s2.${ChatDatabaseHelper.COLUMN_AGENT_ID}
AND s1.${ChatDatabaseHelper.COLUMN_TIMESTAMP} = s2.max_timestamp
ORDER BY s1.${ChatDatabaseHelper.COLUMN_TIMESTAMP} DESC
""".trimIndent()
val cursor = db.rawQuery(query, null)
val sessions = JSONArray()
-
+
+ val agentIdIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_AGENT_ID)
val sessionIdIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_SESSION_ID)
val messageIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_MESSAGE)
val senderIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_SENDER)
@@ -150,13 +154,14 @@ class ChatStorageHelper(context: Context) {
while (cursor.moveToNext()) {
val session = JSONObject().apply {
+ put("agentId", cursor.getString(agentIdIndex))
put("sessionId", cursor.getString(sessionIdIndex))
put("lastMessage", cursor.getString(messageIndex))
put("lastSender", cursor.getString(senderIndex))
put("timestamp", cursor.getString(timestampIndex))
// 获取会话中的消息数量
- val countQuery = "SELECT COUNT(*) FROM ${ChatDatabaseHelper.TABLE_MESSAGES} WHERE ${ChatDatabaseHelper.COLUMN_SESSION_ID} = ?"
+ val countQuery = "SELECT COUNT(*) FROM ${ChatDatabaseHelper.TABLE_MESSAGES} WHERE ${ChatDatabaseHelper.COLUMN_AGENT_ID} = ?"
val countCursor = db.rawQuery(countQuery, arrayOf(cursor.getString(sessionIdIndex)))
if (countCursor.moveToFirst()) {
put("messageCount", countCursor.getInt(0))
@@ -181,7 +186,7 @@ class ChatStorageHelper(context: Context) {
* @param messageIds 消息ID列表(可选,如果提供则删除指定消息)
* @return 成功返回true,失败返回false
*/
- fun deleteMessages(sessionId: String? = null, messageIds: List? = null): Boolean {
+ fun deleteMessages(agentId: String? = null, messageIds: List? = null): Boolean {
try {
val db = dbHelper.writableDatabase
@@ -191,10 +196,10 @@ class ChatStorageHelper(context: Context) {
val deleteQuery = "DELETE FROM ${ChatDatabaseHelper.TABLE_MESSAGES} WHERE ${ChatDatabaseHelper.COLUMN_ID} IN ($idsStr)"
db.execSQL(deleteQuery)
return true
- } else if (!sessionId.isNullOrEmpty()) {
+ } else if (!agentId.isNullOrEmpty()) {
// 删除会话中的所有消息
- val deleteQuery = "DELETE FROM ${ChatDatabaseHelper.TABLE_MESSAGES} WHERE ${ChatDatabaseHelper.COLUMN_SESSION_ID} = ?"
- db.execSQL(deleteQuery, arrayOf(sessionId))
+ val deleteQuery = "DELETE FROM ${ChatDatabaseHelper.TABLE_MESSAGES} WHERE ${ChatDatabaseHelper.COLUMN_AGENT_ID} = ?"
+ db.execSQL(deleteQuery, arrayOf(agentId))
return true
}
return false
@@ -223,11 +228,11 @@ class ChatStorageHelper(context: Context) {
/**
* 查询指定会话的最近N条消息
*
- * @param sessionId 会话ID
+ * @param agentId 会话ID
* @param limit 限制数量
* @return 消息列表,按时间正序排列
*/
- fun getRecentMessages(sessionId: String, limit: Int): List