55 changed files with 2321 additions and 1302 deletions
@ -1,116 +0,0 @@ |
|||||
package com.yunqiinnovation.ble_service |
|
||||
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 android.bluetooth.BluetoothDevice |
|
||||
import android.bluetooth.BluetoothGatt |
|
||||
import android.bluetooth.BluetoothProfile |
|
||||
import android.content.Context |
|
||||
import com.jieli.jl_bt_ota.constant.ErrorCode; |
|
||||
import com.jieli.jl_bt_ota.constant.StateCode; |
|
||||
import com.jieli.jl_bt_ota.impl.BluetoothOTAManager; |
|
||||
import com.jieli.jl_bt_ota.interfaces.BtEventCallback; |
|
||||
import com.jieli.jl_bt_ota.interfaces.IActionCallback; |
|
||||
import com.jieli.jl_bt_ota.interfaces.IUpgradeCallback; |
|
||||
import com.jieli.jl_bt_ota.model.BluetoothOTAConfigure; |
|
||||
import com.jieli.jl_bt_ota.model.base.BaseError; |
|
||||
import com.jieli.jl_bt_ota.model.response.TargetInfoResponse; |
|
||||
import android.util.Log |
|
||||
import java.util.* |
|
||||
class CustomUpdateCallback( |
|
||||
val context: Context, |
|
||||
val device: BluetoothDevice? |
|
||||
) : IUpgradeCallback { |
|
||||
companion object { |
|
||||
private const val TAG = "JlOtaPlugin" |
|
||||
private const val CHANNEL_NAME = "com.yunqiinnovation.jl_ota" |
|
||||
private const val EVENT_CHANNEL_NAME = "com.yunqiinnovation.jl_ota/events" |
|
||||
|
|
||||
} |
|
||||
|
|
||||
override fun onStartOTA() { |
|
||||
Log.d(TAG, "onStartOTA开始升级") |
|
||||
// viewModel.otaStateMLD.value = OTAStart(device) |
|
||||
} |
|
||||
|
|
||||
override fun onNeedReconnect(addr: String?, isNewReconnectWay: Boolean) { |
|
||||
Log.d(TAG, "onNeedReconnect需要重新链接") |
|
||||
|
|
||||
} |
|
||||
|
|
||||
override fun onProgress(type: Int, progress: Float) { |
|
||||
Log.d(TAG, "onProgress正在进展") |
|
||||
//viewModel.otaStateMLD.value = OTAWorking(device, type, progress) |
|
||||
} |
|
||||
|
|
||||
override fun onStopOTA() { |
|
||||
Log.d(TAG, "onStartOTA停止升级") |
|
||||
|
|
||||
} |
|
||||
|
|
||||
override fun onCancelOTA() { |
|
||||
Log.d(TAG, "onCancelOTA取消升级") |
|
||||
|
|
||||
} |
|
||||
|
|
||||
override fun onError(error: BaseError?) { |
|
||||
val errorMessage = error?.message ?: "Unknown error" |
|
||||
Log.e(TAG, "升级失败: ${error?.message ?: "未知错误"}") |
|
||||
|
|
||||
// 如果需要打印完整错误对象信息(假设BaseError有重写toString()) |
|
||||
Log.d(TAG, "完整错误对象: ${error ?: "null"}") |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
class OTAManager(context: Context) : BluetoothOTAManager(context) { |
|
||||
|
|
||||
|
|
||||
// 定义回调接口 |
|
||||
interface DataCallback { |
|
||||
fun onDataReceived(device: BluetoothDevice?, data: ByteArray?) |
|
||||
} |
|
||||
// 保存回调的实例 |
|
||||
private var dataCallback: DataCallback? = null |
|
||||
|
|
||||
// 设置回调的方法 |
|
||||
fun setDataCallback(callback: DataCallback) { |
|
||||
this.dataCallback = callback |
|
||||
} |
|
||||
override fun getConnectedDevice(): BluetoothDevice? { |
|
||||
// 这里可以添加空实现的注释说明 |
|
||||
Log.i(TAG, "ota数据发送getConnectedDevice") |
|
||||
return null |
|
||||
} |
|
||||
|
|
||||
override fun getConnectedBluetoothGatt(): BluetoothGatt? { |
|
||||
// 这里可以添加空实现的注释说明 |
|
||||
Log.i(TAG, "ota数据发送getConnectedBluetoothGatt") |
|
||||
return null |
|
||||
} |
|
||||
|
|
||||
override fun connectBluetoothDevice(bluetoothDevice: BluetoothDevice?) { |
|
||||
// 这里可以添加空实现的注释说明 |
|
||||
Log.i(TAG, "ota数据发送connectBluetoothDevice") |
|
||||
} |
|
||||
|
|
||||
override fun disconnectBluetoothDevice(bluetoothDevice: BluetoothDevice?) { |
|
||||
// 这里可以添加空实现的注释说明 |
|
||||
Log.i(TAG, "ota数据发送disconnectBluetoothDevice") |
|
||||
} |
|
||||
|
|
||||
/** 数据发送方法(协议无关) */ |
|
||||
override fun sendDataToDevice( |
|
||||
bluetoothDevice: BluetoothDevice?, |
|
||||
bytes: ByteArray? |
|
||||
): Boolean { |
|
||||
Log.i(TAG, "ota数据发送${bluetoothDevice},${bytes?.size}") |
|
||||
// 触发回调,将数据回传给调用者 |
|
||||
dataCallback?.onDataReceived(bluetoothDevice, bytes) |
|
||||
// context.onReceiveDeviceData(device, data); |
|
||||
return true |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@ -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 |
||||
|
} |
||||
|
} |
||||
@ -1,33 +0,0 @@ |
|||||
# Miscellaneous |
|
||||
*.class |
|
||||
*.log |
|
||||
*.pyc |
|
||||
*.swp |
|
||||
.DS_Store |
|
||||
.atom/ |
|
||||
.build/ |
|
||||
.buildlog/ |
|
||||
.history |
|
||||
.svn/ |
|
||||
.swiftpm/ |
|
||||
migrate_working_dir/ |
|
||||
|
|
||||
# IntelliJ related |
|
||||
*.iml |
|
||||
*.ipr |
|
||||
*.iws |
|
||||
.idea/ |
|
||||
|
|
||||
# The .vscode folder contains launch configuration and tasks you configure in |
|
||||
# VS Code which you may wish to be included in version control, so this line |
|
||||
# is commented out by default. |
|
||||
#.vscode/ |
|
||||
|
|
||||
# Flutter/Dart/Pub related |
|
||||
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. |
|
||||
/pubspec.lock |
|
||||
**/doc/api/ |
|
||||
.dart_tool/ |
|
||||
.flutter-plugins |
|
||||
.flutter-plugins-dependencies |
|
||||
build/ |
|
||||
@ -1,33 +0,0 @@ |
|||||
# This file tracks properties of this Flutter project. |
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc. |
|
||||
# |
|
||||
# This file should be version controlled and should not be manually edited. |
|
||||
|
|
||||
version: |
|
||||
revision: "ea121f8859e4b13e47a8f845e4586164519588bc" |
|
||||
channel: "stable" |
|
||||
|
|
||||
project_type: plugin |
|
||||
|
|
||||
# Tracks metadata for the flutter migrate command |
|
||||
migration: |
|
||||
platforms: |
|
||||
- platform: root |
|
||||
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc |
|
||||
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc |
|
||||
- platform: android |
|
||||
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc |
|
||||
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc |
|
||||
- platform: ios |
|
||||
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc |
|
||||
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc |
|
||||
|
|
||||
# User provided section |
|
||||
|
|
||||
# List of Local paths (relative to this file) that should be |
|
||||
# ignored by the migrate tool. |
|
||||
# |
|
||||
# Files that are not part of the templates will be ignored by default. |
|
||||
unmanaged_files: |
|
||||
- 'lib/main.dart' |
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj' |
|
||||
@ -1,3 +0,0 @@ |
|||||
## 0.0.1 |
|
||||
|
|
||||
* TODO: Describe initial release. |
|
||||
@ -1 +0,0 @@ |
|||||
TODO: Add your license here. |
|
||||
@ -1,15 +0,0 @@ |
|||||
# jl_ota |
|
||||
|
|
||||
A new Flutter project. |
|
||||
|
|
||||
## Getting Started |
|
||||
|
|
||||
This project is a starting point for a Flutter |
|
||||
[plug-in package](https://flutter.dev/to/develop-plugins), |
|
||||
a specialized package that includes platform-specific implementation code for |
|
||||
Android and/or iOS. |
|
||||
|
|
||||
For help getting started with Flutter development, view the |
|
||||
[online documentation](https://docs.flutter.dev), which offers tutorials, |
|
||||
samples, guidance on mobile development, and a full API reference. |
|
||||
|
|
||||
@ -1,4 +0,0 @@ |
|||||
include: package:flutter_lints/flutter.yaml |
|
||||
|
|
||||
# Additional information about this file can be found at |
|
||||
# https://dart.dev/guides/language/analysis-options |
|
||||
@ -1,9 +0,0 @@ |
|||||
*.iml |
|
||||
.gradle |
|
||||
/local.properties |
|
||||
/.idea/workspace.xml |
|
||||
/.idea/libraries |
|
||||
.DS_Store |
|
||||
/build |
|
||||
/captures |
|
||||
.cxx |
|
||||
@ -1,68 +0,0 @@ |
|||||
group = "com.yunqiinnovation.jl_ota" |
|
||||
version = "1.0-SNAPSHOT" |
|
||||
|
|
||||
buildscript { |
|
||||
ext.kotlin_version = "1.8.22" |
|
||||
repositories { |
|
||||
google() |
|
||||
mavenCentral() |
|
||||
} |
|
||||
|
|
||||
dependencies { |
|
||||
classpath("com.android.tools.build:gradle:8.7.0") |
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
allprojects { |
|
||||
repositories { |
|
||||
google() |
|
||||
mavenCentral() |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
apply plugin: "com.android.library" |
|
||||
apply plugin: "kotlin-android" |
|
||||
|
|
||||
android { |
|
||||
namespace = "com.yunqiinnovation.jl_ota" |
|
||||
|
|
||||
compileSdk = 35 |
|
||||
|
|
||||
compileOptions { |
|
||||
sourceCompatibility = JavaVersion.VERSION_11 |
|
||||
targetCompatibility = JavaVersion.VERSION_11 |
|
||||
} |
|
||||
|
|
||||
kotlinOptions { |
|
||||
jvmTarget = JavaVersion.VERSION_11 |
|
||||
} |
|
||||
|
|
||||
sourceSets { |
|
||||
main.java.srcDirs += "src/main/kotlin" |
|
||||
test.java.srcDirs += "src/test/kotlin" |
|
||||
} |
|
||||
|
|
||||
defaultConfig { |
|
||||
minSdk = 21 |
|
||||
} |
|
||||
|
|
||||
dependencies { |
|
||||
// implementation project(':ble_service') |
|
||||
|
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test") |
|
||||
testImplementation("org.mockito:mockito-core:5.0.0") |
|
||||
} |
|
||||
|
|
||||
testOptions { |
|
||||
unitTests.all { |
|
||||
useJUnitPlatform() |
|
||||
|
|
||||
testLogging { |
|
||||
events "passed", "skipped", "failed", "standardOut", "standardError" |
|
||||
outputs.upToDateWhen {false} |
|
||||
showStandardStreams = true |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
Binary file not shown.
@ -1 +0,0 @@ |
|||||
rootProject.name = 'jl_ota' |
|
||||
@ -1,3 +0,0 @@ |
|||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" |
|
||||
package="com.yunqiinnovation.jl_ota"> |
|
||||
</manifest> |
|
||||
@ -1,76 +0,0 @@ |
|||||
package com.yunqiinnovation.jl_ota |
|
||||
|
|
||||
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 android.bluetooth.BluetoothGatt; |
|
||||
import android.bluetooth.BluetoothDevice |
|
||||
import android.content.Context |
|
||||
import android.os.Handler |
|
||||
import android.os.Looper |
|
||||
import android.util.Log |
|
||||
import androidx.annotation.NonNull |
|
||||
//import com.yunqiinnovation.ble_service.BleService |
|
||||
|
|
||||
/** JlOtaPlugin */ |
|
||||
class JlOtaPlugin() : FlutterPlugin, MethodCallHandler { |
|
||||
/// The MethodChannel that will the communication between Flutter and native Android |
|
||||
/// |
|
||||
/// This local reference serves to register the plugin with the Flutter Engine and unregister it |
|
||||
/// when the Flutter Engine is detached from the Activity |
|
||||
private lateinit var channel: MethodChannel |
|
||||
private lateinit var context: Context |
|
||||
private lateinit var eventChannel: EventChannel |
|
||||
private var streamSink: EventChannel.EventSink? = null |
|
||||
|
|
||||
companion object { |
|
||||
private const val TAG = "JlOtaPlugin" |
|
||||
private const val CHANNEL_NAME = "com.yunqiinnovation.jl_ota" |
|
||||
private const val EVENT_CHANNEL_NAME = "com.yunqiinnovation.jl_ota/events" |
|
||||
|
|
||||
} |
|
||||
|
|
||||
|
|
||||
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { |
|
||||
context = flutterPluginBinding.applicationContext |
|
||||
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "jl_ota") |
|
||||
channel.setMethodCallHandler(this) |
|
||||
|
|
||||
eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, EVENT_CHANNEL_NAME) |
|
||||
|
|
||||
eventChannel.setStreamHandler(object : EventChannel.StreamHandler { |
|
||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { |
|
||||
streamSink = events |
|
||||
} |
|
||||
|
|
||||
override fun onCancel(arguments: Any?) { |
|
||||
streamSink = null |
|
||||
} |
|
||||
}) |
|
||||
|
|
||||
} |
|
||||
|
|
||||
override fun onMethodCall(call: MethodCall, result: Result) { |
|
||||
|
|
||||
if (call.method == "getPlatformVersion") { |
|
||||
|
|
||||
// BleService.otaManager?.configureOTA() |
|
||||
|
|
||||
// BleService.otaManager?.startOTA() |
|
||||
result.success("Android ${android.os.Build.VERSION.RELEASE}") |
|
||||
} else { |
|
||||
result.notImplemented() |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { |
|
||||
channel.setMethodCallHandler(null) |
|
||||
} |
|
||||
|
|
||||
|
|
||||
} |
|
||||
|
|
||||
@ -1,38 +0,0 @@ |
|||||
.idea/ |
|
||||
.vagrant/ |
|
||||
.sconsign.dblite |
|
||||
.svn/ |
|
||||
|
|
||||
.DS_Store |
|
||||
*.swp |
|
||||
profile |
|
||||
|
|
||||
DerivedData/ |
|
||||
build/ |
|
||||
GeneratedPluginRegistrant.h |
|
||||
GeneratedPluginRegistrant.m |
|
||||
|
|
||||
.generated/ |
|
||||
|
|
||||
*.pbxuser |
|
||||
*.mode1v3 |
|
||||
*.mode2v3 |
|
||||
*.perspectivev3 |
|
||||
|
|
||||
!default.pbxuser |
|
||||
!default.mode1v3 |
|
||||
!default.mode2v3 |
|
||||
!default.perspectivev3 |
|
||||
|
|
||||
xcuserdata |
|
||||
|
|
||||
*.moved-aside |
|
||||
|
|
||||
*.pyc |
|
||||
*sync/ |
|
||||
Icon? |
|
||||
.tags* |
|
||||
|
|
||||
/Flutter/Generated.xcconfig |
|
||||
/Flutter/ephemeral/ |
|
||||
/Flutter/flutter_export_environment.sh |
|
||||
@ -1,19 +0,0 @@ |
|||||
import Flutter |
|
||||
import UIKit |
|
||||
|
|
||||
public class JlOtaPlugin: NSObject, FlutterPlugin { |
|
||||
public static func register(with registrar: FlutterPluginRegistrar) { |
|
||||
let channel = FlutterMethodChannel(name: "jl_ota", binaryMessenger: registrar.messenger()) |
|
||||
let instance = JlOtaPlugin() |
|
||||
registrar.addMethodCallDelegate(instance, channel: channel) |
|
||||
} |
|
||||
|
|
||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
|
||||
switch call.method { |
|
||||
case "getPlatformVersion": |
|
||||
result("iOS " + UIDevice.current.systemVersion) |
|
||||
default: |
|
||||
result(FlutterMethodNotImplemented) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,14 +0,0 @@ |
|||||
<?xml version="1.0" encoding="UTF-8"?> |
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
|
||||
<plist version="1.0"> |
|
||||
<dict> |
|
||||
<key>NSPrivacyTrackingDomains</key> |
|
||||
<array/> |
|
||||
<key>NSPrivacyAccessedAPITypes</key> |
|
||||
<array/> |
|
||||
<key>NSPrivacyCollectedDataTypes</key> |
|
||||
<array/> |
|
||||
<key>NSPrivacyTracking</key> |
|
||||
<false/> |
|
||||
</dict> |
|
||||
</plist> |
|
||||
@ -1,29 +0,0 @@ |
|||||
# |
|
||||
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. |
|
||||
# Run `pod lib lint jl_ota.podspec` to validate before publishing. |
|
||||
# |
|
||||
Pod::Spec.new do |s| |
|
||||
s.name = 'jl_ota' |
|
||||
s.version = '0.0.1' |
|
||||
s.summary = 'A new Flutter project.' |
|
||||
s.description = <<-DESC |
|
||||
A new Flutter project. |
|
||||
DESC |
|
||||
s.homepage = 'http://example.com' |
|
||||
s.license = { :file => '../LICENSE' } |
|
||||
s.author = { 'Your Company' => 'email@example.com' } |
|
||||
s.source = { :path => '.' } |
|
||||
s.source_files = 'Classes/**/*' |
|
||||
s.dependency 'Flutter' |
|
||||
s.platform = :ios, '12.0' |
|
||||
|
|
||||
# Flutter.framework does not contain a i386 slice. |
|
||||
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } |
|
||||
s.swift_version = '5.0' |
|
||||
|
|
||||
# If your plugin requires a privacy manifest, for example if it uses any |
|
||||
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your |
|
||||
# plugin's privacy impact, and then uncomment this line. For more information, |
|
||||
# see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files |
|
||||
# s.resource_bundles = {'jl_ota_privacy' => ['Resources/PrivacyInfo.xcprivacy']} |
|
||||
end |
|
||||
@ -1,8 +0,0 @@ |
|||||
|
|
||||
import 'jl_ota_platform_interface.dart'; |
|
||||
|
|
||||
class JlOta { |
|
||||
Future<String?> getPlatformVersion() { |
|
||||
return JlOtaPlatform.instance.getPlatformVersion(); |
|
||||
} |
|
||||
} |
|
||||
@ -1,17 +0,0 @@ |
|||||
import 'package:flutter/foundation.dart'; |
|
||||
import 'package:flutter/services.dart'; |
|
||||
|
|
||||
import 'jl_ota_platform_interface.dart'; |
|
||||
|
|
||||
/// An implementation of [JlOtaPlatform] that uses method channels. |
|
||||
class MethodChannelJlOta extends JlOtaPlatform { |
|
||||
/// The method channel used to interact with the native platform. |
|
||||
@visibleForTesting |
|
||||
final methodChannel = const MethodChannel('jl_ota'); |
|
||||
|
|
||||
@override |
|
||||
Future<String?> getPlatformVersion() async { |
|
||||
final version = await methodChannel.invokeMethod<String>('getPlatformVersion'); |
|
||||
return version; |
|
||||
} |
|
||||
} |
|
||||
@ -1,29 +0,0 @@ |
|||||
import 'package:plugin_platform_interface/plugin_platform_interface.dart'; |
|
||||
|
|
||||
import 'jl_ota_method_channel.dart'; |
|
||||
|
|
||||
abstract class JlOtaPlatform extends PlatformInterface { |
|
||||
/// Constructs a JlOtaPlatform. |
|
||||
JlOtaPlatform() : super(token: _token); |
|
||||
|
|
||||
static final Object _token = Object(); |
|
||||
|
|
||||
static JlOtaPlatform _instance = MethodChannelJlOta(); |
|
||||
|
|
||||
/// The default instance of [JlOtaPlatform] to use. |
|
||||
/// |
|
||||
/// Defaults to [MethodChannelJlOta]. |
|
||||
static JlOtaPlatform get instance => _instance; |
|
||||
|
|
||||
/// Platform-specific implementations should set this with their own |
|
||||
/// platform-specific class that extends [JlOtaPlatform] when |
|
||||
/// they register themselves. |
|
||||
static set instance(JlOtaPlatform instance) { |
|
||||
PlatformInterface.verifyToken(instance, _token); |
|
||||
_instance = instance; |
|
||||
} |
|
||||
|
|
||||
Future<String?> getPlatformVersion() { |
|
||||
throw UnimplementedError('platformVersion() has not been implemented.'); |
|
||||
} |
|
||||
} |
|
||||
@ -1,72 +0,0 @@ |
|||||
name: jl_ota |
|
||||
description: "A new Flutter project." |
|
||||
version: 0.0.1 |
|
||||
homepage: |
|
||||
|
|
||||
environment: |
|
||||
sdk: ^3.7.0 |
|
||||
flutter: '>=3.3.0' |
|
||||
|
|
||||
dependencies: |
|
||||
flutter: |
|
||||
sdk: flutter |
|
||||
plugin_platform_interface: ^2.0.2 |
|
||||
|
|
||||
dev_dependencies: |
|
||||
flutter_test: |
|
||||
sdk: flutter |
|
||||
flutter_lints: ^5.0.0 |
|
||||
|
|
||||
# For information on the generic Dart part of this file, see the |
|
||||
# following page: https://dart.dev/tools/pub/pubspec |
|
||||
|
|
||||
# The following section is specific to Flutter packages. |
|
||||
flutter: |
|
||||
# This section identifies this Flutter project as a plugin project. |
|
||||
# The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) |
|
||||
# which should be registered in the plugin registry. This is required for |
|
||||
# using method channels. |
|
||||
# The Android 'package' specifies package in which the registered class is. |
|
||||
# This is required for using method channels on Android. |
|
||||
# The 'ffiPlugin' specifies that native code should be built and bundled. |
|
||||
# This is required for using `dart:ffi`. |
|
||||
# All these are used by the tooling to maintain consistency when |
|
||||
# adding or updating assets for this project. |
|
||||
plugin: |
|
||||
platforms: |
|
||||
android: |
|
||||
package: com.yunqiinnovation.jl_ota |
|
||||
pluginClass: JlOtaPlugin |
|
||||
ios: |
|
||||
pluginClass: JlOtaPlugin |
|
||||
|
|
||||
# To add assets to your plugin package, add an assets section, like this: |
|
||||
# assets: |
|
||||
# - images/a_dot_burr.jpeg |
|
||||
# - images/a_dot_ham.jpeg |
|
||||
# |
|
||||
# For details regarding assets in packages, see |
|
||||
# https://flutter.dev/to/asset-from-package |
|
||||
# |
|
||||
# An image asset can refer to one or more resolution-specific "variants", see |
|
||||
# https://flutter.dev/to/resolution-aware-images |
|
||||
|
|
||||
# To add custom fonts to your plugin package, add a fonts section here, |
|
||||
# in this "flutter" section. Each entry in this list should have a |
|
||||
# "family" key with the font family name, and a "fonts" key with a |
|
||||
# list giving the asset and other descriptors for the font. For |
|
||||
# example: |
|
||||
# fonts: |
|
||||
# - family: Schyler |
|
||||
# fonts: |
|
||||
# - asset: fonts/Schyler-Regular.ttf |
|
||||
# - asset: fonts/Schyler-Italic.ttf |
|
||||
# style: italic |
|
||||
# - family: Trajan Pro |
|
||||
# fonts: |
|
||||
# - asset: fonts/TrajanPro.ttf |
|
||||
# - asset: fonts/TrajanPro_Bold.ttf |
|
||||
# weight: 700 |
|
||||
# |
|
||||
# For details regarding fonts in packages, see |
|
||||
# https://flutter.dev/to/font-from-package |
|
||||
Binary file not shown.
@ -1,2 +0,0 @@ |
|||||
#Wed May 21 01:03:35 CST 2025 |
|
||||
gradle.version=8.9 |
|
||||
@ -1,26 +0,0 @@ |
|||||
package com.jieli.otasdk.model |
|
||||
|
|
||||
import android.bluetooth.BluetoothDevice |
|
||||
|
|
||||
/** |
|
||||
* @author zqjasonZhong |
|
||||
* @since 2022/9/9 |
|
||||
* @email zhongzhuocheng@zh-jieli.com |
|
||||
* @desc 设备连接状态 |
|
||||
*/ |
|
||||
class DeviceConnection(val device: BluetoothDevice?, var state: Int) { |
|
||||
|
|
||||
override fun hashCode(): Int { |
|
||||
return device.hashCode() |
|
||||
} |
|
||||
|
|
||||
override fun equals(other: Any?): Boolean { |
|
||||
if (other !is DeviceConnection) return false |
|
||||
return device == other.device |
|
||||
} |
|
||||
|
|
||||
override fun toString(): String { |
|
||||
return "DeviceConnection(device=$device, state=$state)" |
|
||||
} |
|
||||
|
|
||||
} |
|
||||
@ -1,19 +1,142 @@ |
|||||
import 'ota_platform_interface.dart'; |
import 'ota_platform_interface.dart'; |
||||
|
import 'dart:async'; |
||||
|
import 'dart:typed_data'; |
||||
|
import 'package:flutter/services.dart'; |
||||
|
|
||||
|
class OtaEvent { |
||||
|
static final int NOT_STARTED = -1; // 未开始 |
||||
|
static final int VALIDATION_FILE = 0; // 验证文件 |
||||
|
static final int ONUPGRADE = 1; // 升级中 |
||||
|
static final int START_UPGRADING = 2; // 开始升级 |
||||
|
static final int UPGRADE_SUCCESS = 3; // 升级成功 |
||||
|
static final int UPGRADE_FAILED = 4; // 升级失败 |
||||
|
static final int CONNECTION_DISCONNECTION = 5; // 连接断开 |
||||
|
static final int CONNECTION_SUCCESS = 6; // 连接成功 |
||||
|
|
||||
|
/// 事件类型 |
||||
|
final String event; |
||||
|
|
||||
|
/// 输出升级状态 |
||||
|
final int? type; |
||||
|
|
||||
|
/// 输出升级进度 |
||||
|
final double? progress; |
||||
|
|
||||
|
/// 错误码 (仅在发生错误时有效) |
||||
|
final int? code; |
||||
|
|
||||
|
/// 错误信息 (仅在发生错误时有效) |
||||
|
final String? message; |
||||
|
|
||||
|
/// 构造函数 |
||||
|
OtaEvent({ |
||||
|
required this.event, |
||||
|
this.type, |
||||
|
this.progress, |
||||
|
this.code, |
||||
|
this.message, |
||||
|
}); |
||||
|
|
||||
|
/// 从Map构造对象 |
||||
|
factory OtaEvent.fromMap(Map<dynamic, dynamic> map) { |
||||
|
return OtaEvent( |
||||
|
event: map['event'], |
||||
|
type: map['type'], |
||||
|
progress: map['progress'], |
||||
|
code: map['code'], |
||||
|
message: map['message'], |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
class Ota { |
class Ota { |
||||
Future<String?> getPlatformVersion() { |
// 单例实例 |
||||
return OtaPlatform.instance.getPlatformVersion(); |
static final Ota _instance = Ota._internal(); |
||||
|
|
||||
|
// 私有构造函数 |
||||
|
Ota._internal() { |
||||
|
// 初始化时设置事件监听 |
||||
|
// _setupEventListener(); |
||||
} |
} |
||||
|
|
||||
Future<String?> searchDevices() { |
// 公共访问点 |
||||
return OtaPlatform.instance.searchDevices(); |
static Ota get instance => _instance; |
||||
|
|
||||
|
static const EventChannel _eventChannel = EventChannel( |
||||
|
'com.example.ota/events', |
||||
|
); |
||||
|
// double progress = 0.0; |
||||
|
// late int type = OtaEvent.NOT_STARTED; |
||||
|
// bool isUpgrade = false; |
||||
|
|
||||
|
/// 事件流 |
||||
|
Stream<OtaEvent>? _eventStream; |
||||
|
// bool _isEventListenerSet = false; // 防止重复监听 |
||||
|
|
||||
|
/// 获取事件流 |
||||
|
Stream<OtaEvent> get eventStream { |
||||
|
_eventStream ??= _eventChannel.receiveBroadcastStream().map((event) { |
||||
|
if (event is Map) { |
||||
|
return OtaEvent.fromMap(event); |
||||
|
} |
||||
|
throw PlatformException(code: 'INVALID_EVENT', message: '无效的事件数据'); |
||||
|
}); |
||||
|
return _eventStream!; |
||||
} |
} |
||||
|
|
||||
Future<String?> connectDevice() { |
/// 释放资源 |
||||
return OtaPlatform.instance.connectDevice(); |
void onClose() { |
||||
|
onOtaClose(); |
||||
|
// 停止事件监听 |
||||
|
_eventStream = null; |
||||
|
//_isEventListenerSet = false; |
||||
|
print("Ota 对象资源已释放"); |
||||
|
} |
||||
|
|
||||
|
// /// 内部设置事件监听(只执行一次) |
||||
|
// void _setupEventListener() { |
||||
|
// if (_isEventListenerSet) return; |
||||
|
|
||||
|
// eventStream.listen((event) { |
||||
|
// if (event.event == 'onProgress' && |
||||
|
// event.type != null && |
||||
|
// event.progress != null) { |
||||
|
// print("flutter 收到数据${event.progress}type=${type}"); |
||||
|
// progress = event.progress!; |
||||
|
// type = event.type!; |
||||
|
// } |
||||
|
// if (event.event == 'onStartOTA') { |
||||
|
// type = OtaEvent.START_UPGRADING; |
||||
|
// isUpgrade = true; // 开始升级 |
||||
|
// } |
||||
|
// if (event.event == 'onStopOTA') { |
||||
|
// type = OtaEvent.UPGRADE_SUCCESS; |
||||
|
// isUpgrade = false; |
||||
|
// } |
||||
|
// if (event.event == 'onError' && |
||||
|
// event.code != null && |
||||
|
// event.message != null) { |
||||
|
// print("flutter onError${event.code}type=${event.message}"); |
||||
|
// type = OtaEvent.UPGRADE_FAILED; |
||||
|
// isUpgrade = false; |
||||
|
// } |
||||
|
// if (event.event == 'NOCONNECTION') { |
||||
|
// type = OtaEvent.CONNECTION_DISCONNECTION; |
||||
|
// isUpgrade = false; |
||||
|
// } |
||||
|
// if (event.event == 'CONNECTION_SUCCESS') { |
||||
|
// type = OtaEvent.CONNECTION_SUCCESS; |
||||
|
// } |
||||
|
// }); |
||||
|
|
||||
|
// _isEventListenerSet = true; |
||||
|
// } |
||||
|
|
||||
|
Future<String?> startUpgrading(String? filePath) { |
||||
|
return OtaPlatform.instance.startUpgrading(filePath); |
||||
} |
} |
||||
|
|
||||
Future<String?> startUpgrade() { |
Future<String?> onOtaClose() { |
||||
return OtaPlatform.instance.startUpgrade(); |
return OtaPlatform.instance.onOtaClose(); |
||||
} |
} |
||||
} |
} |
||||
|
|||||
Loading…
Reference in new issue