13 changed files with 1325 additions and 56 deletions
@ -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. 支持取消正在进行的请求 |
||||
@ -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") |
||||
|
} |
||||
@ -0,0 +1 @@ |
|||||
|
rootProject.name = "chat_api" |
||||
@ -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> |
||||
@ -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) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue