Browse Source

上传优化优化代码

weicu
liwei1dao 1 year ago
parent
commit
585357acd8
  1. 16
      lib/modules/agent/controllers/agent_controller.dart
  2. 34
      lib/modules/agent/views/agent_view.dart
  3. 18
      lib/modules/agent/views/message_bubble.dart
  4. 51
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  5. 70
      local_plugins/chat_storage/ios/chat_storage/Sources/chat_storage/ChatStorageHelper.swift
  6. 2
      pubspec.yaml

16
lib/modules/agent/controllers/agent_controller.dart

@ -79,7 +79,8 @@ class AgentController extends GetxController with WidgetsBindingObserver {
final isTtsEnabled = true.obs; final isTtsEnabled = true.obs;
// TTS播报打断启用状态 // TTS播报打断启用状态
final isTtsInterruptEnabled = true.obs; final isTtsInterruptEnabled = true.obs;
// 是否显示工具调用
final isShowToolInterruptEnabled = false.obs;
// 图片处理状态 // 图片处理状态
final isImageProcessing = false.obs; final isImageProcessing = false.obs;
@ -272,6 +273,19 @@ class AgentController extends GetxController with WidgetsBindingObserver {
} }
} }
/// 切换TTS播报打断状态
/// 当启用播报打断时,用户说话会自动停止当前的TTS播放
Future<void> toggleShowAIToolInterrupt() async {
isShowToolInterruptEnabled.toggle();
// 保存偏好设置
try {
await AgentService.isInterrupt();
} catch (e) {
Logger.e(TAG, '保存TTS播报打断偏好设置失败: $e');
}
}
// 设置文本输入监听器 // 设置文本输入监听器
void _setupTextInputListener() { void _setupTextInputListener() {
textController.addListener(() { textController.addListener(() {

34
lib/modules/agent/views/agent_view.dart

@ -172,6 +172,39 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
), ),
color: isDarkMode ? Colors.grey[800] : Colors.white, color: isDarkMode ? Colors.grey[800] : Colors.white,
itemBuilder: (context) => [ itemBuilder: (context) => [
// 播报打断开关选项
PopupMenuItem(
child: Obx(() => Row(
children: [
Icon(
controller.isShowToolInterruptEnabled.value
? Icons.build_circle
: Icons.build_circle_outlined,
color: controller.isShowToolInterruptEnabled.value
? primaryColor
: (isDarkMode ? Colors.grey[400] : Colors.grey),
size: 20.sp,
),
SizedBox(width: 8.w),
Text(
//'ttsInterruptToggle'.tr,
"显示工具",
style: TextStyle(fontSize: 14.sp),
),
const Spacer(),
Switch(
value: controller.isShowToolInterruptEnabled.value,
onChanged: (value) =>
controller.toggleShowAIToolInterrupt(),
activeColor: primaryColor,
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
),
],
)),
onTap: () => controller.toggleTtsInterrupt(),
),
// 播报打断开关选项 // 播报打断开关选项
PopupMenuItem( PopupMenuItem(
child: Obx(() => Row( child: Obx(() => Row(
@ -204,6 +237,7 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
)), )),
onTap: () => controller.toggleTtsInterrupt(), onTap: () => controller.toggleTtsInterrupt(),
), ),
// 清除聊天记录选项 // 清除聊天记录选项
PopupMenuItem( PopupMenuItem(
child: Row( child: Row(

18
lib/modules/agent/views/message_bubble.dart

@ -84,7 +84,8 @@ class MessageBubble extends GetView<AgentController> {
], ],
), ),
child: Padding( child: Padding(
padding: (!isUser && padding: (controller.isShowToolInterruptEnabled.value &&
!isUser &&
functionCalls != null && functionCalls != null &&
functionCalls!.isNotEmpty) functionCalls!.isNotEmpty)
? const EdgeInsets.only(top: 30) ? const EdgeInsets.only(top: 30)
@ -93,7 +94,8 @@ class MessageBubble extends GetView<AgentController> {
), ),
), ),
//工具调用流程显示(仅在AI消息且有functionCalls时显示) //工具调用流程显示(仅在AI消息且有functionCalls时显示)
if (!isUser && if (controller.isShowToolInterruptEnabled.value &&
!isUser &&
functionCalls != null && functionCalls != null &&
functionCalls!.isNotEmpty) functionCalls!.isNotEmpty)
_buildFunctionCallsWidget(context), _buildFunctionCallsWidget(context),
@ -183,12 +185,20 @@ class MessageBubble extends GetView<AgentController> {
); );
} }
if (!isUser && message.isEmpty && functionCalls == null && meta == null) { if (!isUser &&
message.isEmpty &&
(functionCalls == null ||
!controller.isShowToolInterruptEnabled.value) &&
meta == null) {
return _buildTypingIndicator(); return _buildTypingIndicator();
} }
// 如果只有工具调用而没有文本内容,显示一个占位符确保气泡有足够宽度 // 如果只有工具调用而没有文本内容,显示一个占位符确保气泡有足够宽度
if (!isUser && message.isEmpty && functionCalls != null && meta == null) { if (!isUser &&
message.isEmpty &&
functionCalls != null &&
controller.isShowToolInterruptEnabled.value &&
meta == null) {
return Container( return Container(
constraints: const BoxConstraints( constraints: const BoxConstraints(
minWidth: 200, // 确保最小宽度 minWidth: 200, // 确保最小宽度

51
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift

@ -657,6 +657,15 @@ audioStream.saveAudioDataTo(data: audioData)
messages.append(userMessage) messages.append(userMessage)
os_log("添加用户消息", log: logger, type: .info) os_log("添加用户消息", log: logger, type: .info)
// 保存用户消息
self.chatStorageHelper.saveMessage(
agentId: self.agentId,
sessionId: sessionid,
message: displayText,
sender: "user",
metadata:""
)
addToHistoryMessages(chatApiService.createUserMessage(content: displayText)) addToHistoryMessages(chatApiService.createUserMessage(content: displayText))
currsessionId = sessionid currsessionId = sessionid
let callback = ChatApiStreamCallback( let callback = ChatApiStreamCallback(
@ -781,16 +790,9 @@ audioStream.saveAudioDataTo(data: audioData)
*/ */
internal func saveChatMessage(sessionId:String,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 { DispatchQueue.global(qos: .utility).async {
// 保存用户消息
let userMessageId = self.chatStorageHelper.saveMessage(
agentId: self.agentId,
sessionId: sessionId,
message: userMessage,
sender: "user",
metadata: userMetadata
)
if userMessageId != -1 {
// if userMessageId != -1 {
if !assistantMessage.isEmpty || !metadata.isEmpty { if !assistantMessage.isEmpty || !metadata.isEmpty {
// 保存AI回复 // 保存AI回复
let assistantMessageId = self.chatStorageHelper.saveMessage( let assistantMessageId = self.chatStorageHelper.saveMessage(
@ -804,9 +806,9 @@ audioStream.saveAudioDataTo(data: audioData)
os_log("保存助手消息失败", log: self.logger, type: .error) os_log("保存助手消息失败", log: self.logger, type: .error)
} }
} }
} else { // } else {
os_log("保存用户消息失败", log: self.logger, type: .error) // os_log("保存用户消息失败", log: self.logger, type: .error)
} // }
} }
} }
@ -817,6 +819,7 @@ internal func autoHandleFunctionCallResult(toolName: String, functionCallResult:
guard let contextValue = functionCallResult["context"] else { guard let contextValue = functionCallResult["context"] else {
return [ return [
"meta": "", "meta": "",
"iscard":false,
"broadcast": true, "broadcast": true,
"notreply": false, "notreply": false,
] ]
@ -884,9 +887,9 @@ private func handleJSONDictionary(_ jsonDict: [String: Any], toolName: String) -
metadata["isinternal"] = isinternal metadata["isinternal"] = isinternal
} else if let cardQQMusiclist = jsonDict["data"] as? [[String: Any]], toolName == "qqmusic_searchandplay_songs" { } else if let cardQQMusiclist = jsonDict["data"] as? [[String: Any]], toolName == "qqmusic_searchandplay_songs" {
processQQMusicPlayList(cardQQMusiclist) processQQMusicPlayList(cardQQMusiclist)
} else if toolName == "maps_weather" { } else if toolName == "map_weather" {
iscard = true iscard = true
}else if toolName == "map_weather" { }else if toolName == "maps_weather" {
iscard = true iscard = true
} }
metadata["iscard"] = iscard metadata["iscard"] = iscard
@ -900,6 +903,7 @@ private func handleJSONDictionary(_ jsonDict: [String: Any], toolName: String) -
return [ return [
"meta": metaStr, "meta": metaStr,
"iscard":iscard,
"broadcast": broadcast, "broadcast": broadcast,
"notreply": notreply "notreply": notreply
] ]
@ -1788,11 +1792,15 @@ class ChatApiStreamCallback: StreamCallback {
// agentService.audioPlayer?.stopCallingSound() // agentService.audioPlayer?.stopCallingSound()
let resultmap = agentService.autoHandleFunctionCallResult(toolName:name,functionCallResult:functionCallResult) let resultmap = agentService.autoHandleFunctionCallResult(toolName:name,functionCallResult:functionCallResult)
metadata = resultmap["meta"] as? String ?? ""
broadcast = resultmap["broadcast"] as? Bool ?? true broadcast = resultmap["broadcast"] as? Bool ?? true
reply = !(resultmap["notreply"] as? Bool ?? true) reply = !(resultmap["notreply"] as? Bool ?? true)
iscard = !(resultmap["iscard"] as? Bool ?? false) iscard = resultmap["iscard"] as? Bool ?? false
os_log("收到函数调用结果! broadcast: %{public}@ reply: %{public}@ %{iscard}@",
log: agentService.logger,
type: .info,
"\(broadcast)", "\(reply)", "\(iscard)")
if (iscard){ if (iscard){
metadata = resultmap["meta"] as? String ?? ""
agentService.sendEvent(name: "function_call_result", data: [ agentService.sendEvent(name: "function_call_result", data: [
"sessionid":sessionid, "sessionid":sessionid,
"function_name": name, "function_name": name,
@ -1800,10 +1808,13 @@ class ChatApiStreamCallback: StreamCallback {
"result": functionCallResult, "result": functionCallResult,
"meta":metadata, "meta":metadata,
]) ])
os_log("收到函数调用结果! broadcast: %{public}@ reply: %{public}@ %{metadata}@", }else{
log: agentService.logger, agentService.sendEvent(name: "function_call_result", data: [
type: .info, "sessionid":sessionid,
"\(broadcast)", "\(reply)", "\(metadata)") "function_name": name,
"function_call": functionCall,
"result": functionCallResult,
])
} }
}catch{ }catch{

70
local_plugins/chat_storage/ios/chat_storage/Sources/chat_storage/ChatStorageHelper.swift

@ -16,21 +16,21 @@ public class ChatStorageHelper {
private let logger = OSLog(subsystem: "com.yunqiinnovation.chat_storage", category: "ChatStorageHelper") private let logger = OSLog(subsystem: "com.yunqiinnovation.chat_storage", category: "ChatStorageHelper")
private init() { private init() {
// 获取文档目录路径 // 获取文档目录路径(不变)
let fileURL = try! FileManager.default let fileURL = try! FileManager.default
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) .url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
.appendingPathComponent("chat_storage.db") .appendingPathComponent("chat_storage.db")
dbPath = fileURL.path dbPath = fileURL.path
// 打开数据库 // 打开数据库(不变)
if sqlite3_open(dbPath, &db) != SQLITE_OK { if sqlite3_open(dbPath, &db) != SQLITE_OK {
let errmsg = String(cString: sqlite3_errmsg(db)!) let errmsg = String(cString: sqlite3_errmsg(db)!)
os_log("无法打开数据库: %{public}@", log: logger, type: .error, errmsg) os_log("无法打开数据库: %{public}@", log: logger, type: .error, errmsg)
return return
} }
// 创建消息表 // 关键修改:新增 UNIQUE (agent_id, session_id, sender) 联合唯一索引
let createTableString = """ let createTableString = """
CREATE TABLE IF NOT EXISTS messages ( CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -39,9 +39,10 @@ public class ChatStorageHelper {
message TEXT NOT NULL, message TEXT NOT NULL,
sender TEXT NOT NULL, sender TEXT NOT NULL,
metadata TEXT, metadata TEXT,
created_at INTEGER NOT NULL created_at INTEGER NOT NULL,
UNIQUE (agent_id, session_id, sender) ON CONFLICT REPLACE
); );
CREATE INDEX IF NOT EXISTS idx_agent_id ON messages (session_id); CREATE INDEX IF NOT EXISTS idx_agent_id ON messages (agent_id);
CREATE INDEX IF NOT EXISTS idx_session_id ON messages (session_id); CREATE INDEX IF NOT EXISTS idx_session_id ON messages (session_id);
CREATE INDEX IF NOT EXISTS idx_created_at ON messages (created_at); CREATE INDEX IF NOT EXISTS idx_created_at ON messages (created_at);
""" """
@ -66,38 +67,46 @@ public class ChatStorageHelper {
* @param metadata 可选的元数据 * @param metadata 可选的元数据
* @return 插入的消息ID,失败则返回-1 * @return 插入的消息ID,失败则返回-1
*/ */
public func saveMessage(agentId:String,sessionId: String, message: String, sender: String, metadata: String?) -> Int64 { public func saveMessage(agentId: String, sessionId: String, message: String, sender: String, metadata: String?) -> Int64 {
let insertStatementString = "INSERT INTO messages (agent_id,session_id, message, sender, metadata, created_at) VALUES (?, ?, ?, ?, ?, ?);" // 关键修改:用 INSERT OR REPLACE 替换 INSERT,支持冲突时更新
let insertStatementString = """
INSERT OR REPLACE INTO messages
(agent_id, session_id, message, sender, metadata, created_at)
VALUES (?, ?, ?, ?, ?, ?);
"""
var insertStatement: OpaquePointer? var insertStatement: OpaquePointer?
if sqlite3_prepare_v2(db, insertStatementString, -1, &insertStatement, nil) == SQLITE_OK { if sqlite3_prepare_v2(db, insertStatementString, -1, &insertStatement, nil) == SQLITE_OK {
// 绑定参数(逻辑不变,确保三个唯一字段正确传入)
sqlite3_bind_text(insertStatement, 1, (agentId as NSString).utf8String, -1, nil) sqlite3_bind_text(insertStatement, 1, (agentId as NSString).utf8String, -1, nil)
sqlite3_bind_text(insertStatement, 1, (sessionId as NSString).utf8String, -1, nil) sqlite3_bind_text(insertStatement, 2, (sessionId as NSString).utf8String, -1, nil)
sqlite3_bind_text(insertStatement, 2, (message as NSString).utf8String, -1, nil) sqlite3_bind_text(insertStatement, 3, (message as NSString).utf8String, -1, nil)
sqlite3_bind_text(insertStatement, 3, (sender as NSString).utf8String, -1, nil) sqlite3_bind_text(insertStatement, 4, (sender as NSString).utf8String, -1, nil)
if let metadata = metadata { if let metadata = metadata {
sqlite3_bind_text(insertStatement, 4, (metadata as NSString).utf8String, -1, nil) sqlite3_bind_text(insertStatement, 5, (metadata as NSString).utf8String, -1, nil)
} else { } else {
sqlite3_bind_null(insertStatement, 4) sqlite3_bind_null(insertStatement, 5)
} }
let currentTime = Int(Date().timeIntervalSince1970) let currentTime = Int(Date().timeIntervalSince1970)
sqlite3_bind_int(insertStatement, 5, Int32(currentTime)) sqlite3_bind_int(insertStatement, 6, Int32(currentTime))
// 执行语句(冲突时会自动替换,返回新的 rowid)
if sqlite3_step(insertStatement) == SQLITE_DONE { if sqlite3_step(insertStatement) == SQLITE_DONE {
let id = sqlite3_last_insert_rowid(db) let id = sqlite3_last_insert_rowid(db) // 替换后返回新的 id(原 id 会被删除)
sqlite3_finalize(insertStatement) sqlite3_finalize(insertStatement)
os_log("消息保存成功(新增/更新),id: %{public}lld", log: logger, type: .info, id)
return id return id
} else { } else {
let errmsg = String(cString: sqlite3_errmsg(db)!) let errmsg = String(cString: sqlite3_errmsg(db)!)
os_log("插入消息失败: %{public}@", log: logger, type: .error, errmsg) os_log("插入/更新消息失败: %{public}@", log: logger, type: .error, errmsg)
} }
sqlite3_finalize(insertStatement) sqlite3_finalize(insertStatement)
} else { } else {
let errmsg = String(cString: sqlite3_errmsg(db)!) let errmsg = String(cString: sqlite3_errmsg(db)!)
os_log("插入消息语句准备失败: %{public}@", log: logger, type: .error, errmsg) os_log("插入/更新消息语句准备失败: %{public}@", log: logger, type: .error, errmsg)
} }
return -1 return -1
@ -131,7 +140,7 @@ public class ChatStorageHelper {
let totalPages = totalCount == 0 ? 0 : (totalCount + pageSize - 1) / pageSize let totalPages = totalCount == 0 ? 0 : (totalCount + pageSize - 1) / pageSize
let queryString = """ let queryString = """
SELECT id, message, sender, metadata, created_at SELECT id, session_id, message, sender, metadata, created_at
FROM messages FROM messages
WHERE agent_id = ? WHERE agent_id = ?
ORDER BY created_at DESC ORDER BY created_at DESC
@ -148,16 +157,19 @@ public class ChatStorageHelper {
while sqlite3_step(queryStatement) == SQLITE_ROW { while sqlite3_step(queryStatement) == SQLITE_ROW {
let id = sqlite3_column_int(queryStatement, 0) let id = sqlite3_column_int(queryStatement, 0)
let messagePtr = sqlite3_column_text(queryStatement, 1) let sessionIdPtr = sqlite3_column_text(queryStatement, 1)
let sessionId = sessionIdPtr != nil ? String(cString: sessionIdPtr!) : ""
let messagePtr = sqlite3_column_text(queryStatement, 2)
let message = messagePtr != nil ? String(cString: messagePtr!) : "" let message = messagePtr != nil ? String(cString: messagePtr!) : ""
let senderPtr = sqlite3_column_text(queryStatement, 2) let senderPtr = sqlite3_column_text(queryStatement, 3)
let sender = senderPtr != nil ? String(cString: senderPtr!) : "" let sender = senderPtr != nil ? String(cString: senderPtr!) : ""
let metadataPtr = sqlite3_column_text(queryStatement, 3) let metadataPtr = sqlite3_column_text(queryStatement, 4)
let metadata = metadataPtr != nil ? String(cString: metadataPtr!) : nil let metadata = metadataPtr != nil ? String(cString: metadataPtr!) : nil
let createdAt = sqlite3_column_int(queryStatement, 4) let createdAt = sqlite3_column_int(queryStatement, 5)
// 将时间戳转换为ISO 8601格式的字符串 // 将时间戳转换为ISO 8601格式的字符串
let date = Date(timeIntervalSince1970: TimeInterval(createdAt)) let date = Date(timeIntervalSince1970: TimeInterval(createdAt))
@ -167,6 +179,7 @@ public class ChatStorageHelper {
var messageDict: [String: Any] = [ var messageDict: [String: Any] = [
"id": id, "id": id,
"agentId": agentId, // 添加agentId字段 "agentId": agentId, // 添加agentId字段
"sessionId": sessionId, // 添加sessionId字段
"message": message, "message": message,
"sender": sender, "sender": sender,
"timestamp": timestamp // 使用timestamp而不是created_at "timestamp": timestamp // 使用timestamp而不是created_at
@ -323,7 +336,7 @@ public class ChatStorageHelper {
// 构建查询语句 - 按时间倒序获取最近的N条,然后在结果中再按时间正序 // 构建查询语句 - 按时间倒序获取最近的N条,然后在结果中再按时间正序
let queryString = """ let queryString = """
SELECT * FROM ( SELECT * FROM (
SELECT id, message, sender, metadata, created_at SELECT id, session_id, message, sender, metadata, created_at
FROM messages FROM messages
WHERE agent_id = ? WHERE agent_id = ?
ORDER BY created_at DESC ORDER BY created_at DESC
@ -340,19 +353,24 @@ public class ChatStorageHelper {
while sqlite3_step(queryStatement) == SQLITE_ROW { while sqlite3_step(queryStatement) == SQLITE_ROW {
let id = sqlite3_column_int(queryStatement, 0) let id = sqlite3_column_int(queryStatement, 0)
let messagePtr = sqlite3_column_text(queryStatement, 1) let sessionIdPtr = sqlite3_column_text(queryStatement, 1)
let sessionId = sessionIdPtr != nil ? String(cString: sessionIdPtr!) : ""
let messagePtr = sqlite3_column_text(queryStatement, 2)
let message = messagePtr != nil ? String(cString: messagePtr!) : "" let message = messagePtr != nil ? String(cString: messagePtr!) : ""
let senderPtr = sqlite3_column_text(queryStatement, 2) let senderPtr = sqlite3_column_text(queryStatement, 3)
let sender = senderPtr != nil ? String(cString: senderPtr!) : "" let sender = senderPtr != nil ? String(cString: senderPtr!) : ""
let metadataPtr = sqlite3_column_text(queryStatement, 3) let metadataPtr = sqlite3_column_text(queryStatement, 4)
let metadata = metadataPtr != nil ? String(cString: metadataPtr!) : nil let metadata = metadataPtr != nil ? String(cString: metadataPtr!) : nil
let createdAt = sqlite3_column_int(queryStatement, 4) let createdAt = sqlite3_column_int(queryStatement, 5)
var messageDict: [String: Any] = [ var messageDict: [String: Any] = [
"id": id, "id": id,
"sessionId":sessionId,
"message": message, "message": message,
"sender": sender, "sender": sender,
"created_at": createdAt "created_at": createdAt

2
pubspec.yaml

@ -1,7 +1,7 @@
name: voitrans name: voitrans
description: "Voitrans - AI Voice Assistant." description: "Voitrans - AI Voice Assistant."
publish_to: "none" publish_to: "none"
version: 1.0.16+41 version: 1.0.17+46
environment: environment:
sdk: ">=3.3.0 <4.0.0" sdk: ">=3.3.0 <4.0.0"

Loading…
Cancel
Save