Browse Source

add

newdev_shunjiawei
wolfplus 1 year ago
parent
commit
4992fd03d9
  1. 37
      ios/Runner.xcodeproj/project.pbxproj
  2. 7
      ios/Runner/Info.plist
  3. 1
      local_plugins/agent_service/ios/Classes/AgentServiceImpl.swift
  4. 5
      local_plugins/ble_service/ios/Classes/BleService.swift
  5. 19
      local_plugins/ble_service/ios/Classes/SwiftOpusAudioProcessor.swift
  6. 84
      local_plugins/chat_api/IOS_INTEGRATION_LEGACY.md
  7. 0
      local_plugins/chat_api/LICENSE
  8. 239
      local_plugins/chat_api/README.md
  9. 108
      local_plugins/chat_api/SPM_MIGRATION.md
  10. 64
      local_plugins/chat_api/android/build.gradle
  11. 3
      local_plugins/chat_api/android/src/main/AndroidManifest.xml
  12. 71
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiPlugin.kt
  13. 266
      local_plugins/chat_api/example/lib/main.dart
  14. 24
      local_plugins/chat_api/example/pubspec.yaml
  15. 44
      local_plugins/chat_api/ios/chat_api.podspec
  16. 32
      local_plugins/chat_api/ios/chat_api/Package.swift
  17. 316
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiPlugin.swift
  18. 363
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatService.swift
  19. 358
      local_plugins/chat_api/lib/chat_api.dart
  20. 29
      local_plugins/chat_api/pubspec.yaml

37
ios/Runner.xcodeproj/project.pbxproj

@ -70,6 +70,24 @@
D2A873E56435FC1342AE0A85 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
D7BFB5FE33B0270C30A1DFAA /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
E9725ECE141452716001447D /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
0AD9449B2DE6F89600557D21 /* Remove Bitcode from YbridOpus */ = {
isa = PBXFileReference;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Remove Bitcode from YbridOpus";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# 移除YbridOpus框架中的bitcode\necho \"检查并移除YbridOpus框架中的bitcode...\"\n\nFRAMEWORK_PATHS=(\n \"${PODS_ROOT}/YbridOpus/YbridOpus.xcframework/ios-arm64_armv7/YbridOpus.framework/YbridOpus\"\n \"${PODS_ROOT}/YbridOpus/YbridOpus.xcframework/ios-i386_x86_64-simulator/YbridOpus.framework/YbridOpus\"\n \"${PODS_ROOT}/YbridOpus/YbridOpus.xcframework/ios-arm64_x86_64-maccatalyst/YbridOpus.framework/YbridOpus\"\n \"${PODS_ROOT}/YbridOpus/YbridOpus.xcframework/macos-arm64_x86_64/YbridOpus.framework/YbridOpus\"\n)\n\nfor framework_path in \"${FRAMEWORK_PATHS[@]}\"; do\n if [ -f \"$framework_path\" ]; then\n if otool -l \"$framework_path\" | grep -q __LLVM; then\n echo \"移除bitcode: $framework_path\"\n # 直接覆盖,不创建备份\n xcrun bitcode_strip -r \"$framework_path\" -o \"$framework_path.tmp\"\n mv \"$framework_path.tmp\" \"$framework_path\"\n fi\n fi\ndone\n\n# 清理任何可能的备份文件\nfind \"${PODS_ROOT}\" -name \"*.backup\" -type f -delete 2>/dev/null || true\nfind \"${BUILT_PRODUCTS_DIR}\" -name \"*.backup\" -type f -delete 2>/dev/null || true\n\necho \"bitcode移除和清理完成!\"";
};
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -206,6 +224,7 @@
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
0AD9449B2DE6F89600557D21 /* Remove Bitcode from YbridOpus */,
CC018A1832E51ED5B994765E /* [CP] Embed Pods Frameworks */,
A196696487421EBA40281C38 /* [CP] Copy Pods Resources */,
);
@ -302,6 +321,24 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
0AD9449B2DE6F89600557D21 /* Remove Bitcode from YbridOpus */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Remove Bitcode from YbridOpus";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# 移除YbridOpus框架中的bitcode\necho \"检查并移除YbridOpus框架中的bitcode...\"\n\nFRAMEWORK_PATHS=(\n \"${PODS_ROOT}/YbridOpus/YbridOpus.xcframework/ios-arm64_armv7/YbridOpus.framework/YbridOpus\"\n \"${PODS_ROOT}/YbridOpus/YbridOpus.xcframework/ios-i386_x86_64-simulator/YbridOpus.framework/YbridOpus\"\n \"${PODS_ROOT}/YbridOpus/YbridOpus.xcframework/ios-arm64_x86_64-maccatalyst/YbridOpus.framework/YbridOpus\"\n \"${PODS_ROOT}/YbridOpus/YbridOpus.xcframework/macos-arm64_x86_64/YbridOpus.framework/YbridOpus\"\n)\n\nfor framework_path in \"${FRAMEWORK_PATHS[@]}\"; do\n if [ -f \"$framework_path\" ]; then\n if otool -l \"$framework_path\" | grep -q __LLVM; then\n echo \"移除bitcode: $framework_path\"\n # 直接覆盖,不创建备份\n xcrun bitcode_strip -r \"$framework_path\" -o \"$framework_path.tmp\"\n mv \"$framework_path.tmp\" \"$framework_path\"\n fi\n fi\ndone\n\n# 清理任何可能的备份文件\nfind \"${PODS_ROOT}\" -name \"*.backup\" -type f -delete 2>/dev/null || true\nfind \"${BUILT_PRODUCTS_DIR}\" -name \"*.backup\" -type f -delete 2>/dev/null || true\n\necho \"bitcode移除和清理完成!\"";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;

