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