Browse Source

add

newdev_shunjiawei
wolfplus 1 year ago
parent
commit
8e3a474201
  1. 4
      android/settings.gradle.kts
  2. 4
      ios/Runner/Info.plist
  3. 2
      local_plugins/agent_service/android/build.gradle.kts
  4. 129
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  5. 13
      local_plugins/chat_api/README.md
  6. 70
      local_plugins/chat_api/android/README.md
  7. 50
      local_plugins/chat_api/android/build.gradle.kts
  8. 1
      local_plugins/chat_api/android/settings.gradle.kts
  9. 8
      local_plugins/chat_api/android/src/main/AndroidManifest.xml
  10. 252
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiPlugin.kt
  11. 799
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt
  12. 42
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift
  13. 3
      local_plugins/chat_api/pubspec.yaml

4
android/settings.gradle.kts

@ -39,7 +39,7 @@ include(":ble_service")
include(":classic_bluetooth")
include(":agent_service")
include(":speech")
include(":open_ai_service")
include(":chat_api")
// 设置azure_speech项目的路径
project(":azure_speech").projectDir = file("../local_plugins/azure_speech/android")
@ -50,4 +50,4 @@ project(":ble_service").projectDir = file("../local_plugins/ble_service/android"
project(":classic_bluetooth").projectDir = file("../local_plugins/classic_bluetooth/android")
project(":agent_service").projectDir = file("../local_plugins/agent_service/android")
project(":speech").projectDir = file("../local_plugins/speech/android")
project(":open_ai_service").projectDir = file("../local_plugins/open_ai_service/android")
project(":chat_api").projectDir = file("../local_plugins/chat_api/android")

4
ios/Runner/Info.plist

@ -123,9 +123,9 @@
<key>NSBluetoothPeripheralUsageDescription</key>
<string>需要蓝牙权限用于连接和管理音频设备</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>需要位置权限用于搜索附近的蓝牙设备</string>
<string>语音助手需要位置权限来为您提供基于位置的信息和服务</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>需要位置权限用于搜索附近的蓝牙设备</string>
<string>语音助手需要位置权限来为您提供基于位置的信息和服务</string>
<key>NSMicrophoneUsageDescription</key>
<string>需要麦克风权限用于语音识别和录音功能</string>
<key>NSCalendarsUsageDescription</key>

2
local_plugins/agent_service/android/build.gradle.kts

@ -48,5 +48,5 @@ dependencies {
add("compileOnly", project(":music_service"))
add("compileOnly", project(":amap_service"))
add("compileOnly", project(":speech"))
add("compileOnly", project(":open_ai_service"))
add("compileOnly", project(":chat_api"))
}

129
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt

@ -4,7 +4,8 @@ import android.content.Context
import org.json.JSONArray
import org.json.JSONObject
import com.yunqiinnovation.azure_speech.AzureAsrHelper
import com.yunqiinnovation.open_ai_service.OpenAIService
import com.yunqiinnovation.chat_api.ChatApiService
import com.yunqiinnovation.chat_api.StreamCallback
import com.yunqiinnovation.chat_storage.ChatStorageHelper
import com.yunqiinnovation.music_service.MusicServiceStarter
import com.yunqiinnovation.amap_service.NavigationServiceHelper
@ -53,8 +54,8 @@ object AgentService : CoroutineScope {
// 使用AzureTtsHelper作为唯一的TTS实现
private var ttsService: AzureTtsHelper? = null
// OpenAI服务 - 修改为使用OpenAIService
private lateinit var openAIService: OpenAIService
// ChatAPI服务 - 使用新的ChatApiService
private lateinit var chatApiService: ChatApiService
// 聊天存储服务
private lateinit var chatStorageHelper: ChatStorageHelper
@ -121,7 +122,7 @@ object AgentService : CoroutineScope {
try {
// 初始化上下文和服务
this.context = appContext.applicationContext
this.openAIService = OpenAIService(context)
this.chatApiService = ChatApiService(context)
this.chatStorageHelper = ChatStorageHelper(context)
this.audioPlayer = AudioPlayer(context)
@ -146,8 +147,8 @@ object AgentService : CoroutineScope {
// 初始化TTS服务
initializeTtsEngine(config)
// 初始化OpenAI服务
openAIService.initialize(
// 初始化ChatAPI服务
chatApiService.initialize(
config["openaiApiKey"]?.toString() ?: "",
config["openaiBaseUrl"]?.toString() ?: "",
config["openaiModel"]?.toString() ?: "",
@ -181,9 +182,9 @@ object AgentService : CoroutineScope {
azureAsrHelper?.dispose()
// 释放OpenAI服务
if (::openAIService.isInitialized) {
openAIService.cancelAll()
// 释放ChatAPI服务
if (::chatApiService.isInitialized) {
chatApiService.cancelCurrentStream()
}
// 释放TTS服务
@ -401,7 +402,7 @@ object AgentService : CoroutineScope {
"language" to detectedLanguage
))
processWithOpenAI(result)
processWithChatApi(result)
}
// 重置状态,继续识别
@ -513,8 +514,8 @@ object AgentService : CoroutineScope {
currentAiJob?.cancel()
currentAiJob = null
// 通知OpenAI服务终止当前流式请求
openAIService.cancelCurrentStream()
// 通知ChatAPI服务终止当前流式请求
chatApiService.cancelCurrentStream()
// 记录日志
Log.d(TAG, "AI流输出已停止")
@ -543,43 +544,43 @@ object AgentService : CoroutineScope {
return false
}
// 使用OpenAI处理文本
processWithOpenAI(text, speakResponse)
// 使用ChatAPI处理文本
processWithChatApi(text, speakResponse)
return true
}
/**
* 使用OpenAI处理语音识别结果
* 使用ChatAPI处理语音识别结果
*/
private fun processWithOpenAI(text: String) {
processWithOpenAI(text, true)
private fun processWithChatApi(text: String) {
processWithChatApi(text, true)
}
/**
* 使用OpenAI处理文本消息
* 使用ChatAPI处理文本消息
*
* @param text 用户输入文本
* @param speakResponse 是否使用TTS朗读回复
*/
private fun processWithOpenAI(
private fun processWithChatApi(
text: String,
speakResponse: Boolean = true
) {
Log.d(TAG, "用户问题: $text")
// 创建用户文本消息并处理
val userMessage = openAIService.createUserMessage(text)
processWithOpenAIInternal(userMessage, text, speakResponse)
val userMessage = createUserMessage(text)
processWithChatApiInternal(userMessage, text, speakResponse)
}
/**
* 使用OpenAI处理图片
* 使用ChatAPI处理图片
*
* @param imageBase64 Base64编码的图片数据
* @param text 可选的文本描述或问题
* @param speakResponse 是否朗读回复
*/
private fun processImageWithOpenAI(
private fun processImageWithChatApi(
imageBase64: String,
text: String = "",
speakResponse: Boolean = false
@ -587,14 +588,14 @@ object AgentService : CoroutineScope {
Log.d(TAG, "处理图片输入: ${if (text.isEmpty()) "无附加文本" else "附带文本: $text"}")
// 创建带图片的用户消息并处理
val userMessage = openAIService.createUserMessageWithImage(text, imageBase64)
val userMessage = createUserMessageWithImage(text, imageBase64)
// 图片描述用于存储
val displayText = text.ifEmpty { "[图片]" }
processWithOpenAIInternal(userMessage, displayText, speakResponse, true)
processWithChatApiInternal(userMessage, displayText, speakResponse, true)
}
/**
* 内部方法:通用的OpenAI处理逻辑
* 内部方法:通用的ChatAPI处理逻辑
*
* @param userMessage 用户消息(可以是文本或图片格式)
* @param displayText 用于显示和存储的文本
@ -602,7 +603,7 @@ object AgentService : CoroutineScope {
* @param hasImage 是否包含图片
* @param imagePath 图片路径(如果有)
*/
private fun processWithOpenAIInternal(
private fun processWithChatApiInternal(
userMessage: JSONObject,
displayText: String,
speakResponse: Boolean = true,
@ -634,7 +635,7 @@ object AgentService : CoroutineScope {
// 先添加系统提示词
if (systemPrompt.isNotEmpty()) {
messagesWithSystemPrompt.put(openAIService.createSystemMessage(systemPrompt))
messagesWithSystemPrompt.put(createSystemMessage(systemPrompt))
// Log.d(TAG, "系统提示词: ${systemPrompt}")
}
@ -672,9 +673,9 @@ object AgentService : CoroutineScope {
addToHistoryMessages(userMessage)
}
openAIService.sendMessageStream(
chatApiService.sendMessageStream(
messagesWithSystemPrompt,
object : OpenAIService.StreamCallback {
object : StreamCallback {
override fun onToken(token: String) {
responseBuilder.append(token)
if (speakResponse && !nobroadcast) {
@ -704,7 +705,7 @@ object AgentService : CoroutineScope {
sendEvent("assistant_response", responseData)
// 添加AI回复到历史记录
addToHistoryMessages(openAIService.createAssistantMessage(response))
addToHistoryMessages(createAssistantMessage(response))
// 保存聊天记录
saveChatMessage(displayText, response,aiMetadata,userMetadata.toString())
@ -796,9 +797,9 @@ object AgentService : CoroutineScope {
val content = message["message"] as String
if (sender == "user") {
addToHistoryMessages(openAIService.createUserMessage(content))
addToHistoryMessages(createUserMessage(content))
} else if (sender == "assistant") {
addToHistoryMessages(openAIService.createAssistantMessage(content))
addToHistoryMessages(createAssistantMessage(content))
}
}
@ -945,6 +946,62 @@ object AgentService : CoroutineScope {
return result == android.content.pm.PackageManager.PERMISSION_GRANTED
}
/**
* 创建用户消息
*/
private fun createUserMessage(content: String): JSONObject {
return JSONObject().apply {
put("role", "user")
put("content", content)
}
}
/**
* 创建系统消息
*/
private fun createSystemMessage(content: String): JSONObject {
return JSONObject().apply {
put("role", "system")
put("content", content)
}
}
/**
* 创建助手消息
*/
private fun createAssistantMessage(content: String): JSONObject {
return JSONObject().apply {
put("role", "assistant")
put("content", content)
}
}
/**
* 创建带图片的用户消息
*/
private fun createUserMessageWithImage(text: String, imageBase64: String): JSONObject {
val content = JSONArray().apply {
if (text.isNotEmpty()) {
put(JSONObject().apply {
put("type", "text")
put("text", text)
})
}
put(JSONObject().apply {
put("type", "image_url")
put("image_url", JSONObject().apply {
put("url", "data:image/jpeg;base64,$imageBase64")
put("detail", "auto")
})
})
}
return JSONObject().apply {
put("role", "user")
put("content", content)
}
}
/**
* 处理图片输入
*/
@ -971,7 +1028,7 @@ object AgentService : CoroutineScope {
launch {
try {
// 将图片转换为Base64格式
val imageBase64 = openAIService.fileToBase64(imagePath) ?: run {
val imageBase64 = chatApiService.fileToBase64(imagePath) ?: run {
sendEvent("error", mapOf(
"code" to "IMAGE_CONVERSION_FAILED",
"message" to "图片转换失败"
@ -980,7 +1037,7 @@ object AgentService : CoroutineScope {
}
// 创建带图片的用户消息并处理
val userMessage = openAIService.createUserMessageWithImage(text, imageBase64)
val userMessage = createUserMessageWithImage(text, imageBase64)
// 图片描述用于存储
val displayText = text.ifEmpty { "[图片]" }
@ -991,7 +1048,7 @@ object AgentService : CoroutineScope {
))
// 处理消息并传入存储路径
processWithOpenAIInternal(userMessage, displayText, speakResponse, true, imagePath)
processWithChatApiInternal(userMessage, displayText, speakResponse, true, imagePath)
} catch (e: Exception) {
Log.e(TAG, "处理图片失败: ${e.message}")
sendEvent("error", mapOf(

13
local_plugins/chat_api/README.md

@ -15,6 +15,7 @@
## 平台支持
- iOS 16.0+
- Android (API 21+)
## MCP 功能
@ -138,6 +139,7 @@ chatApi.eventStream.listen((event) {
## 架构
### iOS
```
ChatApiService (Flutter层)
↓
@ -150,3 +152,14 @@ ChatApiService (iOS原生服务)
└── SystemFunctionHandler (内置系统功能)
```
### Android
```
ChatApiService (Flutter层)
↓
ChatApiPlugin (Android插件桥接)
↓
ChatApiService (Android原生服务)
├── OpenAI客户端 (aallam/openai-kotlin 4.0.1)
└── MCP功能 (暂时留空,接口预留)
```

70
local_plugins/chat_api/android/README.md

@ -0,0 +1,70 @@
# ChatAPI Android 实现
基于 [openai-kotlin 4.0.1](https://github.com/aallam/openai-kotlin/tree/4.0.1) 的 ChatAPI 插件 Android 实现。
## 特性
- ✅ 基于 openai-kotlin 4.0.1 的 OpenAI API 客户端
- ✅ 与 iOS 版本接口完全一致
- ✅ 支持流式和非流式对话
- ✅ 支持多模态内容(文本和图像识别)
- ✅ 工具调用架构预留(MCP 功能暂时留空)
- ✅ 协程支持和异步处理
- ✅ 完整的错误处理和取消机制
## 依赖
- openai-kotlin 4.0.1
- ktor-client-okhttp 2.3.2
- kotlinx-coroutines
- gson
## 主要文件
- `ChatApiPlugin.kt` - Flutter 插件桥接层
- `ChatApiService.kt` - 核心 ChatAPI 服务实现
- `StreamCallback.kt` - 流式回调接口
## 与 iOS 版本的一致性
Android 实现完全遵循 iOS 版本的接口定义:
1. **初始化方法**: `initialize(apiKey, baseUrl, model, mcpServer)`
2. **消息创建**: `createUserMessage()`, `createAssistantMessage()`
3. **消息发送**: `sendMessage()` (非流式), `sendMessageStream()` (流式)
4. **函数注册**: `registerFunction()`
5. **MCP 相关**: 接口保留,功能暂时留空
6. **流式事件**: 完全一致的事件类型和回调
## MCP 功能状态
MCP (Model Context Protocol) 相关功能在 Android 版本中暂时留空,包括:
- `initializeMcpClient()` - 返回 false
- `isMcpInitialized()` - 返回 false
- `handleMcpToolCall()` - 返回占位符消息
- 工具调用会通知上层但不会实际执行
## 使用示例
```kotlin
// 初始化
chatApiService.initialize(
apiKey = "your-api-key",
baseUrl = "https://api.openai.com/v1/",
model = "gpt-3.5-turbo",
mcpServer = ""
)
// 流式对话
chatApiService.sendMessageStream(listOf(
mapOf("role" to "user", "content" to "Hello!")
))
```
## 注意事项
1. 需要网络权限 `INTERNET` 和 `ACCESS_NETWORK_STATE`
2. 所有网络请求在后台线程执行
3. 流式回调在主线程触发
4. 支持取消正在进行的请求

50
local_plugins/chat_api/android/build.gradle.kts

@ -0,0 +1,50 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
kotlin("plugin.serialization") version "1.9.24"
}
android {
namespace = "com.yunqiinnovation.chat_api"
compileSdk = 35
defaultConfig {
minSdk = 21
targetSdk = 33
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
packaging {
resources {
excludes.add("META-INF/DEPENDENCIES")
excludes.add("META-INF/LICENSE")
excludes.add("META-INF/LICENSE.txt")
excludes.add("META-INF/license.txt")
excludes.add("META-INF/NOTICE")
excludes.add("META-INF/NOTICE.txt")
excludes.add("META-INF/notice.txt")
excludes.add("META-INF/*.kotlin_module")
}
}
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
// OpenAI Kotlin 4.0.1
implementation("com.aallam.openai:openai-client:4.0.1")
implementation("io.ktor:ktor-client-okhttp:2.3.2")
// JSON处理
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
implementation("com.google.code.gson:gson:2.10.1")
}

1
local_plugins/chat_api/android/settings.gradle.kts

@ -0,0 +1 @@
rootProject.name = "chat_api"

8
local_plugins/chat_api/android/src/main/AndroidManifest.xml

@ -0,0 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yunqiinnovation.chat_api">
<!-- 网络权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>

252
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiPlugin.kt

@ -0,0 +1,252 @@
package com.yunqiinnovation.chat_api
import android.os.Handler
import android.os.Looper
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import kotlinx.coroutines.*
/**
* ChatApiPlugin
*
* 基于 openai-kotlin 4.0.1 的 ChatAPI 插件 Android 实现
* 与 iOS 版本接口完全一致
*/
class ChatApiPlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler {
private lateinit var methodChannel: MethodChannel
private lateinit var eventChannel: EventChannel
private var eventSink: EventChannel.EventSink? = null
private val chatApiService = ChatApiService()
private val mainHandler = Handler(Looper.getMainLooper())
// 协程作用域
private val pluginScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.chat_api/methods")
methodChannel.setMethodCallHandler(this)
eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.chat_api/events")
eventChannel.setStreamHandler(this)
// 设置流式回调
chatApiService.setStreamCallback(object : com.yunqiinnovation.chat_api.StreamCallback {
override fun onToken(token: String) {
sendEvent("token", token)
}
override fun onComplete() {
sendEvent("complete", null)
}
override fun onError(error: Exception) {
sendEvent("error", error.message ?: "未知错误")
}
override fun onFunctionCall(functionCall: org.json.JSONObject) {
try {
val jsonString = functionCall.toString()
sendEvent("functionCall", jsonString)
} catch (e: Exception) {
sendEvent("error", "Failed to serialize function call: ${e.message}")
}
}
override fun onFunctionCallResult(functionCall: org.json.JSONObject, functionCallResult: org.json.JSONObject) {
try {
val meta = org.json.JSONObject().apply {
put("functionCall", functionCall)
put("functionCallResult", functionCallResult)
}
val metaString = meta.toString()
val context = functionCallResult.optString("context", "")
sendEvent("functionCall", context, metaString)
} catch (e: Exception) {
sendEvent("error", "Failed to serialize function call result: ${e.message}")
}
}
})
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
methodChannel.setMethodCallHandler(null)
eventChannel.setStreamHandler(null)
pluginScope.cancel()
}
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"initialize" -> handleInitialize(call, result)
"createUserMessage" -> handleCreateUserMessage(call, result)
"createAssistantMessage" -> handleCreateAssistantMessage(call, result)
"sendMessage" -> handleSendMessage(call, result)
"sendMessageStream" -> handleSendMessageStream(call, result)
"sendFunctionCallResult" -> handleSendFunctionCallResult(call, result)
"cancelCurrentStream" -> handleCancelCurrentStream(result)
"registerFunction" -> handleRegisterFunction(call, result)
"initializeMcpClient" -> handleInitializeMcpClient(call, result)
"isMcpInitialized" -> handleIsMcpInitialized(result)
"closeMcpClient" -> handleCloseMcpClient(result)
"handleMcpToolCall" -> handleMcpToolCall(call, result)
else -> result.notImplemented()
}
}
// MARK: - Method Handlers
private fun handleInitialize(call: MethodCall, result: Result) {
val apiKey = call.argument<String>("apiKey")
if (apiKey.isNullOrEmpty()) {
result.error("INVALID_ARGS", "缺少必要参数", null)
return
}
val baseUrl = call.argument<String>("baseUrl") ?: ""
val model = call.argument<String>("model") ?: ""
val mcpServer = call.argument<String>("mcpServer") ?: ""
val success = chatApiService.initialize(apiKey, baseUrl, model, mcpServer)
result.success(success)
}
private fun handleCreateUserMessage(call: MethodCall, result: Result) {
val content = call.argument<String>("content")
if (content.isNullOrEmpty()) {
result.error("INVALID_ARGS", "缺少必要参数", null)
return
}
val message = chatApiService.createUserMessage(content)
result.success(message)
}
private fun handleCreateAssistantMessage(call: MethodCall, result: Result) {
val content = call.argument<String>("content")
if (content.isNullOrEmpty()) {
result.error("INVALID_ARGS", "缺少必要参数", null)
return
}
val message = chatApiService.createAssistantMessage(content)
result.success(message)
}
private fun handleSendMessage(call: MethodCall, result: Result) {
@Suppress("UNCHECKED_CAST")
val messages = call.argument<List<Map<String, Any>>>("messages")
if (messages == null) {
result.error("INVALID_ARGS", "缺少必要参数", null)
return
}
pluginScope.launch {
try {
val response = chatApiService.sendMessage(messages)
mainHandler.post {
result.success(response)
}
} catch (e: Exception) {
mainHandler.post {
result.error("SEND_ERROR", e.message, null)
}
}
}
}
private fun handleSendMessageStream(call: MethodCall, result: Result) {
@Suppress("UNCHECKED_CAST")
val messages = call.argument<List<Map<String, Any>>>("messages")
if (messages == null) {
result.error("INVALID_ARGS", "缺少必要参数", null)
return
}
chatApiService.sendMessageStream(messages)
result.success(true)
}
private fun handleSendFunctionCallResult(call: MethodCall, result: Result) {
result.error("DEPRECATED", "sendFunctionCallResult已废弃,工具调用结果现在自动处理", null)
}
private fun handleCancelCurrentStream(result: Result) {
val success = chatApiService.cancelCurrentStream()
result.success(success)
}
private fun handleRegisterFunction(call: MethodCall, result: Result) {
val name = call.argument<String>("name")
val description = call.argument<String>("description")
@Suppress("UNCHECKED_CAST")
val parameters = call.argument<Map<String, Any>>("parameters")
if (name.isNullOrEmpty() || description.isNullOrEmpty() || parameters == null) {
result.error("INVALID_ARGS", "缺少必要参数", null)
return
}
val success = chatApiService.registerFunction(name, description, parameters)
result.success(success)
}
private fun handleInitializeMcpClient(call: MethodCall, result: Result) {
val serverUrl = call.argument<String>("serverUrl")
if (serverUrl.isNullOrEmpty()) {
result.error("INVALID_ARGS", "缺少必要参数", null)
return
}
// MCP功能暂时留空
result.success(false)
}
private fun handleIsMcpInitialized(result: Result) {
// MCP功能暂时留空
result.success(false)
}
private fun handleCloseMcpClient(result: Result) {
// MCP功能暂时留空
result.success(true)
}
private fun handleMcpToolCall(call: MethodCall, result: Result) {
val functionCallJson = call.argument<String>("functionCall")
if (functionCallJson.isNullOrEmpty()) {
result.error("INVALID_ARGS", "缺少必要参数", null)
return
}
// MCP功能暂时留空
result.success("MCP功能暂未实现")
}
// MARK: - Event Stream Handler
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
eventSink = events
}
override fun onCancel(arguments: Any?) {
eventSink = null
}
private fun sendEvent(type: String, content: Any?, meta: String? = null) {
mainHandler.post {
eventSink?.let { sink ->
val event = mutableMapOf<String, Any>(
"type" to type
)
content?.let { event["content"] = it }
meta?.let { event["meta"] = it }
sink.success(event)
}
}
}
}

799
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt

@ -0,0 +1,799 @@
package com.yunqiinnovation.chat_api
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Base64
import com.aallam.openai.api.chat.*
import com.aallam.openai.api.core.Role
import com.aallam.openai.api.model.ModelId
import com.aallam.openai.api.http.Timeout
import com.aallam.openai.client.OpenAI
import com.aallam.openai.client.OpenAIConfig
import com.aallam.openai.client.OpenAIHost
import com.google.gson.Gson
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collect
import java.io.ByteArrayOutputStream
import java.io.File
import kotlin.coroutines.CoroutineContext
import kotlin.math.min
import kotlin.math.sqrt
import kotlin.time.Duration.Companion.seconds
import android.util.Log
/**
* ChatAPI服务异常
*/
class ChatApiException(message: String) : Exception("ChatApiException: $message")
/**
* 流式回调接口
* 与 iOS 版本 StreamCallback 协议保持完全一致
*/
interface StreamCallback {
/**
* 接收到新的token
*/
fun onToken(token: String)
/**
* 流式响应完成
*/
fun onComplete()
/**
* 出现错误
*/
fun onError(error: Exception)
/**
* 函数调用 - 兼容JSONObject格式
*/
fun onFunctionCall(functionCall: org.json.JSONObject)
/**
* 函数调用结果 - 兼容JSONObject格式
*/
fun onFunctionCallResult(functionCall: org.json.JSONObject, functionCallResult: org.json.JSONObject)
}
/**
* 工具调用信息
*/
private data class ToolCallInfo(
var id: String = "",
var name: String = "",
var arguments: String = ""
) {
fun isValid(): Boolean = id.isNotEmpty() && name.isNotEmpty()
}
/**
* ChatAPI服务的Android实现
* 基于 openai-kotlin 4.0.1,与 iOS 版本接口完全一致
*/
class ChatApiService(private val context: android.content.Context? = null) : CoroutineScope {
override val coroutineContext: CoroutineContext = Dispatchers.IO + SupervisorJob()
// MARK: - 属性
private var baseUrl = "https://api.openai.com/v1/"
private var apiKey = ""
private var model = "gpt-3.5-turbo"
private var visionModel = "gpt-4-vision-preview"
private var isInitialized = false
// OpenAI 客户端
private var openAI: OpenAI? = null
// 流式请求相关
private var currentStreamJob: Job? = null
private var streamCallback: StreamCallback? = null
private var currentMessages: List<Map<String, Any>> = emptyList()
private var toolCalls: MutableMap<Int, ToolCallInfo> = mutableMapOf()
private var isCanceled = false
// JSON处理
private val gson = Gson()
// MARK: - 公共方法
/**
* 设置流式回调
*/
fun setStreamCallback(callback: StreamCallback) {
streamCallback = callback
}
/**
* 初始化ChatAPI服务
*/
fun initialize(apiKey: String, baseUrl: String, model: String, mcpServer: String): Boolean {
this.apiKey = apiKey
if (baseUrl.isNotEmpty()) {
this.baseUrl = baseUrl
}
if (model.isNotEmpty()) {
this.model = model
}
Log.d("ChatApiService", "原始 baseUrl: $baseUrl")
// 处理 baseUrl:移除末尾的 /chat/completions(如果存在)
// 因为 openai-kotlin 会自动拼接 /chat/completions
var processedBaseUrl = if (this.baseUrl.endsWith("/chat/completions")) {
this.baseUrl.removeSuffix("/chat/completions")
} else {
this.baseUrl.removeSuffix("/")
}
// 确保以 / 结尾
if (!processedBaseUrl.endsWith("/")) {
processedBaseUrl += "/"
}
Log.d("ChatApiService", "处理后 baseUrl: $processedBaseUrl")
return try {
// 创建OpenAI配置
val config = OpenAIConfig(
token = apiKey,
timeout = Timeout(socket = 60.seconds),
host = OpenAIHost(baseUrl = processedBaseUrl)
)
openAI = OpenAI(config)
// 初始化MCP客户端 (暂时留空,但保持接口一致)
if (mcpServer.isNotEmpty()) {
// MCP功能暂时留空,但记录服务器地址以便后续实现
// initializeMcpClient(mcpServer)
}
isInitialized = apiKey.isNotEmpty()
true
} catch (e: Exception) {
false
}
}
/**
* 创建用户消息
*/
fun createUserMessage(content: String): Map<String, Any> {
return mapOf(
"role" to "user",
"content" to content
)
}
/**
* 创建助手消息
*/
fun createAssistantMessage(content: String): Map<String, Any> {
return mapOf(
"role" to "assistant",
"content" to content
)
}
/**
* 创建系统消息
*/
fun createSystemMessage(content: String): Map<String, Any> {
return mapOf(
"role" to "system",
"content" to content
)
}
/**
* 发送消息(非流式输出)
*/
suspend fun sendMessage(messages: List<Map<String, Any>>): String {
if (!isInitialized || apiKey.isEmpty() || openAI == null) {
throw ChatApiException("ChatAPI服务未初始化")
}
// 检查是否包含图片,决定使用哪个模型
val currentModel = if (containsImage(messages)) visionModel else model
// 转换消息格式
val chatMessages = convertToChatMessages(messages)
// 构建请求
val chatCompletionRequest = ChatCompletionRequest(
model = ModelId(currentModel),
messages = chatMessages,
maxTokens = 2000,
temperature = 0.7
)
val result = openAI!!.chatCompletion(chatCompletionRequest)
// 检查是否有工具调用
val choice = result.choices.firstOrNull()
choice?.message?.toolCalls?.firstOrNull()?.let { toolCall ->
if (toolCall is ToolCall.Function) {
val functionCallJson = mapOf(
"name" to toolCall.function.name,
"arguments" to (toolCall.function.arguments ?: ""),
"id" to toolCall.id.toString()
)
return gson.toJson(functionCallJson)
}
}
// 返回普通消息内容
return choice?.message?.content ?: throw ChatApiException("无效的响应格式")
}
/**
* 发送消息(流式输出)- 兼容JSONArray格式
*/
fun sendMessageStream(messages: org.json.JSONArray, callback: StreamCallback) {
// 设置回调
setStreamCallback(callback)
// 转换JSONArray为List<Map<String, Any>>
val messageList = convertJsonArrayToList(messages)
// 调用原始方法
sendMessageStream(messageList)
}
/**
* 发送消息(流式输出)
*/
fun sendMessageStream(messages: List<Map<String, Any>>) {
if (!isInitialized || apiKey.isEmpty() || openAI == null) {
streamCallback?.onError(ChatApiException("ChatAPI服务未初始化"))
return
}
// 重置状态
isCanceled = false
currentMessages = messages
toolCalls.clear()
// 检查是否包含图片,决定使用哪个模型
val currentModel = if (containsImage(messages)) visionModel else model
currentStreamJob = launch {
try {
// 转换消息格式
val chatMessages = convertToChatMessages(messages)
// 构建请求
val chatCompletionRequest = ChatCompletionRequest(
model = ModelId(currentModel),
messages = chatMessages,
maxTokens = 2000,
temperature = 0.7
)
val chatsFlow = openAI!!.chatCompletions(chatCompletionRequest)
chatsFlow.collect { result ->
if (isCanceled) return@collect
val choice = result.choices.firstOrNull() ?: return@collect
val delta = choice.delta ?: return@collect
// 处理普通文本内容
delta.content?.let { content ->
streamCallback?.onToken(content)
}
// 收集工具调用信息
delta.toolCalls?.forEach { toolCall ->
val index = toolCall.index
// 创建或获取现有的工具调用信息
val toolCallInfo = toolCalls.getOrPut(index) { ToolCallInfo() }
// 更新ID
toolCall.id?.let { toolCallInfo.id = it.toString() }
// 更新函数信息
toolCall.function?.let { function ->
function.name?.let { toolCallInfo.name = it }
function.arguments?.let { toolCallInfo.arguments += it }
}
}
}
if (!isCanceled) {
// 处理工具调用或完成
val hasToolCalls = processToolCalls()
if (!hasToolCalls) {
streamCallback?.onComplete()
}
}
} catch (e: Exception) {
if (!isCanceled) {
streamCallback?.onError(ChatApiException("流式请求失败: ${e.message}"))
}
}
}
}
/**
* 处理工具调用
*/
private suspend fun processToolCalls(): Boolean {
val firstToolCall = toolCalls.values.firstOrNull { it.isValid() } ?: return false
// 创建函数调用字典
val functionCall = mapOf(
"name" to firstToolCall.name,
"arguments" to firstToolCall.arguments,
"id" to firstToolCall.id
)
// 通知上层工具调用事件
streamCallback?.onFunctionCall(convertMapToJsonObject(functionCall))
// 在后台队列处理工具调用
launch {
try {
if (!isCanceled) {
// 这里暂时返回占位符结果,实际MCP功能留空
val result = mapOf("context" to "MCP功能暂未实现")
if (!isCanceled) {
// 处理结果
streamCallback?.onFunctionCallResult(
convertMapToJsonObject(functionCall),
convertMapToJsonObject(result)
)
// 将结果发送回OpenAI继续对话
sendFunctionCallResultInternal(
messages = currentMessages,
functionCall = functionCall,
functionResult = gson.toJson(result)
)
}
}
} catch (e: Exception) {
if (!isCanceled) {
val errorMessage = "工具调用处理失败: ${e.message}"
sendFunctionCallResultInternal(
messages = currentMessages,
functionCall = functionCall,
functionResult = errorMessage
)
}
}
}
return true
}
/**
* 发送函数调用结果(内部方法)
*/
private suspend fun sendFunctionCallResultInternal(
messages: List<Map<String, Any>>,
functionCall: Map<String, Any>,
functionResult: String
) {
if (isCanceled) return
val fullMessages = messages.toMutableList()
// 添加函数调用消息
val callId = functionCall["id"] as? String ?: "call_${System.currentTimeMillis()}"
fullMessages.add(
mapOf(
"role" to "assistant",
"content" to "",
"tool_calls" to listOf(
mapOf(
"id" to callId,
"type" to "function",
"function" to mapOf(
"name" to (functionCall["name"] as? String ?: ""),
"arguments" to (functionCall["arguments"] as? String ?: "{}")
)
)
)
)
)
// 添加函数结果消息
fullMessages.add(
mapOf(
"role" to "tool",
"content" to functionResult,
"tool_call_id" to callId
)
)
// 重新发起对话,让AI基于工具调用结果生成回复
sendMessageStream(fullMessages)
}
/**
* 取消当前流式请求
*/
fun cancelCurrentStream(): Boolean {
isCanceled = true
currentStreamJob?.cancel()
currentStreamJob = null
return true
}
/**
* 取消所有操作并释放资源 (与 iOS 版本保持一致)
*/
fun cancelAll() {
cancelCurrentStream()
}
/**
* 注册函数
*/
fun registerFunction(name: String, description: String, parameters: Map<String, Any>): Boolean {
// 暂时返回true,实际功能留空,但保持与iOS版本接口一致
return true
}
/**
* 初始化MCP客户端
*/
fun initializeMcpClient(serverUrl: String): Boolean {
// MCP功能暂时留空,但保持与iOS版本接口一致
return false
}
/**
* MCP客户端是否已初始化
*/
fun isMcpInitialized(): Boolean {
// MCP功能暂时留空,但保持与iOS版本接口一致
return false
}
/**
* 关闭MCP客户端
*/
fun closeMcpClient() {
// MCP功能暂时留空,但保持与iOS版本接口一致
}
/**
* 处理MCP工具调用
*/
suspend fun handleMcpToolCall(functionCallJson: String): String {
// MCP功能暂时留空,但保持与iOS版本接口一致
return "MCP功能暂未实现"
}
// MARK: - 工具方法
/**
* 检查消息中是否包含图片
*/
private fun containsImage(messages: List<Map<String, Any>>): Boolean {
for (message in messages) {
val content = message["content"]
if (content is String && content.contains("image_url")) {
return true
}
if (content is List<*>) {
content.forEach { item ->
if (item is Map<*, *>) {
val type = item["type"] as? String
if (type == "image_url") {
return true
}
}
}
}
}
return false
}
/**
* 转换为OpenAI ChatMessage格式
*/
private fun convertToChatMessages(messages: List<Map<String, Any>>): List<ChatMessage> {
val chatMessages = mutableListOf<ChatMessage>()
for (message in messages) {
val role = message["role"] as? String ?: continue
val chatRole = when (role) {
"system" -> Role.System
"user" -> Role.User
"assistant" -> Role.Assistant
"tool" -> Role.Tool
else -> Role.User
}
// 特殊处理assistant角色的tool_calls消息
if (role == "assistant" && message.containsKey("tool_calls")) {
val content = message["content"] as? String ?: ""
// 解析tool_calls
val toolCallsList = mutableListOf<ToolCall>()
@Suppress("UNCHECKED_CAST")
val toolCallsArray = message["tool_calls"] as? List<Map<String, Any>>
toolCallsArray?.forEach { toolCallDict ->
val id = toolCallDict["id"] as? String ?: return@forEach
val type = toolCallDict["type"] as? String
if (type == "function") {
@Suppress("UNCHECKED_CAST")
val function = toolCallDict["function"] as? Map<String, Any> ?: return@forEach
val name = function["name"] as? String ?: return@forEach
val arguments = function["arguments"] as? String ?: ""
val functionCall = FunctionCall(
nameOrNull = name,
argumentsOrNull = arguments
)
val toolCall = ToolCall.Function(
id = ToolId(id),
function = functionCall
)
toolCallsList.add(toolCall)
}
}
val chatMessage = ChatMessage.Assistant(
content = if (content.isEmpty()) null else content,
toolCalls = if (toolCallsList.isEmpty()) null else toolCallsList
)
chatMessages.add(chatMessage)
continue
}
// 特殊处理tool角色消息
if (role == "tool") {
val content = message["content"] as? String ?: ""
val toolCallId = message["tool_call_id"] as? String ?: ""
val chatMessage = ChatMessage.Tool(
content = content,
toolCallId = ToolId(toolCallId)
)
chatMessages.add(chatMessage)
continue
}
// 处理普通消息的不同类型内容
val content = message["content"]
when (content) {
is String -> {
val chatMessage = when (chatRole) {
Role.System -> ChatMessage.System(content)
Role.User -> ChatMessage.User(content)
Role.Assistant -> ChatMessage.Assistant(content)
else -> ChatMessage.User(content)
}
chatMessages.add(chatMessage)
}
is List<*> -> {
// 对于多模态内容,提取文本部分(简化处理)
var textContent = ""
content.forEach { item ->
if (item is Map<*, *>) {
val type = item["type"] as? String
if (type == "text") {
val text = item["text"] as? String ?: ""
textContent += text
}
}
}
val finalContent = if (textContent.isEmpty()) "多媒体内容" else textContent
val chatMessage = when (chatRole) {
Role.System -> ChatMessage.System(finalContent)
Role.User -> ChatMessage.User(finalContent)
Role.Assistant -> ChatMessage.Assistant(finalContent)
else -> ChatMessage.User(finalContent)
}
chatMessages.add(chatMessage)
}
}
}
return chatMessages
}
/**
* 将文件转换为Base64字符串
*/
fun fileToBase64(filePath: String, maxSizeKB: Int = 20480): String? {
return try {
val file = File(filePath)
if (!file.exists() || !file.isFile) {
return null
}
// 读取文件
var bitmap = BitmapFactory.decodeFile(filePath) ?: return null
// 检查图片尺寸,限制最大为1024*1024
val maxDimension = 1024
if (bitmap.width > maxDimension || bitmap.height > maxDimension) {
// 计算缩放比例,保持纵横比
val ratio = min(maxDimension.toFloat() / bitmap.width, maxDimension.toFloat() / bitmap.height)
val newWidth = (bitmap.width * ratio).toInt()
val newHeight = (bitmap.height * ratio).toInt()
bitmap = Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true)
}
// 压缩图片
val outputStream = ByteArrayOutputStream()
var quality = 100
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
// 如果原始图片太大,继续优化文件大小
var fileSize = file.length() / 1024 // 转为KB
if (fileSize > maxSizeKB) {
val scale = sqrt(maxSizeKB.toDouble() / fileSize)
val newWidth = (bitmap.width * scale).toInt()
val newHeight = (bitmap.height * scale).toInt()
bitmap = Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true)
outputStream.reset()
quality = 85
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
}
// 转为Base64
Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
} catch (e: Exception) {
null
}
}
/**
* 创建带图片的用户消息
*/
fun createUserMessageWithImage(text: String, imageBase64: String): Map<String, Any> {
val contentArray = mutableListOf<Map<String, Any>>()
// 添加文本部分
if (text.isNotEmpty()) {
contentArray.add(
mapOf(
"type" to "text",
"text" to text
)
)
}
// 添加图片部分
contentArray.add(
mapOf(
"type" to "image_url",
"image_url" to mapOf(
"url" to "data:image/jpeg;base64,$imageBase64"
)
)
)
return mapOf(
"role" to "user",
"content" to contentArray
)
}
/**
* 从URL中提取主机信息 (保留向后兼容)
*/
private fun extractHost(url: String): String {
return try {
val javaUrl = java.net.URL(url)
javaUrl.host ?: "api.openai.com"
} catch (e: Exception) {
"api.openai.com"
}
}
/**
* 解析JSON参数 (与 iOS 版本保持一致)
*/
private fun parseJsonArguments(argumentsJson: String): Map<String, Any> {
return try {
if (argumentsJson.isBlank()) {
emptyMap()
} else {
val jsonObject = org.json.JSONObject(argumentsJson)
convertJsonObjectToMap(jsonObject)
}
} catch (e: Exception) {
emptyMap()
}
}
/**
* 转换JSONArray为List<Map<String, Any>>
*/
private fun convertJsonArrayToList(jsonArray: org.json.JSONArray): List<Map<String, Any>> {
val result = mutableListOf<Map<String, Any>>()
for (i in 0 until jsonArray.length()) {
val jsonObject = jsonArray.optJSONObject(i) ?: continue
val map = convertJsonObjectToMap(jsonObject)
result.add(map)
}
return result
}
/**
* 转换JSONObject为Map<String, Any>
*/
private fun convertJsonObjectToMap(jsonObject: org.json.JSONObject): Map<String, Any> {
val map = mutableMapOf<String, Any>()
val keys = jsonObject.keys()
while (keys.hasNext()) {
val key = keys.next()
val value = jsonObject.get(key)
when (value) {
is org.json.JSONObject -> map[key] = convertJsonObjectToMap(value)
is org.json.JSONArray -> {
val list = mutableListOf<Any>()
for (i in 0 until value.length()) {
val item = value.get(i)
when (item) {
is org.json.JSONObject -> list.add(convertJsonObjectToMap(item))
else -> list.add(item)
}
}
map[key] = list
}
else -> map[key] = value
}
}
return map
}
/**
* 转换Map为JSONObject
*/
private fun convertMapToJsonObject(map: Map<String, Any>): org.json.JSONObject {
val jsonObject = org.json.JSONObject()
for ((key, value) in map) {
when (value) {
is Map<*, *> -> {
@Suppress("UNCHECKED_CAST")
jsonObject.put(key, convertMapToJsonObject(value as Map<String, Any>))
}
is List<*> -> {
val jsonArray = org.json.JSONArray()
value.forEach { item ->
when (item) {
is Map<*, *> -> {
@Suppress("UNCHECKED_CAST")
jsonArray.put(convertMapToJsonObject(item as Map<String, Any>))
}
else -> jsonArray.put(item)
}
}
jsonObject.put(key, jsonArray)
}
else -> jsonObject.put(key, value)
}
}
return jsonObject
}
}

42
local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift

@ -340,17 +340,19 @@ public class SystemFunctionHandler: NSObject {
// 检查位置服务是否可用
guard CLLocationManager.locationServicesEnabled() else {
continuation.resume(returning: Result(success: false, message: "位置服务未启用"))
locationCompletion?(Result(success: false, message: "位置服务未启用。请前往 设置 > 隐私与安全 > 定位服务 启用定位服务"))
locationCompletion = nil
return
}
// 确保locationManager已初始化
guard let locationManager = locationManager else {
continuation.resume(returning: Result(success: false, message: "位置管理器未初始化"))
locationCompletion?(Result(success: false, message: "位置管理器未初始化"))
locationCompletion = nil
return
}
// 检查位置权限 - 兼容iOS 13.0+
// 检查位置权限
let authStatus: CLAuthorizationStatus
if #available(iOS 14.0, *) {
authStatus = locationManager.authorizationStatus
@ -358,20 +360,27 @@ public class SystemFunctionHandler: NSObject {
authStatus = CLLocationManager.authorizationStatus()
}
print("当前位置权限状态: \(authStatus.rawValue)")
switch authStatus {
case .notDetermined:
// 请求权限,权限结果会在 didChangeAuthorization 回调中处理
print("请求位置权限...")
// 请求权限
locationManager.requestWhenInUseAuthorization()
// 不要在这里 return,让权限回调来处理后续逻辑
case .denied, .restricted:
continuation.resume(returning: Result(success: false, message: "位置权限被拒绝"))
return
// 权限结果会在 didChangeAuthorization 回调中处理
case .denied:
locationCompletion?(Result(success: false, message: "位置权限被拒绝。请前往 设置 > 隐私与安全 > 定位服务 > Deep Voice 启用定位权限"))
locationCompletion = nil
case .restricted:
locationCompletion?(Result(success: false, message: "位置权限受限制,可能是由于家长控制或企业配置限制"))
locationCompletion = nil
case .authorizedWhenInUse, .authorizedAlways:
print("已有位置权限,开始获取位置...")
// 已有权限,直接请求位置
locationManager.requestLocation()
@unknown default:
continuation.resume(returning: Result(success: false, message: "未知的位置权限状态"))
return
locationCompletion?(Result(success: false, message: "未知的位置权限状态: \(authStatus.rawValue)"))
locationCompletion = nil
}
}
}
@ -404,10 +413,17 @@ extension SystemFunctionHandler: CLLocationManagerDelegate {
return
}
if status == .authorizedWhenInUse || status == .authorizedAlways {
switch status {
case .authorizedWhenInUse, .authorizedAlways:
manager.requestLocation()
} else if status == .denied || status == .restricted {
locationCompletion?(Result(success: false, message: "位置权限被拒绝"))
case .denied, .restricted:
locationCompletion?(Result(success: false, message: "位置权限被拒绝。请前往 设置 > 隐私与安全 > 定位服务 > Deep Voice 启用定位权限"))
locationCompletion = nil
case .notDetermined:
// 权限仍未确定,等待用户操作
break
@unknown default:
locationCompletion?(Result(success: false, message: "位置权限状态异常,请重试"))
locationCompletion = nil
}
}

3
local_plugins/chat_api/pubspec.yaml

@ -34,6 +34,9 @@ flutter:
# adding or updating assets for this project.
plugin:
platforms:
android:
package: com.yunqiinnovation.chat_api
pluginClass: ChatApiPlugin
ios:
pluginClass: ChatApiPlugin
ffiPlugin: true

Loading…
Cancel
Save