7
ios/Runner/Info.plist

@ -107,13 +107,18 @@
<string>需要位置权限用于搜索附近的蓝牙设备</string>
<key>NSMicrophoneUsageDescription</key>
<string>需要麦克风权限用于语音识别和录音功能</string>
<key>NSCalendarsUsageDescription</key>
<string>需要日历权限用于安排和管理您的音频会议和录音任务</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>需要相册权限用于保存录音文件和导入音频文件</string>
<key>NSCameraUsageDescription</key>
<string>需要相机权限用于扫描二维码和视频通话功能</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
<string>audio</string>
<string>processing</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>

1
local_plugins/agent_service/ios/Classes/AgentServiceImpl.swift

@ -148,7 +148,6 @@ class AgentServiceImpl: NSObject {
} else {
initSystemPrompt()
}
initSystemPrompt()
if let azureSpeechKey = config["azureSpeechKey"] as? String {
self.azureSpeechKey = azureSpeechKey
} else {

5
local_plugins/ble_service/ios/Classes/BleService.swift

@ -529,6 +529,11 @@ public class BleService: NSObject {
/// 处理收到的音频数据
private func processAudioData(_ data: Data) {
// 简单过滤:只处理合理大小的音频包
guard data.count > 0 && data.count <= 1275 else {
return
}
// 使用Opus处理器处理音频数据
opusProcessor?.processAudioData(data)
}

19
local_plugins/ble_service/ios/Classes/SwiftOpusAudioProcessor.swift

@ -85,16 +85,20 @@ class SwiftOpusAudioProcessor: NSObject {
return
}
// 过滤明显异常的数据包
guard data.count <= 1275 else {
return
}
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
self?.decodeOpusPacket(data)
}
}
// 使用简单稳定的 Opus 解码方法
// 简化的 Opus 解码方法,重点是容错处理
private func decodeOpusPacket(_ packet: Data) {
guard let decoder = decoder else { return }
// 使用简单的直接解码方式
let maxSamples = Int(maxFrameSize)
var pcmBuffer = [opus_int16](repeating: 0, count: maxSamples * Int(channels))
@ -110,7 +114,10 @@ class SwiftOpusAudioProcessor: NSObject {
)
}
guard decodedSamples > 0 else {
// 简单的错误处理:失败时重新初始化解码器
if decodedSamples <= 0 {
print("Opus解码失败,重新初始化解码器")
resetDecoder()
return
}
@ -124,6 +131,12 @@ class SwiftOpusAudioProcessor: NSObject {
}
}
// 重置解码器的简单方法
private func resetDecoder() {
destroyOpusDecoder()
_ = initializeOpusDecoder()
}
private func processPCMData(_ pcmData: [opus_int16]) {
// 转换为 Data
let data = Data(bytes: pcmData, count: pcmData.count * MemoryLayout<opus_int16>.size)

84
local_plugins/chat_api/IOS_INTEGRATION_LEGACY.md

@ -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
local_plugins/chat_api/LICENSE

239
local_plugins/chat_api/README.md

@ -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

108
local_plugins/chat_api/SPM_MIGRATION.md

@ -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 库的更新

64
local_plugins/chat_api/android/build.gradle

@ -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
}
}
}
}

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

@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yunqiinnovation.chat_api">
</manifest>

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

@ -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
}
}

266
local_plugins/chat_api/example/lib/main.dart

@ -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();
}
}

24
local_plugins/chat_api/example/pubspec.yaml

@ -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

44
local_plugins/chat_api/ios/chat_api.podspec

@ -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

32
local_plugins/chat_api/ios/chat_api/Package.swift

@ -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: [
// 如果有资源文件,在这里声明
]
)
]
)

316
local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiPlugin.swift

@ -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
])
}
}
}

363
local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatService.swift

@ -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)"
}
}
}

358
local_plugins/chat_api/lib/chat_api.dart

@ -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;
}
}

29
local_plugins/chat_api/pubspec.yaml

@ -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…
Cancel
Save