|
|
@ -99,7 +99,7 @@ object AgentService : CoroutineScope { |
|
|
4. 避免过长的列表,尽量将信息分成小段 |
|
|
4. 避免过长的列表,尽量将信息分成小段 |
|
|
5. 不要使用需要视觉展示的元素(如表格、图表或代码块) |
|
|
5. 不要使用需要视觉展示的元素(如表格、图表或代码块) |
|
|
6. 记住用户之前的对话内容,保持对话连贯 |
|
|
6. 记住用户之前的对话内容,保持对话连贯 |
|
|
|
|
|
7. 如果用户发送了图片,请根据图片内容和文字要求回答问题 |
|
|
你不仅可以回答知识性问题,还可以帮助用户设置提醒、提供建议,或进行轻松愉快的对话。 |
|
|
你不仅可以回答知识性问题,还可以帮助用户设置提醒、提供建议,或进行轻松愉快的对话。 |
|
|
无论遇到什么问题,都要尽力以温暖、贴心的语气提供最佳帮助。 |
|
|
无论遇到什么问题,都要尽力以温暖、贴心的语气提供最佳帮助。 |
|
|
""".trimIndent() |
|
|
""".trimIndent() |
|
|
@ -467,12 +467,6 @@ object AgentService : CoroutineScope { |
|
|
return false |
|
|
return false |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 发送与语音识别结果相同格式的事件 |
|
|
|
|
|
sendEvent("recognition_result", mapOf( |
|
|
|
|
|
"text" to text, |
|
|
|
|
|
"language" to "zh-CN", // 默认语言,未来可以从配置或检测中获取 |
|
|
|
|
|
"inputType" to "text" |
|
|
|
|
|
)) |
|
|
|
|
|
|
|
|
|
|
|
// 使用OpenAI处理文本 |
|
|
// 使用OpenAI处理文本 |
|
|
processWithOpenAI(text, speakResponse) |
|
|
processWithOpenAI(text, speakResponse) |
|
|
@ -499,6 +493,46 @@ object AgentService : CoroutineScope { |
|
|
) { |
|
|
) { |
|
|
FileLogger.d(TAG, "用户问题: $text") |
|
|
FileLogger.d(TAG, "用户问题: $text") |
|
|
|
|
|
|
|
|
|
|
|
// 创建用户文本消息并处理 |
|
|
|
|
|
val userMessage = openAIService.createUserMessage(text) |
|
|
|
|
|
processWithOpenAIInternal(userMessage, text, speakResponse) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 使用OpenAI处理图片 |
|
|
|
|
|
* |
|
|
|
|
|
* @param imageBase64 Base64编码的图片数据 |
|
|
|
|
|
* @param text 可选的文本描述或问题 |
|
|
|
|
|
* @param speakResponse 是否朗读回复 |
|
|
|
|
|
*/ |
|
|
|
|
|
private fun processImageWithOpenAI( |
|
|
|
|
|
imageBase64: String, |
|
|
|
|
|
text: String = "", |
|
|
|
|
|
speakResponse: Boolean = false |
|
|
|
|
|
) { |
|
|
|
|
|
FileLogger.d(TAG, "处理图片输入: ${if (text.isEmpty()) "无附加文本" else "附带文本: $text"}") |
|
|
|
|
|
|
|
|
|
|
|
// 创建带图片的用户消息并处理 |
|
|
|
|
|
val userMessage = openAIService.createUserMessageWithImage(text, imageBase64) |
|
|
|
|
|
// 图片描述用于存储 |
|
|
|
|
|
val displayText = text.ifEmpty { "[图片]" } |
|
|
|
|
|
processWithOpenAIInternal(userMessage, displayText, speakResponse, true) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 内部方法:通用的OpenAI处理逻辑 |
|
|
|
|
|
* |
|
|
|
|
|
* @param userMessage 用户消息(可以是文本或图片格式) |
|
|
|
|
|
* @param displayText 用于显示和存储的文本 |
|
|
|
|
|
* @param speakResponse 是否朗读回复 |
|
|
|
|
|
* @param hasImage 是否包含图片 |
|
|
|
|
|
*/ |
|
|
|
|
|
private fun processWithOpenAIInternal( |
|
|
|
|
|
userMessage: JSONObject, |
|
|
|
|
|
displayText: String, |
|
|
|
|
|
speakResponse: Boolean = true, |
|
|
|
|
|
hasImage: Boolean = false |
|
|
|
|
|
) { |
|
|
// 如果有正在进行的AI流式输出,先停止它 |
|
|
// 如果有正在进行的AI流式输出,先停止它 |
|
|
stopAiStream() |
|
|
stopAiStream() |
|
|
|
|
|
|
|
|
@ -508,12 +542,7 @@ object AgentService : CoroutineScope { |
|
|
// 设置状态为正在流式输出 |
|
|
// 设置状态为正在流式输出 |
|
|
isAiStreaming = true |
|
|
isAiStreaming = true |
|
|
|
|
|
|
|
|
// 创建用户消息 |
|
|
|
|
|
val userMessage = openAIService.createUserMessage(text) |
|
|
|
|
|
|
|
|
|
|
|
// 将用户消息添加到历史记录 |
|
|
|
|
|
addToHistoryMessages(userMessage) |
|
|
|
|
|
|
|
|
|
|
|
// 使用历史记录作为上下文发送到OpenAI |
|
|
// 使用历史记录作为上下文发送到OpenAI |
|
|
val responseBuilder = StringBuilder() |
|
|
val responseBuilder = StringBuilder() |
|
|
|
|
|
|
|
|
@ -529,6 +558,36 @@ object AgentService : CoroutineScope { |
|
|
for (i in 0 until historyMessages.length()) { |
|
|
for (i in 0 until historyMessages.length()) { |
|
|
messagesWithSystemPrompt.put(historyMessages.getJSONObject(i)) |
|
|
messagesWithSystemPrompt.put(historyMessages.getJSONObject(i)) |
|
|
} |
|
|
} |
|
|
|
|
|
messagesWithSystemPrompt.put(userMessage) |
|
|
|
|
|
|
|
|
|
|
|
// 将用户消息添加到历史记录(注意:要去掉图片数据再保存) |
|
|
|
|
|
if (userMessage.has("content")) { |
|
|
|
|
|
val content = userMessage.get("content") |
|
|
|
|
|
// 检查是否是带图片的消息(即content是JSONArray而不是String) |
|
|
|
|
|
if (content is JSONArray) { |
|
|
|
|
|
// 提取文本内容 |
|
|
|
|
|
var textContent = "" |
|
|
|
|
|
for (i in 0 until content.length()) { |
|
|
|
|
|
val item = content.getJSONObject(i) |
|
|
|
|
|
if (item.getString("type") == "text") { |
|
|
|
|
|
textContent = item.getString("text") |
|
|
|
|
|
break |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
// 创建新的只含文本的消息 |
|
|
|
|
|
val textOnlyMessage = JSONObject(userMessage.toString()) |
|
|
|
|
|
textOnlyMessage.put("content", textContent.ifEmpty { "[图片]" }) |
|
|
|
|
|
// 添加到历史记录 |
|
|
|
|
|
addToHistoryMessages(textOnlyMessage) |
|
|
|
|
|
} else { |
|
|
|
|
|
// 普通文本消息,直接添加 |
|
|
|
|
|
addToHistoryMessages(userMessage) |
|
|
|
|
|
} |
|
|
|
|
|
} else { |
|
|
|
|
|
// 兜底处理,直接添加原消息 |
|
|
|
|
|
addToHistoryMessages(userMessage) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
openAIService.sendMessageStream( |
|
|
openAIService.sendMessageStream( |
|
|
messages = messagesWithSystemPrompt, |
|
|
messages = messagesWithSystemPrompt, |
|
|
@ -548,20 +607,23 @@ object AgentService : CoroutineScope { |
|
|
azureTtsHelper?.flushStream() |
|
|
azureTtsHelper?.flushStream() |
|
|
} |
|
|
} |
|
|
val response = responseBuilder.toString() |
|
|
val response = responseBuilder.toString() |
|
|
// FileLogger.d(TAG, "AI完整回复: $response, $speakResponse") |
|
|
|
|
|
|
|
|
|
|
|
if (response.isNotEmpty()) { |
|
|
if (response.isNotEmpty()) { |
|
|
// 发送完整回复 |
|
|
// 发送完整回复,包含是否有图片的标记 |
|
|
sendEvent("assistant_response", mapOf( |
|
|
val responseData = mutableMapOf<String, Any>( |
|
|
"text" to response, |
|
|
"text" to response, |
|
|
"userInput" to text |
|
|
"userInput" to displayText |
|
|
)) |
|
|
) |
|
|
|
|
|
if (hasImage) { |
|
|
|
|
|
responseData["hasImage"] = true |
|
|
|
|
|
} |
|
|
|
|
|
sendEvent("assistant_response", responseData) |
|
|
|
|
|
|
|
|
// 添加AI回复到历史记录 |
|
|
// 添加AI回复到历史记录 |
|
|
addToHistoryMessages(openAIService.createAssistantMessage(response)) |
|
|
addToHistoryMessages(openAIService.createAssistantMessage(response)) |
|
|
|
|
|
|
|
|
// 保存聊天记录 |
|
|
// 保存聊天记录 |
|
|
saveChatMessage(text, response) |
|
|
saveChatMessage(displayText, response) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 标记AI流式输出已完成 |
|
|
// 标记AI流式输出已完成 |
|
|
@ -602,9 +664,10 @@ object AgentService : CoroutineScope { |
|
|
) |
|
|
) |
|
|
|
|
|
|
|
|
} catch (e: Exception) { |
|
|
} catch (e: Exception) { |
|
|
|
|
|
val errorType = if (hasImage) "AI_IMAGE_PROCESS_ERROR" else "AI_PROCESS_ERROR" |
|
|
FileLogger.e(TAG, "AI处理出错", e) |
|
|
FileLogger.e(TAG, "AI处理出错", e) |
|
|
sendEvent("error", mapOf( |
|
|
sendEvent("error", mapOf( |
|
|
"code" to "AI_PROCESS_ERROR", |
|
|
"code" to errorType, |
|
|
"message" to e.message.toString() |
|
|
"message" to e.message.toString() |
|
|
)) |
|
|
)) |
|
|
|
|
|
|
|
|
@ -712,11 +775,11 @@ object AgentService : CoroutineScope { |
|
|
sender = "assistant" |
|
|
sender = "assistant" |
|
|
) |
|
|
) |
|
|
|
|
|
|
|
|
if (assistantMessageId != -1L) { |
|
|
// if (assistantMessageId != -1L) { |
|
|
FileLogger.d(TAG, "聊天记录已保存:用户消息ID=$userMessageId, 助手消息ID=$assistantMessageId") |
|
|
// FileLogger.d(TAG, "聊天记录已保存:用户消息ID=$userMessageId, 助手消息ID=$assistantMessageId") |
|
|
} else { |
|
|
// } else { |
|
|
FileLogger.e(TAG, "保存助手消息失败") |
|
|
// FileLogger.e(TAG, "保存助手消息失败") |
|
|
} |
|
|
// } |
|
|
} else { |
|
|
} else { |
|
|
FileLogger.e(TAG, "保存用户消息失败") |
|
|
FileLogger.e(TAG, "保存用户消息失败") |
|
|
} |
|
|
} |
|
|
@ -803,4 +866,73 @@ object AgentService : CoroutineScope { |
|
|
val result = context.checkCallingOrSelfPermission(permission) |
|
|
val result = context.checkCallingOrSelfPermission(permission) |
|
|
return result == android.content.pm.PackageManager.PERMISSION_GRANTED |
|
|
return result == android.content.pm.PackageManager.PERMISSION_GRANTED |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 处理图片输入 |
|
|
|
|
|
* 将图片与文本一起发送给AI进行处理 |
|
|
|
|
|
* |
|
|
|
|
|
* @param imagePath 图片文件路径 |
|
|
|
|
|
* @param text 可选的文本描述或问题,默认为空 |
|
|
|
|
|
* @param speakResponse 是否朗读回复,默认为false |
|
|
|
|
|
* @return 是否成功开始处理 |
|
|
|
|
|
*/ |
|
|
|
|
|
fun processImageInput(imagePath: String, text: String = "", speakResponse: Boolean = false): Boolean { |
|
|
|
|
|
if (!isInitialized) { |
|
|
|
|
|
FileLogger.e(TAG, "服务未初始化") |
|
|
|
|
|
sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化")) |
|
|
|
|
|
return false |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (imagePath.isEmpty()) { |
|
|
|
|
|
FileLogger.e(TAG, "图片路径不能为空") |
|
|
|
|
|
sendEvent("error", mapOf("code" to "EMPTY_IMAGE_PATH", "message" to "图片路径不能为空")) |
|
|
|
|
|
return false |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 通知开始处理图片 |
|
|
|
|
|
sendEvent("image_processing", mapOf( |
|
|
|
|
|
"status" to "processing", |
|
|
|
|
|
"imagePath" to imagePath |
|
|
|
|
|
)) |
|
|
|
|
|
|
|
|
|
|
|
// 使用协程处理耗时的图片转换操作 |
|
|
|
|
|
launch(Dispatchers.IO) { |
|
|
|
|
|
try { |
|
|
|
|
|
// 将图片转换为Base64格式 |
|
|
|
|
|
val imageBase64 = openAIService.fileToBase64(imagePath) |
|
|
|
|
|
|
|
|
|
|
|
if (imageBase64 == null) { |
|
|
|
|
|
withContext(Dispatchers.Main) { |
|
|
|
|
|
FileLogger.e(TAG, "图片转换失败: $imagePath") |
|
|
|
|
|
sendEvent("error", mapOf( |
|
|
|
|
|
"code" to "IMAGE_CONVERSION_FAILED", |
|
|
|
|
|
"message" to "图片转换失败" |
|
|
|
|
|
)) |
|
|
|
|
|
} |
|
|
|
|
|
return@launch |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 通知图片准备完成 |
|
|
|
|
|
withContext(Dispatchers.Main) { |
|
|
|
|
|
sendEvent("image_ready", mapOf( |
|
|
|
|
|
"status" to "ready", |
|
|
|
|
|
"imagePath" to imagePath |
|
|
|
|
|
)) |
|
|
|
|
|
|
|
|
|
|
|
// 使用OpenAI处理包含图片的消息 |
|
|
|
|
|
processImageWithOpenAI(imageBase64, text, speakResponse) |
|
|
|
|
|
} |
|
|
|
|
|
} catch (e: Exception) { |
|
|
|
|
|
withContext(Dispatchers.Main) { |
|
|
|
|
|
FileLogger.e(TAG, "处理图片输入失败", e) |
|
|
|
|
|
sendEvent("error", mapOf( |
|
|
|
|
|
"code" to "IMAGE_PROCESSING_ERROR", |
|
|
|
|
|
"message" to e.message.toString() |
|
|
|
|
|
)) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return true |
|
|
|
|
|
} |
|
|
} |
|
|
} |