20 changed files with 2065 additions and 5 deletions
@ -0,0 +1,84 @@ |
|||||
|
# iOS 集成指南(旧版 CocoaPods 项目)- Chat API Plugin |
||||
|
|
||||
|
> **注意**:本指南仅适用于未启用 Swift Package Manager 的旧版 Flutter 项目。 |
||||
|
> |
||||
|
> 如果你的 Flutter 版本 >= 3.24,强烈建议启用 SPM 支持: |
||||
|
> ```bash |
||||
|
> flutter config --enable-swift-package-manager |
||||
|
> ``` |
||||
|
> 启用后,插件会自动处理所有依赖,无需手动配置。 |
||||
|
|
||||
|
## 手动集成步骤(仅限 CocoaPods 项目) |
||||
|
|
||||
|
由于 Flutter 插件中 CocoaPods 和 Swift Package Manager 的兼容性限制,需要手动在 Xcode 中添加 OpenAI 包依赖。 |
||||
|
|
||||
|
## 步骤说明 |
||||
|
|
||||
|
### 1. 运行 pod install |
||||
|
|
||||
|
首先在项目根目录运行: |
||||
|
|
||||
|
```bash |
||||
|
cd ios |
||||
|
pod install |
||||
|
``` |
||||
|
|
||||
|
### 2. 打开 Xcode 项目 |
||||
|
|
||||
|
打开生成的 workspace 文件: |
||||
|
|
||||
|
```bash |
||||
|
open Runner.xcworkspace |
||||
|
``` |
||||
|
|
||||
|
### 3. 添加 Swift Package 依赖 |
||||
|
|
||||
|
1. 在 Xcode 中,选择左侧的 `Runner` 项目(蓝色图标) |
||||
|
2. 在主编辑区域,选择 `Runner` target |
||||
|
3. 点击顶部的 `Package Dependencies` 标签 |
||||
|
4. 点击左下角的 `+` 按钮 |
||||
|
5. 在弹出的窗口中输入:`https://github.com/MacPaw/OpenAI.git` |
||||
|
6. 点击 `Add Package` |
||||
|
7. 在版本选择界面: |
||||
|
- 选择 `Up to Next Major Version` |
||||
|
- 起始版本填写:`0.4.0` |
||||
|
8. 点击 `Add Package` |
||||
|
9. 在包产品选择界面,确保 `OpenAI` 被选中,并且目标是 `Runner` |
||||
|
10. 点击 `Add Package` |
||||
|
|
||||
|
### 4. 验证集成 |
||||
|
|
||||
|
1. 在 Xcode 左侧导航栏中,展开 `Package Dependencies` |
||||
|
2. 确认能看到 `OpenAI` 包 |
||||
|
3. 尝试构建项目(Cmd + B)确保没有错误 |
||||
|
|
||||
|
### 5. 可能的问题 |
||||
|
|
||||
|
#### 问题:构建时提示找不到 OpenAI 模块 |
||||
|
|
||||
|
**解决方案**: |
||||
|
1. 清理构建缓存:`Product > Clean Build Folder` |
||||
|
2. 关闭 Xcode |
||||
|
3. 删除 `DerivedData`: |
||||
|
```bash |
||||
|
rm -rf ~/Library/Developer/Xcode/DerivedData |
||||
|
``` |
||||
|
4. 重新打开项目并构建 |
||||
|
|
||||
|
#### 问题:包依赖下载失败 |
||||
|
|
||||
|
**解决方案**: |
||||
|
1. 检查网络连接 |
||||
|
2. 在 Xcode 中:`File > Packages > Reset Package Caches` |
||||
|
3. 重新添加包依赖 |
||||
|
|
||||
|
## 注意事项 |
||||
|
|
||||
|
- 每次运行 `pod install` 后,包依赖配置会保留 |
||||
|
- 如果切换了 Xcode 版本,可能需要重新解析包依赖 |
||||
|
- 建议将 `.xcworkspace` 文件加入版本控制,但排除 `xcuserdata` 目录 |
||||
|
|
||||
|
## 相关链接 |
||||
|
|
||||
|
- [MacPaw OpenAI Swift 库](https://github.com/MacPaw/OpenAI) |
||||
|
- [Swift Package Manager 文档](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app) |
||||
@ -0,0 +1,239 @@ |
|||||
|
# Chat API Plugin |
||||
|
|
||||
|
基于 MacPaw OpenAI Swift 库的 Flutter 聊天 API 插件。 |
||||
|
|
||||
|
## 功能特性 |
||||
|
|
||||
|
- ✅ 支持 OpenAI Chat API 的完整功能 |
||||
|
- ✅ 流式输出支持 |
||||
|
- ✅ 函数调用支持 |
||||
|
- ✅ 多模态支持(文本、图片) |
||||
|
- ✅ 自定义模型支持 |
||||
|
- ✅ Token 计数估算 |
||||
|
- ✅ 支持自定义 API 端点 |
||||
|
- ✅ 自动 Swift Package Manager (SPM) 集成 |
||||
|
|
||||
|
## 安装 |
||||
|
|
||||
|
### 1. 启用 Swift Package Manager |
||||
|
|
||||
|
确保你的 Flutter 版本 >= 3.24,并启用 SPM 支持: |
||||
|
|
||||
|
```bash |
||||
|
flutter config --enable-swift-package-manager |
||||
|
``` |
||||
|
|
||||
|
### 2. 添加依赖 |
||||
|
|
||||
|
在你的 `pubspec.yaml` 文件中添加: |
||||
|
|
||||
|
```yaml |
||||
|
dependencies: |
||||
|
chat_api: |
||||
|
path: ../local_plugins/chat_api |
||||
|
``` |
||||
|
|
||||
|
### 3. iOS 配置 |
||||
|
|
||||
|
运行以下命令,Flutter 会自动处理 SPM 依赖: |
||||
|
|
||||
|
```bash |
||||
|
cd your_project |
||||
|
flutter pub get |
||||
|
cd ios |
||||
|
flutter run |
||||
|
``` |
||||
|
|
||||
|
首次运行时,Flutter 会自动: |
||||
|
- 迁移项目到 SPM 结构(如果还没有迁移) |
||||
|
- 下载并集成 MacPaw OpenAI Swift 包 |
||||
|
- 配置所有必要的依赖 |
||||
|
|
||||
|
**注意**:不再需要手动在 Xcode 中添加包依赖! |
||||
|
|
||||
|
### 4. Android 配置 |
||||
|
|
||||
|
当前 Android 端仅提供占位实现,如需完整功能请使用 iOS 平台。 |
||||
|
|
||||
|
## 使用方法 |
||||
|
|
||||
|
### 初始化 |
||||
|
|
||||
|
```dart |
||||
|
import 'package:chat_api/chat_api.dart'; |
||||
|
|
||||
|
final chatApi = ChatApi(); |
||||
|
|
||||
|
// 初始化 |
||||
|
await chatApi.initialize( |
||||
|
apiKey: 'your-api-key', |
||||
|
organization: 'your-org-id', // 可选 |
||||
|
model: 'gpt-3.5-turbo', // 默认模型 |
||||
|
baseUrl: 'https://api.openai.com/v1/chat/completions', // 可选,自定义端点 |
||||
|
); |
||||
|
``` |
||||
|
|
||||
|
### 发送消息(非流式) |
||||
|
|
||||
|
```dart |
||||
|
// 创建消息列表 |
||||
|
final messages = [ |
||||
|
ChatMessage( |
||||
|
role: MessageRole.system, |
||||
|
content: '你是一个有帮助的助手。', |
||||
|
), |
||||
|
ChatMessage( |
||||
|
role: MessageRole.user, |
||||
|
content: '你好,请介绍一下你自己。', |
||||
|
), |
||||
|
]; |
||||
|
|
||||
|
// 发送消息 |
||||
|
try { |
||||
|
final response = await chatApi.sendMessage( |
||||
|
messages: messages, |
||||
|
temperature: 0.7, |
||||
|
maxTokens: 1000, |
||||
|
); |
||||
|
print('回复: $response'); |
||||
|
} catch (e) { |
||||
|
print('错误: $e'); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### 发送消息(流式) |
||||
|
|
||||
|
```dart |
||||
|
// 监听事件流 |
||||
|
chatApi.eventStream.listen((event) { |
||||
|
switch (event.type) { |
||||
|
case ChatApiEventType.token: |
||||
|
// 收到新的token |
||||
|
print(event.content); |
||||
|
break; |
||||
|
case ChatApiEventType.complete: |
||||
|
// 对话完成 |
||||
|
print('完成'); |
||||
|
break; |
||||
|
case ChatApiEventType.error: |
||||
|
// 发生错误 |
||||
|
print('错误: ${event.content}'); |
||||
|
break; |
||||
|
case ChatApiEventType.functionCall: |
||||
|
// 函数调用 |
||||
|
print('函数调用: ${event.content}'); |
||||
|
break; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// 发送流式消息 |
||||
|
await chatApi.sendMessageStream( |
||||
|
messages: messages, |
||||
|
temperature: 0.7, |
||||
|
); |
||||
|
``` |
||||
|
|
||||
|
### 函数调用 |
||||
|
|
||||
|
```dart |
||||
|
// 定义函数 |
||||
|
final functions = [ |
||||
|
{ |
||||
|
'name': 'get_weather', |
||||
|
'description': '获取指定位置的天气', |
||||
|
'parameters': { |
||||
|
'type': 'object', |
||||
|
'properties': { |
||||
|
'location': { |
||||
|
'type': 'string', |
||||
|
'description': '城市名称', |
||||
|
}, |
||||
|
}, |
||||
|
'required': ['location'], |
||||
|
}, |
||||
|
}, |
||||
|
]; |
||||
|
|
||||
|
// 发送带函数的消息 |
||||
|
await chatApi.sendMessageStream( |
||||
|
messages: messages, |
||||
|
functions: functions, |
||||
|
); |
||||
|
``` |
||||
|
|
||||
|
### 多模态支持 |
||||
|
|
||||
|
```dart |
||||
|
// 发送带图片的消息 |
||||
|
final imageMessage = ChatMessage( |
||||
|
role: MessageRole.user, |
||||
|
content: [ |
||||
|
{ |
||||
|
'type': 'text', |
||||
|
'text': '这张图片里有什么?', |
||||
|
}, |
||||
|
{ |
||||
|
'type': 'image_url', |
||||
|
'image_url': { |
||||
|
'url': 'data:image/jpeg;base64,${base64EncodedImage}', |
||||
|
}, |
||||
|
}, |
||||
|
], |
||||
|
); |
||||
|
``` |
||||
|
|
||||
|
### 其他功能 |
||||
|
|
||||
|
```dart |
||||
|
// 获取可用模型列表 |
||||
|
final models = await chatApi.getAvailableModels(); |
||||
|
|
||||
|
// 设置模型 |
||||
|
await chatApi.setModel('gpt-4'); |
||||
|
|
||||
|
// 估算token数量 |
||||
|
final tokenCount = await chatApi.countTokens('这是一段测试文本'); |
||||
|
|
||||
|
// 取消流式请求 |
||||
|
await chatApi.cancelStream(); |
||||
|
``` |
||||
|
|
||||
|
## 兼容性说明 |
||||
|
|
||||
|
### Swift Package Manager 项目 |
||||
|
|
||||
|
如果你的项目已启用 SPM(Flutter 3.24+),插件会自动通过 SPM 集成,无需额外配置。 |
||||
|
|
||||
|
### CocoaPods 项目 |
||||
|
|
||||
|
如果你的项目仍在使用 CocoaPods(或有其他插件未迁移到 SPM),插件会自动降级到 CocoaPods 模式。这种情况下,你需要手动在 Xcode 中添加 OpenAI 包依赖。详见 `IOS_INTEGRATION_LEGACY.md`。 |
||||
|
|
||||
|
## 故障排除 |
||||
|
|
||||
|
### 问题:构建失败,提示找不到 OpenAI 模块 |
||||
|
|
||||
|
**解决方案**: |
||||
|
1. 确保已启用 SPM:`flutter config --enable-swift-package-manager` |
||||
|
2. 清理并重新构建: |
||||
|
```bash |
||||
|
flutter clean |
||||
|
cd ios |
||||
|
rm -rf Pods Podfile.lock |
||||
|
flutter pub get |
||||
|
flutter run |
||||
|
``` |
||||
|
|
||||
|
### 问题:Xcode 中看不到 Package Dependencies |
||||
|
|
||||
|
**解决方案**: |
||||
|
这可能意味着项目还在使用 CocoaPods 模式。运行 `flutter run` 应该会自动迁移到 SPM。 |
||||
|
|
||||
|
## 注意事项 |
||||
|
|
||||
|
1. **API 密钥安全**:不要将 API 密钥硬编码在代码中,建议使用环境变量或安全存储。 |
||||
|
2. **错误处理**:始终使用 try-catch 包装 API 调用以处理可能的错误。 |
||||
|
3. **流式请求**:记得在不需要时取消流式请求,避免资源浪费。 |
||||
|
|
||||
|
## 许可证 |
||||
|
|
||||
|
MIT License |
||||
@ -0,0 +1,108 @@ |
|||||
|
# Chat API 插件 SPM 迁移说明 |
||||
|
|
||||
|
## 迁移概述 |
||||
|
|
||||
|
`chat_api` 插件已经完成了 Swift Package Manager (SPM) 支持的迁移,现在可以自动处理 OpenAI Swift 包的依赖。 |
||||
|
|
||||
|
## 主要变更 |
||||
|
|
||||
|
### 1. 目录结构调整 |
||||
|
|
||||
|
``` |
||||
|
ios/ |
||||
|
├── chat_api.podspec # 保留,支持双轨模式 |
||||
|
├── Classes/ # 已清空(源码已迁移) |
||||
|
└── chat_api/ # 新增 SPM 结构 |
||||
|
├── Package.swift # SPM 包定义 |
||||
|
└── Sources/ |
||||
|
└── chat_api/ # 源码新位置 |
||||
|
├── ChatApiPlugin.swift |
||||
|
└── ChatService.swift |
||||
|
``` |
||||
|
|
||||
|
### 2. Package.swift 配置 |
||||
|
|
||||
|
- 声明了 MacPaw OpenAI 包依赖 |
||||
|
- 自动管理版本(0.4.0+) |
||||
|
- 设置最低 iOS 版本为 13.0 |
||||
|
|
||||
|
### 3. podspec 更新 |
||||
|
|
||||
|
- 更新了 `source_files` 路径指向新的 SPM 结构 |
||||
|
- 添加了友好的迁移提示脚本 |
||||
|
|
||||
|
### 4. pubspec.yaml 更新 |
||||
|
|
||||
|
- 添加了 `dartPluginClass` 声明,明确支持 SPM |
||||
|
|
||||
|
## 使用方式对比 |
||||
|
|
||||
|
### 旧方式(手动集成) |
||||
|
|
||||
|
1. 运行 `pod install` |
||||
|
2. 打开 Xcode |
||||
|
3. 手动添加 OpenAI Swift Package |
||||
|
4. 配置依赖关系 |
||||
|
|
||||
|
### 新方式(自动集成) |
||||
|
|
||||
|
1. 启用 SPM:`flutter config --enable-swift-package-manager` |
||||
|
2. 运行 `flutter run` |
||||
|
3. 完成!Flutter 自动处理所有依赖 |
||||
|
|
||||
|
## 兼容性 |
||||
|
|
||||
|
- **Flutter 3.24+**:完整 SPM 支持,推荐使用 |
||||
|
- **Flutter < 3.24**:自动降级到 CocoaPods 模式 |
||||
|
- **混合项目**:如果项目中有其他插件未迁移到 SPM,Flutter 会智能选择合适的集成方式 |
||||
|
|
||||
|
## 验证集成 |
||||
|
|
||||
|
### SPM 项目 |
||||
|
|
||||
|
```bash |
||||
|
# 在 example 目录下 |
||||
|
flutter clean |
||||
|
flutter pub get |
||||
|
flutter run --verbose |
||||
|
``` |
||||
|
|
||||
|
成功标志: |
||||
|
- 看到 "Adding Swift Package Manager integration..." 消息 |
||||
|
- Xcode 中 Package Dependencies 自动出现 OpenAI 包 |
||||
|
- 构建成功,无需手动干预 |
||||
|
|
||||
|
### CocoaPods 项目 |
||||
|
|
||||
|
如果项目仍使用 CocoaPods,会看到: |
||||
|
- podspec 中的迁移提示 |
||||
|
- 需要按照 `IOS_INTEGRATION_LEGACY.md` 手动添加依赖 |
||||
|
|
||||
|
## 故障排除 |
||||
|
|
||||
|
1. **清理缓存** |
||||
|
```bash |
||||
|
flutter clean |
||||
|
rm -rf ios/Pods ios/Podfile.lock |
||||
|
rm -rf ~/Library/Developer/Xcode/DerivedData |
||||
|
``` |
||||
|
|
||||
|
2. **重置 SPM 缓存** |
||||
|
- Xcode: File > Packages > Reset Package Caches |
||||
|
|
||||
|
3. **确认 SPM 已启用** |
||||
|
```bash |
||||
|
flutter config |
||||
|
# 应该看到 enable-swift-package-manager: true |
||||
|
``` |
||||
|
|
||||
|
## 开发者注意事项 |
||||
|
|
||||
|
1. **源码位置**:所有 Swift 代码现在位于 `ios/chat_api/Sources/chat_api/` |
||||
|
2. **双轨支持**:保持 podspec 和 Package.swift 同时更新 |
||||
|
3. **版本管理**:OpenAI 包版本在 Package.swift 中管理 |
||||
|
|
||||
|
## 未来计划 |
||||
|
|
||||
|
- 当 Flutter 生态系统完全迁移到 SPM 后,可以考虑移除 CocoaPods 支持 |
||||
|
- 持续跟踪 MacPaw OpenAI 库的更新 |
||||
@ -0,0 +1,64 @@ |
|||||
|
group 'com.yunqiinnovation.chat_api' |
||||
|
version '1.0' |
||||
|
|
||||
|
buildscript { |
||||
|
ext.kotlin_version = '1.7.10' |
||||
|
repositories { |
||||
|
google() |
||||
|
mavenCentral() |
||||
|
} |
||||
|
|
||||
|
dependencies { |
||||
|
classpath 'com.android.tools.build:gradle:7.3.0' |
||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
allprojects { |
||||
|
repositories { |
||||
|
google() |
||||
|
mavenCentral() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
apply plugin: 'com.android.library' |
||||
|
apply plugin: 'kotlin-android' |
||||
|
|
||||
|
android { |
||||
|
compileSdkVersion 33 |
||||
|
|
||||
|
compileOptions { |
||||
|
sourceCompatibility JavaVersion.VERSION_1_8 |
||||
|
targetCompatibility JavaVersion.VERSION_1_8 |
||||
|
} |
||||
|
|
||||
|
kotlinOptions { |
||||
|
jvmTarget = '1.8' |
||||
|
} |
||||
|
|
||||
|
sourceSets { |
||||
|
main.java.srcDirs += 'src/main/kotlin' |
||||
|
test.java.srcDirs += 'src/test/kotlin' |
||||
|
} |
||||
|
|
||||
|
defaultConfig { |
||||
|
minSdkVersion 21 |
||||
|
} |
||||
|
|
||||
|
dependencies { |
||||
|
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 |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" |
||||
|
package="com.yunqiinnovation.chat_api"> |
||||
|
</manifest> |
||||
@ -0,0 +1,71 @@ |
|||||
|
package com.yunqiinnovation.chat_api |
||||
|
|
||||
|
import androidx.annotation.NonNull |
||||
|
import io.flutter.embedding.engine.plugins.FlutterPlugin |
||||
|
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 io.flutter.plugin.common.EventChannel |
||||
|
|
||||
|
/** ChatApiPlugin */ |
||||
|
class ChatApiPlugin: FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler { |
||||
|
/// 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 methodChannel : MethodChannel |
||||
|
private lateinit var eventChannel : EventChannel |
||||
|
private var eventSink: EventChannel.EventSink? = null |
||||
|
|
||||
|
override fun onAttachedToEngine(@NonNull 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) |
||||
|
} |
||||
|
|
||||
|
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { |
||||
|
when (call.method) { |
||||
|
"initialize" -> { |
||||
|
// TODO: Implement Android version using OkHttp or Retrofit |
||||
|
result.success(true) |
||||
|
} |
||||
|
"sendMessage" -> { |
||||
|
result.error("NOT_IMPLEMENTED", "Android implementation not available", null) |
||||
|
} |
||||
|
"sendMessageStream" -> { |
||||
|
result.error("NOT_IMPLEMENTED", "Android implementation not available", null) |
||||
|
} |
||||
|
"cancelStream" -> { |
||||
|
result.success(true) |
||||
|
} |
||||
|
"getAvailableModels" -> { |
||||
|
result.success(emptyList<String>()) |
||||
|
} |
||||
|
"setModel" -> { |
||||
|
result.success(true) |
||||
|
} |
||||
|
"countTokens" -> { |
||||
|
result.success(0) |
||||
|
} |
||||
|
else -> { |
||||
|
result.notImplemented() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { |
||||
|
methodChannel.setMethodCallHandler(null) |
||||
|
eventChannel.setStreamHandler(null) |
||||
|
} |
||||
|
|
||||
|
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { |
||||
|
eventSink = events |
||||
|
} |
||||
|
|
||||
|
override fun onCancel(arguments: Any?) { |
||||
|
eventSink = null |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,266 @@ |
|||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:chat_api/chat_api.dart'; |
||||
|
|
||||
|
void main() { |
||||
|
runApp(const MyApp()); |
||||
|
} |
||||
|
|
||||
|
class MyApp extends StatelessWidget { |
||||
|
const MyApp({Key? key}) : super(key: key); |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
return MaterialApp( |
||||
|
title: 'Chat API Demo', |
||||
|
theme: ThemeData( |
||||
|
primarySwatch: Colors.blue, |
||||
|
), |
||||
|
home: const ChatDemo(), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
class ChatDemo extends StatefulWidget { |
||||
|
const ChatDemo({Key? key}) : super(key: key); |
||||
|
|
||||
|
@override |
||||
|
State<ChatDemo> createState() => _ChatDemoState(); |
||||
|
} |
||||
|
|
||||
|
class _ChatDemoState extends State<ChatDemo> { |
||||
|
final ChatApi _chatApi = ChatApi(); |
||||
|
final TextEditingController _controller = TextEditingController(); |
||||
|
final List<ChatMessage> _messages = []; |
||||
|
String _response = ''; |
||||
|
bool _isLoading = false; |
||||
|
bool _isStreaming = false; |
||||
|
|
||||
|
@override |
||||
|
void initState() { |
||||
|
super.initState(); |
||||
|
_initializeChatApi(); |
||||
|
_listenToStream(); |
||||
|
} |
||||
|
|
||||
|
Future<void> _initializeChatApi() async { |
||||
|
try { |
||||
|
// 注意:请替换为你自己的API密钥 |
||||
|
await _chatApi.initialize( |
||||
|
apiKey: 'your-api-key-here', |
||||
|
model: 'gpt-3.5-turbo', |
||||
|
); |
||||
|
print('Chat API 初始化成功'); |
||||
|
} catch (e) { |
||||
|
print('初始化失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void _listenToStream() { |
||||
|
_chatApi.eventStream.listen((event) { |
||||
|
switch (event.type) { |
||||
|
case ChatApiEventType.token: |
||||
|
setState(() { |
||||
|
_response += event.content; |
||||
|
}); |
||||
|
break; |
||||
|
case ChatApiEventType.complete: |
||||
|
setState(() { |
||||
|
_isStreaming = false; |
||||
|
_messages.add(ChatMessage( |
||||
|
role: MessageRole.assistant, |
||||
|
content: _response, |
||||
|
)); |
||||
|
}); |
||||
|
break; |
||||
|
case ChatApiEventType.error: |
||||
|
setState(() { |
||||
|
_isStreaming = false; |
||||
|
_response = '错误: ${event.content}'; |
||||
|
}); |
||||
|
break; |
||||
|
case ChatApiEventType.functionCall: |
||||
|
print('函数调用: ${event.content}'); |
||||
|
break; |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
Future<void> _sendMessage() async { |
||||
|
if (_controller.text.isEmpty) return; |
||||
|
|
||||
|
final userMessage = ChatMessage( |
||||
|
role: MessageRole.user, |
||||
|
content: _controller.text, |
||||
|
); |
||||
|
|
||||
|
setState(() { |
||||
|
_messages.add(userMessage); |
||||
|
_response = ''; |
||||
|
_isStreaming = true; |
||||
|
}); |
||||
|
|
||||
|
_controller.clear(); |
||||
|
|
||||
|
try { |
||||
|
await _chatApi.sendMessageStream( |
||||
|
messages: _messages, |
||||
|
temperature: 0.7, |
||||
|
); |
||||
|
} catch (e) { |
||||
|
setState(() { |
||||
|
_isStreaming = false; |
||||
|
_response = '发送失败: $e'; |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Future<void> _sendNonStreamMessage() async { |
||||
|
if (_controller.text.isEmpty) return; |
||||
|
|
||||
|
final userMessage = ChatMessage( |
||||
|
role: MessageRole.user, |
||||
|
content: _controller.text, |
||||
|
); |
||||
|
|
||||
|
setState(() { |
||||
|
_messages.add(userMessage); |
||||
|
_isLoading = true; |
||||
|
}); |
||||
|
|
||||
|
_controller.clear(); |
||||
|
|
||||
|
try { |
||||
|
final response = await _chatApi.sendMessage( |
||||
|
messages: _messages, |
||||
|
temperature: 0.7, |
||||
|
); |
||||
|
|
||||
|
setState(() { |
||||
|
_messages.add(ChatMessage( |
||||
|
role: MessageRole.assistant, |
||||
|
content: response, |
||||
|
)); |
||||
|
_isLoading = false; |
||||
|
}); |
||||
|
} catch (e) { |
||||
|
setState(() { |
||||
|
_isLoading = false; |
||||
|
}); |
||||
|
ScaffoldMessenger.of(context).showSnackBar( |
||||
|
SnackBar(content: Text('发送失败: $e')), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
return Scaffold( |
||||
|
appBar: AppBar( |
||||
|
title: const Text('Chat API Demo'), |
||||
|
actions: [ |
||||
|
IconButton( |
||||
|
icon: const Icon(Icons.delete), |
||||
|
onPressed: () { |
||||
|
setState(() { |
||||
|
_messages.clear(); |
||||
|
_response = ''; |
||||
|
}); |
||||
|
}, |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
body: Column( |
||||
|
children: [ |
||||
|
Expanded( |
||||
|
child: ListView.builder( |
||||
|
padding: const EdgeInsets.all(16), |
||||
|
itemCount: _messages.length + (_isStreaming ? 1 : 0), |
||||
|
itemBuilder: (context, index) { |
||||
|
if (index == _messages.length && _isStreaming) { |
||||
|
return _buildMessage( |
||||
|
MessageRole.assistant, |
||||
|
_response.isEmpty ? '...' : _response, |
||||
|
); |
||||
|
} |
||||
|
final message = _messages[index]; |
||||
|
return _buildMessage(message.role, message.content.toString()); |
||||
|
}, |
||||
|
), |
||||
|
), |
||||
|
if (_isLoading) |
||||
|
const LinearProgressIndicator(), |
||||
|
Container( |
||||
|
padding: const EdgeInsets.all(16), |
||||
|
decoration: BoxDecoration( |
||||
|
color: Colors.grey[100], |
||||
|
boxShadow: [ |
||||
|
BoxShadow( |
||||
|
color: Colors.black.withOpacity(0.1), |
||||
|
blurRadius: 4, |
||||
|
offset: const Offset(0, -2), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
child: Row( |
||||
|
children: [ |
||||
|
Expanded( |
||||
|
child: TextField( |
||||
|
controller: _controller, |
||||
|
decoration: const InputDecoration( |
||||
|
hintText: '输入消息...', |
||||
|
border: OutlineInputBorder(), |
||||
|
), |
||||
|
onSubmitted: (_) => _sendMessage(), |
||||
|
), |
||||
|
), |
||||
|
const SizedBox(width: 8), |
||||
|
IconButton( |
||||
|
icon: const Icon(Icons.send), |
||||
|
onPressed: _isStreaming || _isLoading ? null : _sendMessage, |
||||
|
color: Colors.blue, |
||||
|
), |
||||
|
IconButton( |
||||
|
icon: const Icon(Icons.send_and_archive), |
||||
|
onPressed: _isStreaming || _isLoading ? null : _sendNonStreamMessage, |
||||
|
color: Colors.green, |
||||
|
tooltip: '非流式发送', |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
Widget _buildMessage(MessageRole role, String content) { |
||||
|
final isUser = role == MessageRole.user; |
||||
|
return Container( |
||||
|
margin: const EdgeInsets.only(bottom: 8), |
||||
|
alignment: isUser ? Alignment.centerRight : Alignment.centerLeft, |
||||
|
child: Container( |
||||
|
padding: const EdgeInsets.all(12), |
||||
|
decoration: BoxDecoration( |
||||
|
color: isUser ? Colors.blue : Colors.grey[300], |
||||
|
borderRadius: BorderRadius.circular(16), |
||||
|
), |
||||
|
constraints: BoxConstraints( |
||||
|
maxWidth: MediaQuery.of(context).size.width * 0.7, |
||||
|
), |
||||
|
child: Text( |
||||
|
content, |
||||
|
style: TextStyle( |
||||
|
color: isUser ? Colors.white : Colors.black87, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
void dispose() { |
||||
|
_controller.dispose(); |
||||
|
_chatApi.dispose(); |
||||
|
super.dispose(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
name: chat_api_example |
||||
|
description: Demonstrates how to use the chat_api plugin. |
||||
|
|
||||
|
version: 1.0.0+1 |
||||
|
|
||||
|
environment: |
||||
|
sdk: '>=2.17.0 <4.0.0' |
||||
|
|
||||
|
dependencies: |
||||
|
flutter: |
||||
|
sdk: flutter |
||||
|
|
||||
|
chat_api: |
||||
|
path: ../ |
||||
|
|
||||
|
cupertino_icons: ^1.0.2 |
||||
|
|
||||
|
dev_dependencies: |
||||
|
flutter_test: |
||||
|
sdk: flutter |
||||
|
flutter_lints: ^2.0.0 |
||||
|
|
||||
|
flutter: |
||||
|
uses-material-design: true |
||||
@ -0,0 +1,44 @@ |
|||||
|
Pod::Spec.new do |s| |
||||
|
s.name = 'chat_api' |
||||
|
s.version = '0.0.1' |
||||
|
s.summary = 'Chat API plugin based on MacPaw OpenAI Swift library' |
||||
|
s.description = <<-DESC |
||||
|
A Flutter plugin that provides chat API functionality using MacPaw OpenAI Swift library. |
||||
|
Supports streaming, function calling and all OpenAI chat features. |
||||
|
DESC |
||||
|
s.homepage = 'https://github.com/yunqiinnovation/deep_voice' |
||||
|
s.license = { :file => '../LICENSE' } |
||||
|
s.author = { 'Yunqi Innovation' => 'support@yunqiinnovation.com' } |
||||
|
s.source = { :path => '.' } |
||||
|
|
||||
|
# 重要:更新源文件路径以支持SPM结构 |
||||
|
# 如果是通过SPM集成,Flutter会自动使用Package.swift |
||||
|
# 如果是通过CocoaPods集成,使用这里的路径 |
||||
|
s.source_files = 'chat_api/Sources/chat_api/**/*' |
||||
|
|
||||
|
s.dependency 'Flutter' |
||||
|
s.platform = :ios, '13.0' |
||||
|
s.swift_version = '5.0' |
||||
|
|
||||
|
# Flutter.framework does not contain a i386 slice |
||||
|
s.pod_target_xcconfig = { |
||||
|
'DEFINES_MODULE' => 'YES', |
||||
|
'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', |
||||
|
'ENABLE_BITCODE' => 'NO' |
||||
|
} |
||||
|
|
||||
|
# 保留这个提示,以防用户仍在使用纯CocoaPods项目 |
||||
|
s.script_phases = [ |
||||
|
{ |
||||
|
:name => 'SPM Migration Notice', |
||||
|
:script => <<-SCRIPT |
||||
|
echo "========================================" |
||||
|
echo "Chat API Plugin Notice:" |
||||
|
echo "This plugin now supports Swift Package Manager (SPM)." |
||||
|
echo "For best results, ensure your Flutter project has SPM enabled:" |
||||
|
echo " flutter config --enable-swift-package-manager" |
||||
|
echo "========================================" |
||||
|
SCRIPT |
||||
|
} |
||||
|
] |
||||
|
end |
||||
@ -0,0 +1,32 @@ |
|||||
|
// swift-tools-version:5.9 |
||||
|
import PackageDescription |
||||
|
|
||||
|
let package = Package( |
||||
|
name: "chat_api", |
||||
|
platforms: [ |
||||
|
.iOS(.v13) // 与podspec中的版本保持一致 |
||||
|
], |
||||
|
products: [ |
||||
|
.library(name: "chat-api", targets: ["ChatApi"]) |
||||
|
], |
||||
|
// 声明MacPaw OpenAI Swift包依赖 |
||||
|
dependencies: [ |
||||
|
.package( |
||||
|
url: "https://github.com/MacPaw/OpenAI.git", |
||||
|
.upToNextMajor(from: "0.4.0") |
||||
|
) |
||||
|
], |
||||
|
targets: [ |
||||
|
.target( |
||||
|
name: "ChatApi", |
||||
|
// 将OpenAI产品添加到target依赖 |
||||
|
dependencies: [ |
||||
|
.product(name: "OpenAI", package: "OpenAI") |
||||
|
], |
||||
|
path: "Sources/chat_api", |
||||
|
resources: [ |
||||
|
// 如果有资源文件,在这里声明 |
||||
|
] |
||||
|
) |
||||
|
] |
||||
|
) |
||||
@ -0,0 +1,316 @@ |
|||||
|
import Flutter |
||||
|
import UIKit |
||||
|
import Foundation |
||||
|
import OpenAI |
||||
|
|
||||
|
/// 聊天API插件主类 |
||||
|
public class ChatApiPlugin: NSObject, FlutterPlugin, FlutterStreamHandler { |
||||
|
|
||||
|
// MARK: - 常量 |
||||
|
private static let methodChannelName = "com.yunqiinnovation.chat_api/methods" |
||||
|
private static let eventChannelName = "com.yunqiinnovation.chat_api/events" |
||||
|
|
||||
|
// MARK: - 属性 |
||||
|
private var methodChannel: FlutterMethodChannel? |
||||
|
private var eventChannel: FlutterEventChannel? |
||||
|
private var eventSink: FlutterEventSink? |
||||
|
private var chatService: ChatService? |
||||
|
|
||||
|
// MARK: - 插件注册 |
||||
|
public static func register(with registrar: FlutterPluginRegistrar) { |
||||
|
let instance = ChatApiPlugin() |
||||
|
|
||||
|
// 初始化方法通道 |
||||
|
let methodChannel = FlutterMethodChannel( |
||||
|
name: methodChannelName, |
||||
|
binaryMessenger: registrar.messenger() |
||||
|
) |
||||
|
instance.methodChannel = methodChannel |
||||
|
registrar.addMethodCallDelegate(instance, channel: methodChannel) |
||||
|
|
||||
|
// 初始化事件通道 |
||||
|
let eventChannel = FlutterEventChannel( |
||||
|
name: eventChannelName, |
||||
|
binaryMessenger: registrar.messenger() |
||||
|
) |
||||
|
instance.eventChannel = eventChannel |
||||
|
eventChannel.setStreamHandler(instance) |
||||
|
|
||||
|
// 初始化聊天服务 |
||||
|
instance.chatService = ChatService() |
||||
|
} |
||||
|
|
||||
|
// MARK: - FlutterPlugin |
||||
|
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
||||
|
guard let service = chatService else { |
||||
|
result(FlutterError(code: "SERVICE_NOT_INITIALIZED", |
||||
|
message: "聊天服务未初始化", |
||||
|
details: nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
switch call.method { |
||||
|
case "initialize": |
||||
|
handleInitialize(call: call, result: result, service: service) |
||||
|
|
||||
|
case "sendMessage": |
||||
|
handleSendMessage(call: call, result: result, service: service) |
||||
|
|
||||
|
case "sendMessageStream": |
||||
|
handleSendMessageStream(call: call, result: result, service: service) |
||||
|
|
||||
|
case "cancelStream": |
||||
|
handleCancelStream(call: call, result: result, service: service) |
||||
|
|
||||
|
case "getAvailableModels": |
||||
|
handleGetAvailableModels(call: call, result: result, service: service) |
||||
|
|
||||
|
case "setModel": |
||||
|
handleSetModel(call: call, result: result, service: service) |
||||
|
|
||||
|
case "countTokens": |
||||
|
handleCountTokens(call: call, result: result, service: service) |
||||
|
|
||||
|
default: |
||||
|
result(FlutterMethodNotImplemented) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - FlutterStreamHandler |
||||
|
public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { |
||||
|
self.eventSink = events |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
public func onCancel(withArguments arguments: Any?) -> FlutterError? { |
||||
|
self.eventSink = nil |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// MARK: - 方法处理 |
||||
|
|
||||
|
private func handleInitialize(call: FlutterMethodCall, result: @escaping FlutterResult, service: ChatService) { |
||||
|
guard let args = call.arguments as? [String: Any], |
||||
|
let apiKey = args["apiKey"] as? String else { |
||||
|
result(FlutterError(code: "INVALID_ARGUMENTS", |
||||
|
message: "缺少API密钥参数", |
||||
|
details: nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
let organization = args["organization"] as? String |
||||
|
let model = args["model"] as? String ?? "gpt-3.5-turbo" |
||||
|
let baseUrl = args["baseUrl"] as? String |
||||
|
|
||||
|
Task { |
||||
|
do { |
||||
|
let success = try await service.initialize( |
||||
|
apiKey: apiKey, |
||||
|
organization: organization, |
||||
|
model: model, |
||||
|
baseUrl: baseUrl |
||||
|
) |
||||
|
DispatchQueue.main.async { |
||||
|
result(success) |
||||
|
} |
||||
|
} catch { |
||||
|
DispatchQueue.main.async { |
||||
|
result(FlutterError(code: "INIT_ERROR", |
||||
|
message: "初始化失败: \(error.localizedDescription)", |
||||
|
details: nil)) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private func handleSendMessage(call: FlutterMethodCall, result: @escaping FlutterResult, service: ChatService) { |
||||
|
guard let args = call.arguments as? [String: Any], |
||||
|
let messagesArray = args["messages"] as? [[String: Any]] else { |
||||
|
result(FlutterError(code: "INVALID_ARGUMENTS", |
||||
|
message: "缺少消息参数", |
||||
|
details: nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
let parameters = extractChatParameters(from: args) |
||||
|
|
||||
|
Task { |
||||
|
do { |
||||
|
let response = try await service.sendMessage( |
||||
|
messages: messagesArray, |
||||
|
parameters: parameters |
||||
|
) |
||||
|
DispatchQueue.main.async { |
||||
|
result(response) |
||||
|
} |
||||
|
} catch { |
||||
|
DispatchQueue.main.async { |
||||
|
result(FlutterError(code: "CHAT_ERROR", |
||||
|
message: "发送消息失败: \(error.localizedDescription)", |
||||
|
details: nil)) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private func handleSendMessageStream(call: FlutterMethodCall, result: @escaping FlutterResult, service: ChatService) { |
||||
|
guard let args = call.arguments as? [String: Any], |
||||
|
let messagesArray = args["messages"] as? [[String: Any]] else { |
||||
|
result(FlutterError(code: "INVALID_ARGUMENTS", |
||||
|
message: "缺少消息参数", |
||||
|
details: nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
guard let eventSink = self.eventSink else { |
||||
|
result(FlutterError(code: "NO_EVENT_SINK", |
||||
|
message: "没有可用的事件流接收器", |
||||
|
details: nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
let parameters = extractChatParameters(from: args) |
||||
|
|
||||
|
// 创建流式回调处理器 |
||||
|
let streamHandler = StreamEventHandler(eventSink: eventSink) |
||||
|
|
||||
|
Task { |
||||
|
do { |
||||
|
await service.sendMessageStream( |
||||
|
messages: messagesArray, |
||||
|
parameters: parameters, |
||||
|
handler: streamHandler |
||||
|
) |
||||
|
DispatchQueue.main.async { |
||||
|
result(true) |
||||
|
} |
||||
|
} catch { |
||||
|
DispatchQueue.main.async { |
||||
|
result(FlutterError(code: "STREAM_ERROR", |
||||
|
message: "发送流式消息失败: \(error.localizedDescription)", |
||||
|
details: nil)) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private func handleCancelStream(call: FlutterMethodCall, result: @escaping FlutterResult, service: ChatService) { |
||||
|
service.cancelStream() |
||||
|
result(true) |
||||
|
} |
||||
|
|
||||
|
private func handleGetAvailableModels(call: FlutterMethodCall, result: @escaping FlutterResult, service: ChatService) { |
||||
|
Task { |
||||
|
do { |
||||
|
let models = try await service.getAvailableModels() |
||||
|
DispatchQueue.main.async { |
||||
|
result(models) |
||||
|
} |
||||
|
} catch { |
||||
|
DispatchQueue.main.async { |
||||
|
result(FlutterError(code: "MODEL_ERROR", |
||||
|
message: "获取模型列表失败: \(error.localizedDescription)", |
||||
|
details: nil)) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private func handleSetModel(call: FlutterMethodCall, result: @escaping FlutterResult, service: ChatService) { |
||||
|
guard let args = call.arguments as? [String: Any], |
||||
|
let model = args["model"] as? String else { |
||||
|
result(FlutterError(code: "INVALID_ARGUMENTS", |
||||
|
message: "缺少模型参数", |
||||
|
details: nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
service.setModel(model) |
||||
|
result(true) |
||||
|
} |
||||
|
|
||||
|
private func handleCountTokens(call: FlutterMethodCall, result: @escaping FlutterResult, service: ChatService) { |
||||
|
guard let args = call.arguments as? [String: Any], |
||||
|
let text = args["text"] as? String else { |
||||
|
result(FlutterError(code: "INVALID_ARGUMENTS", |
||||
|
message: "缺少文本参数", |
||||
|
details: nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 简单的token估算(实际应该使用tiktoken或类似库) |
||||
|
let estimatedTokens = service.estimateTokens(text: text) |
||||
|
result(estimatedTokens) |
||||
|
} |
||||
|
|
||||
|
// MARK: - 辅助方法 |
||||
|
|
||||
|
private func extractChatParameters(from args: [String: Any]) -> ChatParameters { |
||||
|
return ChatParameters( |
||||
|
temperature: args["temperature"] as? Double ?? 0.7, |
||||
|
maxTokens: args["maxTokens"] as? Int, |
||||
|
topP: args["topP"] as? Double ?? 1.0, |
||||
|
n: args["n"] as? Int ?? 1, |
||||
|
stop: args["stop"] as? [String], |
||||
|
presencePenalty: args["presencePenalty"] as? Double ?? 0.0, |
||||
|
frequencyPenalty: args["frequencyPenalty"] as? Double ?? 0.0, |
||||
|
functions: args["functions"] as? [[String: Any]] |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - 聊天参数 |
||||
|
struct ChatParameters { |
||||
|
let temperature: Double |
||||
|
let maxTokens: Int? |
||||
|
let topP: Double |
||||
|
let n: Int |
||||
|
let stop: [String]? |
||||
|
let presencePenalty: Double |
||||
|
let frequencyPenalty: Double |
||||
|
let functions: [[String: Any]]? |
||||
|
} |
||||
|
|
||||
|
// MARK: - 流式事件处理器 |
||||
|
private class StreamEventHandler: ChatStreamHandler { |
||||
|
private let eventSink: FlutterEventSink |
||||
|
|
||||
|
init(eventSink: @escaping FlutterEventSink) { |
||||
|
self.eventSink = eventSink |
||||
|
} |
||||
|
|
||||
|
func onToken(_ token: String) { |
||||
|
DispatchQueue.main.async { |
||||
|
self.eventSink([ |
||||
|
"type": "token", |
||||
|
"content": token |
||||
|
]) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func onComplete() { |
||||
|
DispatchQueue.main.async { |
||||
|
self.eventSink([ |
||||
|
"type": "complete" |
||||
|
]) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func onError(_ error: Error) { |
||||
|
DispatchQueue.main.async { |
||||
|
self.eventSink([ |
||||
|
"type": "error", |
||||
|
"content": error.localizedDescription |
||||
|
]) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func onFunctionCall(_ functionCall: [String: Any]) { |
||||
|
DispatchQueue.main.async { |
||||
|
self.eventSink([ |
||||
|
"type": "functionCall", |
||||
|
"content": functionCall |
||||
|
]) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,363 @@ |
|||||
|
import Foundation |
||||
|
import OpenAI |
||||
|
|
||||
|
/// 聊天流处理器协议 |
||||
|
protocol ChatStreamHandler { |
||||
|
func onToken(_ token: String) |
||||
|
func onComplete() |
||||
|
func onError(_ error: Error) |
||||
|
func onFunctionCall(_ functionCall: [String: Any]) |
||||
|
} |
||||
|
|
||||
|
/// 聊天服务 |
||||
|
class ChatService { |
||||
|
|
||||
|
// MARK: - 属性 |
||||
|
private var openAI: OpenAI? |
||||
|
private var currentModel: Model = .gpt3_5Turbo |
||||
|
private var currentTask: Task<Void, Never>? |
||||
|
|
||||
|
// MARK: - 初始化 |
||||
|
|
||||
|
/// 初始化OpenAI客户端 |
||||
|
func initialize(apiKey: String, organization: String?, model: String, baseUrl: String?) async throws -> Bool { |
||||
|
// 创建配置 |
||||
|
var configuration = OpenAI.Configuration(token: apiKey) |
||||
|
|
||||
|
// 设置组织ID |
||||
|
if let organization = organization { |
||||
|
configuration.organizationIdentifier = organization |
||||
|
} |
||||
|
|
||||
|
// 设置自定义基础URL |
||||
|
if let baseUrl = baseUrl, let url = URL(string: baseUrl) { |
||||
|
configuration.url = url |
||||
|
} |
||||
|
|
||||
|
// 创建OpenAI客户端 |
||||
|
openAI = OpenAI(configuration: configuration) |
||||
|
|
||||
|
// 设置模型 |
||||
|
setModel(model) |
||||
|
|
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
// MARK: - 发送消息 |
||||
|
|
||||
|
/// 发送消息(非流式) |
||||
|
func sendMessage(messages: [[String: Any]], parameters: ChatParameters) async throws -> String { |
||||
|
guard let openAI = openAI else { |
||||
|
throw ChatApiError.notInitialized |
||||
|
} |
||||
|
|
||||
|
// 转换消息格式 |
||||
|
let chatMessages = try messages.map { dict -> Chat in |
||||
|
guard let role = dict["role"] as? String, |
||||
|
let content = dict["content"] else { |
||||
|
throw ChatApiError.invalidMessage |
||||
|
} |
||||
|
|
||||
|
// 创建聊天角色 |
||||
|
let chatRole = try parseChatRole(role) |
||||
|
|
||||
|
// 处理不同类型的内容 |
||||
|
if let contentString = content as? String { |
||||
|
return Chat(role: chatRole, content: contentString) |
||||
|
} else if let contentArray = content as? [[String: Any]] { |
||||
|
// 处理多模态内容(如图片) |
||||
|
var contentItems: [ChatContent] = [] |
||||
|
for item in contentArray { |
||||
|
if let type = item["type"] as? String { |
||||
|
switch type { |
||||
|
case "text": |
||||
|
if let text = item["text"] as? String { |
||||
|
contentItems.append(.text(text)) |
||||
|
} |
||||
|
case "image_url": |
||||
|
if let imageUrl = item["image_url"] as? [String: Any], |
||||
|
let url = imageUrl["url"] as? String { |
||||
|
contentItems.append(.image(.init(url: url))) |
||||
|
} |
||||
|
default: |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return Chat(role: chatRole, content: contentItems) |
||||
|
} else { |
||||
|
throw ChatApiError.invalidMessage |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 构建查询 |
||||
|
var query = ChatQuery( |
||||
|
messages: chatMessages, |
||||
|
model: currentModel, |
||||
|
temperature: parameters.temperature, |
||||
|
topP: parameters.topP, |
||||
|
n: parameters.n, |
||||
|
stop: parameters.stop ?? [], |
||||
|
maxTokens: parameters.maxTokens, |
||||
|
presencePenalty: parameters.presencePenalty, |
||||
|
frequencyPenalty: parameters.frequencyPenalty |
||||
|
) |
||||
|
|
||||
|
// 添加函数 |
||||
|
if let functions = parameters.functions { |
||||
|
query.tools = try functions.map { dict in |
||||
|
guard let name = dict["name"] as? String, |
||||
|
let description = dict["description"] as? String, |
||||
|
let parameters = dict["parameters"] as? [String: Any] else { |
||||
|
throw ChatApiError.invalidFunction |
||||
|
} |
||||
|
|
||||
|
return ChatQuery.ChatCompletionToolParam( |
||||
|
function: .init( |
||||
|
name: name, |
||||
|
description: description, |
||||
|
parameters: parameters |
||||
|
) |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 发送请求 |
||||
|
let result = try await openAI.chats(query: query) |
||||
|
|
||||
|
// 处理函数调用 |
||||
|
if let toolCalls = result.choices.first?.message.toolCalls, |
||||
|
let toolCall = toolCalls.first { |
||||
|
// 返回函数调用信息 |
||||
|
let functionCall: [String: Any] = [ |
||||
|
"name": toolCall.function.name, |
||||
|
"arguments": toolCall.function.arguments ?? "", |
||||
|
"id": toolCall.id |
||||
|
] |
||||
|
|
||||
|
if let jsonData = try? JSONSerialization.data(withJSONObject: functionCall), |
||||
|
let jsonString = String(data: jsonData, encoding: .utf8) { |
||||
|
return jsonString |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 返回普通消息内容 |
||||
|
return result.choices.first?.message.content?.string ?? "" |
||||
|
} |
||||
|
|
||||
|
/// 发送消息(流式) |
||||
|
func sendMessageStream(messages: [[String: Any]], parameters: ChatParameters, handler: ChatStreamHandler) async { |
||||
|
// 取消之前的任务 |
||||
|
currentTask?.cancel() |
||||
|
|
||||
|
currentTask = Task { |
||||
|
do { |
||||
|
guard let openAI = openAI else { |
||||
|
handler.onError(ChatApiError.notInitialized) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 转换消息格式(与非流式相同) |
||||
|
let chatMessages = try messages.map { dict -> Chat in |
||||
|
guard let role = dict["role"] as? String, |
||||
|
let content = dict["content"] else { |
||||
|
throw ChatApiError.invalidMessage |
||||
|
} |
||||
|
|
||||
|
let chatRole = try parseChatRole(role) |
||||
|
|
||||
|
if let contentString = content as? String { |
||||
|
return Chat(role: chatRole, content: contentString) |
||||
|
} else if let contentArray = content as? [[String: Any]] { |
||||
|
var contentItems: [ChatContent] = [] |
||||
|
for item in contentArray { |
||||
|
if let type = item["type"] as? String { |
||||
|
switch type { |
||||
|
case "text": |
||||
|
if let text = item["text"] as? String { |
||||
|
contentItems.append(.text(text)) |
||||
|
} |
||||
|
case "image_url": |
||||
|
if let imageUrl = item["image_url"] as? [String: Any], |
||||
|
let url = imageUrl["url"] as? String { |
||||
|
contentItems.append(.image(.init(url: url))) |
||||
|
} |
||||
|
default: |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return Chat(role: chatRole, content: contentItems) |
||||
|
} else { |
||||
|
throw ChatApiError.invalidMessage |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 构建查询 |
||||
|
var query = ChatQuery( |
||||
|
messages: chatMessages, |
||||
|
model: currentModel, |
||||
|
temperature: parameters.temperature, |
||||
|
topP: parameters.topP, |
||||
|
n: parameters.n, |
||||
|
stop: parameters.stop ?? [], |
||||
|
maxTokens: parameters.maxTokens, |
||||
|
presencePenalty: parameters.presencePenalty, |
||||
|
frequencyPenalty: parameters.frequencyPenalty |
||||
|
) |
||||
|
|
||||
|
// 添加函数 |
||||
|
if let functions = parameters.functions { |
||||
|
query.tools = try functions.map { dict in |
||||
|
guard let name = dict["name"] as? String, |
||||
|
let description = dict["description"] as? String, |
||||
|
let parameters = dict["parameters"] as? [String: Any] else { |
||||
|
throw ChatApiError.invalidFunction |
||||
|
} |
||||
|
|
||||
|
return ChatQuery.ChatCompletionToolParam( |
||||
|
function: .init( |
||||
|
name: name, |
||||
|
description: description, |
||||
|
parameters: parameters |
||||
|
) |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 创建流式请求 |
||||
|
let stream = openAI.chatsStream(query: query) |
||||
|
|
||||
|
// 处理流式响应 |
||||
|
for try await result in stream { |
||||
|
// 检查任务是否被取消 |
||||
|
if Task.isCancelled { |
||||
|
break |
||||
|
} |
||||
|
|
||||
|
// 处理增量内容 |
||||
|
if let choice = result.choices.first { |
||||
|
// 处理文本内容 |
||||
|
if let content = choice.delta?.content { |
||||
|
handler.onToken(content) |
||||
|
} |
||||
|
|
||||
|
// 处理函数调用 |
||||
|
if let toolCalls = choice.delta?.toolCalls, |
||||
|
let toolCall = toolCalls.first { |
||||
|
var functionCall: [String: Any] = [:] |
||||
|
|
||||
|
if let name = toolCall.function?.name { |
||||
|
functionCall["name"] = name |
||||
|
} |
||||
|
if let arguments = toolCall.function?.arguments { |
||||
|
functionCall["arguments"] = arguments |
||||
|
} |
||||
|
if let id = toolCall.id { |
||||
|
functionCall["id"] = id |
||||
|
} |
||||
|
|
||||
|
if !functionCall.isEmpty { |
||||
|
handler.onFunctionCall(functionCall) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 检查是否完成 |
||||
|
if choice.finishReason != nil { |
||||
|
handler.onComplete() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} catch { |
||||
|
handler.onError(error) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 取消流式请求 |
||||
|
func cancelStream() { |
||||
|
currentTask?.cancel() |
||||
|
currentTask = nil |
||||
|
} |
||||
|
|
||||
|
// MARK: - 模型管理 |
||||
|
|
||||
|
/// 获取可用的模型列表 |
||||
|
func getAvailableModels() async throws -> [String] { |
||||
|
guard let openAI = openAI else { |
||||
|
throw ChatApiError.notInitialized |
||||
|
} |
||||
|
|
||||
|
let models = try await openAI.models() |
||||
|
return models.data.map { $0.id } |
||||
|
} |
||||
|
|
||||
|
/// 设置当前模型 |
||||
|
func setModel(_ modelString: String) { |
||||
|
// 尝试解析标准模型 |
||||
|
if let model = Model(rawValue: modelString) { |
||||
|
currentModel = model |
||||
|
} else { |
||||
|
// 使用自定义模型 |
||||
|
currentModel = Model.custom(modelString) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 估算token数量(简单实现) |
||||
|
func estimateTokens(text: String) -> Int { |
||||
|
// 粗略估算:1个token约等于4个字符或0.75个单词 |
||||
|
// 中文通常1个字符算1个token |
||||
|
let words = text.split(separator: " ").count |
||||
|
let chars = text.count |
||||
|
|
||||
|
// 检测是否包含中文 |
||||
|
let hasChineseCharacters = text.range(of: "\\p{Han}", options: .regularExpression) != nil |
||||
|
|
||||
|
if hasChineseCharacters { |
||||
|
// 中文为主的文本,按字符数估算 |
||||
|
return chars |
||||
|
} else { |
||||
|
// 英文为主的文本,按单词数估算 |
||||
|
return Int(Double(words) * 1.3) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - 辅助方法 |
||||
|
|
||||
|
private func parseChatRole(_ role: String) throws -> Chat.Role { |
||||
|
switch role.lowercased() { |
||||
|
case "system": |
||||
|
return .system |
||||
|
case "user": |
||||
|
return .user |
||||
|
case "assistant": |
||||
|
return .assistant |
||||
|
case "function": |
||||
|
return .function(name: nil) |
||||
|
default: |
||||
|
throw ChatApiError.invalidRole(role) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - 错误定义 |
||||
|
enum ChatApiError: LocalizedError { |
||||
|
case notInitialized |
||||
|
case invalidMessage |
||||
|
case invalidFunction |
||||
|
case invalidRole(String) |
||||
|
|
||||
|
var errorDescription: String? { |
||||
|
switch self { |
||||
|
case .notInitialized: |
||||
|
return "OpenAI客户端未初始化" |
||||
|
case .invalidMessage: |
||||
|
return "无效的消息格式" |
||||
|
case .invalidFunction: |
||||
|
return "无效的函数定义" |
||||
|
case .invalidRole(let role): |
||||
|
return "无效的角色类型: \(role)" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,358 @@ |
|||||
|
import 'dart:async'; |
||||
|
import 'dart:convert'; |
||||
|
|
||||
|
import 'package:flutter/services.dart'; |
||||
|
|
||||
|
/// 聊天API异常 |
||||
|
class ChatApiException implements Exception { |
||||
|
final String message; |
||||
|
|
||||
|
ChatApiException(this.message); |
||||
|
|
||||
|
@override |
||||
|
String toString() => 'ChatApiException: $message'; |
||||
|
} |
||||
|
|
||||
|
/// 聊天API事件类型 |
||||
|
enum ChatApiEventType { |
||||
|
/// 收到新的token |
||||
|
token, |
||||
|
/// 对话完成 |
||||
|
complete, |
||||
|
/// 发生错误 |
||||
|
error, |
||||
|
/// 函数调用 |
||||
|
functionCall, |
||||
|
} |
||||
|
|
||||
|
/// 聊天API事件 |
||||
|
class ChatApiEvent { |
||||
|
final ChatApiEventType type; |
||||
|
final dynamic content; |
||||
|
final Map<String, dynamic>? meta; |
||||
|
|
||||
|
ChatApiEvent({ |
||||
|
required this.type, |
||||
|
this.content, |
||||
|
this.meta, |
||||
|
}); |
||||
|
|
||||
|
factory ChatApiEvent.fromMap(Map<String, dynamic> map) { |
||||
|
final typeStr = map['type'] as String; |
||||
|
final content = map['content']; |
||||
|
final meta = map['meta'] as Map<String, dynamic>?; |
||||
|
|
||||
|
return ChatApiEvent( |
||||
|
type: _typeFromString(typeStr), |
||||
|
content: content, |
||||
|
meta: meta, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
static ChatApiEventType _typeFromString(String typeStr) { |
||||
|
switch (typeStr) { |
||||
|
case 'token': |
||||
|
return ChatApiEventType.token; |
||||
|
case 'complete': |
||||
|
return ChatApiEventType.complete; |
||||
|
case 'error': |
||||
|
return ChatApiEventType.error; |
||||
|
case 'functionCall': |
||||
|
return ChatApiEventType.functionCall; |
||||
|
default: |
||||
|
throw ArgumentError('未知的事件类型: $typeStr'); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 消息角色 |
||||
|
enum MessageRole { |
||||
|
system, |
||||
|
user, |
||||
|
assistant, |
||||
|
function, |
||||
|
} |
||||
|
|
||||
|
extension MessageRoleExtension on MessageRole { |
||||
|
String get value { |
||||
|
switch (this) { |
||||
|
case MessageRole.system: |
||||
|
return 'system'; |
||||
|
case MessageRole.user: |
||||
|
return 'user'; |
||||
|
case MessageRole.assistant: |
||||
|
return 'assistant'; |
||||
|
case MessageRole.function: |
||||
|
return 'function'; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 聊天消息 |
||||
|
class ChatMessage { |
||||
|
final MessageRole role; |
||||
|
final dynamic content; |
||||
|
final String? name; |
||||
|
final Map<String, dynamic>? functionCall; |
||||
|
|
||||
|
ChatMessage({ |
||||
|
required this.role, |
||||
|
required this.content, |
||||
|
this.name, |
||||
|
this.functionCall, |
||||
|
}); |
||||
|
|
||||
|
Map<String, dynamic> toMap() { |
||||
|
final map = { |
||||
|
'role': role.value, |
||||
|
'content': content, |
||||
|
}; |
||||
|
if (name != null) { |
||||
|
map['name'] = name!; |
||||
|
} |
||||
|
if (functionCall != null) { |
||||
|
map['function_call'] = functionCall!; |
||||
|
} |
||||
|
return map; |
||||
|
} |
||||
|
|
||||
|
factory ChatMessage.fromMap(Map<String, dynamic> map) { |
||||
|
return ChatMessage( |
||||
|
role: _roleFromString(map['role'] as String), |
||||
|
content: map['content'], |
||||
|
name: map['name'] as String?, |
||||
|
functionCall: map['function_call'] as Map<String, dynamic>?, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
static MessageRole _roleFromString(String roleStr) { |
||||
|
switch (roleStr) { |
||||
|
case 'system': |
||||
|
return MessageRole.system; |
||||
|
case 'user': |
||||
|
return MessageRole.user; |
||||
|
case 'assistant': |
||||
|
return MessageRole.assistant; |
||||
|
case 'function': |
||||
|
return MessageRole.function; |
||||
|
default: |
||||
|
throw ArgumentError('未知的角色类型: $roleStr'); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 聊天API插件 |
||||
|
class ChatApi { |
||||
|
static const MethodChannel _channel = |
||||
|
MethodChannel('com.yunqiinnovation.chat_api/methods'); |
||||
|
static const EventChannel _eventChannel = |
||||
|
EventChannel('com.yunqiinnovation.chat_api/events'); |
||||
|
|
||||
|
/// 事件流控制器 |
||||
|
StreamController<ChatApiEvent>? _eventStreamController; |
||||
|
|
||||
|
/// 事件流 |
||||
|
Stream<ChatApiEvent>? _eventStream; |
||||
|
|
||||
|
/// 获取事件流 |
||||
|
Stream<ChatApiEvent> get eventStream { |
||||
|
if (_eventStream == null) { |
||||
|
_eventStreamController = StreamController<ChatApiEvent>.broadcast(); |
||||
|
_eventStream = _eventStreamController!.stream; |
||||
|
|
||||
|
// 监听原生事件 |
||||
|
_eventChannel.receiveBroadcastStream().listen( |
||||
|
(dynamic event) { |
||||
|
if (event is Map<dynamic, dynamic>) { |
||||
|
final eventMap = Map<String, dynamic>.from(event); |
||||
|
final chatApiEvent = ChatApiEvent.fromMap(eventMap); |
||||
|
_eventStreamController!.add(chatApiEvent); |
||||
|
} |
||||
|
}, |
||||
|
onError: (error) { |
||||
|
_eventStreamController!.addError(ChatApiException('事件流错误: $error')); |
||||
|
}, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return _eventStream!; |
||||
|
} |
||||
|
|
||||
|
/// 初始化聊天API服务 |
||||
|
/// |
||||
|
/// [apiKey] OpenAI API密钥 |
||||
|
/// [organization] 可选,组织ID |
||||
|
/// [model] 可选,使用的模型,默认为gpt-3.5-turbo |
||||
|
/// [baseUrl] 可选,自定义API基础URL |
||||
|
Future<bool> initialize({ |
||||
|
required String apiKey, |
||||
|
String? organization, |
||||
|
String model = 'gpt-3.5-turbo', |
||||
|
String? baseUrl, |
||||
|
}) async { |
||||
|
try { |
||||
|
final result = await _channel.invokeMethod<bool>( |
||||
|
'initialize', |
||||
|
{ |
||||
|
'apiKey': apiKey, |
||||
|
'organization': organization, |
||||
|
'model': model, |
||||
|
'baseUrl': baseUrl, |
||||
|
}, |
||||
|
); |
||||
|
|
||||
|
return result ?? false; |
||||
|
} catch (e) { |
||||
|
throw ChatApiException('初始化失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 发送聊天消息 |
||||
|
/// |
||||
|
/// [messages] 消息列表 |
||||
|
/// [temperature] 温度参数,控制输出的随机性 |
||||
|
/// [maxTokens] 最大token数 |
||||
|
/// [topP] 核心采样参数 |
||||
|
/// [n] 生成的回复数量 |
||||
|
/// [stop] 停止序列 |
||||
|
/// [presencePenalty] 存在惩罚 |
||||
|
/// [frequencyPenalty] 频率惩罚 |
||||
|
/// [functions] 可用的函数列表 |
||||
|
Future<String> sendMessage({ |
||||
|
required List<ChatMessage> messages, |
||||
|
double temperature = 0.7, |
||||
|
int? maxTokens, |
||||
|
double topP = 1.0, |
||||
|
int n = 1, |
||||
|
List<String>? stop, |
||||
|
double presencePenalty = 0.0, |
||||
|
double frequencyPenalty = 0.0, |
||||
|
List<Map<String, dynamic>>? functions, |
||||
|
}) async { |
||||
|
try { |
||||
|
final messagesMap = messages.map((m) => m.toMap()).toList(); |
||||
|
|
||||
|
final result = await _channel.invokeMethod<String>( |
||||
|
'sendMessage', |
||||
|
{ |
||||
|
'messages': messagesMap, |
||||
|
'temperature': temperature, |
||||
|
'maxTokens': maxTokens, |
||||
|
'topP': topP, |
||||
|
'n': n, |
||||
|
'stop': stop, |
||||
|
'presencePenalty': presencePenalty, |
||||
|
'frequencyPenalty': frequencyPenalty, |
||||
|
'functions': functions, |
||||
|
}, |
||||
|
); |
||||
|
|
||||
|
if (result == null) { |
||||
|
throw ChatApiException('发送消息失败: 结果为空'); |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} catch (e) { |
||||
|
throw ChatApiException('发送消息失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 发送聊天消息(流式输出) |
||||
|
/// |
||||
|
/// [messages] 消息列表 |
||||
|
/// [temperature] 温度参数,控制输出的随机性 |
||||
|
/// [maxTokens] 最大token数 |
||||
|
/// [topP] 核心采样参数 |
||||
|
/// [n] 生成的回复数量 |
||||
|
/// [stop] 停止序列 |
||||
|
/// [presencePenalty] 存在惩罚 |
||||
|
/// [frequencyPenalty] 频率惩罚 |
||||
|
/// [functions] 可用的函数列表 |
||||
|
Future<bool> sendMessageStream({ |
||||
|
required List<ChatMessage> messages, |
||||
|
double temperature = 0.7, |
||||
|
int? maxTokens, |
||||
|
double topP = 1.0, |
||||
|
int n = 1, |
||||
|
List<String>? stop, |
||||
|
double presencePenalty = 0.0, |
||||
|
double frequencyPenalty = 0.0, |
||||
|
List<Map<String, dynamic>>? functions, |
||||
|
}) async { |
||||
|
try { |
||||
|
final messagesMap = messages.map((m) => m.toMap()).toList(); |
||||
|
|
||||
|
final result = await _channel.invokeMethod<bool>( |
||||
|
'sendMessageStream', |
||||
|
{ |
||||
|
'messages': messagesMap, |
||||
|
'temperature': temperature, |
||||
|
'maxTokens': maxTokens, |
||||
|
'topP': topP, |
||||
|
'n': n, |
||||
|
'stop': stop, |
||||
|
'presencePenalty': presencePenalty, |
||||
|
'frequencyPenalty': frequencyPenalty, |
||||
|
'functions': functions, |
||||
|
}, |
||||
|
); |
||||
|
|
||||
|
return result ?? false; |
||||
|
} catch (e) { |
||||
|
throw ChatApiException('发送流式消息失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 取消当前的流式请求 |
||||
|
Future<bool> cancelStream() async { |
||||
|
try { |
||||
|
final result = await _channel.invokeMethod<bool>('cancelStream'); |
||||
|
return result ?? false; |
||||
|
} catch (e) { |
||||
|
throw ChatApiException('取消流式请求失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 获取可用的模型列表 |
||||
|
Future<List<String>> getAvailableModels() async { |
||||
|
try { |
||||
|
final result = await _channel.invokeMethod<List<dynamic>>('getAvailableModels'); |
||||
|
return result?.cast<String>() ?? []; |
||||
|
} catch (e) { |
||||
|
throw ChatApiException('获取模型列表失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 设置默认模型 |
||||
|
Future<bool> setModel(String model) async { |
||||
|
try { |
||||
|
final result = await _channel.invokeMethod<bool>( |
||||
|
'setModel', |
||||
|
{'model': model}, |
||||
|
); |
||||
|
return result ?? false; |
||||
|
} catch (e) { |
||||
|
throw ChatApiException('设置模型失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 估算token数量 |
||||
|
Future<int> countTokens(String text) async { |
||||
|
try { |
||||
|
final result = await _channel.invokeMethod<int>( |
||||
|
'countTokens', |
||||
|
{'text': text}, |
||||
|
); |
||||
|
return result ?? 0; |
||||
|
} catch (e) { |
||||
|
throw ChatApiException('计算token失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 释放资源 |
||||
|
Future<void> dispose() async { |
||||
|
await _eventStreamController?.close(); |
||||
|
_eventStreamController = null; |
||||
|
_eventStream = null; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
name: chat_api |
||||
|
description: 基于MacPaw OpenAI Swift库的聊天API插件,支持流式输出和函数调用。 |
||||
|
version: 0.0.1 |
||||
|
homepage: https://github.com/yunqiinnovation/deep_voice |
||||
|
|
||||
|
environment: |
||||
|
sdk: ">=2.17.0 <4.0.0" |
||||
|
flutter: ">=2.5.0" |
||||
|
|
||||
|
dependencies: |
||||
|
flutter: |
||||
|
sdk: flutter |
||||
|
|
||||
|
dev_dependencies: |
||||
|
flutter_test: |
||||
|
sdk: flutter |
||||
|
flutter_lints: ^2.0.0 |
||||
|
|
||||
|
flutter: |
||||
|
plugin: |
||||
|
platforms: |
||||
|
ios: |
||||
|
pluginClass: ChatApiPlugin |
||||
|
sharedDarwinSource: true |
||||
|
# 明确声明支持SPM |
||||
|
dartPluginClass: ChatApi |
||||
|
android: |
||||
|
package: com.yunqiinnovation.chat_api |
||||
|
pluginClass: ChatApiPlugin |
||||
Loading…
Reference in new issue